1529 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
MacRimi
d987d639ab Update version.txt 2025-08-20 21:38:45 +02:00
MacRimi
e7e180e468 Update CHANGELOG.md 2025-08-20 21:38:20 +02:00
MacRimi
76770f82cd Update system_utils.sh 2025-08-20 21:22:49 +02:00
MacRimi
4079d4fd7c Update customizable_post_install.sh 2025-08-20 21:21:13 +02:00
MacRimi
ac48178369 Update lxc-privileged-to-unprivileged.sh 2025-08-20 21:10:05 +02:00
MacRimi
c2e9f038ee Update lxc-privileged-to-unprivileged.sh 2025-08-20 21:04:50 +02:00
MacRimi
70220d9829 Update lxc-privileged-to-unprivileged.sh 2025-08-20 21:03:57 +02:00
MacRimi
b9a1f378ec Update lxc_menu.sh 2025-08-20 20:59:58 +02:00
MacRimi
f6bc090a98 Update lxc_menu.sh 2025-08-20 20:55:43 +02:00
MacRimi
be519f3932 Create lxc_menu.sh 2025-08-20 20:46:51 +02:00
MacRimi
0a46f77555 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-08-20 20:43:56 +02:00
MacRimi
0e6cc0c7e5 Create lxc-privileged-to-unprivileged.sh 2025-08-20 20:43:54 +02:00
ProxMenuxBot
11cd425162 Update helpers_cache.json 2025-08-20 18:19:44 +00:00
MacRimi
aa269688d6 Update utilities_menu.sh 2025-08-20 19:51:54 +02:00
MacRimi
4c9e94768e Update upgrade_pve8_to_pve9.sh 2025-08-20 19:48:25 +02:00
MacRimi
581157fa82 Update upgrade_pve8_to_pve9.sh 2025-08-20 19:46:30 +02:00
MacRimi
e748e479cc Update utilities_menu.sh 2025-08-20 19:44:16 +02:00
MacRimi
5c9e4eea1e Update upgrade_pve8_to_pve9.sh 2025-08-20 19:41:09 +02:00
MacRimi
0c1189b233 Update pve8to9_check.sh 2025-08-20 19:37:14 +02:00
MacRimi
5ec9b82b4a Update upgrade_pve8_to_pve9.sh 2025-08-20 19:22:22 +02:00
MacRimi
c84ec533da Create pve8to9_check.sh 2025-08-20 19:22:12 +02:00
MacRimi
fb80c6ad7a Update upgrade_pve8_to_pve9.sh 2025-08-20 19:09:52 +02:00
ProxMenuxBot
2e3bfff6a4 Update helpers_cache.json 2025-08-19 18:18:30 +00:00
ProxMenuxBot
e96ce30891 Update helpers_cache.json 2025-08-19 12:26:48 +00:00
MacRimi
10de5b2e5f Update customizable_post_install.sh 2025-08-19 14:15:34 +02:00
MacRimi
1966081239 Update auto_post_install.sh 2025-08-19 14:13:56 +02:00
MacRimi
b48d806d53 Update system_utils.sh 2025-08-19 13:42:42 +02:00
MacRimi
97784d74e7 Update customizable_post_install.sh 2025-08-19 13:38:59 +02:00
ProxMenuxBot
c42e92b07d Update helpers_cache.json 2025-08-19 01:03:35 +00:00
MacRimi
2c52943b54 Update customizable_post_install.sh 2025-08-18 23:36:30 +02:00
MacRimi
4ccb1902cb Update customizable_post_install.sh 2025-08-18 23:30:59 +02:00
MacRimi
349b0572cd Update customizable_post_install.sh 2025-08-18 23:23:35 +02:00
MacRimi
87fae8a9eb Update customizable_post_install.sh 2025-08-18 23:18:26 +02:00
ProxMenuxBot
a77a097f47 Update helpers_cache.json 2025-08-18 18:20:28 +00:00
MacRimi
a84d81143e Update auto_post_install.sh 2025-08-18 15:10:23 +02:00
MacRimi
d9cee50ef3 Update auto_post_install.sh 2025-08-18 15:08:56 +02:00
MacRimi
0fc414e5e9 Update customizable_post_install.sh 2025-08-18 15:08:25 +02:00
ProxMenuxBot
e18f20ce4c Update helpers_cache.json 2025-08-18 12:29:04 +00:00
MacRimi
c12af4060c Update system_utils.sh 2025-08-18 14:20:47 +02:00
MacRimi
9992ea0dee Update customizable_post_install.sh 2025-08-18 14:15:19 +02:00
MacRimi
b8310f1c5d Update main_menu.sh 2025-08-18 09:37:06 +02:00
MacRimi
78f66af702 Update auto_post_install.sh 2025-08-17 15:16:25 +02:00
MacRimi
4d7564094e Update auto_post_install.sh 2025-08-17 15:14:46 +02:00
MacRimi
370f4694d1 Update customizable_post_install.sh 2025-08-17 15:14:20 +02:00
MacRimi
fc7c740691 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-08-17 15:05:42 +02:00
MacRimi
8e1f955519 Update auto_post_install.sh 2025-08-17 15:04:27 +02:00
MacRimi
aa3d16d981 Update auto_post_install.sh 2025-08-17 15:02:42 +02:00
MacRimi
5447f0e4df Update auto_post_install.sh 2025-08-17 15:00:06 +02:00
MacRimi
97294df208 Update customizable_post_install.sh 2025-08-17 14:21:37 +02:00
MacRimi
c6f53629da Update auto_post_install.sh 2025-08-17 14:20:58 +02:00
MacRimi
fcba907658 Update upgrade_pve8_to_pve9.sh 2025-08-17 13:24:32 +02:00
MacRimi
f481df7b8d Update configure_igpu_lxc.sh 2025-08-17 13:08:39 +02:00
MacRimi
81079a35d9 Update uninstall-tools.sh 2025-08-17 11:23:13 +02:00
MacRimi
bda344c382 Update uninstall-tools.sh 2025-08-17 11:01:21 +02:00
MacRimi
c4ebc396af Update customizable_post_install.sh 2025-08-17 10:36:03 +02:00
MacRimi
4cdbf1231b Update customizable_post_install.sh 2025-08-16 18:32:19 +02:00
MacRimi
92db58a9f6 Update auto_post_install.sh 2025-08-16 18:31:10 +02:00
MacRimi
5f07f47308 Update configure_igpu_lxc.sh 2025-08-16 18:12:14 +02:00
MacRimi
2132ae79a6 Update configure_igpu_lxc.sh 2025-08-16 17:34:34 +02:00
MacRimi
bda7834a4f Update disk-passthrough_ct.sh 2025-08-16 17:22:33 +02:00
MacRimi
7693f313c4 Update disk-passthrough_ct.sh 2025-08-16 17:13:45 +02:00
MacRimi
d2200a64e0 Update upgrade_pve8_to_pve9.sh 2025-08-16 17:04:59 +02:00
MacRimi
a5c46ab837 Update upgrade_pve8_to_pve9.sh 2025-08-16 16:43:44 +02:00
MacRimi
a389282e23 Update upgrade_pve8_to_pve9.sh 2025-08-16 11:54:11 +02:00
MacRimi
1f90b5b739 Update upgrade_pve8_to_pve9.sh 2025-08-16 11:46:09 +02:00
MacRimi
2c59500046 Update configure_igpu_lxc.sh 2025-08-16 11:15:41 +02:00
MacRimi
a59a056e12 Update configure_igpu_lxc.sh 2025-08-16 11:11:43 +02:00
ProxMenuxBot
235364013b Update helpers_cache.json 2025-08-16 01:03:53 +00:00
MacRimi
1049ac6eac Update upgrade_pve8_to_pve9.sh 2025-08-15 12:15:42 +02:00
MacRimi
04dc7af25c Create remove-banner-pve9.sh 2025-08-15 12:06:54 +02:00
MacRimi
f62ea3ad04 update remove-banner-pve9.sh 2025-08-15 12:05:24 +02:00
MacRimi
14c75f2cd9 Update proxmox-upgrade-pve8-to-pve9-manual-guide.sh 2025-08-15 11:15:17 +02:00
MacRimi
9ae68b9653 Update customizable_post_install.sh 2025-08-15 10:22:57 +02:00
MacRimi
b7ab4c4568 Update customizable_post_install.sh 2025-08-15 10:13:44 +02:00
MacRimi
7d0b3a0c87 Update system_utils.sh 2025-08-15 09:55:22 +02:00
MacRimi
0d38f7f290 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-08-15 09:54:02 +02:00
MacRimi
12e5ef4231 Update customizable_post_install.sh 2025-08-15 09:54:00 +02:00
ProxMenuxBot
f3aa1f7414 Update helpers_cache.json 2025-08-15 01:06:47 +00:00
MacRimi
f2eaec6e02 Update upgrade_pve8_to_pve9.sh 2025-08-14 20:08:22 +02:00
MacRimi
0654a3ed55 Update upgrade_pve8_to_pve9.sh 2025-08-14 20:03:47 +02:00
MacRimi
9a27138d96 Update proxmox-upgrade-pve8-to-pve9-manual-guide.sh 2025-08-14 19:46:13 +02:00
MacRimi
b3c9f71c02 Update proxmox-upgrade-pve8-to-pve9-manual-guide.sh 2025-08-14 19:41:22 +02:00
MacRimi
e9c9b957db Delete upgrade_pve8_topve9.sh 2025-08-14 19:39:36 +02:00
MacRimi
29cdf6fa48 Create upgrade_pve8_to_pve9.sh 2025-08-14 19:39:12 +02:00
MacRimi
8466a8e21e Update upgrade_pve8_topve9.sh 2025-08-14 19:37:15 +02:00
MacRimi
1523b6b8a8 Update proxmox-upgrade-pve8-to-pve9-manual-guide.sh 2025-08-14 19:34:42 +02:00
MacRimi
33205e1008 Create upgrade_pve8_topve9.sh 2025-08-14 19:24:29 +02:00
MacRimi
537af385f8 Update main menu PVE 9 2025-08-14 17:58:59 +02:00
MacRimi
7259b0a850 Update install_proxmenux.sh 2025-08-14 17:52:57 +02:00
MacRimi
11fbfda6bf Update cache.json 2025-08-13 22:46:58 +02:00
MacRimi
4f0353d0fb Create proxmox-upgrade-pve8-to-pve9-manual-guide.sh 2025-08-13 22:46:38 +02:00
MacRimi
a605d68d73 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-08-13 20:29:26 +02:00
MacRimi
237b7fbf1b Update cache.json 2025-08-13 20:29:24 +02:00
ProxMenuxBot
4a7e21f6b4 Update helpers_cache.json 2025-08-13 18:19:42 +00:00
MacRimi
b7017573b8 Update update-pve8.sh 2025-08-13 19:45:31 +02:00
MacRimi
a98b087c5d Update log2RAM 2025-08-13 15:55:22 +02:00
MacRimi
161c840136 Update utils.sh 2025-08-11 08:57:26 +02:00
MacRimi
4dd2abc202 Update utils.sh 2025-08-10 21:56:07 +02:00
MacRimi
cc0e9f61a7 Update utils.sh 2025-08-10 21:43:05 +02:00
MacRimi
21a658f1f4 Update utils.sh 2025-08-10 21:23:19 +02:00
MacRimi
b99f391c2a Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-08-10 21:09:25 +02:00
MacRimi
9abe25b91a Update README.md 2025-08-10 21:09:01 +02:00
ProxMenuxBot
2531fc6dac Update helpers_cache.json 2025-08-09 12:25:42 +00:00
ProxMenuxBot
e5551cb179 Update helpers_cache.json 2025-08-09 01:05:21 +00:00
MacRimi
4728b7a8b7 Update utils.sh 2025-08-08 18:26:32 +02:00
ProxMenuxBot
2863921e15 Update helpers_cache.json 2025-08-08 12:29:54 +00:00
ProxMenuxBot
b93668edfe Update helpers_cache.json 2025-08-08 06:23:01 +00:00
ProxMenuxBot
8ae91b8c31 Update helpers_cache.json 2025-08-07 18:21:56 +00:00
MacRimi
894a23d701 Update common-functions.sh 2025-08-07 20:10:36 +02:00
MacRimi
3598906cbd Update common-functions.sh 2025-08-07 19:37:28 +02:00
MacRimi
75c12e2d4b update common funtions 2025-08-07 19:24:39 +02:00
MacRimi
d6e0519a3d Update common-functions.sh 2025-08-07 19:08:44 +02:00
MacRimi
41efc71626 Update update-pve.sh 2025-08-07 19:05:52 +02:00
MacRimi
6e3d97f472 Update common-functions.sh 2025-08-07 19:05:23 +02:00
MacRimi
9d58d02522 Update proxmox_update.sh 2025-08-07 18:11:32 +02:00
MacRimi
fe86396b21 Update proxmox_update.sh 2025-08-07 18:08:37 +02:00
MacRimi
97994f7632 Update update-pve.sh 2025-08-07 17:58:24 +02:00
MacRimi
33d63457b3 Update update-pve8.sh 2025-08-07 17:58:00 +02:00
MacRimi
ed36d9e953 Update proxmox_update.sh 2025-08-07 15:49:22 +02:00
MacRimi
9a478d74d2 Update proxmox_update.sh 2025-08-07 15:45:46 +02:00
MacRimi
72d72544a4 Update update-pve.sh 2025-08-07 15:31:03 +02:00
MacRimi
4bbbe81182 Update update-pve.sh 2025-08-07 15:05:54 +02:00
MacRimi
a0af0c2492 Update update-pve.sh 2025-08-07 14:47:58 +02:00
MacRimi
ce7d3e4702 Update update-pve.sh 2025-08-07 14:30:48 +02:00
MacRimi
1bb4ca8541 Update common-functions.sh 2025-08-07 14:23:33 +02:00
MacRimi
ea65445772 Update update-pve.sh 2025-08-07 14:02:02 +02:00
MacRimi
972db8fcea Update update-pve8.sh 2025-08-07 13:04:05 +02:00
MacRimi
a3c12631f0 Update auto_post_install.sh 2025-08-07 11:16:24 +02:00
MacRimi
3cadfd08d8 Update install_proxmenux.sh 2025-08-07 08:04:04 +02:00
MacRimi
104f3de013 Update version.txt 2025-08-06 23:10:08 +02:00
MacRimi
713b41bd52 Update CHANGELOG.md 2025-08-06 23:09:36 +02:00
MacRimi
253093fa2f Update update-pve.sh 2025-08-06 23:08:53 +02:00
MacRimi
f36af5af64 Update update-pve.sh 2025-08-06 22:37:13 +02:00
MacRimi
97b6c0e44d Delete common-functions_.sh 2025-08-06 22:14:12 +02:00
MacRimi
c4f6dabd4d Update system_utils.sh 2025-08-06 22:02:40 +02:00
MacRimi
d1c8aeb25d update pve 8 2025-08-06 22:01:33 +02:00
MacRimi
6e1cb2e0fe Update common-functions.sh 2025-08-06 21:47:53 +02:00
MacRimi
da9762f60e Update update-pve.sh 2025-08-06 21:45:09 +02:00
MacRimi
27affdec14 Update common-functions.sh 2025-08-06 21:17:06 +02:00
MacRimi
433a19e46a Update customizable_post_install.sh 2025-08-06 20:10:38 +02:00
MacRimi
da9db9d3d1 Update update-pve.sh 2025-08-06 20:09:01 +02:00
MacRimi
ed4b0eba2f Update auto_post_install.sh 2025-08-06 20:04:52 +02:00
MacRimi
615aecf80f Update common funtions 2025-08-06 20:04:12 +02:00
MacRimi
4622d1a610 Update update-pve8.sh 2025-08-06 19:31:15 +02:00
MacRimi
f1b80d8f57 Update customizable_post_install.sh 2025-08-06 19:22:11 +02:00
MacRimi
e76e303383 Update update-pve8.sh 2025-08-06 19:20:03 +02:00
MacRimi
97133c3fcb Update auto_post_install.sh 2025-08-06 19:17:45 +02:00
MacRimi
450610b6e6 Update update-pve8.sh 2025-08-06 19:14:32 +02:00
MacRimi
4dc3fd92cc Update update-pve8.sh 2025-08-06 19:04:32 +02:00
MacRimi
f4b5e7c044 Create update-pve8.sh 2025-08-06 18:47:41 +02:00
MacRimi
6c0b2a468d Update common-functions.sh 2025-08-06 18:47:16 +02:00
MacRimi
e33f724f1b Create common-functions.sh 2025-08-06 18:17:51 +02:00
MacRimi
b0d5562917 Update remove-banner-pve8.sh 2025-08-06 17:45:22 +02:00
MacRimi
eecf7a2194 Update remove subscription banner PVE 8.4.9 2025-08-06 17:17:00 +02:00
MacRimi
54fd8a0332 Update remove-banner-pve8.sh 2025-08-06 17:12:16 +02:00
MacRimi
b6ca91980b global update 2025-08-06 16:50:16 +02:00
MacRimi
6af7e2d749 Update remove-banner-pve8.sh 2025-08-06 15:27:26 +02:00
MacRimi
86d334c204 Update remove-banner-pve9.sh 2025-08-06 15:21:31 +02:00
MacRimi
585a4fa449 Update remove-banner-pve9.sh 2025-08-06 15:19:56 +02:00
MacRimi
7438073e7e Update remove-banner-pve8.sh 2025-08-06 15:19:33 +02:00
MacRimi
6e808ae35a Update remove-banner-pve8.sh 2025-08-06 14:32:14 +02:00
MacRimi
99ec64e852 Create remove-banner-pve9.sh 2025-08-06 14:31:19 +02:00
MacRimi
eeac63c0a5 Create remove-banner-pve8.sh 2025-08-06 14:23:22 +02:00
MacRimi
5d5a3c3301 update funtions to pve9 2025-08-05 20:30:29 +02:00
ProxMenuxBot
31e9730236 Update helpers_cache.json 2025-08-05 12:31:00 +00:00
MacRimi
69b32a02ff Update utils.sh 2025-08-05 09:46:30 +02:00
ProxMenuxBot
a222df8176 Update helpers_cache.json 2025-08-04 12:30:37 +00:00
MacRimi
7f4c99be60 Update customizable_post_install.sh 2025-08-04 11:29:43 +02:00
MacRimi
ccff657a62 Update auto_post_install.sh 2025-08-04 11:29:28 +02:00
MacRimi
fb258499e1 Update customizable_post_install.sh 2025-08-04 11:22:10 +02:00
MacRimi
79c6d6c742 Update customizable_post_install.sh 2025-08-04 11:20:38 +02:00
MacRimi
80d9d5480c Update customizable_post_install.sh 2025-08-04 11:19:41 +02:00
MacRimi
958a553922 Update auto_post_install.sh 2025-08-04 11:19:27 +02:00
MacRimi
a44bbc3513 Update customizable_post_install.sh 2025-08-04 11:16:52 +02:00
MacRimi
d7f2f4a3e7 Update customizable_post_install.sh 2025-08-04 11:15:59 +02:00
MacRimi
073566a23e Update auto_post_install.sh 2025-08-04 11:11:14 +02:00
MacRimi
590aecfcf1 Update customizable_post_install.sh 2025-08-04 11:11:00 +02:00
MacRimi
77ab52310e Update auto_post_install.sh 2025-08-04 11:03:31 +02:00
MacRimi
2c2ccddbe4 Update customizable_post_install.sh 2025-08-04 11:02:23 +02:00
MacRimi
87062db9d5 Update customizable_post_install.sh 2025-08-04 10:44:58 +02:00
MacRimi
b74701dbc5 Update system_utils.sh 2025-08-04 08:52:55 +02:00
MacRimi
a88db8830b Update customizable_post_install.sh 2025-08-04 08:34:40 +02:00
ProxMenuxBot
36cd83c796 Update helpers_cache.json 2025-08-04 01:16:40 +00:00
MacRimi
a039c93c05 Update customizable_post_install.sh 2025-08-03 23:13:36 +02:00
MacRimi
57b4ade3be Update auto_post_install.sh 2025-08-03 23:12:32 +02:00
MacRimi
87ce6cfa98 Update auto_post_install.sh 2025-08-03 23:08:06 +02:00
MacRimi
6a99c8c81d Update customizable_post_install.sh 2025-08-03 23:06:31 +02:00
ProxMenuxBot
46e8188d5a Update helpers_cache.json 2025-08-02 18:19:25 +00:00
MacRimi
3c990df1fe Actualizar main_menu.sh 2025-08-02 12:02:10 +02:00
ProxMenuxBot
8969bc5aa6 Update helpers_cache.json 2025-08-02 01:07:27 +00:00
MacRimi
45e8ca8d42 Update customizable_post_install.sh 2025-08-01 08:28:12 +02:00
MacRimi
39930153c9 Update customizable_post_install.sh 2025-08-01 07:25:44 +02:00
MacRimi
5890e46db3 Update auto_post_install.sh 2025-08-01 06:57:21 +02:00
MacRimi
c5c06a08ba Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-08-01 06:50:09 +02:00
MacRimi
2a0e677a89 Update proxmox_update.sh 2025-08-01 06:50:07 +02:00
ProxMenuxBot
8f7a968dc9 Update helpers_cache.json 2025-07-31 18:21:21 +00:00
MacRimi
20cfc50448 Update select_linux_iso.sh 2025-07-31 19:39:03 +02:00
MacRimi
4bf019ec7e Update jd2.sh 2025-07-31 19:11:18 +02:00
MacRimi
57fe45484c Update jd2.sh 2025-07-31 18:00:30 +02:00
MacRimi
7744f4ed76 Update jd2.sh 2025-07-31 17:43:14 +02:00
MacRimi
a43e81e229 Update jd2.sh 2025-07-31 15:14:00 +02:00
MacRimi
f6ad7e250b Update jd2.sh 2025-07-31 14:45:02 +02:00
MacRimi
0f2b0482ec Create jd2.sh 2025-07-31 14:27:12 +02:00
MacRimi
21d850d39e Rename jd2.sh to jd2_.sh 2025-07-31 14:26:46 +02:00
MacRimi
d3f2e42301 Create jd2.sh 2025-07-31 12:50:19 +02:00
MacRimi
df68154f10 Update disk-passthrough_ct.sh 2025-07-30 18:43:54 +02:00
MacRimi
f8ebf03afd Update system_utils.sh 2025-07-30 18:18:13 +02:00
MacRimi
23f8b97319 Update customizable_post_install.sh 2025-07-30 17:59:12 +02:00
MacRimi
d712054353 Update customizable_post_install.sh 2025-07-30 17:55:51 +02:00
MacRimi
58da896b14 Update system_utils.sh 2025-07-30 17:54:15 +02:00
MacRimi
8db57bda6e Update install utilities 2025-07-30 17:34:44 +02:00
MacRimi
53f29ec710 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-29 22:22:38 +02:00
MacRimi
43a8fc0e86 Update customizable_post_install.sh 2025-07-29 22:22:36 +02:00
ProxMenuxBot
7f2adb068e Update helpers_cache.json 2025-07-29 18:21:40 +00:00
MacRimi
218ae9f9bf Update uninstall-tools.sh 2025-07-29 20:15:55 +02:00
MacRimi
350c03874d Update customizable_post_install.sh 2025-07-29 20:09:54 +02:00
MacRimi
575c0e5bf9 Update customizable_post_install.sh 2025-07-29 19:58:49 +02:00
ProxMenuxBot
3a890ba2c7 Update helpers_cache.json 2025-07-29 12:30:07 +00:00
ProxMenuxBot
2a345f4869 Update helpers_cache.json 2025-07-28 12:30:05 +00:00
ProxMenuxBot
658ebbd84d Update helpers_cache.json 2025-07-27 12:26:49 +00:00
ProxMenuxBot
0c54ade367 Update helpers_cache.json 2025-07-26 17:08:21 +00:00
ProxMenuxBot
f16ba64026 Update helpers_cache.json 2025-07-25 12:28:04 +00:00
ProxMenuxBot
d72127aaeb Update helpers_cache.json 2025-07-25 01:09:29 +00:00
ProxMenuxBot
40e0b1291c Update helpers_cache.json 2025-07-24 18:20:25 +00:00
MacRimi
0fae7f0166 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-22 22:34:12 +02:00
MacRimi
6f916a4c32 Update customizable_post_install.sh 2025-07-22 22:33:58 +02:00
ProxMenuxBot
41979d5389 Update helpers_cache.json 2025-07-22 18:20:56 +00:00
MacRimi
0d51205bd6 Update customizable_post_install.sh 2025-07-22 19:10:35 +02:00
MacRimi
416dd52a30 Update fastfetch 2025-07-22 19:03:41 +02:00
MacRimi
850e45b9a5 Update uninstall-tools.sh 2025-07-22 18:54:14 +02:00
MacRimi
040d2ca7f6 Update customizable_post_install.sh 2025-07-22 18:39:48 +02:00
ProxMenuxBot
e173072622 Update helpers_cache.json 2025-07-22 12:29:23 +00:00
MacRimi
991dd80382 Update select_linux_iso.sh 2025-07-22 12:02:38 +02:00
MacRimi
5fc5d02134 Update auto_post_install.sh 2025-07-22 09:27:40 +02:00
MacRimi
0f51256add Update auto_post_install.sh 2025-07-22 09:22:59 +02:00
ProxMenuxBot
a78860dbc4 Update helpers_cache.json 2025-07-21 18:20:56 +00:00
MacRimi
f655a3c52d Update auto_post_install.sh 2025-07-20 20:03:12 +02:00
MacRimi
b69aebd5be update post-install menu 2025-07-20 19:36:59 +02:00
MacRimi
769a7c391f Update customizable_post_install.sh 2025-07-20 14:18:06 +02:00
MacRimi
9a4d55aa36 Update customizable_post_install.sh 2025-07-20 12:34:21 +02:00
MacRimi
e776acfbab Update CHANGELOG.md 2025-07-20 12:08:37 +02:00
MacRimi
b4f58286b4 Update network_menu.sh 2025-07-20 11:50:04 +02:00
MacRimi
59779cc931 Update network_menu.sh 2025-07-20 11:46:26 +02:00
MacRimi
567bcecc80 Update network_menu.sh 2025-07-20 11:42:25 +02:00
MacRimi
0ca87dc29b Update network_menu.sh 2025-07-20 11:38:54 +02:00
MacRimi
6e0824e357 Update customizable_post_install.sh 2025-07-20 11:27:08 +02:00
MacRimi
9a8a620658 Update uninstall-tools.sh 2025-07-20 11:17:33 +02:00
MacRimi
eb1db3120d Update auto_post_install.sh 2025-07-20 10:55:04 +02:00
MacRimi
98a9225c32 Update customizable_post_install.sh 2025-07-20 02:27:46 +02:00
MacRimi
1b8fb766a8 Update auto_post_install.sh 2025-07-20 02:26:26 +02:00
MacRimi
c28ef3ec3b Update auto_post_install.sh 2025-07-20 02:12:27 +02:00
MacRimi
fab3f0630c Update customizable_post_install.sh 2025-07-20 02:09:11 +02:00
MacRimi
d2499ad157 Update customizable_post_install.sh 2025-07-20 01:55:47 +02:00
MacRimi
724a37bbf4 Update auto_post_install.sh 2025-07-20 01:51:14 +02:00
MacRimi
c5f1c30b1c Update disk-passthrough_ct.sh 2025-07-19 18:28:28 +02:00
MacRimi
e90363df71 Update customizable_post_install.sh 2025-07-19 17:43:31 +02:00
MacRimi
0932008619 Update auto_post_install.sh 2025-07-19 17:40:52 +02:00
MacRimi
a24e00ad5a Update auto_post_install.sh 2025-07-19 17:25:43 +02:00
MacRimi
fab938055f Update auto_post_install.sh 2025-07-19 17:19:06 +02:00
MacRimi
b2026b0dac Update auto_post_install.sh 2025-07-19 17:06:33 +02:00
MacRimi
c1c742084e Update auto_post_install.sh 2025-07-19 17:02:38 +02:00
MacRimi
7140f590cf Update uninstall-tools.sh 2025-07-19 16:52:13 +02:00
MacRimi
dcc26ad666 Update customizable_post_install.sh 2025-07-19 16:43:14 +02:00
MacRimi
70f1ecad49 Update customizable_post_install.sh 2025-07-19 16:38:55 +02:00
ProxMenuxBot
03c7383b54 Update helpers_cache.json 2025-07-17 18:20:17 +00:00
ProxMenuxBot
d3734971cc Update helpers_cache.json 2025-07-14 18:20:49 +00:00
ProxMenuxBot
cff8358b3e Update helpers_cache.json 2025-07-14 12:28:40 +00:00
ProxMenuxBot
42d691c4ce Update helpers_cache.json 2025-07-13 18:18:07 +00:00
MacRimi
33dcbe8b5a Update install_coral_lxc.sh 2025-07-12 18:34:49 +02:00
MacRimi
980c7a4390 Update uupdump_creator.sh 2025-07-11 21:20:18 +02:00
MacRimi
1b7f881d5a Update uupdump_creator.sh 2025-07-11 20:20:32 +02:00
MacRimi
8635e2cf67 Update uupdump_creator.sh 2025-07-11 20:13:33 +02:00
MacRimi
e36bc8bab2 Update uupdump_creator.sh 2025-07-11 20:09:16 +02:00
MacRimi
693da7733f Update uupdump_creator.sh 2025-07-11 19:59:34 +02:00
MacRimi
6b6128d92d Update uupdump_creator.sh 2025-07-11 17:28:02 +02:00
MacRimi
64586a44b4 Update customizable_post_install.sh 2025-07-10 19:25:50 +02:00
MacRimi
2d28ecca32 Update network_menu.sh 2025-07-10 19:16:16 +02:00
MacRimi
92f3edb337 Update network_menu.sh 2025-07-10 19:11:32 +02:00
MacRimi
32f8edecdd Update network_menu.sh 2025-07-10 17:09:59 +02:00
MacRimi
14b061cd28 Update cache.json 2025-07-10 12:37:59 +02:00
MacRimi
aeb90cbdd2 Update network_menu.sh 2025-07-10 12:31:51 +02:00
MacRimi
e2a0b627b2 Update cache.json 2025-07-09 22:29:03 +02:00
MacRimi
de5eb0d914 Update version.txt 2025-07-09 22:07:26 +02:00
MacRimi
fc97499504 New version 2025-07-09 21:55:23 +02:00
MacRimi
681f5622c6 Update install_proxmenux.sh 2025-07-09 20:48:54 +02:00
MacRimi
323812710e Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-09 20:48:02 +02:00
MacRimi
d421c0c62c Update install_proxmenux.sh 2025-07-09 20:47:17 +02:00
MacRimi
8cbcd74f1a update menu 2025-07-09 20:46:24 +02:00
ProxMenuxBot
328cdad26e Update helpers_cache.json 2025-07-09 18:19:15 +00:00
MacRimi
25b83cb86e Update menu 2025-07-09 19:50:55 +02:00
MacRimi
d3c5e4c376 Update emergency_repair.sh 2025-07-09 19:36:54 +02:00
MacRimi
bf518f548c Update cache.json 2025-07-09 19:18:12 +02:00
MacRimi
3999416366 Update cache.json 2025-07-09 19:13:34 +02:00
MacRimi
707811cc4a update script coral TPU 2025-07-09 18:59:26 +02:00
MacRimi
05a7f4ba83 Update install_coral_lxc.sh 2025-07-09 18:11:45 +02:00
MacRimi
be339435b8 Update menu_post_install.sh 2025-07-09 14:55:05 +02:00
MacRimi
ea7adb9b5b Update auto_post_install.sh 2025-07-09 14:42:47 +02:00
ProxMenuxBot
3583cb007a Update helpers_cache.json 2025-07-09 12:28:00 +00:00
MacRimi
cc06a4e1b1 Update proxmox_update.sh 2025-07-09 08:44:29 +02:00
MacRimi
6e0c09816b Update proxmox_update.sh 2025-07-09 08:36:00 +02:00
MacRimi
50a4514954 Update network_menu.sh 2025-07-09 08:26:30 +02:00
MacRimi
91bca917c2 update menu network 2025-07-09 08:19:07 +02:00
MacRimi
0550aa9bd2 Update main menu 2025-07-08 23:21:11 +02:00
MacRimi
c9d172d301 update network repair 2025-07-08 22:58:00 +02:00
MacRimi
77c950bc1e Update network_menu.sh 2025-07-08 22:42:12 +02:00
ProxMenuxBot
d0bbfa82d1 Update helpers_cache.json 2025-07-08 01:06:28 +00:00
ProxMenuxBot
53d657ea0e Update helpers_cache.json 2025-07-07 12:27:17 +00:00
ProxMenuxBot
bf8e677b55 Update helpers_cache.json 2025-07-07 06:21:40 +00:00
MacRimi
8deebf83ee Update network_menu.sh 2025-07-06 23:09:27 +02:00
MacRimi
31ec2a0b3a Update network_menu.sh 2025-07-06 17:45:33 +02:00
MacRimi
e6b568d7d6 update menu netwokr 2025-07-06 17:44:01 +02:00
MacRimi
dfbfea5169 update cache 2025-07-06 16:47:50 +02:00
MacRimi
66862c9bd3 Update cache.json 2025-07-06 16:38:25 +02:00
MacRimi
84c217717f Update customizable_post_install.sh 2025-07-06 16:33:23 +02:00
MacRimi
67f309f32e Update post-install 2025-07-06 16:30:16 +02:00
MacRimi
6e7e7cc7fa Update auto_post_install.sh 2025-07-06 14:31:04 +02:00
MacRimi
5f39a1e08c delete scripts 2025-07-06 14:27:46 +02:00
MacRimi
b175ddb0ca Update cache.json 2025-07-06 14:26:03 +02:00
MacRimi
4f36b2eab9 Update cache.json 2025-07-06 14:22:26 +02:00
MacRimi
853c86c40b Update cache.json 2025-07-06 14:17:17 +02:00
MacRimi
242dfa6c9e update post install automated 2025-07-06 14:02:24 +02:00
MacRimi
d98b68ca65 Update menu_post_install.sh 2025-07-06 13:49:52 +02:00
MacRimi
a41df16f14 Update auto_post_install.sh 2025-07-06 13:06:57 +02:00
MacRimi
48b545d731 new script post-install automated 2025-07-06 12:21:29 +02:00
MacRimi
707b6a6f1b Update config_menu.sh 2025-07-05 17:31:18 +02:00
MacRimi
c21b374b49 Update config_menu.sh 2025-07-05 17:26:31 +02:00
MacRimi
e09749c6f2 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-05 16:31:26 +02:00
MacRimi
b7876d1774 Update install_proxmenux.sh 2025-07-05 16:31:24 +02:00
MacRimi
d0590600f3 Update cache.json 2025-07-05 16:28:15 +02:00
MacRimi
fe5b30b4c6 Update cache.json 2025-07-05 16:20:49 +02:00
MacRimi
b7ce73d338 Update cache.json 2025-07-05 15:51:09 +02:00
MacRimi
8139eb607c Update utils.sh 2025-07-05 11:55:41 +02:00
MacRimi
96c08e6563 Update utils.sh 2025-07-05 11:52:20 +02:00
MacRimi
38977af9d3 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-05 11:48:36 +02:00
MacRimi
0584081c33 Update utils.sh 2025-07-05 11:48:33 +02:00
MacRimi
ea5e471bf8 Update cache.json 2025-07-05 11:28:51 +02:00
MacRimi
230847dace Update install Proxmenux 2025-07-04 22:37:16 +02:00
MacRimi
5a91810e9a Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-04 21:44:21 +02:00
MacRimi
49ff63fad4 Update proxmox_update.sh 2025-07-04 21:44:19 +02:00
MacRimi
96cc87cde8 Update cache.json 2025-07-04 21:15:25 +02:00
MacRimi
baddd82ecc Update main_menu.sh 2025-07-04 20:58:43 +02:00
MacRimi
a4b127a8f3 Update main_menu.sh 2025-07-04 20:50:07 +02:00
MacRimi
2851fd20b8 Update main_menu.sh 2025-07-04 20:36:19 +02:00
MacRimi
dd278fe3d3 Update install ProxMenux 2025-07-04 20:30:19 +02:00
MacRimi
196c29da17 Update menu 2025-07-04 19:40:38 +02:00
ProxMenuxBot
a1f7fb57d9 Update helpers_cache.json 2025-07-04 12:26:57 +00:00
ProxMenuxBot
27acd37a2c Update helpers_cache.json 2025-07-03 18:18:48 +00:00
ProxMenuxBot
6fb7202e89 Update helpers_cache.json 2025-07-02 18:19:27 +00:00
MacRimi
dddd12a036 update script iso creator 2025-07-02 18:47:08 +02:00
MacRimi
f4c0211cba Update utilities menu 2025-07-02 18:36:30 +02:00
MacRimi
57d291a13c Update uup_dump_iso_creator.sh 2025-07-02 18:05:38 +02:00
MacRimi
5b2011cf82 Update uup_dump_iso_creator.sh 2025-07-02 18:02:42 +02:00
MacRimi
d14f0e9295 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-02 17:57:28 +02:00
MacRimi
f2a15f84a8 Updates menus 2025-07-02 17:57:25 +02:00
ProxMenuxBot
d86831467b Update helpers_cache.json 2025-07-02 01:06:29 +00:00
MacRimi
d12365667f Update proxmox_update.sh 2025-07-01 20:56:57 +02:00
MacRimi
dcc07d4590 Create proxmox_update.sh 2025-07-01 20:49:36 +02:00
MacRimi
c996149476 Update uup_dump_iso_creator.sh 2025-07-01 19:03:35 +02:00
MacRimi
39ff81dee4 Update uup_dump_iso_creator.sh 2025-07-01 18:13:32 +02:00
ProxMenuxBot
22aa3aef96 Update helpers_cache.json 2025-06-30 18:47:59 +00:00
MacRimi
3faff72519 utilities create menu 2025-06-30 18:54:16 +02:00
MacRimi
bac3245eb7 Update customizable_post_install.sh 2025-06-30 17:28:26 +02:00
MacRimi
ec9e8e348f Update customizable_post_install.sh 2025-06-30 15:25:10 +02:00
MacRimi
131da2ec60 Update customizable_post_install.sh 2025-06-30 15:12:31 +02:00
MacRimi
38d44deef6 Update uninstall-tools.sh 2025-06-29 18:20:15 +02:00
MacRimi
71c31b182a Update uninstall-tools.sh 2025-06-29 17:59:45 +02:00
MacRimi
02baeba8b9 Update uninstall-tools.sh 2025-06-29 17:43:48 +02:00
MacRimi
1d6ce959c8 Update customizable_post_install.sh 2025-06-29 16:58:34 +02:00
MacRimi
f599015473 Update customizable_post_install.sh 2025-06-29 16:44:14 +02:00
MacRimi
e98d804902 update menu nas 2025-06-29 15:40:36 +02:00
MacRimi
18f5ac1ead update menus nas and linux 2025-06-29 15:02:13 +02:00
MacRimi
5f2e346bd8 Update select_linux_iso.sh 2025-06-29 14:07:07 +02:00
MacRimi
96c23bf138 Update select_nas_iso.sh 2025-06-29 13:57:23 +02:00
MacRimi
446a724077 update menu linux and nas 2025-06-29 13:53:10 +02:00
MacRimi
6e9afa7f9f Update disk-passthrough_ct.sh 2025-06-28 21:43:48 +02:00
ProxMenuxBot
5c13d124de Update helpers_cache.json 2025-06-28 01:03:36 +00:00
ProxMenuxBot
4f94e21ea8 Update helpers_cache.json 2025-06-27 18:18:47 +00:00
ProxMenuxBot
7498aab76a Update helpers_cache.json 2025-06-26 18:20:04 +00:00
MacRimi
e65ac4ac8b Update uupdump_creator.sh 2025-06-25 20:23:45 +02:00
MacRimi
df2ec69448 Create Iso.sh 2025-06-25 20:23:31 +02:00
ProxMenuxBot
6fe474e494 Update helpers_cache.json 2025-06-25 12:27:42 +00:00
ProxMenuxBot
6408477939 Update helpers_cache.json 2025-06-24 18:18:58 +00:00
ProxMenuxBot
5a20a4260b Update helpers_cache.json 2025-06-24 12:27:48 +00:00
ProxMenuxBot
b604b81f37 Update helpers_cache.json 2025-06-24 01:06:00 +00:00
MacRimi
40e36b3203 Update customizable_post_install.sh 2025-06-21 19:09:10 +02:00
MacRimi
e098b63beb Update customizable_post_install.sh 2025-06-21 09:18:58 +02:00
ProxMenuxBot
56e99bc6ba Update helpers_cache.json 2025-06-19 18:18:49 +00:00
MacRimi
0abc42bf2c Update uupdump_creator.sh 2025-06-19 19:46:33 +02:00
ProxMenuxBot
f648eba8dd Update helpers_cache.json 2025-06-19 12:52:20 +00:00
ProxMenuxBot
20648b479f Update helpers_cache.json 2025-06-19 06:20:00 +00:00
ProxMenuxBot
ef82bac8fc Update helpers_cache.json 2025-06-18 18:19:19 +00:00
MacRimi
28e330520b update menu backup 2025-06-18 18:56:11 +02:00
MacRimi
b481bb08cc Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-06-17 19:52:39 +02:00
MacRimi
506d7fff22 Create backup_host.sh 2025-06-17 19:52:36 +02:00
ProxMenuxBot
da3b42b6ac Update helpers_cache.json 2025-06-17 12:27:56 +00:00
ProxMenuxBot
eea50c23b5 Update helpers_cache.json 2025-06-16 18:19:26 +00:00
ProxMenuxBot
b1f5860335 Update helpers_cache.json 2025-06-16 06:21:42 +00:00
ProxMenuxBot
958c567b6b Update helpers_cache.json 2025-06-16 01:07:19 +00:00
MacRimi
b443f278da Update mount_disk_host_bk.sh 2025-06-15 11:58:17 +02:00
MacRimi
f5ae187012 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-06-15 11:48:40 +02:00
MacRimi
6e48279c8a Update mount_disk_host_bk.sh 2025-06-15 11:48:30 +02:00
MacRimi
61976e8c13 Update mount_disk_host_bk.sh 2025-06-15 11:47:28 +02:00
MacRimi
2009b0ff7e Update mount_disk_host_bk.sh 2025-06-15 11:35:57 +02:00
MacRimi
60dd0d45b9 Update mount to host 2025-06-14 20:17:51 +02:00
MacRimi
a7422e4c1e Update mount_disk_host_bk.sh 2025-06-14 16:19:06 +02:00
MacRimi
ffd79d2404 Update mount_disk_host_bk.sh 2025-06-14 16:17:43 +02:00
MacRimi
7363a461de Update mount_disk_host_bk.sh 2025-06-14 15:58:49 +02:00
MacRimi
f7325f030c Rename Mount_disk_host_bk.sh to mount_disk_host_bk.sh 2025-06-14 11:59:25 +02:00
MacRimi
2668e5d8b2 Create Mount_disk_host_bk.sh 2025-06-14 11:52:50 +02:00
ProxMenuxBot
f09e3ffcdb Update helpers_cache.json 2025-06-13 01:05:40 +00:00
ProxMenuxBot
de4bba2e85 Update helpers_cache.json 2025-06-12 18:18:59 +00:00
ProxMenuxBot
bb50ecc86c Update helpers_cache.json 2025-06-10 18:18:50 +00:00
MacRimi
2191fe4cdd Update customizable_post_install.sh 2025-06-10 19:58:34 +02:00
MacRimi
321e0b2331 Update customizable_post_install.sh 2025-06-10 19:54:31 +02:00
MacRimi
f4611280a7 Update menu_Helper_Scripts.sh 2025-06-10 14:34:30 +02:00
MacRimi
ed3f2415bb Update hw_grafics_menu.sh 2025-06-10 14:29:34 +02:00
MacRimi
495bc24b2f Update install_coral_pve.sh 2025-06-10 14:27:09 +02:00
MacRimi
397c84cacb Update install_coral_lxc.sh 2025-06-10 14:24:30 +02:00
MacRimi
f04fb7e756 Update configure_igpu_lxc.sh 2025-06-10 14:18:05 +02:00
MacRimi
dcbed8b173 Update configure_igpu_lxc.sh 2025-06-10 14:03:58 +02:00
MacRimi
3e5e79ba18 Update config_menu.sh 2025-06-10 13:57:07 +02:00
MacRimi
ddaee77b59 Update config_menu.sh 2025-06-10 13:55:10 +02:00
MacRimi
2d5a08a921 Update create_vm_menu.sh 2025-06-10 13:52:30 +02:00
MacRimi
240a325ef1 Update create_vm_menu_.sh 2025-06-10 13:48:09 +02:00
MacRimi
663a0f15df Update cache.json 2025-06-10 13:30:03 +02:00
MacRimi
cb7afac17b Update main_menu.sh 2025-06-10 13:28:06 +02:00
MacRimi
b04710cf50 Update cache.json 2025-06-10 13:24:49 +02:00
MacRimi
ce3fd894ae Update storage_menu.sh 2025-06-10 12:56:05 +02:00
MacRimi
fd11f4e866 Update create_vm_menu.sh 2025-06-10 12:48:26 +02:00
MacRimi
5422af1e82 Update main_menu.sh 2025-06-10 12:45:17 +02:00
MacRimi
444002b006 Update storage_menu.sh 2025-06-10 12:44:49 +02:00
MacRimi
f01c474536 Actualizar storage_menu.sh 2025-06-10 11:41:07 +02:00
ProxMenuxBot
696b42666f Update helpers_cache.json 2025-06-10 01:06:21 +00:00
ProxMenuxBot
84190e0806 Update helpers_cache.json 2025-06-09 01:08:35 +00:00
ProxMenuxBot
80253426b7 Update helpers_cache.json 2025-06-07 01:03:54 +00:00
MacRimi
26ccc63c96 Actualizar CHANGELOG.md 2025-06-06 19:19:11 +02:00
MacRimi
1124ac41f9 Update CHANGELOG.md 2025-06-06 18:29:10 +02:00
MacRimi
d534d8b25c Update CHANGELOG.md 2025-06-06 18:28:26 +02:00
MacRimi
618afaacd4 add images 2025-06-06 17:54:44 +02:00
MacRimi
53b6ce56bf Update menu_Helper_Scripts.sh 2025-06-06 17:42:52 +02:00
MacRimi
8257c7d7e4 Update menu_Helper_Scripts.sh 2025-06-06 17:40:53 +02:00
MacRimi
769416f474 Update menu_Helper_Scripts.sh 2025-06-06 17:34:51 +02:00
MacRimi
f978e5d261 Update main_menu.sh 2025-06-06 17:30:24 +02:00
MacRimi
9d3660a1e2 update menu 2025-06-06 17:29:06 +02:00
MacRimi
f2637aad46 Update menu 2025-06-06 17:24:56 +02:00
MacRimi
371e8a9570 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-06-06 17:11:59 +02:00
MacRimi
56987fe7a0 Update customizable_post_install.sh 2025-06-06 17:11:57 +02:00
ProxMenuxBot
dfe5138cad Update helpers_cache.json 2025-06-06 12:25:55 +00:00
ProxMenuxBot
90a2d83670 Update helpers_cache.json 2025-06-06 01:04:20 +00:00
MacRimi
8c8981ea9f Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-06-06 00:02:47 +02:00
MacRimi
de49d67361 Update helpers-menu.sh 2025-06-06 00:02:43 +02:00
ProxMenuxBot
5826c383b7 Update helpers_cache.json 2025-06-05 12:27:18 +00:00
ProxMenuxBot
24962f44e1 Update helpers_cache.json 2025-06-04 21:26:29 +00:00
MacRimi
9f2fc40c76 Update generate_helpers_cache.py 2025-06-04 23:25:29 +02:00
MacRimi
b8bdcf4c71 Update helpers-menu.sh 2025-06-04 20:50:01 +02:00
MacRimi
c9c6dc7666 Update helpers-menu.sh 2025-06-04 20:47:11 +02:00
MacRimi
9f686b91a2 Update helpers-menu.sh 2025-06-04 20:45:05 +02:00
MacRimi
9e879d6582 Update helpers-menu.sh 2025-06-04 20:42:23 +02:00
MacRimi
2fc1df729b Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-06-04 20:39:57 +02:00
MacRimi
6586b9746a Create helpers-menu.sh 2025-06-04 20:39:50 +02:00
ProxMenuxBot
37d3ba3bc1 Update helpers_cache.json 2025-06-04 18:19:08 +00:00
ProxMenuxBot
1126860834 Update helpers_cache.json 2025-06-03 18:42:23 +00:00
MacRimi
c26141bc5d Update generate_helpers_cache.py 2025-06-03 20:41:20 +02:00
ProxMenuxBot
3d6cfb44bb Update helpers_cache.json 2025-06-03 17:41:05 +00:00
MacRimi
4b1bdc55f1 Update generate_helpers_cache.py 2025-06-03 19:40:01 +02:00
ProxMenuxBot
c2bdd5f0bb Update helpers_cache.json 2025-06-03 16:44:28 +00:00
MacRimi
c22544672c Update update-helpers-cache.yml 2025-06-03 18:42:14 +02:00
MacRimi
78064cc07c Update generate_helpers_cache.py 2025-06-03 18:41:49 +02:00
MacRimi
84f5897e38 Update generate_helpers_cache.py 2025-06-03 18:37:36 +02:00
ProxMenuxBot
9044f13d2b 🔄 Update helpers_cache.json 2025-06-03 09:13:31 +00:00
MacRimi
cf9ee44970 Actualizar update-helpers-cache.yml 2025-06-03 11:11:24 +02:00
MacRimi
0cf5830671 Actualizar generate_helpers_cache.py 2025-06-03 11:09:29 +02:00
MacRimi
f25d8aec3c Crear update-helpers-cache.yml 2025-06-03 11:00:47 +02:00
MacRimi
4ecb3f9943 Crear generate_helpers_cache.py 2025-06-03 10:58:58 +02:00
MacRimi
3dcb521422 Update import-disk-image.sh 2025-05-30 18:25:10 +02:00
MacRimi
de4db1de9a Update import-disk-image.sh 2025-05-30 15:11:18 +02:00
MacRimi
a6c2b958a2 Update import-disk-image.sh 2025-05-30 14:56:57 +02:00
MacRimi
f721d9d774 Update README.md 2025-05-30 11:12:45 +02:00
MacRimi
3a8c1c3fd9 Update README.md 2025-05-30 11:08:59 +02:00
MacRimi
891c70dd4c Update CODE_OF_CONDUCT.md 2025-05-30 11:07:26 +02:00
MacRimi
f2b99722e3 Create SECURITY.md 2025-05-30 11:03:54 +02:00
MacRimi
32204d3e17 Update vm_configurator.sh 2025-05-29 22:18:52 +02:00
MacRimi
112dfe08b3 Update vm_configurator.sh 2025-05-29 22:07:42 +02:00
MacRimi
4c3736fad7 Update storage_menu.sh 2025-05-29 21:50:48 +02:00
MacRimi
69a5b76e97 Update menu import image to vm 2025-05-29 21:49:21 +02:00
MacRimi
f941207699 Update import-disk-image.sh 2025-05-29 21:43:39 +02:00
MacRimi
084a8956ca Update menu_post_install.sh 2025-05-28 23:33:59 +02:00
MacRimi
571b5270a2 Update menus dialog 2025-05-28 23:26:58 +02:00
MacRimi
dcae6e1cd0 Update RSS page 2025-05-28 17:24:11 +02:00
MacRimi
a7d84d27fd Update page.tsx 2025-05-28 17:16:06 +02:00
MacRimi
9fba81f51d Update CHANGELOG.md 2025-05-27 20:48:54 +02:00
MacRimi
35e399dbaf Update storage_menu.sh 2025-05-27 20:32:30 +02:00
MacRimi
c3fc013002 Update storage_menu.sh 2025-05-27 20:30:17 +02:00
MacRimi
a34fc6eaa4 Update menus 2025-05-27 19:58:03 +02:00
184 changed files with 61541 additions and 4798 deletions

View File

@@ -0,0 +1,76 @@
import requests, json
from pathlib import Path
# GitHub API URL to fetch all .json files describing scripts
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
# Base path to build the full URL for the installable scripts
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
# Output file where the consolidated helper scripts cache will be stored
OUTPUT_FILE = Path("json/helpers_cache.json")
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
res = requests.get(API_URL)
data = res.json()
cache = []
# Loop over each file in the JSON directory
for item in data:
url = item.get("download_url")
if not url or not url.endswith(".json"):
continue
try:
raw = requests.get(url).json()
if not isinstance(raw, dict):
continue
except:
continue
# Extract fields required to identify a valid helper script
name = raw.get("name", "")
slug = raw.get("slug")
type_ = raw.get("type", "")
script = raw.get("install_methods", [{}])[0].get("script", "")
if not slug or not script:
continue # Skip if it's not a valid script
desc = raw.get("description", "")
categories = raw.get("categories", [])
notes = [note.get("text", "") for note in raw.get("notes", []) if isinstance(note, dict)]
full_script_url = f"{SCRIPT_BASE}/{script}"
credentials = raw.get("default_credentials", {})
cred_username = credentials.get("username")
cred_password = credentials.get("password")
add_credentials = (
(cred_username is not None and str(cred_username).strip() != "") or
(cred_password is not None and str(cred_password).strip() != "")
)
entry = {
"name": name,
"slug": slug,
"desc": desc,
"script": script,
"script_url": full_script_url,
"categories": categories,
"notes": notes,
"type": type_
}
if add_credentials:
entry["default_credentials"] = {
"username": cred_username,
"password": cred_password
}
cache.append(entry)
# Write the JSON cache to disk
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
json.dump(cache, f, indent=2)
print(f"✅ helpers_cache.json created at {OUTPUT_FILE} with {len(cache)} valid scripts.")

86
.github/workflows/build-appimage.yml vendored Normal file
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

View File

@@ -0,0 +1,38 @@
name: Update Helper Scripts Cache
on:
# Manual trigger from GitHub Actions UI
workflow_dispatch:
# Automatic run every 6 hours
schedule:
- cron: "0 */6 * * *"
jobs:
update-cache:
runs-on: ubuntu-latest
permissions:
contents: write # Required to push changes to the repository
steps:
- name: ⬇️ Checkout the repository
uses: actions/checkout@v3
- name: 🐍 Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: 📦 Install Python dependencies
run: pip install requests
- name: ⚙️ Generate json/helpers_cache.json
run: python .github/scripts/generate_helpers_cache.py
- name: 📤 Commit and push if updated
run: |
git config user.name "ProxMenuxBot"
git config user.email "bot@proxmenux.local"
git add json/helpers_cache.json
git diff --cached --quiet || git commit -m "Update helpers_cache.json"
git push

BIN
AppImage/ProxMenux-1.0.0.AppImage Executable file

Binary file not shown.

View File

@@ -0,0 +1 @@
e896eb10de4bf990d31c1d8357289f64cbce481921647f2be53efb850d0b73b2 ProxMenux-1.0.0.AppImage

41
AppImage/README.md Normal file
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".

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
AppImage/app/globals.css Normal file
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);
}
}

View File

@@ -0,0 +1,5 @@
import Hardware from "@/components/hardware"
export default function HardwarePage() {
return <Hardware />
}

46
AppImage/app/layout.tsx Normal file
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
AppImage/app/page.tsx Normal file
View File

@@ -0,0 +1,7 @@
"use client"
import { ProxmoxDashboard } from "../components/proxmox-dashboard"
export default function Home() {
return <ProxmoxDashboard />
}

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

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>
)
}

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

View File

@@ -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>
)
}

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>
)
}

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>
)
}

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>
)
}

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 },
]

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>
)
}

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

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>
)
}

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>
}

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>
)
}

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 }

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 }

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 }

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,
}

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 }

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 }

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 }

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,
}

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,
}

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
AppImage/lib/utils.ts Normal file
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
AppImage/next.config.mjs Normal file
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
AppImage/package.json Normal file
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"
}
}

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

BIN
AppImage/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
AppImage/public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

3
AppImage/public/icon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

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

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

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

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

View File

@@ -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

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
AppImage/scripts/AppRun Normal file
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

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

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()

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
AppImage/tsconfig.json Normal file
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
AppImage/types/hardware.ts Normal file
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())

View File

@@ -1,3 +1,408 @@
## 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
### Added
- **New Script: Upgrade PVE 8 to PVE 9**
Added a full upgrade tool located under `Utilities and Tools`. It provides:
1. **Automatic upgrade** from PVE 8 to 9
2. **Interactive upgrade** with step-by-step confirmations
3. **Check-only mode** using `check-pve8to9`
4. **Manual instructions** shown in order for users who prefer to upgrade manually
- **New Tools in System Utilities**
- [`s-tui`](https://github.com/amanusk/s-tui): Terminal-based CPU monitoring with graphs
- [`intel-gpu-tools`](https://gitlab.freedesktop.org/drm/igt-gpu-tools): Useful for Intel GPU diagnostics
---
### Improved
- **APT Upgrade Handling**
The PVE upgrade function now blocks the process if any package prompts for manual confirmation. This avoids partial upgrades and ensures consistency.
- **Network Optimization (sysctl)**
- Obsolete kernel parameters removed (e.g., `tcp_tw_recycle`, `nf_conntrack_helper`) to prevent warnings in **Proxmox 9 / kernel 6.14**
- Now generates only valid, up-to-date sysctl parameters
- **AMD CPU Patch Handling**
- Now applies correct `idle=nomwait` and KVM options (`ignore_msrs=1`, `report_ignored_msrs=0`)
- Expected warning is now documented and safely handled for stability with Ryzen/EPYC
- **Timezone & NTP Fixes**
- Automatically detects timezone using public IP geolocation
- Falls back to UTC if detection fails
- Restarts Postfix after timezone set → resolves `/var/spool/postfix/etc/localtime` mismatch warning
- **Repository & Package Installer Logic**
- Now verifies that working repositories exist before installing any package
- If none are available, adds a fallback **Debian stable** repository
- Replaces deprecated `mlocate` with `plocate` (compatible with Debian 13 and Proxmox 9)
- **Improved Logs and User Feedback**
- Actions that fail now provide precise messages (instead of falsely marking as success)
- Helps users clearly understand what's been applied or skipped
## 2025-08-06
### New version v1.1.4
### Added
- **Proxmox 9 Compatibility Preparation**
This version prepares **ProxMenux** for the upcoming **Proxmox VE 9**:
- The function to add the official Proxmox repositories now supports the new `.sources` format used in Proxmox 9, while maintaining backward compatibility with Proxmox 8.
- Banner removal is now optionally supported for Proxmox 9.
- **xshok-proxmox Detection**
Added a check to detect if the `xshok-proxmox` post-install script has already been executed.
If detected, a warning is shown to avoid conflicting adjustments:
```
It appears that you have already executed the xshok-proxmox post-install script on this system.
If you continue, some adjustments may be duplicated or conflict with those already made by xshok.
Do you want to continue anyway?
```
---
### Improved
- **Banner Removal (Proxmox 8.4.9+)**
Updated the logic for removing the subscription banner in **Proxmox 8.4.9**, due to changes in `proxmoxlib.js`.
- **LXC Disk Passthrough (Persistent UUID)**
The function to add a physical disk to an LXC container now uses **UUID-based persistent paths**.
This ensures that disks remain correctly mounted, even if the `/dev/sdX` order changes due to new hardware.
```bash
PERSISTENT_DISK=$(get_persistent_path "$DISK")
if [[ "$PERSISTENT_DISK" != "$DISK" ]] ...
```
- **System Utilities Installer**
Now checks whether APT sources are available before installing selected tools.
If a new Proxmox installation has no active repos, it will **automatically add the default sources** to avoid installation failure.
- **IOMMU Activation on ZFS Systems**
The function that enables IOMMU for passthrough now verifies existing kernel parameters to avoid duplication if the user has already configured them manually.
---
### Fixed
- Minor code cleanup and improved runtime performance across several modules.
## 2025-07-20
### Changed
- **Subscription Banner Removal (Proxmox 8.4.5+)**
Improved the `remove_subscription_banner` function to ensure compatibility with Proxmox 8.4.5, where the banner removal method was failing after clean installations.
- **Improved Log2RAM Detection**
In both the automatic and customizable post-install scripts, the logic for Log2RAM installation has been improved.
Now it correctly detects if Log2RAM is already configured and avoids triggering errors or reconfiguration.
- **Optimized Figurine Installation**
The `install_figurine` function now avoids duplicating `.bashrc` entries if the customization for the root prompt already exists.
### Added
- **New Function: Persistent Network Interface Naming**
Added a new function `setup_persistent_network` to create stable network interface names using `.link` files based on MAC addresses.
This avoids unpredictable renaming (e.g., `enp2s0` becoming `enp3s0`) when hardware changes, PCI topology shifts, or passthrough configurations are applied.
**Why use `.link` files?**
Because predictable interface names in `systemd` can change with hardware reordering or replacement. Using static `.link` files bound to MAC addresses ensures consistency, especially on systems with multiple NICs or passthrough setups.
Special thanks to [@Andres_Eduardo_Rojas_Moya] for contributing the persistent
network naming function and for the original idea.
```bash
[Match]
MACAddress=XX:XX:XX:XX:XX:XX
[Link]
Name=eth0
```
## 2025-07-01
### New version v1.1.3
![Installer Menu](https://macrimi.github.io/ProxMenux/install/install.png)
- **Dual Installation Modes for ProxMenux**
The installer now offers two distinct modes:
1. **Lite version (no translations):** Only installs two official Debian packages (`dialog`, `jq`) to enable menus and JSON parsing. No files are written beyond the configuration directory.
2. **Full version (with translations):** Uses a virtual environment and allows selecting the interface language during installation.
When updating, if the user switches from full to lite, the old version will be **automatically removed** for a clean transition.
### Added
- **New Script: Automated Post-Installation Setup**
A new minimal post-install script that performs essential setup automatically:
- System upgrade and sync
- Remove enterprise banner
- Optimize APT, journald, logrotate, system limits
- Improve kernel panic handling, memory settings, entropy, network
- Add `.bashrc` tweaks and **Log2RAM auto-install** (if SSD/M.2 is detected)
- **New Function: Log2RAM Configuration**
Now available in both the customizable and automatic post-install scripts.
On systems with SSD/NVMe, Log2RAM is **enabled automatically** to preserve disk life.
- **New Menus:**
- 🧰 **System Utilities Menu**
Lets users select and install useful CLI tools with proper command validation.
- 🌐 **Network Configuration & Repair**
A new interactive menu for analyzing and repairing network interfaces.
### Improved
- **Post-Install Menu Logic**
Options are now grouped more logically for better usability.
- **VM Creation Menu**
Enhanced with improved CPU model support and custom options.
- **UUP Dump ISO Creator Script**
- Added option to **customize the temporary folder location**
- Fixed issue where entire temp folder was deleted instead of just contents
💡 Suggested by [@igrokit](https://github.com/igrokit)
[#17](https://github.com/MacRimi/ProxMenux/issues/17), [#11](https://github.com/MacRimi/ProxMenux/issues/11)
- **Physical Disk to LXC Script**
Now handles **XFS-formatted disks** correctly.
Thanks to [@antroxin](https://github.com/antroxin) for reporting and testing!
- **System Utilities Installer**
Rewritten to **verify command availability** after installation, ensuring tools work as expected.
🐛 Fix for [#18](https://github.com/MacRimi/ProxMenux/issues/18) by [@DST73](https://github.com/DST73)
### Fixed
- **Enable IOMMU on ZFS**
The detection and configuration for enabling IOMMU on ZFS-based systems is now fully functional.
🐛 Fix for [#15](https://github.com/MacRimi/ProxMenux/issues/15) by [@troponaut](https://github.com/troponaut)
### Other
- Performance and code cleanup improvements across several modules.
## 2025-06-06
### Added
- **New Menu: Proxmox PVE Helper Scripts**
Officially introduced the new **Proxmox PVE Helper Scripts** menu, replacing the previous: Esenciales Proxmox.
This new menu includes:
- Script search by name in real time
- Category-based browsing
Its a cleaner, faster, and more functional way to access community scripts in Proxmox.
![Helper Scripts Menu](https://macrimi.github.io/ProxMenux/menu-helpers-script.png)
- **New CPU Models in VM Creation**
The CPU selection menu in VM creation has been greatly expanded to support advanced QEMU and x86-64 CPU profiles.
This allows better compatibility with modern guest systems and fine-tuning performance for specific workloads, including nested virtualization and hardware-assisted features.
![CPU Config](https://macrimi.github.io/ProxMenux/vm/config-cpu.png)
Thanks to **@Nida Légé (Nidouille)** for suggesting this enhancement.
- **Support for `.raw` Disk Images**
The disk import tool for VMs now supports `.raw` files, in addition to `.img`, `.qcow2`, and `.vmdk`.
This improves compatibility when working with disk exports from other hypervisors or backup tools.
💡 Suggested by **@guilloking** in [GitHub Issue #5](https://github.com/MacRimi/ProxMenux/issues/5)
- **Locale Detection in Language Skipping**
The function that disables extra APT languages now includes:
- Automatic locale detection (`LANG`)
- Auto-generation of `en_US.UTF-8` if none is found
- Prevents warnings during script execution due to undefined locale
### Improved
- **APT Language Skipping Logic**
Improved locale handling ensures system compatibility before disabling translations:
```bash
if ! locale -a | grep -qi "^${default_locale//-/_}$"; then
echo "$default_locale UTF-8" >> /etc/locale.gen
locale-gen "$default_locale"
fi
```
- **System Update Speed**
Post-install system upgrades are now faster:
- The upgrade process (`dist-upgrade`) is separated from container template index updates.
- Index refresh is now an optional feature selected in the script.
## 2025-05-27
### Fixed
- **Kali Linux ISO URL Updated**
Fixed the incorrect download URL for Kali Linux ISO in the Linux installer module. The new correct path is:
```
https://cdimage.kali.org/kali-2025.1c/kali-linux-2025.1c-installer-amd64.iso
```
### Improved
- **Faster Dialog Menu Transitions**
Improved UI responsiveness across all interactive menus by replacing `whiptail` with `dialog`, offering faster transitions and smoother navigation.
- **Coral USB Support in LXC**
Improved the logic for configuring Coral USB TPU passthrough into LXC containers:
- Refactored configuration into modular blocks with better structure and inline comments.
- Clear separation of Coral USB (`/dev/coral`) and Coral M.2 (`/dev/apex_0`) logic.
- Maintains backward compatibility with existing LXC configurations.
- Introduced persistent Coral USB passthrough using a udev rule:
```bash
# Create udev rule for Coral USB
SUBSYSTEM=="usb", ATTRS{idVendor}=="18d1", ATTRS{idProduct}=="9302", MODE="0666", TAG+="uaccess", SYMLINK+="coral"
# Map /dev/coral if it exists
if [ -e /dev/coral ]; then
echo "lxc.mount.entry: /dev/coral dev/coral none bind,optional,create=file" >> "$CONFIG_FILE"
fi
```
- Special thanks to **@Blaspt** for validating the persistent Coral USB passthrough and suggesting the use of `/dev/coral` symbolic link.
### Added
- **Persistent Coral USB Passthrough Support**
Added udev rule support for Coral USB devices to persistently map them as `/dev/coral`, enabling consistent passthrough across reboots. This path is automatically detected and mapped in the container configuration.
- **RSS Feed Integration**
Added support for generating an RSS feed for the changelog, allowing users to stay informed of updates through news clients.
- **Release Service Automation**
Implemented a new release management service to automate publishing and tagging of versions, starting with version **v1.1.2**.
## 2025-05-13
### Fixed

View File

@@ -29,6 +29,7 @@ Instead, please report it privately via email:
📧 proxmenux@macrimi.pro
For detailed information on security vulnerabilities and how to report them, please refer to our [Security Policy](./SECURITY.md).
## 🤝 3. Community Guidelines

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>
@@ -59,7 +59,7 @@ Then, follow the on-screen options to manage your Proxmox server efficiently.
## 📌 System Requirements
🖥 **Compatible with:**
- Proxmox VE 8.x**
- Proxmox VE 8.x and 9.x
📦 **Dependencies:**
- `bash`, `curl`, `wget`, `jq`, `whiptail`, `python3-venv` (These dependencies are installed automatically during setup.)
@@ -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).

41
SECURITY.md Normal file
View File

@@ -0,0 +1,41 @@
# 🔒 Security Policy
## 📅 Supported Versions
We actively maintain the latest release of ProxMenux. Only the most recent version receives security updates.
| Version | Supported |
| ------- | --------- |
| Latest | ✅ |
| Older versions | ❌ |
## 📢 Reporting a Vulnerability
If you discover a **security vulnerability**, please help us keep the community safe by reporting it **privately**.
**Do not report vulnerabilities in public GitHub Issues or Discussions.**
### 📬 Contact
To report a vulnerability, email:
**📧 proxmenux@macrimi.pro**
Please include as much detail as possible, including:
- Steps to reproduce the issue
- A description of the impact
- Any known mitigations
We aim to respond as soon as possible, typically within **48 hours**.
## ⚠️ Coordinated Disclosure
We follow responsible disclosure principles. If a vulnerability is confirmed, we will:
1. Work on a fix immediately.
2. Inform you of the resolution status.
---
🔐 Thank you for helping make ProxMenux a safer project for everyone!

View File

@@ -1,13 +1,13 @@
#!/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
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.2
# Last Updated: 04/04/2025
# Version : 1.3
# Last Updated: 04/07/2025
# ==========================================================
# Description:
# This script installs and configures ProxMenux, a menu-driven
@@ -33,7 +33,6 @@
# the system for running ProxMenux efficiently.
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
UTILS_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main/scripts/utils.sh"
@@ -46,59 +45,366 @@ LOCAL_VERSION_FILE="$BASE_DIR/version.txt"
MENU_SCRIPT="menu"
VENV_PATH="/opt/googletrans-env"
# Source utils.sh for common functions and styles
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
fi
cleanup_corrupted_files() {
if [ -f "$CONFIG_FILE" ] && ! jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
echo "Cleaning up corrupted configuration file..."
rm -f "$CONFIG_FILE"
fi
if [ -f "$CACHE_FILE" ] && ! jq empty "$CACHE_FILE" >/dev/null 2>&1; then
echo "Cleaning up corrupted cache file..."
rm -f "$CACHE_FILE"
fi
}
# ==========================================================
check_existing_installation() {
local has_venv=false
local has_config=false
local has_language=false
local has_menu=false
if [ -f "$INSTALL_DIR/$MENU_SCRIPT" ]; then
has_menu=true
fi
if [ -d "$VENV_PATH" ] && [ -f "$VENV_PATH/bin/activate" ]; then
has_venv=true
fi
if [ -f "$CONFIG_FILE" ]; then
if jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
has_config=true
local current_language=$(jq -r '.language // empty' "$CONFIG_FILE" 2>/dev/null)
if [[ -n "$current_language" && "$current_language" != "null" && "$current_language" != "empty" ]]; then
has_language=true
fi
else
echo "Warning: Corrupted config file detected, removing..."
rm -f "$CONFIG_FILE"
fi
fi
if [ "$has_venv" = true ] && [ "$has_language" = true ]; then
echo "translation"
elif [ "$has_menu" = true ] && [ "$has_venv" = false ]; then
echo "normal"
elif [ "$has_menu" = true ]; then
echo "unknown"
else
echo "none"
fi
}
uninstall_proxmenux() {
local install_type="$1"
local force_clean="$2"
if [ "$force_clean" != "force" ]; then
if ! whiptail --title "Uninstall ProxMenux" --yesno "Are you sure you want to uninstall ProxMenux?" 10 60; then
return 1
fi
fi
echo "Uninstalling ProxMenux..."
if [ -f "$VENV_PATH/bin/activate" ]; then
echo "Removing googletrans and virtual environment..."
source "$VENV_PATH/bin/activate"
pip uninstall -y googletrans >/dev/null 2>&1
deactivate
rm -rf "$VENV_PATH"
fi
if [ "$install_type" = "translation" ] && [ "$force_clean" != "force" ]; then
DEPS_TO_REMOVE=$(whiptail --title "Remove Translation Dependencies" --checklist \
"Select translation-specific dependencies to remove:" 15 60 3 \
"python3-venv" "Python virtual environment" OFF \
"python3-pip" "Python package installer" OFF \
"python3" "Python interpreter" OFF \
3>&1 1>&2 2>&3)
if [ -n "$DEPS_TO_REMOVE" ]; then
echo "Removing selected dependencies..."
read -r -a DEPS_ARRAY <<< "$(echo "$DEPS_TO_REMOVE" | tr -d '"')"
for dep in "${DEPS_ARRAY[@]}"; do
echo "Removing $dep..."
apt-mark auto "$dep" >/dev/null 2>&1
apt-get -y --purge autoremove "$dep" >/dev/null 2>&1
done
apt-get autoremove -y --purge >/dev/null 2>&1
fi
fi
rm -f "$INSTALL_DIR/$MENU_SCRIPT"
rm -rf "$BASE_DIR"
[ -f /root/.bashrc.bak ] && mv /root/.bashrc.bak /root/.bashrc
if [ -f /etc/motd.bak ]; then
mv /etc/motd.bak /etc/motd
else
sed -i '/This system is optimised by: ProxMenux/d' /etc/motd
fi
echo "ProxMenux has been uninstalled."
return 0
}
handle_installation_change() {
local current_type="$1"
local new_type="$2"
if [ "$current_type" = "$new_type" ]; then
return 0
fi
case "$current_type-$new_type" in
"translation-1"|"translation-normal")
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_proxmenux "translation" "force" >/dev/null 2>&1
return 0
else
return 1
fi
;;
"normal-2"|"normal-translation")
if whiptail --title "Installation Type Change" \
--yesno "Switch from Normal to Translation Version?\n\nThis will add translation components." 10 60; then
return 0
else
return 1
fi
;;
*)
return 0
;;
esac
}
update_config() {
local component="$1"
local status="$2"
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# List of components we want to track
local tracked_components=("whiptail" "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")
# Check if the component is in the list of tracked components
if [[ " ${tracked_components[@]} " =~ " ${component} " ]]; then
mkdir -p "$(dirname "$CONFIG_FILE")"
if [ ! -f "$CONFIG_FILE" ]; then
if [ ! -f "$CONFIG_FILE" ] || ! jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
echo '{}' > "$CONFIG_FILE"
fi
tmp=$(mktemp)
local tmp_file=$(mktemp)
if jq --arg comp "$component" --arg stat "$status" --arg time "$timestamp" \
'.[$comp] = {status: $stat, timestamp: $time}' "$CONFIG_FILE" > "$tmp_file" 2>/dev/null; then
mv "$tmp_file" "$CONFIG_FILE"
else
echo '{}' > "$CONFIG_FILE"
jq --arg comp "$component" --arg stat "$status" --arg time "$timestamp" \
'.[$comp] = {status: $stat, timestamp: $time}' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE"
'.[$comp] = {status: $stat, timestamp: $time}' "$CONFIG_FILE" > "$tmp_file" && mv "$tmp_file" "$CONFIG_FILE"
fi
[ -f "$tmp_file" ] && rm -f "$tmp_file"
fi
}
show_progress() {
local step="$1"
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"
}
select_language() {
if [ -f "$CONFIG_FILE" ] && jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
local existing_language=$(jq -r '.language // empty' "$CONFIG_FILE" 2>/dev/null)
if [[ -n "$existing_language" && "$existing_language" != "null" && "$existing_language" != "empty" ]]; then
LANGUAGE="$existing_language"
msg_ok "Using existing language configuration: $LANGUAGE"
return 0
fi
fi
# # Main installation function =============================
LANGUAGE=$(whiptail --title "Select Language" --menu "Choose a language for the menu:" 20 60 12 \
"en" "English (Recommended)" \
"es" "Spanish" \
"fr" "French" \
"de" "German" \
"it" "Italian" \
"pt" "Portuguese" 3>&1 1>&2 2>&3)
install_proxmenu() {
local total_steps=4
if [ -z "$LANGUAGE" ]; then
msg_error "No language selected. Exiting."
exit 1
fi
mkdir -p "$(dirname "$CONFIG_FILE")"
if [ ! -f "$CONFIG_FILE" ] || ! jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
echo '{}' > "$CONFIG_FILE"
fi
local tmp_file=$(mktemp)
if jq --arg lang "$LANGUAGE" '. + {language: $lang}' "$CONFIG_FILE" > "$tmp_file" 2>/dev/null; then
mv "$tmp_file" "$CONFIG_FILE"
else
echo "{\"language\": \"$LANGUAGE\"}" > "$CONFIG_FILE"
fi
[ -f "$tmp_file" ] && rm -f "$tmp_file"
msg_ok "Language set to: $LANGUAGE"
}
# Show installation confirmation for new installations
show_installation_confirmation() {
local install_type="$1"
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• 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
fi
;;
"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• 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
fi
;;
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=4 # Increased from 3 to 4 for monitor installation
local current_step=1
# Step 1: Check and install system dependencies
show_progress $current_step $total_steps "Checking system dependencies"
show_progress $current_step $total_steps "Installing basic dependencies"
if ! dpkg -l | grep -qw "jq"; then
msg_info "Installing jq..."
@@ -116,8 +422,96 @@ install_proxmenu() {
update_config "jq" "already_installed"
fi
BASIC_DEPS=("dialog" "curl")
for pkg in "${BASIC_DEPS[@]}"; do
if ! dpkg -l | grep -qw "$pkg"; then
msg_info "Installing $pkg..."
if apt-get install -y "$pkg" > /dev/null 2>&1; then
msg_ok "$pkg installed successfully."
update_config "$pkg" "installed"
else
msg_error "Failed to install $pkg. Please install it manually."
update_config "$pkg" "failed"
return 1
fi
else
msg_ok "$pkg is already installed."
update_config "$pkg" "already_installed"
fi
done
DEPS=("whiptail" "dialog" "curl" "python3" "python3-venv" "python3-pip")
((current_step++))
show_progress $current_step $total_steps "Creating directories and configuration"
mkdir -p "$BASE_DIR"
mkdir -p "$INSTALL_DIR"
if [ ! -f "$CONFIG_FILE" ]; then
echo '{}' > "$CONFIG_FILE"
fi
msg_ok "Directories and configuration created."
((current_step++))
show_progress $current_step $total_steps "Downloading necessary files"
FILES=(
"$UTILS_FILE $REPO_URL/scripts/utils.sh"
"$INSTALL_DIR/$MENU_SCRIPT $REPO_URL/$MENU_SCRIPT"
"$LOCAL_VERSION_FILE $REPO_URL/version.txt"
)
for file in "${FILES[@]}"; do
IFS=" " read -r dest url <<< "$file"
msg_info "Downloading ${dest##*/}..."
sleep 2
if wget -qO "$dest" "$url"; then
msg_ok "${dest##*/} downloaded successfully."
else
msg_error "Failed to download ${dest##*/}. Check your Internet connection."
return 1
fi
done
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
((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=5 # Increased from 4 to 5 for monitor installation
local current_step=1
show_progress $current_step $total_steps "Language selection"
select_language
((current_step++))
show_progress $current_step $total_steps "Installing system dependencies"
if ! dpkg -l | grep -qw "jq"; then
msg_info "Installing jq..."
apt-get update > /dev/null 2>&1
if apt-get install -y jq > /dev/null 2>&1; then
msg_ok "jq installed successfully."
update_config "jq" "installed"
else
msg_error "Failed to install jq. Please install it manually."
update_config "jq" "failed"
return 1
fi
else
msg_ok "jq is already installed."
update_config "jq" "already_installed"
fi
DEPS=("dialog" "curl" "python3" "python3-venv" "python3-pip")
for pkg in "${DEPS[@]}"; do
if ! dpkg -l | grep -qw "$pkg"; then
msg_info "Installing $pkg..."
@@ -137,14 +531,11 @@ install_proxmenu() {
((current_step++))
show_progress $current_step $total_steps "Setting up translation environment"
# Step 2: Set up virtual environment
show_progress $current_step $total_steps "Setting up virtual environment for translate"
if [ ! -d "$VENV_PATH" ] || [ ! -f "$VENV_PATH/bin/activate" ]; then
msg_info "Creating the virtual environment..."
python3 -m venv --system-site-packages "$VENV_PATH" > /dev/null 2>&1
if [ ! -f "$VENV_PATH/bin/activate" ]; then
msg_error "Failed to create virtual environment. Please check your Python installation."
update_config "virtual_environment" "failed"
@@ -157,12 +548,9 @@ install_proxmenu() {
msg_ok "Virtual environment already exists."
update_config "virtual_environment" "already_exists"
fi
source "$VENV_PATH/bin/activate"
((current_step++))
# Step 3: Install and upgrade pip and googletrans
show_progress $current_step $total_steps "Installing and upgrading pip and googletrans"
msg_info "Upgrading pip..."
if pip install --upgrade pip > /dev/null 2>&1; then
msg_ok "Pip upgraded successfully."
@@ -183,12 +571,12 @@ install_proxmenu() {
deactivate
return 1
fi
deactivate
((current_step++))
# Step 4: Download necessary files
show_progress $current_step $total_steps "Downloading necessary files"
mkdir -p "$BASE_DIR"
mkdir -p "$INSTALL_DIR"
@@ -198,64 +586,137 @@ install_proxmenu() {
"$INSTALL_DIR/$MENU_SCRIPT $REPO_URL/$MENU_SCRIPT"
"$LOCAL_VERSION_FILE $REPO_URL/version.txt"
)
for file in "${FILES[@]}"; do
IFS=" " read -r dest url <<< "$file"
msg_info "Downloading ${dest##*/}..."
sleep 2
if wget -qO "$dest" "$url"; then
msg_ok "${dest##*/} downloaded successfully."
if [[ "$dest" == "$CACHE_FILE" ]]; then
msg_ok "Cache file updated with latest translations."
fi
else
msg_error "Failed to download ${dest##*/}. Check your Internet connection."
return 1
fi
done
((current_step++))
# Final setup
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
((current_step++))
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
# Installation complete ====================================
echo
#echo -e "${YW}╭─────────────────────────────────────────────────────╮${CL}"
#echo -e "${YW}│${CL} ${GN}🌟 ProxMenux has been installed successfull 🌟 ${CL} ${YW}│${CL}"
#echo -e "${YW}╰─────────────────────────────────────────────────────╯${CL}"
msg_title "ProxMenux has been installed successfull"
echo
echo -ne "${GN}"
type_text "To run ProxMenux, simply execute this command in the console or terminal:"
echo -e "${YWB} menu${CL}"
echo
if install_proxmenux_monitor; then
create_monitor_service
fi
}
####################################################
show_installation_options() {
local current_install_type
current_install_type=$(check_existing_installation)
local pve_version
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
local menu_title="ProxMenux Installation"
local menu_text="Choose installation type:"
if [ "$current_install_type" != "none" ]; then
case "$current_install_type" in
"translation")
menu_title="ProxMenux Update - Translation Version Detected"
;;
"normal")
menu_title="ProxMenux Update - Normal Version Detected"
;;
"unknown")
menu_title="ProxMenux Update - Existing Installation Detected"
;;
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)
if [ -z "$INSTALL_TYPE" ]; then
show_proxmenux_logo
msg_warn "Installation cancelled."
exit 1
fi
else
INSTALL_TYPE=$(whiptail --backtitle "ProxMenux" --title "$menu_title" --menu "\n$menu_text" 14 70 2 \
"1" "Normal Version (English only)" \
"2" "Translation Version (Multi-language support)" 3>&1 1>&2 2>&3)
if [ -z "$INSTALL_TYPE" ]; then
show_proxmenux_logo
msg_warn "Installation cancelled."
exit 1
fi
fi
if [ -z "$INSTALL_TYPE" ]; then
show_proxmenux_logo
msg_warn "Installation cancelled."
exit 1
fi
# For new installations, show confirmation with details
if [ "$current_install_type" = "none" ]; then
if ! show_installation_confirmation "$INSTALL_TYPE"; then
show_proxmenux_logo
msg_warn "Installation cancelled."
exit 1
fi
fi
if ! handle_installation_change "$current_install_type" "$INSTALL_TYPE"; then
show_proxmenux_logo
msg_warn "Installation cancelled."
exit 1
fi
}
install_proxmenu() {
show_installation_options
case "$INSTALL_TYPE" in
"1")
show_proxmenux_logo
msg_title "Installing ProxMenux - Normal Version"
install_normal_version
;;
"2")
show_proxmenux_logo
msg_title "Installing ProxMenux - Translation Version"
install_translation_version
;;
*)
msg_error "Invalid option selected."
exit 1
;;
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}"
echo
}
# Main execution ==========================================
if [ "$(id -u)" -ne 0 ]; then
msg_error "This script must be run as root."
exit 1
fi
clear
show_proxmenux_logo
echo
echo -e "${BOLD}${YW}To function correctly, ProxMenux needs to install the following components:${CL}"
echo -e "${TAB}- whiptail (if not already installed)"
echo -e "${TAB}- curl (if not already installed)"
echo -e "${TAB}- jq (if not already installed)"
echo -e "${TAB}- Python 3 (if not already installed)"
echo -e "${TAB}- Virtual environment for Google Translate"
echo -e "${TAB}- ProxMenux scripts and configuration files"
echo
read -p "Do you want to proceed with the installation? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]
then
install_proxmenu
else
msg_warn "Installation cancelled."
exit 1
fi
cleanup_corrupted_files
install_proxmenu

File diff suppressed because it is too large Load Diff

5819
json/helpers_cache.json Normal file

File diff suppressed because it is too large Load Diff

66
menu
View File

@@ -1,25 +1,25 @@
#!/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
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# Version : 1.1
# Last Updated: 04/07/2025
# ==========================================================
# Description:
# 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.
@@ -29,6 +29,7 @@
# for managing Proxmox VE using ProxMenux.
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
@@ -41,78 +42,37 @@ VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
# ==========================================================
#show_proxmenux_logo
# Initialize language configuration
initialize_config() {
show_proxmenux_logo
# Check if config file exists and has language field
if [ ! -f "$CONFIG_FILE" ] || [ -z "$(jq -r '.language // empty' "$CONFIG_FILE")" ]; then
LANGUAGE=$(whiptail --title "$(translate "Select Language")" --menu "$(translate "Choose a language for the menu:")" 20 60 12 \
"en" "$(translate "English (Recommended)")" \
"es" "$(translate "Spanish")" \
"fr" "$(translate "French")" \
"de" "$(translate "German")" \
"it" "$(translate "Italian")" \
"pt" "$(translate "Portuguese")" 3>&1 1>&2 2>&3)
if [ -z "$LANGUAGE" ]; then
msg_error "$(translate "No language selected. Exiting.")"
exit 1
fi
if [ -f "$CONFIG_FILE" ]; then
# Update existing config file with new language
tmp=$(mktemp)
jq --arg lang "$LANGUAGE" '. + {language: $lang}' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE"
else
# Create new config file if it doesn't exist
echo "{\"language\": \"$LANGUAGE\"}" > "$CONFIG_FILE"
fi
msg_ok "$(translate "Initial language set to:") $LANGUAGE"
fi
}
# =========================================================
check_updates() {
local INSTALL_SCRIPT="$BASE_DIR/install_proxmenux.sh"
# Fetch the remote version
local REMOTE_VERSION
REMOTE_VERSION=$(curl -fsSL "$REPO_URL/version.txt" | head -n 1)
# Exit silently if unable to fetch the remote version
if [ -z "$REMOTE_VERSION" ]; then
return 0
fi
# Read the local version
local LOCAL_VERSION
LOCAL_VERSION=$(head -n 1 "$LOCAL_VERSION_FILE")
# If the local version matches the remote version, no update is needed
[ "$LOCAL_VERSION" = "$REMOTE_VERSION" ] && return 0
# Prompt the user for update confirmation
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 ProxMenux update...")"
msg_warn "$(translate "Starting ProxMenu update...")"
# Download the installation script
if wget -qO "$INSTALL_SCRIPT" "$REPO_URL/install_proxmenux.sh"; then
chmod +x "$INSTALL_SCRIPT"
# Execute the script directly in the current environment
source "$INSTALL_SCRIPT"
fi
else
msg_warn "$(translate "Update postponed. You can update later from the menu.")"
@@ -126,8 +86,6 @@ main_menu() {
}
# Main flow
initialize_config
load_language
initialize_cache
check_updates

View File

@@ -0,0 +1,854 @@
#!/bin/bash
# ==========================================================
# ProxMenux - Complete Post-Installation Script with Registration
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.0
# Last Updated: 06/07/2025
# ==========================================================
# Description:
#
# The script performs system optimizations including:
# - Repository configuration and system upgrades
# - Subscription banner removal and UI enhancements
# - Advanced memory management and kernel optimizations
# - Network stack tuning and security hardening
# - Storage optimizations including log2ram for SSD protection
# - System limits increases and entropy generation improvements
# - Journald and logrotate optimizations for better log management
# - Security enhancements including RPC disabling and time synchronization
# - Bash environment customization and system monitoring setup
#
# Key Features:
# - Zero-interaction automation: Runs completely unattended
# - Intelligent hardware detection: Automatically detects SSD/NVMe for log2ram
# - RAM-aware configurations: Adjusts settings based on available system memory
# - Comprehensive error handling: Robust installation with fallback mechanisms
# - Registration system: Tracks installed optimizations for easy management
# - Reboot management: Intelligently handles reboot requirements
# - Translation support: Multi-language compatible through ProxMenux framework
# - Rollback compatibility: All optimizations can be reversed using the uninstall script
#
# This script is based on the post-install script cutotomizable
# ==========================================================
# 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
# Global variables
OS_CODENAME="$(grep "VERSION_CODENAME=" /etc/os-release | cut -d"=" -f 2 | xargs)"
RAM_SIZE_GB=$(( $(vmstat -s | grep -i "total memory" | xargs | cut -d" " -f 1) / 1024 / 1000))
NECESSARY_REBOOT=0
SCRIPT_TITLE="Customizable post-installation optimization script"
# ==========================================================
# Tool registration system
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"
}
# ==========================================================
lvm_repair_check() {
msg_info "$(translate "Checking and repairing old LVM PV headers (if needed)...")"
pvs_output=$(LC_ALL=C pvs -v 2>&1 | grep "old PV header")
if [ -z "$pvs_output" ]; then
msg_ok "$(translate "No PVs with old headers found.")"
register_tool "lvm_repair" true
return
fi
declare -A vg_map
while read -r line; do
pv=$(echo "$line" | grep -o '/dev/[^ ]*')
vg=$(pvs -o vg_name --noheadings "$pv" | awk '{print $1}')
if [ -n "$vg" ]; then
vg_map["$vg"]=1
fi
done <<< "$pvs_output"
for vg in "${!vg_map[@]}"; do
msg_warn "$(translate "Old PV header(s) found in VG $vg. Updating metadata...")"
vgck --updatemetadata "$vg"
vgchange -ay "$vg"
if [ $? -ne 0 ]; then
msg_warn "$(translate "Metadata update failed for VG $vg. Review manually.")"
else
msg_ok "$(translate "Metadata updated successfully for VG $vg")"
fi
done
msg_ok "$(translate "LVM PV headers check completed")"
}
# ==========================================================
cleanup_duplicate_repos() {
local sources_file="/etc/apt/sources.list"
local temp_file=$(mktemp)
local cleaned_count=0
declare -A seen_repos
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "$line" ]]; then
echo "$line" >> "$temp_file"
continue
fi
if [[ "$line" =~ ^deb ]]; then
read -r _ url dist components <<< "$line"
local key="${url}_${dist}"
if [[ -v "seen_repos[$key]" ]]; then
echo "# $line" >> "$temp_file"
cleaned_count=$((cleaned_count + 1))
else
echo "$line" >> "$temp_file"
seen_repos[$key]="$components"
fi
else
echo "$line" >> "$temp_file"
fi
done < "$sources_file"
mv "$temp_file" "$sources_file"
chmod 644 "$sources_file"
local pve_files=(/etc/apt/sources.list.d/*proxmox*.list /etc/apt/sources.list.d/*pve*.list)
local pve_content="deb http://download.proxmox.com/debian/pve ${OS_CODENAME} pve-no-subscription"
local pve_public_repo="/etc/apt/sources.list.d/pve-public-repo.list"
local pve_public_repo_exists=false
if [ -f "$pve_public_repo" ] && grep -q "^deb.*pve-no-subscription" "$pve_public_repo"; then
pve_public_repo_exists=true
fi
for file in "${pve_files[@]}"; do
if [ -f "$file" ] && grep -q "^deb.*pve-no-subscription" "$file"; then
if ! $pve_public_repo_exists && [[ "$file" == "$pve_public_repo" ]]; then
sed -i 's/^# *deb/deb/' "$file"
pve_public_repo_exists=true
elif [[ "$file" != "$pve_public_repo" ]]; then
sed -i 's/^deb/# deb/' "$file"
cleaned_count=$((cleaned_count + 1))
fi
fi
done
apt update
}
apt_upgrade() {
NECESSARY_REBOOT=1
if [ -f /etc/apt/sources.list.d/pve-enterprise.list ] && grep -q "^deb" /etc/apt/sources.list.d/pve-enterprise.list; then
msg_info "$(translate "Disabling enterprise Proxmox repository...")"
sed -i "s/^deb/#deb/g" /etc/apt/sources.list.d/pve-enterprise.list
msg_ok "$(translate "Enterprise Proxmox repository disabled")"
fi
if [ -f /etc/apt/sources.list.d/ceph.list ] && grep -q "^deb" /etc/apt/sources.list.d/ceph.list; then
msg_info "$(translate "Disabling enterprise Proxmox Ceph repository...")"
sed -i "s/^deb/#deb/g" /etc/apt/sources.list.d/ceph.list
msg_ok "$(translate "Enterprise Proxmox Ceph repository disabled")"
fi
if [ ! -f /etc/apt/sources.list.d/pve-public-repo.list ] || ! grep -q "pve-no-subscription" /etc/apt/sources.list.d/pve-public-repo.list; then
msg_info "$(translate "Enabling free public Proxmox repository...")"
echo "deb http://download.proxmox.com/debian/pve ${OS_CODENAME} pve-no-subscription" > /etc/apt/sources.list.d/pve-public-repo.list
msg_ok "$(translate "Free public Proxmox repository enabled")"
fi
sources_file="/etc/apt/sources.list"
need_update=false
sed -i 's|ftp.es.debian.org|deb.debian.org|g' "$sources_file"
if grep -q "^deb http://security.debian.org ${OS_CODENAME}-security main contrib" "$sources_file"; then
sed -i "s|^deb http://security.debian.org ${OS_CODENAME}-security main contrib|deb http://security.debian.org/debian-security ${OS_CODENAME}-security main contrib non-free non-free-firmware|" "$sources_file"
msg_ok "$(translate "Replaced security repository with full version")"
need_update=true
fi
if ! grep -q "deb http://security.debian.org/debian-security ${OS_CODENAME}-security" "$sources_file"; then
echo "deb http://security.debian.org/debian-security ${OS_CODENAME}-security main contrib non-free non-free-firmware" >> "$sources_file"
need_update=true
fi
if ! grep -q "deb http://deb.debian.org/debian ${OS_CODENAME} " "$sources_file"; then
echo "deb http://deb.debian.org/debian ${OS_CODENAME} main contrib non-free non-free-firmware" >> "$sources_file"
need_update=true
fi
if ! grep -q "deb http://deb.debian.org/debian ${OS_CODENAME}-updates" "$sources_file"; then
echo "deb http://deb.debian.org/debian ${OS_CODENAME}-updates main contrib non-free non-free-firmware" >> "$sources_file"
need_update=true
fi
msg_ok "$(translate "Debian repositories configured correctly")"
# ===================================================
if [ ! -f /etc/apt/apt.conf.d/no-bookworm-firmware.conf ]; then
msg_info "$(translate "Disabling non-free firmware warnings...")"
echo 'APT::Get::Update::SourceListWarnings::NonFreeFirmware "false";' > /etc/apt/apt.conf.d/no-bookworm-firmware.conf
msg_ok "$(translate "Non-free firmware warnings disabled")"
fi
msg_info "$(translate "Updating package lists...")"
if apt-get update > /dev/null 2>&1; then
msg_ok "$(translate "Package lists updated")"
else
msg_error "$(translate "Failed to update package lists")"
return 1
fi
msg_info "$(translate "Removing conflicting utilities...")"
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")"
else
msg_error "$(translate "Failed to remove conflicting utilities")"
fi
msg_info "$(translate "Performing packages upgrade...")"
apt-get install pv -y > /dev/null 2>&1
total_packages=$(apt-get -s dist-upgrade | grep "^Inst" | wc -l)
if [ "$total_packages" -eq 0 ]; then
total_packages=1
fi
msg_ok "$(translate "Packages upgrade successfull")"
tput civis
tput sc
(
/usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' dist-upgrade 2>&1 | \
while IFS= read -r line; do
if [[ "$line" =~ ^(Setting up|Unpacking|Preparing to unpack|Processing triggers for) ]]; then
package_name=$(echo "$line" | sed -E 's/.*(Setting up|Unpacking|Preparing to unpack|Processing triggers for) ([^ ]+).*/\2/')
[ -z "$package_name" ] && package_name="$(translate "Unknown")"
tput rc
tput ed
row=$(( $(tput lines) - 6 ))
tput cup $row 0; echo "$(translate "Installing packages...")"
tput cup $((row + 1)) 0; echo "──────────────────────────────────────────────"
tput cup $((row + 2)) 0; echo "Package: $package_name"
tput cup $((row + 3)) 0; echo "Progress: [ ] 0%"
tput cup $((row + 4)) 0; echo "──────────────────────────────────────────────"
for i in $(seq 1 10); do
progress=$((i * 10))
tput cup $((row + 3)) 9
printf "[%-50s] %3d%%" "$(printf "#%.0s" $(seq 1 $((progress/2))))" "$progress"
done
fi
done
)
if [ $? -eq 0 ]; then
tput rc
tput ed
msg_ok "$(translate "System upgrade completed")"
fi
msg_info "$(translate "Installing additional Proxmox packages...")"
if /usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' install zfsutils-linux proxmox-backup-restore-image chrony > /dev/null 2>&1; then
msg_ok "$(translate "Additional Proxmox packages installed")"
else
msg_error "$(translate "Failed to install additional Proxmox packages")"
fi
lvm_repair_check
cleanup_duplicate_repos
msg_ok "$(translate "Proxmox update completed")"
}
# ==========================================================
remove_subscription_banner() {
msg_info "$(translate "Removing Proxmox subscription nag banner...")"
local JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
local GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
local APT_HOOK="/etc/apt/apt.conf.d/no-nag-script"
if [[ ! -f "$APT_HOOK" ]]; then
cat <<'EOF' > "$APT_HOOK"
DPkg::Post-Invoke { "dpkg -V proxmox-widget-toolkit | grep -q '/proxmoxlib\.js$'; if [ $? -eq 1 ]; then { echo 'Removing subscription nag from UI...'; sed -i '/.*data\.status.*{/{s/\!//;s/active/NoMoreNagging/;s/Active/NoMoreNagging/}' /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js; rm -f /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz; }; fi"; };
EOF
fi
if [[ -f "$JS_FILE" ]]; then
sed -i '/.*data\.status.*{/{s/\!//;s/active/NoMoreNagging/;s/Active/NoMoreNagging/}' "$JS_FILE"
[[ -f "$GZ_FILE" ]] && rm -f "$GZ_FILE"
touch "$JS_FILE"
fi
apt --reinstall install proxmox-widget-toolkit -y > /dev/null 2>&1
msg_ok "$(translate "Subscription nag banner removed successfully")"
register_tool "subscription_banner" true
}
# ==========================================================
configure_time_sync() {
msg_info "$(translate "Configuring system time settings...")"
this_ip=$(dig +short myip.opendns.com @resolver1.opendns.com)
if [ -z "$this_ip" ]; then
msg_warn "$(translate "Failed to obtain public IP address")"
timezone="UTC"
else
timezone=$(curl -s "https://ipapi.co/${this_ip}/timezone")
if [ -z "$timezone" ]; then
msg_warn "$(translate "Failed to determine timezone from IP address")"
timezone="UTC"
else
msg_ok "$(translate "Found timezone $timezone for IP $this_ip")"
fi
fi
msg_info "$(translate "Enabling automatic time synchronization...")"
if timedatectl set-ntp true; then
msg_ok "$(translate "Time settings configured - Timezone:") $timezone"
register_tool "time_sync" true
else
msg_error "$(translate "Failed to enable automatic time synchronization")"
fi
}
# ==========================================================
skip_apt_languages() {
msg_info "$(translate "Configuring APT to skip downloading additional languages...")"
local default_locale=""
if [ -f /etc/default/locale ]; then
default_locale=$(grep '^LANG=' /etc/default/locale | cut -d= -f2 | tr -d '"')
elif [ -f /etc/environment ]; then
default_locale=$(grep '^LANG=' /etc/environment | cut -d= -f2 | tr -d '"')
fi
default_locale="${default_locale:-en_US.UTF-8}"
local normalized_locale=$(echo "$default_locale" | tr 'A-Z' 'a-z' | sed 's/utf-8/utf8/;s/-/_/')
if ! locale -a | grep -qi "^$normalized_locale$"; then
if ! grep -qE "^${default_locale}[[:space:]]+UTF-8" /etc/locale.gen; then
echo "$default_locale UTF-8" >> /etc/locale.gen
fi
locale-gen "$default_locale" > /dev/null 2>&1
fi
echo 'Acquire::Languages "none";' > /etc/apt/apt.conf.d/99-disable-translations
msg_ok "$(translate "APT configured to skip additional languages")"
register_tool "apt_languages" true
}
# ==========================================================
optimize_journald() {
msg_info "$(translate "Limiting size and optimizing journald...")"
NECESSARY_REBOOT=1
cat <<EOF > /etc/systemd/journald.conf
[Journal]
Storage=persistent
SplitMode=none
RateLimitInterval=0
RateLimitIntervalSec=0
RateLimitBurst=0
ForwardToSyslog=no
ForwardToWall=yes
Seal=no
Compress=yes
SystemMaxUse=64M
RuntimeMaxUse=60M
MaxLevelStore=warning
MaxLevelSyslog=warning
MaxLevelKMsg=warning
MaxLevelConsole=notice
MaxLevelWall=crit
EOF
systemctl restart systemd-journald.service > /dev/null 2>&1
journalctl --vacuum-size=64M --vacuum-time=1d > /dev/null 2>&1
journalctl --rotate > /dev/null 2>&1
msg_ok "$(translate "Journald optimized - Max size: 64M")"
register_tool "journald" true
}
# ==========================================================
optimize_logrotate() {
msg_info "$(translate "Optimizing logrotate configuration...")"
local logrotate_conf="/etc/logrotate.conf"
local backup_conf="${logrotate_conf}.bak"
if ! grep -q "# ProxMenux optimized configuration" "$logrotate_conf"; then
cp "$logrotate_conf" "$backup_conf"
cat <<EOF > "$logrotate_conf"
# ProxMenux optimized configuration
daily
su root adm
rotate 7
create
compress
size=10M
delaycompress
copytruncate
include /etc/logrotate.d
EOF
systemctl restart logrotate > /dev/null 2>&1
fi
msg_ok "$(translate "Logrotate optimization completed")"
register_tool "logrotate" true
}
# ==========================================================
increase_system_limits() {
msg_info "$(translate "Increasing various system limits...")"
NECESSARY_REBOOT=1
cat > /etc/sysctl.d/99-maxwatches.conf << EOF
# ProxMenux configuration
fs.inotify.max_user_watches = 1048576
fs.inotify.max_user_instances = 1048576
fs.inotify.max_queued_events = 1048576
EOF
cat > /etc/security/limits.d/99-limits.conf << EOF
# ProxMenux configuration
* soft nproc 1048576
* hard nproc 1048576
* soft nofile 1048576
* hard nofile 1048576
root soft nproc unlimited
root hard nproc unlimited
root soft nofile unlimited
root hard nofile unlimited
EOF
cat > /etc/sysctl.d/99-maxkeys.conf << EOF
# ProxMenux configuration
kernel.keys.root_maxkeys=1000000
kernel.keys.maxkeys=1000000
EOF
for file in /etc/systemd/system.conf /etc/systemd/user.conf; do
if ! grep -q "^DefaultLimitNOFILE=" "$file"; then
echo "DefaultLimitNOFILE=256000" >> "$file"
fi
done
for file in /etc/pam.d/common-session /etc/pam.d/runuser-l; do
if ! grep -q "^session required pam_limits.so" "$file"; then
echo 'session required pam_limits.so' >> "$file"
fi
done
if ! grep -q "ulimit -n 256000" /root/.profile; then
echo "ulimit -n 256000" >> /root/.profile
fi
cat > /etc/sysctl.d/99-swap.conf << EOF
# ProxMenux configuration
vm.swappiness = 10
vm.vfs_cache_pressure = 100
EOF
cat > /etc/sysctl.d/99-fs.conf << EOF
# ProxMenux configuration
fs.nr_open = 12000000
fs.file-max = 9223372036854775807
fs.aio-max-nr = 1048576
EOF
msg_ok "$(translate "System limits increase completed.")"
register_tool "system_limits" true
}
# ==========================================================
configure_entropy() {
msg_info "$(translate "Configuring entropy generation to prevent slowdowns...")"
/usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' install haveged > /dev/null 2>&1
cat <<EOF > /etc/default/haveged
# -w sets low entropy watermark (in bits)
DAEMON_ARGS="-w 1024"
EOF
systemctl daemon-reload > /dev/null 2>&1
systemctl enable haveged > /dev/null 2>&1
msg_ok "$(translate "Entropy generation configuration completed")"
register_tool "entropy" true
}
# ==========================================================
optimize_memory_settings() {
msg_info "$(translate "Optimizing memory settings...")"
NECESSARY_REBOOT=1
cat <<EOF > /etc/sysctl.d/99-memory.conf
# Balanced Memory Optimization
vm.swappiness = 10
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
vm.overcommit_memory = 1
vm.max_map_count = 65530
EOF
if [ -f /proc/sys/vm/compaction_proactiveness ]; then
echo "vm.compaction_proactiveness = 20" >> /etc/sysctl.d/99-memory.conf
fi
msg_ok "$(translate "Memory optimization completed.")"
register_tool "memory_settings" true
}
# ==========================================================
configure_kernel_panic() {
msg_info "$(translate "Configuring kernel panic behavior")"
NECESSARY_REBOOT=1
cat <<EOF > /etc/sysctl.d/99-kernelpanic.conf
# Enable restart on kernel panic, kernel oops and hardlockup
kernel.core_pattern = /var/crash/core.%t.%p
kernel.panic = 10
kernel.panic_on_oops = 1
kernel.hardlockup_panic = 1
EOF
msg_ok "$(translate "Kernel panic behavior configuration completed")"
register_tool "kernel_panic" true
}
# ==========================================================
force_apt_ipv4() {
msg_info "$(translate "Configuring APT to use IPv4...")"
echo 'Acquire::ForceIPv4 "true";' > /etc/apt/apt.conf.d/99-force-ipv4
msg_ok "$(translate "APT IPv4 configuration completed")"
register_tool "apt_ipv4" true
}
# ==========================================================
apply_network_optimizations() {
msg_info "$(translate "Optimizing network settings...")"
NECESSARY_REBOOT=1
cat <<EOF > /etc/sysctl.d/99-network.conf
net.core.netdev_max_backlog=8192
net.core.optmem_max=8192
net.core.rmem_max=16777216
net.core.somaxconn=8151
net.core.wmem_max=16777216
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.log_martians = 0
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.default.log_martians = 0
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.ip_local_port_range=1024 65535
net.ipv4.tcp_base_mss = 1024
net.ipv4.tcp_challenge_ack_limit = 999999999
net.ipv4.tcp_fin_timeout=10
net.ipv4.tcp_keepalive_intvl=30
net.ipv4.tcp_keepalive_probes=3
net.ipv4.tcp_keepalive_time=240
net.ipv4.tcp_limit_output_bytes=65536
net.ipv4.tcp_max_syn_backlog=8192
net.ipv4.tcp_max_tw_buckets = 1440000
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_rfc1337=1
net.ipv4.tcp_rmem=8192 87380 16777216
net.ipv4.tcp_sack=1
net.ipv4.tcp_slow_start_after_idle=0
net.ipv4.tcp_syn_retries=3
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_tw_recycle = 0
net.ipv4.tcp_tw_reuse = 0
net.ipv4.tcp_wmem=8192 65536 16777216
net.netfilter.nf_conntrack_generic_timeout = 60
net.netfilter.nf_conntrack_helper=0
net.netfilter.nf_conntrack_max = 524288
net.netfilter.nf_conntrack_tcp_timeout_established = 28800
net.unix.max_dgram_qlen = 4096
EOF
sysctl --system > /dev/null 2>&1
local interfaces_file="/etc/network/interfaces"
if ! grep -q 'source /etc/network/interfaces.d/*' "$interfaces_file"; then
echo "source /etc/network/interfaces.d/*" >> "$interfaces_file"
fi
msg_ok "$(translate "Network optimization completed")"
register_tool "network_optimization" true
}
# ==========================================================
disable_rpc() {
msg_info "$(translate "Disabling portmapper/rpcbind for security...")"
systemctl disable rpcbind > /dev/null 2>&1
systemctl stop rpcbind > /dev/null 2>&1
msg_ok "$(translate "portmapper/rpcbind has been disabled and removed")"
register_tool "disable_rpc" true
}
# ==========================================================
customize_bashrc() {
msg_info "$(translate "Customizing bashrc for root user...")"
local bashrc="/root/.bashrc"
local bash_profile="/root/.bash_profile"
if [ ! -f "${bashrc}.bak" ]; then
cp "$bashrc" "${bashrc}.bak"
fi
cat >> "$bashrc" << 'EOF'
# ProxMenux customizations
export HISTTIMEFORMAT="%d/%m/%y %T "
export PS1="\[\e[31m\][\[\e[m\]\[\e[38;5;172m\]\u\[\e[m\]@\[\e[38;5;153m\]\h\[\e[m\] \[\e[38;5;214m\]\W\[\e[m\]\[\e[31m\]]\[\e[m\]\\$ "
alias l='ls -CF'
alias la='ls -A'
alias ll='ls -alF'
alias ls='ls --color=auto'
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
source /etc/profile.d/bash_completion.sh
EOF
if ! grep -q "source /root/.bashrc" "$bash_profile"; then
echo "source /root/.bashrc" >> "$bash_profile"
fi
msg_ok "$(translate "Bashrc customization completed")"
register_tool "bashrc_custom" true
}
# ==========================================================
install_log2ram_auto() {
msg_info "$(translate "Checking if system disk is SSD or M.2...")"
ROOT_PART=$(lsblk -no NAME,MOUNTPOINT | grep ' /$' | awk '{print $1}')
SYSTEM_DISK=$(lsblk -no PKNAME /dev/$ROOT_PART 2>/dev/null)
SYSTEM_DISK=${SYSTEM_DISK:-sda}
if [[ "$SYSTEM_DISK" == nvme* || "$(cat /sys/block/$SYSTEM_DISK/queue/rotational 2>/dev/null)" == "0" ]]; then
msg_ok "$(translate "System disk ($SYSTEM_DISK) is SSD or M.2. Proceeding with log2ram setup.")"
else
msg_warn "$(translate "System disk ($SYSTEM_DISK) is not SSD/M.2. Skipping log2ram installation.")"
return 0
fi
# Clean up previous state
rm -rf /tmp/log2ram
rm -f /etc/systemd/system/log2ram*
rm -f /etc/systemd/system/log2ram-daily.*
rm -f /etc/cron.d/log2ram*
rm -f /usr/sbin/log2ram
rm -f /etc/log2ram.conf
rm -f /usr/local/bin/log2ram-check.sh
rm -rf /var/log.hdd
systemctl daemon-reexec >/dev/null 2>&1
systemctl daemon-reload >/dev/null 2>&1
msg_info "$(translate "Installing log2ram from GitHub...")"
git clone https://github.com/azlux/log2ram.git /tmp/log2ram >/dev/null 2>>/tmp/log2ram_install.log
cd /tmp/log2ram || return 1
bash install.sh >>/tmp/log2ram_install.log 2>&1
if [[ -f /etc/log2ram.conf ]] && systemctl list-units --all | grep -q log2ram; then
msg_ok "$(translate "log2ram installed successfully")"
else
msg_error "$(translate "Failed to install log2ram. See /tmp/log2ram_install.log")"
return 1
fi
# Detect RAM
RAM_SIZE_GB=$(free -g | awk '/^Mem:/{print $2}')
[[ -z "$RAM_SIZE_GB" || "$RAM_SIZE_GB" -eq 0 ]] && RAM_SIZE_GB=4
if (( RAM_SIZE_GB <= 8 )); then
LOG2RAM_SIZE="128M"
CRON_HOURS=1
elif (( RAM_SIZE_GB <= 16 )); then
LOG2RAM_SIZE="256M"
CRON_HOURS=3
else
LOG2RAM_SIZE="512M"
CRON_HOURS=6
fi
msg_ok "$(translate "Detected RAM:") $RAM_SIZE_GB GB — $(translate "log2ram size set to:") $LOG2RAM_SIZE"
sed -i "s/^SIZE=.*/SIZE=$LOG2RAM_SIZE/" /etc/log2ram.conf
rm -f /etc/cron.hourly/log2ram
echo "0 */$CRON_HOURS * * * root /usr/sbin/log2ram write" > /etc/cron.d/log2ram
msg_ok "$(translate "log2ram write scheduled every") $CRON_HOURS $(translate "hour(s)")"
cat << 'EOF' > /usr/local/bin/log2ram-check.sh
#!/bin/bash
CONF_FILE="/etc/log2ram.conf"
LIMIT_KB=$(grep '^SIZE=' "$CONF_FILE" | cut -d'=' -f2 | tr -d 'M')000
USED_KB=$(df /var/log --output=used | tail -1)
THRESHOLD=$(( LIMIT_KB * 90 / 100 ))
if (( USED_KB > THRESHOLD )); then
/usr/sbin/log2ram write
fi
EOF
chmod +x /usr/local/bin/log2ram-check.sh
echo "*/5 * * * * root /usr/local/bin/log2ram-check.sh" > /etc/cron.d/log2ram-auto-sync
msg_ok "$(translate "Auto-sync enabled when /var/log exceeds 90% of") $LOG2RAM_SIZE"
register_tool "log2ram" true
}
# ==========================================================
run_complete_optimization() {
clear
show_proxmenux_logo
msg_title "$(translate "ProxMenux Optimization Post-Installation")"
ensure_tools_json
apt_upgrade
remove_subscription_banner
configure_time_sync
skip_apt_languages
optimize_journald
optimize_logrotate
increase_system_limits
configure_entropy
optimize_memory_settings
configure_kernel_panic
force_apt_ipv4
apply_network_optimizations
disable_rpc
customize_bashrc
install_log2ram_auto
echo -e
msg_success "$(translate "Complete post-installation optimization finished!")"
if [[ "$NECESSARY_REBOOT" -eq 1 ]]; then
whiptail --title "Reboot Required" \
--yesno "$(translate "Some changes require a reboot to take effect. Do you want to restart now?")" 10 60
if [[ $? -eq 0 ]]; then
msg_info "$(translate "Removing no longer required packages and purging old cached updates...")"
apt-get -y autoremove >/dev/null 2>&1
apt-get -y autoclean >/dev/null 2>&1
msg_ok "$(translate "Cleanup finished")"
msg_success "$(translate "Press Enter to continue...")"
read -r
msg_warn "$(translate "Rebooting the system...")"
reboot
else
msg_info "$(translate "Removing no longer required packages and purging old cached updates...")"
apt-get -y autoremove >/dev/null 2>&1
apt-get -y autoclean >/dev/null 2>&1
msg_ok "$(translate "Cleanup finished")"
msg_info2 "$(translate "You can reboot later manually.")"
msg_success "$(translate "Press Enter to continue...")"
read -r
exit 0
fi
fi
msg_success "$(translate "All changes applied. No reboot required.")"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
clear
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
run_complete_optimization
fi

View File

@@ -0,0 +1,917 @@
#!/usr/bin/env bash
# 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
# ==========================================================
get_external_backup_mount_point() {
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
local STORAGE_REPO="$REPO_URL/scripts/backup_restore"
local MOUNT_POINT
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
MOUNT_POINT=$(head -n1 "$BACKUP_MOUNT_FILE" | tr -d '\r\n' | xargs)
>&2 echo "DEBUG: Valor MOUNT_POINT='$MOUNT_POINT'"
if [[ ! -d "$MOUNT_POINT" ]]; then
msg_error "Mount point does not exist: $MOUNT_POINT"
rm -f "$BACKUP_MOUNT_FILE"
return 1
fi
if ! mountpoint -q "$MOUNT_POINT"; then
msg_error "Mount point is not mounted: $MOUNT_POINT"
rm -f "$BACKUP_MOUNT_FILE"
return 1
fi
echo "$MOUNT_POINT"
return 0
else
source <(curl -s "$STORAGE_REPO/mount_disk_host_bk.sh")
MOUNT_POINT=$(mount_disk_host_bk)
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
echo "$MOUNT_POINT"
return 0
fi
}
# === Host Backup Main Menu ===
host_backup_menu() {
while true; do
local CHOICE
CHOICE=$(dialog --backtitle "ProxMenux" \
--title "$(translate 'Host Backup')" \
--menu "\n$(translate 'Select backup option:')" 22 70 12 \
"" "$(translate '--- FULL BACKUP ---')" \
1 "$(translate 'Full backup to Proxmox Backup Server (PBS)')" \
2 "$(translate 'Full backup with BorgBackup')" \
3 "$(translate 'Full backup to local .tar.gz')" \
"" "$(translate '--- CUSTOM BACKUP ---')" \
4 "$(translate 'Custom backup to PBS')" \
5 "$(translate 'Custom backup with BorgBackup')" \
6 "$(translate 'Custom backup to local .tar.gz')" \
0 "$(translate 'Return')" \
3>&1 1>&2 2>&3) || return 0
case "$CHOICE" in
1) backup_full_pbs_root ;;
2) backup_with_borg "/boot/efi /etc/pve /etc/network /var/lib/pve-cluster /root /etc/ssh /home /usr/local/bin /etc/cron.d /etc/systemd/system /var/lib/vz" ;;
3) backup_to_local_tar "/boot/efi /etc/pve /etc/network /var/lib/pve-cluster /root /etc/ssh /home /usr/local/bin /etc/cron.d /etc/systemd/system /var/lib/vz" ;;
4) custom_backup_menu backup_to_pbs ;;
5) custom_backup_menu backup_with_borg ;;
6) custom_backup_menu backup_to_local_tar ;;
0) break ;;
esac
done
}
# === Menu checklist for custom backup ===
custom_backup_menu() {
declare -A BACKUP_PATHS=(
[etc-pve]="/etc/pve"
[etc-network]="/etc/network"
[var-lib-pve-cluster]="/var/lib/pve-cluster"
[root-dir]="/root"
[etc-ssh]="/etc/ssh"
[home]="/home"
[local-bin]="/usr/local/bin"
[cron]="/etc/cron.d"
[custom-systemd]="/etc/systemd/system"
[var-lib-vz]="/var/lib/vz"
)
local CHECKLIST_OPTIONS=()
for KEY in "${!BACKUP_PATHS[@]}"; do
DIR="${BACKUP_PATHS[$KEY]}"
CHECKLIST_OPTIONS+=("$KEY" "$DIR" "off")
done
SELECTED_KEYS=$(dialog --separate-output --checklist \
"$(translate 'Select directories to backup:')" 22 70 12 \
"${CHECKLIST_OPTIONS[@]}" \
3>&1 1>&2 2>&3) || return 1
local BACKUP_DIRS=()
for KEY in $SELECTED_KEYS; do
BACKUP_DIRS+=("${BACKUP_PATHS[$KEY]}")
done
# "$1" "${BACKUP_DIRS[*]}"
"$1" "${BACKUP_DIRS[@]}"
}
# === Configure PBS ===
configure_pbs_repository() {
local PBS_REPO_FILE="/usr/local/share/proxmenux/pbs-repo.conf"
local PBS_PASS_FILE="/usr/local/share/proxmenux/pbs-pass.txt"
local PBS_MANUAL_CONFIGS="/usr/local/share/proxmenux/pbs-manual-configs.txt"
[[ ! -f "$PBS_MANUAL_CONFIGS" ]] && touch "$PBS_MANUAL_CONFIGS"
local PBS_CONFIGS=()
local PBS_SOURCES=()
if [[ -f "/etc/pve/storage.cfg" ]]; then
local current_pbs="" server="" datastore="" username=""
while IFS= read -r line; do
if [[ $line =~ ^pbs:\ (.+)$ ]]; then
if [[ -n "$current_pbs" && -n "$server" && -n "$datastore" && -n "$username" ]]; then
PBS_CONFIGS+=("$current_pbs|$username@$server:$datastore")
PBS_SOURCES+=("proxmox|$current_pbs")
fi
current_pbs="${BASH_REMATCH[1]}"
server="" datastore="" username=""
elif [[ -n "$current_pbs" ]]; then
if [[ $line =~ ^[[:space:]]*server[[:space:]]+(.+)$ ]]; then
server="${BASH_REMATCH[1]}"
elif [[ $line =~ ^[[:space:]]*datastore[[:space:]]+(.+)$ ]]; then
datastore="${BASH_REMATCH[1]}"
elif [[ $line =~ ^[[:space:]]*username[[:space:]]+(.+)$ ]]; then
username="${BASH_REMATCH[1]}"
elif [[ $line =~ ^[a-zA-Z]+: ]]; then
if [[ -n "$server" && -n "$datastore" && -n "$username" ]]; then
PBS_CONFIGS+=("$current_pbs|$username@$server:$datastore")
PBS_SOURCES+=("proxmox|$current_pbs")
fi
current_pbs=""
fi
fi
done < "/etc/pve/storage.cfg"
if [[ -n "$current_pbs" && -n "$server" && -n "$datastore" && -n "$username" ]]; then
PBS_CONFIGS+=("$current_pbs|$username@$server:$datastore")
PBS_SOURCES+=("proxmox|$current_pbs")
fi
fi
if [[ -f "$PBS_MANUAL_CONFIGS" ]]; then
while IFS= read -r line; do
if [[ -n "$line" ]]; then
PBS_CONFIGS+=("$line")
local name="${line%%|*}"
PBS_SOURCES+=("manual|$name")
fi
done < "$PBS_MANUAL_CONFIGS"
fi
local menu_options=()
local i=1
for j in "${!PBS_CONFIGS[@]}"; do
local config="${PBS_CONFIGS[$j]}"
local source="${PBS_SOURCES[$j]}"
local name="${config%%|*}"
local repo="${config##*|}"
local source_type="${source%%|*}"
if [[ "$source_type" == "proxmox" ]]; then
menu_options+=("$i" " $name ($repo) [Proxmox]")
else
menu_options+=("$i" " $name ($repo) [Manual]")
fi
((i++))
done
menu_options+=("" "")
menu_options+=("$i" "\Z4\Zb $(translate 'Configure new PBS')\Zn")
local choice
choice=$(dialog --colors --backtitle "ProxMenux" --title "PBS Server Selection" \
--menu "\n$(translate 'Select PBS server for this backup:')" 22 70 12 "${menu_options[@]}" 3>&1 1>&2 2>&3)
local dialog_result=$?
clear
if [[ $dialog_result -ne 0 ]]; then
return 1
fi
if [[ $choice -eq $i ]]; then
configure_pbs_manually
else
local selected_config="${PBS_CONFIGS[$((choice-1))]}"
local selected_source="${PBS_SOURCES[$((choice-1))]}"
local pbs_name="${selected_config%%|*}"
local source_type="${selected_source%%|*}"
PBS_REPO="${selected_config##*|}"
{
mkdir -p "$(dirname "$PBS_REPO_FILE")"
echo "$PBS_REPO" > "$PBS_REPO_FILE"
} >/dev/null 2>&1
local password_found=false
if [[ "$source_type" == "proxmox" ]]; then
local password_file="/etc/pve/priv/storage/${pbs_name}.pw"
if [[ -f "$password_file" ]]; then
{
cp "$password_file" "$PBS_PASS_FILE"
chmod 600 "$PBS_PASS_FILE"
} >/dev/null 2>&1
password_found=true
fi
else
local manual_pass_file="/usr/local/share/proxmenux/pbs-pass-${pbs_name}.txt"
if [[ -f "$manual_pass_file" ]]; then
{
cp "$manual_pass_file" "$PBS_PASS_FILE"
chmod 600 "$PBS_PASS_FILE"
} >/dev/null 2>&1
password_found=true
dialog --backtitle "ProxMenux" --title "PBS Selected" --msgbox "$(translate 'Using manual PBS:') $pbs_name\n\n$(translate 'Repository:') $PBS_REPO\n$(translate 'Password:') $(translate 'Previously saved')" 12 80
fi
fi
if ! $password_found; then
dialog --backtitle "ProxMenux" --title "Password Required" --msgbox "$(translate 'Password not found for:') $pbs_name\n$(translate 'Please enter the password.')" 10 60
get_pbs_password "$pbs_name"
fi
clear
fi
}
configure_pbs_manually() {
local PBS_REPO_FILE="/usr/local/share/proxmenux/pbs-repo.conf"
local PBS_MANUAL_CONFIGS="/usr/local/share/proxmenux/pbs-manual-configs.txt"
local PBS_NAME
PBS_NAME=$(dialog --backtitle "ProxMenux" --title "New PBS Configuration" --inputbox "$(translate 'Enter a name for this PBS configuration:')" 10 60 "PBS-$(date +%m%d)" 3>&1 1>&2 2>&3) || return 1
PBS_USER=$(dialog --backtitle "ProxMenux" --title "New PBS Configuration" --inputbox "$(translate 'Enter PBS username:')" 10 50 "root@pam" 3>&1 1>&2 2>&3) || return 1
PBS_HOST=$(dialog --backtitle "ProxMenux" --title "New PBS Configuration" --inputbox "$(translate 'Enter PBS host or IP:')" 10 50 "" 3>&1 1>&2 2>&3) || return 1
PBS_DATASTORE=$(dialog --backtitle "ProxMenux" --title "New PBS Configuration" --inputbox "$(translate 'Enter PBS datastore name:')" 10 50 "" 3>&1 1>&2 2>&3) || return 1
if [[ -z "$PBS_NAME" || -z "$PBS_USER" || -z "$PBS_HOST" || -z "$PBS_DATASTORE" ]]; then
dialog --backtitle "ProxMenux" --title "Error" --msgbox "$(translate 'All fields are required!')" 8 40
return 1
fi
PBS_REPO="${PBS_USER}@${PBS_HOST}:${PBS_DATASTORE}"
{
mkdir -p "$(dirname "$PBS_REPO_FILE")"
echo "$PBS_REPO" > "$PBS_REPO_FILE"
} >/dev/null 2>&1
local config_line="$PBS_NAME|$PBS_REPO"
if ! grep -Fxq "$config_line" "$PBS_MANUAL_CONFIGS" 2>/dev/null; then
echo "$config_line" >> "$PBS_MANUAL_CONFIGS"
fi
get_pbs_password "$PBS_NAME"
dialog --backtitle "ProxMenux" --title "Success" --msgbox "$(translate 'PBS configuration saved:') $PBS_NAME\n\n$(translate 'Repository:') $PBS_REPO\n\n$(translate 'This configuration will appear in future backups.')" 12 80
}
get_pbs_password() {
local PBS_NAME="$1"
local PBS_PASS_FILE="/usr/local/share/proxmenux/pbs-pass.txt"
local PBS_MANUAL_PASS_FILE="/usr/local/share/proxmenux/pbs-pass-${PBS_NAME}.txt"
while true; do
PBS_REPO_PASS=$(dialog --backtitle "ProxMenux" --title "PBS Password" --insecure --passwordbox "$(translate 'Enter PBS repository password for:') $PBS_NAME" 10 70 "" 3>&1 1>&2 2>&3) || return 1
PBS_REPO_PASS2=$(dialog --backtitle "ProxMenux" --title "PBS Password" --insecure --passwordbox "$(translate 'Confirm PBS repository password:')" 10 60 "" 3>&1 1>&2 2>&3) || return 1
if [[ "$PBS_REPO_PASS" == "$PBS_REPO_PASS2" ]]; then
break
else
dialog --backtitle "ProxMenux" --title "Error" --msgbox "$(translate 'Repository passwords do not match! Please try again.')" 8 50
fi
done
{
echo "$PBS_REPO_PASS" > "$PBS_PASS_FILE"
chmod 600 "$PBS_PASS_FILE"
} >/dev/null 2>&1
{
echo "$PBS_REPO_PASS" > "$PBS_MANUAL_PASS_FILE"
chmod 600 "$PBS_MANUAL_PASS_FILE"
} >/dev/null 2>&1
}
# ===============================
# ========== PBS BACKUP ==========
backup_full_pbs_root() {
local HOSTNAME PBS_REPO PBS_KEY_FILE PBS_PASS_FILE PBS_ENCRYPTION_PASS_FILE ENCRYPT_OPT=""
HOSTNAME=$(hostname)
local PBS_REPO_FILE="/usr/local/share/proxmenux/pbs-repo.conf"
PBS_KEY_FILE="/usr/local/share/proxmenux/pbs-key.conf"
PBS_PASS_FILE="/usr/local/share/proxmenux/pbs-pass.txt"
PBS_ENCRYPTION_PASS_FILE="/usr/local/share/proxmenux/pbs-encryption-pass.txt"
LOGFILE="/tmp/pbs-backup-${HOSTNAME}.log"
configure_pbs_repository
if [[ ! -f "$PBS_REPO_FILE" ]]; then
msg_error "$(translate "Failed to configure PBS connection")"
sleep 3
return 1
fi
PBS_REPO=$(<"$PBS_REPO_FILE")
if [[ ! -f "$PBS_PASS_FILE" ]]; then
msg_error "$(translate "PBS password not configured")"
sleep 3
return 1
fi
dialog --backtitle "ProxMenux" --title "Encryption" --yesno "$(translate 'Do you want to encrypt the backup?')" 8 60
if [[ $? -eq 0 ]]; then
if [[ ! -f "$PBS_ENCRYPTION_PASS_FILE" ]]; then
while true; do
PBS_KEY_PASS=$(dialog --backtitle "ProxMenux" --title "Encryption Password" --insecure --passwordbox "$(translate 'Enter encryption password (different from PBS login):')" 12 70 "" 3>&1 1>&2 2>&3) || return 1
PBS_KEY_PASS2=$(dialog --backtitle "ProxMenux" --title "Encryption Password" --insecure --passwordbox "$(translate 'Confirm encryption password:')" 10 60 "" 3>&1 1>&2 2>&3) || return 1
if [[ "$PBS_KEY_PASS" == "$PBS_KEY_PASS2" ]]; then
break
else
dialog --backtitle "ProxMenux" --title "Error" --msgbox "$(translate 'Passwords do not match! Please try again.')" 8 50
fi
done
{
echo "$PBS_KEY_PASS" > "$PBS_ENCRYPTION_PASS_FILE"
chmod 600 "$PBS_ENCRYPTION_PASS_FILE"
} >/dev/null 2>&1
dialog --backtitle "ProxMenux" --title "Success" --msgbox "$(translate 'Encryption password saved successfully!')" 8 50
fi
if [[ ! -f "$PBS_KEY_FILE" ]]; then
PBS_ENCRYPTION_PASS=$(<"$PBS_ENCRYPTION_PASS_FILE")
dialog --backtitle "ProxMenux" --title "Encryption" --infobox "$(translate 'Creating encryption key...')" 5 50
expect -c "
set timeout 30
spawn proxmox-backup-client key create \"$PBS_KEY_FILE\"
expect {
\"Encryption Key Password:\" {
send \"$PBS_ENCRYPTION_PASS\r\"
exp_continue
}
\"Verify Password:\" {
send \"$PBS_ENCRYPTION_PASS\r\"
exp_continue
}
eof
}
" >/dev/null 2>&1
if [[ ! -f "$PBS_KEY_FILE" ]]; then
dialog --backtitle "ProxMenux" --title "Error" --msgbox "$(translate 'Error creating encryption key.')" 8 40
return 1
fi
dialog --backtitle "ProxMenux" --title "Important" --msgbox "$(translate 'IMPORTANT: Save the key file. Without it you will not be able to restore your backups!')\n\n$(translate 'Key file location:') $PBS_KEY_FILE" 12 70
fi
ENCRYPT_OPT="--keyfile $PBS_KEY_FILE"
else
ENCRYPT_OPT=""
fi
clear
show_proxmenux_logo
echo -e
msg_info2 "$(translate "Starting backup to PBS")"
echo -e
echo -e "${BL}$(translate "PBS Repository:")${WHITE} $PBS_REPO${RESET}"
echo -e "${BL}$(translate "Backup ID:")${WHITE} $HOSTNAME${RESET}"
echo -e "${BL}$(translate "Included:")${WHITE} /boot/efi /etc/pve (all root)${RESET}"
echo -e "${BL}$(translate "Encryption:")${WHITE} $([[ -n "$ENCRYPT_OPT" ]] && echo "Enabled" || echo "Disabled")${RESET}"
echo -e "${BL}$(translate "Log file:")${WHITE} $LOGFILE${RESET}"
echo -e "${BOLD}${NEON_PURPLE_BLUE}-------------------------------${RESET}"
echo ""
PBS_REPO_PASS=$(<"$PBS_PASS_FILE")
if [[ -n "$ENCRYPT_OPT" ]]; then
PBS_ENCRYPTION_PASS=$(<"$PBS_ENCRYPTION_PASS_FILE")
echo "$(translate "Starting encrypted full backup...")"
echo ""
expect -c "
set timeout 3600
log_file $LOGFILE
spawn proxmox-backup-client backup \
--include-dev /boot/efi \
--include-dev /etc/pve \
root-${HOSTNAME}.pxar:/ \
--repository \"$PBS_REPO\" \
$ENCRYPT_OPT \
--backup-type host \
--backup-id \"$HOSTNAME\" \
--backup-time \"$(date +%s)\"
expect {
-re \"Password for .*:\" {
send \"$PBS_REPO_PASS\r\"
exp_continue
}
\"Encryption Key Password:\" {
send \"$PBS_ENCRYPTION_PASS\r\"
exp_continue
}
eof
}
" | tee -a "$LOGFILE"
else
echo "$(translate "Starting unencrypted full backup...")"
echo ""
expect -c "
set timeout 3600
log_file $LOGFILE
spawn proxmox-backup-client backup \
--include-dev /boot/efi \
--include-dev /etc/pve \
root-${HOSTNAME}.pxar:/ \
--repository \"$PBS_REPO\" \
--backup-type host \
--backup-id \"$HOSTNAME\" \
--backup-time \"$(date +%s)\"
expect {
-re \"Password for .*:\" {
send \"$PBS_REPO_PASS\r\"
exp_continue
}
eof
}
" | tee -a "$LOGFILE"
fi
local backup_result=$?
echo -e "${BOLD}${NEON_PURPLE_BLUE}===============================${RESET}\n"
if [[ $backup_result -eq 0 ]]; then
msg_ok "$(translate "Full backup process completed successfully")"
else
msg_error "$(translate "Backup process finished with errors")"
fi
echo ""
msg_success "$(translate "Press Enter to return to the main menu...")"
read -r
}
backup_to_pbs() {
local HOSTNAME TIMESTAMP SNAPSHOT
HOSTNAME=$(hostname)
TIMESTAMP=$(date +%Y-%m-%d_%H-%M)
SNAPSHOT="${HOSTNAME}-${TIMESTAMP}"
local PBS_REPO_FILE="/usr/local/share/proxmenux/pbs-repo.conf"
local PBS_KEY_FILE="/usr/local/share/proxmenux/pbs-key.conf"
local PBS_PASS_FILE="/usr/local/share/proxmenux/pbs-pass.txt"
local PBS_ENCRYPTION_PASS_FILE="/usr/local/share/proxmenux/pbs-encryption-pass.txt"
local PBS_REPO ENCRYPT_OPT USE_ENCRYPTION
local PBS_KEY_PASS PBS_REPO_PASS
configure_pbs_repository
PBS_REPO=$(<"$PBS_REPO_FILE")
USE_ENCRYPTION=false
dialog --backtitle "ProxMenux" --yesno "$(translate 'Do you want to encrypt the backup?')" 8 60
[[ $? -eq 0 ]] && USE_ENCRYPTION=true
if $USE_ENCRYPTION && ! command -v expect >/dev/null 2>&1; then
apt-get update -qq >/dev/null 2>&1
apt-get install -y expect >/dev/null 2>&1
fi
if [[ "$#" -lt 1 ]]; then
clear
show_proxmenux_logo
msg_error "$(translate "No directories specified for backup.")"
sleep 2
return 1
fi
local TOTAL="$#"
local COUNT=1
for dir in "$@"; do
local SAFE_NAME SAFE_ID PXAR_NAME
SAFE_NAME=$(basename "$dir" | tr '.-/' '_')
PXAR_NAME="root-custom-${SAFE_NAME}-${SNAPSHOT}.pxar"
SAFE_ID="custom-${HOSTNAME}-${SAFE_NAME}"
msg_info2 "$(translate "[$COUNT/$TOTAL] Backing up") $dir $(translate "as") $PXAR_NAME"
ENCRYPT_OPT=""
if $USE_ENCRYPTION; then
if [[ -f "$PBS_KEY_FILE" ]]; then
ENCRYPT_OPT="--keyfile $PBS_KEY_FILE"
else
while true; do
PBS_KEY_PASS=$(dialog --backtitle "ProxMenux" --insecure --passwordbox "$(translate 'Enter encryption password (different from PBS login):')" 10 60 "" 3>&1 1>&2 2>&3) || return 1
PBS_KEY_PASS2=$(dialog --backtitle "ProxMenux" --insecure --passwordbox "$(translate 'Confirm encryption password:')" 10 60 "" 3>&1 1>&2 2>&3) || return 1
if [[ "$PBS_KEY_PASS" == "$PBS_KEY_PASS2" ]]; then
break
else
dialog --backtitle "ProxMenux" --msgbox "$(translate 'Passwords do not match! Please try again.')" 8 50
fi
done
{
echo "$PBS_KEY_PASS" > "$PBS_ENCRYPTION_PASS_FILE"
chmod 600 "$PBS_ENCRYPTION_PASS_FILE"
} >/dev/null 2>&1
expect -c "
set timeout 30
spawn proxmox-backup-client key create \"$PBS_KEY_FILE\"
expect {
\"Encryption Key Password:\" {
send \"$PBS_KEY_PASS\r\"
exp_continue
}
\"Verify Password:\" {
send \"$PBS_KEY_PASS\r\"
exp_continue
}
eof
}
" >/dev/null 2>&1
if [[ ! -f "$PBS_KEY_FILE" ]]; then
dialog --backtitle "ProxMenux" --msgbox "$(translate 'Error creating encryption key.')" 8 40
return 1
fi
ENCRYPT_OPT="--keyfile $PBS_KEY_FILE"
dialog --backtitle "ProxMenux" --msgbox "$(translate 'Encryption key generated. Save it in a safe place!')" 10 60
fi
fi
clear
show_proxmenux_logo
echo -e
msg_info2 "$(translate "Starting backup to PBS")"
TOTAL_SIZE=$(du -cb "$@" | awk '/total$/ {print $1}')
TOTAL_SIZE_GB=$(awk "BEGIN {printf \"%.2f\", $TOTAL_SIZE/1024/1024/1024}")
echo -e
echo -e "${BL}$(translate "PBS Repository:")${WHITE} $PBS_REPO${RESET}"
echo -e "${BL}$(translate "Backup ID:")${WHITE} $HOSTNAME${RESET}"
echo -e "${BL}$(translate "Encryption:")${WHITE} $([[ -n "$ENCRYPT_OPT" ]] && echo "Enabled" || echo "Disabled")${RESET}"
echo -e "${BL}$(translate "Included directories:")${WHITE} $*${RESET}"
echo -e "${BL}$(translate "Total size:")${WHITE} ${TOTAL_SIZE_GB} GB${RESET}"
echo -e "${BOLD}${NEON_PURPLE_BLUE}-------------------------------${RESET}"
PBS_REPO_PASS=$(<"$PBS_PASS_FILE")
if $USE_ENCRYPTION && [[ -f "$PBS_ENCRYPTION_PASS_FILE" ]]; then
PBS_KEY_PASS=$(<"$PBS_ENCRYPTION_PASS_FILE")
expect -c "
set timeout 300
spawn proxmox-backup-client backup \"${PXAR_NAME}:$dir\" --repository \"$PBS_REPO\" $ENCRYPT_OPT --backup-type host --backup-id \"$SAFE_ID\" --backup-time \"$(date +%s)\"
expect {
-re \"Password for .*:\" {
send \"$PBS_REPO_PASS\r\"
exp_continue
}
\"Encryption Key Password:\" {
send \"$PBS_KEY_PASS\r\"
exp_continue
}
eof
}
"
else
expect -c "
set timeout 300
spawn proxmox-backup-client backup \"${PXAR_NAME}:$dir\" --repository \"$PBS_REPO\" $ENCRYPT_OPT --backup-type host --backup-id \"$SAFE_ID\" --backup-time \"$(date +%s)\"
expect {
-re \"Password for .*:\" {
send \"$PBS_REPO_PASS\r\"
exp_continue
}
eof
}
"
fi
COUNT=$((COUNT+1))
done
echo -e "${BOLD}${NEON_PURPLE_BLUE}===============================${RESET}\n"
msg_ok "$(translate "Backup process finished.")"
echo ""
msg_success "$(translate "Press Enter to return to the main menu...")"
read -r
}
# ===============================
# ========== BORGBACKUP ==========
backup_with_borg() {
# local SRC="$1"
local BORG_APPIMAGE="/usr/local/share/proxmenux/borg"
local LOGFILE="/tmp/borg-backup.log"
local DEST
local TYPE
local ENCRYPT_OPT=""
local BORG_KEY
if [[ ! -x "$BORG_APPIMAGE" ]]; then
clear
show_proxmenux_logo
msg_info "$(translate "BorgBackup not found. Downloading AppImage...")"
mkdir -p /usr/local/share/proxmenux
wget -qO "$BORG_APPIMAGE" "https://github.com/borgbackup/borg/releases/download/1.2.8/borg-linux64"
chmod +x "$BORG_APPIMAGE"
msg_ok "$(translate "BorgBackup downloaded and ready.")"
fi
TYPE=$(dialog --backtitle "ProxMenux" --menu "$(translate 'Select Borg backup destination:')" 15 60 3 \
"local" "$(translate 'Local directory')" \
"usb" "$(translate 'Internal/External dedicated disk')" \
"remote" "$(translate 'Remote server')" \
3>&1 1>&2 2>&3) || return 1
if [[ "$TYPE" == "local" ]]; then
DEST=$(dialog --backtitle "ProxMenux" --inputbox "$(translate 'Enter local directory for backup:')" 10 60 "/backup/borgbackup" 3>&1 1>&2 2>&3) || return 1
mkdir -p "$DEST"
elif [[ "$TYPE" == "usb" ]]; then
while true; do
BASE_DEST=$(get_external_backup_mount_point)
if [[ -z "$BASE_DEST" ]]; then
dialog --backtitle "ProxMenux" --yesno "$(translate 'No external disk detected or mounted. Would you like to retry?')" 8 60
[[ $? -eq 0 ]] && continue
return 1
fi
DEST="$BASE_DEST/borgbackup"
mkdir -p "$DEST"
DISK_DEV=$(df "$BASE_DEST" | awk 'NR==2{print $1}')
PKNAME=$(lsblk -no PKNAME "$DISK_DEV" 2>/dev/null)
[[ -z "$PKNAME" ]] && PKNAME=$(basename "$DISK_DEV" | sed 's/[0-9]*$//')
if [[ -n "$PKNAME" && -b /dev/$PKNAME ]]; then
DISK_MODEL=$(lsblk -no MODEL "/dev/$PKNAME")
else
DISK_MODEL="(unknown)"
fi
FREE_SPACE=$(df -h "$BASE_DEST" | awk 'NR==2{print $4}')
dialog --backtitle "ProxMenux" \
--title "$(translate "Dedicated Backup Disk")" \
--yesno "\n$(translate "Mount point:") $DEST\n\n\
$(translate "Disk model:") $DISK_MODEL\n\
$(translate "Available space:") $FREE_SPACE\n\n\
$(translate "Use this disk for backup?")" 12 70
if [[ $? -eq 0 ]]; then
break
else
return 1
fi
done
elif [[ "$TYPE" == "remote" ]]; then
REMOTE_USER=$(dialog --backtitle "ProxMenux" --inputbox "$(translate 'Enter SSH user for remote:')" 10 60 "root" 3>&1 1>&2 2>&3) || return 1
REMOTE_HOST=$(dialog --backtitle "ProxMenux" --inputbox "$(translate 'Enter SSH host:')" 10 60 "" 3>&1 1>&2 2>&3) || return 1
REMOTE_PATH=$(dialog --backtitle "ProxMenux" --inputbox "$(translate 'Enter remote path:')" 10 60 "/backup/borgbackup" 3>&1 1>&2 2>&3) || return 1
DEST="ssh://$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH"
fi
dialog --backtitle "ProxMenux" --yesno "$(translate 'Do you want to encrypt the backup?')" 8 60
if [[ $? -eq 0 ]]; then
BORG_KEY=$(dialog --backtitle "ProxMenux" --inputbox "$(translate 'Enter Borg encryption passphrase (will be saved):')" 10 60 "" 3>&1 1>&2 2>&3) || return 1
ENCRYPT_OPT="--encryption=repokey"
export BORG_PASSPHRASE="$BORG_KEY"
else
ENCRYPT_OPT="--encryption=none"
fi
if [[ "$TYPE" == "local" || "$TYPE" == "usb" ]]; then
if [[ ! -f "$DEST/config" ]]; then
"$BORG_APPIMAGE" init $ENCRYPT_OPT "$DEST"
if [[ $? -ne 0 ]]; then
clear
show_proxmenux_logo
msg_error "$(translate "Failed to initialize Borg repo at") $DEST"
sleep 5
return 1
fi
fi
fi
dialog --backtitle "ProxMenux" --msgbox "$(translate 'Borg backup will start now. This may take a while.')" 8 40
clear
show_proxmenux_logo
msg_info2 "$(translate "Starting backup with BorgBackup...")"
echo -e
TOTAL_SIZE=$(du -cb "$@" | awk '/total$/ {print $1}')
TOTAL_SIZE_GB=$(awk "BEGIN {printf \"%.2f\", $TOTAL_SIZE/1024/1024/1024}")
echo -e "${BL}$(translate "Included directories:")${WHITE} $*${RESET}"
echo -e "${BL}$(translate "Total size:")${WHITE} ${TOTAL_SIZE_GB} GB${RESET}"
# 6. Lanzar el backup y guardar log
# "$BORG_APPIMAGE" create --progress "$DEST"::"root-$(hostname)-$(date +%Y%m%d_%H%M)" $SRC 2>&1 | tee "$LOGFILE"
"$BORG_APPIMAGE" create --progress "$DEST"::"root-$(hostname)-$(date +%Y%m%d_%H%M)" "$@" 2>&1 | tee "$LOGFILE"
echo -e "${BOLD}${NEON_PURPLE_BLUE}===============================${RESET}\n"
msg_ok "$(translate "Backup process finished.")"
echo
msg_success "$(translate "Press Enter to return to the main menu...")"
read -r
}
# ===============================
# ========== LOCAL TAR ==========
backup_to_local_tar() {
# local SRC="$1"
local TYPE
local DEST
local LOGFILE="/tmp/tar-backup.log"
if ! command -v pv &>/dev/null; then
apt-get update -qq && apt-get install -y pv >/dev/null 2>&1
fi
TYPE=$(dialog --backtitle "ProxMenux" --menu "$(translate 'Select backup destination:')" 15 60 2 \
"local" "$(translate 'Local directory')" \
"usb" "$(translate 'Internal/External dedicated disk')" \
3>&1 1>&2 2>&3) || return 1
if [[ "$TYPE" == "local" ]]; then
DEST=$(dialog --backtitle "ProxMenux" --inputbox "$(translate 'Enter directory for backup:')" 10 60 "/backup" 3>&1 1>&2 2>&3) || return 1
mkdir -p "$DEST"
else
while true; do
DEST=$(get_external_backup_mount_point)
if [[ -z "$DEST" ]]; then
dialog --backtitle "ProxMenux" --yesno "No external disk detected or mounted. Would you like to retry?" 8 60
[[ $? -eq 0 ]] && continue
return 1
fi
DISK_DEV=$(df "$DEST" | awk 'NR==2{print $1}')
PKNAME=$(lsblk -no PKNAME "$DISK_DEV" 2>/dev/null)
[[ -z "$PKNAME" ]] && PKNAME=$(basename "$DISK_DEV" | sed 's/[0-9]*$//')
if [[ -n "$PKNAME" && -b /dev/$PKNAME ]]; then
DISK_MODEL=$(lsblk -no MODEL "/dev/$PKNAME")
else
DISK_MODEL="(unknown)"
fi
FREE_SPACE=$(df -h "$DEST" | awk 'NR==2{print $4}')
dialog --backtitle "ProxMenux" \
--title "$(translate "Dedicated Backup Disk")" \
--yesno "\n$(translate "Mount point:") $DEST\n\n\
$(translate "Disk model:") $DISK_MODEL\n\
$(translate "Available space:") $FREE_SPACE\n\n\
$(translate "Use this disk for backup?")" 12 70
if [[ $? -eq 0 ]]; then
mkdir -p "$DEST"
break
else
return 1
fi
done
fi
TAR_INPUT=""
TOTAL_SIZE=0
for src in $SRC; do
sz=$(du -sb "$src" 2>/dev/null | awk '{print $1}')
TOTAL_SIZE=$((TOTAL_SIZE + sz))
TAR_INPUT="$TAR_INPUT $src"
done
local FILENAME="root-$(hostname)-$(date +%Y%m%d_%H%M).tar.gz"
clear
show_proxmenux_logo
msg_info2 "$(translate "Starting backup with tar...")"
echo -e
TOTAL_SIZE=$(du -cb "$@" | awk '/total$/ {print $1}')
TOTAL_SIZE_GB=$(awk "BEGIN {printf \"%.2f\", $TOTAL_SIZE/1024/1024/1024}")
echo -e "${BL}$(translate "Included directories:")${WHITE} $*${RESET}"
echo -e "${BL}$(translate "Total size:")${WHITE} ${TOTAL_SIZE_GB} GB${RESET}"
tar -cf - "$@" 2> >(grep -v "Removing leading \`/'" >&2) \
| pv -s "$TOTAL_SIZE" \
| gzip > "$DEST/$FILENAME"
echo -ne "\033[1A\r\033[K"
echo -e "${BOLD}${NEON_PURPLE_BLUE}===============================${RESET}\n"
msg_ok "$(translate "Backup process finished. Review log above or in /tmp/tar-backup.log")"
echo
msg_success "$(translate "Press Enter to return to the main menu...")"
read -r
}
# ===============================
host_backup_menu

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,433 @@
#!/bin/bash
# ==========================================================
# ProxMenu - Mount disk on Proxmox host for backups
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT
# Version : 1.3-dialog
# Last Updated: 13/12/2024
# ==========================================================
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
mount_disk_host_bk() {
get_disk_info() {
local disk=$1
MODEL=$(lsblk -dn -o MODEL "$disk" | xargs)
SIZE=$(lsblk -dn -o SIZE "$disk" | xargs)
echo "$MODEL" "$SIZE"
}
is_usb_disk() {
local disk=$1
local disk_name=$(basename "$disk")
if readlink -f "/sys/block/$disk_name/device" 2>/dev/null | grep -q "usb"; then
return 0
fi
if udevadm info --query=property --name="$disk" 2>/dev/null | grep -q "ID_BUS=usb"; then
return 0
fi
return 1
}
is_system_disk() {
local disk=$1
local disk_name=$(basename "$disk")
local system_mounts=$(df -h | grep -E '^\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(/|/boot|/usr|/var|/home)$' | awk '{print $1}')
for mount_dev in $system_mounts; do
local mount_disk=""
if [[ "$mount_dev" =~ ^/dev/mapper/ ]]; then
local vg_name=$(lvs --noheadings -o vg_name "$mount_dev" 2>/dev/null | xargs)
if [[ -n "$vg_name" ]]; then
local pvs_list=$(pvs --noheadings -o pv_name -S vg_name="$vg_name" 2>/dev/null | xargs)
for pv in $pvs_list; do
if [[ -n "$pv" && -e "$pv" ]]; then
mount_disk=$(lsblk -no PKNAME "$pv" 2>/dev/null)
if [[ -n "$mount_disk" && "/dev/$mount_disk" == "$disk" ]]; then
return 0
fi
fi
done
fi
elif [[ "$mount_dev" =~ ^/dev/[hsv]d[a-z][0-9]* || "$mount_dev" =~ ^/dev/nvme[0-9]+n[0-9]+p[0-9]+ ]]; then
mount_disk=$(lsblk -no PKNAME "$mount_dev" 2>/dev/null)
if [[ -n "$mount_disk" && "/dev/$mount_disk" == "$disk" ]]; then
return 0
fi
fi
done
local fs_type=$(lsblk -no FSTYPE "$disk" 2>/dev/null | head -1)
if [[ "$fs_type" == "btrfs" ]]; then
local temp_mount=$(mktemp -d)
if mount -o ro "$disk" "$temp_mount" 2>/dev/null; then
if btrfs subvolume list "$temp_mount" 2>/dev/null | grep -qE '(@|@home|@var|@boot|@root|root)'; then
umount "$temp_mount" 2>/dev/null
rmdir "$temp_mount" 2>/dev/null
return 0
fi
umount "$temp_mount" 2>/dev/null
fi
rmdir "$temp_mount" 2>/dev/null
while read -r part; do
if [[ -n "$part" ]]; then
local part_fs=$(lsblk -no FSTYPE "/dev/$part" 2>/dev/null)
if [[ "$part_fs" == "btrfs" ]]; then
local mount_point=$(lsblk -no MOUNTPOINT "/dev/$part" 2>/dev/null)
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
return 0
fi
fi
fi
done < <(lsblk -ln -o NAME "$disk" | tail -n +2)
fi
local disk_uuid=$(blkid -s UUID -o value "$disk" 2>/dev/null)
local part_uuids=()
while read -r part; do
if [[ -n "$part" ]]; then
local uuid=$(blkid -s UUID -o value "/dev/$part" 2>/dev/null)
if [[ -n "$uuid" ]]; then
part_uuids+=("$uuid")
fi
fi
done < <(lsblk -ln -o NAME "$disk" | tail -n +2)
for uuid in "${part_uuids[@]}" "$disk_uuid"; do
if [[ -n "$uuid" ]] && grep -q "UUID=$uuid" /etc/fstab; then
local mount_point=$(grep "UUID=$uuid" /etc/fstab | awk '{print $2}')
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
return 0
fi
fi
done
if grep -q "$disk" /etc/fstab; then
local mount_point=$(grep "$disk" /etc/fstab | awk '{print $2}')
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
return 0
fi
fi
local disk_count=$(lsblk -dn -e 7,11 -o PATH | wc -l)
if [[ "$disk_count" -eq 1 ]]; then
return 0
fi
return 1
}
USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}')
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
ZFS_DISKS=""
ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
for entry in $ZFS_RAW; do
path=""
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
if [ -e "/dev/disk/by-id/$entry" ]; then
path=$(readlink -f "/dev/disk/by-id/$entry")
fi
elif [[ "$entry" == /dev/* ]]; then
path="$entry"
fi
if [ -n "$path" ]; then
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
if [ -n "$base_disk" ]; then
ZFS_DISKS+="/dev/$base_disk"$'\n'
fi
fi
done
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
LVM_DEVICES=$(
pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') |
while read -r dev; do
[[ -n "$dev" && -e "$dev" ]] && readlink -f "$dev"
done | sort -u
)
FREE_DISKS=()
while read -r DISK; do
[[ "$DISK" =~ /dev/zd ]] && continue
INFO=($(get_disk_info "$DISK"))
MODEL="${INFO[@]::${#INFO[@]}-1}"
SIZE="${INFO[-1]}"
LABEL=""
SHOW_DISK=true
IS_MOUNTED=false
IS_RAID=false
IS_ZFS=false
IS_LVM=false
IS_SYSTEM=false
IS_USB=false
if is_system_disk "$DISK"; then
IS_SYSTEM=true
fi
if is_usb_disk "$DISK"; then
IS_USB=true
fi
while read -r part fstype; do
[[ "$fstype" == "zfs_member" ]] && IS_ZFS=true
[[ "$fstype" == "linux_raid_member" ]] && IS_RAID=true
[[ "$fstype" == "LVM2_member" ]] && IS_LVM=true
if grep -q "/dev/$part" <<< "$MOUNTED_DISKS"; then
IS_MOUNTED=true
fi
done < <(lsblk -ln -o NAME,FSTYPE "$DISK" | tail -n +2)
REAL_PATH=""
if [[ -n "$DISK" && -e "$DISK" ]]; then
REAL_PATH=$(readlink -f "$DISK")
fi
if [[ -n "$REAL_PATH" ]] && echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then
IS_MOUNTED=true
fi
USED_BY=""
REAL_PATH=""
if [[ -n "$DISK" && -e "$DISK" ]]; then
REAL_PATH=$(readlink -f "$DISK")
fi
CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then
USED_BY="$(translate "In use")"
else
for SYMLINK in /dev/disk/by-id/*; do
[[ -e "$SYMLINK" ]] || continue
if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then
if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then
USED_BY="$(translate "In use")"
break
fi
fi
done
fi
if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)"; then
if grep -q "active raid" /proc/mdstat; then
SHOW_DISK=false
fi
fi
if $IS_ZFS; then SHOW_DISK=false; fi
if $IS_MOUNTED; then SHOW_DISK=false; fi
if $IS_SYSTEM; then SHOW_DISK=false; fi
if $SHOW_DISK; then
[[ -n "$USED_BY" ]] && LABEL+=" [$USED_BY]"
[[ "$IS_RAID" == true ]] && LABEL+=" ⚠ RAID"
[[ "$IS_LVM" == true ]] && LABEL+=" ⚠ LVM"
[[ "$IS_ZFS" == true ]] && LABEL+=" ⚠ ZFS"
if $IS_USB; then
LABEL+=" USB"
else
LABEL+=" $(translate "Internal")"
fi
DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL")
FREE_DISKS+=("$DISK" "$DESCRIPTION" "off")
fi
done < <(lsblk -dn -e 7,11 -o PATH)
if [ "${#FREE_DISKS[@]}" -eq 0 ]; then
dialog --title "$(translate "Error")" --msgbox "$(translate "No available disks found on the host.")" 8 60
exit 1
fi
# Building the array for dialog (format: tag item on/off tag item on/off...)
DLG_LIST=()
for ((i=0; i<${#FREE_DISKS[@]}; i+=3)); do
DLG_LIST+=("${FREE_DISKS[i]}" "${FREE_DISKS[i+1]}" "${FREE_DISKS[i+2]}")
done
SELECTED=$(dialog --clear --backtitle "ProxMenux" --title "$(translate "Select Disk")" \
--radiolist "\n$(translate "Select the disk you want to mount on the host:")" 20 90 10 \
"${DLG_LIST[@]}" 2>&1 >/dev/tty)
if [ -z "$SELECTED" ]; then
dialog --title "$(translate "Error")" --msgbox "$(translate "No disk was selected.")" 8 50
exit 1
fi
# ------------------- Partitions and formatting ------------------------
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
SKIP_FORMAT=false
DEFAULT_MOUNT="/mnt/backup"
if [ -n "$PARTITION" ]; then
PARTITION="/dev/$PARTITION"
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
SKIP_FORMAT=true
else
dialog --title "$(translate "Unsupported Filesystem")" --yesno \
"$(translate "The partition") $PARTITION $(translate "has an unsupported filesystem ($CURRENT_FS).\nDo you want to format it?")" 10 70
if [ $? -ne 0 ]; then exit 0; fi
fi
else
CURRENT_FS=$(lsblk -no FSTYPE "$SELECTED" | xargs)
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
SKIP_FORMAT=true
PARTITION="$SELECTED"
else
dialog --title "$(translate "No Valid Partitions")" --yesno \
"$(translate "The disk has no partitions and no valid filesystem. Do you want to create a new partition and format it?")" 10 70
if [ $? -ne 0 ]; then exit 0; fi
echo -e "$(translate "Creating partition table and partition...")"
parted -s "$SELECTED" mklabel gpt
parted -s "$SELECTED" mkpart primary 0% 100%
sleep 2
partprobe "$SELECTED"
sleep 2
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
if [ -n "$PARTITION" ]; then
PARTITION="/dev/$PARTITION"
else
dialog --title "$(translate "Partition Error")" --msgbox \
"$(translate "Failed to create partition on disk") $SELECTED." 8 70
exit 1
fi
fi
fi
if [ "$SKIP_FORMAT" != true ]; then
FORMAT_TYPE=$(dialog --title "$(translate "Select Format Type")" --menu \
"$(translate "Select the filesystem type for") $PARTITION:" 15 60 5 \
"ext4" "$(translate "Extended Filesystem 4 (recommended)")" \
"xfs" "XFS" \
"btrfs" "Btrfs" 2>&1 >/dev/tty)
if [ -z "$FORMAT_TYPE" ]; then
dialog --title "$(translate "Format Cancelled")" --msgbox \
"$(translate "Format operation cancelled. The disk will not be added.")" 8 60
exit 0
fi
dialog --title "$(translate "WARNING")" --yesno \
"$(translate "WARNING: This operation will FORMAT the disk") $PARTITION $(translate "with") $FORMAT_TYPE.\n\n$(translate "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!")\n\n$(translate "Are you sure you want to continue")" 15 70
if [ $? -ne 0 ]; then exit 0; fi
echo -e "$(translate "Formatting partition") $PARTITION $(translate "with") $FORMAT_TYPE..."
case "$FORMAT_TYPE" in
"ext4") mkfs.ext4 -F "$PARTITION" ;;
"xfs") mkfs.xfs -f "$PARTITION" ;;
"btrfs") mkfs.btrfs -f "$PARTITION" ;;
esac
if [ $? -ne 0 ]; then
cleanup
dialog --title "$(translate "Format Failed")" --msgbox \
"$(translate "Failed to format partition") $PARTITION $(translate "with") $FORMAT_TYPE." 12 70
exit 1
else
partprobe "$SELECTED"
sleep 2
fi
fi
# ------------------- Mount point and permissions (modular, non-blocking) -------------------
MOUNT_POINT=$(dialog --clear --title "$(translate "Mount Point")" \
--inputbox "$(translate "Enter the mount point for the disk (e.g., /mnt/backup):")" \
10 60 "$DEFAULT_MOUNT" 2>&1 >/dev/tty)
if [ -z "$MOUNT_POINT" ]; then
>&2 echo "$(translate "No mount point was specified.")"
return 1
fi
mkdir -p "$MOUNT_POINT"
UUID=$(blkid -s UUID -o value "$PARTITION")
FS_TYPE=$(lsblk -no FSTYPE "$PARTITION" | xargs)
FSTAB_ENTRY="UUID=$UUID $MOUNT_POINT $FS_TYPE defaults 0 0"
if grep -q "UUID=$UUID" /etc/fstab; then
sed -i "s|^.*UUID=$UUID.*|$FSTAB_ENTRY|" /etc/fstab
else
echo "$FSTAB_ENTRY" >> /etc/fstab
fi
mount "$MOUNT_POINT" 2> >(grep -v "systemd still uses")
if [ $? -eq 0 ]; then
if ! getent group sharedfiles >/dev/null; then
groupadd sharedfiles
fi
chown root:sharedfiles "$MOUNT_POINT"
chmod 2775 "$MOUNT_POINT"
echo "$MOUNT_POINT" > /usr/local/share/proxmenux/last_backup_mount.txt
MOUNT_POINT=$(echo "$MOUNT_POINT" | head -n1 | tr -d '\r\n\t ')
echo "$MOUNT_POINT"
else
>&2 echo "$(translate "Failed to mount the disk at") $MOUNT_POINT"
return 1
fi
}

View File

@@ -1,13 +1,13 @@
#!/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
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# Version : 1.1
# Last Updated: 17/08/2025
# ==========================================================
# Description:
# This script automates the process of enabling and configuring Intel Integrated GPU (iGPU) support in Proxmox VE LXC containers.
@@ -32,7 +32,7 @@ initialize_cache
# Select LXC container
select_container() {
CONTAINERS=$(pct list | awk 'NR>1 {print $1, $3}' | xargs -n2)
@@ -42,7 +42,7 @@ select_container() {
fi
CONTAINER_ID=$(whiptail --title "$(translate 'Select Container')" \
--menu "$(translate 'Select the LXC container:')" 15 60 8 $CONTAINERS 3>&1 1>&2 2>&3)
--menu "$(translate 'Select the LXC container:')" 20 70 10 $CONTAINERS 3>&1 1>&2 2>&3)
if [ -z "$CONTAINER_ID" ]; then
msg_error "$(translate 'No container selected. Exiting.')"
@@ -59,14 +59,14 @@ select_container() {
# Validate that the selected container is valid
validate_container_id() {
if [ -z "$CONTAINER_ID" ]; then
msg_error "$(translate 'Container ID not defined. Make sure to select a container first.')"
exit 1
fi
# Check if the container is running and stop it before configuration
if pct status "$CONTAINER_ID" | grep -q "running"; then
msg_info "$(translate 'Stopping the container before applying configuration...')"
pct stop "$CONTAINER_ID"
@@ -76,77 +76,103 @@ validate_container_id() {
# Configure LXC for Coral TPU and iGPU
configure_lxc_for_igpu() {
validate_container_id
CONFIG_FILE="/etc/pve/lxc/${CONTAINER_ID}.conf"
if [ ! -f "$CONFIG_FILE" ]; then
msg_error "$(translate 'Configuration file for container') $CONTAINER_ID $(translate 'not found.')"
exit 1
[[ -f "$CONFIG_FILE" ]] || { msg_error "$(translate 'Configuration file for container') $CONTAINER_ID $(translate 'not found.')"; exit 1; }
if [[ ! -d /dev/dri ]]; then
modprobe i915 2>/dev/null || true
for _ in {1..5}; do
[[ -d /dev/dri ]] && break
sleep 1
done
fi
if grep -q "^unprivileged: 1" "$CONFIG_FILE"; then
msg_info "$(translate 'The container is unprivileged. Changing to privileged...')"
sed -i "s/^unprivileged: 1/unprivileged: 0/" "$CONFIG_FILE"
STORAGE_TYPE=$(pct config "$CONTAINER_ID" | grep "^rootfs:" | awk -F, '{print $2}' | cut -d'=' -f2)
if [[ "$STORAGE_TYPE" == "dir" ]]; then
STORAGE_PATH=$(pct config "$CONTAINER_ID" | grep "^rootfs:" | awk '{print $2}' | cut -d',' -f1)
chown -R root:root "$STORAGE_PATH"
fi
msg_ok "$(translate 'Container changed to privileged.')"
CT_TYPE=$(pct config "$CONTAINER_ID" | awk '/^unprivileged:/ {print $2}')
[[ -z "$CT_TYPE" ]] && CT_TYPE="0"
msg_info "$(translate 'Configuring Intel iGPU passthrough for container...')"
for rn in /dev/dri/renderD*; do
[[ -e "$rn" ]] || continue
chmod 660 "$rn" 2>/dev/null || true
chgrp render "$rn" 2>/dev/null || true
done
mapfile -t RENDER_NODES < <(find /dev/dri -maxdepth 1 -type c -name 'renderD*' 2>/dev/null || true)
mapfile -t CARD_NODES < <(find /dev/dri -maxdepth 1 -type c -name 'card*' 2>/dev/null || true)
FB_NODE=""
[[ -e /dev/fb0 ]] && FB_NODE="/dev/fb0"
if [[ ${#RENDER_NODES[@]} -eq 0 && ${#CARD_NODES[@]} -eq 0 && -z "$FB_NODE" ]]; then
msg_warn "$(translate 'No VA-API devices found on host (/dev/dri*, /dev/fb0). Is i915 loaded?')"
return 0
fi
if grep -q "^lxc.apparmor.profile" "$CONFIG_FILE"; then
msg_info "$(translate 'Disabling AppArmor profile to avoid conflicts...')"
sed -i "/^lxc.apparmor.profile/d" "$CONFIG_FILE"
msg_ok "$(translate 'AppArmor profile removed.')"
fi
# Configure iGPU
if ! grep -q "features: nesting=1" "$CONFIG_FILE"; then
if grep -q '^features:' "$CONFIG_FILE"; then
grep -Eq '^features:.*(^|,)\s*nesting=1(\s|,|$)' "$CONFIG_FILE" || sed -i 's/^features:\s*/&nesting=1, /' "$CONFIG_FILE"
else
echo "features: nesting=1" >> "$CONFIG_FILE"
fi
if ! grep -q "c 226:0 rwm" "$CONFIG_FILE"; then
echo "lxc.cgroup2.devices.allow: c 226:0 rwm # iGPU" >> "$CONFIG_FILE"
echo "lxc.cgroup2.devices.allow: c 226:128 rwm # iGPU" >> "$CONFIG_FILE"
if [[ "$CT_TYPE" == "0" ]]; then
sed -i '/^lxc\.cgroup2\.devices\.allow:\s*c\s*226:/d' "$CONFIG_FILE"
sed -i '\|^lxc\.mount\.entry:\s*/dev/dri|d' "$CONFIG_FILE"
sed -i '\|^lxc\.mount\.entry:\s*/dev/fb0|d' "$CONFIG_FILE"
echo "lxc.cgroup2.devices.allow: c 226:* rwm" >> "$CONFIG_FILE"
echo "lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir" >> "$CONFIG_FILE"
echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >> "$CONFIG_FILE"
[[ -n "$FB_NODE" ]] && echo "lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file" >> "$CONFIG_FILE"
else
sed -i '/^dev[0-9]\+:/d' "$CONFIG_FILE"
idx=0
for c in "${CARD_NODES[@]}"; do
echo "dev${idx}: $c,gid=44" >> "$CONFIG_FILE"
idx=$((idx+1))
done
for r in "${RENDER_NODES[@]}"; do
echo "dev${idx}: $r,gid=104" >> "$CONFIG_FILE"
idx=$((idx+1))
done
fi
msg_ok "$(translate 'iGPU configuration added to container') $CONTAINER_ID."
if ! grep -q "c 29:0 rwm # Framebuffer" "$CONFIG_FILE"; then
echo "lxc.cgroup2.devices.allow: c 29:0 rwm # Framebuffer" >> "$CONFIG_FILE"
fi
if ! grep -q "lxc.mount.entry: /dev/fb0" "$CONFIG_FILE"; then
echo "lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file" >> "$CONFIG_FILE"
fi
msg_ok "$(translate 'Coral TPU and iGPU configuration added to container') $CONTAINER_ID."
}
# Install iGPU drivers in the container
install_igpu_in_container() {
msg_info2 "$(translate 'Installing iGPU drivers inside the container...')"
tput sc
LOG_FILE=$(mktemp)
msg_info "$(translate 'Installing iGPU drivers...')"
pct start "$CONTAINER_ID" >/dev/null 2>&1
script -q -c "pct exec \"$CONTAINER_ID\" -- bash -c '
set -e
getent group video >/dev/null || groupadd -g 44 video
getent group render >/dev/null || groupadd -g 104 render
usermod -aG video,render root || true
apt-get update >/dev/null 2>&1
apt-get install -y va-driver-all ocl-icd-libopencl1 intel-opencl-icd vainfo intel-gpu-tools
chgrp video /dev/dri && chmod 755 /dev/dri
adduser root video && adduser root render
chgrp video /dev/dri 2>/dev/null || true
chmod 755 /dev/dri 2>/dev/null || true
'" "$LOG_FILE"
if [ $? -eq 0 ]; then
@@ -165,11 +191,14 @@ install_igpu_in_container() {
}
select_container
show_proxmenux_logo
msg_title "$(translate "Add HW iGPU acceleration to an LXC")"
configure_lxc_for_igpu
install_igpu_in_container
msg_success "$(translate 'iGPU configuration completed in container') $CONTAINER_ID."
sleep 2
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r

File diff suppressed because it is too large Load Diff

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

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
@@ -31,7 +31,7 @@ if [[ -f "$UTILS_FILE" ]]; then
fi
load_language
initialize_cache
show_proxmenux_logo
# ==========================================================
@@ -187,7 +187,12 @@ is_disk_in_use() {
FREE_DISKS=()
LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -n1 readlink -f | sort -u)
LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -r -n1 readlink -f | sort -u)
if [[ -n "$LVM_DEVICES" ]] && echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then
IS_MOUNTED=true
fi
RAID_ACTIVE=$(grep -Po 'md\d+\s*:\s*active\s+raid[0-9]+' /proc/mdstat | awk '{print $1}' | sort -u)
while read -r DISK; do

944
scripts/emergency_repair.sh Normal file
View File

@@ -0,0 +1,944 @@
#!/bin/bash
# ==========================================================
# ProxMenux - Network Management and Repair Tool
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 2.0
# Last Updated: 07/01/2025
# ==========================================================
# Description:
# Advanced network management and troubleshooting tool for Proxmox VE.
# Features include interface detection, bridge management, connectivity testing,
# network diagnostics, configuration backup/restore, and automated repairs.
# Configuration ============================================
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
BACKUP_DIR="/var/backups/proxmenux"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# ==========================================================
# Utility Functions
create_backup_dir() {
[ ! -d "$BACKUP_DIR" ] && mkdir -p "$BACKUP_DIR"
}
backup_network_config() {
create_backup_dir
local timestamp=$(date +"%Y-%m-%d_%H-%M-%S")
local backup_file="$BACKUP_DIR/interfaces_backup_$timestamp"
cp /etc/network/interfaces "$backup_file"
echo "$backup_file"
}
# ==========================================================
# Network Detection Functions
detect_network_method() {
# Detect Netplan
if compgen -G "/etc/netplan/*.yaml" > /dev/null; then
echo "netplan"
return 0
fi
# Detect systemd-networkd
if systemctl is-active --quiet systemd-networkd 2>/dev/null; then
echo "systemd-networkd"
return 0
fi
# Detect NetworkManager
if systemctl is-active --quiet NetworkManager 2>/dev/null; then
echo "networkmanager"
return 0
fi
# Default: Debian/Proxmox classic
echo "classic"
}
detect_physical_interfaces() {
ip -o link show | awk -F': ' '$2 !~ /^(lo|veth|dummy|bond|tap|tun|docker|br-)/ && $2 !~ /vmbr/ {print $2}' | sort
}
detect_bridge_interfaces() {
ip -o link show | awk -F': ' '$2 ~ /^vmbr/ {print $2}' | sort
}
detect_all_interfaces() {
ip -o link show | awk -F': ' '$2 !~ /^(lo|veth|dummy|tap|tun)/ {print $2}' | sort
}
get_interface_info() {
local interface="$1"
local info=""
# Get IP address
local ip=$(ip -4 addr show "$interface" 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}/\d+' | head -1)
[ -z "$ip" ] && ip="$(translate "No IP")"
# Get status
local status=$(ip link show "$interface" 2>/dev/null | grep -o "state [A-Z]*" | cut -d' ' -f2)
[ -z "$status" ] && status="UNKNOWN"
# Get MAC address
local mac=$(ip link show "$interface" 2>/dev/null | grep -o "link/ether [a-f0-9:]*" | cut -d' ' -f2)
[ -z "$mac" ] && mac="$(translate "No MAC")"
echo "$interface|$ip|$status|$mac"
}
# ==========================================================
# Network Testing Functions
test_connectivity() {
local test_results=""
local tests=(
"8.8.8.8|Google DNS"
"1.1.1.1|Cloudflare DNS"
"$(ip route | grep default | awk '{print $3}' | head -1)|Gateway"
)
show_proxmenux_logo
msg_info "$(translate "Test Connectivity")"
test_results+="$(translate "Connectivity Test Results")\n"
test_results+="$(printf '=%.0s' {1..35})\n\n"
for test in "${tests[@]}"; do
IFS='|' read -r target name <<< "$test"
if [ -n "$target" ] && [ "$target" != "" ]; then
if ping -c 2 -W 3 "$target" >/dev/null 2>&1; then
test_results+="$name ($target): $(translate "OK")\n"
else
test_results+="$name ($target): $(translate "FAILED")\n"
fi
fi
done
# DNS Resolution test
if nslookup google.com >/dev/null 2>&1; then
test_results+="$(translate "DNS Resolution"): $(translate "OK")\n"
else
test_results+="$(translate "DNS Resolution"): $(translate "FAILED")\n"
fi
cleanup
dialog --backtitle "ProxMenux" --title "$(translate "Connectivity Test")" \
--msgbox "$test_results" 15 60
}
advanced_network_diagnostics() {
NETWORK_METHOD=$(detect_network_method)
if [[ "$NETWORK_METHOD" != "classic" ]]; then
dialog --title "Unsupported Network Stack" \
--msgbox "WARNING: This script only supports the classic Debian/Proxmox network configuration (/etc/network/interfaces).\n\nDetected: $NETWORK_METHOD.\n\nAborting for safety.\n\nPlease configure your network using your distribution's supported tools." 14 70
exit 1
fi
show_proxmenux_logo
msg_info "$(translate "Advanced Diagnostics")"
sleep 1
local diag_info=""
diag_info+="$(translate "Advanced Network Diagnostics")\n"
diag_info+="$(printf '=%.0s' {1..40})\n\n"
# Network statistics
diag_info+="$(translate "Active Connections"): $(ss -tuln | wc -l)\n"
diag_info+="$(translate "Listening Ports"): $(ss -tln | grep LISTEN | wc -l)\n"
diag_info+="$(translate "Network Interfaces"): $(ip link show | grep -c "^[0-9]")\n\n"
# Check for common issues
diag_info+="$(translate "Common Issues Check"):\n"
# Check if NetworkManager is running (shouldn't be on Proxmox)
if systemctl is-active --quiet NetworkManager 2>/dev/null; then
diag_info+="$(translate "NetworkManager is running (may cause conflicts)")\n"
if dialog --title "$(translate "NetworkManager Detected")" \
--yesno "$(translate "NetworkManager is running, which may conflict with Proxmox.")\n\n$(translate "Do you want to disable and remove it now?")" 10 70; then
dialog --infobox "$(translate "Disabling and removing NetworkManager...")" 6 60
systemctl stop NetworkManager >/dev/null 2>&1
systemctl disable NetworkManager >/dev/null 2>&1
apt-get purge -y network-manager >/dev/null 2>&1
diag_info+="$(translate "NetworkManager has been removed successfully")\n"
else
diag_info+=" $(translate "User chose not to remove NetworkManager")\n"
fi
else
diag_info+="$(translate "NetworkManager not running")\n"
fi
# Check for duplicate IPs
local ips=($(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | sort | uniq -d))
if [ ${#ips[@]} -gt 0 ]; then
diag_info+="$(translate "Duplicate IP addresses found"): ${ips[*]}\n"
else
diag_info+="$(translate "No duplicate IP addresses")\n"
fi
cleanup
dialog --backtitle "ProxMenux" --title "$(translate "Network Diagnostics")" \
--msgbox "$diag_info" 18 70
}
# ==========================================================
# SAFE Network Analysis Functions (NO AUTO-REPAIR)
# ==========================================================
analyze_bridge_configuration() {
NETWORK_METHOD=$(detect_network_method)
if [[ "$NETWORK_METHOD" != "classic" ]]; then
dialog --title "Unsupported Network Stack" \
--msgbox "WARNING: This script only supports the classic Debian/Proxmox network configuration (/etc/network/interfaces).\n\nDetected: $NETWORK_METHOD.\n\nAborting for safety.\n\nPlease configure your network using your distribution's supported tools." 14 70
exit 1
fi
show_proxmenux_logo
msg_info "$(translate "Analyzing Bridge Configuration - READ ONLY MODE")"
sleep 1
local physical_interfaces=($(detect_physical_interfaces))
local bridges=($(detect_bridge_interfaces))
local analysis_report=""
local issues_found=0
local suggestions=""
analysis_report+="🔍 $(translate "BRIDGE CONFIGURATION ANALYSIS")\n"
analysis_report+="$(printf '=%.0s' {1..50})\n\n"
cleanup
if [ ${#bridges[@]} -eq 0 ]; then
analysis_report+=" $(translate "No bridges found in system")\n"
dialog --backtitle "ProxMenux" --title "$(translate "Bridge Analysis")" --msgbox "$analysis_report" 10 60
return
fi
# Analyze each bridge
for bridge in "${bridges[@]}"; do
analysis_report+="🌉 $(translate "Bridge"): $bridge\n"
# Get current configuration
local current_ports=$(grep -A5 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep "bridge-ports" | cut -d' ' -f2-)
local bridge_ip=$(ip -4 addr show "$bridge" 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}/\d+' | head -1)
local bridge_status=$(ip link show "$bridge" 2>/dev/null | grep -o "state [A-Z]*" | cut -d' ' -f2)
analysis_report+=" 📍 $(translate "Status"): ${bridge_status:-UNKNOWN}\n"
analysis_report+=" 🌐 $(translate "IP"): ${bridge_ip:-$(translate "No IP assigned")}\n"
analysis_report+=" 🔌 $(translate "Configured Ports"): ${current_ports:-$(translate "None")}\n"
if [ -n "$current_ports" ]; then
local invalid_ports=""
local valid_ports=""
# Check each configured port
for port in $current_ports; do
if ip link show "$port" >/dev/null 2>&1; then
valid_ports+="$port "
analysis_report+="$(translate "Port") $port: $(translate "EXISTS")\n"
else
invalid_ports+="$port "
analysis_report+="$(translate "Port") $port: $(translate "NOT FOUND")\n"
((issues_found++))
fi
done
# Generate suggestions for invalid ports
if [ -n "$invalid_ports" ]; then
suggestions+="🔧 $(translate "SUGGESTION FOR") $bridge:\n"
if [ ${#physical_interfaces[@]} -gt 0 ]; then
suggestions+=" $(translate "Replace invalid port(s)") '$invalid_ports' $(translate "with"): ${physical_interfaces[0]}\n"
suggestions+=" $(translate "Command"): sed -i 's/bridge-ports.*/bridge-ports ${physical_interfaces[0]}/' /etc/network/interfaces\n"
else
suggestions+=" $(translate "Remove invalid port(s)") '$invalid_ports'\n"
suggestions+=" $(translate "Command"): sed -i 's/bridge-ports.*/bridge-ports none/' /etc/network/interfaces\n"
fi
suggestions+="\n"
fi
else
analysis_report+=" ⚠️ $(translate "No ports configured")\n"
if [ ${#physical_interfaces[@]} -gt 0 ]; then
suggestions+="🔧 $(translate "SUGGESTION FOR") $bridge:\n"
suggestions+=" $(translate "Consider adding physical interface"): ${physical_interfaces[0]}\n"
suggestions+=" $(translate "Command"): sed -i '/iface $bridge/a\\ bridge-ports ${physical_interfaces[0]}' /etc/network/interfaces\n\n"
fi
fi
analysis_report+="\n"
done
# Summary
analysis_report+="📊 $(translate "ANALYSIS SUMMARY")\n"
analysis_report+="$(printf '=%.0s' {1..25})\n"
analysis_report+="$(translate "Bridges analyzed"): ${#bridges[@]}\n"
analysis_report+="$(translate "Issues found"): $issues_found\n"
local auto_only=$(grep "^auto" /etc/network/interfaces | awk '{print $2}' | while read i; do
grep -q "^iface $i" /etc/network/interfaces || echo "$i"
done)
if [ -n "$auto_only" ]; then
analysis_report+="⚠️ $(translate "Interfaces defined with 'auto' but no 'iface' block"): $auto_only\n"
((issues_found++))
fi
analysis_report+="$(translate "Physical interfaces available"): ${#physical_interfaces[@]}\n\n"
if [ $issues_found -gt 0 ]; then
analysis_report+="$suggestions"
analysis_report+="⚠️ $(translate "IMPORTANT"): $(translate "No changes have been made to your system")\n"
analysis_report+="$(translate "Use the Guided Repair option to fix issues safely")\n"
else
analysis_report+="$(translate "No bridge configuration issues found")\n"
fi
# Show analysis in scrollable dialog
local temp_file=$(mktemp)
echo -e "$analysis_report" > "$temp_file"
dialog --backtitle "ProxMenux" --title "$(translate "Bridge Configuration Analysis")" \
--textbox "$temp_file" 25 80
rm -f "$temp_file"
# Offer guided repair if issues found
if [ $issues_found -gt 0 ]; then
if dialog --backtitle "ProxMenux" --title "$(translate "Guided Repair Available")" \
--yesno "$(translate "Issues were found. Would you like to use the Guided Repair Assistant?")" 8 60; then
guided_bridge_repair
fi
fi
}
guided_bridge_repair() {
local step=1
local total_steps=5
local timestamp=$(date +"%Y%m%d_%H%M%S")
local preview_backup_file="$BACKUP_DIR/interfaces_backup_$timestamp"
if ! dialog --backtitle "ProxMenux" --title "$(translate "Step") $step/$total_steps: $(translate "Safety Backup")" \
--yesno "$(translate "Before making any changes, we'll create a safety backup.")\n\n$(translate "Backup location"): $preview_backup_file\n\n$(translate "Continue?")" 12 70; then
return
fi
((step++))
show_proxmenux_logo
local backup_file=$(backup_network_config)
sleep 1
dialog --backtitle "ProxMenux" --title "$(translate "Backup Created")" \
--msgbox "$(translate "Safety backup created"): $backup_file\n\n$(translate "You can restore it anytime with"):\ncp $backup_file /etc/network/interfaces" 10 70
# Step 2: Show current configuration
if ! dialog --backtitle "ProxMenux" --title "$(translate "Step") $step/$total_steps: $(translate "Current Configuration")" \
--yesno "$(translate "Let's review your current network configuration.")\n\n$(translate "Would you like to see the current") /etc/network/interfaces $(translate "file?")" 10 70; then
return
fi
((step++))
# Show current config
local temp_config=$(mktemp)
cat /etc/network/interfaces > "$temp_config"
dialog --backtitle "ProxMenux" --title "$(translate "Current Network Configuration")" \
--textbox "$temp_config" 20 80
rm -f "$temp_config"
# Step 3: Identify specific changes needed
local physical_interfaces=($(detect_physical_interfaces))
local bridges=($(detect_bridge_interfaces))
local changes_needed=""
for bridge in "${bridges[@]}"; do
local current_ports=$(grep -A5 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep "bridge-ports" | cut -d' ' -f2-)
if [ -n "$current_ports" ]; then
for port in $current_ports; do
if ! ip link show "$port" >/dev/null 2>&1; then
if [ ${#physical_interfaces[@]} -gt 0 ]; then
changes_needed+="$(translate "Bridge") $bridge: $(translate "Replace") '$port' $(translate "with") '${physical_interfaces[0]}'\n"
else
changes_needed+="$(translate "Bridge") $bridge: $(translate "Remove invalid port") '$port'\n"
fi
fi
done
fi
done
if [ -z "$changes_needed" ]; then
dialog --backtitle "ProxMenux" --title "$(translate "No Changes Needed")" \
--msgbox "$(translate "After detailed analysis, no changes are needed.")" 8 50
return
fi
if ! dialog --backtitle "ProxMenux" --title "$(translate "Step") $step/$total_steps: $(translate "Proposed Changes")" \
--yesno "$(translate "These are the changes that will be made"):\n\n$changes_needed\n$(translate "Do you want to proceed?")" 15 70; then
return
fi
((step++))
# Step 4: Apply changes with verification
dialog --backtitle "ProxMenux" --title "$(translate "Step") $step/$total_steps: $(translate "Applying Changes")" \
--infobox "$(translate "Applying changes safely...")\n\n$(translate "This may take a few seconds...")" 8 50
# Apply the changes
for bridge in "${bridges[@]}"; do
local current_ports=$(grep -A5 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep "bridge-ports" | cut -d' ' -f2-)
if [ -n "$current_ports" ]; then
local new_ports=""
for port in $current_ports; do
if ip link show "$port" >/dev/null 2>&1; then
new_ports+="$port "
fi
done
# If no valid ports and we have physical interfaces, use the first one
if [ -z "$new_ports" ] && [ ${#physical_interfaces[@]} -gt 0 ]; then
new_ports="${physical_interfaces[0]}"
fi
# Apply the change
if [ "$new_ports" != "$current_ports" ]; then
sed -i "/iface $bridge/,/bridge-ports/ s/bridge-ports.*/bridge-ports $new_ports/" /etc/network/interfaces
fi
fi
done
((step++))
# Step 5: Verification
local verification_report=""
verification_report+="$(translate "CHANGES APPLIED SUCCESSFULLY")\n\n"
verification_report+="$(translate "Verification"):\n"
for bridge in "${bridges[@]}"; do
local new_ports=$(grep -A5 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep "bridge-ports" | cut -d' ' -f2-)
verification_report+="$(translate "Bridge") $bridge: $new_ports\n"
# Verify each port exists
for port in $new_ports; do
if ip link show "$port" >/dev/null 2>&1; then
verification_report+="$port: $(translate "EXISTS")\n"
else
verification_report+="$port: $(translate "NOT FOUND")\n"
fi
done
done
verification_report+="\n$(translate "Backup available at"): $backup_file\n"
verification_report+="$(translate "To restore"): cp $backup_file /etc/network/interfaces"
dialog --backtitle "ProxMenux" --title "$(translate "Step") $step/$total_steps: $(translate "Repair Complete")" \
--msgbox "$verification_report" 18 70
# Ask about network restart
if dialog --backtitle "ProxMenux" --title "$(translate "Network Restart")" \
--yesno "$(translate "Changes have been applied to the configuration file.")\n\n$(translate "Do you want to restart the network service to apply changes?")\n\n$(translate "WARNING: This may cause a brief disconnection.")" 12 70; then
clear
msg_info "$(translate "Restarting network service...")"
if systemctl restart networking; then
msg_ok "$(translate "Network service restarted successfully")"
else
msg_error "$(translate "Failed to restart network service")"
msg_warn "$(translate "You can restore the backup with"): cp $backup_file /etc/network/interfaces"
fi
msg_success "$(translate "Press ENTER to continue...")"
read -r
fi
}
analyze_network_configuration() {
NETWORK_METHOD=$(detect_network_method)
if [[ "$NETWORK_METHOD" != "classic" ]]; then
dialog --title "Unsupported Network Stack" \
--msgbox "WARNING: This script only supports the classic Debian/Proxmox network configuration (/etc/network/interfaces).\n\nDetected: $NETWORK_METHOD.\n\nAborting for safety.\n\nPlease configure your network using your distribution's supported tools." 14 70
exit 1
fi
show_proxmenux_logo
msg_info "$(translate "Analyzing Network Configuration - READ ONLY MODE")"
sleep 1
local configured_interfaces=($(grep "^iface" /etc/network/interfaces | awk '{print $2}' | grep -v "lo"))
local analysis_report=""
local issues_found=0
local suggestions=""
analysis_report+="🔍 $(translate "NETWORK CONFIGURATION ANALYSIS")\n"
analysis_report+="$(printf '=%.0s' {1..50})\n\n"
cleanup
if [ ${#configured_interfaces[@]} -eq 0 ]; then
analysis_report+=" $(translate "No network interfaces configured (besides loopback)")\n"
dialog --title "$(translate "Configuration Analysis")" --msgbox "$analysis_report" 10 60
return
fi
analysis_report+="📋 $(translate "CONFIGURED INTERFACES")\n"
analysis_report+="$(printf '=%.0s' {1..30})\n"
# Analyze each configured interface
for iface in "${configured_interfaces[@]}"; do
analysis_report+="🔌 $(translate "Interface"): $iface\n"
# Check if interface exists physically
if ip link show "$iface" >/dev/null 2>&1; then
local status=$(ip link show "$iface" 2>/dev/null | grep -o "state [A-Z]*" | cut -d' ' -f2)
local ip=$(ip -4 addr show "$iface" 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}/\d+' | head -1)
analysis_report+="$(translate "Status"): $(translate "EXISTS") ($status)\n"
analysis_report+=" 🌐 $(translate "IP"): ${ip:-$(translate "No IP assigned")}\n"
# Check if it's a bridge or bond (these are virtual, so it's normal they exist)
if [[ $iface =~ ^(vmbr|bond) ]]; then
analysis_report+=" $(translate "Type"): $(translate "Virtual interface (normal)")\n"
else
analysis_report+=" $(translate "Type"): $(translate "Physical interface")\n"
fi
else
analysis_report+="$(translate "Status"): $(translate "NOT FOUND")\n"
analysis_report+=" ⚠️ $(translate "Issue"): $(translate "Configured but doesn't exist")\n"
((issues_found++))
# Only suggest removal for non-virtual interfaces
if [[ ! $iface =~ ^(vmbr|bond) ]]; then
suggestions+="🔧 $(translate "SUGGESTION FOR") $iface:\n"
suggestions+=" $(translate "This interface is configured but doesn't exist physically")\n"
suggestions+=" $(translate "Consider removing its configuration")\n"
suggestions+=" $(translate "Command"): sed -i '/iface $iface/,/^$/d' /etc/network/interfaces\n\n"
fi
fi
analysis_report+="\n"
done
# Summary
analysis_report+="📊 $(translate "ANALYSIS SUMMARY")\n"
analysis_report+="$(printf '=%.0s' {1..25})\n"
analysis_report+="$(translate "Interfaces configured"): ${#configured_interfaces[@]}\n"
analysis_report+="$(translate "Issues found"): $issues_found\n\n"
if [ $issues_found -gt 0 ]; then
analysis_report+="$suggestions"
analysis_report+="⚠️ $(translate "IMPORTANT"): $(translate "No changes have been made to your system")\n"
analysis_report+="$(translate "Use the Guided Cleanup option to fix issues safely")\n"
else
analysis_report+="$(translate "No configuration issues found")\n"
fi
# Show analysis in scrollable dialog
local temp_file=$(mktemp)
echo -e "$analysis_report" > "$temp_file"
dialog --backtitle "ProxMenux" --title "$(translate "Network Configuration Analysis")" \
--textbox "$temp_file" 25 80
rm -f "$temp_file"
# Offer guided cleanup if issues found
if [ $issues_found -gt 0 ]; then
if dialog --backtitle "ProxMenux" --title "$(translate "Guided Cleanup Available")" \
--yesno "$(translate "Issues were found. Would you like to use the Guided Cleanup Assistant?")" 8 60; then
guided_configuration_cleanup
fi
fi
}
guided_configuration_cleanup() {
local step=1
local total_steps=5
local timestamp=$(date +"%Y%m%d_%H%M%S")
local preview_backup_file="$BACKUP_DIR/interfaces_backup_$timestamp"
if ! dialog --backtitle "ProxMenux" --title "$(translate "Step") $step/$total_steps: $(translate "Safety Backup")" \
--yesno "$(translate "Before making any changes, we'll create a safety backup.")\n\n$(translate "Backup location"): $preview_backup_file\n\n$(translate "Continue?")" 12 70; then
return
fi
((step++))
show_proxmenux_logo
local backup_file=$(backup_network_config)
sleep 1
dialog --backtitle "ProxMenux" --title "$(translate "Backup Created")" \
--msgbox "$(translate "Safety backup created"): $backup_file\n\n$(translate "You can restore it anytime with"):\ncp $backup_file /etc/network/interfaces" 10 70
# Step 2: Identify interfaces to remove
local configured_interfaces=($(grep "^iface" /etc/network/interfaces | awk '{print $2}' | grep -v "lo"))
local interfaces_to_remove=""
local removal_list=""
for iface in "${configured_interfaces[@]}"; do
if [[ ! $iface =~ ^(vmbr|bond) ]] && ! ip link show "$iface" >/dev/null 2>&1; then
interfaces_to_remove+="$iface "
removal_list+="$iface: $(translate "Configured but doesn't exist")\n"
fi
done
if [ -z "$interfaces_to_remove" ]; then
dialog --backtitle "ProxMenux" --title "$(translate "No Cleanup Needed")" \
--msgbox "$(translate "After detailed analysis, no cleanup is needed.")" 8 50
return
fi
if ! dialog --backtitle "ProxMenux" --title "$(translate "Step") $step/$total_steps: $(translate "Interfaces to Remove")" \
--yesno "$(translate "These interface configurations will be removed"):\n\n$removal_list\n$(translate "Do you want to proceed?")" 15 70; then
return
fi
((step++))
# Step 3: Show what will be removed
local temp_preview=$(mktemp)
echo "$(translate "Configuration sections that will be REMOVED"):" > "$temp_preview"
echo "=================================================" >> "$temp_preview"
echo "" >> "$temp_preview"
for iface in $interfaces_to_remove; do
echo "# Interface: $iface" >> "$temp_preview"
sed -n "/^iface $iface/,/^$/p" /etc/network/interfaces >> "$temp_preview"
echo "" >> "$temp_preview"
done
if ! dialog --backtitle "ProxMenux" --title "$(translate "Step") $step/$total_steps: $(translate "Preview Changes")" \
--yesno "$(translate "Review what will be removed"):\n\n$(translate "Press OK to see the preview, then confirm")" 10 60; then
rm -f "$temp_preview"
return
fi
dialog --backtitle "ProxMenux" --title "$(translate "Configuration to be Removed")" \
--textbox "$temp_preview" 20 80
rm -f "$temp_preview"
if ! dialog --backtitle "ProxMenux" --title "$(translate "Final Confirmation")" \
--yesno "$(translate "Are you sure you want to remove these configurations?")" 8 60; then
return
fi
((step++))
# Step 4: Apply changes
dialog --backtitle "ProxMenux" --title "$(translate "Step") $step/$total_steps: $(translate "Applying Changes")" \
--infobox "$(translate "Removing invalid configurations...")\n\n$(translate "This may take a few seconds...")" 8 50
for iface in $interfaces_to_remove; do
sed -i "/^iface $iface/,/^$/d" /etc/network/interfaces
done
((step++))
# Step 5: Verification
local verification_report=""
verification_report+="$(translate "CLEANUP COMPLETED SUCCESSFULLY")\n\n"
verification_report+="$(translate "Removed configurations for"):\n"
for iface in $interfaces_to_remove; do
verification_report+="$iface\n"
done
verification_report+="\n$(translate "Verification"): $(translate "Checking remaining interfaces")\n"
local remaining_interfaces=($(grep "^iface" /etc/network/interfaces | awk '{print $2}' | grep -v "lo"))
for iface in "${remaining_interfaces[@]}"; do
if ip link show "$iface" >/dev/null 2>&1; then
verification_report+="$iface: $(translate "OK")\n"
else
verification_report+="⚠️ $iface: $(translate "Still has issues")\n"
fi
done
verification_report+="\n$(translate "Backup available at"): $backup_file\n"
verification_report+="$(translate "To restore"): cp $backup_file /etc/network/interfaces"
dialog --backtitle "ProxMenux" --title "$(translate "Step") $step/$total_steps: $(translate "Cleanup Complete")" \
--msgbox "$verification_report" 18 70
}
# ==========================================================
# Configuration Management
show_network_config() {
NETWORK_METHOD=$(detect_network_method)
if [[ "$NETWORK_METHOD" != "classic" ]]; then
dialog --title "Unsupported Network Stack" \
--msgbox "WARNING: This script only supports the classic Debian/Proxmox network configuration (/etc/network/interfaces).\n\nDetected: $NETWORK_METHOD.\n\nAborting for safety.\n\nPlease configure your network using your distribution's supported tools." 14 70
exit 1
fi
local config_content
config_content=$(cat /etc/network/interfaces)
show_proxmenux_logo
echo -e
echo -e
echo "========== $(translate "Network Configuration File") =========="
echo
cat /etc/network/interfaces
echo
msg_success "$(translate "Press Enter to continue...")"
read -r
}
restore_network_backup() {
NETWORK_METHOD=$(detect_network_method)
if [[ "$NETWORK_METHOD" != "classic" ]]; then
dialog --title "Unsupported Network Stack" \
--msgbox "WARNING: This script only supports the classic Debian/Proxmox network configuration (/etc/network/interfaces).\n\nDetected: $NETWORK_METHOD.\n\nAborting for safety.\n\nPlease configure your network using your distribution's supported tools." 14 70
exit 1
fi
local backups=($(ls -1 "$BACKUP_DIR"/interfaces_backup_* 2>/dev/null | sort -r))
if [ ${#backups[@]} -eq 0 ]; then
dialog --backtitle "ProxMenux" --title "$(translate "No Backups")" \
--msgbox "\n$(translate "No network configuration backups found.")" 14 60
return
fi
local menu_items=()
local counter=1
for backup in "${backups[@]}"; do
local filename=$(basename "$backup")
local timestamp=$(basename "$backup" | sed 's/interfaces_backup_//')
menu_items+=("$counter" "$timestamp")
((counter++))
done
local selection=$(dialog --backtitle "ProxMenux" --title "$(translate "Restore Backup")" \
--menu "$(translate "Select backup to restore:"):" 15 60 8 \
"${menu_items[@]}" 3>&1 1>&2 2>&3)
if [ -n "$selection" ] && [ "$selection" -ge 1 ] && [ "$selection" -le ${#backups[@]} ]; then
local selected_backup="${backups[$((selection-1))]}"
if dialog --backtitle "ProxMenux" --title "$(translate "Preview Backup")" \
--yesno "\n$(translate "Do you want to view the selected backup before restoring?")" 8 60; then
dialog --backtitle "ProxMenux" --title "$(translate "Backup Preview")" \
--textbox "$selected_backup" 22 80
fi
if dialog --backtitle "ProxMenux" --title "$(translate "Confirm Restore")" \
--yesno "\n$(translate "Are you sure you want to restore this backup?\nCurrent configuration will be overwritten.")\n\n$(translate "For your safety, a backup of the current configuration will be created automatically before restoring.")" 14 70; then
local pre_restore_backup=$(backup_network_config)
cp "$selected_backup" /etc/network/interfaces
dialog --backtitle "ProxMenux" --title "$(translate "Backup Restored")" \
--msgbox "\n$(translate "Network configuration has been restored from backup.")" 8 60
if dialog --backtitle "ProxMenux" --title "$(translate "Restart Network")" \
--yesno "\n$(translate "Do you want to restart the network service now to apply changes?")" 8 60; then
if systemctl restart networking; then
dialog --backtitle "ProxMenux" --title "$(translate "Network Restarted")" \
--msgbox "\n$(translate "Network service restarted successfully.")" 8 50
fi
fi
fi
fi
}
# ==========================================================
# Emergency System Repair Functions
# ==========================================================
emergency_proxmox_repair() {
clear
show_proxmenux_logo
echo -e
echo "=========================================="
echo " $(translate "EMERGENCY PROXMOX SYSTEM REPAIR")"
echo "=========================================="
echo
msg_warn "$(translate "This will reinstall core Proxmox packages and regenerate certificates")"
echo "$(translate "This operation may take several minutes and requires internet connectivity.")"
echo
echo -n "$(translate "Do you want to continue?") (y/N): "
read -r confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
msg_info2 "$(translate "Operation cancelled by user.")"
return
fi
msg_info2 "$(translate "Starting Proxmox system repair...")"
echo
# Step 1: Update package lists
msg_success "$(translate "Step") 1/3: $(translate "Updating package lists...")"
if apt-get update; then
msg_ok "$(translate "Package lists updated successfully")"
else
msg_error "$(translate "Failed to update package lists")"
echo "$(translate "This might indicate network connectivity issues.")"
echo
echo "$(translate "Press ENTER to continue...")"
read -r
return 1
fi
echo
# Step 2: Reinstall core Proxmox packages
msg_success "$(translate "Step") 2/3: $(translate "Reinstalling core Proxmox packages...")"
echo "$(translate "This may take several minutes...")"
if apt-get install --reinstall proxmox-widget-toolkit pve-manager -y; then
msg_ok "$(translate "Core Proxmox packages reinstalled successfully")"
else
msg_error "$(translate "Failed to reinstall Proxmox packages")"
echo "$(translate "Check the error messages above for details.")"
echo
echo "$(translate "Press ENTER to continue...")"
read -r
return 1
fi
echo
# Step 3: Regenerate certificates and restart services
msg_success "$(translate "Step") 3/3: $(translate "Regenerating certificates and restarting services...")"
# Update certificates
if command -v pvecm >/dev/null 2>&1; then
msg_info "$(translate "Updating cluster certificates...")"
if pvecm updatecerts -f; then
msg_ok "$(translate "Cluster certificates updated")"
else
msg_warn "$(translate "Failed to update cluster certificates (might not be in a cluster)")"
fi
else
msg_warn "$(translate "pvecm command not found (might not be in a cluster)")"
fi
# Restart Proxmox services
msg_success "$(translate "Restarting Proxmox services...")"
local services_restarted=0
local services_failed=0
for service in pveproxy pvedaemon; do
if systemctl restart "$service"; then
msg_ok " $service $(translate "restarted successfully")"
((services_restarted++))
else
msg_error " $(translate "Failed to restart") $service"
((services_failed++))
fi
done
echo
echo "$(translate "REPAIR SUMMARY"):"
echo "==============="
echo " $(translate "Package lists"): $(translate "Updated")"
echo " $(translate "Core packages"): $(translate "Reinstalled")"
echo " $(translate "Services restarted"): $services_restarted"
echo " $(translate "Services failed"): $services_failed"
if [ $services_failed -eq 0 ]; then
msg_ok "$(translate "Proxmox system repair completed successfully!")"
echo
echo "$(translate "You should now be able to access the Proxmox web interface.")"
echo "$(translate "Try accessing"): https://$(hostname -I | awk '{print $1}'):8006"
else
msg_warn "$(translate "Proxmox system repair completed with some issues.")"
echo "$(translate "Check the service status manually if needed.")"
fi
echo
echo "$(translate "Press ENTER to continue...")"
read -r
}
restart_network_service() {
if dialog --title "$(translate "Restart Network")" \
--yesno "$(translate "This will restart the network service and may cause a brief disconnection. Continue?")" 10 60; then
show_proxmenux_logo
msg_info "$(translate "Restarting network service...")"
if systemctl restart networking; then
msg_ok "$(translate "Network service restarted successfully")"
else
msg_error "$(translate "Failed to restart network service")"
msg_warn "$(translate "If you lose connectivity, you can restore from backup using the console.")"
fi
msg_success "$(translate "Press ENTER to continue...")"
read -r
fi
}
# ==========================================================
# Main Menu
show_main_menu() {
while true; do
local selection=$(dialog --clear \
--backtitle "ProxMenux" \
--title "$(translate "Network Management - SAFE MODE")" \
--menu "$(translate "Select an option:"):" 20 70 12 \
"1" "$(translate "Test Connectivity")" \
"2" "$(translate "Advanced Diagnostics")" \
"3" "$(translate "Analyze Bridge Configuration")" \
"4" "$(translate "Analyze Network Configuration")" \
"5" "$(translate "Restart Network Service")" \
"6" "$(translate "Show Network Config File")" \
"7" "$(translate "Emergency Proxmox System Repair")" \
"8" "$(translate "Restore Network Backup")" \
"0" "$(translate "Exit")" \
3>&1 1>&2 2>&3)
case $selection in
1) test_connectivity ;;
2) advanced_network_diagnostics ;;
3) analyze_bridge_configuration ;;
4) analyze_network_configuration ;;
5) restart_network_service ;;
6) show_network_config ;;
7) emergency_proxmox_repair ;;
8) restore_network_backup ;;
0|"") exit ;;
esac
done
}
# ==========================================================
show_main_menu

View File

@@ -0,0 +1,338 @@
#!/bin/bash
# ==========================================================
# Common Functions for Proxmox VE Scripts
# ==========================================================
# 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
get_pve_info() {
local pve_full_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
local pve_major=$(echo "$pve_full_version" | cut -d. -f1)
local os_codename="$(grep "VERSION_CODENAME=" /etc/os-release | cut -d"=" -f 2 | xargs)"
if [ -z "$os_codename" ]; then
os_codename=$(lsb_release -cs 2>/dev/null)
fi
local target_codename
if [ "$pve_major" -ge 9 ] 2>/dev/null; then
target_codename="trixie"
else
target_codename="$os_codename"
if [ -z "$target_codename" ]; then
target_codename="bookworm"
fi
fi
echo "$pve_full_version|$pve_major|$os_codename|$target_codename"
}
lvm_repair_check() {
msg_info "$(translate "Checking and repairing old LVM PV headers (if needed)...")"
if ! command -v pvs >/dev/null 2>&1; then
msg_info "$(translate "LVM tools not available, skipping LVM check")"
return
fi
pvs_output=$(LC_ALL=C pvs -v 2>&1 | grep "old PV header" || true)
if [ -z "$pvs_output" ]; then
msg_ok "$(translate "No PVs with old headers found.")"
return
fi
declare -A vg_map
while read -r line; do
pv=$(echo "$line" | grep -o '/dev/[^ ]*' || true)
if [ -n "$pv" ]; then
vg=$(pvs -o vg_name --noheadings "$pv" 2>/dev/null | awk '{print $1}' || true)
if [ -n "$vg" ]; then
vg_map["$vg"]=1
fi
fi
done <<< "$pvs_output"
for vg in "${!vg_map[@]}"; do
msg_warn "$(translate "Old PV header(s) found in VG $vg. Updating metadata...")"
vgck --updatemetadata "$vg" 2>/dev/null
vgchange -ay "$vg" 2>/dev/null
if [ $? -ne 0 ]; then
msg_warn "$(translate "Metadata update failed for VG $vg. Review manually.")"
else
msg_ok "$(translate "Metadata updated successfully for VG $vg")"
fi
done
msg_ok "$(translate "LVM PV headers check completed")"
}
cleanup_duplicate_repos_pve9() {
msg_info "$(translate "Cleaning up duplicate repositories...")"
local sources_file="/etc/apt/sources.list"
local temp_file=$(mktemp)
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
echo "$line" >> "$temp_file"
continue
fi
if [[ "$line" =~ ^deb ]]; then
read -r _ url dist components <<< "$line"
local key="${url}_${dist}"
if [[ -v "seen_repos[$key]" ]]; then
echo "# $line" >> "$temp_file"
cleaned_count=$((cleaned_count + 1))
msg_info "$(translate "Commented duplicate: $url $dist")"
else
echo "$line" >> "$temp_file"
seen_repos[$key]="$components"
fi
else
echo "$line" >> "$temp_file"
fi
done < "$sources_file"
mv "$temp_file" "$sources_file"
chmod 644 "$sources_file"
if [ -f "/etc/apt/sources.list.d/proxmox.sources" ]; then
if grep -q "^deb.*download\.proxmox\.com" "$sources_file"; then
sed -i '/^deb.*download\.proxmox\.com/s/^/# /' "$sources_file"
cleaned_count=$((cleaned_count + 1))
fi
for list_file in /etc/apt/sources.list.d/pve-*.list; do
if [ -f "$list_file" ] && [[ "$list_file" != "/etc/apt/sources.list.d/pve-enterprise.list" ]]; then
if grep -q "^deb" "$list_file"; then
sed -i 's/^deb/# deb/g' "$list_file"
cleaned_count=$((cleaned_count + 1))
fi
fi
done
if [ -f "/etc/apt/sources.list.d/debian.sources" ]; then
if grep -q "^deb.*deb\.debian\.org" "$sources_file"; then
sed -i '/^deb.*deb\.debian\.org/s/^/# /' "$sources_file"
cleaned_count=$((cleaned_count + 1))
fi
if grep -q "^deb.*security\.debian\.org" "$sources_file"; then
sed -i '/^deb.*security\.debian\.org/s/^/# /' "$sources_file"
cleaned_count=$((cleaned_count + 1))
fi
fi
fi
if [ -f "/etc/apt/sources.list.d/proxmox.sources" ]; then
for old_file in /etc/apt/sources.list.d/pve-public-repo.list /etc/apt/sources.list.d/pve-install-repo.list; do
if [ -f "$old_file" ]; then
rm -f "$old_file"
cleaned_count=$((cleaned_count + 1))
fi
done
fi
if [ $cleaned_count -gt 0 ]; then
msg_ok "$(translate "Cleaned up $cleaned_count duplicate/old repositories")"
apt-get update > /dev/null 2>&1 || true
else
msg_ok "$(translate "No duplicate repositories found")"
fi
}
cleanup_duplicate_repos_pve9_() {
msg_info "$(translate "Cleaning up duplicate repositories...")"
local sources_file="/etc/apt/sources.list"
local temp_file=$(mktemp)
local cleaned_count=0
declare -A seen_repos
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "$line" ]]; then
echo "$line" >> "$temp_file"
continue
fi
if [[ "$line" =~ ^deb ]]; then
read -r _ url dist components <<< "$line"
local key="${url}_${dist}"
if [[ -v "seen_repos[$key]" ]]; then
echo "# $line" >> "$temp_file"
cleaned_count=$((cleaned_count + 1))
else
echo "$line" >> "$temp_file"
seen_repos[$key]="$components"
fi
else
echo "$line" >> "$temp_file"
fi
done < "$sources_file"
mv "$temp_file" "$sources_file"
chmod 644 "$sources_file"
for src in proxmox debian ceph; do
local sources_path="/etc/apt/sources.list.d/${src}.sources"
if [ -f "$sources_path" ]; then
case "$src" in
proxmox)
url_match="download.proxmox.com"
;;
debian)
url_match="deb.debian.org"
;;
ceph)
url_match="download.proxmox.com/ceph"
;;
*)
url_match=""
;;
esac
if [[ -n "$url_match" ]]; then
if grep -q "^deb.*$url_match" "$sources_file"; then
sed -i "/^deb.*$url_match/s/^/# /" "$sources_file"
cleaned_count=$((cleaned_count + 1))
fi
fi
for list_file in /etc/apt/sources.list.d/*.list; do
[[ -f "$list_file" ]] || continue
if grep -q "^deb.*$url_match" "$list_file"; then
sed -i "/^deb.*$url_match/s/^/# /" "$list_file"
cleaned_count=$((cleaned_count + 1))
fi
done
fi
done
if [ $cleaned_count -gt 0 ]; then
msg_ok "$(translate "Cleaned up $cleaned_count duplicate/old repositories")"
apt-get update > /dev/null 2>&1 || true
else
msg_ok "$(translate "No duplicate repositories found")"
fi
}
cleanup_duplicate_repos_pve8() {
msg_info "$(translate "Cleaning up duplicate repositories...")"
local cleaned_count=0
local sources_file="/etc/apt/sources.list"
if [[ -f "$sources_file" ]]; then
local temp_file
temp_file=$(mktemp)
declare -A seen_repos
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "$line" ]]; then
echo "$line" >> "$temp_file"
continue
fi
if [[ "$line" =~ ^[[:space:]]*deb ]]; then
read -r _ url dist components <<< "$line"
local key="${url}_${dist}"
if [[ -v "seen_repos[$key]" ]]; then
echo "# $line" >> "$temp_file"
cleaned_count=$((cleaned_count + 1))
else
echo "$line" >> "$temp_file"
seen_repos[$key]="$components"
fi
else
echo "$line" >> "$temp_file"
fi
done < "$sources_file"
mv "$temp_file" "$sources_file"
chmod 644 "$sources_file"
fi
local old_pve_files=(/etc/apt/sources.list.d/pve-*.list /etc/apt/sources.list.d/proxmox.list)
for file in "${old_pve_files[@]}"; do
if [[ -f "$file" ]]; then
local base_name
base_name=$(basename "$file" .list)
local sources_equiv="/etc/apt/sources.list.d/${base_name}.sources"
if [[ -f "$sources_equiv" ]] && grep -q "^Enabled: *true" "$sources_equiv"; then
msg_info "$(translate "Removing old repository file: $(basename "$file")")"
rm -f "$file"
cleaned_count=$((cleaned_count + 1))
fi
fi
done
if [ "$cleaned_count" -gt 0 ]; then
msg_ok "$(translate "Cleaned up $cleaned_count duplicate/old repositories")"
apt-get update > /dev/null 2>&1 || true
else
msg_ok "$(translate "No duplicate repositories found")"
fi
}
cleanup_duplicate_repos() {
local pve_version
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
if [[ -z "$pve_version" ]]; then
msg_error "Unable to detect Proxmox version."
return 1
fi
if [[ "$pve_version" -ge 9 ]]; then
cleanup_duplicate_repos_pve9
else
cleanup_duplicate_repos_pve8
fi
}

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

View File

@@ -0,0 +1,76 @@
#!/bin/bash
# ==========================================================
# Remove Subscription Banner - Proxmox VE 8.4.9
# ==========================================================
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"
}
remove_subscription_banner_pve8() {
local JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
local GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
local APT_HOOK="/etc/apt/apt.conf.d/no-nag-script"
local BACKUP_FILE="${JS_FILE}.bak.$(date +%F_%T)"
local pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1)
local pve_major=$(echo "$pve_version" | cut -d. -f1)
if [[ "$pve_major" -ne 8 ]]; then
msg_error "This script is only for Proxmox VE 8.x. Detected: $pve_version"
return 1
fi
msg_info "Detected Proxmox VE $pve_version - Applying safe JS patch..."
if [[ ! -f "$JS_FILE" ]]; then
msg_error "JavaScript file not found: $JS_FILE"
return 1
fi
cp "$JS_FILE" "$BACKUP_FILE"
sed -i "s/No valid subscription/Subscription active/g" "$JS_FILE"
sed -i "s/Ext.Msg.WARNING/Ext.Msg.INFO/g" "$JS_FILE"
sed -i "s/res.data.status.toLowerCase() !== 'active'/false/g" "$JS_FILE"
sed -i "s/subscriptionActive: ''/subscriptionActive: true/g" "$JS_FILE"
[[ -f "$GZ_FILE" ]] && rm -f "$GZ_FILE"
find /var/cache/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
find /var/lib/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
[[ -f "$APT_HOOK" ]] && rm -f "$APT_HOOK"
msg_ok "Subscription banner removed successfully."
register_tool "subscription_banner" true
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
remove_subscription_banner_pve8
fi

View File

@@ -0,0 +1,124 @@
#!/bin/bash
# ==========================================================
# Remove Subscription Banner - Proxmox VE 9.x
# ==========================================================
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"
}
remove_subscription_banner_pve9() {
local JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
local MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
local GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
local APT_HOOK="/etc/apt/apt.conf.d/no-nag-script"
local pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1)
local pve_major=$(echo "$pve_version" | cut -d. -f1)
if [ "$pve_major" -lt 9 ] 2>/dev/null; then
msg_error "This script is for PVE 9.x only. Detected PVE $pve_version"
return 1
fi
msg_info "Detected Proxmox VE $pve_version - Applying PVE 9.x patches"
if [ ! -f "$JS_FILE" ]; then
msg_error "JavaScript file not found: $JS_FILE"
return 1
fi
local backup_file="${JS_FILE}.backup.pve9.$(date +%Y%m%d_%H%M%S)"
cp "$JS_FILE" "$backup_file"
for f in /etc/apt/apt.conf.d/*nag*; do
[[ -e "$f" ]] && rm -f "$f"
done
[[ -f "$GZ_FILE" ]] && rm -f "$GZ_FILE"
[[ -f "$MIN_JS_FILE" ]] && rm -f "$MIN_JS_FILE"
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
sed -i "s/res\.data\.status\.toLowerCase() !== 'active'/false/g" "$JS_FILE"
sed -i "s/subscriptionActive: ''/subscriptionActive: true/g" "$JS_FILE"
sed -i "s/title: gettext('No valid subscription')/title: gettext('Community Edition')/g" "$JS_FILE"
sed -i "s/You do not have a valid subscription for this server/Community Edition - No subscription required/g" "$JS_FILE"
sed -i "s/Enterprise repository needs valid subscription/Enterprise repository configured/g" "$JS_FILE"
sed -i "s/icon: Ext\.Msg\.WARNING/icon: Ext.Msg.INFO/g" "$JS_FILE"
sed -i "s/subscription = !(/subscription = false \&\& (/g" "$JS_FILE"
if grep -q "res\.data\.status\.toLowerCase() !== 'active'" "$JS_FILE"; then
msg_warn "Some patches may not have applied correctly, retrying..."
sed -i "s/res\.data\.status\.toLowerCase() !== 'active'/false/g" "$JS_FILE"
fi
[[ -f "$APT_HOOK" ]] && rm -f "$APT_HOOK"
cat > "$APT_HOOK" << 'EOF'
DPkg::Post-Invoke {
"test -e /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js && sed -i 's/res\\.data\\.status\\.toLowerCase() !== '\''active'\''/false/g' /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js || true";
"test -e /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js && sed -i 's/subscriptionActive: '\'\'\''/subscriptionActive: true/g' /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js || true";
"test -e /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js && sed -i 's/title: gettext('\''No valid subscription'\'')/title: gettext('\''Community Edition'\'')/g' /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js || true";
"test -e /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js && sed -i 's/subscription = !(/subscription = false \\&\\& (/g' /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js || true";
"rm -f /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz || true";
};
EOF
chmod 644 "$APT_HOOK"
if ! apt-config dump >/dev/null 2>&1; then
msg_warn "APT hook has syntax issues, removing..."
rm -f "$APT_HOOK"
else
msg_ok "APT hook created successfully"
fi
systemctl reload nginx 2>/dev/null || true
msg_ok "Subscription banner removed successfully for Proxmox VE $pve_version"
msg_ok "Banner removal process completed - refresh your browser to see changes"
register_tool "subscription_banner" true
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
remove_subscription_banner_pve9
fi

View File

@@ -0,0 +1,119 @@
#!/bin/bash
# ==========================================================
# Remove Subscription Banner - Proxmox VE 9.x ONLY
# ==========================================================
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
# Tool registration system
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"
}
remove_subscription_banner_pve9() {
local JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
local MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
local GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
local APT_HOOK="/etc/apt/apt.conf.d/no-nag-script"
# Verify PVE 9.x
local pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1)
local pve_major=$(echo "$pve_version" | cut -d. -f1)
if [ "$pve_major" -lt 9 ] 2>/dev/null; then
msg_error "This script is for PVE 9.x only. Detected PVE $pve_version"
return 1
fi
msg_info "Detected Proxmox VE $pve_version - Applying PVE 9.x patches"
# Verify that the file exists
if [ ! -f "$JS_FILE" ]; then
msg_error "JavaScript file not found: $JS_FILE"
return 1
fi
# Create backup of original file
local backup_file="${JS_FILE}.backup.pve9.$(date +%Y%m%d_%H%M%S)"
cp "$JS_FILE" "$backup_file"
# Clean any existing problematic APT hooks
for f in /etc/apt/apt.conf.d/*nag*; do
[[ -e "$f" ]] && rm -f "$f"
done
# Main subscription check patches for PVE 9
sed -i "s/res\.data\.status\.toLowerCase() !== 'active'/false/g" "$JS_FILE"
sed -i "s/subscriptionActive: ''/subscriptionActive: true/g" "$JS_FILE"
sed -i "s/title: gettext('No valid subscription')/title: gettext('Community Edition')/g" "$JS_FILE"
# Additional UX improvements for PVE 9
sed -i "s/You do not have a valid subscription for this server/Community Edition - No subscription required/g" "$JS_FILE"
sed -i "s/Enterprise repository needs valid subscription/Enterprise repository configured/g" "$JS_FILE"
sed -i "s/icon: Ext\.Msg\.WARNING/icon: Ext.Msg.INFO/g" "$JS_FILE"
# Additional subscription patterns that may exist in PVE 9
sed -i "s/subscription = !(/subscription = false \&\& (/g" "$JS_FILE"
# Remove compressed/minified files to force regeneration
[[ -f "$GZ_FILE" ]] && rm -f "$GZ_FILE"
[[ -f "$MIN_JS_FILE" ]] && rm -f "$MIN_JS_FILE"
# Clear various caches
find /var/cache/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
find /var/lib/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
# Create PVE 9.x specific APT hook
[[ -f "$APT_HOOK" ]] && rm -f "$APT_HOOK"
cat > "$APT_HOOK" << 'EOF'
DPkg::Post-Invoke {
"test -e /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js && sed -i 's/res\\.data\\.status\\.toLowerCase() !== '\''active'\''/false/g' /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js || true";
"test -e /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js && sed -i 's/subscriptionActive: '\'\'\''/subscriptionActive: true/g' /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js || true";
"test -e /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js && sed -i 's/title: gettext('\''No valid subscription'\'')/title: gettext('\''Community Edition'\'')/g' /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js || true";
"test -e /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js && sed -i 's/subscription = !(/subscription = false \\&\\& (/g' /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js || true";
"rm -f /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz || true";
};
EOF
chmod 644 "$APT_HOOK"
# Verify APT hook syntax
if ! apt-config dump >/dev/null 2>&1; then
msg_warn "APT hook has syntax issues, removing..."
rm -f "$APT_HOOK"
else
msg_ok "APT hook created successfully"
fi
msg_ok "Subscription banner removed successfully for Proxmox VE $pve_version"
msg_ok "Banner removal process completed"
register_tool "subscription_banner" true
}
# Execute function if called directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
remove_subscription_banner_pve9
fi

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

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
}

View File

@@ -0,0 +1,339 @@
#!/bin/bash
# ==========================================================
# Proxmox VE Update Script
# ==========================================================
# 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"
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)")"
echo -e
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")"
changes_made=true
fi
if disable_sources_repo "/etc/apt/sources.list.d/ceph.sources"; then
msg_ok "$(translate "Enterprise Proxmox Ceph repository disabled")"
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")")"
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")"
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")"
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_exit_code=$?
if [ $update_exit_code -eq 0 ]; then
msg_ok "$(translate "Package lists updated successfully")"
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")"
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")"
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=$?
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
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
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
msg_info "$(translate "Updating packages...")"
apt-get install pv -y > /dev/null 2>&1
msg_ok "$(translate "Packages updated successfully")"
tput sc
DEBIAN_FRONTEND=noninteractive apt-get -y \
-o Dpkg::Options::='--force-confdef' \
-o Dpkg::Options::='--force-confold' \
dist-upgrade 2>&1 | while IFS= read -r line; do
echo "$line" >> "$log_file"
if [[ "$line" =~ \[[#=\-]+\]\ *[0-9]{1,3}% ]]; then
continue
fi
if [[ "$line" =~ ^(Setting\ up|Unpacking|Preparing\ to\ unpack|Processing\ triggers\ for) ]]; then
package_name=$(echo "$line" | sed -E 's/.*(Setting up|Unpacking|Preparing to unpack|Processing triggers for) ([^ :]+).*/\2/')
[ -z "$package_name" ] && package_name="$(translate "Unknown")"
row=$(( $(tput lines) - 6 ))
tput cup $row 0; printf "%s\n" "$(translate "Installing packages...")"
tput cup $((row + 1)) 0; printf "%s\n" "──────────────────────────────────────────────"
tput cup $((row + 2)) 0; printf "%s %s\n" "$(translate "Package:")" "$package_name"
tput cup $((row + 3)) 0; printf "%s\n" "Progress: [ ] 0%"
tput cup $((row + 4)) 0; printf "%s\n" "──────────────────────────────────────────────"
for i in $(seq 1 10); do
sleep 0.1
progress=$((i * 10))
tput cup $((row + 3)) 9
printf "[%-50s] %3d%%" "$(printf "#%.0s" $(seq 1 $((progress/2))))" "$progress"
done
fi
done
tput rc
tput ed
upgrade_exit_code=${PIPESTATUS[0]}
if [ $upgrade_exit_code -eq 0 ]; then
msg_ok "$(translate "System upgrade completed successfully")"
else
msg_error "$(translate "System upgrade failed. Check log: $log_file")"
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
#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")"
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}$target_version (Debian $OS_CODENAME)${CL}"
msg_ok "$(translate "Proxmox VE 9.x configuration completed.")"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
update_pve9
fi

View File

@@ -0,0 +1,284 @@
#!/bin/bash
# ==========================================================
# Proxmox VE 8.x Update Script
# ==========================================================
# 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_pve8() {
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)"
if [ -z "$OS_CODENAME" ]; then
OS_CODENAME=$(lsb_release -cs 2>/dev/null || echo "bookworm")
fi
download_common_functions
msg_info2 "$(translate "Detected: Proxmox VE 8.x (Debian $OS_CODENAME)")"
echo
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
if [ -f /etc/apt/sources.list.d/pve-enterprise.list ] && grep -q "^deb" /etc/apt/sources.list.d/pve-enterprise.list; then
sed -i "s/^deb/#deb/g" /etc/apt/sources.list.d/pve-enterprise.list
msg_ok "$(translate "Enterprise Proxmox repository disabled")"
changes_made=true
fi
if [ -f /etc/apt/sources.list.d/ceph.list ] && grep -q "^deb" /etc/apt/sources.list.d/ceph.list; then
sed -i "s/^deb/#deb/g" /etc/apt/sources.list.d/ceph.list
msg_ok "$(translate "Enterprise Proxmox Ceph repository disabled")"
changes_made=true
fi
if [ ! -f /etc/apt/sources.list.d/pve-public-repo.list ] || ! grep -q "pve-no-subscription" /etc/apt/sources.list.d/pve-public-repo.list; then
echo "deb http://download.proxmox.com/debian/pve $OS_CODENAME pve-no-subscription" > /etc/apt/sources.list.d/pve-public-repo.list
msg_ok "$(translate "Free public Proxmox repository enabled")"
changes_made=true
fi
local sources_file="/etc/apt/sources.list"
cp "$sources_file" "${sources_file}.backup.$(date +%Y%m%d_%H%M%S)"
if grep -q -E "(debian-security -security|debian main$|debian -updates)" "$sources_file"; then
sed -i '/^deb.*debian-security -security/d' "$sources_file"
sed -i '/^deb.*debian main$/d' "$sources_file"
sed -i '/^deb.*debian -updates/d' "$sources_file"
changes_made=true
msg_ok "$(translate "Malformed repository entries cleaned")"
fi
cat > "$sources_file" << EOF
# Debian $OS_CODENAME repositories
deb http://deb.debian.org/debian $OS_CODENAME main contrib non-free non-free-firmware
deb http://deb.debian.org/debian $OS_CODENAME-updates main contrib non-free non-free-firmware
deb http://security.debian.org/debian-security $OS_CODENAME-security main contrib non-free non-free-firmware
EOF
msg_ok "$(translate "Debian repositories configured for $OS_CODENAME")"
local firmware_conf="/etc/apt/apt.conf.d/no-firmware-warnings.conf"
if [ ! -f "$firmware_conf" ]; then
echo 'APT::Get::Update::SourceListWarnings::NonFreeFirmware "false";' > "$firmware_conf"
fi
cleanup_duplicate_repos
msg_info "$(translate "Updating package lists...")"
if apt-get update > "$log_file" 2>&1; then
msg_ok "$(translate "Package lists updated successfully")"
else
msg_error "$(translate "Failed to update package lists. Check log: $log_file")"
return 1
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=$?
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
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
return 0
fi
local conflicting_packages=$(dpkg -l 2>/dev/null | grep -E "^ii.*(ntp|openntpd|systemd-timesyncd)" | awk '{print $2}')
if [ -n "$conflicting_packages" ]; then
msg_info "$(translate "Removing conflicting utilities...")"
DEBIAN_FRONTEND=noninteractive apt-get -y purge $conflicting_packages >> "$log_file" 2>&1
msg_ok "$(translate "Conflicting utilities removed")"
fi
export DEBIAN_FRONTEND=noninteractive
export APT_LISTCHANGES_FRONTEND=none
export NEEDRESTART_MODE=a
export UCF_FORCE_CONFOLD=1
export DPKG_OPTIONS="--force-confdef --force-confold"
msg_info "$(translate "Performing packages upgrade...")"
apt-get install pv -y > /dev/null 2>&1
total_packages=$(apt-get -s dist-upgrade | grep "^Inst" | wc -l)
msg_ok "$(translate "Packages upgrade successfull")"
if [ "$total_packages" -eq 0 ]; then
total_packages=1
fi
tput civis
tput sc
(
/usr/bin/env \
DEBIAN_FRONTEND=noninteractive \
APT_LISTCHANGES_FRONTEND=none \
NEEDRESTART_MODE=a \
UCF_FORCE_CONFOLD=1 \
apt-get -y \
-o Dpkg::Options::="--force-confdef" \
-o Dpkg::Options::="--force-confold" \
dist-upgrade 2>&1 | \
while IFS= read -r line; do
if [[ "$line" =~ ^(Setting\ up|Unpacking|Preparing\ to\ unpack|Processing\ triggers\ for) ]]; then
package_name=$(echo "$line" | sed -E 's/.*(Setting up|Unpacking|Preparing to unpack|Processing triggers for) ([^ ]+).*/\2/')
[ -z "$package_name" ] && package_name="$(translate "Unknown")"
tput rc
tput ed
row=$(( $(tput lines) - 6 ))
tput cup $row 0; echo "$(translate "Installing packages...")"
tput cup $((row + 1)) 0; echo "──────────────────────────────────────────────"
tput cup $((row + 2)) 0; echo "Package: $package_name"
tput cup $((row + 3)) 0; echo "Progress: [ ] 0%"
tput cup $((row + 4)) 0; echo "──────────────────────────────────────────────"
for i in $(seq 1 10); do
progress=$((i * 10))
tput cup $((row + 3)) 9
printf "[%-50s] %3d%%" "$(printf "#%.0s" $(seq 1 $((progress/2))))" "$progress"
done
fi
done
)
if [ $? -eq 0 ]; then
tput rc
tput ed
tput cnorm
msg_ok "$(translate "System upgrade completed")"
fi
local essential_packages=("zfsutils-linux" "proxmox-backup-restore-image" "chrony")
local missing_packages=()
for package in "${essential_packages[@]}"; do
if ! dpkg -l 2>/dev/null | grep -q "^ii $package "; then
missing_packages+=("$package")
fi
done
if [ ${#missing_packages[@]} -gt 0 ]; then
msg_info "$(translate "Installing essential Proxmox packages...")"
DEBIAN_FRONTEND=noninteractive apt-get -y install "${missing_packages[@]}" >> "$log_file" 2>&1
msg_ok "$(translate "Essential Proxmox packages installed")"
fi
lvm_repair_check
cleanup_duplicate_repos
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")"
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}$target_version (Debian $OS_CODENAME)${CL}"
msg_ok "$(translate "Proxmox VE 8 system update completed successfully")"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
update_pve8
fi

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

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

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"

View File

@@ -1,20 +1,20 @@
#!/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
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# Version : 1.1
# Last Updated: 29/05/2025
# ==========================================================
# Description:
# This script automates the process of importing disk images into Proxmox VE virtual machines (VMs),
# making it easy to attach pre-existing disk files without manual configuration.
#
# Before running the script, ensure that disk images are available in /var/lib/vz/template/images/.
# The script scans this directory for compatible formats (.img, .qcow2, .vmdk) and lists the available files.
# The script scans this directory for compatible formats (.img, .qcow2, .vmdk, .raw) and lists the available files.
#
# Using an interactive menu, you can:
# - Select a VM to attach the imported disk.
@@ -32,201 +32,152 @@ 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
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
load_language
initialize_cache
show_proxmenux_logo
# ==========================================================
# Path where disk images are stored
IMAGES_DIR="/var/lib/vz/template/images/"
# Configuration ============================================
# Initial setup
if [ ! -d "$IMAGES_DIR" ]; then
msg_info "$(translate 'Creating images directory')"
mkdir -p "$IMAGES_DIR"
chmod 755 "$IMAGES_DIR"
msg_ok "$(translate 'Images directory created:') $IMAGES_DIR"
detect_image_dir() {
for store in $(pvesm status -content images | awk 'NR>1 {print $1}'); do
path=$(pvesm path "${store}:template" 2>/dev/null)
if [[ -d "$path" ]]; then
for ext in raw img qcow2 vmdk; do
if compgen -G "$path/*.$ext" > /dev/null; then
echo "$path"
return 0
fi
done
for sub in images iso; do
dir="$path/$sub"
if [[ -d "$dir" ]]; then
for ext in raw img qcow2 vmdk; do
if compgen -G "$dir/*.$ext" > /dev/null; then
echo "$dir"
return 0
fi
done
fi
done
fi
done
for fallback in /var/lib/vz/template/images /var/lib/vz/template/iso; do
if [[ -d "$fallback" ]]; then
for ext in raw img qcow2 vmdk; do
if compgen -G "$fallback/*.$ext" > /dev/null; then
echo "$fallback"
return 0
fi
done
fi
done
return 1
}
IMAGES_DIR=$(detect_image_dir)
if [[ -z "$IMAGES_DIR" ]]; then
dialog --title "$(translate 'No Images Found')" \
--msgbox "$(translate 'Could not find any directory containing disk images')\n\n$(translate 'Make sure there is at least one file with extension .img, .qcow2, .vmdk or .raw')" 15 60
exit 1
fi
# Check if there are any images in the directory
IMAGES=$(ls -A "$IMAGES_DIR" | grep -E "\.(img|qcow2|vmdk)$")
IMAGES=$(ls -A "$IMAGES_DIR" | grep -E "\.(img|qcow2|vmdk|raw)$")
if [ -z "$IMAGES" ]; then
whiptail --title "$(translate 'No Images Found')" \
--msgbox "$(translate 'No images available for import in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk')\n\n$(translate 'Please add some images and try again.')" 15 60
dialog --title "$(translate 'No Disk Images Found')" \
--msgbox "$(translate 'No compatible disk images found in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk, .raw')" 15 60
exit 1
fi
# Display initial message
whiptail --title "$(translate 'Import Disk Image')" --msgbox "$(translate 'Make sure the disk images you want to import are located in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk.')" 15 60
# 1. Select VM
# === Select VM
msg_info "$(translate 'Getting VM list')"
VM_LIST=$(qm list | awk 'NR>1 {print $1" "$2}')
if [ -z "$VM_LIST" ]; then
msg_error "$(translate 'No VMs available in the system')"
exit 1
fi
[[ -z "$VM_LIST" ]] && { msg_error "$(translate 'No VMs available in the system')"; exit 1; }
msg_ok "$(translate 'VM list obtained')"
VMID=$(whiptail --title "$(translate 'Select VM')" --menu "$(translate 'Select the VM where you want to import the disk image:')" 15 60 8 $VM_LIST 3>&1 1>&2 2>&3)
if [ -z "$VMID" ]; then
# msg_error "$(translate 'No VM selected')"
exit 1
fi
VMID=$(whiptail --title "$(translate 'Select VM')" \
--menu "$(translate 'Select the VM where you want to import the disk image:')" 20 70 10 $VM_LIST 3>&1 1>&2 2>&3)
[[ -z "$VMID" ]] && exit 1
# 2. Select storage volume
# === Select storage
msg_info "$(translate 'Getting storage volumes')"
STORAGE_LIST=$(pvesm status -content images | awk 'NR>1 {print $1}')
if [ -z "$STORAGE_LIST" ]; then
msg_error "$(translate 'No storage volumes available')"
exit 1
fi
[[ -z "$STORAGE_LIST" ]] && { msg_error "$(translate 'No storage volumes available')"; exit 1; }
msg_ok "$(translate 'Storage volumes obtained')"
# Create an array of storage options for whiptail
STORAGE_OPTIONS=()
while read -r storage; do
STORAGE_OPTIONS+=("$storage" "")
done <<< "$STORAGE_LIST"
STORAGE=$(whiptail --title "$(translate 'Select Storage')" --menu "$(translate 'Select the storage volume for disk import:')" 15 60 8 "${STORAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
if [ -z "$STORAGE" ]; then
# msg_error "$(translate 'No storage selected')"
exit 1
fi
while read -r storage; do STORAGE_OPTIONS+=("$storage" ""); done <<< "$STORAGE_LIST"
STORAGE=$(whiptail --title "$(translate 'Select Storage')" \
--menu "$(translate 'Select the storage volume for disk import:')" 20 70 10 "${STORAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -z "$STORAGE" ]] && exit 1
# 3. Select disk images
msg_info "$(translate 'Scanning disk images')"
if [ -z "$IMAGES" ]; then
msg_warn "$(translate 'No compatible disk images found in') $IMAGES_DIR"
exit 0
fi
msg_ok "$(translate 'Disk images found')"
# === Select images
IMAGE_OPTIONS=()
while read -r img; do
IMAGE_OPTIONS+=("$img" "" "OFF")
done <<< "$IMAGES"
while read -r img; do IMAGE_OPTIONS+=("$img" "" "OFF"); done <<< "$IMAGES"
SELECTED_IMAGES=$(whiptail --title "$(translate 'Select Disk Images')" \
--checklist "$(translate 'Select the disk images to import:')" 20 70 12 "${IMAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -z "$SELECTED_IMAGES" ]] && exit 1
SELECTED_IMAGES=$(whiptail --title "$(translate 'Select Disk Images')" --checklist "$(translate 'Select the disk images to import:')" 20 60 10 "${IMAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
if [ -z "$SELECTED_IMAGES" ]; then
# msg_error "$(translate 'No images selected')"
exit 1
fi
# 4. Import each selected image
# === Import each selected image
for IMAGE in $SELECTED_IMAGES; do
# Remove quotes from selected image
IMAGE=$(echo "$IMAGE" | tr -d '"')
# 5. Select interface type for each image
INTERFACE=$(whiptail --title "$(translate 'Interface Type')" --menu "$(translate 'Select the interface type for the image:') $IMAGE" 15 40 4 \
"sata" "SATA" \
"scsi" "SCSI" \
"virtio" "VirtIO" \
"ide" "IDE" 3>&1 1>&2 2>&3)
if [ -z "$INTERFACE" ]; then
msg_error "$(translate 'No interface type selected for') $IMAGE"
continue
fi
"sata" "SATA" "scsi" "SCSI" "virtio" "VirtIO" "ide" "IDE" 3>&1 1>&2 2>&3)
[[ -z "$INTERFACE" ]] && { msg_error "$(translate 'No interface type selected for') $IMAGE"; continue; }
FULL_PATH="$IMAGES_DIR/$IMAGE"
# Show initial message
msg_info "$(translate 'Importing image:')"
# Temporary file to capture the imported disk
msg_info "$(translate 'Importing image:') $IMAGE"
TEMP_DISK_FILE=$(mktemp)
# Execute the command and process its output in real-time
qm importdisk "$VMID" "$FULL_PATH" "$STORAGE" 2>&1 | while read -r line; do
if [[ "$line" =~ transferred ]]; then
# Extract the progress percentage
PERCENT=$(echo "$line" | grep -oP "\(\d+\.\d+%\)" | tr -d '()%')
# Show progress with custom format without translation
echo -ne "\r${TAB}${YW}-$(translate 'Importing image:') $IMAGE-${CL} ${PERCENT}%"
echo -ne "\r${TAB}${BL}-$(translate 'Importing image:') $IMAGE-${CL} ${PERCENT}%"
elif [[ "$line" =~ successfully\ imported\ disk ]]; then
# Extract the imported disk name and save it to the temporary file
echo "$line" | grep -oP "(?<=successfully imported disk ').*(?=')" > "$TEMP_DISK_FILE"
fi
done
echo -ne "\n"
IMPORT_STATUS=${PIPESTATUS[0]}
IMPORT_STATUS=${PIPESTATUS[0]} # Capture the exit status of the main command
if [ $IMPORT_STATUS -eq 0 ]; then
if [ "$IMPORT_STATUS" -eq 0 ]; then
msg_ok "$(translate 'Image imported successfully')"
# Read the imported disk from the temporary file
IMPORTED_DISK=$(cat "$TEMP_DISK_FILE")
rm -f "$TEMP_DISK_FILE" # Delete the temporary file
rm -f "$TEMP_DISK_FILE"
if [ -n "$IMPORTED_DISK" ]; then
# Find the next available disk slot
EXISTING_DISKS=$(qm config "$VMID" | grep -oP "${INTERFACE}\d+" | sort -n)
if [ -z "$EXISTING_DISKS" ]; then
# If there are no existing disks, start from 0
NEXT_SLOT=0
else
# If there are existing disks, take the last one and add 1
LAST_SLOT=$(echo "$EXISTING_DISKS" | tail -n1 | sed "s/${INTERFACE}//")
NEXT_SLOT=$((LAST_SLOT + 1))
fi
[[ -n "$EXISTING_DISKS" ]] && NEXT_SLOT=$(( $(echo "$EXISTING_DISKS" | tail -n1 | sed "s/${INTERFACE}//") + 1 ))
# Ask if SSD emulation is desired (only for non-VirtIO interfaces)
SSD_OPTION=""
if [ "$INTERFACE" != "virtio" ]; then
if (whiptail --title "$(translate 'SSD Emulation')" --yesno "$(translate 'Do you want to use SSD emulation for this disk?')" 10 60); then
SSD_OPTION=",ssd=1"
else
SSD_OPTION=""
whiptail --yesno "$(translate 'Do you want to use SSD emulation for this disk?')" 10 60 && SSD_OPTION=",ssd=1"
fi
else
SSD_OPTION=""
fi
msg_info "$(translate 'Configuring disk')"
# Configure the disk in the VM
if qm set "$VMID" --${INTERFACE}${NEXT_SLOT} "$IMPORTED_DISK${SSD_OPTION}" &>/dev/null; then
msg_ok "$(translate 'Image') $IMAGE $(translate 'configured as') ${INTERFACE}${NEXT_SLOT}"
# Ask if the disk should be bootable
if (whiptail --title "$(translate 'Make Bootable')" --yesno "$(translate 'Do you want to make this disk bootable?')" 10 60); then
whiptail --yesno "$(translate 'Do you want to make this disk bootable?')" 10 60 && {
msg_info "$(translate 'Configuring disk as bootable')"
if qm set "$VMID" --boot c --bootdisk ${INTERFACE}${NEXT_SLOT} &>/dev/null; then
msg_ok "$(translate 'Disk configured as bootable')"
else
msg_error "$(translate 'Could not configure the disk as bootable')"
fi
fi
}
else
msg_error "$(translate 'Could not configure disk') ${INTERFACE}${NEXT_SLOT} $(translate 'for VM') $VMID"
fi
@@ -239,4 +190,5 @@ for IMAGE in $SELECTED_IMAGES; do
done
msg_ok "$(translate 'All selected images have been processed')"
sleep 2
msg_success "$(translate "Press Enter to return to menu...")"
read -r

View File

@@ -1,13 +1,14 @@
#!/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)
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# Version : 1.1
# Last Updated: 16/05/2025
# ==========================================================
# Description:
# This script automates the configuration and installation of
@@ -17,13 +18,10 @@
# - Installs necessary drivers inside the container
# - Manages required system and container restarts
#
# The script aims to simplify the process of enabling
# AI-powered video analysis capabilities in containers
# LXC, leveraging hardware acceleration for
# improved performance.
# Supports Coral USB and Coral M.2 (PCIe) devices.
# Includes USB passthrough enhancement using persistent udev alias (/dev/coral).
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
@@ -38,10 +36,7 @@ initialize_cache
# ==========================================================
# Select LXC container
select_container() {
CONTAINERS=$(pct list | awk 'NR>1 {print $1, $3}' | xargs -n2)
if [ -z "$CONTAINERS" ]; then
msg_error "$(translate 'No containers available in Proxmox.')"
@@ -49,7 +44,7 @@ select_container() {
fi
CONTAINER_ID=$(whiptail --title "$(translate 'Select Container')" \
--menu "$(translate 'Select the LXC container:')" 15 60 5 $CONTAINERS 3>&1 1>&2 2>&3)
--menu "$(translate 'Select the LXC container:')" 20 70 10 $CONTAINERS 3>&1 1>&2 2>&3)
if [ -z "$CONTAINER_ID" ]; then
msg_error "$(translate 'No container selected. Exiting.')"
@@ -64,15 +59,12 @@ select_container() {
msg_ok "$(translate 'Container selected:') $CONTAINER_ID"
}
# Validate that the selected container is valid
validate_container_id() {
if [ -z "$CONTAINER_ID" ]; then
msg_error "$(translate 'Container ID not defined. Make sure to select a container first.')"
exit 1
fi
# Check if the container is running and stop it before configuration
if pct status "$CONTAINER_ID" | grep -q "running"; then
msg_info "$(translate 'Stopping the container before applying configuration...')"
pct stop "$CONTAINER_ID"
@@ -81,7 +73,48 @@ validate_container_id() {
}
# Configure LXC for Coral TPU and iGPU
add_udev_rule_for_coral_usb_() {
RULE_FILE="/etc/udev/rules.d/99-coral-usb.rules"
RULE_CONTENT='SUBSYSTEM=="usb", ATTRS{idVendor}=="18d1", ATTRS{idProduct}=="9302", MODE="0666", TAG+="uaccess"'
if [[ ! -f "$RULE_FILE" ]] || ! grep -qF "$RULE_CONTENT" "$RULE_FILE"; then
echo "$RULE_CONTENT" > "$RULE_FILE"
udevadm control --reload-rules && udevadm trigger
msg_ok "$(translate 'Udev rule for Coral USB added and rules reloaded.')"
else
msg_ok "$(translate 'Udev rule for Coral USB already exists.')"
fi
}
add_udev_rule_for_coral_usb() {
RULE_FILE="/etc/udev/rules.d/99-coral-usb.rules"
RULE_CONTENT='# Coral USB Accelerator
SUBSYSTEM=="usb", ATTRS{idVendor}=="18d1", ATTRS{idProduct}=="9302", MODE="0666", TAG+="uaccess", SYMLINK+="coral"
# Coral Dev Board / Mini PCIe
SUBSYSTEM=="usb", ATTRS{idVendor}=="1a6e", ATTRS{idProduct}=="089a", MODE="0666", TAG+="uaccess", SYMLINK+="coral"'
if [[ ! -f "$RULE_FILE" ]] || ! grep -q "18d1.*9302\|1a6e.*089a" "$RULE_FILE"; then
echo "$RULE_CONTENT" > "$RULE_FILE"
udevadm control --reload-rules && udevadm trigger
msg_ok "$(translate 'Udev rules for Coral USB devices added and rules reloaded.')"
else
msg_ok "$(translate 'Udev rules for Coral USB devices already exist.')"
fi
}
add_mount_if_needed() {
local DEVICE="$1"
local DEST="$2"
local CONFIG_FILE="$3"
if [ -e "$DEVICE" ] && ! grep -q "lxc.mount.entry: $DEVICE" "$CONFIG_FILE"; then
echo "lxc.mount.entry: $DEVICE $DEST none bind,optional,create=$( [ -c "$DEVICE" ] && echo file || echo dir )" >> "$CONFIG_FILE"
fi
}
configure_lxc_hardware() {
validate_container_id
CONFIG_FILE="/etc/pve/lxc/${CONTAINER_ID}.conf"
@@ -90,6 +123,7 @@ configure_lxc_hardware() {
exit 1
fi
# Privileged container
if grep -q "^unprivileged: 1" "$CONFIG_FILE"; then
msg_info "$(translate 'The container is unprivileged. Changing to privileged...')"
sed -i "s/^unprivileged: 1/unprivileged: 0/" "$CONFIG_FILE"
@@ -103,46 +137,57 @@ configure_lxc_hardware() {
msg_ok "$(translate 'The container is already privileged.')"
fi
# Configure iGPU
sed -i '/^dev[0-9]\+:/d' "$CONFIG_FILE"
# Enable nesting feature
if ! grep -q "features: nesting=1" "$CONFIG_FILE"; then
echo "features: nesting=1" >> "$CONFIG_FILE"
fi
# iGPU support
if ! grep -q "c 226:0 rwm" "$CONFIG_FILE"; then
echo "lxc.cgroup2.devices.allow: c 226:0 rwm # iGPU" >> "$CONFIG_FILE"
echo "lxc.cgroup2.devices.allow: c 226:128 rwm # iGPU" >> "$CONFIG_FILE"
echo "lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir" >> "$CONFIG_FILE"
echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >> "$CONFIG_FILE"
fi
add_mount_if_needed "/dev/dri" "dev/dri" "$CONFIG_FILE"
add_mount_if_needed "/dev/dri/renderD128" "dev/dri/renderD128" "$CONFIG_FILE"
add_mount_if_needed "/dev/dri/card0" "dev/dri/card0" "$CONFIG_FILE"
# Framebuffer support
if ! grep -q "c 29:0 rwm # Framebuffer" "$CONFIG_FILE"; then
echo "lxc.cgroup2.devices.allow: c 29:0 rwm # Framebuffer" >> "$CONFIG_FILE"
fi
add_mount_if_needed "/dev/fb0" "dev/fb0" "$CONFIG_FILE"
if ! grep -q "lxc.mount.entry: /dev/fb0" "$CONFIG_FILE"; then
echo "lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file" >> "$CONFIG_FILE"
fi
# Configure Coral TPU (USB and M.2)
# ----------------------------------------------------------
# Coral USB passthrough (via udev + /dev/coral)
# ----------------------------------------------------------
add_udev_rule_for_coral_usb
if ! grep -Pq "^lxc.cgroup2.devices.allow: c 189:\* rwm # Coral USB$" "$CONFIG_FILE"; then
echo "lxc.cgroup2.devices.allow: c 189:* rwm # Coral USB" >> "$CONFIG_FILE"
fi
add_mount_if_needed "/dev/coral" "dev/coral" "$CONFIG_FILE"
if ! grep -Pq "^lxc.mount.entry: /dev/bus/usb dev/bus/usb none bind,optional,create=dir$" "$CONFIG_FILE"; then
echo "lxc.mount.entry: /dev/bus/usb dev/bus/usb none bind,optional,create=dir" >> "$CONFIG_FILE"
# ----------------------------------------------------------
# Coral M.2 (PCIe) support
# ----------------------------------------------------------
if lspci | grep -iq "Global Unichip"; then
if ! grep -Pq "^lxc.cgroup2.devices.allow: c 245:0 rwm # Coral M2 Apex$" "$CONFIG_FILE"; then
echo "lxc.cgroup2.devices.allow: c 245:0 rwm # Coral M2 Apex" >> "$CONFIG_FILE"
fi
add_mount_if_needed "/dev/apex_0" "dev/apex_0" "$CONFIG_FILE"
fi
if ! grep -Pq "^lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file$" "$CONFIG_FILE"; then
echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >> "$CONFIG_FILE"
fi
msg_ok "$(translate 'Coral TPU and iGPU configuration added to container') $CONTAINER_ID."
}
# Install Coral TPU drivers in the container
install_coral_in_container() {
msg_info2 "$(translate 'Installing iGPU and Coral TPU drivers inside the container...')"
tput sc
LOG_FILE=$(mktemp)
@@ -200,13 +245,12 @@ install_coral_in_container() {
fi
}
select_container
show_proxmenux_logo
configure_lxc_hardware
install_coral_in_container
msg_ok "$(translate 'Configuration completed.')"
sleep 2
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r

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
@@ -68,6 +68,7 @@ verify_and_add_repos() {
# Function to install Coral TPU drivers on the host
install_coral_host() {
show_proxmenux_logo
verify_and_add_repos
apt-get install -y git devscripts dh-dkms dkms pve-headers-$(uname -r) >/dev/null 2>&1
@@ -93,19 +94,23 @@ install_coral_host() {
exit 1
fi
msg_ok "$(translate 'Coral TPU drivers installed successfully on the host.')"
msg_success "$(translate 'Coral TPU drivers installed successfully on the host.')"
echo -e
}
# Prompt for reboot after installation
restart_prompt() {
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
#echo -ne "\r${TAB}${YW}-$(translate 'Restarting the server...') ${CL}"
msg_warn "$(translate 'Restarting the server...')"
reboot
else
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r
fi
}
# Main logic
pre_install_prompt
install_coral_host
restart_prompt

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