556 Commits

Author SHA1 Message Date
ProxMenuxBot 484f117b8e Update helpers_cache.json 2026-03-22 12:05:28 +00:00
MacRimi 83889d7e3c Add discussion template for AI notifications prompts 2026-03-22 00:28:09 +01:00
MacRimi 2eb970a6a2 Create discussion template for custom prompts
Added a discussion template for sharing custom prompts, including fields for prompt name, AI provider, model, output language, description, prompt content, example output, additional notes, and confirmation checkboxes.
2026-03-22 00:18:20 +01:00
MacRimi e3a611f33d Remove star history chart from README
Removed the star history chart HTML section from README.
2026-03-21 11:08:33 +01:00
MacRimi 8fb2a9094e Enhance README with star history chart
Added responsive star history chart with dark and light themes.
2026-03-21 11:07:56 +01:00
ProxMenuxBot d1e7154040 Update helpers_cache.json 2026-03-20 18:10:20 +00:00
ProxMenuxBot e695b4e764 Update helpers_cache.json 2026-03-20 12:08:24 +00:00
ProxMenuxBot b1eae7b768 Update helpers_cache.json 2026-03-19 12:09:20 +00:00
MacRimi 55bb5b5a1c Add beta program details to README
Added a beta program section with installation instructions and feedback guidelines.
2026-03-18 22:16:28 +01:00
ProxMenuxBot e8232a9ea0 Update helpers_cache.json 2026-03-18 18:18:46 +00:00
ProxMenuxBot aeabb99be6 Update helpers_cache.json 2026-03-18 12:14:02 +00:00
MacRimi 38ee6d836d Add GitHub Actions workflow for AppImage beta build 2026-03-18 11:22:29 +01:00
MacRimi 7bb4bd3da5 Rename workflow for building AppImage release 2026-03-18 11:21:26 +01:00
MacRimi 7524615671 Save compiled AppImage files before git operations
Added steps to save compiled AppImage files before cleaning local changes.
2026-03-18 11:15:14 +01:00
MacRimi f5fe883d49 Enhance AppImage workflow to include checksum upload
Updated the workflow to upload both AppImage and checksum artifacts, and modified the commit process to use the current branch instead of main.
2026-03-18 11:11:05 +01:00
MacRimi bec6406216 Update GitHub Actions workflow for AppImage build 2026-03-18 10:48:41 +01:00
ProxMenuxBot ef041f2702 Update helpers_cache.json 2026-03-18 00:17:00 +00:00
MacRimi df0f15419e Add files via upload 2026-03-17 18:07:18 +01:00
ProxMenuxBot dc531eaa37 Update helpers_cache.json 2026-03-17 12:13:15 +00:00
ProxMenuxBot 1eaabd14bd Update helpers_cache.json 2026-03-16 18:18:06 +00:00
ProxMenuxBot c9c8987cca Update helpers_cache.json 2026-03-16 12:14:04 +00:00
ProxMenuxBot 06dc6ea23f Update helpers_cache.json 2026-03-16 00:17:59 +00:00
MacRimi 8b3a76dfc5 Upgrade GitHub Actions to latest versions 2026-03-15 14:14:00 +01:00
ProxMenuxBot 60398210c7 Update helpers_cache.json 2026-03-15 12:06:54 +00:00
ProxMenuxBot 486c7ef530 Update helpers_cache.json 2026-03-15 00:18:01 +00:00
ProxMenuxBot 94131097a5 Update helpers_cache.json 2026-03-14 18:06:03 +00:00
MacRimi 6d69e009dc Rebuild Helper Scripts catalog for new data architecture
Rebuilt the Helper Scripts catalog to connect directly to the PocketBase API, enhancing data structure and script options. Acknowledged contributions from Community Scripts maintainers for their support in the integration.
2026-03-14 18:29:27 +01:00
MacRimi 6d9b132ab8 Bump version from 1.1.8 to 1.1.9 2026-03-14 18:18:44 +01:00
MacRimi efec1aff18 Update script version and improve loading logic
Updated version to 1.3 and last updated date to 14/03/2025. Removed dependency on metadata.json and improved script loading and error handling.
2026-03-14 18:16:55 +01:00
MacRimi 258d6d9a49 Update menu_Helper_Scripts.sh 2026-03-14 18:15:15 +01:00
MacRimi 1c4b7c7b97 Update menu_Helper_Scripts.sh 2026-03-14 18:13:34 +01:00
ProxMenuxBot 8bc6306813 Update helpers_cache.json 2026-03-14 17:04:43 +00:00
MacRimi 2923c00738 Add files via upload 2026-03-14 18:04:09 +01:00
MacRimi b30b6a062a Delete .github/scripts/generate_helpers_cache.py 2026-03-14 18:03:51 +01:00
MacRimi 8f5df889ab Rename helpers_cache__.json to helpers_cache.json 2026-03-14 17:50:43 +01:00
MacRimi 4ec8b19251 Add backup JSON cache file 2026-03-14 17:50:25 +01:00
MacRimi 1035a94775 Rename helpers_cache_back.json to helpers_cache.json 2026-03-14 17:32:12 +01:00
MacRimi 3ca2ae7175 Add helpers_cache__.json file 2026-03-14 17:31:56 +01:00
ProxMenuxBot 4ba1ca890c Update helpers_cache.json 2026-03-14 15:44:57 +00:00
MacRimi cba012bd15 Add backup JSON helpers cache file 2026-03-14 16:44:28 +01:00
MacRimi 9515ccd816 Add files via upload 2026-03-14 16:43:44 +01:00
MacRimi 46622f5028 Add generate_helpers_cache_back.py script 2026-03-14 16:40:25 +01:00
MacRimi 9190c8e5bf Update API_URL for JSON content retrieval 2026-03-13 17:15:01 +01:00
MacRimi 109498e2df Update API_URL to point to Frontend-Archive 2026-03-13 17:08:57 +01:00
MacRimi 60d7c395bc Update Node.js version and add environment variable 2026-03-12 19:35:23 +01:00
ProxMenuxBot 782d847e54 Update helpers_cache.json 2026-03-08 18:04:29 +00:00
ProxMenuxBot d96e4019aa Update helpers_cache.json 2026-03-08 00:15:29 +00:00
ProxMenuxBot 6b438bc4aa Update helpers_cache.json 2026-03-07 00:14:59 +00:00
ProxMenuxBot 50d07f81fd Update helpers_cache.json 2026-03-06 12:08:42 +00:00
ProxMenuxBot 7d69e64adc Update helpers_cache.json 2026-03-05 18:34:30 +00:00
ProxMenuxBot c2fa6095cc Update helpers_cache.json 2026-03-03 12:08:48 +00:00
ProxMenuxBot 0b8b72be5c Update helpers_cache.json 2026-03-02 18:12:26 +00:00
ProxMenuxBot fd6f0967b0 Update helpers_cache.json 2026-03-02 12:08:55 +00:00
ProxMenuxBot ca9698f75d Update helpers_cache.json 2026-02-28 12:05:10 +00:00
ProxMenuxBot 968a5bd789 Update helpers_cache.json 2026-02-27 18:10:15 +00:00
ProxMenuxBot 1fe4ee5b81 Update helpers_cache.json 2026-02-26 12:12:55 +00:00
ProxMenuxBot 137aeac91a Update helpers_cache.json 2026-02-25 18:21:39 +00:00
ProxMenuxBot ccb0b58a2d Update helpers_cache.json 2026-02-25 12:11:18 +00:00
ProxMenuxBot 680123eb64 Update helpers_cache.json 2026-02-24 12:11:48 +00:00
ProxMenuxBot aec04f0b8c Update helpers_cache.json 2026-02-24 00:15:52 +00:00
ProxMenuxBot e75bbc0a22 Update helpers_cache.json 2026-02-23 18:20:58 +00:00
ProxMenuxBot 81fc625c5d Update helpers_cache.json 2026-02-23 12:10:39 +00:00
ProxMenuxBot f85683239f Update helpers_cache.json 2026-02-22 12:05:24 +00:00
ProxMenuxBot c0f54c334e Update helpers_cache.json 2026-02-21 00:16:10 +00:00
ProxMenuxBot 5c2d4e4718 Update helpers_cache.json 2026-02-19 18:16:51 +00:00
ProxMenuxBot 64a0aa6157 Update helpers_cache.json 2026-02-19 12:11:34 +00:00
ProxMenuxBot ff2e40d49a Update helpers_cache.json 2026-02-17 12:10:47 +00:00
ProxMenuxBot 1226e7bee1 Update helpers_cache.json 2026-02-16 12:10:34 +00:00
ProxMenuxBot 342203bb81 Update helpers_cache.json 2026-02-16 00:16:44 +00:00
ProxMenuxBot e4bc526a09 Update helpers_cache.json 2026-02-15 18:05:49 +00:00
ProxMenuxBot 5941bd4b68 Update helpers_cache.json 2026-02-13 12:08:41 +00:00
ProxMenuxBot 1c95319608 Update helpers_cache.json 2026-02-11 12:14:33 +00:00
ProxMenuxBot eeea948844 Update helpers_cache.json 2026-02-11 00:20:59 +00:00
ProxMenuxBot 59bb0070e9 Update helpers_cache.json 2026-02-10 12:15:22 +00:00
ProxMenuxBot ec2206ade0 Update helpers_cache.json 2026-02-09 18:16:40 +00:00
ProxMenuxBot 7796f7d3bc Update helpers_cache.json 2026-02-09 12:14:11 +00:00
ProxMenuxBot b806bf80b1 Update helpers_cache.json 2026-02-08 12:05:47 +00:00
ProxMenuxBot 173ea58701 Update helpers_cache.json 2026-02-08 00:20:03 +00:00
ProxMenuxBot 775b6ff4fd Update helpers_cache.json 2026-02-07 00:15:38 +00:00
ProxMenuxBot b0f18461b3 Update helpers_cache.json 2026-02-06 18:13:29 +00:00
ProxMenuxBot b8ccbfd222 Update helpers_cache.json 2026-02-06 12:09:32 +00:00
ProxMenuxBot c2fa497137 Update helpers_cache.json 2026-02-05 18:16:47 +00:00
ProxMenuxBot bdcfa6929c Update helpers_cache.json 2026-02-05 12:09:37 +00:00
ProxMenuxBot 8470b58b60 Update helpers_cache.json 2026-02-04 18:14:29 +00:00
ProxMenuxBot 002413c067 Update helpers_cache.json 2026-02-04 12:08:26 +00:00
ProxMenuxBot ecce59e734 Update helpers_cache.json 2026-02-04 00:14:14 +00:00
ProxMenuxBot 1935c76f30 Update helpers_cache.json 2026-02-02 18:11:22 +00:00
ProxMenuxBot 81b7a3e665 Update helpers_cache.json 2026-02-02 12:08:58 +00:00
ProxMenuxBot a68bf6fc8f Update helpers_cache.json 2026-02-01 00:16:53 +00:00
ProxMenuxBot 459dd2d9c7 Update helpers_cache.json 2026-01-31 00:14:20 +00:00
ProxMenuxBot fed4cc2a97 Update helpers_cache.json 2026-01-29 19:56:29 +00:00
ProxMenuxBot 7eaa692712 Update helpers_cache.json 2026-01-28 18:06:50 +00:00
ProxMenuxBot 691bae9a96 Update helpers_cache.json 2026-01-28 12:05:34 +00:00
ProxMenuxBot d5a8c9b7d1 Update helpers_cache.json 2026-01-26 18:05:37 +00:00
ProxMenuxBot 8c20e7c661 Update helpers_cache.json 2026-01-25 00:14:01 +00:00
ProxMenuxBot 47a2d28c6a Update helpers_cache.json 2026-01-24 00:12:27 +00:00
ProxMenuxBot 31f8961e27 Update helpers_cache.json 2026-01-23 18:04:28 +00:00
ProxMenuxBot 424bd0bc28 Update helpers_cache.json 2026-01-23 12:05:47 +00:00
ProxMenuxBot 9c078583dd Update helpers_cache.json 2026-01-22 12:05:55 +00:00
ProxMenuxBot ca27048679 Update helpers_cache.json 2026-01-22 00:13:22 +00:00
MacRimi 4e65663748 Update coral lxc 2026-01-20 20:17:53 +01:00
ProxMenuxBot c7c5cbde83 Update helpers_cache.json 2026-01-20 00:12:48 +00:00
ProxMenuxBot a4905ad207 Update helpers_cache.json 2026-01-19 18:04:06 +00:00
MacRimi bebf0e692a Update License 2026-01-19 17:15:00 +01:00
ProxMenuxBot 8ff9a87dfe Update helpers_cache.json 2026-01-19 12:06:36 +00:00
ProxMenuxBot 62f2d8ac16 Update helpers_cache.json 2026-01-19 00:12:53 +00:00
ProxMenuxBot 8fef2a6232 Update helpers_cache.json 2026-01-18 18:04:16 +00:00
ProxMenuxBot 94064fe78c Update helpers_cache.json 2026-01-17 12:05:12 +00:00
ProxMenuxBot 2ffcc43adc Update helpers_cache.json 2026-01-16 18:05:23 +00:00
ProxMenuxBot 3846fce73a Update helpers_cache.json 2026-01-16 12:05:44 +00:00
ProxMenuxBot ea950e9dbc Update helpers_cache.json 2026-01-15 18:07:27 +00:00
ProxMenuxBot f2639c4ff1 Update helpers_cache.json 2026-01-14 18:05:47 +00:00
ProxMenuxBot 32c1798eb8 Update helpers_cache.json 2026-01-13 12:05:29 +00:00
ProxMenuxBot 75e3167b65 Update helpers_cache.json 2026-01-12 18:04:41 +00:00
ProxMenuxBot ad07a61aa7 Update helpers_cache.json 2026-01-08 18:04:25 +00:00
ProxMenuxBot c91b6329f3 Update helpers_cache.json 2026-01-07 00:11:18 +00:00
ProxMenuxBot 9cc60efd5a Update helpers_cache.json 2026-01-03 12:04:28 +00:00
ProxMenuxBot 08eeea6b9c Update helpers_cache.json 2026-01-02 12:05:29 +00:00
ProxMenuxBot 8dea7335de Update helpers_cache.json 2025-12-30 18:04:18 +00:00
ProxMenuxBot 2ad6d43422 Update helpers_cache.json 2025-12-29 00:12:44 +00:00
ProxMenuxBot 12c2e7aefb Update helpers_cache.json 2025-12-28 12:04:17 +00:00
ProxMenuxBot 6b62e46950 Update helpers_cache.json 2025-12-28 00:13:42 +00:00
ProxMenuxBot 853c58e0a0 Update helpers_cache.json 2025-12-26 18:03:55 +00:00
ProxMenuxBot eb0abc425a Update helpers_cache.json 2025-12-26 12:05:50 +00:00
ProxMenuxBot c808e40bf6 Update helpers_cache.json 2025-12-25 12:29:08 +00:00
ProxMenuxBot f0bbb14f3f Update helpers_cache.json 2025-12-24 18:20:56 +00:00
ProxMenuxBot 95dd0ea6fb Update helpers_cache.json 2025-12-24 01:06:53 +00:00
ProxMenuxBot 7f34102ae6 Update helpers_cache.json 2025-12-23 01:07:03 +00:00
ProxMenuxBot 7623962da5 Update helpers_cache.json 2025-12-22 01:10:38 +00:00
ProxMenuxBot cfb34b59df Update helpers_cache.json 2025-12-21 12:25:59 +00:00
ProxMenuxBot e5004bb55e Update helpers_cache.json 2025-12-20 01:03:45 +00:00
ProxMenuxBot c0193fdf73 Update helpers_cache.json 2025-12-19 01:08:01 +00:00
ProxMenuxBot 6cbafd557c Update helpers_cache.json 2025-12-16 12:30:30 +00:00
ProxMenuxBot ee8ab75907 Update helpers_cache.json 2025-12-15 12:32:47 +00:00
MacRimi f2e93ad69e Update route.ts 2025-12-14 02:37:02 +01:00
MacRimi 5faf3fd61c Update route.ts 2025-12-14 02:20:07 +01:00
MacRimi 956a8f4864 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-12-14 02:12:40 +01:00
MacRimi d26bc56b5c Update route.ts 2025-12-14 02:12:26 +01:00
ProxMenuxBot 7457770ef8 Update helpers_cache.json 2025-12-14 01:11:59 +00:00
MacRimi 54af9073cb Update web 2025-12-14 01:57:36 +01:00
MacRimi a8dcf5e8f5 Update web 2025-12-14 01:38:07 +01:00
MacRimi 9e3334d75f Update web 2025-12-13 21:19:08 +01:00
MacRimi cca6e71911 Update web 2025-12-13 20:20:21 +01:00
ProxMenuxBot 7fbd377ab2 Update helpers_cache.json 2025-12-13 18:18:09 +00:00
MacRimi 24417feba3 Update next.config.mjs 2025-12-13 19:09:26 +01:00
ProxMenuxBot f8c24964e3 Update helpers_cache.json 2025-12-13 01:02:58 +00:00
MacRimi 1ae2ebfaf0 Update nvidia_installer.sh 2025-12-11 22:07:01 +01:00
MacRimi 4feea6d153 Update script-terminal-modal.tsx 2025-12-11 21:30:30 +01:00
MacRimi ec6b658685 Update script-terminal-modal.tsx 2025-12-11 21:22:15 +01:00
MacRimi fb0f05a08d Update script-terminal-modal.tsx 2025-12-11 19:56:20 +01:00
MacRimi 11bc477f1f Update script-terminal-modal.tsx 2025-12-11 19:06:53 +01:00
MacRimi 9760375855 Update script-terminal-modal.tsx 2025-12-11 18:44:56 +01:00
MacRimi a6e20bd9f0 Update script-terminal-modal.tsx 2025-12-11 18:27:11 +01:00
MacRimi 90fedbf9a2 Update script-terminal-modal.tsx 2025-12-11 18:19:38 +01:00
MacRimi eb03262abc Update script-terminal-modal.tsx 2025-12-11 18:13:37 +01:00
MacRimi 59eb6e5f1b Update script-terminal-modal.tsx 2025-12-11 18:06:24 +01:00
MacRimi edf513aca9 Update script-terminal-modal.tsx 2025-12-11 17:55:19 +01:00
MacRimi efed63519a Update script-terminal-modal.tsx 2025-12-11 17:44:23 +01:00
MacRimi d78f781506 Update script-terminal-modal.tsx 2025-12-11 17:30:12 +01:00
ProxMenuxBot 93fe269b09 Update helpers_cache.json 2025-12-11 12:31:23 +00:00
ProxMenuxBot 8cad6c4e56 Update helpers_cache.json 2025-12-11 01:07:29 +00:00
MacRimi f92049dc71 Update script-terminal-modal.tsx 2025-12-10 23:48:43 +01:00
MacRimi a3497a9d39 Update script-terminal-modal.tsx 2025-12-10 23:43:05 +01:00
MacRimi bfc0a2ed57 Update script-terminal-modal.tsx 2025-12-10 23:34:30 +01:00
MacRimi c49b45d262 Update script-terminal-modal.tsx 2025-12-10 23:26:11 +01:00
MacRimi 15678cf96a Update script-terminal-modal.tsx 2025-12-10 23:15:50 +01:00
MacRimi feeaaa7f2b Update script-terminal-modal.tsx 2025-12-10 23:06:03 +01:00
MacRimi 50df1a2212 Update script-terminal-modal.tsx 2025-12-10 22:57:19 +01:00
MacRimi ac9254d049 Update script-terminal-modal.tsx 2025-12-10 22:48:07 +01:00
MacRimi e15eeb36a5 Update script-terminal-modal.tsx 2025-12-10 22:30:11 +01:00
MacRimi e275e03d4e Update script-terminal-modal.tsx 2025-12-10 22:19:41 +01:00
MacRimi 41c8826ca8 Update script-terminal-modal.tsx 2025-12-10 22:10:41 +01:00
MacRimi d8af31ba5b Update script-terminal-modal.tsx 2025-12-10 21:57:16 +01:00
MacRimi 2eb7cb1687 Update AppImage 2025-12-10 21:47:52 +01:00
MacRimi 207e75f5b9 Update script-terminal-modal.tsx 2025-12-10 20:17:13 +01:00
MacRimi 7b9e1a71a3 Update script-terminal-modal.tsx 2025-12-10 20:12:03 +01:00
MacRimi 345838c6ce Update script-terminal-modal.tsx 2025-12-10 19:54:39 +01:00
MacRimi b02a60f4b3 Update script-terminal-modal.tsx 2025-12-10 19:44:39 +01:00
MacRimi ecd3a4e490 Update script-terminal-modal.tsx 2025-12-10 19:26:19 +01:00
MacRimi 8c0c9bd60a Update script-terminal-modal.tsx 2025-12-10 19:14:57 +01:00
MacRimi 943a8bf02d Update script-terminal-modal.tsx 2025-12-10 18:54:35 +01:00
MacRimi d3beb72652 Update script-terminal-modal.tsx 2025-12-10 18:36:31 +01:00
MacRimi c62dd2014e Update script-terminal-modal.tsx 2025-12-10 18:22:30 +01:00
MacRimi 62fee7827b Update script-terminal-modal.tsx 2025-12-10 18:15:45 +01:00
MacRimi 80b9d16494 Update AppImage 2025-12-10 18:09:21 +01:00
MacRimi cb5581c49f Update AppImage 2025-12-10 17:56:11 +01:00
MacRimi 0098000ae0 Update terminal-panel.tsx 2025-12-10 17:45:38 +01:00
MacRimi ddc8429499 Update AppImage 2025-12-10 17:35:43 +01:00
MacRimi 0424961d46 Update AppImage 2025-12-10 17:08:41 +01:00
MacRimi cbf510cfd1 Update script-terminal-modal.tsx 2025-12-10 17:01:17 +01:00
MacRimi cbb44ae253 Update script-terminal-modal.tsx 2025-12-10 16:56:56 +01:00
MacRimi 4dd4f045aa Update AppImage 2025-12-10 16:50:52 +01:00
ProxMenuxBot ab0d7f8dc6 Update helpers_cache.json 2025-12-09 12:29:33 +00:00
MacRimi 69f93fcb59 Actualizar select_nas_iso.sh 2025-12-08 22:52:34 +01:00
ProxMenuxBot de68e0d7c2 Update helpers_cache.json 2025-12-08 01:05:59 +00:00
MacRimi cdbcb451e1 Update script-terminal-modal.tsx 2025-12-06 23:41:34 +01:00
MacRimi 105c543a98 Update script-terminal-modal.tsx 2025-12-06 23:25:35 +01:00
MacRimi ab421e3184 Update AppImage 2025-12-06 23:06:18 +01:00
MacRimi d76b7a99b8 Apdate AppImage 2025-12-06 22:40:24 +01:00
MacRimi e8dae63e05 Update script-terminal-modal.tsx 2025-12-06 22:33:17 +01:00
MacRimi ea58b70435 Update script-terminal-modal.tsx 2025-12-06 22:26:42 +01:00
MacRimi f90f6f364a Update script-terminal-modal.tsx 2025-12-06 22:20:34 +01:00
MacRimi 7fc967c64c Update script-terminal-modal.tsx 2025-12-06 22:09:54 +01:00
MacRimi 8969a229d1 Update script-terminal-modal.tsx 2025-12-06 22:03:12 +01:00
MacRimi 9601e0428e Update script-terminal-modal.tsx 2025-12-06 21:55:31 +01:00
MacRimi 94fd91ce4a Update script-terminal-modal.tsx 2025-12-06 21:50:15 +01:00
MacRimi 310f972c7f Update script-terminal-modal.tsx 2025-12-06 21:37:43 +01:00
MacRimi 4378a5843c Update AppImage 2025-12-06 21:30:17 +01:00
MacRimi 9bd403ec51 Update script-terminal-modal.tsx 2025-12-06 21:20:23 +01:00
MacRimi 2f53786ca9 Update terminal-panel.tsx 2025-12-06 20:56:36 +01:00
MacRimi 07ed213c94 Update script-terminal-modal.tsx 2025-12-06 20:46:13 +01:00
MacRimi 05a2eca9a7 Update AppImage 2025-12-06 20:27:00 +01:00
MacRimi d30c836d04 Update AppImage 2025-12-06 20:10:24 +01:00
MacRimi 8c623adad8 Update AppImage 2025-12-06 19:43:41 +01:00
MacRimi 5191edfc0c Update AppImage 2025-12-06 19:31:07 +01:00
MacRimi ff99663d5c Update AppImage 2025-12-06 19:19:54 +01:00
MacRimi 360335a608 Update AppImage 2025-12-06 19:03:19 +01:00
MacRimi 1c83e5eeab Update AppImage 2025-12-06 18:36:34 +01:00
MacRimi 122ebb12f4 Update AppImage 2025-12-06 13:54:37 +01:00
MacRimi fed242315d Update AppImage 2025-12-06 13:48:46 +01:00
MacRimi 84e8e18ef8 Update script-terminal-modal.tsx 2025-12-06 13:38:19 +01:00
MacRimi 36a1916b5f Update script-terminal-modal.tsx 2025-12-06 13:28:56 +01:00
MacRimi a1089460d7 Update AppImage 2025-12-06 13:11:04 +01:00
MacRimi c62f0dea6f Update script-terminal-modal.tsx 2025-12-06 13:00:29 +01:00
MacRimi a6c121dc33 Update AppImage 2025-12-06 12:46:41 +01:00
MacRimi c627c65a7d Update AppImage 2025-12-06 12:25:57 +01:00
MacRimi 72006aff21 Update hardware.tsx 2025-12-06 12:14:08 +01:00
MacRimi 68338ebeff Update flask_terminal_routes.py 2025-12-06 12:06:12 +01:00
MacRimi 49b8503b64 Update AppImage 2025-12-06 11:54:36 +01:00
MacRimi 0fc41df7e7 Update flask_terminal_routes.py 2025-12-06 11:37:16 +01:00
MacRimi bb82c52747 Update AppImage 2025-12-06 11:30:49 +01:00
MacRimi a79367fb1c Update script-terminal-modal.tsx 2025-12-06 11:25:27 +01:00
MacRimi 4dbc6db6f0 Update script-terminal-modal.tsx 2025-12-06 11:19:26 +01:00
ProxMenuxBot a50cee62be Update helpers_cache.json 2025-12-05 18:18:53 +00:00
ProxMenuxBot 382aa5cb16 Update helpers_cache.json 2025-12-05 01:05:55 +00:00
ProxMenuxBot d1c2ff277b Update helpers_cache.json 2025-12-03 01:05:21 +00:00
ProxMenuxBot 92b08b5550 Update helpers_cache.json 2025-12-02 12:30:00 +00:00
ProxMenuxBot da85470fef Update helpers_cache.json 2025-12-02 01:04:45 +00:00
MacRimi 9d1e7d94cc Update script-terminal-modal.tsx 2025-12-01 01:40:04 +01:00
MacRimi 65438286ec Update script-terminal-modal.tsx 2025-12-01 01:26:29 +01:00
MacRimi ffa7d27148 Update script-terminal-modal.tsx 2025-12-01 01:22:04 +01:00
MacRimi 4a7d951d0d Update script-terminal-modal.tsx 2025-12-01 01:15:19 +01:00
MacRimi 89f1911a6e Update package.json 2025-12-01 01:07:41 +01:00
MacRimi b990bd1792 Update AppImage 2025-12-01 01:04:31 +01:00
MacRimi 88667416d8 Update hybrid-script-monitor.tsx 2025-12-01 00:38:42 +01:00
MacRimi 216491012e Update hybrid-script-monitor.tsx 2025-12-01 00:31:23 +01:00
MacRimi c88f3dcf75 Update flask_script_runner.py 2025-12-01 00:17:04 +01:00
MacRimi 6c3e21339d Update hybrid-script-monitor.tsx 2025-11-30 23:57:08 +01:00
MacRimi e7f9f9f13d Update hybrid-script-monitor.tsx 2025-11-30 23:47:33 +01:00
MacRimi 6b8d6da5be Update AppImage 2025-11-30 23:40:13 +01:00
MacRimi 8c73c5d662 Update hybrid-script-monitor.tsx 2025-11-30 23:27:32 +01:00
MacRimi f7dc2c9a9e Update hybrid-script-monitor.tsx 2025-11-30 23:20:34 +01:00
MacRimi eadf825b67 Update hybrid-script-monitor.tsx 2025-11-30 23:13:37 +01:00
MacRimi 150999d71b Update AppImage 2025-11-30 23:00:02 +01:00
MacRimi 7cd89a594e Update AppImage 2025-11-30 22:50:40 +01:00
MacRimi b67f1cb4b8 Update hardware.tsx 2025-11-30 22:12:51 +01:00
MacRimi 4678f8c7da Update hardware.tsx 2025-11-30 21:55:37 +01:00
MacRimi 0577f48437 Update hardware.tsx 2025-11-30 21:49:13 +01:00
MacRimi 0c079482f0 Update hardware.tsx 2025-11-30 21:38:49 +01:00
MacRimi 684fe3945d Update AppImage 2025-11-30 21:19:38 +01:00
MacRimi d91d325744 Update route.ts 2025-11-30 20:36:25 +01:00
MacRimi 040d7564ed Update AppImage 2025-11-30 20:25:44 +01:00
MacRimi d1db34445e Update AppImage 2025-11-30 19:40:42 +01:00
MacRimi 9639dd422a Update AppImage 2025-11-30 19:15:07 +01:00
MacRimi f60bfe8c54 Update nvidia installer 2025-11-30 18:29:05 +01:00
MacRimi fe53c11447 Update utils.sh 2025-11-30 18:05:56 +01:00
MacRimi 9bd17bdf6f Update utils.sh 2025-11-30 17:47:30 +01:00
MacRimi 4b64308951 Update utils.sh 2025-11-30 17:12:11 +01:00
MacRimi bb7dacea91 Update AppImage 2025-11-30 16:02:44 +01:00
MacRimi 0a369621a3 Update nvidia_installer.sh 2025-11-30 15:41:40 +01:00
MacRimi e0ee1a50ae new script nvidia 2025-11-30 15:31:19 +01:00
MacRimi 6b49fc4294 Update utils.sh 2025-11-30 12:57:29 +01:00
ProxMenuxBot ed20ea6af4 Update helpers_cache.json 2025-11-30 01:11:38 +00:00
ProxMenuxBot 73fe4dc7a0 Update helpers_cache.json 2025-11-29 18:18:19 +00:00
MacRimi c4967de530 Update utils.sh 2025-11-29 11:04:28 +01:00
ProxMenuxBot bcf3d36ba1 Update helpers_cache.json 2025-11-28 18:19:11 +00:00
MacRimi d52bd7f012 Update update-pve9_2.sh 2025-11-28 19:10:26 +01:00
MacRimi e6232be244 Update update-pve9_2.sh 2025-11-28 17:31:32 +01:00
MacRimi b33f313e2e Update update-pve9_2.sh 2025-11-28 17:06:59 +01:00
ProxMenuxBot 0b4372fe88 Update helpers_cache.json 2025-11-27 18:19:16 +00:00
MacRimi 4e07c7f2dc Update system-overview.tsx 2025-11-27 18:11:56 +01:00
MacRimi 941e194df3 Update system-overview.tsx 2025-11-27 17:50:26 +01:00
MacRimi 2b8f94f457 Update health_monitor.py 2025-11-27 17:30:19 +01:00
MacRimi 7ec8c0cea5 Update health_monitor.py 2025-11-27 14:56:12 +01:00
MacRimi c69384dabd Update health_monitor.py 2025-11-27 13:39:02 +01:00
MacRimi 8c92216a1d Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-27 13:29:27 +01:00
MacRimi 41537c0bad Update health_monitor.py 2025-11-27 13:29:15 +01:00
ProxMenuxBot c112f56b37 Update helpers_cache.json 2025-11-27 12:28:29 +00:00
MacRimi f22de50527 Update flask_server.py 2025-11-27 12:45:57 +01:00
MacRimi a22e08f39d Update AppImage 2025-11-27 12:34:51 +01:00
MacRimi 210d470473 Update AppImage 2025-11-27 12:17:52 +01:00
MacRimi 0eebb77438 Update AppImage 2025-11-27 11:58:20 +01:00
MacRimi f819cb9c5f Update hardware.tsx 2025-11-27 09:32:41 +01:00
MacRimi 240963f1f3 Update hardware.tsx 2025-11-27 09:29:12 +01:00
MacRimi 16819d98fa Update AppImage 2025-11-27 09:19:45 +01:00
MacRimi 8be7e0f0cb Update hardware.tsx 2025-11-26 21:33:25 +01:00
MacRimi 3a51daf51b Update hardware.tsx 2025-11-26 21:27:56 +01:00
MacRimi 7622e72b70 Update flask_server.py 2025-11-26 21:15:35 +01:00
MacRimi b59173cac4 Update flask_server.py 2025-11-26 20:46:56 +01:00
MacRimi 18411ee5bd Update AppImage 2025-11-26 20:31:09 +01:00
MacRimi 6e1c6fab2d Update flask_server.py 2025-11-26 20:22:10 +01:00
MacRimi 98eb2d8836 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-26 19:38:28 +01:00
MacRimi 504e32f922 Update flask_server.py 2025-11-26 19:38:24 +01:00
ProxMenuxBot c096054b1f Update helpers_cache.json 2025-11-26 18:17:30 +00:00
MacRimi ac2f198851 Update flask_server.py 2025-11-26 18:57:01 +01:00
MacRimi 9aed659f17 Update AppImage 2025-11-26 18:44:37 +01:00
MacRimi 0b8f5d3b22 Update AppImage 2025-11-26 18:00:01 +01:00
MacRimi 55c74e8891 Update AppImage 2025-11-26 17:36:23 +01:00
MacRimi 3a49aa6a67 Update hardware_monitor.py 2025-11-26 16:48:24 +01:00
MacRimi 10770b6fe1 Update AppImage 2025-11-26 12:27:25 +01:00
MacRimi c81ea08f42 Update terminal-panel.tsx 2025-11-25 22:54:27 +01:00
MacRimi 73b6ab4a18 Update terminal-panel.tsx 2025-11-25 22:42:14 +01:00
MacRimi 7497235d7b Update terminal-panel.tsx 2025-11-25 22:35:23 +01:00
MacRimi 27191e4234 Update terminal-panel.tsx 2025-11-25 22:23:02 +01:00
MacRimi 7b0110ce42 Update AppImage 2025-11-25 19:44:40 +01:00
MacRimi 117a635a1e Update sidebar.tsx 2025-11-25 19:31:22 +01:00
MacRimi 98c922fb3e Update AppImage 2025-11-25 19:26:50 +01:00
MacRimi bf84d04f1f Update AppImage 2025-11-25 19:08:00 +01:00
MacRimi f4e358b509 Update AppImge 2025-11-25 19:04:54 +01:00
MacRimi 060ad7966e Update proxmox-dashboard.tsx 2025-11-25 17:21:29 +01:00
MacRimi f0301fd1a4 Update AppImage 2025-11-25 17:21:20 +01:00
MacRimi ae8212a51d Update terminal-panel.tsx 2025-11-24 23:54:46 +01:00
MacRimi 393a0d5cdc Update terminal-panel.tsx 2025-11-24 23:26:36 +01:00
MacRimi 4cf43a8d74 Update terminal-panel.tsx 2025-11-24 23:10:50 +01:00
MacRimi 74b2f47e3a Update terminal-panel.tsx 2025-11-24 22:57:09 +01:00
ProxMenuxBot 1e727db09a Update helpers_cache.json 2025-11-24 18:21:47 +00:00
MacRimi 1daa120d06 Update terminal-panel.tsx 2025-11-24 19:11:11 +01:00
MacRimi a1d2445ae6 Update terminal-panel.tsx 2025-11-24 19:01:15 +01:00
MacRimi 4d4e35e24b Update terminal-panel.tsx 2025-11-24 18:41:42 +01:00
MacRimi 400cc599e3 Update terminal-panel.tsx 2025-11-24 18:28:06 +01:00
MacRimi e55352346b Update terminal-panel.tsx 2025-11-24 18:18:10 +01:00
MacRimi cca226dec0 Update terminal-panel.tsx 2025-11-24 17:50:41 +01:00
MacRimi fec95c91f8 Update terminal-panel.tsx 2025-11-24 17:29:38 +01:00
MacRimi 9955418a8e Remove 'Contributing' section from README 2025-11-24 15:43:10 +01:00
MacRimi 90c7539956 Remove contributing and development setup sections
Removed the contributing section and development setup instructions from the README.
2025-11-24 15:41:52 +01:00
MacRimi a751e45602 Update README.md 2025-11-24 15:40:49 +01:00
MacRimi b50d388f9e Update terminal-panel.tsx 2025-11-24 13:37:17 +01:00
MacRimi fd60292b5d Update terminal-panel.tsx 2025-11-24 13:25:20 +01:00
MacRimi 4ebb0c432e Update terminal-panel.tsx 2025-11-24 13:16:35 +01:00
MacRimi 897b2478e8 Update terminal-panel.tsx 2025-11-24 13:02:04 +01:00
MacRimi b8ebb7f6c4 Update terminal-panel.tsx 2025-11-24 12:24:16 +01:00
MacRimi f32dba72b4 Update terminal-panel.tsx 2025-11-24 12:10:07 +01:00
MacRimi 498ad280e0 Update terminal-panel.tsx 2025-11-24 11:49:20 +01:00
MacRimi 32358de718 Update AppImage 2025-11-24 11:37:00 +01:00
MacRimi 2474a6ce01 Update terminal-panel.tsx 2025-11-24 11:21:50 +01:00
MacRimi 1ba45200ee Update AppImage 2025-11-24 11:01:48 +01:00
MacRimi da793856ce Update terminal-panel.tsx 2025-11-23 23:46:34 +01:00
MacRimi d950588c36 Update terminal-panel.tsx 2025-11-23 23:42:19 +01:00
MacRimi 2b4a5d2ce7 Update terminal-panel.tsx 2025-11-23 23:29:49 +01:00
MacRimi 86daedc802 Update AppImage 2025-11-23 23:23:59 +01:00
MacRimi 3788487196 Update terminal-panel.tsx 2025-11-23 23:10:23 +01:00
MacRimi 25559b7e3e Update terminal-panel.tsx 2025-11-23 22:51:37 +01:00
MacRimi 246db33ee6 Update terminal-panel.tsx 2025-11-23 22:47:30 +01:00
MacRimi d435e9b58b Update terminal-panel.tsx 2025-11-23 22:45:09 +01:00
MacRimi 09ecc79050 Update terminal-panel.tsx 2025-11-23 22:39:46 +01:00
MacRimi 1914435707 Update terminal-panel.tsx 2025-11-23 22:34:50 +01:00
MacRimi f6c237afc5 Update terminal-panel.tsx 2025-11-23 22:30:14 +01:00
MacRimi a1f2579047 Update terminal-panel.tsx 2025-11-23 22:25:21 +01:00
MacRimi 1ea6617a5d Update terminal-panel.tsx 2025-11-23 22:21:24 +01:00
MacRimi 489175aa45 Update AppImage 2025-11-23 22:17:45 +01:00
MacRimi cb72f43b03 Update globals.css 2025-11-23 22:01:50 +01:00
MacRimi 4bbbcc7c39 Update terminal-panel.tsx 2025-11-23 21:43:17 +01:00
MacRimi af1e4884b7 Update AppImage 2025-11-23 21:39:45 +01:00
MacRimi 5213d6255a Update globals.css 2025-11-23 21:32:46 +01:00
MacRimi a9af689aa5 Update terminal-panel.tsx 2025-11-23 21:22:52 +01:00
MacRimi 407a9f7780 Update terminal-panel.tsx 2025-11-23 21:15:54 +01:00
MacRimi a0ca667ca7 Update terminal-panel.tsx 2025-11-23 21:08:14 +01:00
MacRimi c2f6f97c34 Update terminal-panel.tsx 2025-11-23 20:59:50 +01:00
MacRimi 2daefbe2f4 Update terminal-panel.tsx 2025-11-23 20:47:42 +01:00
MacRimi 84b0c9d4b7 Update terminal-panel.tsx 2025-11-23 20:26:26 +01:00
MacRimi 0d848569f0 Update terminal-panel.tsx 2025-11-23 20:19:07 +01:00
MacRimi 611f8397ca Update terminal-panel.tsx 2025-11-23 20:16:05 +01:00
MacRimi 11ed0a1367 Update terminal-panel.tsx 2025-11-23 20:06:33 +01:00
MacRimi ff51966fbb Update terminal-panel.tsx 2025-11-23 19:58:31 +01:00
MacRimi 5491d51eba Update terminal-panel.tsx 2025-11-23 19:53:39 +01:00
MacRimi 61a5a7e929 Update terminal-panel.tsx 2025-11-23 19:46:57 +01:00
MacRimi 3de000bc94 Update terminal-panel.tsx 2025-11-23 19:41:36 +01:00
MacRimi ef456e6ea0 Update terminal-panel.tsx 2025-11-23 19:30:01 +01:00
MacRimi 2a8b67e22a Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-23 19:25:58 +01:00
MacRimi c35b66f6e1 Update terminal-panel.tsx 2025-11-23 19:25:49 +01:00
ProxMenuxBot c8348dcaaa Update helpers_cache.json 2025-11-23 18:18:06 +00:00
MacRimi e38174110e Update terminal-panel.tsx 2025-11-23 19:14:18 +01:00
MacRimi a95130c01f Update terminal-panel.tsx 2025-11-23 19:10:22 +01:00
MacRimi 0e93417090 Update terminal-panel.tsx 2025-11-23 18:56:14 +01:00
MacRimi 07054bf55a Update terminal-panel.tsx 2025-11-23 18:51:46 +01:00
MacRimi 368eab476a Update AppImage 2025-11-23 18:41:58 +01:00
MacRimi 996679a2d2 Update AppImage 2025-11-23 18:34:47 +01:00
MacRimi 85a6943cd5 Update terminal-panel.tsx 2025-11-23 18:17:00 +01:00
MacRimi 0b96893f3b Update terminal-panel.tsx 2025-11-23 18:12:09 +01:00
MacRimi 846e2e27ba Update terminal-panel.tsx 2025-11-23 17:57:46 +01:00
MacRimi 43ea9b7696 Update globals.css 2025-11-23 17:42:00 +01:00
MacRimi 9dd4df2ca9 Update terminal-panel.tsx 2025-11-23 17:29:11 +01:00
MacRimi 2b4fb55526 Update globals.css 2025-11-23 17:17:26 +01:00
MacRimi 72cf16301f Update AppImage 2025-11-23 17:09:25 +01:00
MacRimi c512dde028 Update terminal-panel.tsx 2025-11-23 16:56:41 +01:00
MacRimi 1e13c7ab31 Update globals.css 2025-11-23 16:48:32 +01:00
MacRimi cdbab86dee Update AppImage 2025-11-23 16:32:01 +01:00
MacRimi fec03d1fd4 Update AppImage 2025-11-23 16:10:41 +01:00
MacRimi 6aa24e23c0 Update terminal-panel.tsx 2025-11-23 14:16:21 +01:00
MacRimi 78770d1da5 Update terminal-panel.tsx 2025-11-23 14:04:43 +01:00
MacRimi 6f72447e2e Update terminal-panel.tsx 2025-11-23 13:52:11 +01:00
MacRimi cb75a15a6f Update terminal-panel.tsx 2025-11-23 13:47:40 +01:00
MacRimi c3555237b3 Update terminal-panel.tsx 2025-11-23 13:39:54 +01:00
MacRimi e4a2cc7ac8 Update terminal-panel.tsx 2025-11-23 13:28:30 +01:00
MacRimi 3900d305b9 Update terminal-panel.tsx 2025-11-23 12:15:55 +01:00
MacRimi cb3d501649 Update terminal-panel.tsx 2025-11-23 12:02:34 +01:00
MacRimi 28323a486a Update terminal-panel.tsx 2025-11-23 11:56:38 +01:00
MacRimi dfcad4b9fd Update terminal-panel.tsx 2025-11-23 11:51:51 +01:00
MacRimi 6fb2869cd8 Update terminal-panel.tsx 2025-11-23 11:42:30 +01:00
MacRimi e764e39ba9 Update terminal-panel.tsx 2025-11-23 11:21:17 +01:00
MacRimi 128077dcbc Update AppImage 2025-11-23 10:57:28 +01:00
MacRimi 1c51107f1e Update AppImage 2025-11-22 23:59:55 +01:00
MacRimi d154cab054 Update proxmox-dashboard.tsx 2025-11-22 23:55:10 +01:00
MacRimi 7ed4368d5b Update appImage 2025-11-22 23:46:43 +01:00
MacRimi ee64df2376 Update AppImage 2025-11-22 23:34:09 +01:00
MacRimi b13f03eb97 Update terminal-panel.tsx 2025-11-22 23:19:10 +01:00
MacRimi 8d20829428 Update AppImage 2025-11-22 23:08:11 +01:00
MacRimi 97401f609e Update terminal-panel.tsx 2025-11-22 22:57:49 +01:00
MacRimi fe074729ea Update AppImage 2025-11-22 22:50:23 +01:00
MacRimi db5141e010 Update AppImage 2025-11-22 22:28:34 +01:00
MacRimi 4564fdc6aa Update AppImage 2025-11-22 22:15:39 +01:00
MacRimi a477b36a57 Update terminal-panel.tsx 2025-11-22 22:06:07 +01:00
MacRimi 3b8ae2c879 Update terminal-panel.tsx 2025-11-22 21:58:29 +01:00
MacRimi ebe3a51398 Update appImage 2025-11-22 21:43:14 +01:00
MacRimi 76d22f0cb5 Update AppImage 2025-11-22 21:35:22 +01:00
MacRimi c61d676dfb Update terminal-panel.tsx 2025-11-22 21:26:35 +01:00
MacRimi b1913e7204 Update terminal-panel.tsx 2025-11-22 21:12:05 +01:00
MacRimi b6609e0a14 Update AppImage 2025-11-22 21:06:44 +01:00
MacRimi 55fa759344 Update AppImage 2025-11-22 20:58:06 +01:00
MacRimi 8992a713cc Update AppImage 2025-11-22 20:50:05 +01:00
MacRimi c55dcec252 Update AppImage 2025-11-22 20:41:36 +01:00
MacRimi e3dd6cbef5 Update AppImage 2025-11-22 20:27:41 +01:00
MacRimi dd3e5ea368 Update globals.css 2025-11-22 20:20:57 +01:00
MacRimi ac2e77e0d6 Update globals.css 2025-11-22 20:17:10 +01:00
MacRimi 9f57622f54 Update globals.css 2025-11-22 20:13:02 +01:00
MacRimi cfed460eba Update globals.css 2025-11-22 20:09:36 +01:00
MacRimi 06f97b671f Update globals.css 2025-11-22 20:06:43 +01:00
MacRimi aebf83d735 Update AppImage 2025-11-22 20:02:42 +01:00
MacRimi 31894dd117 Update globals.css 2025-11-22 19:44:00 +01:00
MacRimi e041d802ec Update globals.css 2025-11-22 19:37:31 +01:00
MacRimi 82ea15388c Update globals.css 2025-11-22 19:33:58 +01:00
MacRimi bf9ed8ff00 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-22 19:27:57 +01:00
MacRimi c02606df6a Update Appimage 2025-11-22 19:27:45 +01:00
ProxMenuxBot 7372e2e385 Update helpers_cache.json 2025-11-22 18:17:41 +00:00
MacRimi ba86fa6d3e Update terminal-panel.tsx 2025-11-22 19:07:27 +01:00
MacRimi 0e434cbd1c Update AppImage 2025-11-22 18:15:12 +01:00
MacRimi c89300022a Update AppImage 2025-11-22 17:54:30 +01:00
MacRimi 1300756d6f Update AppImage 2025-11-22 17:40:47 +01:00
MacRimi c4ad02ff92 Update terminal-panel.tsx 2025-11-22 17:32:47 +01:00
MacRimi b3f47f140a Update AppImage 2025-11-22 17:16:18 +01:00
MacRimi 2206b3d5b5 Update terminal-panel.tsx 2025-11-22 11:56:29 +01:00
MacRimi b08f8a450d Update terminal-panel.tsx 2025-11-22 11:43:17 +01:00
MacRimi 37c8be8a6e Update terminal-panel.tsx 2025-11-22 11:21:11 +01:00
MacRimi ae58c265a0 Update AppImage 2025-11-22 11:04:21 +01:00
MacRimi 54e6d1aa16 Update AppImage 2025-11-22 10:33:35 +01:00
MacRimi 4ddb5f14d9 Update flask_terminal_routes.py 2025-11-21 20:12:07 +01:00
MacRimi 623aec495b Update proxmox-dashboard.tsx 2025-11-21 19:56:22 +01:00
MacRimi f6d2b9bad0 Update AppImage 2025-11-21 19:49:42 +01:00
MacRimi 08b5a278f3 Update proxmox-dashboard.tsx 2025-11-21 19:44:15 +01:00
MacRimi f62b30b50d Update AppImage 2025-11-21 19:32:27 +01:00
MacRimi 50e3b8e7d4 Update terminal-panel.tsx 2025-11-21 19:25:23 +01:00
MacRimi e26956dbe8 Update terminal-panel.tsx 2025-11-21 19:15:35 +01:00
MacRimi cff2c12d70 Update build_appimage.sh 2025-11-21 18:53:30 +01:00
MacRimi 5781d532a4 Update build_appimage.sh 2025-11-21 18:47:56 +01:00
MacRimi f161a593f8 Update page.tsx 2025-11-21 18:40:19 +01:00
MacRimi 5725d5a2fe Update AppImage 2025-11-21 18:36:09 +01:00
MacRimi 23280fd97b Update AppImage 2025-11-21 18:32:10 +01:00
MacRimi fe6679f16a Update update-pve9_2.sh 2025-11-21 18:01:34 +01:00
MacRimi 19a95a3670 Update network-traffic-chart.tsx 2025-11-19 22:54:41 +01:00
MacRimi 90cffb3791 Update format-network.ts 2025-11-19 22:47:24 +01:00
MacRimi 31168fbeca Update network-metrics.tsx 2025-11-19 22:33:28 +01:00
MacRimi c4cce5d184 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-19 22:05:49 +01:00
MacRimi 08b59dd082 Update network-metrics.tsx 2025-11-19 22:05:45 +01:00
ProxMenuxBot 4aaf1a5868 Update helpers_cache.json 2025-11-19 18:20:10 +00:00
MacRimi 6e78fa0b1f Update AppImage 2025-11-19 18:43:08 +01:00
MacRimi e1a42189a6 Update system-overview.tsx 2025-11-19 18:18:47 +01:00
MacRimi 386e0c9b6b Update format-network.ts 2025-11-19 18:09:52 +01:00
MacRimi 3b1b423936 Update AppImage 2025-11-19 17:58:03 +01:00
MacRimi 8e8e8161bb Update AppImage 2025-11-19 17:30:01 +01:00
MacRimi b368fde82d Update appImage 2025-11-19 17:15:32 +01:00
ProxMenuxBot 7267111083 Update helpers_cache.json 2025-11-19 12:28:30 +00:00
MacRimi d05dab6633 Update AppImage 2025-11-18 22:05:54 +01:00
MacRimi e1409a8045 Update AppImage 2025-11-18 21:27:24 +01:00
MacRimi ae69fec7ce Update AppImage 2025-11-18 21:11:56 +01:00
MacRimi a2862f22f6 Update AppImage 2025-11-18 20:56:15 +01:00
MacRimi 7db8e18bcc Update AppImage 2025-11-18 19:43:18 +01:00
MacRimi 0ffe1272fe Update virtual-machines.tsx 2025-11-18 19:24:23 +01:00
MacRimi 92b54075c4 Update AppImage 2025-11-18 19:11:14 +01:00
MacRimi ce5c679d6b Update AppImage 2025-11-18 19:00:51 +01:00
MacRimi 4f61386b21 Update flask_server.py 2025-11-18 18:33:57 +01:00
MacRimi 2738ae1abc Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-18 17:14:46 +01:00
MacRimi f5e43ff7b4 Update flask_server.py 2025-11-18 17:14:30 +01:00
MacRimi 63c499bf2c Merge pull request #93 from riri-314/networkGraph
Add option to change network unit
2025-11-18 17:00:23 +01:00
riri-314 9e72720bda revert code formating 2025-11-18 13:30:37 +01:00
ProxMenuxBot bbe10b2dab Update helpers_cache.json 2025-11-18 12:29:38 +00:00
riri-314 f3b0784651 Network metrics take network unit into acount 2025-11-18 13:19:23 +01:00
riri-314 9c0ea9b1c7 System oberview take network unit into account 2025-11-18 13:08:39 +01:00
riri-314 620a088c6c Removed debug code 2025-11-18 11:32:32 +01:00
riri-314 867a74cffb Added optional prop to display network traffic in bits or bytes 2025-11-18 11:26:49 +01:00
riri-314 f2316fdd3a Add setting to change network unit 2025-11-18 10:58:06 +01:00
MacRimi 7d49d4f948 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-17 19:26:44 +01:00
MacRimi f85b2b889c Update flask_server.py 2025-11-17 19:26:30 +01:00
ProxMenuxBot 9471ac4a52 Update helpers_cache.json 2025-11-17 18:20:20 +00:00
MacRimi db520c39e3 Update flask_server.py 2025-11-17 18:48:20 +01:00
MacRimi cc59fbe2ba Delete ProxMenux-1.0.2-deb.AppImage 2025-11-17 18:22:15 +01:00
MacRimi e260af58f2 Create ProxMenux-1.0.2-deb.AppImage 2025-11-17 17:53:50 +01:00
MacRimi 166fc6dad9 Update AppImage 2025-11-17 17:47:58 +01:00
MacRimi 959433d737 Update install_proxmenux.sh 2025-11-17 17:11:41 +01:00
MacRimi f9fa9ce6d8 Update install_proxmenux.sh 2025-11-17 16:58:45 +01:00
MacRimi 6b3a41dfe0 Update install_proxmenux.sh 2025-11-17 16:40:56 +01:00
ProxMenuxBot 37428ecca4 Update helpers_cache.json 2025-11-16 18:18:01 +00:00
MacRimi 6934df253f update menu 2025-11-16 10:19:50 +01:00
MacRimi 00782598a4 Update menu 2025-11-16 01:21:07 +01:00
MacRimi 565c500810 Update menu 2025-11-16 01:17:54 +01:00
MacRimi e3c16166e6 Update menu 2025-11-16 01:15:48 +01:00
MacRimi cfa8d1b689 Update menu 2025-11-16 01:13:24 +01:00
MacRimi a19397f9b5 Update menu 2025-11-16 01:11:49 +01:00
MacRimi ddfc80b45f Update menu 2025-11-16 01:07:25 +01:00
MacRimi 8591f9b2a1 Update menu 2025-11-16 01:04:16 +01:00
MacRimi cb26a55e65 Update menu 2025-11-16 00:59:54 +01:00
MacRimi ef92394685 Update menu 2025-11-16 00:56:04 +01:00
MacRimi d588ef438e Update menu 2025-11-16 00:53:40 +01:00
MacRimi 09cd363b11 Update menu 2025-11-16 00:51:12 +01:00
MacRimi 2d5c7fdbb5 Update menu 2025-11-16 00:47:26 +01:00
MacRimi 2f0e28368d Update menu 2025-11-16 00:44:00 +01:00
MacRimi f7f1a2a3b3 Update menu 2025-11-16 00:33:26 +01:00
MacRimi 30afb85260 Update menu 2025-11-16 00:23:14 +01:00
MacRimi 78d883a1b4 Update menu 2025-11-16 00:17:22 +01:00
MacRimi 7913b673a3 Update menu 2025-11-16 00:13:52 +01:00
MacRimi 5edc27297f Update menu 2025-11-16 00:02:52 +01:00
MacRimi ebc24c2476 Update menu 2025-11-15 23:58:26 +01:00
MacRimi ed7dd037e5 Update menu 2025-11-15 23:52:39 +01:00
MacRimi 277924c04d Update menu 2025-11-15 23:48:50 +01:00
MacRimi 26ea0feddb Update menu 2025-11-15 23:46:02 +01:00
MacRimi 63c1eab930 Update menu 2025-11-15 23:42:17 +01:00
MacRimi 813e7711df Update menu 2025-11-15 23:40:46 +01:00
MacRimi 6c1f50a230 Remove Nginx configuration example from README
Removed example Nginx configuration from README.
2025-11-15 16:17:31 +01:00
MacRimi 470b6359ba Remove onboarding image sections from README
Removed multiple image sections related to storage management, network monitoring, virtual machines, hardware information, and system logs from the README.
2025-11-15 16:16:30 +01:00
MacRimi 2f45233748 Fix formatting issues in menu file 2025-11-15 15:20:12 +01:00
MacRimi 82fd52f572 Add LOCAL_SCRIPTS variable to menu configuration 2025-11-15 14:30:34 +01:00
MacRimi ed6331e6a4 Update config_menu.sh 2025-11-14 22:17:11 +01:00
MacRimi 2ae9188535 Update version.txt 2025-11-14 21:44:50 +01:00
MacRimi 1a55a5394a Update menu 2025-11-14 21:43:38 +01:00
MacRimi 99d2f37cfc Update version.txt 2025-11-14 21:42:15 +01:00
MacRimi 09b531e0c1 Update version.txt 2025-11-14 21:40:38 +01:00
MacRimi 232e872c0d Update menu 2025-11-14 21:37:55 +01:00
MacRimi acdb0d2838 Update menu 2025-11-14 21:30:58 +01:00
MacRimi bd0ea1379f Update menu 2025-11-14 21:02:43 +01:00
MacRimi 5461ea1a3a Update menu 2025-11-14 20:48:38 +01:00
MacRimi 200ee075b5 Update menu 2025-11-14 20:38:07 +01:00
MacRimi 79e9e5fcf1 Update version.txt 2025-11-14 20:33:07 +01:00
MacRimi a2df23d562 Update version.txt 2025-11-14 20:23:30 +01:00
MacRimi 55af3d7f65 Update version.txt 2025-11-14 20:21:12 +01:00
MacRimi ef54f3fe59 Update ChangeLog 2025-11-14 20:14:03 +01:00
MacRimi 9d84ff6aa7 Update version.txt 2025-11-14 20:10:57 +01:00
MacRimi ee26006f3c Update CHANGELOG.md 2025-11-14 20:08:07 +01:00
114 changed files with 30549 additions and 3442 deletions
@@ -0,0 +1,143 @@
title: "[Prompt] "
labels:
- custom-prompt
- community
body:
- type: markdown
attributes:
value: |
## Share Your Custom Prompt
Thank you for sharing your custom prompt with the community!
Please fill in all the required fields so others can use your prompt effectively.
- type: input
id: prompt-name
attributes:
label: Prompt Name
description: A short descriptive name for your prompt
placeholder: "e.g., Concise Technical Alerts"
validations:
required: true
- type: dropdown
id: provider
attributes:
label: AI Provider
description: Which AI provider did you test this prompt with?
options:
- OpenAI
- Gemini (Google)
- Anthropic (Claude)
- Groq
- OpenRouter
- Ollama (Local)
- Other
validations:
required: true
- type: input
id: model
attributes:
label: Model
description: The specific model you tested with
placeholder: "e.g., gpt-4o-mini, gemini-2.0-flash, llama3.2:3b"
validations:
required: true
- type: dropdown
id: language
attributes:
label: Output Language
description: What language does your prompt output?
options:
- English
- Spanish
- German
- French
- Italian
- Portuguese
- Dutch
- Polish
- Russian
- Chinese
- Japanese
- Korean
- Other (specify in description)
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Describe what your prompt does and its main features
placeholder: |
This prompt generates concise, technical notifications focused on...
Features:
- Brief format (2-3 lines)
- Includes severity indicators
- etc.
validations:
required: true
- type: textarea
id: prompt-content
attributes:
label: Prompt Content
description: Paste your complete custom prompt here
render: text
placeholder: |
You are a notification formatter for ProxMenux Monitor.
Your task is to...
RULES:
1. ...
2. ...
OUTPUT FORMAT:
[TITLE]
...
[BODY]
...
validations:
required: true
- type: textarea
id: example-output
attributes:
label: Example Output
description: Show an example of how a notification looks with your prompt
placeholder: |
**Input notification:**
CPU usage high on node pve01
**Output with this prompt:**
pve01: High CPU Usage
CPU at 95% for 5 minutes. Check running processes.
validations:
required: false
- type: textarea
id: additional-notes
attributes:
label: Additional Notes
description: Any tips, variations, or known limitations
placeholder: |
- Works best with models that support system prompts
- May need adjustment for very long notifications
- Tested with Proxmox VE 8.x
validations:
required: false
- type: checkboxes
id: confirmation
attributes:
label: Confirmation
options:
- label: I have tested this prompt and it works correctly
required: true
- label: I am sharing this prompt for the community to use freely
required: true
+174 -113
View File
@@ -3,26 +3,28 @@ import json
import re
import sys
from pathlib import Path
from typing import Any
import requests
# ---------- Config ----------
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
POCKETBASE_BASE = "https://db.community-scripts.org/api/collections"
SCRIPT_COLLECTION_URL = f"{POCKETBASE_BASE}/script_scripts/records"
CATEGORY_COLLECTION_URL = f"{POCKETBASE_BASE}/script_categories/records"
# Escribimos siempre en <raiz_repo>/json/helpers_cache.json, independientemente del cwd
REPO_ROOT = Path(__file__).resolve().parents[2]
OUTPUT_FILE = REPO_ROOT / "json" / "helpers_cache.json"
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
# ----------------------------
TYPE_TO_PATH_PREFIX = {
"lxc": "ct",
"vm": "vm",
"addon": "tools/addon",
"pve": "tools/pve",
}
def to_mirror_url(raw_url: str) -> str:
"""
Convierte una URL raw de GitHub al raw del mirror.
GH : https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/docker.sh
MIR: https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/main/ct/docker.sh
"""
m = re.match(r"^https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)$", raw_url or "")
if not m:
return ""
@@ -32,143 +34,202 @@ def to_mirror_url(raw_url: str) -> str:
return f"https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/{branch}/{path}"
def guess_os_from_script_path(script_path: str) -> str | None:
"""
Heurística suave cuando el JSON no publica resources.os:
- tools/pve/* -> proxmox
- ct/alpine-* -> alpine
- tools/addon/* -> generic (suele ejecutarse sobre LXC existente)
- ct/* -> debian (por defecto para CTs)
"""
if not script_path:
return None
if script_path.startswith("tools/pve/") or script_path == "tools/pve/host-backup.sh" or script_path.startswith("vm/"):
return "proxmox"
if "/alpine-" in script_path or script_path.startswith("ct/alpine-"):
return "alpine"
if script_path.startswith("tools/addon/"):
return "generic"
if script_path.startswith("ct/"):
return "debian"
return None
def fetch_directory_json(api_url: str) -> list[dict]:
r = requests.get(api_url, timeout=30)
def fetch_json(url: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]:
r = requests.get(url, params=params, timeout=60)
r.raise_for_status()
data = r.json()
if not isinstance(data, list):
raise RuntimeError("GitHub API no devolvió una lista.")
if not isinstance(data, dict):
raise RuntimeError(f"Unexpected response from {url}: expected object")
return data
def fetch_all_records(url: str, *, expand: str | None = None, per_page: int = 500) -> list[dict[str, Any]]:
page = 1
items: list[dict[str, Any]] = []
while True:
params: dict[str, Any] = {"page": page, "perPage": per_page}
if expand:
params["expand"] = expand
data = fetch_json(url, params=params)
page_items = data.get("items", [])
if not isinstance(page_items, list):
raise RuntimeError(f"Unexpected items list from {url}")
items.extend(page_items)
total_pages = data.get("totalPages", page)
if not isinstance(total_pages, int) or page >= total_pages:
break
page += 1
return items
def normalize_os_variants(install_methods_json: list[dict[str, Any]]) -> list[str]:
os_values: list[str] = []
for item in install_methods_json:
if not isinstance(item, dict):
continue
resources = item.get("resources", {})
if not isinstance(resources, dict):
continue
os_name = resources.get("os")
if isinstance(os_name, str) and os_name.strip():
normalized = os_name.strip().lower()
if normalized not in os_values:
os_values.append(normalized)
return os_values
def build_script_path(type_name: str, slug: str) -> str:
type_name = (type_name or "").strip().lower()
slug = (slug or "").strip()
if type_name == "turnkey":
return "turnkey/turnkey.sh"
prefix = TYPE_TO_PATH_PREFIX.get(type_name)
if not prefix or not slug:
return ""
return f"{prefix}/{slug}.sh"
def main() -> int:
try:
directory = fetch_directory_json(API_URL)
scripts = fetch_all_records(SCRIPT_COLLECTION_URL, expand="type,categories")
categories = fetch_all_records(CATEGORY_COLLECTION_URL)
except Exception as e:
print(f"ERROR: No se pudo leer el índice de JSONs: {e}", file=sys.stderr)
print(f"ERROR: Unable to fetch PocketBase data: {e}", file=sys.stderr)
return 1
cache: list[dict] = []
seen: set[tuple[str, str]] = set() # (slug, script) para evitar duplicados
category_map: dict[str, dict[str, Any]] = {}
for category in categories:
category_id = category.get("id")
if isinstance(category_id, str) and category_id:
category_map[category_id] = category
total_items = len(directory)
processed = 0
kept = 0
cache: list[dict[str, Any]] = []
for item in directory:
url = item.get("download_url")
name_in_dir = item.get("name", "")
if not url or not url.endswith(".json"):
print(f"Fetched {len(scripts)} scripts and {len(category_map)} categories")
for idx, raw in enumerate(scripts, start=1):
if not isinstance(raw, dict):
continue
try:
raw = requests.get(url, timeout=30).json()
if not isinstance(raw, dict):
continue
except Exception:
print(f"❌ Error al obtener/parsing {name_in_dir}", file=sys.stderr)
continue
processed += 1
name = raw.get("name", "")
slug = raw.get("slug")
type_ = raw.get("type", "")
name = raw.get("name", "")
desc = raw.get("description", "")
categories = raw.get("categories", [])
notes = [n.get("text", "") for n in raw.get("notes", []) if isinstance(n, dict)]
# Credenciales (si existen, se copian tal cual)
credentials = raw.get("default_credentials", {})
cred_username = credentials.get("username") if isinstance(credentials, dict) else None
cred_password = credentials.get("password") if isinstance(credentials, dict) else None
add_credentials = any([
cred_username not in (None, ""),
cred_password not in (None, "")
])
install_methods = raw.get("install_methods", [])
if not isinstance(install_methods, list) or not install_methods:
# Sin install_methods válidos -> continuamos
if not isinstance(slug, str) or not slug.strip():
continue
for im in install_methods:
if not isinstance(im, dict):
continue
script = im.get("script", "")
if not script:
continue
expand = raw.get("expand", {}) if isinstance(raw.get("expand"), dict) else {}
type_expanded = expand.get("type", {}) if isinstance(expand.get("type"), dict) else {}
type_name = type_expanded.get("type", "") if isinstance(type_expanded.get("type"), str) else ""
# OS desde resources u heurística
resources = im.get("resources", {}) if isinstance(im, dict) else {}
os_name = resources.get("os") if isinstance(resources, dict) else None
if not os_name:
os_name = guess_os_from_script_path(script)
if isinstance(os_name, str):
os_name = os_name.strip().lower()
script_path = build_script_path(type_name, slug)
if not script_path:
print(f"[{idx:03d}] WARNING: Unable to build script path for slug={slug} type={type_name!r}", file=sys.stderr)
continue
full_script_url = f"{SCRIPT_BASE}/{script}"
script_url_mirror = to_mirror_url(full_script_url)
full_script_url = f"{SCRIPT_BASE}/{script_path}"
script_url_mirror = to_mirror_url(full_script_url)
key = (slug or "", script)
if key in seen:
continue
seen.add(key)
install_methods_json = raw.get("install_methods_json", [])
if not isinstance(install_methods_json, list):
install_methods_json = []
entry = {
"name": name,
"slug": slug,
"desc": desc,
"script": script,
"script_url": full_script_url,
"script_url_mirror": script_url_mirror, # nuevo
"os": os_name, # nuevo
"categories": categories,
"notes": notes,
"type": type_,
notes_json = raw.get("notes_json", [])
if not isinstance(notes_json, list):
notes_json = []
notes = [
note.get("text", "")
for note in notes_json
if isinstance(note, dict) and isinstance(note.get("text"), str) and note.get("text", "").strip()
]
category_ids = raw.get("categories", [])
if not isinstance(category_ids, list):
category_ids = []
expanded_categories = expand.get("categories", []) if isinstance(expand.get("categories"), list) else []
category_names: list[str] = []
for cat in expanded_categories:
if isinstance(cat, dict):
cat_name = cat.get("name")
if isinstance(cat_name, str) and cat_name.strip():
category_names.append(cat_name.strip())
if not category_names:
for cat_id in category_ids:
cat = category_map.get(cat_id, {})
cat_name = cat.get("name")
if isinstance(cat_name, str) and cat_name.strip():
category_names.append(cat_name.strip())
# Shared fields across all install method entries
default_user = raw.get("default_user")
default_passwd = raw.get("default_passwd")
default_credentials: dict[str, str] | None = None
if (isinstance(default_user, str) and default_user.strip()) or (isinstance(default_passwd, str) and default_passwd.strip()):
default_credentials = {
"username": default_user if isinstance(default_user, str) else "",
"password": default_passwd if isinstance(default_passwd, str) else "",
}
if add_credentials:
entry["default_credentials"] = {
"username": cred_username,
"password": cred_password,
}
base_entry: dict[str, Any] = {
"name": name,
"slug": slug,
"desc": desc,
"script": script_path,
"script_url": full_script_url,
"script_url_mirror": script_url_mirror,
"type": type_name,
"type_id": raw.get("type", ""),
"categories": category_ids,
"category_names": category_names,
"notes": notes,
"port": raw.get("port", 0),
"website": raw.get("website", ""),
"documentation": raw.get("documentation", ""),
"logo": raw.get("logo", ""),
"updateable": bool(raw.get("updateable", False)),
"privileged": bool(raw.get("privileged", False)),
"has_arm": bool(raw.get("has_arm", False)),
"is_dev": bool(raw.get("is_dev", False)),
"execute_in": raw.get("execute_in", []),
"config_path": raw.get("config_path", ""),
}
if default_credentials:
base_entry["default_credentials"] = default_credentials
# Emit one entry per install method so the menu shell can offer an
# explicit OS choice. When there is only one method (or none), a
# single entry is emitted with os="" (script decides at runtime).
os_variants = normalize_os_variants(install_methods_json)
if len(os_variants) > 1:
for os_name in os_variants:
entry = {**base_entry, "os": os_name}
cache.append(entry)
print(f"[{len(cache):03d}] {slug:<24}{script_path:<28} type={type_name:<7} os={os_name}")
else:
os_name = os_variants[0] if os_variants else ""
entry = {**base_entry, "os": os_name}
cache.append(entry)
kept += 1
print(f"[{len(cache):03d}] {slug:<24}{script_path:<28} type={type_name:<7} os={os_name or 'n/a'}")
# Progreso ligero
print(f"[{kept:03d}] {slug or name:<24}{script:<28} os={os_name or 'n/a'} src={'GH+MR' if script_url_mirror else 'GH'}")
# Orden estable para commits reproducibles
cache.sort(key=lambda x: (x.get("slug") or "", x.get("script") or ""))
with OUTPUT_FILE.open("w", encoding="utf-8") as f:
json.dump(cache, f, ensure_ascii=False, indent=2)
print(f"\n✅ helpers_cache.json → {OUTPUT_FILE}")
print(f" Total JSON en índice: {total_items}")
print(f" Procesados: {processed} | Guardados: {kept} | Únicos (slug,script): {len(seen)}")
print(f" Guardados: {len(cache)}")
return 0
@@ -0,0 +1,178 @@
#!/usr/bin/env python3
import json
import re
import sys
from pathlib import Path
import requests
# ---------- Config ----------
# API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE-Frontend-Archive/contents/public/json"
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
# Escribimos siempre en <raiz_repo>/json/helpers_cache.json, independientemente del cwd
REPO_ROOT = Path(__file__).resolve().parents[2]
OUTPUT_FILE = REPO_ROOT / "json" / "helpers_cache.json"
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
# ----------------------------
def to_mirror_url(raw_url: str) -> str:
"""
Convierte una URL raw de GitHub al raw del mirror.
GH : https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/docker.sh
MIR: https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/main/ct/docker.sh
"""
m = re.match(r"^https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)$", raw_url or "")
if not m:
return ""
org, repo, branch, path = m.groups()
if org.lower() != "community-scripts" or repo != "ProxmoxVE":
return ""
return f"https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/{branch}/{path}"
def guess_os_from_script_path(script_path: str) -> str | None:
"""
Heurística suave cuando el JSON no publica resources.os:
- tools/pve/* -> proxmox
- ct/alpine-* -> alpine
- tools/addon/* -> generic (suele ejecutarse sobre LXC existente)
- ct/* -> debian (por defecto para CTs)
"""
if not script_path:
return None
if script_path.startswith("tools/pve/") or script_path == "tools/pve/host-backup.sh" or script_path.startswith("vm/"):
return "proxmox"
if "/alpine-" in script_path or script_path.startswith("ct/alpine-"):
return "alpine"
if script_path.startswith("tools/addon/"):
return "generic"
if script_path.startswith("ct/"):
return "debian"
return None
def fetch_directory_json(api_url: str) -> list[dict]:
r = requests.get(api_url, timeout=30)
r.raise_for_status()
data = r.json()
if not isinstance(data, list):
raise RuntimeError("GitHub API no devolvió una lista.")
return data
def main() -> int:
try:
directory = fetch_directory_json(API_URL)
except Exception as e:
print(f"ERROR: No se pudo leer el índice de JSONs: {e}", file=sys.stderr)
return 1
cache: list[dict] = []
seen: set[tuple[str, str]] = set() # (slug, script) para evitar duplicados
total_items = len(directory)
processed = 0
kept = 0
for item in directory:
url = item.get("download_url")
name_in_dir = item.get("name", "")
if not url or not url.endswith(".json"):
continue
try:
raw = requests.get(url, timeout=30).json()
if not isinstance(raw, dict):
continue
except Exception:
print(f"❌ Error al obtener/parsing {name_in_dir}", file=sys.stderr)
continue
processed += 1
name = raw.get("name", "")
slug = raw.get("slug")
type_ = raw.get("type", "")
desc = raw.get("description", "")
categories = raw.get("categories", [])
notes = [n.get("text", "") for n in raw.get("notes", []) if isinstance(n, dict)]
# Credenciales (si existen, se copian tal cual)
credentials = raw.get("default_credentials", {})
cred_username = credentials.get("username") if isinstance(credentials, dict) else None
cred_password = credentials.get("password") if isinstance(credentials, dict) else None
add_credentials = any([
cred_username not in (None, ""),
cred_password not in (None, "")
])
install_methods = raw.get("install_methods", [])
if not isinstance(install_methods, list) or not install_methods:
# Sin install_methods válidos -> continuamos
continue
for im in install_methods:
if not isinstance(im, dict):
continue
script = im.get("script", "")
if not script:
continue
# OS desde resources u heurística
resources = im.get("resources", {}) if isinstance(im, dict) else {}
os_name = resources.get("os") if isinstance(resources, dict) else None
if not os_name:
os_name = guess_os_from_script_path(script)
if isinstance(os_name, str):
os_name = os_name.strip().lower()
full_script_url = f"{SCRIPT_BASE}/{script}"
script_url_mirror = to_mirror_url(full_script_url)
key = (slug or "", script)
if key in seen:
continue
seen.add(key)
entry = {
"name": name,
"slug": slug,
"desc": desc,
"script": script,
"script_url": full_script_url,
"script_url_mirror": script_url_mirror, # nuevo
"os": os_name, # nuevo
"categories": categories,
"notes": notes,
"type": type_,
}
if add_credentials:
entry["default_credentials"] = {
"username": cred_username,
"password": cred_password,
}
cache.append(entry)
kept += 1
# Progreso ligero
print(f"[{kept:03d}] {slug or name:<24}{script:<28} os={os_name or 'n/a'} src={'GH+MR' if script_url_mirror else 'GH'}")
# Orden estable para commits reproducibles
cache.sort(key=lambda x: (x.get("slug") or "", x.get("script") or ""))
with OUTPUT_FILE.open("w", encoding="utf-8") as f:
json.dump(cache, f, ensure_ascii=False, indent=2)
print(f"\n✅ helpers_cache.json → {OUTPUT_FILE}")
print(f" Total JSON en índice: {total_items}")
print(f" Procesados: {processed} | Guardados: {kept} | Únicos (slug,script): {len(seen)}")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -1,24 +1,29 @@
name: Build ProxMenux Monitor AppImage
name: Build AppImage Release
on:
workflow_dispatch:
workflow_dispatch:
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Checkout main
uses: actions/checkout@v5
with:
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '20'
node-version: '22'
- name: Install dependencies
working-directory: AppImage
@@ -45,13 +50,6 @@ jobs:
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: |
@@ -60,22 +58,26 @@ jobs:
echo "Generated SHA256:"
cat ProxMenux-Monitor.AppImage.sha256
- name: Upload AppImage and checksum to /AppImage folder in main
- name: Upload AppImage artifact
uses: actions/upload-artifact@v5
with:
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
path: |
AppImage/dist/*.AppImage
AppImage/dist/*.sha256
retention-days: 30
- name: Commit AppImage to 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 commit -m "Update AppImage release build ($(date +'%Y-%m-%d %H:%M:%S'))" || echo "No changes to commit"
git push origin main
+83
View File
@@ -0,0 +1,83 @@
name: Build AppImage Beta
on:
workflow_dispatch:
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: Checkout develop
uses: actions/checkout@v5
with:
ref: develop
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '22'
- 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: Generate SHA256 checksum
run: |
cd AppImage/dist
sha256sum *.AppImage > ProxMenux-Monitor.AppImage.sha256
echo "Generated SHA256:"
cat ProxMenux-Monitor.AppImage.sha256
- name: Upload AppImage artifact
uses: actions/upload-artifact@v5
with:
name: ProxMenux-${{ steps.version.outputs.VERSION }}-beta-AppImage
path: |
AppImage/dist/*.AppImage
AppImage/dist/*.sha256
retention-days: 30
- name: Commit AppImage to develop
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"
rm -f AppImage/*.AppImage AppImage/*.sha256 || true
cp AppImage/dist/*.AppImage AppImage/
cp AppImage/dist/ProxMenux-Monitor.AppImage.sha256 AppImage/
git add AppImage/*.AppImage AppImage/*.sha256
git commit -m "Update AppImage beta build ($(date +'%Y-%m-%d %H:%M:%S'))" || echo "No changes to commit"
git push origin develop
+7 -4
View File
@@ -9,18 +9,21 @@ on:
paths: [ 'AppImage/**' ]
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '20'
node-version: '22'
- name: Install dependencies
working-directory: AppImage
@@ -49,7 +52,7 @@ jobs:
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
path: AppImage/dist/*.AppImage
-76
View File
@@ -21,7 +21,6 @@ A modern, responsive dashboard for monitoring Proxmox VE systems built with Next
- [Integration Examples](#integration-examples)
- [Homepage Integration](#homepage-integration)
- [Home Assistant Integration](#home-assistant-integration)
- [Contributing](#contributing)
- [License](#license)
---
@@ -43,35 +42,6 @@ Get a quick overview of ProxMenux Monitor's main features:
<em>System Overview - Monitor CPU, memory, temperature, and uptime in real-time</em>
</p>
<p align="center">
<img src="public/images/onboarding/imagen2.png" alt="Storage Management" width="800"/>
<br/>
<em>Storage Management - Visual representation of disk usage and health</em>
</p>
<p align="center">
<img src="public/images/onboarding/imagen3.png" alt="Network Monitoring" width="800"/>
<br/>
<em>Network Monitoring - Real-time traffic graphs and interface statistics</em>
</p>
<p align="center">
<img src="public/images/onboarding/imagen4.png" alt="Virtual Machines & LXC" width="800"/>
<br/>
<em>VMs & LXC Containers - Comprehensive view with resource usage and controls</em>
</p>
<p align="center">
<img src="public/images/onboarding/imagen5.png" alt="Hardware Information" width="800"/>
<br/>
<em>Hardware Information - Detailed specs for CPU, GPU, and PCIe devices</em>
</p>
<p align="center">
<img src="public/images/onboarding/imagen6.png" alt="System Logs" width="800"/>
<br/>
<em>System Logs - Real-time monitoring with filtering and search</em>
</p>
---
@@ -122,17 +92,6 @@ ProxMenux Monitor includes built-in support for reverse proxy configurations. If
- Adjust API endpoints to work correctly through the proxy
- Maintain full functionality for all features including authentication and API access
**Example Nginx configuration:**
```nginx
location /proxmenux-monitor/ {
proxy_pass http://localhost:8008/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
```
## Authentication & Security
@@ -770,42 +729,7 @@ entities:
![Home Assistant Integration Example](AppImage/public/images/docs/homeassistant-integration.png)
---
## Contributing
Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.
### Development Setup
1. Clone the repository
2. Install dependencies: `npm install`
3. Run development server: `npm run dev`
4. Build AppImage: `./build_appimage.sh`
---
## License
This project is licensed under the **Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0)**.
You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material
Under the following terms:
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made
- NonCommercial — You may not use the material for commercial purposes
For more details, see the [full license](https://creativecommons.org/licenses/by-nc/4.0/).
---
## Support
For support, feature requests, or bug reports, please visit:
- GitHub Issues: [github.com/your-repo/issues](https://github.com/your-repo/issues)
- Documentation: [github.com/your-repo/wiki](https://github.com/your-repo/wiki)
---
+19
View File
@@ -144,3 +144,22 @@
stroke: var(--border);
}
}
/* ===================== */
/* Ajustes para xterm.js */
/* ===================== */
/* Quitar padding para que la terminal ocupe el 100% del ancho */
.xterm {
padding: 0 !important;
}
/* Por si acaso el viewport añade padding extra */
.xterm .xterm-viewport {
padding: 0 !important;
}
/* Opcional: asegurar que no haya margen raro */
.xterm-rows {
margin: 0 !important;
}
+25 -4
View File
@@ -2,10 +2,10 @@
import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Dialog, DialogContent } from "./ui/dialog"
import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Shield, Lock, User, AlertCircle } from "lucide-react"
import { Shield, Lock, User, AlertCircle, Eye, EyeOff } from "lucide-react"
import { getApiUrl } from "../lib/api-config"
interface AuthSetupProps {
@@ -20,6 +20,8 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
const [confirmPassword, setConfirmPassword] = useState("")
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
useEffect(() => {
const checkOnboardingStatus = async () => {
@@ -135,6 +137,9 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
<DialogTitle className="sr-only">
{step === "choice" ? "Setup Dashboard Protection" : "Create Password"}
</DialogTitle>
{step === "choice" ? (
<div className="space-y-6 py-2">
<div className="text-center space-y-2">
@@ -210,7 +215,7 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="password"
type="password"
type={showPassword ? "text" : "password"}
placeholder="Enter password"
value={password}
onChange={(e) => setPassword(e.target.value)}
@@ -218,6 +223,14 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
disabled={loading}
autoComplete="new-password"
/>
<Button
variant="ghost"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2"
disabled={loading}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
@@ -229,7 +242,7 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="confirm-password"
type="password"
type={showConfirmPassword ? "text" : "password"}
placeholder="Confirm password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
@@ -237,6 +250,14 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
disabled={loading}
autoComplete="new-password"
/>
<Button
variant="ghost"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2"
disabled={loading}
>
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
+157 -111
View File
@@ -5,19 +5,21 @@ import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import {
Thermometer,
CpuIcon,
Zap,
HardDrive,
Network,
FanIcon,
PowerIcon,
Battery,
Cpu,
MemoryStick,
Cpu as Gpu,
HardDrive,
Thermometer,
Zap,
Loader2,
CpuIcon,
Cpu as Gpu,
Network,
MemoryStick,
PowerIcon,
FanIcon,
Battery,
} from "lucide-react"
import { Download } from "lucide-react"
import { Button } from "@/components/ui/button"
import useSWR from "swr"
import { useState, useEffect } from "react"
import {
@@ -28,6 +30,7 @@ import {
fetcher as swrFetcher,
} from "../types/hardware"
import { fetchApi } from "@/lib/api-config"
import { ScriptTerminalModal } from "./script-terminal-modal"
const parseLsblkSize = (sizeStr: string | undefined): number => {
if (!sizeStr) return 0
@@ -236,6 +239,8 @@ export default function Hardware() {
const [selectedDisk, setSelectedDisk] = useState<StorageDevice | null>(null)
const [selectedNetwork, setSelectedNetwork] = useState<PCIDevice | null>(null)
const [selectedUPS, setSelectedUPS] = useState<any>(null)
const [showNvidiaInstaller, setShowNvidiaInstaller] = useState(false)
const [installingNvidiaDriver, setInstallingNvidiaDriver] = useState(false)
const fetcher = async (url: string) => {
const data = await fetchApi(url)
@@ -246,12 +251,17 @@ export default function Hardware() {
data: hardwareDataSWR,
error: swrError,
isLoading: swrLoading,
mutate,
mutate: mutateHardware,
} = useSWR<HardwareData>("/api/hardware", fetcher, {
refreshInterval: 30000,
revalidateOnFocus: false,
})
const handleInstallNvidiaDriver = () => {
console.log("[v0] Opening NVIDIA installer terminal")
setShowNvidiaInstaller(true)
}
useEffect(() => {
if (!selectedGPU) return
@@ -778,13 +788,7 @@ export default function Hardware() {
)}
{/* GPU Detail Modal - Shows immediately with basic info, then loads real-time data */}
<Dialog
open={selectedGPU !== null}
onOpenChange={() => {
setSelectedGPU(null)
setRealtimeGPUData(null)
}}
>
<Dialog open={!!selectedGPU} onOpenChange={(open) => !open && setSelectedGPU(null)}>
<DialogContent className="max-w-3xl max-h-[85vh] overflow-y-auto">
{selectedGPU && (
<>
@@ -1090,11 +1094,22 @@ export default function Hardware() {
/>
</svg>
</div>
<div>
<div className="flex-1">
<h4 className="text-sm font-semibold text-blue-500 mb-1">Extended Monitoring Not Available</h4>
<p className="text-sm text-muted-foreground">
<p className="text-sm text-muted-foreground mb-3">
{getMonitoringToolRecommendation(selectedGPU.vendor)}
</p>
{selectedGPU.vendor.toLowerCase().includes("nvidia") && (
<Button
onClick={handleInstallNvidiaDriver}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
<>
<Download className="mr-2 h-4 w-4" />
Install NVIDIA Drivers
</>
</Button>
)}
</div>
</div>
</div>
@@ -1105,92 +1120,6 @@ export default function Hardware() {
</DialogContent>
</Dialog>
{/* PCI Devices - Changed to modal */}
{hardwareDataSWR?.pci_devices && hardwareDataSWR.pci_devices.length > 0 && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<CpuIcon className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">PCI Devices</h2>
<Badge variant="outline" className="ml-auto">
{hardwareDataSWR.pci_devices.length} devices
</Badge>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{hardwareDataSWR.pci_devices.map((device, index) => (
<div
key={index}
onClick={() => setSelectedPCIDevice(device)}
className="cursor-pointer rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 p-3 transition-colors"
>
<div className="flex items-center justify-between gap-2 mb-2">
<Badge className={`${getDeviceTypeColor(device.type)} text-xs shrink-0`}>{device.type}</Badge>
<span className="font-mono text-xs text-muted-foreground shrink-0">{device.slot}</span>
</div>
<p className="font-medium text-sm line-clamp-2 break-words">{device.device}</p>
<p className="text-xs text-muted-foreground truncate">{device.vendor}</p>
{device.driver && (
<p className="mt-1 font-mono text-xs text-green-500 truncate">Driver: {device.driver}</p>
)}
</div>
))}
</div>
</Card>
)}
{/* PCI Device Detail Modal */}
<Dialog open={selectedPCIDevice !== null} onOpenChange={() => setSelectedPCIDevice(null)}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{selectedPCIDevice?.device}</DialogTitle>
<DialogDescription>PCI Device Information</DialogDescription>
</DialogHeader>
{selectedPCIDevice && (
<div className="space-y-3">
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Device Type</span>
<Badge className={getDeviceTypeColor(selectedPCIDevice.type)}>{selectedPCIDevice.type}</Badge>
</div>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">PCI Slot</span>
<span className="font-mono text-sm">{selectedPCIDevice.slot}</span>
</div>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Device Name</span>
<span className="text-sm text-right">{selectedPCIDevice.device}</span>
</div>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Vendor</span>
<span className="text-sm">{selectedPCIDevice.vendor}</span>
</div>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Class</span>
<span className="font-mono text-sm">{selectedPCIDevice.class}</span>
</div>
{selectedPCIDevice.driver && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Driver</span>
<span className="font-mono text-sm text-green-500">{selectedPCIDevice.driver}</span>
</div>
)}
{selectedPCIDevice.kernel_module && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Kernel Module</span>
<span className="font-mono text-sm">{selectedPCIDevice.kernel_module}</span>
</div>
)}
</div>
)}
</DialogContent>
</Dialog>
{/* Power Consumption */}
{hardwareDataSWR?.power_meter && (
<Card className="border-border/50 bg-card/50 p-6">
@@ -1525,31 +1454,31 @@ export default function Hardware() {
<div className="grid gap-2">
{selectedUPS.manufacturer && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Manufacturer</span>
<span className="text-sm font-medium text-muted-foreground">Manufacturer</span>
<span className="text-sm font-medium">{selectedUPS.manufacturer}</span>
</div>
)}
{selectedUPS.model && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Model</span>
<span className="text-sm font-medium text-muted-foreground">Model</span>
<span className="text-sm font-medium">{selectedUPS.model}</span>
</div>
)}
{selectedUPS.serial && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Serial Number</span>
<span className="text-sm font-medium text-muted-foreground">Serial Number</span>
<span className="font-mono text-sm">{selectedUPS.serial}</span>
</div>
)}
{selectedUPS.firmware && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Firmware</span>
<span className="text-sm font-medium text-muted-foreground">Firmware</span>
<span className="text-sm font-medium">{selectedUPS.firmware}</span>
</div>
)}
{selectedUPS.driver && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm text-muted-foreground">Driver</span>
<span className="text-sm font-medium text-muted-foreground">Driver</span>
<span className="font-mono text-sm text-green-500">{selectedUPS.driver}</span>
</div>
)}
@@ -1561,6 +1490,92 @@ export default function Hardware() {
</DialogContent>
</Dialog>
{/* PCI Devices - Changed to modal */}
{hardwareDataSWR?.pci_devices && hardwareDataSWR.pci_devices.length > 0 && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<CpuIcon className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">PCI Devices</h2>
<Badge variant="outline" className="ml-auto">
{hardwareDataSWR.pci_devices.length} devices
</Badge>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{hardwareDataSWR.pci_devices.map((device, index) => (
<div
key={index}
onClick={() => setSelectedPCIDevice(device)}
className="cursor-pointer rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 p-3 transition-colors"
>
<div className="flex items-center justify-between gap-2 mb-2">
<Badge className={`${getDeviceTypeColor(device.type)} text-xs shrink-0`}>{device.type}</Badge>
<span className="font-mono text-xs text-muted-foreground shrink-0">{device.slot}</span>
</div>
<p className="font-medium text-sm line-clamp-2 break-words">{device.device}</p>
<p className="text-xs text-muted-foreground truncate">{device.vendor}</p>
{device.driver && (
<p className="mt-1 font-mono text-xs text-green-500 truncate">Driver: {device.driver}</p>
)}
</div>
))}
</div>
</Card>
)}
{/* PCI Device Detail Modal */}
<Dialog open={selectedPCIDevice !== null} onOpenChange={() => setSelectedPCIDevice(null)}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{selectedPCIDevice?.device}</DialogTitle>
<DialogDescription>PCI Device Information</DialogDescription>
</DialogHeader>
{selectedPCIDevice && (
<div className="space-y-3">
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Device Type</span>
<Badge className={getDeviceTypeColor(selectedPCIDevice.type)}>{selectedPCIDevice.type}</Badge>
</div>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">PCI Slot</span>
<span className="font-mono text-sm">{selectedPCIDevice.slot}</span>
</div>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Device Name</span>
<span className="text-sm text-right">{selectedPCIDevice.device}</span>
</div>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Vendor</span>
<span className="text-sm">{selectedPCIDevice.vendor}</span>
</div>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Class</span>
<span className="font-mono text-sm">{selectedPCIDevice.class}</span>
</div>
{selectedPCIDevice.driver && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Driver</span>
<span className="font-mono text-sm text-green-500">{selectedPCIDevice.driver}</span>
</div>
)}
{selectedPCIDevice.kernel_module && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Kernel Module</span>
<span className="font-mono text-sm">{selectedPCIDevice.kernel_module}</span>
</div>
)}
</div>
)}
</DialogContent>
</Dialog>
{/* Network Summary - Clickable */}
{hardwareDataSWR?.pci_devices &&
hardwareDataSWR.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length > 0 && (
@@ -2006,6 +2021,37 @@ export default function Hardware() {
)}
</DialogContent>
</Dialog>
{/* NVIDIA Installation Monitor */}
{/* <HybridScriptMonitor
sessionId={nvidiaSessionId}
title="NVIDIA Driver Installation"
description="Installing NVIDIA proprietary drivers for GPU monitoring..."
onClose={() => {
setNvidiaSessionId(null)
mutateHardware()
}}
onComplete={(success) => {
console.log("[v0] NVIDIA installation completed:", success ? "success" : "failed")
if (success) {
mutateHardware()
}
}}
/> */}
<ScriptTerminalModal
open={showNvidiaInstaller}
onClose={() => {
setShowNvidiaInstaller(false)
mutateHardware()
}}
scriptPath="/usr/local/share/proxmenux/scripts/gpu_tpu/nvidia_installer.sh"
scriptName="nvidia_installer"
params={{
EXECUTION_MODE: "web",
}}
title="NVIDIA Driver Installation"
description="Installing NVIDIA proprietary drivers for GPU monitoring..."
/>
</div>
)
}
+4 -1
View File
@@ -29,6 +29,7 @@ interface CategoryCheck {
status: string
reason?: string
details?: any
dismissable?: boolean
[key: string]: any
}
@@ -315,6 +316,8 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
<div className="mt-2 space-y-1">
{Object.entries(details).map(([detailKey, detailValue]: [string, any]) => {
if (typeof detailValue === "object" && detailValue !== null) {
const isDismissable = detailValue.dismissable !== false
return (
<div
key={detailKey}
@@ -326,7 +329,7 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
<span className="ml-1 text-muted-foreground">{detailValue.reason}</span>
)}
</div>
{(status === "WARNING" || status === "CRITICAL") && (
{(status === "WARNING" || status === "CRITICAL") && isDismissable && (
<Button
size="sm"
variant="outline"
+1 -1
View File
@@ -237,7 +237,7 @@ export function Login({ onLogin }: LoginProps) {
</form>
</div>
<p className="text-center text-sm text-muted-foreground">ProxMenux Monitor v1.0.1</p>
<p className="text-center text-sm text-muted-foreground">ProxMenux Monitor v1.0.2</p>
</div>
</div>
)
+22 -23
View File
@@ -2,9 +2,10 @@
import { Card, CardContent } from "./ui/card"
import { Badge } from "./ui/badge"
import { Wifi, Zap } from "lucide-react"
import { Wifi, Zap } from 'lucide-react'
import { useState, useEffect } from "react"
import { fetchApi } from "../lib/api-config"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
interface NetworkCardProps {
interface_: {
@@ -59,39 +60,37 @@ const getVMTypeBadge = (vmType: string | undefined) => {
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 [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">(getNetworkUnit())
const [trafficData, setTrafficData] = useState<{ received: number; sent: number }>({
received: 0,
sent: 0,
})
useEffect(() => {
const handleUnitChange = () => {
setNetworkUnit(getNetworkUnit())
}
window.addEventListener("networkUnitChanged", handleUnitChange)
window.addEventListener("storage", handleUnitChange)
return () => {
window.removeEventListener("networkUnitChanged", handleUnitChange)
window.removeEventListener("storage", handleUnitChange)
}
}, [])
useEffect(() => {
const fetchTrafficData = async () => {
try {
@@ -207,15 +206,15 @@ export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps
<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-green-500"> {formatNetworkTraffic(trafficData.received * 1024 * 1024 * 1024, networkUnit)}</span>
{" / "}
<span className="text-blue-500"> {formatStorage(trafficData.sent * 1024 * 1024 * 1024)}</span>
<span className="text-blue-500"> {formatNetworkTraffic(trafficData.sent * 1024 * 1024 * 1024, networkUnit)}</span>
</>
) : (
<>
<span className="text-green-500"> {formatBytes(interface_.bytes_recv)}</span>
<span className="text-green-500"> {formatNetworkTraffic(interface_.bytes_recv || 0, networkUnit)}</span>
{" / "}
<span className="text-blue-500"> {formatBytes(interface_.bytes_sent)}</span>
<span className="text-blue-500"> {formatNetworkTraffic(interface_.bytes_sent || 0, networkUnit)}</span>
</>
)}
</div>
+53 -22
View File
@@ -1,14 +1,15 @@
"use client"
import { useState } from "react"
import { useEffect, useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Badge } from "./ui/badge"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog"
import { Wifi, Activity, Network, Router, AlertCircle, Zap } from "lucide-react"
import { Wifi, Activity, Network, Router, AlertCircle, Zap } from 'lucide-react'
import useSWR from "swr"
import { NetworkTrafficChart } from "./network-traffic-chart"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { fetchApi } from "../lib/api-config"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
interface NetworkData {
interfaces: NetworkInterface[]
@@ -132,6 +133,7 @@ const fetcher = async (url: string): Promise<NetworkData> => {
return fetchApi<NetworkData>(url)
}
export function NetworkMetrics() {
const {
data: networkData,
@@ -149,6 +151,19 @@ export function NetworkMetrics() {
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
const [interfaceTotals, setInterfaceTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">(() => getNetworkUnit())
useEffect(() => {
setNetworkUnit(getNetworkUnit())
const handleUnitChange = (e: CustomEvent) => {
setNetworkUnit(e.detail === "Bits" ? "Bits" : "Bytes")
}
window.addEventListener("networkUnitChanged" as any, handleUnitChange)
return () => window.removeEventListener("networkUnitChanged" as any, handleUnitChange)
}, [])
const { data: modalNetworkData } = useSWR<NetworkData>(selectedInterface ? "/api/network" : null, fetcher, {
refreshInterval: 17000,
revalidateOnFocus: false,
@@ -191,8 +206,16 @@ export function NetworkMetrics() {
)
}
const trafficInFormatted = formatStorage(networkTotals.received * 1024 * 1024 * 1024) // Convert GB to bytes
const trafficOutFormatted = formatStorage(networkTotals.sent * 1024 * 1024 * 1024)
const trafficInFormatted = formatNetworkTraffic(
networkTotals.received * 1024 ** 3,
networkUnit,
2
)
const trafficOutFormatted = formatNetworkTraffic(
networkTotals.sent * 1024 ** 3,
networkUnit,
2
)
const packetsRecvK = networkData.traffic.packets_recv ? (networkData.traffic.packets_recv / 1000).toFixed(0) : "0"
const totalErrors = (networkData.traffic.errin || 0) + (networkData.traffic.errout || 0)
@@ -375,7 +398,7 @@ export function NetworkMetrics() {
</CardTitle>
</CardHeader>
<CardContent>
<NetworkTrafficChart timeframe={timeframe} onTotalsCalculated={setNetworkTotals} />
<NetworkTrafficChart timeframe={timeframe} onTotalsCalculated={setNetworkTotals} networkUnit={networkUnit} />
</CardContent>
</Card>
@@ -712,13 +735,6 @@ export function NetworkMetrics() {
const displayInterface = currentInterfaceData || selectedInterface
console.log("[v0] Selected Interface:", selectedInterface.name)
console.log("[v0] Selected Interface bytes_recv:", selectedInterface.bytes_recv)
console.log("[v0] Selected Interface bytes_sent:", selectedInterface.bytes_sent)
console.log("[v0] Display Interface bytes_recv:", displayInterface.bytes_recv)
console.log("[v0] Display Interface bytes_sent:", displayInterface.bytes_sent)
console.log("[v0] Modal Network Data available:", !!modalNetworkData)
return (
<>
{/* Basic Information */}
@@ -869,29 +885,40 @@ export function NetworkMetrics() {
)
</h3>
<div className="space-y-4">
{/* Traffic Data - Top Row */}
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm text-muted-foreground">Bytes Received</div>
<div className="text-sm text-muted-foreground">
{networkUnit === "Bits" ? "Bits Received" : "Bytes Received"}
</div>
<div className="font-medium text-green-500 text-lg">
{formatStorage(interfaceTotals.received * 1024 * 1024 * 1024)}
{formatNetworkTraffic(
interfaceTotals.received * 1024 ** 3,
networkUnit,
2
)}
</div>
</div>
<div>
<div className="text-sm text-muted-foreground">Bytes Sent</div>
<div className="text-sm text-muted-foreground">
{networkUnit === "Bits" ? "Bits Sent" : "Bytes Sent"}
</div>
<div className="font-medium text-blue-500 text-lg">
{formatStorage(interfaceTotals.sent * 1024 * 1024 * 1024)}
{formatNetworkTraffic(
interfaceTotals.sent * 1024 ** 3,
networkUnit,
2
)}
</div>
</div>
</div>
{/* Network Traffic Chart - Full Width Below */}
<div className="bg-muted/30 rounded-lg p-4">
<NetworkTrafficChart
timeframe={modalTimeframe}
interfaceName={displayInterface.name}
onTotalsCalculated={setInterfaceTotals}
refreshInterval={60000}
networkUnit={networkUnit}
/>
</div>
@@ -932,15 +959,19 @@ export function NetworkMetrics() {
<h3 className="text-sm font-semibold text-muted-foreground mb-4">Traffic since last boot</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm text-muted-foreground">Bytes Received</div>
<div className="text-sm text-muted-foreground">
{networkUnit === "Bits" ? "Bits Received" : "Bytes Received"}
</div>
<div className="font-medium text-green-500 text-lg">
{formatBytes(displayInterface.bytes_recv)}
{formatNetworkTraffic(displayInterface.bytes_recv || 0, networkUnit)}
</div>
</div>
<div>
<div className="text-sm text-muted-foreground">Bytes Sent</div>
<div className="text-sm text-muted-foreground">
{networkUnit === "Bits" ? "Bits Sent" : "Bytes Sent"}
</div>
<div className="font-medium text-blue-500 text-lg">
{formatBytes(displayInterface.bytes_sent)}
{formatNetworkTraffic(displayInterface.bytes_sent || 0, networkUnit)}
</div>
</div>
<div>
+64 -11
View File
@@ -2,8 +2,9 @@
import { useState, useEffect } from "react"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
import { Loader2 } from "lucide-react"
import { fetchApi } from "@/lib/api-config"
import { Loader2 } from 'lucide-react'
import { fetchApi } from "../lib/api-config"
import { getNetworkUnit } from "../lib/format-network"
interface NetworkMetricsData {
time: string
@@ -17,9 +18,10 @@ interface NetworkTrafficChartProps {
interfaceName?: string
onTotalsCalculated?: (totals: { received: number; sent: number }) => void
refreshInterval?: number // En milisegundos, por defecto 60000 (60 segundos)
networkUnit?: "Bytes" | "Bits" // Added networkUnit prop
}
const CustomNetworkTooltip = ({ active, payload, label }: any) => {
const CustomNetworkTooltip = ({ active, payload, label, networkUnit }: 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">
@@ -29,7 +31,9 @@ const CustomNetworkTooltip = ({ active, payload, label }: any) => {
<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>
<span className="text-sm font-semibold text-white">
{entry.value.toFixed(3)} {networkUnit === "Bits" ? "Gb" : "GB"}
</span>
</div>
))}
</div>
@@ -44,6 +48,7 @@ export function NetworkTrafficChart({
interfaceName,
onTotalsCalculated,
refreshInterval = 60000,
networkUnit: networkUnitProp, // Rename prop to avoid conflict
}: NetworkTrafficChartProps) {
const [data, setData] = useState<NetworkMetricsData[]>([])
const [loading, setLoading] = useState(true)
@@ -53,11 +58,36 @@ export function NetworkTrafficChart({
netIn: true,
netOut: true,
})
const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">(
networkUnitProp || getNetworkUnit()
)
useEffect(() => {
const handleUnitChange = () => {
const newUnit = getNetworkUnit()
setNetworkUnit(newUnit)
}
window.addEventListener("networkUnitChanged", handleUnitChange)
window.addEventListener("storage", handleUnitChange)
return () => {
window.removeEventListener("networkUnitChanged", handleUnitChange)
window.removeEventListener("storage", handleUnitChange)
}
}, [])
useEffect(() => {
if (networkUnitProp) {
setNetworkUnit(networkUnitProp)
}
}, [networkUnitProp])
useEffect(() => {
setIsInitialLoad(true)
fetchMetrics()
}, [timeframe, interfaceName])
}, [timeframe, interfaceName, networkUnit])
useEffect(() => {
if (refreshInterval > 0) {
@@ -67,7 +97,7 @@ export function NetworkTrafficChart({
return () => clearInterval(interval)
}
}, [timeframe, interfaceName, refreshInterval])
}, [timeframe, interfaceName, refreshInterval, networkUnit]) // Added networkUnit to dependencies
const fetchMetrics = async () => {
if (isInitialLoad) {
@@ -138,6 +168,15 @@ export function NetworkTrafficChart({
const netInBytes = (item.netin || 0) * intervalSeconds
const netOutBytes = (item.netout || 0) * intervalSeconds
if (networkUnit === "Bits") {
return {
time: timeLabel,
timestamp: item.time,
netIn: Number(((netInBytes * 8) / 1024 / 1024 / 1024).toFixed(4)),
netOut: Number(((netOutBytes * 8) / 1024 / 1024 / 1024).toFixed(4)),
}
}
return {
time: timeLabel,
timestamp: item.time,
@@ -148,11 +187,20 @@ export function NetworkTrafficChart({
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)
const totalReceivedGB = result.data.reduce((sum: number, item: any, index: number) => {
const intervalSeconds = index > 0 ? item.time - result.data[index - 1].time : 60
const netInBytes = (item.netin || 0) * intervalSeconds
return sum + (netInBytes / 1024 / 1024 / 1024)
}, 0)
const totalSentGB = result.data.reduce((sum: number, item: any, index: number) => {
const intervalSeconds = index > 0 ? item.time - result.data[index - 1].time : 60
const netOutBytes = (item.netout || 0) * intervalSeconds
return sum + (netOutBytes / 1024 / 1024 / 1024)
}, 0)
if (onTotalsCalculated) {
onTotalsCalculated({ received: totalReceived, sent: totalSent })
onTotalsCalculated({ received: totalReceivedGB, sent: totalSentGB })
}
if (isInitialLoad) {
@@ -240,10 +288,15 @@ export function NetworkTrafficChart({
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
label={{ value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }}
label={{
value: networkUnit === "Bits" ? "Gb" : "GB", // Dynamic label based on unit
angle: -90,
position: "insideLeft",
fill: "currentColor",
}}
domain={[0, "auto"]}
/>
<Tooltip content={<CustomNetworkTooltip />} />
<Tooltip content={<CustomNetworkTooltip networkUnit={networkUnit} />} /> // Pass networkUnit to tooltip
<Legend verticalAlign="top" height={36} content={renderLegend} />
<Area
type="monotone"
+2 -1
View File
@@ -4,7 +4,7 @@ import type React from "react"
import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Dialog, DialogContent } from "./ui/dialog"
import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"
import {
ChevronLeft,
ChevronRight,
@@ -159,6 +159,7 @@ export function OnboardingCarousel() {
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl p-0 gap-0 overflow-hidden border-0 bg-transparent">
<DialogTitle className="sr-only">ProxMenux Onboarding</DialogTitle>
<div className="relative bg-card rounded-lg overflow-hidden shadow-2xl">
<Button
variant="ghost"
+31 -2
View File
@@ -15,6 +15,7 @@ import { OnboardingCarousel } from "./onboarding-carousel"
import { HealthStatusModal } from "./health-status-modal"
import { ReleaseNotesModal, useVersionCheck } from "./release-notes-modal"
import { getApiUrl, fetchApi } from "../lib/api-config"
import { TerminalPanel } from "./terminal-panel"
import {
RefreshCw,
AlertTriangle,
@@ -29,6 +30,7 @@ import {
Cpu,
FileText,
SettingsIcon,
Terminal,
} from "lucide-react"
import Image from "next/image"
import { ThemeToggle } from "./theme-toggle"
@@ -259,6 +261,8 @@ export function ProxmoxDashboard() {
return "VMs & LXCs"
case "hardware":
return "Hardware"
case "terminal":
return "Terminal"
case "logs":
return "System Logs"
case "settings":
@@ -412,7 +416,7 @@ export function ProxmoxDashboard() {
>
<div className="container mx-auto px-4 md:px-6 pt-4 md:pt-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-0">
<TabsList className="hidden md:grid w-full grid-cols-7 bg-card border border-border">
<TabsList className="hidden md:grid w-full grid-cols-8 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"
@@ -449,6 +453,12 @@ export function ProxmoxDashboard() {
>
System Logs
</TabsTrigger>
<TabsTrigger
value="terminal"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
Terminal
</TabsTrigger>
<TabsTrigger
value="settings"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
@@ -563,6 +573,21 @@ export function ProxmoxDashboard() {
<FileText className="h-5 w-5" />
<span>System Logs</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("terminal")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "terminal"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<Terminal className="h-5 w-5" />
<span>Terminal</span>
</Button>
<Button
variant="ghost"
onClick={() => {
@@ -611,13 +636,17 @@ export function ProxmoxDashboard() {
<SystemLogs key={`logs-${componentKey}`} />
</TabsContent>
<TabsContent value="terminal" className="mt-0">
<TerminalPanel key={`terminal-${componentKey}`} />
</TabsContent>
<TabsContent value="settings" className="space-y-4 md:space-y-6 mt-0">
<Settings />
</TabsContent>
</Tabs>
<footer className="mt-8 md:mt-12 pt-4 md:pt-6 border-t border-border text-center text-xs md:text-sm text-muted-foreground">
<p className="font-medium mb-2">ProxMenux Monitor v1.0.1</p>
<p className="font-medium mb-2">ProxMenux Monitor v1.0.2</p>
<p>
<a
href="https://ko-fi.com/macrimi"
+3 -2
View File
@@ -2,11 +2,11 @@
import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Dialog, DialogContent } from "./ui/dialog"
import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"
import { X, Sparkles, Link2, Shield, Zap, HardDrive, Gauge, Wrench, Settings } from "lucide-react"
import { Checkbox } from "./ui/checkbox"
const APP_VERSION = "1.0.1" // Sync with AppImage/package.json
const APP_VERSION = "1.0.2" // Sync with AppImage/package.json
interface ReleaseNote {
date: string
@@ -110,6 +110,7 @@ export function ReleaseNotesModal({ open, onClose }: ReleaseNotesModalProps) {
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-2xl max-h-[85vh] p-0 gap-0 border-0 bg-transparent">
<DialogTitle className="sr-only">Release Notes - Version {APP_VERSION}</DialogTitle>
<div className="relative bg-card rounded-lg shadow-2xl h-full flex flex-col max-h-[85vh]">
<Button
variant="ghost"
@@ -0,0 +1,910 @@
"use client"
import type React from "react"
import { useState, useEffect, useRef, useCallback } from "react"
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Loader2,
Activity,
ArrowUp,
ArrowDown,
ArrowLeft,
ArrowRight,
CornerDownLeft,
GripHorizontal,
} from "lucide-react"
import "xterm/css/xterm.css"
import { API_PORT } from "@/lib/api-config"
interface WebInteraction {
type: "yesno" | "menu" | "msgbox" | "input" | "inputbox"
id: string
title: string
message: string
options?: Array<{ label: string; value: string }>
default?: string
}
interface ScriptTerminalModalProps {
open: boolean
onClose: () => void
scriptPath: string
title: string
description: string
}
export function ScriptTerminalModal({
open: isOpen,
onClose,
scriptPath,
title,
description,
}: ScriptTerminalModalProps) {
const termRef = useRef<any>(null)
const wsRef = useRef<WebSocket | null>(null)
const fitAddonRef = useRef<any>(null)
const sessionIdRef = useRef<string>(Math.random().toString(36).substring(2, 8))
const [connectionStatus, setConnectionStatus] = useState<"connecting" | "online" | "offline">("connecting")
const [isComplete, setIsComplete] = useState(false)
const [currentInteraction, setCurrentInteraction] = useState<WebInteraction | null>(null)
const [interactionInput, setInteractionInput] = useState("")
const checkConnectionInterval = useRef<NodeJS.Timeout | null>(null)
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const reconnectAttemptsRef = useRef(0)
const keepAliveIntervalRef = useRef<NodeJS.Timeout | null>(null)
const [isMobile, setIsMobile] = useState(false)
const [isTablet, setIsTablet] = useState(false)
const [isWaitingNextInteraction, setIsWaitingNextInteraction] = useState(false)
const waitingTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const [modalHeight, setModalHeight] = useState(600)
const [isResizing, setIsResizing] = useState(false)
const resizeBarRef = useRef<HTMLDivElement>(null)
const modalHeightRef = useRef(600)
const terminalContainerRef = useRef<HTMLDivElement>(null)
const attemptReconnect = useCallback(() => {
if (!isOpen || isComplete || reconnectAttemptsRef.current >= 3) {
return
}
reconnectAttemptsRef.current++
setConnectionStatus("connecting")
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
}
reconnectTimeoutRef.current = setTimeout(() => {
if (wsRef.current?.readyState !== WebSocket.OPEN && termRef.current) {
if (wsRef.current) {
wsRef.current.close()
}
const wsUrl = getScriptWebSocketUrl(sessionIdRef.current)
const ws = new WebSocket(wsUrl)
wsRef.current = ws
ws.onopen = () => {
setConnectionStatus("online")
reconnectAttemptsRef.current = 0
if (keepAliveIntervalRef.current) {
clearInterval(keepAliveIntervalRef.current)
}
keepAliveIntervalRef.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "ping" }))
}
}, 30000)
const initMessage = {
script_path: scriptPath,
params: {
EXECUTION_MODE: "web",
},
}
ws.send(JSON.stringify(initMessage))
setTimeout(() => {
if (fitAddonRef.current && termRef.current && ws.readyState === WebSocket.OPEN) {
const cols = termRef.current.cols
const rows = termRef.current.rows
ws.send(JSON.stringify({ type: "resize", cols, rows }))
}
}, 100)
}
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data)
if (msg.type === "web_interaction" && msg.interaction) {
setIsWaitingNextInteraction(false)
if (waitingTimeoutRef.current) {
clearTimeout(waitingTimeoutRef.current)
}
setCurrentInteraction({
type: msg.interaction.type,
id: msg.interaction.id,
title: msg.interaction.title || "",
message: msg.interaction.message || "",
options: msg.interaction.options,
default: msg.interaction.default,
})
return
}
if (msg.type === "error") {
termRef.current?.writeln(`\x1b[31m${msg.message}\x1b[0m`)
return
}
} catch {}
termRef.current?.write(event.data)
setIsWaitingNextInteraction(false)
if (waitingTimeoutRef.current) {
clearTimeout(waitingTimeoutRef.current)
}
}
ws.onerror = () => {
setConnectionStatus("offline")
}
ws.onclose = (event) => {
setConnectionStatus("offline")
if (keepAliveIntervalRef.current) {
clearInterval(keepAliveIntervalRef.current)
keepAliveIntervalRef.current = null
}
if (!isComplete && reconnectAttemptsRef.current < 3) {
reconnectTimeoutRef.current = setTimeout(attemptReconnect, 2000)
} else {
setIsComplete(true)
}
}
}
}, 1000)
}, [isOpen, isComplete, scriptPath])
const sendKey = useCallback((key: string) => {
if (!termRef.current) return
const keyMap: Record<string, string> = {
escape: "\x1b",
tab: "\t",
up: "\x1bOA",
down: "\x1bOB",
left: "\x1bOD",
right: "\x1bOC",
enter: "\r",
ctrlc: "\x03",
}
const sequence = keyMap[key]
if (sequence && wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(sequence)
}
}, [])
const initializeTerminal = async () => {
const [TerminalClass, FitAddonClass] = await Promise.all([
import("xterm").then((mod) => mod.Terminal),
import("xterm-addon-fit").then((mod) => mod.FitAddon),
import("xterm/css/xterm.css"),
])
const fontSize = window.innerWidth < 768 ? 12 : 16
const term = new TerminalClass({
rendererType: "dom",
fontFamily: '"Courier", "Courier New", "Liberation Mono", "DejaVu Sans Mono", monospace',
fontSize: fontSize,
lineHeight: 1,
cursorBlink: true,
scrollback: 2000,
disableStdin: false,
customGlyphs: true,
fontWeight: "500",
fontWeightBold: "700",
theme: {
background: "#000000",
foreground: "#ffffff",
cursor: "#ffffff",
cursorAccent: "#000000",
black: "#2e3436",
red: "#cc0000",
green: "#4e9a06",
yellow: "#c4a000",
blue: "#3465a4",
magenta: "#75507b",
cyan: "#06989a",
white: "#d3d7cf",
brightBlack: "#555753",
brightRed: "#ef2929",
brightGreen: "#8ae234",
brightYellow: "#fce94f",
brightBlue: "#729fcf",
brightMagenta: "#ad7fa8",
brightCyan: "#34e2e2",
brightWhite: "#eeeeec",
},
})
const fitAddon = new FitAddonClass()
term.loadAddon(fitAddon)
if (terminalContainerRef.current) {
term.open(terminalContainerRef.current)
}
termRef.current = term
fitAddonRef.current = fitAddon
setTimeout(() => {
if (fitAddonRef.current && termRef.current) {
fitAddonRef.current.fit()
}
}, 100)
const wsUrl = getScriptWebSocketUrl(sessionIdRef.current)
const ws = new WebSocket(wsUrl)
wsRef.current = ws
ws.onopen = () => {
setConnectionStatus("online")
if (keepAliveIntervalRef.current) {
clearInterval(keepAliveIntervalRef.current)
}
keepAliveIntervalRef.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "ping" }))
}
}, 30000)
const initMessage = {
script_path: scriptPath,
params: {
EXECUTION_MODE: "web",
},
}
ws.send(JSON.stringify(initMessage))
setTimeout(() => {
if (fitAddonRef.current && termRef.current && ws.readyState === WebSocket.OPEN) {
const cols = termRef.current.cols
const rows = termRef.current.rows
ws.send(
JSON.stringify({
type: "resize",
cols: cols,
rows: rows,
}),
)
}
}, 100)
}
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data)
if (msg.type === "web_interaction" && msg.interaction) {
setIsWaitingNextInteraction(false)
if (waitingTimeoutRef.current) {
clearTimeout(waitingTimeoutRef.current)
}
setCurrentInteraction({
type: msg.interaction.type,
id: msg.interaction.id,
title: msg.interaction.title || "",
message: msg.interaction.message || "",
options: msg.interaction.options,
default: msg.interaction.default,
})
return
}
if (msg.type === "error") {
term.writeln(`\x1b[31m${msg.message}\x1b[0m`)
return
}
} catch {
// Not JSON, es output normal de terminal
}
term.write(event.data)
setIsWaitingNextInteraction(false)
if (waitingTimeoutRef.current) {
clearTimeout(waitingTimeoutRef.current)
}
}
ws.onerror = (error) => {
setConnectionStatus("offline")
term.writeln("\x1b[31mWebSocket error occurred\x1b[0m")
}
ws.onclose = (event) => {
setConnectionStatus("offline")
term.writeln("\x1b[33mConnection closed\x1b[0m")
if (keepAliveIntervalRef.current) {
clearInterval(keepAliveIntervalRef.current)
keepAliveIntervalRef.current = null
}
if (!isComplete) {
setIsComplete(true)
}
}
term.onData((data) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(data)
}
})
checkConnectionInterval.current = setInterval(() => {
if (wsRef.current) {
setConnectionStatus(
wsRef.current.readyState === WebSocket.OPEN
? "online"
: wsRef.current.readyState === WebSocket.CONNECTING
? "connecting"
: "offline",
)
}
}, 500)
let resizeTimeout: NodeJS.Timeout | null = null
const resizeObserver = new ResizeObserver(() => {
if (resizeTimeout) clearTimeout(resizeTimeout)
resizeTimeout = setTimeout(() => {
if (fitAddonRef.current && termRef.current && wsRef.current?.readyState === WebSocket.OPEN) {
fitAddonRef.current.fit()
wsRef.current.send(
JSON.stringify({
type: "resize",
cols: termRef.current.cols,
rows: termRef.current.rows,
}),
)
}
}, 100)
})
if (terminalContainerRef.current) {
resizeObserver.observe(terminalContainerRef.current)
}
}
useEffect(() => {
const savedHeight = localStorage.getItem("scriptModalHeight")
if (savedHeight) {
const height = Number.parseInt(savedHeight, 10)
setModalHeight(height)
modalHeightRef.current = height
}
if (isOpen) {
initializeTerminal()
} else {
if (checkConnectionInterval.current) {
clearInterval(checkConnectionInterval.current)
}
if (waitingTimeoutRef.current) {
clearTimeout(waitingTimeoutRef.current)
}
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
}
if (wsRef.current) {
wsRef.current.close()
wsRef.current = null
}
if (termRef.current) {
termRef.current.dispose()
termRef.current = null
}
if (keepAliveIntervalRef.current) {
clearInterval(keepAliveIntervalRef.current)
keepAliveIntervalRef.current = null
}
sessionIdRef.current = Math.random().toString(36).substring(2, 8)
reconnectAttemptsRef.current = 0
setIsComplete(false)
setInteractionInput("")
setCurrentInteraction(null)
setIsWaitingNextInteraction(false)
setConnectionStatus("connecting")
}
}, [isOpen])
useEffect(() => {
const updateDeviceType = () => {
const width = window.innerWidth
const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 0
const isTabletSize = width >= 768 && width <= 1366
setIsMobile(width < 768)
setIsTablet(isTouchDevice && isTabletSize)
}
updateDeviceType()
const handleResize = () => updateDeviceType()
window.addEventListener("resize", handleResize)
const handleVisibilityChange = () => {
if (!document.hidden && isOpen) {
if (wsRef.current?.readyState !== WebSocket.OPEN && !isComplete) {
attemptReconnect()
}
}
}
const handleFocus = () => {
if (isOpen && wsRef.current?.readyState !== WebSocket.OPEN && !isComplete) {
attemptReconnect()
}
}
let wakeLock: any = null
const requestWakeLock = async () => {
if ("wakeLock" in navigator && isOpen) {
try {
wakeLock = await (navigator as any).wakeLock.request("screen")
} catch (err) {
// Wake Lock no soportado o denegado, continuar sin él
}
}
}
requestWakeLock()
document.addEventListener("visibilitychange", handleVisibilityChange)
window.addEventListener("focus", handleFocus)
return () => {
window.removeEventListener("resize", handleResize)
document.removeEventListener("visibilitychange", handleVisibilityChange)
window.removeEventListener("focus", handleFocus)
if (wakeLock) {
wakeLock.release().catch(() => {})
}
}
}, [isOpen, isComplete, attemptReconnect])
const getScriptWebSocketUrl = (sid: string): string => {
if (typeof window === "undefined") {
return `ws://localhost:${API_PORT}/ws/script/${sid}`
}
const { protocol, hostname, port } = window.location
const isStandardPort = port === "" || port === "80" || port === "443"
const wsProtocol = protocol === "https:" ? "wss:" : "ws:"
if (isStandardPort) {
return `${wsProtocol}//${hostname}/ws/script/${sid}`
} else {
return `${wsProtocol}//${hostname}:${API_PORT}/ws/script/${sid}`
}
}
const handleInteractionResponse = (value: string) => {
if (!wsRef.current || !currentInteraction) {
return
}
if (value === "cancel" || value === "") {
setCurrentInteraction(null)
setInteractionInput("")
handleCloseModal()
return
}
const response = JSON.stringify({
type: "interaction_response",
id: currentInteraction.id,
value: value,
})
if (wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(response)
}
setCurrentInteraction(null)
setInteractionInput("")
waitingTimeoutRef.current = setTimeout(() => {
setIsWaitingNextInteraction(true)
}, 50)
}
const handleCloseModal = () => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.close()
}
if (checkConnectionInterval.current) {
clearInterval(checkConnectionInterval.current)
}
if (termRef.current) {
termRef.current.dispose()
}
onClose()
}
const handleResizeStart = (e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault()
e.stopPropagation()
setIsResizing(true)
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY
const startY = clientY
const startHeight = modalHeight
const handleMove = (moveEvent: MouseEvent | TouchEvent) => {
const currentY = "touches" in moveEvent ? moveEvent.touches[0].clientY : moveEvent.clientY
const deltaY = currentY - startY
const newHeight = Math.max(300, Math.min(window.innerHeight - 50, startHeight + deltaY))
modalHeightRef.current = newHeight
setModalHeight(newHeight)
}
const handleEnd = () => {
const finalHeight = modalHeightRef.current
setIsResizing(false)
document.removeEventListener("mousemove", handleMove as any)
document.removeEventListener("mouseup", handleEnd)
document.removeEventListener("touchmove", handleMove as any)
document.removeEventListener("touchend", handleEnd)
document.removeEventListener("touchcancel", handleEnd)
localStorage.setItem("scriptModalHeight", finalHeight.toString())
if (fitAddonRef.current && termRef.current && wsRef.current?.readyState === WebSocket.OPEN) {
setTimeout(() => {
fitAddonRef.current?.fit()
wsRef.current?.send(
JSON.stringify({
type: "resize",
cols: termRef.current.cols,
rows: termRef.current.rows,
}),
)
}, 100)
}
}
document.addEventListener("mousemove", handleMove as any)
document.addEventListener("mouseup", handleEnd)
document.addEventListener("touchmove", handleMove as any, { passive: false })
document.addEventListener("touchend", handleEnd)
document.addEventListener("touchcancel", handleEnd)
}
const sendCommand = (command: string) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(command)
}
}
return (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent
className="max-w-7xl p-0 flex flex-col gap-0 overflow-hidden"
style={{
height: isMobile ? "80vh" : `${modalHeight}px`,
maxHeight: "none",
}}
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
hideClose
>
<DialogTitle className="sr-only">{title}</DialogTitle>
<div className="flex items-center gap-2 p-4 border-b">
<div>
<h2 className="text-lg font-semibold">{title}</h2>
{description && <p className="text-sm text-muted-foreground">{description}</p>}
</div>
</div>
<div className="overflow-hidden relative flex-1">
<div ref={terminalContainerRef} className="w-full h-full" />
{isWaitingNextInteraction && !currentInteraction && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="flex flex-col items-center gap-3">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
<p className="text-sm text-muted-foreground">Processing...</p>
</div>
</div>
)}
</div>
{!isMobile && (
<div
ref={resizeBarRef}
onMouseDown={handleResizeStart}
onTouchStart={handleResizeStart}
className={`h-2 w-full cursor-row-resize transition-colors flex items-center justify-center group relative ${
isResizing ? "bg-blue-500" : "bg-zinc-800 hover:bg-blue-600"
}`}
style={{ touchAction: "none" }}
>
<GripHorizontal
className={`h-4 w-4 transition-colors pointer-events-none ${
isResizing ? "text-white" : "text-zinc-600 group-hover:text-white"
}`}
/>
</div>
)}
{(isMobile || isTablet) && (
<div className="flex items-center justify-center gap-1.5 px-1 py-2 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\x1b")
}}
variant="outline"
size="sm"
className="h-8 px-2 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white min-w-[50px]"
>
ESC
</Button>
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\t")
}}
variant="outline"
size="sm"
className="h-8 px-2 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white min-w-[50px]"
>
TAB
</Button>
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\x1bOA")
}}
variant="outline"
size="sm"
className="h-8 px-2.5 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white"
>
<ArrowUp className="h-4 w-4" />
</Button>
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\x1bOB")
}}
variant="outline"
size="sm"
className="h-8 px-2.5 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white"
>
<ArrowDown className="h-4 w-4" />
</Button>
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\x1bOD")
}}
variant="outline"
size="sm"
className="h-8 px-2.5 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white"
>
<ArrowLeft className="h-4 w-4" />
</Button>
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\x1bOC")
}}
variant="outline"
size="sm"
className="h-8 px-2.5 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white"
>
<ArrowRight className="h-4 w-4" />
</Button>
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\r")
}}
variant="outline"
size="sm"
className="h-8 px-2.5 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white"
>
<CornerDownLeft className="h-4 w-4" />
</Button>
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\x03")
}}
variant="outline"
size="sm"
className="h-8 px-2 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white min-w-[65px]"
>
CTRL+C
</Button>
</div>
)}
<div className="flex items-center justify-between px-4 py-3 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex items-center gap-3">
<Activity className="h-5 w-5 text-blue-500" />
<div
className={`w-2 h-2 rounded-full ${
connectionStatus === "online"
? "bg-green-500"
: connectionStatus === "connecting"
? "bg-blue-500"
: "bg-red-500"
}`}
title={
connectionStatus === "online"
? "Connected"
: connectionStatus === "connecting"
? "Connecting"
: "Disconnected"
}
></div>
<span className="text-xs text-muted-foreground">
{connectionStatus === "online"
? "Online"
: connectionStatus === "connecting"
? "Connecting..."
: "Offline"}
</span>
</div>
<Button
onClick={handleCloseModal}
variant="outline"
className="bg-red-600 hover:bg-red-700 border-red-500 text-white"
>
Close
</Button>
</div>
</DialogContent>
</Dialog>
{currentInteraction && (
<Dialog open={true}>
<DialogContent
className="max-w-4xl max-h-[80vh] overflow-y-auto animate-in fade-in-0 zoom-in-95 duration-100"
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
hideClose
>
<DialogTitle>{currentInteraction.title}</DialogTitle>
<div className="space-y-4">
<p
className="whitespace-pre-wrap"
dangerouslySetInnerHTML={{
__html: currentInteraction.message.replace(/\\n/g, "<br/>").replace(/\n/g, "<br/>"),
}}
/>
{currentInteraction.type === "yesno" && (
<div className="flex gap-2">
<Button
onClick={() => handleInteractionResponse("yes")}
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white transition-all duration-150"
>
Yes
</Button>
<Button
onClick={() => handleInteractionResponse("cancel")}
variant="outline"
className="flex-1 hover:bg-red-600 hover:text-white hover:border-red-600 transition-all duration-150"
>
Cancel
</Button>
</div>
)}
{currentInteraction.type === "menu" && currentInteraction.options && (
<div className="space-y-2">
{currentInteraction.options.map((option, index) => (
<Button
key={option.value}
onClick={() => handleInteractionResponse(option.value)}
variant="outline"
className="w-full justify-start hover:bg-blue-600 hover:text-white transition-all duration-100 animate-in fade-in-0 slide-in-from-left-2"
style={{ animationDelay: `${index * 30}ms` }}
>
{option.label}
</Button>
))}
<Button
onClick={() => handleInteractionResponse("cancel")}
variant="outline"
className="w-full hover:bg-red-600 hover:text-white hover:border-red-600 transition-all duration-150"
>
Cancel
</Button>
</div>
)}
{(currentInteraction.type === "input" || currentInteraction.type === "inputbox") && (
<div className="space-y-2">
<Label>Your input:</Label>
<Input
value={interactionInput}
onChange={(e) => setInteractionInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleInteractionResponse(interactionInput)
}
}}
placeholder={currentInteraction.default || ""}
className="transition-all duration-150"
/>
<div className="flex gap-2">
<Button
onClick={() => handleInteractionResponse(interactionInput)}
className="flex-1 bg-blue-600 hover:bg-blue-700 transition-all duration-150"
>
Submit
</Button>
<Button
onClick={() => handleInteractionResponse("cancel")}
variant="outline"
className="flex-1 hover:bg-red-600 hover:text-white hover:border-red-600 transition-all duration-150"
>
Cancel
</Button>
</div>
</div>
)}
{currentInteraction.type === "msgbox" && (
<div className="flex gap-2">
<Button
onClick={() => handleInteractionResponse("ok")}
className="flex-1 bg-blue-600 hover:bg-blue-700 transition-all duration-150"
>
OK
</Button>
<Button
onClick={() => handleInteractionResponse("cancel")}
variant="outline"
className="flex-1 hover:bg-red-600 hover:text-white hover:border-red-600 transition-all duration-150"
>
Cancel
</Button>
</div>
)}
</div>
</DialogContent>
</Dialog>
)}
</>
)
}
+60 -15
View File
@@ -5,24 +5,12 @@ import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
import {
Shield,
Lock,
User,
AlertCircle,
CheckCircle,
Info,
LogOut,
Wrench,
Package,
Key,
Copy,
Eye,
EyeOff,
} from "lucide-react"
import { Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut, Wrench, Package, Key, Copy, Eye, EyeOff, Ruler } from 'lucide-react'
import { APP_VERSION } from "./release-notes-modal"
import { getApiUrl, fetchApi } from "../lib/api-config"
import { TwoFactorSetup } from "./two-factor-setup"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { getNetworkUnit } from "../lib/format-network"
interface ProxMenuxTool {
key: string
@@ -68,9 +56,13 @@ export function Settings() {
const [generatingToken, setGeneratingToken] = useState(false)
const [tokenCopied, setTokenCopied] = useState(false)
const [networkUnitSettings, setNetworkUnitSettings] = useState<"Bytes" | "Bits">("Bytes")
const [loadingUnitSettings, setLoadingUnitSettings] = useState(true)
useEffect(() => {
checkAuthStatus()
loadProxmenuxTools()
getUnitsSettings() // Load units settings on mount
}, [])
const checkAuthStatus = async () => {
@@ -361,6 +353,28 @@ export function Settings() {
}))
}
const changeNetworkUnit = (unit: string) => {
const networkUnit = unit as "Bytes" | "Bits"
localStorage.setItem("proxmenux-network-unit", networkUnit)
setNetworkUnitSettings(networkUnit)
// Dispatch custom event to notify other components
window.dispatchEvent(new CustomEvent("networkUnitChanged", { detail: networkUnit }))
// Also dispatch storage event for backward compatibility
window.dispatchEvent(new StorageEvent("storage", {
key: "proxmenux-network-unit",
newValue: networkUnit,
url: window.location.href
}))
}
const getUnitsSettings = () => {
const networkUnit = getNetworkUnit()
setNetworkUnitSettings(networkUnit)
setLoadingUnitSettings(false)
}
return (
<div className="space-y-6">
<div>
@@ -662,6 +676,37 @@ export function Settings() {
</CardContent>
</Card>
{/* Network Units Settings */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Ruler className="h-5 w-5 text-green-500" />
<CardTitle>Network Units</CardTitle>
</div>
<CardDescription>Change how network traffic is displayed</CardDescription>
</CardHeader>
<CardContent>
{loadingUnitSettings ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin h-8 w-8 border-4 border-green-500 border-t-transparent rounded-full" />
</div>
) : (
<div className="text-foreground flex items-center justify-between">
<div className="flex items-center">Network Unit Display</div>
<Select value={networkUnitSettings} onValueChange={changeNetworkUnit}>
<SelectTrigger className="w-28 h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Bytes">Bytes</SelectItem>
<SelectItem value="Bits">Bits</SelectItem>
</SelectContent>
</Select>
</div>
)}
</CardContent>
</Card>
{/* API Access Tokens */}
{authEnabled && (
<Card>
+114 -2
View File
@@ -1,4 +1,6 @@
import { LayoutDashboard, HardDrive, Network, Server, Cpu, FileText, SettingsIcon } from "lucide-react"
"use client"
import { LayoutDashboard, HardDrive, Network, Server, Cpu, FileText, SettingsIcon, Terminal } from "lucide-react"
const menuItems = [
{ name: "Overview", href: "/", icon: LayoutDashboard },
@@ -7,7 +9,117 @@ const menuItems = [
{ name: "Virtual Machines", href: "/virtual-machines", icon: Server },
{ name: "Hardware", href: "/hardware", icon: Cpu },
{ name: "System Logs", href: "/logs", icon: FileText },
{ name: "Terminal", href: "/terminal", icon: Terminal },
{ name: "Settings", href: "/settings", icon: SettingsIcon },
]
// ... existing code ...
const Sidebar = ({ currentPath, setOpen }) => {
const handleNavigation = (tabName: string) => {
// Dispatch custom event to change tab in dashboard
const event = new CustomEvent("changeTab", { detail: { tab: tabName } })
window.dispatchEvent(event)
setOpen(false)
}
return (
<div>
<button
onClick={() => handleNavigation("overview")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/" || currentPath === "/overview"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<LayoutDashboard className="h-5 w-5" />
<span>Overview</span>
</button>
<button
onClick={() => handleNavigation("storage")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/storage"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<HardDrive className="h-5 w-5" />
<span>Storage</span>
</button>
<button
onClick={() => handleNavigation("network")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/network"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<Network className="h-5 w-5" />
<span>Network</span>
</button>
<button
onClick={() => handleNavigation("vms")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/virtual-machines"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<Server className="h-5 w-5" />
<span>VMs & LXCs</span>
</button>
<button
onClick={() => handleNavigation("hardware")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/hardware"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<Cpu className="h-5 w-5" />
<span>Hardware</span>
</button>
<button
onClick={() => handleNavigation("logs")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/logs"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<FileText className="h-5 w-5" />
<span>System Logs</span>
</button>
<button
onClick={() => handleNavigation("terminal")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/terminal"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<Terminal className="h-5 w-5" />
<span>Terminal</span>
</button>
<button
onClick={() => handleNavigation("settings")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/settings"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<SettingsIcon className="h-5 w-5" />
<span>Settings</span>
</button>
</div>
)
}
export default Sidebar
+10 -6
View File
@@ -597,13 +597,15 @@ export function StorageOverview() {
<CardContent>
<div className="space-y-4">
{proxmoxStorage.storage
.filter(
(storage) =>
storage && storage.name && storage.total > 0 && storage.used >= 0 && storage.available >= 0,
)
.filter((storage) => storage && storage.name && storage.used >= 0 && storage.available >= 0)
.sort((a, b) => a.name.localeCompare(b.name))
.map((storage) => (
<div key={storage.name} className="border rounded-lg p-4">
<div
key={storage.name}
className={`border rounded-lg p-4 ${
storage.status === "error" ? "border-red-500/50 bg-red-500/5" : ""
}`}
>
<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">
@@ -625,7 +627,9 @@ export function StorageOverview() {
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 === "error"
? "bg-red-500/10 text-red-500 border-red-500/20"
: "bg-gray-500/10 text-gray-500 border-gray-500/20"
}
>
{storage.status}
+92 -57
View File
@@ -9,6 +9,7 @@ import { NodeMetricsCharts } from "./node-metrics-charts"
import { NetworkTrafficChart } from "./network-traffic-chart"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { fetchApi } from "../lib/api-config"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
interface SystemData {
cpu_usage: number
@@ -96,14 +97,21 @@ interface ProxmoxStorageData {
}>
}
const fetchSystemData = async (): Promise<SystemData | null> => {
try {
const data = await fetchApi<SystemData>("/api/system")
return data
} catch (error) {
console.error("[v0] Failed to fetch system data:", error)
return null
const fetchSystemData = async (retries = 3, delayMs = 500): Promise<SystemData | null> => {
for (let attempt = 0; attempt < retries; attempt++) {
try {
const data = await fetchApi<SystemData>("/api/system")
return data
} catch (error) {
if (attempt === retries - 1) {
console.error("[v0] Failed to fetch system data after retries:", error)
return null
}
// Wait before retry
await new Promise((resolve) => setTimeout(resolve, delayMs))
}
}
return null
}
const fetchVMData = async (): Promise<VMData[]> => {
@@ -146,6 +154,12 @@ const fetchProxmoxStorageData = async (): Promise<ProxmoxStorageData | null> =>
}
}
const getUnitsSettings = (): "Bytes" | "Bits" => {
if (typeof window === "undefined") return "Bytes"
const raw = window.localStorage.getItem("proxmenux-network-unit")
return raw && raw.toLowerCase() === "bits" ? "Bits" : "Bytes"
}
export function SystemOverview() {
const [systemData, setSystemData] = useState<SystemData | null>(null)
const [vmData, setVmData] = useState<VMData[]>([])
@@ -159,8 +173,10 @@ export function SystemOverview() {
network: true,
})
const [error, setError] = useState<string | null>(null)
const [hasAttemptedLoad, setHasAttemptedLoad] = useState(false) // Added hasAttemptedLoad state
const [networkTimeframe, setNetworkTimeframe] = useState("day")
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes") // Added networkUnit state
useEffect(() => {
const fetchAllData = async () => {
@@ -173,6 +189,8 @@ export function SystemOverview() {
fetchNetworkData().finally(() => setLoadingStates((prev) => ({ ...prev, network: false }))),
])
setHasAttemptedLoad(true)
if (!systemResult) {
setError("Flask server not available. Please ensure the server is running.")
return
@@ -215,24 +233,27 @@ export function SystemOverview() {
if (data) setNetworkData(data)
}, 59000)
setNetworkUnit(getNetworkUnit()) // Load initial setting
const handleUnitChange = (e: CustomEvent) => {
setNetworkUnit(e.detail === "Bits" ? "Bits" : "Bytes")
}
window.addEventListener("networkUnitChanged" as any, handleUnitChange)
return () => {
clearInterval(systemInterval)
clearInterval(vmInterval)
clearInterval(storageInterval)
clearInterval(networkInterval)
window.removeEventListener("networkUnitChanged" as any, handleUnitChange)
}
}, [])
const isInitialLoading = loadingStates.system && !systemData
if (isInitialLoading) {
if (!hasAttemptedLoad || loadingStates.system) {
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-2 lg:grid-cols-4 gap-3 lg:gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
{[...Array(4)].map((_, i) => (
<Card key={i} className="bg-card border-border animate-pulse">
<CardContent className="p-6">
@@ -298,16 +319,6 @@ export function SystemOverview() {
return (bytes / 1024 ** 3).toFixed(2)
}
const formatStorage = (sizeInGB: number): string => {
if (sizeInGB < 1) {
return `${(sizeInGB * 1024).toFixed(1)} MB`
} else if (sizeInGB > 999) {
return `${(sizeInGB / 1024).toFixed(2)} TB`
} else {
return `${sizeInGB.toFixed(2)} GB`
}
}
const tempStatus = getTemperatureStatus(systemData.temperature)
const localStorage = proxmoxStorageData?.storage.find((s) => s.name === "local")
@@ -411,26 +422,6 @@ export function SystemOverview() {
</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>
<CardTitle className="text-foreground flex items-center">
@@ -465,6 +456,26 @@ export function SystemOverview() {
)}
</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>
</div>
<NodeMetricsCharts />
@@ -496,7 +507,9 @@ export function SystemOverview() {
<div className="space-y-2 pb-4 border-b-2 border-border">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-foreground">Total Node Capacity:</span>
<span className="text-lg font-bold text-foreground">{formatStorage(totalCapacity)}</span>
<span className="text-lg font-bold text-foreground">
{formatNetworkTraffic(totalCapacity, "Bytes")}
</span>
</div>
<Progress
value={totalPercent}
@@ -505,10 +518,16 @@ export function SystemOverview() {
<div className="flex justify-between items-center mt-1">
<div className="flex items-center gap-3">
<span className="text-xs text-muted-foreground">
Used: <span className="font-semibold text-foreground">{formatStorage(totalUsed)}</span>
Used:{" "}
<span className="font-semibold text-foreground">
{formatNetworkTraffic(totalUsed, "Bytes")}
</span>
</span>
<span className="text-xs text-muted-foreground">
Free: <span className="font-semibold text-green-500">{formatStorage(totalAvailable)}</span>
Free:{" "}
<span className="font-semibold text-green-500">
{formatNetworkTraffic(totalAvailable, "Bytes")}
</span>
</span>
</div>
<span className="text-xs font-semibold text-muted-foreground">{totalPercent.toFixed(1)}%</span>
@@ -535,18 +554,21 @@ export function SystemOverview() {
<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>
<span className="text-sm font-semibold text-foreground">
{formatNetworkTraffic(vmLxcStorageUsed, "Bytes")}
</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)}
{formatNetworkTraffic(vmLxcStorageAvailable, "Bytes")}
</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)}
{formatNetworkTraffic(vmLxcStorageUsed, "Bytes")} /{" "}
{formatNetworkTraffic(vmLxcStorageTotal, "Bytes")}
</span>
<span className="text-xs text-muted-foreground">{vmLxcStoragePercent.toFixed(1)}%</span>
</div>
@@ -568,18 +590,21 @@ export function SystemOverview() {
<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>
<span className="text-sm font-semibold text-foreground">
{formatNetworkTraffic(localStorage.used, "Bytes")}
</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)}
{formatNetworkTraffic(localStorage.available, "Bytes")}
</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)}
{formatNetworkTraffic(localStorage.used, "Bytes")} /{" "}
{formatNetworkTraffic(localStorage.total, "Bytes")}
</span>
<span className="text-xs text-muted-foreground">{localStorage.percent.toFixed(1)}%</span>
</div>
@@ -667,21 +692,31 @@ export function SystemOverview() {
<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)}
{" "}
{networkUnit === "Bytes"
? `${networkTotals.received.toFixed(2)} GB`
: formatNetworkTraffic(networkTotals.received * 1024 * 1024 * 1024, "Bits")}
<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)}
{" "}
{networkUnit === "Bytes"
? `${networkTotals.sent.toFixed(2)} GB`
: formatNetworkTraffic(networkTotals.sent * 1024 * 1024 * 1024, "Bits")}
<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} />
<NetworkTrafficChart
timeframe={networkTimeframe}
onTotalsCalculated={setNetworkTotals}
networkUnit={networkUnit}
/>
</div>
</div>
) : (
File diff suppressed because it is too large Load Diff
+10 -6
View File
@@ -31,8 +31,10 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
hideClose?: boolean
}
>(({ className, children, hideClose, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
@@ -45,10 +47,12 @@ const DialogContent = React.forwardRef<
{...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>
{!hideClose && (
<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>
))
+38 -25
View File
@@ -8,24 +8,11 @@ import { Badge } from "./ui/badge"
import { Progress } from "./ui/progress"
import { Button } from "./ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"
import {
Server,
Play,
Square,
Cpu,
MemoryStick,
HardDrive,
Network,
Power,
RotateCcw,
StopCircle,
Container,
ChevronDown,
ChevronUp,
} from "lucide-react"
import { Server, Play, Square, Cpu, MemoryStick, HardDrive, Network, Power, RotateCcw, StopCircle, Container, ChevronDown, ChevronUp } from 'lucide-react'
import useSWR from "swr"
import { MetricsView } from "./metrics-dialog"
import { formatStorage } from "@/lib/utils" // Import formatStorage utility
import { formatStorage } from "../lib/utils"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
import { fetchApi } from "../lib/api-config"
interface VMData {
@@ -137,8 +124,15 @@ const fetcher = async (url: string) => {
return fetchApi(url)
}
const formatBytes = (bytes: number | undefined): string => {
if (!bytes || bytes === 0) return "0 B"
const formatBytes = (bytes: number | undefined, isNetwork: boolean = false): string => {
if (!bytes || bytes === 0) return isNetwork ? "0 B/s" : "0 B"
if (isNetwork) {
const networkUnit = getNetworkUnit()
return formatNetworkTraffic(bytes, networkUnit, 2)
}
// For non-network (disk), use standard bytes
const k = 1024
const sizes = ["B", "KB", "MB", "GB", "TB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
@@ -272,6 +266,7 @@ export function VirtualMachines() {
const [selectedMetric, setSelectedMetric] = useState<string | null>(null)
const [ipsLoaded, setIpsLoaded] = useState(false)
const [loadingIPs, setLoadingIPs] = useState(false)
const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes")
useEffect(() => {
const fetchLXCIPs = async () => {
@@ -324,6 +319,23 @@ export function VirtualMachines() {
fetchLXCIPs()
}, [vmData, ipsLoaded, loadingIPs])
// Load initial network unit and listen for changes
useEffect(() => {
setNetworkUnit(getNetworkUnit())
const handleNetworkUnitChange = () => {
setNetworkUnit(getNetworkUnit())
}
window.addEventListener("networkUnitChanged", handleNetworkUnitChange)
window.addEventListener("storage", handleNetworkUnitChange)
return () => {
window.removeEventListener("networkUnitChanged", handleNetworkUnitChange)
window.removeEventListener("storage", handleNetworkUnitChange)
}
}, [])
const handleVMClick = async (vm: VMData) => {
setSelectedVM(vm)
setCurrentView("main")
@@ -924,11 +936,11 @@ export function VirtualMachines() {
<div className="text-sm font-semibold space-y-0.5">
<div className="flex items-center gap-1">
<HardDrive className="h-3 w-3 text-green-500" />
<span className="text-green-500"> {formatBytes(vm.diskread)}</span>
<span className="text-green-500"> {formatBytes(vm.diskread, false)}</span>
</div>
<div className="flex items-center gap-1">
<HardDrive className="h-3 w-3 text-blue-500" />
<span className="text-blue-500"> {formatBytes(vm.diskwrite)}</span>
<span className="text-blue-500"> {formatBytes(vm.diskwrite, false)}</span>
</div>
</div>
</div>
@@ -938,11 +950,11 @@ export function VirtualMachines() {
<div className="text-sm font-semibold space-y-0.5">
<div className="flex items-center gap-1">
<Network className="h-3 w-3 text-green-500" />
<span className="text-green-500"> {formatBytes(vm.netin)}</span>
<span className="text-green-500"> {formatBytes(vm.netin, true)}</span>
</div>
<div className="flex items-center gap-1">
<Network className="h-3 w-3 text-blue-500" />
<span className="text-blue-500"> {formatBytes(vm.netout)}</span>
<span className="text-blue-500"> {formatBytes(vm.netout, true)}</span>
</div>
</div>
</div>
@@ -1167,11 +1179,11 @@ export function VirtualMachines() {
<div className="space-y-1">
<div className="text-sm text-green-500 flex items-center gap-1">
<span></span>
<span>{((selectedVM.netin || 0) / 1024 ** 2).toFixed(2)} MB</span>
<span>{formatNetworkTraffic(selectedVM.netin || 0, networkUnit)}</span>
</div>
<div className="text-sm text-blue-500 flex items-center gap-1">
<span></span>
<span>{((selectedVM.netout || 0) / 1024 ** 2).toFixed(2)} MB</span>
<span>{formatNetworkTraffic(selectedVM.netout || 0, networkUnit)}</span>
</div>
</div>
</div>
@@ -1226,7 +1238,8 @@ export function VirtualMachines() {
</>
) : (
<>
<ChevronDown className="h-3 w-3 mr-1" />+ Info
<ChevronDown className="h-3 w-3 mr-1" />
+ Info
</>
)}
</Button>
+68
View File
@@ -0,0 +1,68 @@
/**
* Utility functions for formatting network traffic data
* Supports conversion between Bytes and Bits based on user preferences
*/
export type NetworkUnit = 'Bytes' | 'Bits';
/**
* Format network traffic value with appropriate unit
* @param bytes - Value in bytes
* @param unit - Target unit ('Bytes' or 'Bits')
* @param decimals - Number of decimal places (default: 2)
* @returns Formatted string with value and unit
*/
export function formatNetworkTraffic(
bytes: number,
unit: NetworkUnit = 'Bytes',
decimals: number = 2
): string {
if (bytes === 0) return unit === 'Bits' ? '0 b' : '0 B';
const k = unit === 'Bits' ? 1000 : 1024;
const dm = decimals < 0 ? 0 : Math.min(decimals, 2);
// For Bits: convert bytes to bits first (multiply by 8)
const value = unit === 'Bits' ? bytes * 8 : bytes;
const sizes = unit === 'Bits'
? ['b', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb']
: ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(value) / Math.log(k));
const finalDecimals = 2; // Always use 2 decimals for consistency
const formattedValue = parseFloat((value / Math.pow(k, i)).toFixed(finalDecimals));
return `${formattedValue} ${sizes[i]}`;
}
/**
* Get the current network unit preference from localStorage
* @returns 'Bytes' or 'Bits'
*/
export function getNetworkUnit(): NetworkUnit {
if (typeof window === 'undefined') return 'Bytes';
const stored = localStorage.getItem('proxmenux-network-unit');
return stored === 'Bits' ? 'Bits' : 'Bytes';
}
/**
* Get the label for network traffic based on current unit
* @param direction - 'received' or 'sent'
* @returns Label string
*/
export function getNetworkLabel(direction: 'received' | 'sent'): string {
const unit = getNetworkUnit();
const prefix = direction === 'received' ? 'Received' : 'Sent';
return unit === 'Bits' ? `${prefix}` : `${prefix}`;
}
/**
* Get the unit suffix for displaying in charts
* @returns Unit suffix string (e.g., 'GB' or 'Gb')
*/
export function getNetworkUnitSuffix(): string {
const unit = getNetworkUnit();
return unit === 'Bits' ? 'b' : 'B';
}
+39
View File
@@ -0,0 +1,39 @@
import { exec } from "child_process"
import { promisify } from "util"
const execAsync = promisify(exec)
interface ScriptExecutorOptions {
env?: Record<string, string>
timeout?: number
}
interface ScriptResult {
stdout: string
stderr: string
exitCode: number
}
export async function executeScript(scriptPath: string, options: ScriptExecutorOptions = {}): Promise<ScriptResult> {
const { env = {}, timeout = 300000 } = options // 5 minutes default timeout
try {
const { stdout, stderr } = await execAsync(`bash ${scriptPath}`, {
env: { ...process.env, ...env },
timeout,
maxBuffer: 1024 * 1024 * 10, // 10MB buffer
})
return {
stdout,
stderr,
exitCode: 0,
}
} catch (error: any) {
return {
stdout: error.stdout || "",
stderr: error.stderr || error.message || "Unknown error",
exitCode: error.code || 1,
}
}
}
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ProxMenux-Monitor",
"version": "1.0.1",
"version": "1.0.2",
"description": "Proxmox System Monitoring Dashboard",
"private": true,
"scripts": {
@@ -55,11 +55,14 @@
"react-hook-form": "^7.60.0",
"react-resizable-panels": "^2.1.7",
"recharts": "2.15.4",
"socket.io-client": "^4.8.1",
"sonner": "^1.7.4",
"swr": "^2.2.5",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.9",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"zod": "3.25.67"
},
"devDependencies": {
+19 -3
View File
@@ -85,6 +85,10 @@ cp "$SCRIPT_DIR/health_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠
cp "$SCRIPT_DIR/health_persistence.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ health_persistence.py not found"
cp "$SCRIPT_DIR/flask_health_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_health_routes.py not found"
cp "$SCRIPT_DIR/flask_proxmenux_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_proxmenux_routes.py not found"
cp "$SCRIPT_DIR/flask_terminal_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_terminal_routes.py not found"
cp "$SCRIPT_DIR/hardware_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ hardware_monitor.py not found"
cp "$SCRIPT_DIR/proxmox_storage_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ proxmox_storage_monitor.py not found"
cp "$SCRIPT_DIR/flask_script_runner.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_script_runner.py not found"
echo "📋 Adding translation support..."
cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF'
@@ -281,7 +285,15 @@ if [ -f "$APP_DIR/proxmenux-monitor.png" ]; then
fi
echo "📦 Installing Python dependencies..."
# Phase 1: Install googletrans with its old dependencies
pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" \
googletrans==4.0.0-rc1 \
httpx==0.13.3 \
httpcore==0.9.1 \
h11==0.9.0 || true
# Phase 2: Install modern Flask/WebSocket dependencies (will upgrade h11 and related packages)
pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" --upgrade --no-deps \
flask \
flask-cors \
psutil \
@@ -289,11 +301,15 @@ pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" \
PyJWT \
pyotp \
segno \
googletrans==4.0.0-rc1 \
httpx==0.13.3 \
httpcore==0.9.1 \
beautifulsoup4
# Phase 3: Install WebSocket with newer h11
pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" --upgrade \
h11>=0.14.0 \
wsproto>=1.2.0 \
simple-websocket>=0.10.0 \
flask-sock>=0.6.0
cat > "$APP_DIR/usr/lib/python3/dist-packages/cgi.py" << 'PYEOF'
from typing import Tuple, Dict
try:
+261
View File
@@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""
Script Runner System for ProxMenux
Executes bash scripts and provides real-time log streaming with interactive menu support
"""
import os
import sys
import json
import subprocess
import threading
import time
from datetime import datetime
from pathlib import Path
import uuid
class ScriptRunner:
"""Manages script execution with real-time log streaming and menu interactions"""
def __init__(self):
self.active_sessions = {}
self.log_dir = Path("/var/log/proxmenux/scripts")
self.log_dir.mkdir(parents=True, exist_ok=True)
self.interaction_handlers = {}
def create_session(self, script_name):
"""Create a new script execution session"""
session_id = str(uuid.uuid4())[:8]
log_file = self.log_dir / f"{script_name}_{session_id}_{int(time.time())}.log"
self.active_sessions[session_id] = {
'script_name': script_name,
'log_file': str(log_file),
'start_time': datetime.now().isoformat(),
'status': 'initializing',
'process': None,
'exit_code': None,
'pending_interaction': None
}
return session_id
def execute_script(self, script_path, session_id, env_vars=None):
"""Execute a script in web mode with logging"""
if session_id not in self.active_sessions:
return {'success': False, 'error': 'Invalid session ID'}
session = self.active_sessions[session_id]
log_file = session['log_file']
print(f"[DEBUG] execute_script called for session {session_id}", file=sys.stderr, flush=True)
print(f"[DEBUG] Script path: {script_path}", file=sys.stderr, flush=True)
print(f"[DEBUG] Log file: {log_file}", file=sys.stderr, flush=True)
# Prepare environment
env = os.environ.copy()
env['EXECUTION_MODE'] = 'web'
env['LOG_FILE'] = log_file
if env_vars:
env.update(env_vars)
print(f"[DEBUG] Environment variables set: EXECUTION_MODE=web, LOG_FILE={log_file}", file=sys.stderr, flush=True)
# Initialize log file
with open(log_file, 'w') as f:
init_line = json.dumps({
'type': 'init',
'session_id': session_id,
'script': script_path,
'timestamp': int(time.time())
}) + '\n'
f.write(init_line)
print(f"[DEBUG] Wrote init line to log: {init_line.strip()}", file=sys.stderr, flush=True)
try:
# Execute script
session['status'] = 'running'
print(f"[DEBUG] Starting subprocess with /bin/bash {script_path}", file=sys.stderr, flush=True)
process = subprocess.Popen(
['/bin/bash', script_path],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0 # Unbuffered
)
print(f"[DEBUG] Process started with PID: {process.pid}", file=sys.stderr, flush=True)
session['process'] = process
lines_read = [0] # Lista para compartir entre threads
def monitor_output():
print(f"[DEBUG] monitor_output thread started for session {session_id}", file=sys.stderr, flush=True)
print(f"[DEBUG] Will monitor log file: {log_file}", file=sys.stderr, flush=True)
try:
# Read log file in real-time (similar to tail -f)
last_position = 0
# Wait a moment for script to start writing
time.sleep(0.5)
while process.poll() is None or last_position < os.path.getsize(log_file):
try:
if os.path.exists(log_file):
with open(log_file, 'r') as log_f:
log_f.seek(last_position)
new_lines = log_f.readlines()
for line in new_lines:
decoded_line = line.rstrip()
if decoded_line: # Skip empty lines
lines_read[0] += 1
print(f"[DEBUG] Read line {lines_read[0]} from log: {decoded_line[:100]}...", file=sys.stderr, flush=True)
# Check for interaction requests in the line
if 'WEB_INTERACTION:' in decoded_line:
print(f"[DEBUG] Detected WEB_INTERACTION line: {decoded_line}", file=sys.stderr, flush=True)
session['pending_interaction'] = decoded_line
last_position = log_f.tell()
except Exception as e:
print(f"[DEBUG ERROR] Error reading log file: {e}", file=sys.stderr, flush=True)
time.sleep(0.1) # Poll every 100ms
print(f"[DEBUG] monitor_output thread finished. Total lines read: {lines_read[0]}", file=sys.stderr, flush=True)
except Exception as e:
print(f"[DEBUG ERROR] Exception in monitor_output: {e}", file=sys.stderr, flush=True)
monitor_thread = threading.Thread(target=monitor_output, daemon=False)
monitor_thread.start()
print(f"[DEBUG] Waiting for process to complete...", file=sys.stderr, flush=True)
# Wait for completion
process.wait()
print(f"[DEBUG] Process exited with code: {process.returncode}", file=sys.stderr, flush=True)
monitor_thread.join(timeout=30)
if monitor_thread.is_alive():
print(f"[DEBUG WARNING] monitor_thread still alive after 30s timeout", file=sys.stderr, flush=True)
else:
print(f"[DEBUG] monitor_thread joined successfully", file=sys.stderr, flush=True)
session['exit_code'] = process.returncode
session['status'] = 'completed' if process.returncode == 0 else 'failed'
session['end_time'] = datetime.now().isoformat()
print(f"[DEBUG] Script execution completed. Lines captured: {lines_read[0]}", file=sys.stderr, flush=True)
return {
'success': True,
'session_id': session_id,
'exit_code': process.returncode,
'log_file': log_file
}
except Exception as e:
print(f"[DEBUG ERROR] Exception in execute_script: {e}", file=sys.stderr, flush=True)
session['status'] = 'error'
session['error'] = str(e)
return {
'success': False,
'error': str(e)
}
def get_session_status(self, session_id):
"""Get current status of a script execution session"""
if session_id not in self.active_sessions:
return {'success': False, 'error': 'Session not found'}
session = self.active_sessions[session_id]
return {
'success': True,
'session_id': session_id,
'status': session['status'],
'start_time': session['start_time'],
'script_name': session['script_name'],
'exit_code': session['exit_code'],
'pending_interaction': session.get('pending_interaction')
}
def respond_to_interaction(self, session_id, interaction_id, value):
"""Respond to a script interaction request"""
if session_id not in self.active_sessions:
return {'success': False, 'error': 'Session not found'}
session = self.active_sessions[session_id]
# Write response to file that script is waiting for
response_file = f"/tmp/nvidia_response_{interaction_id}.json"
with open(response_file, 'w') as f:
json.dump({
'interaction_id': interaction_id,
'value': value,
'timestamp': int(time.time())
}, f)
# Clear pending interaction
session['pending_interaction'] = None
return {'success': True}
def stream_logs(self, session_id):
"""Generator that yields log entries as they are written"""
if session_id not in self.active_sessions:
yield json.dumps({'type': 'error', 'message': 'Invalid session ID'})
return
session = self.active_sessions[session_id]
log_file = session['log_file']
# Wait for log file to be created
timeout = 10
start = time.time()
while not os.path.exists(log_file) and (time.time() - start) < timeout:
time.sleep(0.1)
if not os.path.exists(log_file):
yield json.dumps({'type': 'error', 'message': 'Log file not created'})
return
# Stream log file
with open(log_file, 'r') as f:
# Start from beginning
f.seek(0)
while session['status'] in ['initializing', 'running']:
line = f.readline()
if line:
# Try to parse as JSON, yield as-is if not JSON
try:
log_entry = json.loads(line.strip())
yield json.dumps(log_entry)
except json.JSONDecodeError:
yield json.dumps({'type': 'raw', 'message': line.strip()})
else:
time.sleep(0.1)
# Read any remaining lines after completion
for line in f:
try:
log_entry = json.loads(line.strip())
yield json.dumps(log_entry)
except json.JSONDecodeError:
yield json.dumps({'type': 'raw', 'message': line.strip()})
def cleanup_session(self, session_id):
"""Clean up a completed session"""
if session_id in self.active_sessions:
del self.active_sessions[session_id]
return {'success': True}
return {'success': False, 'error': 'Session not found'}
# Global instance
script_runner = ScriptRunner()
+390 -58
View File
@@ -1,49 +1,123 @@
#!/usr/bin/env python3
"""
ProxMenux Flask Server
Provides REST API endpoints for Proxmox monitoring data
Runs on port 8008 and serves system metrics, storage info, network stats, etc.
Also serves the Next.js dashboard as static files
- Provides REST API endpoints for Proxmox monitoring (system, storage, network, VMs, etc.)
- Serves the Next.js dashboard as static files
- Integrates a web terminal powered by xterm.js
"""
from flask import Flask, jsonify, request, send_from_directory, send_file
from flask_cors import CORS
import psutil
import subprocess
import json
import logging
import math
import os
import platform
import re
import select
import shutil
import socket
import subprocess
import sys
import time
import socket
import urllib.parse
import hardware_monitor
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta
import re # Added for regex matching
import select # Added for non-blocking read
import shutil # Added for shutil.which
import xml.etree.ElementTree as ET # Added for XML parsing
import math # Imported math for format_bytes function
import urllib.parse # Added for URL encoding
import platform # Added for platform.release()
import hashlib
import secrets
import jwt
from functools import wraps
from pathlib import Path
from flask_health_routes import health_bp
import jwt
import psutil
from flask import Flask, jsonify, request, send_file, send_from_directory, Response
from flask_cors import CORS
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Ensure local imports work even if working directory changes
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
if BASE_DIR not in sys.path:
sys.path.insert(0, BASE_DIR)
from flask_auth_routes import auth_bp
from flask_proxmenux_routes import proxmenux_bp
from jwt_middleware import require_auth
from flask_script_runner import script_runner
import threading
from proxmox_storage_monitor import proxmox_storage_monitor
from flask_terminal_routes import terminal_bp, init_terminal_routes # noqa: E402
from flask_health_routes import health_bp # noqa: E402
from flask_auth_routes import auth_bp # noqa: E402
from flask_proxmenux_routes import proxmenux_bp # noqa: E402
from jwt_middleware import require_auth # noqa: E402
# -------------------------------------------------------------------
# Logging
# -------------------------------------------------------------------
logger = logging.getLogger("proxmenux.flask")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
# -------------------------------------------------------------------
# Proxmox node name cache
# -------------------------------------------------------------------
_PROXMOX_NODE_CACHE = {"name": None, "timestamp": 0.0}
_PROXMOX_NODE_CACHE_TTL = 300 # seconds (5 minutes)
def get_proxmox_node_name() -> str:
"""
Retrieve the real Proxmox node name.
- First tries reading from: `pvesh get /nodes`
- Uses an in-memory cache to avoid repeated API calls
- Falls back to the short hostname if the API call fails
"""
now = time.time()
cached_name = _PROXMOX_NODE_CACHE.get("name")
cached_ts = _PROXMOX_NODE_CACHE.get("timestamp", 0.0)
# Cache hit
if cached_name and (now - float(cached_ts)) < _PROXMOX_NODE_CACHE_TTL:
return str(cached_name)
# Try Proxmox API
try:
result = subprocess.run(
["pvesh", "get", "/nodes", "--output-format", "json"],
capture_output=True,
text=True,
timeout=5,
check=False,
)
if result.returncode == 0 and result.stdout:
nodes = json.loads(result.stdout)
if isinstance(nodes, list) and nodes:
node_name = nodes[0].get("node")
if node_name:
_PROXMOX_NODE_CACHE["name"] = node_name
_PROXMOX_NODE_CACHE["timestamp"] = now
return node_name
except Exception as exc:
logger.warning("Failed to get Proxmox node name from API: %s", exc)
# Fallback: short hostname (without domain)
hostname = socket.gethostname()
short_hostname = hostname.split(".", 1)[0]
return short_hostname
# -------------------------------------------------------------------
# Flask application and Blueprints
# -------------------------------------------------------------------
app = Flask(__name__)
CORS(app) # Enable CORS for Next.js frontend
# Register Blueprints
app.register_blueprint(auth_bp)
app.register_blueprint(health_bp)
app.register_blueprint(proxmenux_bp)
# Initialize terminal / WebSocket routes
init_terminal_routes(app)
def identify_gpu_type(name, vendor=None, bus=None, driver=None):
@@ -451,7 +525,8 @@ def get_vm_lxc_names():
vm_lxc_map = {}
try:
local_node = socket.gethostname()
# local_node = socket.gethostname()
local_node = get_proxmox_node_name()
result = subprocess.run(['pvesh', 'get', '/cluster/resources', '--type', 'vm', '--output-format', 'json'],
capture_output=True, text=True, timeout=10)
@@ -1645,7 +1720,8 @@ def get_smart_data(disk_name):
def get_proxmox_storage():
"""Get Proxmox storage information using pvesh (filtered by local node)"""
try:
local_node = socket.gethostname()
# local_node = socket.gethostname()
local_node = get_proxmox_node_name()
result = subprocess.run(['pvesh', 'get', '/cluster/resources', '--type', 'storage', '--output-format', 'json'],
capture_output=True, text=True, timeout=10)
@@ -1685,18 +1761,7 @@ def get_proxmox_storage():
pass
continue
# Si total es 0, significa que hay un error de conexión o el datastore no está disponible
if total == 0:
# print(f"[v0] Skipping storage {name} - invalid data (total=0, likely connection error)")
pass
continue
# Si el status es "inactive", también lo omitimos
if status.lower() != "available":
# print(f"[v0] Skipping storage {name} - status is not available: {status}")
pass
continue
# No filtrar storages no disponibles - mantenerlos para mostrar errores
# Calcular porcentaje
percent = (used / total * 100) if total > 0 else 0.0
@@ -1705,10 +1770,18 @@ def get_proxmox_storage():
used_gb = round(used / (1024**3), 2)
available_gb = round(available / (1024**3), 2)
# Determine storage status
if total == 0:
storage_status = 'error'
elif status.lower() != "available":
storage_status = 'error'
else:
storage_status = 'active'
storage_info = {
'name': name,
'type': storage_type,
'status': 'active', # Normalizar status para compatibilidad con frontend
'status': storage_status, # Usar el status determinado (active o error)
'total': total_gb,
'used': used_gb,
'available': available_gb,
@@ -1719,6 +1792,17 @@ def get_proxmox_storage():
storage_list.append(storage_info)
# Get unavailable storages from monitor
storage_status_data = proxmox_storage_monitor.get_storage_status()
unavailable_storages = storage_status_data.get('unavailable', [])
# Get list of storage names already added
existing_storage_names = {s['name'] for s in storage_list}
# Add unavailable storages to the list (only if not already present)
for unavailable_storage in unavailable_storages:
if unavailable_storage['name'] not in existing_storage_names:
storage_list.append(unavailable_storage)
return {'storage': storage_list}
@@ -1970,7 +2054,8 @@ def get_network_info():
'bridge_interfaces': [], # Added separate list for bridge interfaces
'vm_lxc_interfaces': [],
'traffic': {'bytes_sent': 0, 'bytes_recv': 0, 'packets_sent': 0, 'packets_recv': 0},
'hostname': socket.gethostname(),
# 'hostname': socket.gethostname(),
'hostname': get_proxmox_node_name(),
'domain': None,
'dns_servers': []
}
@@ -2197,7 +2282,9 @@ def get_proxmox_vms():
all_vms = []
try:
local_node = socket.gethostname()
# local_node = socket.gethostname()
local_node = get_proxmox_node_name()
# print(f"[v0] Local node detected: {local_node}")
pass
@@ -2508,10 +2595,11 @@ def get_ups_info():
# END OF CHANGES FOR get_ups_info
def identify_temperature_sensor(sensor_name, adapter):
def identify_temperature_sensor(sensor_name, adapter, chip_name=None):
"""Identify what a temperature sensor corresponds to"""
sensor_lower = sensor_name.lower()
adapter_lower = adapter.lower() if adapter else ""
chip_lower = chip_name.lower() if chip_name else ""
# CPU/Package temperatures
if "package" in sensor_lower or "tctl" in sensor_lower or "tccd" in sensor_lower:
@@ -2519,6 +2607,18 @@ def identify_temperature_sensor(sensor_name, adapter):
if "core" in sensor_lower:
core_num = re.search(r'(\d+)', sensor_name)
return f"CPU Core {core_num.group(1)}" if core_num else "CPU Core"
# <CHANGE> DDR5 Memory temperature sensors (SPD5118)
if "spd5118" in chip_lower or ("smbus" in adapter_lower and "temp1" in sensor_lower):
# Try to identify which DIMM slot
# Example: spd5118-i2c-0-50 -> i2c bus 0, address 0x50 (DIMM A1)
# Addresses: 0x50=DIMM1, 0x51=DIMM2, 0x52=DIMM3, 0x53=DIMM4, etc.
dimm_match = re.search(r'i2c-\d+-([0-9a-f]+)', chip_lower)
if dimm_match:
i2c_addr = int(dimm_match.group(1), 16)
dimm_num = (i2c_addr - 0x50) + 1
return f"DDR5 DIMM {dimm_num}"
return "DDR5 Memory"
# Motherboard/Chipset
if "temp1" in sensor_lower and ("isa" in adapter_lower or "acpi" in adapter_lower):
@@ -2532,16 +2632,103 @@ def identify_temperature_sensor(sensor_name, adapter):
if "sata" in sensor_lower or "ata" in sensor_lower:
return "SATA Drive"
# GPU
if any(gpu in adapter_lower for gpu in ["nouveau", "amdgpu", "radeon", "i915"]):
# GPU - Enhanced detection using both adapter and chip name
if any(gpu_driver in (adapter_lower + " " + chip_lower) for gpu_driver in ["nouveau", "amdgpu", "radeon", "i915"]):
gpu_vendor = None
# Determine GPU vendor from driver
if "nouveau" in adapter_lower or "nouveau" in chip_lower:
gpu_vendor = "NVIDIA"
elif "amdgpu" in adapter_lower or "amdgpu" in chip_lower or "radeon" in adapter_lower or "radeon" in chip_lower:
gpu_vendor = "AMD"
elif "i915" in adapter_lower or "i915" in chip_lower:
gpu_vendor = "Intel"
# Try to get detailed GPU name from lspci if possible
if gpu_vendor:
# Extract PCI address from chip name or adapter
pci_match = re.search(r'pci-([0-9a-f]{4})', adapter_lower + " " + chip_lower)
if pci_match:
pci_code = pci_match.group(1)
pci_address = f"{pci_code[0:2]}:{pci_code[2:4]}.0"
# Try to get detailed GPU name from hardware_monitor
try:
gpu_map = hardware_monitor.get_pci_gpu_map()
if pci_address in gpu_map:
gpu_info = gpu_map[pci_address]
return f"GPU {gpu_info['vendor']} {gpu_info['name']}"
except Exception:
pass
# Fallback: return vendor name only
return f"GPU {gpu_vendor}"
return "GPU"
# Network adapters
# Network adapters and other PCI devices
if "pci" in adapter_lower and "temp" in sensor_lower:
return "PCI Device"
return sensor_name
def identify_fan(sensor_name, adapter, chip_name=None):
"""Identify what a fan sensor corresponds to, using hardware_monitor for GPU detection"""
sensor_lower = sensor_name.lower()
adapter_lower = adapter.lower() if adapter else ""
chip_lower = chip_name.lower() if chip_name else "" # <CHANGE> Add chip name
# GPU fans - Check both adapter and chip name for GPU drivers
if "pci adapter" in adapter_lower or "pci adapter" in chip_lower or any(gpu_driver in adapter_lower + chip_lower for gpu_driver in ["nouveau", "amdgpu", "radeon", "i915"]):
gpu_vendor = None
# Determine GPU vendor from driver
if "nouveau" in adapter_lower or "nouveau" in chip_lower:
gpu_vendor = "NVIDIA"
elif "amdgpu" in adapter_lower or "amdgpu" in chip_lower or "radeon" in adapter_lower or "radeon" in chip_lower:
gpu_vendor = "AMD"
elif "i915" in adapter_lower or "i915" in chip_lower:
gpu_vendor = "Intel"
# Try to get detailed GPU name from lspci if possible
if gpu_vendor:
# Extract PCI address from adapter string
# Example: "nouveau-pci-0200" -> "02:00.0"
pci_match = re.search(r'pci-([0-9a-f]{4})', adapter_lower + " " + chip_lower)
if pci_match:
pci_code = pci_match.group(1)
pci_address = f"{pci_code[0:2]}:{pci_code[2:4]}.0"
# Try to get detailed GPU name from hardware_monitor
try:
gpu_map = hardware_monitor.get_pci_gpu_map()
if pci_address in gpu_map:
gpu_info = gpu_map[pci_address]
return f"GPU {gpu_info['vendor']} {gpu_info['name']}"
except Exception:
pass
# Fallback: return vendor name only
return f"GPU {gpu_vendor}"
# Ultimate fallback if vendor detection fails
return "GPU"
# CPU/System fans - keep original name
if any(cpu_fan in sensor_lower for cpu_fan in ["cpu_fan", "cpufan", "sys_fan", "sysfan"]):
return sensor_name
# Chassis fans - keep original name
if "chassis" in sensor_lower or "case" in sensor_lower:
return sensor_name
# Default: return original name
return sensor_name
def get_temperature_info():
"""Get detailed temperature information from sensors command"""
temperatures = []
@@ -2551,6 +2738,7 @@ def get_temperature_info():
result = subprocess.run(['sensors'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
current_adapter = None
current_chip = None
current_sensor = None
for line in result.stdout.split('\n'):
@@ -2558,11 +2746,16 @@ def get_temperature_info():
if not line:
continue
# Detect chip name (e.g., "nouveau-pci-0200")
if not ':' in line and not line.startswith(' ') and not line.startswith('Adapter'):
current_chip = line
continue
# Detect adapter line
if line.startswith('Adapter:'):
current_adapter = line.replace('Adapter:', '').strip()
continue
# Detect sensor name (lines without ':' at the start are sensor names)
if ':' in line and not line.startswith(' '):
parts = line.split(':', 1)
@@ -2599,8 +2792,14 @@ def get_temperature_info():
high_value = float(high_match.group(1)) if high_match else 0
crit_value = float(crit_match.group(1)) if crit_match else 0
# Skip internal NVMe sensors (only keep Composite)
if current_chip and 'nvme' in current_chip.lower():
sensor_lower_check = sensor_name.lower()
# Skip "Sensor 1", "Sensor 2", "Sensor 8", etc. (keep only "Composite")
if sensor_lower_check.startswith('sensor') and sensor_lower_check.replace('sensor', '').strip().split()[0].isdigit():
continue
identified_name = identify_temperature_sensor(sensor_name, current_adapter)
identified_name = identify_temperature_sensor(sensor_name, current_adapter, current_chip)
temperatures.append({
'name': identified_name,
@@ -2625,7 +2824,30 @@ def get_temperature_info():
except Exception as e:
# print(f"[v0] Error getting temperature info: {e}")
pass
if power_meter is None:
try:
rapl_power = hardware_monitor.get_power_info()
if rapl_power:
power_meter = rapl_power
# print(f"[v0] Power meter from RAPL: {power_meter.get('watts', 0)}W")
pass
except Exception as e:
# print(f"[v0] Error getting RAPL power info: {e}")
pass
try:
hba_temps = hardware_monitor.get_hba_temperatures()
for hba_temp in hba_temps:
temperatures.append({
'name': hba_temp['name'],
'value': hba_temp['temperature'],
'adapter': hba_temp['adapter']
})
except Exception:
pass
return {
'temperatures': temperatures,
'power_meter': power_meter
@@ -4401,6 +4623,7 @@ def get_hardware_info():
result = subprocess.run(['sensors'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
current_adapter = None
current_chip = None # <CHANGE> Add chip name tracking
fans = []
for line in result.stdout.split('\n'):
@@ -4408,6 +4631,12 @@ def get_hardware_info():
if not line:
continue
# <CHANGE> Detect chip name (e.g., "nouveau-pci-0200")
# Chip names don't have ":" and are not indented
if not ':' in line and not line.startswith(' ') and not line.startswith('Adapter'):
current_chip = line
continue
# Detect adapter line
if line.startswith('Adapter:'):
current_adapter = line.replace('Adapter:', '').strip()
@@ -4425,9 +4654,7 @@ def get_hardware_info():
if rpm_match:
fan_speed = int(float(rpm_match.group(1)))
# Placeholder for identify_fan - needs implementation
# identified_name = identify_fan(sensor_name, current_adapter)
identified_name = sensor_name # Use original name for now
identified_name = identify_fan(sensor_name, current_adapter, current_chip)
fans.append({
'name': identified_name,
@@ -4564,6 +4791,7 @@ def api_system():
'uptime': uptime,
'load_average': list(load_avg),
'hostname': socket.gethostname(),
'proxmox_node': get_proxmox_node_name(),
'node_id': socket.gethostname(),
'timestamp': datetime.now().isoformat(),
'cpu_cores': cpu_cores,
@@ -4691,7 +4919,8 @@ def api_network_interface_metrics(interface_name):
return jsonify({'error': f'Invalid timeframe. Must be one of: {", ".join(valid_timeframes)}'}), 400
# Get local node name
local_node = socket.gethostname()
# local_node = socket.gethostname()
local_node = get_proxmox_node_name()
# Determine interface type and get appropriate RRD data
@@ -4780,7 +5009,8 @@ def api_vm_metrics(vmid):
return jsonify({'error': f'Invalid timeframe. Must be one of: {", ".join(valid_timeframes)}'}), 400
# Get local node name
local_node = socket.gethostname()
# local_node = socket.gethostname()
local_node = get_proxmox_node_name()
# First, determine if it's a qemu VM or lxc container
@@ -4847,10 +5077,26 @@ def api_node_metrics():
return jsonify({'error': f'Invalid timeframe. Must be one of: {", ".join(valid_timeframes)}'}), 400
# Get local node name
local_node = socket.gethostname()
# local_node = socket.gethostname()
local_node = get_proxmox_node_name()
# print(f"[v0] Local node: {local_node}")
pass
zfs_arc_size = 0
try:
with open('/proc/spl/kstat/zfs/arcstats', 'r') as f:
for line in f:
if line.startswith('size'):
parts = line.split()
if len(parts) >= 3:
zfs_arc_size = int(parts[2])
break
except (FileNotFoundError, PermissionError, ValueError):
# ZFS not available or no access
pass
# Get RRD data for the node
rrd_result = subprocess.run(['pvesh', 'get', f'/nodes/{local_node}/rrddata',
@@ -4858,16 +5104,20 @@ def api_node_metrics():
capture_output=True, text=True, timeout=10)
if rrd_result.returncode == 0:
rrd_data = json.loads(rrd_result.stdout)
if zfs_arc_size > 0:
for item in rrd_data:
# If zfsarc field is missing or 0, add current value
if 'zfsarc' not in item or item.get('zfsarc', 0) == 0:
item['zfsarc'] = zfs_arc_size
return jsonify({
'node': local_node,
'timeframe': timeframe,
'data': rrd_data
})
else:
return jsonify({'error': f'Failed to get RRD data: {rrd_result.stderr}'}), 500
except Exception as e:
@@ -5455,7 +5705,7 @@ def api_health():
return jsonify({
'status': 'healthy',
'timestamp': datetime.now().isoformat(),
'version': '1.0.1'
'version': '1.0.2'
})
@app.route('/api/prometheus', methods=['GET'])
@@ -5721,7 +5971,7 @@ def api_info():
"""Root endpoint with API information"""
return jsonify({
'name': 'ProxMenux Monitor API',
'version': '1.0.1',
'version': '1.0.2',
'endpoints': [
'/api/system',
'/api/system-info',
@@ -5850,7 +6100,8 @@ def get_vm_config(vmid):
"""Get detailed configuration for a specific VM/LXC"""
try:
# Get VM/LXC configuration
node = socket.gethostname() # Get node name
# node = socket.gethostname() # Get node name
node = get_proxmox_node_name()
result = subprocess.run(
['pvesh', 'get', f'/nodes/{node}/qemu/{vmid}/config', '--output-format', 'json'],
@@ -6093,6 +6344,87 @@ def api_vm_config_update(vmid):
pass
return jsonify({'error': str(e)}), 500
@app.route('/api/scripts/execute', methods=['POST'])
def execute_script():
"""Execute a script with real-time logging"""
try:
data = request.json
script_name = data.get('script_name')
script_params = data.get('params', {})
script_relative_path = data.get('script_relative_path')
if not script_relative_path:
return jsonify({'error': 'script_relative_path is required'}), 400
BASE_SCRIPTS_DIR = '/usr/local/share/proxmenux/scripts'
script_path = os.path.join(BASE_SCRIPTS_DIR, script_relative_path)
script_path = os.path.abspath(script_path)
if not script_path.startswith(BASE_SCRIPTS_DIR):
return jsonify({'error': 'Invalid script path'}), 403
if not os.path.exists(script_path):
return jsonify({'success': False, 'error': 'Script file not found'}), 404
# Create session and start execution in background thread
session_id = script_runner.create_session(script_name)
def run_script():
script_runner.execute_script(script_path, session_id, script_params)
thread = threading.Thread(target=run_script, daemon=True)
thread.start()
return jsonify({
'success': True,
'session_id': session_id
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/scripts/status/<session_id>', methods=['GET'])
def get_script_status(session_id):
"""Get status of a running script"""
try:
status = script_runner.get_session_status(session_id)
return jsonify(status)
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/scripts/respond', methods=['POST'])
def respond_to_script():
"""Respond to script interaction"""
try:
data = request.json
session_id = data.get('session_id')
interaction_id = data.get('interaction_id')
value = data.get('value')
result = script_runner.respond_to_interaction(session_id, interaction_id, value)
return jsonify(result)
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/scripts/logs/<session_id>', methods=['GET'])
def stream_script_logs(session_id):
"""Stream logs from a running script"""
try:
def generate():
for log_entry in script_runner.stream_logs(session_id):
yield f"data: {log_entry}\n\n"
return Response(generate(), mimetype='text/event-stream')
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
if __name__ == '__main__':
# API endpoints available at: /api/system, /api/system-info, /api/storage, /api/proxmox-storage, /api/network, /api/vms, /api/logs, /api/health, /api/hardware, /api/prometheus, /api/node/metrics
+465
View File
@@ -0,0 +1,465 @@
#!/usr/bin/env python3
"""
ProxMenux Terminal WebSocket Routes
Provides a WebSocket endpoint for interactive terminal sessions
"""
from flask import Blueprint, jsonify, request
from flask_sock import Sock
import subprocess
import os
import pty
import select
import struct
import fcntl
import termios
import threading
import time
import requests
import json
import tempfile
import base64
terminal_bp = Blueprint('terminal', __name__)
sock = Sock()
# Active terminal sessions
active_sessions = {}
@terminal_bp.route('/api/terminal/health', methods=['GET'])
def terminal_health():
"""Health check for terminal service"""
return {'success': True, 'active_sessions': len(active_sessions)}
@terminal_bp.route('/api/terminal/search-command', methods=['GET'])
def search_command():
"""Proxy endpoint for cheat.sh API to avoid CORS issues"""
query = request.args.get('q', '')
if not query or len(query) < 2:
return jsonify({'error': 'Query too short'}), 400
try:
url = f'https://cht.sh/{query.replace(" ", "+")}?QT'
headers = {
'User-Agent': 'curl/7.68.0'
}
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
content = response.text
examples = []
current_description = []
for line in content.split('\n'):
stripped = line.strip()
# Ignorar líneas vacías
if not stripped:
continue
# Si es un comentario
if stripped.startswith('#'):
# Acumular descripciones
current_description.append(stripped[1:].strip())
# Si no es comentario, es un comando
elif stripped and not stripped.startswith('http'):
# Unir las descripciones acumuladas
description = ' '.join(current_description) if current_description else ''
examples.append({
'description': description,
'command': stripped
})
# Resetear descripciones para el siguiente comando
current_description = []
return jsonify({
'success': True,
'examples': examples
})
else:
return jsonify({
'success': False,
'error': f'API returned status {response.status_code}'
}), response.status_code
except requests.Timeout:
return jsonify({
'success': False,
'error': 'Request timeout'
}), 504
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
def set_winsize(fd, rows, cols):
"""Set terminal window size"""
try:
winsize = struct.pack('HHHH', rows, cols, 0, 0)
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
except Exception as e:
print(f"Error setting window size: {e}")
def read_and_forward_output(master_fd, ws):
"""Read from PTY and send to WebSocket"""
while True:
try:
# Use select with timeout to check if data is available
r, _, _ = select.select([master_fd], [], [], 0.01)
if master_fd in r:
try:
data = os.read(master_fd, 4096)
if data:
ws.send(data.decode('utf-8', errors='ignore'))
else:
break
except OSError:
break
except Exception as e:
print(f"Error reading from PTY: {e}")
break
@sock.route('/ws/terminal')
def terminal_websocket(ws):
"""WebSocket endpoint for terminal sessions"""
# Create pseudo-terminal
master_fd, slave_fd = pty.openpty()
# Start bash process
shell_process = subprocess.Popen(
['/bin/bash', '-i'],
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
preexec_fn=os.setsid,
cwd='/',
env=dict(os.environ, TERM='xterm-256color', PS1='\\u@\\h:\\w\\$ ')
)
session_id = id(ws)
active_sessions[session_id] = {
'process': shell_process,
'master_fd': master_fd
}
# Set non-blocking mode for master_fd
flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
# Set initial terminal size
set_winsize(master_fd, 30, 120)
# Start thread to read PTY output and forward to WebSocket
output_thread = threading.Thread(
target=read_and_forward_output,
args=(master_fd, ws),
daemon=True
)
output_thread.start()
try:
while True:
# Receive data from WebSocket (blocking)
data = ws.receive(timeout=None)
if data is None:
# Client closed connection
break
handled = False
# Try to handle JSON control messages (e.g. resize)
if isinstance(data, str):
try:
msg = json.loads(data)
except Exception:
msg = None
if isinstance(msg, dict) and msg.get('type') == 'resize':
cols = int(msg.get('cols', 120))
rows = int(msg.get('rows', 30))
set_winsize(master_fd, rows, cols)
handled = True
if handled:
# Control message processed, do not send to bash
continue
# Optional: legacy resize escape sequence support
if isinstance(data, str) and data.startswith('\x1b[8;'):
try:
parts = data[4:-1].split(';')
rows, cols = int(parts[0]), int(parts[1])
set_winsize(master_fd, rows, cols)
continue
except Exception:
pass
# Send input to bash
try:
os.write(master_fd, data.encode('utf-8'))
except OSError as e:
print(f"Error writing to PTY: {e}")
break
# Check if process is still alive
if shell_process.poll() is not None:
break
except Exception as e:
print(f"Terminal session error: {e}")
finally:
# Cleanup
try:
shell_process.terminate()
shell_process.wait(timeout=1)
except:
try:
shell_process.kill()
except:
pass
try:
os.close(master_fd)
except:
pass
try:
os.close(slave_fd)
except:
pass
if session_id in active_sessions:
del active_sessions[session_id]
@sock.route('/ws/script/<session_id>')
def script_websocket(ws, session_id):
"""WebSocket endpoint for executing scripts with hybrid web mode"""
try:
init_data = ws.receive(timeout=10)
if not init_data:
error_msg = '{"type": "error", "message": "No script data received"}\r\n'
ws.send(error_msg)
return
script_data = json.loads(init_data)
script_path = script_data.get('script_path')
params = script_data.get('params', {})
if not script_path:
error_msg = '{"type": "error", "message": "No script_path provided"}\r\n'
ws.send(error_msg)
return
if not os.path.exists(script_path):
error_msg = f'{{"type": "error", "message": "Script not found: {script_path}"}}\r\n'
ws.send(error_msg)
return
except Exception as e:
error_msg = f'{{"type": "error", "message": "Invalid init data: {str(e)}"}}\r\n'
ws.send(error_msg)
return
web_log_fd, web_log_path = tempfile.mkstemp(suffix='.log', prefix='proxmenux_web_')
# Create pseudo-terminal for script execution
master_fd, slave_fd = pty.openpty()
env = os.environ.copy()
env['EXECUTION_MODE'] = 'web'
env['WEB_LOG'] = web_log_path
for key, value in params.items():
env[key] = str(value)
env['PYTHONUNBUFFERED'] = '1'
env['TERM'] = 'xterm-256color'
script_process = subprocess.Popen(
['/bin/bash', script_path],
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
preexec_fn=os.setsid,
env=env
)
# Set non-blocking mode for master_fd
flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
# Set terminal size
set_winsize(master_fd, 30, 120)
def monitor_web_log():
last_position = 0
while script_process.poll() is None:
try:
if os.path.exists(web_log_path):
with open(web_log_path, 'r') as f:
f.seek(last_position)
new_lines = f.readlines()
last_position = f.tell()
for line in new_lines:
line = line.strip()
if line.startswith('WEB_INTERACTION:'):
try:
# Parse: WEB_INTERACTION:type:id:title_b64:message_b64[:options_json]
parts = line[16:].split(':', 4)
interaction_type = parts[0]
interaction_id = parts[1]
title_b64 = parts[2]
message_b64 = parts[3]
title = base64.b64decode(title_b64).decode('utf-8')
message = base64.b64decode(message_b64).decode('utf-8')
interaction_data = {
'type': 'web_interaction',
'interaction': {
'type': interaction_type,
'id': interaction_id,
'title': title,
'message': message
}
}
# Parse options for menu
if interaction_type == 'menu' and len(parts) > 4:
options_json = parts[4]
interaction_data['interaction']['options'] = json.loads(options_json)
# Parse default for inputbox
if interaction_type == 'inputbox' and len(parts) > 4:
default_b64 = parts[4]
interaction_data['interaction']['default'] = base64.b64decode(default_b64).decode('utf-8')
# Send interaction to WebSocket
ws.send(json.dumps(interaction_data))
except Exception as e:
pass
time.sleep(0.01)
except Exception as e:
break
web_log_thread = threading.Thread(target=monitor_web_log, daemon=True)
web_log_thread.start()
# Thread to read script output and forward to WebSocket
def read_script_output():
while True:
try:
r, _, _ = select.select([master_fd], [], [], 0.01)
if master_fd in r:
try:
data = os.read(master_fd, 4096)
if not data:
break
text = data.decode('utf-8', errors='ignore')
# Send raw text to terminal
try:
ws.send(text)
except Exception as e:
break
except OSError as e:
break
except Exception as e:
break
script_process.wait()
exit_code = script_process.returncode if script_process.returncode is not None else 0
try:
ws.send(f'\r\n[Script exited with code {exit_code}]\r\n')
except Exception as e:
pass
output_thread = threading.Thread(target=read_script_output, daemon=True)
output_thread.start()
try:
while True:
data = ws.receive(timeout=None)
if data is None:
break
try:
msg = json.loads(data)
if msg.get('type') == 'interaction_response':
interaction_id = msg.get('id')
value = msg.get('value')
# Write response to the file the script is waiting for
response_file = f"/tmp/proxmenux_response_{interaction_id}"
with open(response_file, 'w') as f:
f.write(value)
continue
# Handle resize
if msg.get('type') == 'resize':
cols = int(msg.get('cols', 120))
rows = int(msg.get('rows', 30))
set_winsize(master_fd, rows, cols)
continue
except json.JSONDecodeError:
# Raw text input, send to script
try:
os.write(master_fd, data.encode('utf-8'))
except OSError as e:
break
if script_process.poll() is not None:
break
except Exception as e:
pass
finally:
try:
script_process.terminate()
script_process.wait(timeout=1)
except:
try:
script_process.kill()
except:
pass
try:
os.close(master_fd)
except:
pass
try:
os.close(slave_fd)
except:
pass
try:
os.close(web_log_fd)
os.unlink(web_log_path)
except:
pass
def init_terminal_routes(app):
"""Initialize terminal routes with Flask app"""
sock.init_app(app)
app.register_blueprint(terminal_bp)
+390 -346
View File
@@ -1,369 +1,413 @@
#!/usr/bin/env python3
import json
"""
Hardware Monitor - RAPL Power Monitoring and GPU Identification
This module provides:
1. CPU power consumption monitoring using Intel RAPL (Running Average Power Limit)
2. PCI GPU identification for better fan labeling
3. HBA controller detection and temperature monitoring
Only contains these specialized functions - all other hardware monitoring
is handled by flask_server.py to avoid code duplication.
"""
import os
import time
import subprocess
import re
import os
from typing import Dict, List, Any, Optional
from typing import Dict, Any, Optional
def run_command(cmd: List[str]) -> str:
"""Run a command and return its output."""
# Global variable to store previous energy reading for power calculation
_last_energy_reading = {'energy_uj': None, 'timestamp': None}
def get_pci_gpu_map() -> Dict[str, Dict[str, str]]:
"""
Get a mapping of PCI addresses to GPU names from lspci.
This function parses lspci output to identify GPU models by their PCI addresses,
which allows us to provide meaningful names for GPU fans in sensors output.
Returns:
dict: Mapping of PCI addresses (e.g., '02:00.0') to GPU info
Example: {
'02:00.0': {
'vendor': 'NVIDIA',
'name': 'GeForce GTX 1080',
'full_name': 'NVIDIA Corporation GP104 [GeForce GTX 1080]'
}
}
"""
gpu_map = {}
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
return result.stdout
# Run lspci to get VGA/3D/Display controllers
result = subprocess.run(
['lspci', '-nn'],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
for line in result.stdout.split('\n'):
if 'VGA compatible controller' in line or '3D controller' in line or 'Display controller' in line:
# Example line: "02:00.0 VGA compatible controller [0300]: NVIDIA Corporation GP104 [GeForce GTX 1080] [10de:1b80]"
match = re.match(r'^([0-9a-f]{2}:[0-9a-f]{2}\.[0-9a-f])\s+.*:\s+(.+?)\s+\[([0-9a-f]{4}):([0-9a-f]{4})\]', line)
if match:
pci_address = match.group(1)
device_name = match.group(2).strip()
# Extract vendor
vendor = None
if 'NVIDIA' in device_name.upper() or 'GEFORCE' in device_name.upper() or 'QUADRO' in device_name.upper():
vendor = 'NVIDIA'
elif 'AMD' in device_name.upper() or 'RADEON' in device_name.upper():
vendor = 'AMD'
elif 'INTEL' in device_name.upper() or 'ARC' in device_name.upper():
vendor = 'Intel'
# Extract model name (text between brackets is usually the commercial name)
bracket_match = re.search(r'\[([^\]]+)\]', device_name)
if bracket_match:
model_name = bracket_match.group(1)
else:
# Fallback: use everything after the vendor name
if vendor:
model_name = device_name.split(vendor)[-1].strip()
else:
model_name = device_name
gpu_map[pci_address] = {
'vendor': vendor if vendor else 'Unknown',
'name': model_name,
'full_name': device_name
}
except Exception:
return ""
pass
return gpu_map
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)
"""
Get CPU power consumption using Intel RAPL interface.
This function measures power consumption by reading energy counters
from /sys/class/powercap/intel-rapl interfaces and calculating
the power draw based on the change in energy over time.
Used as fallback when IPMI power monitoring is not available.
Returns:
dict: Power meter information with 'name', 'watts', and 'adapter' keys
or None if RAPL interface is unavailable
Example:
{
'name': 'CPU Power',
'watts': 45.32,
'adapter': 'Intel RAPL (CPU only)'
}
"""
global _last_energy_reading
rapl_path = '/sys/class/powercap/intel-rapl/intel-rapl:0/energy_uj'
if os.path.exists(rapl_path):
try:
# Read current energy value in microjoules
with open(rapl_path, 'r') as f:
energy_uj = int(f.read().strip())
current_energy_uj = int(f.read().strip())
current_time = time.time()
watts = 0.0
# Calculate power if we have a previous reading
if _last_energy_reading['energy_uj'] is not None and _last_energy_reading['timestamp'] is not None:
time_diff = current_time - _last_energy_reading['timestamp']
if time_diff > 0:
energy_diff = current_energy_uj - _last_energy_reading['energy_uj']
# Handle counter overflow (wraps around at max value)
if energy_diff < 0:
energy_diff = current_energy_uj
# Power (W) = Energy (µJ) / time (s) / 1,000,000
watts = round((energy_diff / time_diff) / 1000000, 2)
# Store current reading for next calculation
_last_energy_reading['energy_uj'] = current_energy_uj
_last_energy_reading['timestamp'] = current_time
# Detect CPU vendor for display purposes
cpu_vendor = 'CPU'
try:
with open('/proc/cpuinfo', 'r') as f:
cpuinfo = f.read()
if 'GenuineIntel' in cpuinfo:
cpu_vendor = 'Intel'
elif 'AuthenticAMD' in cpuinfo:
cpu_vendor = 'AMD'
except:
pass
# 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'
'name': 'CPU Power',
'watts': watts,
'adapter': f'{cpu_vendor} RAPL (CPU only)'
}
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()
def get_hba_info() -> list[Dict[str, Any]]:
"""
Detect HBA/RAID controllers from lspci.
This function identifies LSI/Broadcom, Adaptec, and other RAID/HBA controllers
present in the system via lspci output.
Returns:
list: List of HBA controller dictionaries
Example: [
{
'pci_address': '01:00.0',
'vendor': 'LSI/Broadcom',
'model': 'SAS3008 PCI-Express Fusion-MPT SAS-3',
'controller_id': 0
}
]
"""
hba_list = []
try:
# Run lspci to find RAID/SAS controllers
result = subprocess.run(
['lspci', '-nn'],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
controller_id = 0
for line in result.stdout.split('\n'):
# Look for RAID bus controller, SCSI storage controller, Serial Attached SCSI controller
if any(keyword in line for keyword in ['RAID bus controller', 'SCSI storage controller', 'Serial Attached SCSI']):
# Example: "01:00.0 RAID bus controller [0104]: Broadcom / LSI SAS3008 PCI-Express Fusion-MPT SAS-3 [1000:0097]"
match = re.match(r'^([0-9a-f]{2}:[0-9a-f]{2}\.[0-9a-f])\s+.*:\s+(.+?)\s+\[([0-9a-f]{4}):([0-9a-f]{4})\]', line)
if match:
pci_address = match.group(1)
device_name = match.group(2).strip()
# Extract vendor
vendor = 'Unknown'
if 'LSI' in device_name.upper() or 'BROADCOM' in device_name.upper() or 'AVAGO' in device_name.upper():
vendor = 'LSI/Broadcom'
elif 'ADAPTEC' in device_name.upper():
vendor = 'Adaptec'
elif 'ARECA' in device_name.upper():
vendor = 'Areca'
elif 'HIGHPOINT' in device_name.upper():
vendor = 'HighPoint'
elif 'DELL' in device_name.upper():
vendor = 'Dell'
elif 'HP' in device_name.upper() or 'HEWLETT' in device_name.upper():
vendor = 'HP'
# Extract model name
model_name = device_name
# Remove vendor prefix if present
for v in ['Broadcom / LSI', 'Broadcom', 'LSI Logic', 'LSI', 'Adaptec', 'Areca', 'HighPoint', 'Dell', 'HP', 'Hewlett-Packard']:
if model_name.startswith(v):
model_name = model_name[len(v):].strip()
hba_list.append({
'pci_address': pci_address,
'vendor': vendor,
'model': model_name,
'controller_id': controller_id,
'full_name': device_name
})
controller_id += 1
except Exception:
pass
return hba_list
def get_hba_temperatures() -> list[Dict[str, Any]]:
"""
Get HBA controller temperatures using storcli64 or megacli.
This function attempts to read temperature data from LSI/Broadcom RAID controllers
using the storcli64 tool (preferred) or megacli as fallback.
Returns:
list: List of temperature dictionaries
Example: [
{
'name': 'HBA Controller 0',
'temperature': 65,
'adapter': 'LSI/Broadcom SAS3008'
}
]
"""
temperatures = []
# Check which tool is available
storcli_paths = [
'/opt/MegaRAID/storcli/storcli64',
'/usr/sbin/storcli64',
'/usr/local/sbin/storcli64',
'storcli64'
]
megacli_paths = [
'/opt/MegaRAID/MegaCli/MegaCli64',
'/usr/sbin/megacli',
'/usr/local/sbin/megacli',
'megacli'
]
storcli_path = None
megacli_path = None
# Find storcli64
for path in storcli_paths:
try:
result = subprocess.run([path, '-v'], capture_output=True, timeout=2)
if result.returncode == 0:
storcli_path = path
break
except:
continue
# Try storcli64 first (preferred)
if storcli_path:
try:
# Get list of controllers
result = subprocess.run(
[storcli_path, 'show'],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
# Parse controller IDs
controller_ids = []
for line in result.stdout.split('\n'):
match = re.search(r'^\s*(\d+)\s+', line)
if match and 'Ctl' in line:
controller_ids.append(match.group(1))
# Get temperature for each controller
for ctrl_id in controller_ids:
try:
temp_result = subprocess.run(
[storcli_path, f'/c{ctrl_id}', 'show', 'temperature'],
capture_output=True,
text=True,
timeout=10
)
if temp_result.returncode == 0:
# Parse temperature from output
for line in temp_result.stdout.split('\n'):
if 'ROC temperature' in line or 'Controller Temp' in line:
temp_match = re.search(r'(\d+)\s*C', line)
if temp_match:
temp_c = int(temp_match.group(1))
# Get HBA info for better naming
hba_list = get_hba_info()
adapter_name = 'LSI/Broadcom Controller'
if int(ctrl_id) < len(hba_list):
hba = hba_list[int(ctrl_id)]
adapter_name = f"{hba['vendor']} {hba['model']}"
temperatures.append({
'name': f'HBA Controller {ctrl_id}',
'temperature': temp_c,
'adapter': adapter_name
})
break
except:
continue
except:
pass
# Fallback to megacli if storcli not available
elif not temperatures:
for path in megacli_paths:
try:
result = subprocess.run([path, '-v'], capture_output=True, timeout=2)
if result.returncode == 0:
megacli_path = path
break
except:
continue
if megacli_path:
try:
# Get adapter count
result = subprocess.run(
[megacli_path, '-adpCount'],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
# Parse adapter count
adapter_count = 0
for line in result.stdout.split('\n'):
if 'Controller Count' in line:
count_match = re.search(r'(\d+)', line)
if count_match:
adapter_count = int(count_match.group(1))
break
# Get temperature for each adapter
for adapter_id in range(adapter_count):
try:
temp_result = subprocess.run(
[megacli_path, '-AdpAllInfo', f'-a{adapter_id}'],
capture_output=True,
text=True,
timeout=10
)
if temp_result.returncode == 0:
# Parse temperature
for line in temp_result.stdout.split('\n'):
if 'ROC temperature' in line or 'Controller Temp' in line:
temp_match = re.search(r'(\d+)\s*C', line)
if temp_match:
temp_c = int(temp_match.group(1))
# Get HBA info for better naming
hba_list = get_hba_info()
adapter_name = 'LSI/Broadcom Controller'
if adapter_id < len(hba_list):
hba = hba_list[adapter_id]
adapter_name = f"{hba['vendor']} {hba['model']}"
temperatures.append({
'name': f'HBA Controller {adapter_id}',
'temperature': temp_c,
'adapter': adapter_name
})
break
except:
continue
except:
pass
return temperatures
File diff suppressed because it is too large Load Diff
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ProxMenux - Proxmox Storage Monitor
Monitors configured Proxmox storages and tracks unavailable storages
"""
import json
import subprocess
import socket
from typing import Dict, List, Any, Optional
class ProxmoxStorageMonitor:
"""Monitor Proxmox storage configuration and status"""
def __init__(self):
self.configured_storages: Dict[str, Dict[str, Any]] = {}
self._load_configured_storages()
def _get_node_name(self) -> str:
"""Get current Proxmox node name"""
try:
result = subprocess.run(
['pvesh', 'get', '/nodes', '--output-format', 'json'],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
nodes = json.loads(result.stdout)
hostname = socket.gethostname()
for node in nodes:
if node.get('node') == hostname:
return hostname
if nodes:
return nodes[0].get('node', hostname)
return socket.gethostname()
except Exception:
return socket.gethostname()
def _load_configured_storages(self) -> None:
"""Load configured storages from Proxmox configuration"""
try:
local_node = self._get_node_name()
# Read storage configuration from pvesh
result = subprocess.run(
['pvesh', 'get', '/storage', '--output-format', 'json'],
capture_output=True,
text=True,
timeout=5
)
if result.returncode != 0:
return
storages = json.loads(result.stdout)
for storage in storages:
storage_id = storage.get('storage')
if not storage_id:
continue
# Check if storage is enabled for this node
nodes = storage.get('nodes')
if nodes and local_node not in nodes.split(','):
continue
disabled = storage.get('disable', 0)
if disabled == 1:
continue
self.configured_storages[storage_id] = {
'name': storage_id,
'type': storage.get('type', 'unknown'),
'content': storage.get('content', ''),
'path': storage.get('path', ''),
'enabled': True
}
except Exception:
pass
def get_storage_status(self) -> Dict[str, List[Dict[str, Any]]]:
"""
Get storage status, including unavailable storages
Returns:
{
'available': [...],
'unavailable': [...]
}
"""
try:
local_node = self._get_node_name()
# Get current storage status from pvesh
result = subprocess.run(
['pvesh', 'get', '/cluster/resources', '--type', 'storage', '--output-format', 'json'],
capture_output=True,
text=True,
timeout=10
)
if result.returncode != 0:
return {'available': [], 'unavailable': list(self.configured_storages.values())}
resources = json.loads(result.stdout)
# Track which configured storages are available
available_storages = []
unavailable_storages = []
seen_storage_names = set()
for resource in resources:
node = resource.get('node', '')
# Filter only local node storages
if node != local_node:
continue
name = resource.get('storage', 'unknown')
seen_storage_names.add(name)
storage_type = resource.get('plugintype', 'unknown')
status = resource.get('status', 'unknown')
try:
total = int(resource.get('maxdisk', 0))
used = int(resource.get('disk', 0))
available = total - used if total > 0 else 0
except (ValueError, TypeError):
total = 0
used = 0
available = 0
# Calculate percentage
percent = (used / total * 100) if total > 0 else 0.0
# Convert bytes to GB
total_gb = round(total / (1024**3), 2)
used_gb = round(used / (1024**3), 2)
available_gb = round(available / (1024**3), 2)
storage_info = {
'name': name,
'type': storage_type,
'total': total_gb,
'used': used_gb,
'available': available_gb,
'percent': round(percent, 2),
'node': node
}
# Check if storage is available
if total == 0 or status.lower() != "available":
storage_info['status'] = 'error'
storage_info['status_detail'] = 'unavailable' if total == 0 else status
unavailable_storages.append(storage_info)
else:
storage_info['status'] = 'active'
available_storages.append(storage_info)
# Check for configured storages that are completely missing
for storage_name, storage_config in self.configured_storages.items():
if storage_name not in seen_storage_names:
unavailable_storages.append({
'name': storage_name,
'type': storage_config['type'],
'status': 'error',
'status_detail': 'not_found',
'total': 0,
'used': 0,
'available': 0,
'percent': 0,
'node': local_node
})
return {
'available': available_storages,
'unavailable': unavailable_storages
}
except Exception:
return {
'available': [],
'unavailable': list(self.configured_storages.values())
}
def get_unavailable_count(self) -> int:
"""Get count of unavailable storages"""
status = self.get_storage_status()
return len(status['unavailable'])
def reload_configuration(self) -> None:
"""Reload storage configuration from Proxmox"""
self.configured_storages.clear()
self._load_configured_storages()
# Global instance
proxmox_storage_monitor = ProxmoxStorageMonitor()
+85
View File
@@ -1,3 +1,88 @@
## 2026-03-14
### New version v1.1.9 — *Helper Scripts Catalog Rebuilt*
### Changed
- **Helper Scripts Menu — Full Catalog Rebuild**
The Helper Scripts catalog has been completely rebuilt to adapt to the new data architecture of the [Community Scripts](https://community-scripts.github.io/ProxmoxVE/) project.
The previous implementation relied on a `metadata.json` file that no longer exists in the upstream repository. The catalog now connects directly to the **PocketBase API** (`db.community-scripts.org`), which is the new official data source for the project.
A new GitHub Actions workflow generates a local `helpers_cache.json` index that replaces the old metadata dependency. This new cache is richer, more structured, and includes:
- Script type, slug, description, notes, and default credentials
- OS variants per script (e.g. Debian, Alpine) — each shown as a separate selectable option in the menu
- Direct GitHub URL and **Mirror URL** (`git.community-scripts.org`) for every script
- Category names embedded directly in the cache — no external requests needed to build the menu
- Additional metadata: default port, website, logo, update support, ARM availability
Scripts that support multiple OS variants (e.g. Docker with Alpine and Debian) now correctly show **one entry per OS**, each with its own GitHub and Mirror download option — restoring the behavior that existed before the upstream migration.
---
### 🎖 Special Acknowledgment
This update would not have been possible without the openness and collaboration of the **Community Scripts** maintainers.
When the upstream metadata structure changed and broke the ProxMenux catalog, the maintainers responded quickly, explained the new architecture in detail, and provided all the information needed to rebuild the integration cleanly.
Special thanks to:
- **MickLeskCanbiZ ([@MickLesk](https://github.com/MickLesk))** — for documenting the new script path structure by type and slug, and for the clear and direct technical guidance.
- **Michel Roegl-Brunner ([@michelroegl-brunner](https://github.com/michelroegl-brunner))** — for explaining the new PocketBase collections structure (`script_scripts`, `script_categories`).
The Helper Scripts project is an extraordinary resource for the Proxmox community. The scripts belong entirely to their authors and maintainers — ProxMenux simply offers a guided way to discover and launch them. All credit goes to the community behind [community-scripts/ProxmoxVE](https://github.com/community-scripts/ProxmoxVE).
## 2025-09-18
### New version v1.1.8 — *ProxMenux Offline Mode*
![ProxMenux Offline](https://macrimi.github.io/ProxMenux/ProxMenux_offline.png)
---
### Added
- **Offline Execution Mode (no GitHub dependency)**
All ProxMenux core scripts now run **entirely locally**, without requiring live requests to GitHub (`raw.githubusercontent.com`).
This change provides:
- Greater stability during execution
- No interruptions due to network timeouts or regional GitHub blocks
- Support for **offline or isolated environments**
⚠️ This update resolves recent issues where users in certain regions were unable to run scripts due to CDN or TLS filtering errors while downloading `.sh` files from GitHub raw URLs.
**🎖 Special Acknowledgment: @cod378**
This offline conversion has been made possible thanks to the extraordinary work of **@cod378**,
who redesigned the entire internal logic of the installer and updater, refactored the file management system,
and implemented the new fully local execution workflow.
Without his collaboration, dedication, and technical contribution, this transformation would not have been possible.
- **ProxMenux Monitor v1.0.1**
This update brings a major leap in the **ProxMenux Monitor** interface.
New features and improvements:
- `Proxy Support`: Access ProxMenux through reverse proxies with full functionality
- `Authentication System`: Secure your dashboard with password protection
- `Two-Factor Authentication (2FA)`: Optional TOTP support for enhanced security
- `PCIe Link Speed Detection`: View NVMe connection speeds and detect performance bottlenecks
- `Enhanced Storage Display`: Auto-formats disk sizes (GB → TB when appropriate)
- `SATA/SAS Interface Info`: Detect and show storage type (SATA, SAS, NVMe, etc.)
- `Health Monitoring System`: Built-in system health check with dismissible alerts
- Improved rendering across browsers and better performance
- **Helper Scripts Menu (Mirror Support)**
The `Helper Scripts` menu now:
- Detects **mirror URLs** and shows alternative download options when available
- Lists available OS versions when a helper script is version-dependent (e.g. template installers)
---
### Fixed
- Minor fixes and refinements throughout the codebase to ensure full offline compatibility and a smoother user experience.
## 2025-09-04
### New version v1.1.7
+32 -29
View File
@@ -1,34 +1,37 @@
ProxMenux An Interactive Menu for Proxmox VE Management
ProxMenux - An Interactive Menu for Proxmox VE Management
Copyright (c) 2025 MacRimi
This project is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License.
See the full license terms below.
======================================================================
LICENSE: GNU General Public License v3.0 (GPL-3.0)
======================================================================
ProxMenux is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
Under this license:
1. Attribution: You must give appropriate credit to the original author (MacRimi).
2. Copyleft: If you remix, transform, or build upon ProxMenux, you must
distribute your contributions under the same GPL-3.0 license.
3. Source Code: Anyone distributing a modified version must make the
source code available.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
======================================================================
Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)
This is a human-readable summary of (and not a substitute for) the license.
You may obtain a copy of the full license at:
https://creativecommons.org/licenses/by-nc/4.0/
You are free to:
- Share — copy and redistribute the material in any medium or format.
- Adapt — remix, transform, and build upon the material.
Under the following terms:
- Attribution — You must give appropriate credit, provide a link to the license,
and indicate if changes were made.
- NonCommercial — You may not use the material for commercial purposes.
No additional restrictions — You may not apply legal terms or technological
measures that legally restrict others from doing anything the license permits.
Disclaimer:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
DISCLAIMER:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING
FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+101 -6
View File
@@ -57,25 +57,120 @@ Then, follow the on-screen options to manage your Proxmox server efficiently.
---
## 📌 System Requirements
🖥 **Compatible with:**
- Proxmox VE 8.x and 9.x
📦 **Dependencies:**
- `bash`, `curl`, `wget`, `jq`, `whiptail`, `python3-venv` (These dependencies are installed automatically during setup.)
- **Translations are handled in a Python virtual environment using `googletrans-env`.**
## 🧪 Beta Program
Want to try the latest features before the official release and help shape the final version?
The **ProxMenux Beta Program** gives early access to new functionality — including the newest builds of ProxMenux Monitor — directly from the `develop` branch. Beta builds may contain bugs or incomplete features. Your feedback is what helps fix them before the stable release.
**Install the beta version:**
```bash
bash -c "$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/develop/install_proxmenux_beta.sh)"
```
**What to expect:**
- You'll get new features and Monitor builds before anyone else
- Some things may not work perfectly — that's expected and normal
- When a stable release is published, ProxMenux will notify you on the next `menu` launch and offer to switch automatically
**How to report issues:**
Open a [GitHub Issue](https://github.com/MacRimi/ProxMenux/issues) and include:
- What you did and what you expected to happen
- Any error messages shown on screen
- Logs from the Monitor if relevant:
```bash
journalctl -u proxmenux-monitor -n 50
```
> 💙 Thank you for being part of the beta program. Your help makes ProxMenux better for everyone.
---
## 🖥️ ProxMenux Monitor
ProxMenux Monitor is an integrated web dashboard that provides real-time visibility into your Proxmox infrastructure — accessible from any browser on your network, without needing a terminal.
**What it offers:**
- Real-time monitoring of CPU, RAM, disk usage and network traffic
- Overview of running VMs and LXC containers with status indicators
- Login authentication to protect access
- Two-Factor Authentication (2FA) with TOTP support
- Reverse proxy support (Nginx / Traefik)
- Designed to work across desktop and mobile devices
**Access:**
Once installed, the dashboard is available at:
```
http://<your-proxmox-ip>:8008
```
The Monitor is installed automatically as part of the standard ProxMenux installation and runs as a systemd service (`proxmenux-monitor.service`) that starts automatically on boot.
**Useful commands:**
```bash
# Check service status
systemctl status proxmenux-monitor
# View logs
journalctl -u proxmenux-monitor -n 50
# Restart the service
systemctl restart proxmenux-monitor
```
---
## 🔧 Dependencies
The following dependencies are installed automatically during setup:
| Package | Purpose |
|---|---|
| `dialog` | Interactive terminal menus |
| `curl` | Downloads and connectivity checks |
| `jq` | JSON processing |
| `git` | Repository cloning and updates |
| `python3` + `python3-venv` | Translation support *(Translation version only)* |
| `googletrans` | Google Translate library *(Translation version only)* |
---
## ⭐ Support the Project!
If you find **ProxMenux** useful, consider giving it a ⭐ on GitHub to help others discover it!
## 🤝 Contributing
Contributions, bug reports and feature suggestions are welcome!
- 🐛 [Report a bug](https://github.com/MacRimi/ProxMenux/issues/new)
- 💡 [Suggest a feature](https://github.com/MacRimi/ProxMenux/discussions)
- 🔀 [Submit a pull request](https://github.com/MacRimi/ProxMenux/pulls)
---
## 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;"/>
+48 -3
View File
@@ -7,7 +7,7 @@
# Contributors : cod378
# Subproject : ProxMenux Monitor (System Health & Web Dashboard)
# Copyright : (c) 2024-2025 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.4
# Last Updated : 12/11/2025
# ==========================================================
@@ -440,7 +440,7 @@ update_config() {
local status="$2"
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local tracked_components=("dialog" "curl" "jq" "python3" "python3-venv" "python3-pip" "virtual_environment" "pip" "googletrans" "proxmenux_monitor")
local tracked_components=("dialog" "curl" "jq" "git" "python3" "python3-venv" "python3-pip" "virtual_environment" "pip" "googletrans" "proxmenux_monitor")
if [[ " ${tracked_components[@]} " =~ " ${component} " ]]; then
mkdir -p "$(dirname "$CONFIG_FILE")"
@@ -727,7 +727,17 @@ install_normal_version() {
update_config "jq" "already_installed"
fi
BASIC_DEPS=("dialog" "curl" "git")
if [ -z "${APT_UPDATED:-}" ]; then
apt-get update -y > /dev/null 2>&1 || true
APT_UPDATED=1
fi
for pkg in "${BASIC_DEPS[@]}"; do
if ! dpkg -l | grep -qw "$pkg"; then
if apt-get install -y "$pkg" > /dev/null 2>&1; then
@@ -741,9 +751,42 @@ install_normal_version() {
update_config "$pkg" "already_installed"
fi
done
if ! command -v git > /dev/null 2>&1; then
msg_info "Installing git (required to clone the ProxMenux repository)."
if [ -z "${APT_UPDATED:-}" ]; then
apt-get update -y > /dev/null 2>&1 || true
APT_UPDATED=1
fi
if ! apt-get install -y git > /dev/null 2>&1; then
msg_error "Failed to install git. Please run 'apt-get install git' manually and rerun the installer."
update_config "git" "failed"
return 1
fi
if ! command -v git > /dev/null 2>&1; then
msg_error "Git is still not available after installation. Aborting to avoid a broken setup."
update_config "git" "failed"
return 1
fi
update_config "git" "installed"
else
update_config "git" "already_installed"
fi
msg_ok "jq, dialog, curl and git installed successfully."
((current_step++))
show_progress $current_step $total_steps "Install ProxMenux repository"
@@ -1037,6 +1080,8 @@ install_proxmenux() {
type_text "To run ProxMenux, simply execute this command in the console or terminal:"
echo -e "${YWB} menu${CL}"
echo
# -------
exit 0
}
if [ "$(id -u)" -ne 0 ]; then
+12961 -1923
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+42 -32
View File
@@ -5,14 +5,13 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# 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 ProxMenux logo on startup.
# - Loads necessary configurations and language settings.
# - Checks for available updates and installs them if confirmed.
@@ -29,10 +28,10 @@
# for managing Proxmox VE using ProxMenux.
# ==========================================================
# Configuration ============================================
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
LOCAL_SCRIPTS="$BASE_DIR/scripts"
CONFIG_FILE="$BASE_DIR/config.json"
CACHE_FILE="$BASE_DIR/cache.json"
UTILS_FILE="$BASE_DIR/utils.sh"
@@ -40,56 +39,67 @@ LOCAL_VERSION_FILE="$BASE_DIR/version.txt"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
: "${LOCAL_SCRIPTS:=/usr/local/share/proxmenux/scripts}"
# =========================================================
# For now, update is not available in the local version.
# Take in mind that in future versions, updates must be
# a warning to update the .deb package
# =========================================================
check_updates() {
local INSTALL_SCRIPT="$BASE_DIR/install_proxmenux.sh"
local VERSION_URL INSTALL_URL INSTALL_SCRIPT
local REMOTE_VERSION LOCAL_VERSION
local REMOTE_VERSION
REMOTE_VERSION=$(curl -fsSL "$REPO_URL/version.txt" | head -n 1)
VERSION_URL="$REPO_URL/version.txt"
INSTALL_URL="$REPO_URL/install_proxmenux.sh"
INSTALL_SCRIPT="$BASE_DIR/install_proxmenux.sh"
if [ -z "$REMOTE_VERSION" ]; then
return 0
fi
local LOCAL_VERSION
LOCAL_VERSION=$(head -n 1 "$LOCAL_VERSION_FILE")
[[ ! -f "$LOCAL_VERSION_FILE" ]] && return 0
[ "$LOCAL_VERSION" = "$REMOTE_VERSION" ] && return 0
REMOTE_VERSION="$(curl -fsSL "$VERSION_URL" 2>/dev/null | head -n 1)"
[[ -z "$REMOTE_VERSION" ]] && return 0
if whiptail --title "$(translate "Update Available")" \
--yesno "$(translate "New version available") ($REMOTE_VERSION)\n\n$(translate "Do you want to update now?")" \
LOCAL_VERSION="$(head -n 1 "$LOCAL_VERSION_FILE" 2>/dev/null)"
[[ -z "$LOCAL_VERSION" ]] && return 0
[[ "$LOCAL_VERSION" = "$REMOTE_VERSION" ]] && return 0
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...")"
if wget -qO "$INSTALL_SCRIPT" "$REPO_URL/install_proxmenux.sh"; then
msg_warn "$(translate 'Starting ProxMenux update...')"
if curl -fsSL "$INSTALL_URL" -o "$INSTALL_SCRIPT"; then
chmod +x "$INSTALL_SCRIPT"
source "$INSTALL_SCRIPT"
bash "$INSTALL_SCRIPT" --update
return 0
fi
else
msg_warn "$(translate "Update postponed. You can update later from the menu.")"
fi
}
main_menu() {
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
}
local MAIN_MENU="$LOCAL_SCRIPTS/menus/main_menu.sh"
exec bash "$MAIN_MENU"
}
load_language
initialize_cache
# Check updates doesn't make sense in offline mode
# check_updates
check_updates
main_menu
+1 -1
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 06/07/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 17/08/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
+1 -1
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 2.0
# Last Updated: 07/01/2025
# ==========================================================
+38 -20
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# ==========================================================
# Proxmox VE Update Script - Improved Version
# Proxmox VE Update Script - Improved Version (with apt progress)
# ==========================================================
# Configuration
@@ -9,6 +9,7 @@ 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"
APT_ENV="env DEBIAN_FRONTEND=noninteractive LC_ALL=C LANG=C"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
@@ -35,11 +36,14 @@ download_common_functions() {
}
update_pve9() {
local pve_version=$(pveversion | awk -F'/' '{print $2}' | cut -d'-' -f1)
local start_time=$(date +%s)
local pve_version
pve_version=$(pveversion | awk -F'/' '{print $2}' | cut -d'-' -f1)
local start_time
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 OS_CODENAME
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"
@@ -55,7 +59,8 @@ update_pve9() {
} | tee -a "$screen_capture"
local available_space=$(df /var/cache/apt/archives | awk 'NR==2 {print int($4/1024)}')
local available_space
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
@@ -162,7 +167,6 @@ EOF
# Handle common apt errors
if echo "$update_output" | grep -Eq "NO_PUBKEY|GPG error"; then
# Extract first missing key (NO_PUBKEY ABCDEF... pattern)
key=$(echo "$update_output" | sed -n 's/.*NO_PUBKEY \([0-9A-F]\{8,40\}\).*/\1/p' | head -1)
@@ -202,17 +206,29 @@ EOF
fi
fi
if apt policy 2>/dev/null | grep -q "${TARGET_CODENAME}.*pve-no-subscription"; then
msg_ok "$(translate "Proxmox VE $pve_version repositories verified")" | tee -a "$screen_capture"
else
msg_warn "$(translate "Proxmox VE $pve_version repositories verification inconclusive, continuing...")"
fi
local current_pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
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")
local current_pve_version
current_pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
local available_pve_version
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
upgradable=$($APT_ENV apt list --upgradable 2>/dev/null \
| sed '1d' \
| sed '/^\s*$/d' \
| wc -l)
local security_updates
security_updates=$($APT_ENV apt list --upgradable 2>/dev/null \
| sed '1d' \
| grep -ci '\-security')
show_update_menu() {
local current_version="$1"
@@ -220,7 +236,8 @@ EOF
local upgradable_count="$3"
local security_count="$4"
local menu_text="$(translate "System Update Information")\n\n"
local menu_text
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"
@@ -250,7 +267,6 @@ EOF
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
@@ -273,20 +289,21 @@ EOF
fi
echo -e
DEBIAN_FRONTEND=noninteractive apt-get -y \
DEBIAN_FRONTEND=noninteractive apt -y \
-o Dpkg::Options::='--force-confdef' \
-o Dpkg::Options::='--force-confold' \
dist-upgrade 2>&1 | tee -a "$log_file"
upgrade_exit_code=${PIPESTATUS[0]}
full-upgrade 2> >(tee -a "$log_file" >&2)
upgrade_exit_code=$?
echo -e
clear
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
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"
@@ -309,7 +326,8 @@ EOF
apt-get -y autoclean > /dev/null 2>&1 || true
msg_ok "$(translate "Cleanup finished")"
local end_time=$(date +%s)
local end_time
end_time=$(date +%s)
local duration=$((end_time - start_time))
local minutes=$((duration / 60))
local seconds=$((duration % 60))
+430
View File
@@ -0,0 +1,430 @@
#!/bin/bash
# ==========================================================
# 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 : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.2
# Last Updated: 20/01/2025
# ==========================================================
# Description:
# This script automates the configuration and installation of
# Coral TPU and iGPU support in Proxmox VE containers. It:
# - Configures a selected LXC container for hardware acceleration
# - Installs and sets up Coral TPU drivers on the Proxmox host
# - Installs necessary drivers inside the container
# - Manages required system and container restarts
#
# Supports Coral USB and Coral M.2 (PCIe) devices.
# Includes USB passthrough enhancement using persistent udev alias (/dev/coral).
#
# Changelog v1.2:
# - Fixed symlink detection for /dev/coral (create=dir for symlinks)
# - Fixed /dev/apex_0 not being mounted in PVE 9 (device existence not required)
# - Fixed grep patterns to avoid matching commented lines
# - Improved device type inference for non-existent devices
# - Added duplicate entry cleanup
# - Better error handling and logging
# ==========================================================
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
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
# ==========================================================
# CONTAINER SELECTION AND VALIDATION
# ==========================================================
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.')"
exit 1
fi
CONTAINER_ID=$(whiptail --title "$(translate 'Select Container')" \
--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.')"
exit 1
fi
if ! pct list | awk 'NR>1 {print $1}' | grep -qw "$CONTAINER_ID"; then
msg_error "$(translate 'Container with ID') $CONTAINER_ID $(translate 'does not exist. Exiting.')"
exit 1
fi
msg_ok "$(translate 'Container selected:') $CONTAINER_ID"
}
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
if pct status "$CONTAINER_ID" | grep -q "running"; then
msg_info "$(translate 'Stopping the container before applying configuration...')"
pct stop "$CONTAINER_ID"
msg_ok "$(translate 'Container stopped.')"
fi
}
# ==========================================================
# UDEV RULES FOR CORAL USB
# ==========================================================
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
}
# ==========================================================
# MOUNT CONFIGURATION HELPER
# ==========================================================
add_mount_if_needed() {
local DEVICE="$1"
local DEST="$2"
local CONFIG_FILE="$3"
if grep -q "lxc.mount.entry: $DEVICE" "$CONFIG_FILE"; then
return 0
fi
local create_type="dir"
if [ -e "$DEVICE" ]; then
if [ -L "$DEVICE" ]; then
create_type="dir"
elif [ -c "$DEVICE" ]; then
create_type="file"
elif [ -d "$DEVICE" ]; then
create_type="dir"
fi
else
case "$DEVICE" in
*/apex_*|*/fb*|*/renderD*|*/card*)
create_type="file"
;;
*/coral)
create_type="dir"
;;
*/dri|*/bus/usb*)
create_type="dir"
;;
*)
create_type="dir"
;;
esac
fi
echo "lxc.mount.entry: $DEVICE $DEST none bind,optional,create=$create_type" >> "$CONFIG_FILE"
}
# ==========================================================
# CLEANUP DUPLICATE ENTRIES
# ==========================================================
cleanup_duplicate_entries() {
local CONFIG_FILE="$1"
local TEMP_FILE=$(mktemp)
awk '!seen[$0]++' "$CONFIG_FILE" > "$TEMP_FILE"
cat "$TEMP_FILE" > "$CONFIG_FILE"
rm -f "$TEMP_FILE"
}
# ==========================================================
# CONFIGURE LXC HARDWARE PASSTHROUGH
# ==========================================================
configure_lxc_hardware() {
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
fi
cleanup_duplicate_entries "$CONFIG_FILE"
# ============================================================
# Convert to privileged container if needed
# ============================================================
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.')"
else
msg_ok "$(translate 'The container is already privileged.')"
fi
sed -i '/^dev[0-9]\+:/d' "$CONFIG_FILE"
# ============================================================
# Enable nesting feature
# ============================================================
if ! grep -Pq "^features:.*nesting=1" "$CONFIG_FILE"; then
if grep -Pq "^features:" "$CONFIG_FILE"; then
sed -i 's/^features: \(.*\)/features: nesting=1,\1/' "$CONFIG_FILE"
else
echo "features: nesting=1" >> "$CONFIG_FILE"
fi
msg_ok "$(translate 'Nesting feature enabled')"
fi
# ============================================================
# iGPU support
# ============================================================
msg_info "$(translate 'Configuring iGPU support...')"
if ! grep -Pq "^lxc.cgroup2.devices.allow: c 226:0 rwm" "$CONFIG_FILE"; then
echo "lxc.cgroup2.devices.allow: c 226:0 rwm # iGPU" >> "$CONFIG_FILE"
fi
if ! grep -Pq "^lxc.cgroup2.devices.allow: c 226:128 rwm" "$CONFIG_FILE"; then
echo "lxc.cgroup2.devices.allow: c 226:128 rwm # iGPU" >> "$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"
msg_ok "$(translate 'iGPU configuration added')"
# ============================================================
# Framebuffer support
# ============================================================
if [ -e "/dev/fb0" ]; then
msg_info "$(translate 'Configuring Framebuffer support...')"
if ! grep -Pq "^lxc.cgroup2.devices.allow: c 29:0 rwm" "$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"
msg_ok "$(translate 'Framebuffer configuration added')"
fi
# ============================================================
# Coral USB passthrough
# ============================================================
msg_info "$(translate 'Configuring Coral USB support...')"
add_udev_rule_for_coral_usb
if ! grep -Pq "^lxc.cgroup2.devices.allow: c 189:\\\* rwm" "$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 [ -L "/dev/coral" ]; then
msg_ok "$(translate 'Coral USB configuration added - device detected')"
else
msg_ok "$(translate 'Coral USB configured but device not currently connected')"
fi
# ============================================================
# Coral M.2 (PCIe) support
# ============================================================
stop_spinner
if lspci | grep -iq "Global Unichip"; then
msg_info "$(translate 'Coral M.2 Apex detected, configuring...')"
if ! grep -Pq "^lxc.cgroup2.devices.allow: c 245:0 rwm" "$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"
if [ -e "/dev/apex_0" ]; then
msg_ok "$(translate 'Coral M.2 Apex configuration added - device ready')"
else
msg_ok "$(translate 'Coral M.2 Apex configuration added - device will be available after reboot')"
fi
fi
cleanup_duplicate_entries "$CONFIG_FILE"
msg_ok "$(translate 'Hardware configuration completed for container') $CONTAINER_ID"
}
# ==========================================================
# INSTALL DRIVERS INSIDE CONTAINER
# ==========================================================
install_coral_in_container() {
msg_info "$(translate 'Installing iGPU and Coral TPU drivers inside the container...')"
tput sc
LOG_FILE=$(mktemp)
if ! pct status "$CONTAINER_ID" | grep -q "running"; then
pct start "$CONTAINER_ID"
sleep 5
fi
stop_spinner
# Determine driver package for Coral M.2
CORAL_M2=$(lspci | grep -i "Global Unichip")
if [[ -n "$CORAL_M2" ]]; then
DRIVER_OPTION=$(whiptail --title "$(translate 'Select driver version')" \
--menu "$(translate 'Choose the driver version for Coral M.2:\n\nCaution: Maximum mode generates more heat.')" 15 60 2 \
1 "libedgetpu1-std ($(translate 'standard performance'))" \
2 "libedgetpu1-max ($(translate 'maximum performance'))" 3>&1 1>&2 2>&3)
case "$DRIVER_OPTION" in
1) DRIVER_PACKAGE="libedgetpu1-std" ;;
2) DRIVER_PACKAGE="libedgetpu1-max" ;;
*) DRIVER_PACKAGE="libedgetpu1-std" ;;
esac
else
DRIVER_PACKAGE="libedgetpu1-std"
fi
# Install drivers inside container
script -q -c "pct exec \"$CONTAINER_ID\" -- bash -c '
set -e
echo \"[1/6] Updating package lists...\"
apt-get update -qq
echo \"[2/6] Installing iGPU drivers...\"
apt-get install -y -qq va-driver-all ocl-icd-libopencl1 intel-opencl-icd vainfo intel-gpu-tools
echo \"[3/6] Configuring DRI permissions...\"
if [ -e /dev/dri ]; then
chgrp video /dev/dri 2>/dev/null || true
chmod 755 /dev/dri 2>/dev/null || true
fi
echo \"[4/6] Adding users to video/render groups...\"
adduser root video 2>/dev/null || true
adduser root render 2>/dev/null || true
echo \"[5/6] Installing Coral TPU dependencies...\"
apt-get install -y -qq gnupg curl ca-certificates
echo \"[6/6] Adding Coral TPU repository...\"
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/coral-edgetpu.gpg
echo \"deb [signed-by=/usr/share/keyrings/coral-edgetpu.gpg] https://packages.cloud.google.com/apt coral-edgetpu-stable main\" | tee /etc/apt/sources.list.d/coral-edgetpu.list >/dev/null
echo \"\"
echo \"Updating package lists for Coral repository...\"
apt-get update -qq
echo \"Installing Coral TPU driver ($DRIVER_PACKAGE)...\"
apt-get install -y -qq $DRIVER_PACKAGE
'" "$LOG_FILE" 2>&1
if [ $? -eq 0 ]; then
tput rc
tput ed
rm -f "$LOG_FILE"
msg_ok "$(translate 'iGPU and Coral TPU drivers installed successfully inside the container.')"
else
tput rc
tput ed
msg_error "$(translate 'Failed to install drivers inside the container.')"
echo ""
echo "$(translate 'Installation log:')"
cat "$LOG_FILE"
rm -f "$LOG_FILE"
exit 1
fi
}
# ==========================================================
# VERIFICATION AND SUMMARY
# ==========================================================
show_configuration_summary() {
local CONFIG_FILE="/etc/pve/lxc/${CONTAINER_ID}.conf"
# iGPU
if grep -q "c 226:0 rwm" "$CONFIG_FILE"; then
msg_ok2 "✓ iGPU support: $(translate 'Enabled')"
fi
# Coral USB
if grep -q "c 189:.*rwm.*Coral USB" "$CONFIG_FILE"; then
if [ -L "/dev/coral" ]; then
msg_ok2 "✓ Coral USB: $(translate 'Enabled and detected')"
else
msg_ok2 "⚠ Coral USB: $(translate 'Enabled but not connected')"
fi
fi
# Coral M.2
if grep -q "c 245:0 rwm.*Coral M2" "$CONFIG_FILE"; then
if [ -e "/dev/apex_0" ]; then
msg_ok2 "✓ Coral M.2: $(translate 'Enabled and ready')"
else
msg_ok2 "⚠ Coral M.2: $(translate 'Enabled (device pending)')"
fi
fi
}
# ==========================================================
# MAIN EXECUTION
# ==========================================================
main() {
select_container
show_proxmenux_logo
configure_lxc_hardware
install_coral_in_container
show_configuration_summary
msg_ok "$(translate 'Configuration completed successfully!')"
echo ""
msg_success "$(translate 'Press Enter to return to menu...')"
read -r
}
# Run main function
main
+931
View File
@@ -0,0 +1,931 @@
#!/bin/bash
# ProxMenux - NVIDIA Driver Installer (PVE 9.x)
# ============================================
# Author : MacRimi
# License : MIT
# Version : 0.9 (PVE9, fixed download issues)
# Last Updated: 29/11/2025
# ============================================
SCRIPT_TITLE="NVIDIA GPU Driver Installer for Proxmox VE"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
COMPONENTS_STATUS_FILE="$BASE_DIR/components_status.json"
LOG_FILE="/tmp/nvidia_install.log"
screen_capture="/tmp/proxmenux_nvidia_screen_capture_$$.txt"
NVIDIA_BASE_URL="https://download.nvidia.com/XFree86/Linux-x86_64"
NVIDIA_WORKDIR="/opt/nvidia"
export BASE_DIR
export COMPONENTS_STATUS_FILE
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
if [[ ! -f "$COMPONENTS_STATUS_FILE" ]]; then
echo "{}" > "$COMPONENTS_STATUS_FILE"
fi
load_language
initialize_cache
# ==========================================================
# GPU detection and current status
# ==========================================================
detect_nvidia_gpus() {
# Only video controllers (not audio)
local lspci_output
lspci_output=$(lspci | grep -i "NVIDIA" \
| grep -Ei "VGA compatible controller|3D controller|Display controller" || true)
if [[ -z "$lspci_output" ]]; then
NVIDIA_GPU_PRESENT=false
DETECTED_GPUS_TEXT="$(translate 'No NVIDIA GPU detected on this system.')"
else
NVIDIA_GPU_PRESENT=true
DETECTED_GPUS_TEXT=""
local i=1
while IFS= read -r line; do
DETECTED_GPUS_TEXT+=" ${i}. ${line}\n"
((i++))
done <<< "$lspci_output"
fi
}
detect_driver_status() {
CURRENT_DRIVER_INSTALLED=false
CURRENT_DRIVER_VERSION=""
# First check if nvidia kernel module is actually loaded
if lsmod | grep -q "^nvidia "; then
modprobe nvidia-uvm 2>/dev/null || true
sleep 1
if command -v nvidia-smi >/dev/null 2>&1; then
CURRENT_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1)
if [[ -n "$CURRENT_DRIVER_VERSION" ]]; then
CURRENT_DRIVER_INSTALLED=true
# Register the installed driver version in components_status.json
update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}'
fi
fi
fi
if $CURRENT_DRIVER_INSTALLED; then
CURRENT_STATUS_TEXT="$(printf '%s %s' "$(translate 'NVIDIA driver installed:')" "$CURRENT_DRIVER_VERSION")"
else
CURRENT_STATUS_TEXT="$(translate 'No NVIDIA driver installed.')"
fi
if $CURRENT_DRIVER_INSTALLED; then
CURRENT_STATUS_COLORED="${CURRENT_STATUS_TEXT}"
else
CURRENT_STATUS_COLORED="${CURRENT_STATUS_TEXT}"
fi
}
# ==========================================================
# System preparation (repos, headers, etc.)
# ==========================================================
ensure_repos_and_headers() {
msg_info "$(translate 'Checking kernel headers and build tools...')"
local kver
kver=$(uname -r)
apt-get update -qq >>"$LOG_FILE" 2>&1
if ! dpkg -s "pve-headers-$kver" >/dev/null 2>&1 && \
! dpkg -s "proxmox-headers-$kver" >/dev/null 2>&1; then
apt-get install -y "pve-headers-$kver" "proxmox-headers-$kver" build-essential dkms >>"$LOG_FILE" 2>&1 || true
else
apt-get install -y build-essential dkms >>"$LOG_FILE" 2>&1 || true
fi
msg_ok "$(translate 'Kernel headers and build tools verified.')" | tee -a "$screen_capture"
}
blacklist_nouveau() {
msg_info "$(translate 'Blacklisting nouveau driver...')"
if ! grep -q '^blacklist nouveau' /etc/modprobe.d/blacklist.conf 2>/dev/null; then
echo "blacklist nouveau" >> /etc/modprobe.d/blacklist.conf
fi
msg_ok "$(translate 'nouveau driver has been blacklisted.')" | tee -a "$screen_capture"
}
ensure_modules_config() {
msg_info "$(translate 'Configuring NVIDIA and VFIO modules...')"
cat > /etc/modules-load.d/nvidia-vfio.conf <<'EOF'
vfio
vfio_iommu_type1
vfio_pci
vfio_virqfd
nvidia
nvidia_uvm
EOF
msg_ok "$(translate 'Modules configuration updated.')" | tee -a "$screen_capture"
}
stop_and_disable_nvidia_services() {
local services=(
"nvidia-persistenced.service"
"nvidia-persistenced"
"nvidia-powerd.service"
)
local services_detected=0
for service in "${services[@]}"; do
if systemctl is-active --quiet "$service" 2>/dev/null || \
systemctl is-enabled --quiet "$service" 2>/dev/null; then
services_detected=1
break
fi
done
if [ "$services_detected" -eq 1 ]; then
msg_info "$(translate 'Stopping and disabling NVIDIA services...')"
for service in "${services[@]}"; do
if systemctl is-active --quiet "$service" 2>/dev/null; then
systemctl stop "$service" >/dev/null 2>&1 || true
fi
if systemctl is-enabled --quiet "$service" 2>/dev/null; then
systemctl disable "$service" >/dev/null 2>&1 || true
fi
done
sleep 2
msg_ok "$(translate 'NVIDIA services stopped and disabled.')" | tee -a "$screen_capture"
fi
}
unload_nvidia_modules() {
msg_info "$(translate 'Unloading NVIDIA kernel modules...')"
for mod in nvidia_uvm nvidia_drm nvidia_modeset nvidia; do
modprobe -r "$mod" >/dev/null 2>&1 || true
done
if lsmod | grep -qi '\bnvidia'; then
for mod in nvidia_uvm nvidia_drm nvidia_modeset nvidia; do
modprobe -r --force "$mod" >/dev/null 2>&1 || true
done
fi
if lsmod | grep -qi '\bnvidia'; then
msg_warn "$(translate 'Some NVIDIA modules could not be unloaded. Installation may fail. Ensure no processes are using the GPU.')"
if command -v lsof >/dev/null 2>&1; then
echo "$(translate 'Processes using NVIDIA:'):" >> "$LOG_FILE"
lsof /dev/nvidia* 2>/dev/null >> "$LOG_FILE" || true
fi
else
msg_ok "$(translate 'NVIDIA kernel modules unloaded successfully.')" | tee -a "$screen_capture"
fi
}
complete_nvidia_uninstall() {
stop_and_disable_nvidia_services
unload_nvidia_modules
if command -v nvidia-uninstall >/dev/null 2>&1; then
msg_info "$(translate 'Running NVIDIA uninstaller...')"
nvidia-uninstall --silent >>"$LOG_FILE" 2>&1 || true
msg_ok "$(translate 'NVIDIA uninstaller completed.')"
fi
cleanup_nvidia_dkms
msg_info "$(translate 'Removing NVIDIA packages...')"
apt-get -y purge 'nvidia-*' 'libnvidia-*' 'cuda-*' 'libcudnn*' >>"$LOG_FILE" 2>&1 || true
apt-get -y autoremove --purge >>"$LOG_FILE" 2>&1 || true
apt-get -y autoclean >>"$LOG_FILE" 2>&1 || true
rm -f /etc/modules-load.d/nvidia-vfio.conf
rm -f /etc/udev/rules.d/70-nvidia.rules
rm -rf /usr/lib/modprobe.d/nvidia*.conf
rm -rf /etc/modprobe.d/nvidia*.conf
if [[ -d "$NVIDIA_WORKDIR" ]]; then
find "$NVIDIA_WORKDIR" -type d -name "nvidia-persistenced" -exec rm -rf {} + 2>/dev/null || true
find "$NVIDIA_WORKDIR" -type d -name "nvidia-patch" -exec rm -rf {} + 2>/dev/null || true
fi
update_component_status "nvidia_driver" "removed" "" "gpu" '{}'
msg_ok "$(translate 'Complete NVIDIA uninstallation finished.')" | tee -a "$screen_capture"
}
cleanup_nvidia_dkms() {
local versions
versions=$(dkms status 2>/dev/null | awk -F, '/nvidia/ {gsub(/ /,"",$2); print $2}' || true)
[[ -z "$versions" ]] && return 0
msg_info "$(translate 'Removing NVIDIA DKMS entries...')"
while IFS= read -r ver; do
[[ -z "$ver" ]] && continue
dkms remove -m nvidia -v "$ver" --all >/dev/null 2>&1 || true
done <<< "$versions"
msg_ok "$(translate 'NVIDIA DKMS entries removed.')"
}
ensure_workdir() {
mkdir -p "$NVIDIA_WORKDIR"
}
# ==========================================================
# Kernel compatibility detection
# ==========================================================
get_kernel_compatibility_info() {
local kernel_version
kernel_version=$(uname -r)
# Determine Proxmox and kernel version
if [[ -f /etc/pve/.version ]]; then
PVE_VERSION=$(cat /etc/pve/.version)
else
PVE_VERSION="unknown"
fi
# Extract kernel major version (6.x, 5.x, etc)
KERNEL_MAJOR=$(echo "$kernel_version" | cut -d. -f1)
KERNEL_MINOR=$(echo "$kernel_version" | cut -d. -f2)
# Define minimum compatible versions based on kernel
# Based on https://docs.nvidia.com/datacenter/tesla/drivers/index.html
if [[ "$KERNEL_MAJOR" -ge 6 ]] && [[ "$KERNEL_MINOR" -ge 17 ]]; then
# Kernel 6.17+ (Proxmox 9.x) - Requires 580.82.07 or higher
MIN_DRIVER_VERSION="580.82.07"
RECOMMENDED_BRANCH="580"
COMPATIBILITY_NOTE="Kernel $kernel_version requires NVIDIA driver 580.82.07 or newer"
elif [[ "$KERNEL_MAJOR" -ge 6 ]] && [[ "$KERNEL_MINOR" -ge 8 ]]; then
# Kernel 6.8-6.16 (Proxmox 8.2+) - Works with 550.x or higher
MIN_DRIVER_VERSION="550"
RECOMMENDED_BRANCH="580"
COMPATIBILITY_NOTE="Kernel $kernel_version works best with NVIDIA driver 550.x or newer"
elif [[ "$KERNEL_MAJOR" -ge 6 ]]; then
# Kernel 6.2-6.7 (Proxmox 8.x initial) - Works with 535.x or higher
MIN_DRIVER_VERSION="535"
RECOMMENDED_BRANCH="550"
COMPATIBILITY_NOTE="Kernel $kernel_version works with NVIDIA driver 535.x or newer"
elif [[ "$KERNEL_MAJOR" -eq 5 ]] && [[ "$KERNEL_MINOR" -ge 15 ]]; then
# Kernel 5.15+ (Proxmox 7.x, 8.x legacy) - Works with 470.x or higher
MIN_DRIVER_VERSION="470"
RECOMMENDED_BRANCH="535"
COMPATIBILITY_NOTE="Kernel $kernel_version works with NVIDIA driver 470.x or newer"
else
# Old kernels
MIN_DRIVER_VERSION="450"
RECOMMENDED_BRANCH="470"
COMPATIBILITY_NOTE="For older kernels, compatibility may vary"
fi
}
is_version_compatible() {
local version="$1"
local ver_major ver_minor ver_patch
# Extract version components (major.minor.patch)
ver_major=$(echo "$version" | cut -d. -f1)
ver_minor=$(echo "$version" | cut -d. -f2)
ver_patch=$(echo "$version" | cut -d. -f3)
if [[ "$MIN_DRIVER_VERSION" == "580.82.07" ]]; then
# Compare full version: must be >= 580.82.07
if [[ ${ver_major} -gt 580 ]]; then
return 0
elif [[ ${ver_major} -eq 580 ]]; then
if [[ $((10#${ver_minor})) -gt 82 ]]; then
return 0
elif [[ $((10#${ver_minor})) -eq 82 ]]; then
if [[ $((10#${ver_patch:-0})) -ge 7 ]]; then
return 0
fi
fi
fi
return 1
fi
if [[ ${ver_major} -ge ${MIN_DRIVER_VERSION} ]]; then
return 0
else
return 1
fi
}
version_le() {
local v1="$1"
local v2="$2"
IFS='.' read -r a1 b1 c1 <<<"$v1"
IFS='.' read -r a2 b2 c2 <<<"$v2"
a1=${a1:-0}; b1=${b1:-0}; c1=${c1:-0}
a2=${a2:-0}; b2=${b2:-0}; c2=${c2:-0}
a1=$((10#$a1)); b1=$((10#$b1)); c1=$((10#$c1))
a2=$((10#$a2)); b2=$((10#$b2)); c2=$((10#$c2))
if (( a1 < a2 )); then
return 0
elif (( a1 > a2 )); then
return 1
fi
if (( b1 < b2 )); then
return 0
elif (( b1 > b2 )); then
return 1
fi
if (( c1 <= c2 )); then
return 0
else
return 1
fi
}
# ==========================================================
# NVIDIA version management - FIXED VERSION
# ==========================================================
download_latest_version() {
local latest_line version
latest_line=$(curl -fsSL "${NVIDIA_BASE_URL}/latest.txt" 2>&1)
if [[ -z "$latest_line" ]]; then
echo "" >&2
return 1
fi
version=$(echo "$latest_line" | awk '{print $1}' | tr -d '[:space:]')
if [[ -z "$version" ]]; then
echo "" >&2
return 1
fi
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
echo "" >&2
return 1
fi
echo "$version"
return 0
}
list_available_versions() {
local html_content versions
html_content=$(curl -s "$NVIDIA_BASE_URL/" 2>&1)
if [[ -z "$html_content" ]]; then
echo "" >&2
return 1
fi
versions=$(echo "$html_content" \
| grep -o 'href=[^ >]*' \
| awk -F"'" '{print $2}' \
| grep -E '^[0-9]' \
| sed 's/\/$//' \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
| sort -Vr \
| uniq)
if [[ -z "$versions" ]]; then
echo "" >&2
return 1
fi
echo "$versions"
return 0
}
verify_version_exists() {
local version="$1"
local url="${NVIDIA_BASE_URL}/${version}/"
if curl -fsSL --head "$url" >/dev/null 2>&1; then
return 0
else
return 1
fi
}
download_nvidia_installer() {
ensure_workdir
local version="$1"
version=$(echo "$version" | tr -d '[:space:]' | tr -d '\n' | tr -d '\r')
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
msg_error "Invalid version format: $version" >&2
echo "ERROR: Invalid version format: '$version'" >> "$LOG_FILE"
return 1
fi
local run_file="$NVIDIA_WORKDIR/NVIDIA-Linux-x86_64-${version}.run"
if [[ -f "$run_file" ]]; then
echo "Found existing file: $run_file" >> "$LOG_FILE"
local existing_size file_type
existing_size=$(stat -c%s "$run_file" 2>/dev/null || stat -f%z "$run_file" 2>/dev/null || echo "0")
file_type=$(file "$run_file" 2>/dev/null || echo "unknown")
echo "Existing file size: $existing_size bytes" >> "$LOG_FILE"
echo "Existing file type: $file_type" >> "$LOG_FILE"
if [[ $existing_size -gt 40000000 ]] && echo "$file_type" | grep -q "executable"; then
if sh "$run_file" --check 2>&1 | tee -a "$LOG_FILE" | grep -q "OK"; then
echo "Existing file passed integrity check" >> "$LOG_FILE"
msg_ok "$(translate 'Installer already downloaded and verified.')" >&2
printf '%s\n' "$run_file"
return 0
else
echo "Existing file FAILED integrity check, removing..." >> "$LOG_FILE"
msg_warn "$(translate 'Existing file failed verification, re-downloading...')" >&2
rm -f "$run_file"
fi
else
echo "Existing file invalid (size or type), removing..." >> "$LOG_FILE"
msg_warn "$(translate 'Removing invalid existing file...')" >&2
rm -f "$run_file"
fi
fi
if ! verify_version_exists "$version"; then
msg_error "Version $version does not exist on NVIDIA servers" >&2
echo "ERROR: Version $version not found on server" >> "$LOG_FILE"
return 1
fi
local urls=(
"${NVIDIA_BASE_URL}/${version}/NVIDIA-Linux-x86_64-${version}.run"
"${NVIDIA_BASE_URL}/${version}/NVIDIA-Linux-x86_64-${version}-no-compat32.run"
)
local success=false
local url_index=0
for url in "${urls[@]}"; do
((url_index++))
echo "Attempting download from: $url" >> "$LOG_FILE"
rm -f "$run_file"
if curl -fL --connect-timeout 30 --max-time 600 "$url" -o "$run_file" >> "$LOG_FILE" 2>&1; then
echo "Download completed, verifying file..." >> "$LOG_FILE"
if [[ ! -f "$run_file" ]]; then
echo "ERROR: File not created after download" >> "$LOG_FILE"
continue
fi
local file_size
file_size=$(stat -c%s "$run_file" 2>/dev/null || stat -f%z "$run_file" 2>/dev/null || echo "0")
echo "Downloaded file size: $file_size bytes" >> "$LOG_FILE"
if [[ $file_size -lt 40000000 ]]; then
echo "ERROR: File too small ($file_size bytes, expected >40MB)" >> "$LOG_FILE"
head -c 200 "$run_file" >> "$LOG_FILE" 2>&1
rm -f "$run_file"
continue
fi
local file_type
file_type=$(file "$run_file" 2>/dev/null)
echo "File type: $file_type" >> "$LOG_FILE"
if echo "$file_type" | grep -q "executable"; then
echo "SUCCESS: Valid executable downloaded" >> "$LOG_FILE"
success=true
break
else
echo "ERROR: Not a valid executable" >> "$LOG_FILE"
head -c 200 "$run_file" | od -c >> "$LOG_FILE" 2>&1
rm -f "$run_file"
fi
else
echo "ERROR: curl failed for $url (exit code: $?)" >> "$LOG_FILE"
rm -f "$run_file"
fi
done
if ! $success; then
msg_error "$(translate 'Download failed for all attempted URLs')" >&2
msg_error "Version $version may not be available for your architecture" >&2
echo "ERROR: All download attempts failed" >> "$LOG_FILE"
return 1
fi
chmod +x "$run_file"
echo "Installation file ready: $run_file" >> "$LOG_FILE"
printf '%s\n' "$run_file"
}
# ==========================================================
# Installation / uninstallation
# ==========================================================
run_nvidia_installer() {
local installer="$1"
msg_info2 "$(translate 'Starting NVIDIA installer. This may take several minutes...')"
echo "" >>"$LOG_FILE"
echo "=== Running NVIDIA installer: $installer ===" >>"$LOG_FILE"
local tmp_extract_dir="$NVIDIA_WORKDIR/tmp_extract"
mkdir -p "$tmp_extract_dir"
sh "$installer" --tmpdir="$tmp_extract_dir" --no-questions --ui=none --disable-nouveau --dkms 2>&1 | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
echo "" >>"$LOG_FILE"
rm -rf "$tmp_extract_dir"
if [[ $rc -ne 0 ]]; then
msg_error "$(translate 'NVIDIA installer reported an error. Check /tmp/nvidia_install.log')"
update_component_status "nvidia_driver" "failed" "" "gpu" '{"patched":false}'
return 1
fi
msg_ok "$(translate 'NVIDIA driver installed successfully.')" | tee -a "$screen_capture"
return 0
}
remove_nvidia_driver() {
complete_nvidia_uninstall
}
install_udev_rules_and_persistenced() {
msg_info "$(translate 'Installing NVIDIA udev rules and persistence service...')"
cat >/etc/udev/rules.d/70-nvidia.rules <<'EOF'
# /etc/udev/rules.d/70-nvidia.rules
KERNEL=="nvidia", RUN+="/bin/bash -c '/usr/bin/nvidia-smi -L'"
KERNEL=="nvidia_uvm", RUN+="/bin/bash -c '/usr/bin/nvidia-modprobe -c0 -u'"
EOF
udevadm control --reload-rules
udevadm trigger --subsystem-match=drm --subsystem-match=pci || true
ensure_workdir
cd "$NVIDIA_WORKDIR" || return 1
if [[ ! -d nvidia-persistenced ]]; then
git clone https://github.com/NVIDIA/nvidia-persistenced.git >>"$LOG_FILE" 2>&1 || true
fi
if [[ -d nvidia-persistenced/init ]]; then
cd nvidia-persistenced/init || return 1
./install.sh >>"$LOG_FILE" 2>&1 || true
fi
msg_ok "$(translate 'NVIDIA udev rules and persistence service installed.')" | tee -a "$screen_capture"
}
apply_nvidia_patch_if_needed() {
if ! hybrid_whiptail_yesno "$(translate 'NVIDIA Patch')" \
"\n$(translate 'Do you want to apply the optional NVIDIA patch to remove some GPU limitations?')"; then
msg_info2 "$(translate 'NVIDIA patch not applied.')"
update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}'
return 0
fi
msg_info "$(translate 'Cloning and applying NVIDIA patch (keylase/nvidia-patch)...')"
ensure_workdir
cd "$NVIDIA_WORKDIR" || return 1
if [[ ! -d nvidia-patch ]]; then
git clone https://github.com/keylase/nvidia-patch.git >>"$LOG_FILE" 2>&1 || true
fi
if [[ -x nvidia-patch/patch.sh ]]; then
cd nvidia-patch || return 1
./patch.sh >>"$LOG_FILE" 2>&1 || true
msg_ok "$(translate 'NVIDIA patch applied - check README for supported versions.')"
update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":true}'
else
msg_warn "$(translate 'Could not run NVIDIA patch script. Please verify repository and driver version.')"
update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}'
fi
}
restart_prompt() {
if hybrid_whiptail_yesno "$(translate 'NVIDIA Drivers')" \
"\n$(translate 'The installation/changes require a server restart to apply correctly. Do you want to reboot now?')"; then
msg_success "$(translate 'Installation completed. Press Enter to continue...')"
read -r
msg_warn "$(translate 'Restarting the server...')"
rm -f "$screen_capture"
reboot
else
msg_success "$(translate 'Installation completed. Please reboot the server manually as soon as possible.')"
msg_success "$(translate 'Completed. Press Enter to return to menu...')"
read -r
rm -f "$screen_capture"
fi
}
# ==========================================================
# Dialog menus
# ==========================================================
show_action_menu_if_installed() {
if ! $CURRENT_DRIVER_INSTALLED; then
ACTION="install"
return 0
fi
local menu_choices=(
"install" "$(translate 'Reinstall/Update NVIDIA drivers')"
"remove" "$(translate 'Uninstall NVIDIA drivers and configuration')"
)
ACTION=$(hybrid_menu "ProxMenux" "$(translate 'NVIDIA Actions')\n\n$(translate 'Choose an action:')" 14 80 8 "${menu_choices[@]}") || ACTION="cancel"
}
show_install_overview() {
local overview
overview="\n$(translate 'This installation will:')\n\n"
overview+="$(translate 'Install NVIDIA proprietary drivers')\n"
overview+="$(translate 'Configure GPU passthrough with VFIO')\n"
overview+="$(translate 'Blacklist nouveau driver')\n"
overview+="$(translate 'Enable IOMMU support if not enabled')\n\n"
overview+="$(translate 'Detected GPU(s):')\n"
overview+="\Zb\Z4$DETECTED_GPUS_TEXT\Zn\n"
overview+="\n\Zn$(translate 'Current status: ') "
overview+="\Zb${CURRENT_STATUS_TEXT}\Zn\n\n"
overview+="$(translate 'After confirming, you will be asked to choose the NVIDIA driver version to install.')\n\n"
overview+="$(translate 'Do you want to continue?')"
hybrid_yesno "$(translate 'NVIDIA GPU Driver Installation')" "$overview" 22 90
}
show_version_menu() {
local latest versions_list
local kernel_version
kernel_version=$(uname -r)
latest=$(download_latest_version 2>/dev/null)
versions_list=$(list_available_versions 2>/dev/null)
if [[ -z "$latest" ]] && [[ -z "$versions_list" ]]; then
hybrid_msgbox "$(translate 'Error')" \
"$(translate 'Could not retrieve versions list from NVIDIA. Please check your internet connection.')\n\nURL: ${NVIDIA_BASE_URL}" 10 80
DRIVER_VERSION="cancel"
return 1
fi
if [[ -z "$latest" ]] && [[ -n "$versions_list" ]]; then
latest=$(echo "$versions_list" | head -n1)
fi
if [[ -n "$latest" ]] && [[ -z "$versions_list" ]]; then
versions_list="$latest"
fi
# Clean latest version
latest=$(echo "$latest" | tr -d '[:space:]')
local current_list="$versions_list"
# Apply kernel compatibility filter if needed
if [[ -n "$MIN_DRIVER_VERSION" ]]; then
local filtered_list=""
while IFS= read -r ver; do
[[ -z "$ver" ]] && continue
if is_version_compatible "$ver"; then
filtered_list+="$ver"$'\n'
fi
done <<< "$current_list"
current_list="$filtered_list"
fi
if [[ -n "$latest" ]]; then
local filtered_max_list=""
while IFS= read -r ver; do
[[ -z "$ver" ]] && continue
if version_le "$ver" "$latest"; then
filtered_max_list+="$ver"$'\n'
fi
done <<< "$current_list"
current_list="$filtered_max_list"
fi
local menu_text="$(translate 'Select the NVIDIA driver version to install:')\n\n"
menu_text+="$(translate 'Versions shown are compatible with your kernel. Latest available is recommended in most cases.')"
local choices=()
choices+=("latest" "$(translate 'Latest available') (${latest:-unknown})")
choices+=("" "")
if [[ -n "$current_list" ]]; then
while IFS= read -r ver; do
[[ -z "$ver" ]] && continue
ver=$(echo "$ver" | tr -d '[:space:]')
[[ -z "$ver" ]] && continue
choices+=("$ver" "$ver")
done <<< "$current_list"
else
choices+=("" "$(translate 'No compatible versions found for your kernel')")
fi
local selection=$(hybrid_menu "$(translate 'NVIDIA Driver Version')" "$menu_text" 26 90 16 "${choices[@]}") || { DRIVER_VERSION="cancel"; return 1; }
case "$selection" in
"")
DRIVER_VERSION="cancel"
return 1
;;
latest)
DRIVER_VERSION="$latest"
DRIVER_VERSION=$(echo "$DRIVER_VERSION" | tr -d '[:space:]')
return 0
;;
*)
DRIVER_VERSION="$selection"
DRIVER_VERSION=$(echo "$DRIVER_VERSION" | tr -d '[:space:]')
return 0
;;
esac
}
# ==========================================================
# Main flow
# ==========================================================
main() {
: >"$LOG_FILE"
: >"$screen_capture"
detect_nvidia_gpus
detect_driver_status
if ! $NVIDIA_GPU_PRESENT; then
dialog --backtitle "ProxMenux" --title "$(translate 'NVIDIA GPU Driver Installation')" --msgbox \
"\n$(translate 'No NVIDIA GPU has been detected on this system. The installer will now exit.')" 20 70
exit 1
fi
show_action_menu_if_installed
case "$ACTION" in
install)
if ! show_install_overview; then
exit 0
fi
get_kernel_compatibility_info
show_version_menu
if [[ "$DRIVER_VERSION" == "cancel" || -z "$DRIVER_VERSION" ]]; then
exit 0
fi
if $CURRENT_DRIVER_INSTALLED; then
if [[ "$CURRENT_DRIVER_VERSION" == "$DRIVER_VERSION" ]]; then
local confirm_text
confirm_text="\n\n\n$(translate 'Version') \Zb\Z4$DRIVER_VERSION\Zn\n\n$(translate 'is already installed. Do you want to reinstall it? This will perform a clean uninstall first.')"
if ! hybrid_yesno "$(translate 'Same Version Detected')" "$confirm_text" 14 70; then
exit 0
fi
else
local confirm_text
confirm_text="\n\n$(translate 'Current version:') \Zb$CURRENT_DRIVER_VERSION\Zn\n"
confirm_text+="$(translate 'New version:') \Zb\Z4$DRIVER_VERSION\Zn\n\n"
confirm_text+="$(translate 'The current driver will be completely uninstalled before installing the new version. Continue?')"
if ! hybrid_yesno "$(translate 'Version Change Detected')" "$confirm_text" 20 70; then
exit 0
fi
fi
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
msg_info2 "$(translate 'Uninstalling current NVIDIA driver before installing new version...')"
complete_nvidia_uninstall
sleep 2
CURRENT_DRIVER_INSTALLED=false
CURRENT_DRIVER_VERSION=""
fi
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
ensure_repos_and_headers
blacklist_nouveau
ensure_modules_config
stop_and_disable_nvidia_services
unload_nvidia_modules
msg_info "$(translate 'Downloading NVIDIA driver version:') $DRIVER_VERSION"
local installer
installer=$(download_nvidia_installer "$DRIVER_VERSION" 2>>"$LOG_FILE")
local download_result=$?
if [[ $download_result -ne 0 ]]; then
msg_error "$(translate 'Failed to download NVIDIA installer')"
exit 1
fi
msg_ok "$(translate 'NVIDIA installer downloaded successfully')"
if [[ -z "$installer" || ! -f "$installer" ]]; then
msg_error "$(translate 'Internal error: NVIDIA installer path is empty or file not found.')"
rm -f "$screen_capture"
exit 1
fi
if ! run_nvidia_installer "$installer"; then
rm -f "$screen_capture"
exit 1
fi
sleep 2
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
cat "$screen_capture"
echo -e "${TAB}${GN}📄 $(translate "Log file")${CL}: ${BL}$LOG_FILE${CL}"
install_udev_rules_and_persistenced
msg_info "$(translate 'Updating initramfs for all kernels...')"
update-initramfs -u -k all >>"$LOG_FILE" 2>&1 || true
msg_ok "$(translate 'initramfs updated.')"
msg_info2 "$(translate 'Checking NVIDIA driver status with nvidia-smi')"
if command -v nvidia-smi >/dev/null 2>&1; then
nvidia-smi || true
CURRENT_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1)
CURRENT_DRIVER_INSTALLED=true
else
msg_warn "$(translate 'nvidia-smi not found in PATH. Please verify the driver installation.')"
fi
if [[ -n "$CURRENT_DRIVER_VERSION" ]]; then
msg_ok "$(translate 'NVIDIA driver') $CURRENT_DRIVER_VERSION $(translate 'installed successfully.')"
update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}'
msg_success "$(translate 'Driver installed successfully. Press Enter to continue...')"
read -r
else
msg_error "$(translate 'Failed to detect installed NVIDIA driver version.')"
update_component_status "nvidia_driver" "failed" "" "gpu" '{"patched":false}'
fi
apply_nvidia_patch_if_needed
restart_prompt
;;
remove)
if hybrid_yesno "$(translate 'NVIDIA Driver Uninstall')" \
"\n\n\n$(translate 'This will remove NVIDIA drivers and related configuration. Do you want to continue?')" 14 70; then
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
remove_nvidia_driver
msg_info "$(translate 'Updating initramfs for all kernels...')"
update-initramfs -u -k all >>"$LOG_FILE" 2>&1 || true
msg_ok "$(translate 'initramfs updated.')"
restart_prompt
fi
;;
cancel|*)
exit 0
;;
esac
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main
fi
+916
View File
@@ -0,0 +1,916 @@
#!/bin/bash
# ProxMenux - NVIDIA Driver Installer (PVE 9.x)
# ============================================
# Author : MacRimi
# License : MIT
# Version : 0.9 (PVE9, fixed download issues)
# Last Updated: 29/11/2025
# ============================================
SCRIPT_TITLE="NVIDIA GPU Driver Installer for Proxmox VE"
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
COMPONENTS_STATUS_FILE="$BASE_DIR/components_status.json"
LOG_FILE="/tmp/nvidia_install.log"
screen_capture="/tmp/proxmenux_nvidia_screen_capture_$$.txt"
NVIDIA_BASE_URL="https://download.nvidia.com/XFree86/Linux-x86_64"
NVIDIA_WORKDIR="/opt/nvidia"
export BASE_DIR
export COMPONENTS_STATUS_FILE
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
if [[ ! -f "$COMPONENTS_STATUS_FILE" ]]; then
echo "{}" > "$COMPONENTS_STATUS_FILE"
fi
load_language
initialize_cache
# ==========================================================
# GPU detection and current status
# ==========================================================
detect_nvidia_gpus() {
# Only video controllers (not audio)
local lspci_output
lspci_output=$(lspci | grep -i "NVIDIA" \
| grep -Ei "VGA compatible controller|3D controller|Display controller" || true)
if [[ -z "$lspci_output" ]]; then
NVIDIA_GPU_PRESENT=false
DETECTED_GPUS_TEXT="$(translate 'No NVIDIA GPU detected on this system.')"
else
NVIDIA_GPU_PRESENT=true
DETECTED_GPUS_TEXT=""
local i=1
while IFS= read -r line; do
DETECTED_GPUS_TEXT+=" ${i}. ${line}\n"
((i++))
done <<< "$lspci_output"
fi
}
detect_driver_status() {
CURRENT_DRIVER_INSTALLED=false
CURRENT_DRIVER_VERSION=""
# First check if nvidia kernel module is actually loaded
if lsmod | grep -q "^nvidia "; then
modprobe nvidia-uvm 2>/dev/null || true
sleep 1
if command -v nvidia-smi >/dev/null 2>&1; then
CURRENT_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1)
if [[ -n "$CURRENT_DRIVER_VERSION" ]]; then
CURRENT_DRIVER_INSTALLED=true
# Register the installed driver version in components_status.json
update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}'
fi
fi
fi
if $CURRENT_DRIVER_INSTALLED; then
CURRENT_STATUS_TEXT="$(printf '%s %s' "$(translate 'NVIDIA driver installed:')" "$CURRENT_DRIVER_VERSION")"
else
CURRENT_STATUS_TEXT="$(translate 'No NVIDIA driver installed.')"
fi
if $CURRENT_DRIVER_INSTALLED; then
CURRENT_STATUS_COLORED="\Z2${CURRENT_STATUS_TEXT}\Zn"
else
CURRENT_STATUS_COLORED="\Z3${CURRENT_STATUS_TEXT}\Zn"
fi
}
# ==========================================================
# System preparation (repos, headers, etc.)
# ==========================================================
ensure_repos_and_headers() {
msg_info "$(translate 'Checking kernel headers and build tools...')"
local kver
kver=$(uname -r)
apt-get update -qq >>"$LOG_FILE" 2>&1
if ! dpkg -s "pve-headers-$kver" >/dev/null 2>&1 && \
! dpkg -s "proxmox-headers-$kver" >/dev/null 2>&1; then
apt-get install -y "pve-headers-$kver" "proxmox-headers-$kver" build-essential dkms >>"$LOG_FILE" 2>&1 || true
else
apt-get install -y build-essential dkms >>"$LOG_FILE" 2>&1 || true
fi
msg_ok "$(translate 'Kernel headers and build tools verified.')" | tee -a "$screen_capture"
}
blacklist_nouveau() {
msg_info "$(translate 'Blacklisting nouveau driver...')"
if ! grep -q '^blacklist nouveau' /etc/modprobe.d/blacklist.conf 2>/dev/null; then
echo "blacklist nouveau" >> /etc/modprobe.d/blacklist.conf
fi
msg_ok "$(translate 'nouveau driver has been blacklisted.')" | tee -a "$screen_capture"
}
ensure_modules_config() {
msg_info "$(translate 'Configuring NVIDIA and VFIO modules...')"
cat > /etc/modules-load.d/nvidia-vfio.conf <<'EOF'
vfio
vfio_iommu_type1
vfio_pci
vfio_virqfd
nvidia
nvidia_uvm
EOF
msg_ok "$(translate 'Modules configuration updated.')" | tee -a "$screen_capture"
}
stop_and_disable_nvidia_services() {
local services=(
"nvidia-persistenced.service"
"nvidia-persistenced"
"nvidia-powerd.service"
)
local services_detected=0
for service in "${services[@]}"; do
if systemctl is-active --quiet "$service" 2>/dev/null || \
systemctl is-enabled --quiet "$service" 2>/dev/null; then
services_detected=1
break
fi
done
if [ "$services_detected" -eq 1 ]; then
msg_info "$(translate 'Stopping and disabling NVIDIA services...')"
for service in "${services[@]}"; do
if systemctl is-active --quiet "$service" 2>/dev/null; then
systemctl stop "$service" >/dev/null 2>&1 || true
fi
if systemctl is-enabled --quiet "$service" 2>/dev/null; then
systemctl disable "$service" >/dev/null 2>&1 || true
fi
done
sleep 2
msg_ok "$(translate 'NVIDIA services stopped and disabled.')" | tee -a "$screen_capture"
fi
}
unload_nvidia_modules() {
msg_info "$(translate 'Unloading NVIDIA kernel modules...')"
for mod in nvidia_uvm nvidia_drm nvidia_modeset nvidia; do
modprobe -r "$mod" >/dev/null 2>&1 || true
done
if lsmod | grep -qi '\bnvidia'; then
for mod in nvidia_uvm nvidia_drm nvidia_modeset nvidia; do
modprobe -r --force "$mod" >/dev/null 2>&1 || true
done
fi
if lsmod | grep -qi '\bnvidia'; then
msg_warn "$(translate 'Some NVIDIA modules could not be unloaded. Installation may fail. Ensure no processes are using the GPU.')"
if command -v lsof >/dev/null 2>&1; then
echo "$(translate 'Processes using NVIDIA:'):" >> "$LOG_FILE"
lsof /dev/nvidia* 2>/dev/null >> "$LOG_FILE" || true
fi
else
msg_ok "$(translate 'NVIDIA kernel modules unloaded successfully.')" | tee -a "$screen_capture"
fi
}
complete_nvidia_uninstall() {
stop_and_disable_nvidia_services
unload_nvidia_modules
if command -v nvidia-uninstall >/dev/null 2>&1; then
msg_info "$(translate 'Running NVIDIA uninstaller...')"
nvidia-uninstall --silent >>"$LOG_FILE" 2>&1 || true
msg_ok "$(translate 'NVIDIA uninstaller completed.')"
fi
cleanup_nvidia_dkms
msg_info "$(translate 'Removing NVIDIA packages...')"
apt-get -y purge 'nvidia-*' 'libnvidia-*' 'cuda-*' 'libcudnn*' >>"$LOG_FILE" 2>&1 || true
apt-get -y autoremove --purge >>"$LOG_FILE" 2>&1 || true
apt-get -y autoclean >>"$LOG_FILE" 2>&1 || true
rm -f /etc/modules-load.d/nvidia-vfio.conf
rm -f /etc/udev/rules.d/70-nvidia.rules
rm -rf /usr/lib/modprobe.d/nvidia*.conf
rm -rf /etc/modprobe.d/nvidia*.conf
if [[ -d "$NVIDIA_WORKDIR" ]]; then
find "$NVIDIA_WORKDIR" -type d -name "nvidia-persistenced" -exec rm -rf {} + 2>/dev/null || true
find "$NVIDIA_WORKDIR" -type d -name "nvidia-patch" -exec rm -rf {} + 2>/dev/null || true
fi
update_component_status "nvidia_driver" "removed" "" "gpu" '{}'
msg_ok "$(translate 'Complete NVIDIA uninstallation finished.')" | tee -a "$screen_capture"
}
cleanup_nvidia_dkms() {
local versions
versions=$(dkms status 2>/dev/null | awk -F, '/nvidia/ {gsub(/ /,"",$2); print $2}' || true)
[[ -z "$versions" ]] && return 0
msg_info "$(translate 'Removing NVIDIA DKMS entries...')"
while IFS= read -r ver; do
[[ -z "$ver" ]] && continue
dkms remove -m nvidia -v "$ver" --all >/dev/null 2>&1 || true
done <<< "$versions"
msg_ok "$(translate 'NVIDIA DKMS entries removed.')"
}
ensure_workdir() {
mkdir -p "$NVIDIA_WORKDIR"
}
# ==========================================================
# Kernel compatibility detection
# ==========================================================
get_kernel_compatibility_info() {
local kernel_version
kernel_version=$(uname -r)
# Determine Proxmox and kernel version
if [[ -f /etc/pve/.version ]]; then
PVE_VERSION=$(cat /etc/pve/.version)
else
PVE_VERSION="unknown"
fi
# Extract kernel major version (6.x, 5.x, etc)
KERNEL_MAJOR=$(echo "$kernel_version" | cut -d. -f1)
KERNEL_MINOR=$(echo "$kernel_version" | cut -d. -f2)
# Define minimum compatible versions based on kernel
# Based on https://docs.nvidia.com/datacenter/tesla/drivers/index.html
if [[ "$KERNEL_MAJOR" -ge 6 ]] && [[ "$KERNEL_MINOR" -ge 17 ]]; then
# Kernel 6.17+ (Proxmox 9.x) - Requires 580.82.07 or higher
MIN_DRIVER_VERSION="580.82.07"
RECOMMENDED_BRANCH="580"
COMPATIBILITY_NOTE="Kernel $kernel_version requires NVIDIA driver 580.82.07 or newer"
elif [[ "$KERNEL_MAJOR" -ge 6 ]] && [[ "$KERNEL_MINOR" -ge 8 ]]; then
# Kernel 6.8-6.16 (Proxmox 8.2+) - Works with 550.x or higher
MIN_DRIVER_VERSION="550"
RECOMMENDED_BRANCH="580"
COMPATIBILITY_NOTE="Kernel $kernel_version works best with NVIDIA driver 550.x or newer"
elif [[ "$KERNEL_MAJOR" -ge 6 ]]; then
# Kernel 6.2-6.7 (Proxmox 8.x initial) - Works with 535.x or higher
MIN_DRIVER_VERSION="535"
RECOMMENDED_BRANCH="550"
COMPATIBILITY_NOTE="Kernel $kernel_version works with NVIDIA driver 535.x or newer"
elif [[ "$KERNEL_MAJOR" -eq 5 ]] && [[ "$KERNEL_MINOR" -ge 15 ]]; then
# Kernel 5.15+ (Proxmox 7.x, 8.x legacy) - Works with 470.x or higher
MIN_DRIVER_VERSION="470"
RECOMMENDED_BRANCH="535"
COMPATIBILITY_NOTE="Kernel $kernel_version works with NVIDIA driver 470.x or newer"
else
# Old kernels
MIN_DRIVER_VERSION="450"
RECOMMENDED_BRANCH="470"
COMPATIBILITY_NOTE="For older kernels, compatibility may vary"
fi
}
is_version_compatible() {
local version="$1"
local ver_major ver_minor ver_patch
# Extract version components (major.minor.patch)
ver_major=$(echo "$version" | cut -d. -f1)
ver_minor=$(echo "$version" | cut -d. -f2)
ver_patch=$(echo "$version" | cut -d. -f3)
if [[ "$MIN_DRIVER_VERSION" == "580.82.07" ]]; then
# Compare full version: must be >= 580.82.07
if [[ ${ver_major} -gt 580 ]]; then
return 0
elif [[ ${ver_major} -eq 580 ]]; then
if [[ $((10#${ver_minor})) -gt 82 ]]; then
return 0
elif [[ $((10#${ver_minor})) -eq 82 ]]; then
if [[ $((10#${ver_patch:-0})) -ge 7 ]]; then
return 0
fi
fi
fi
return 1
fi
if [[ ${ver_major} -ge ${MIN_DRIVER_VERSION} ]]; then
return 0
else
return 1
fi
}
# ==========================================================
# NVIDIA version management - FIXED VERSION
# ==========================================================
download_latest_version() {
local latest_line version
latest_line=$(curl -fsSL "${NVIDIA_BASE_URL}/latest.txt" 2>&1)
if [[ -z "$latest_line" ]]; then
echo "" >&2
return 1
fi
version=$(echo "$latest_line" | awk '{print $1}' | tr -d '[:space:]')
if [[ -z "$version" ]]; then
echo "" >&2
return 1
fi
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
echo "" >&2
return 1
fi
echo "$version"
return 0
}
list_available_versions() {
local html_content versions
html_content=$(curl -s "$NVIDIA_BASE_URL/" 2>&1)
if [[ -z "$html_content" ]]; then
echo "" >&2
return 1
fi
versions=$(echo "$html_content" \
| grep -o 'href=[^ >]*' \
| awk -F"'" '{print $2}' \
| grep -E '^[0-9]' \
| sed 's/\/$//' \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
| sort -Vr \
| uniq)
if [[ -z "$versions" ]]; then
echo "" >&2
return 1
fi
echo "$versions"
return 0
}
verify_version_exists() {
local version="$1"
local url="${NVIDIA_BASE_URL}/${version}/"
if curl -fsSL --head "$url" >/dev/null 2>&1; then
return 0
else
return 1
fi
}
download_nvidia_installer() {
ensure_workdir
local version="$1"
version=$(echo "$version" | tr -d '[:space:]' | tr -d '\n' | tr -d '\r')
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
msg_error "Invalid version format: $version" >&2
echo "ERROR: Invalid version format: '$version'" >> "$LOG_FILE"
return 1
fi
local run_file="$NVIDIA_WORKDIR/NVIDIA-Linux-x86_64-${version}.run"
if [[ -f "$run_file" ]]; then
echo "Found existing file: $run_file" >> "$LOG_FILE"
local existing_size file_type
existing_size=$(stat -c%s "$run_file" 2>/dev/null || stat -f%z "$run_file" 2>/dev/null || echo "0")
file_type=$(file "$run_file" 2>/dev/null || echo "unknown")
echo "Existing file size: $existing_size bytes" >> "$LOG_FILE"
echo "Existing file type: $file_type" >> "$LOG_FILE"
if [[ $existing_size -gt 40000000 ]] && echo "$file_type" | grep -q "executable"; then
if sh "$run_file" --check 2>&1 | tee -a "$LOG_FILE" | grep -q "OK"; then
echo "Existing file passed integrity check" >> "$LOG_FILE"
msg_ok "$(translate 'Installer already downloaded and verified.')" >&2
printf '%s\n' "$run_file"
return 0
else
echo "Existing file FAILED integrity check, removing..." >> "$LOG_FILE"
msg_warn "$(translate 'Existing file failed verification, re-downloading...')" >&2
rm -f "$run_file"
fi
else
echo "Existing file invalid (size or type), removing..." >> "$LOG_FILE"
msg_warn "$(translate 'Removing invalid existing file...')" >&2
rm -f "$run_file"
fi
fi
if ! verify_version_exists "$version"; then
msg_error "Version $version does not exist on NVIDIA servers" >&2
echo "ERROR: Version $version not found on server" >> "$LOG_FILE"
return 1
fi
local urls=(
"${NVIDIA_BASE_URL}/${version}/NVIDIA-Linux-x86_64-${version}.run"
"${NVIDIA_BASE_URL}/${version}/NVIDIA-Linux-x86_64-${version}-no-compat32.run"
)
local success=false
local url_index=0
for url in "${urls[@]}"; do
((url_index++))
echo "Attempting download from: $url" >> "$LOG_FILE"
rm -f "$run_file"
if curl -fL --connect-timeout 30 --max-time 600 "$url" -o "$run_file" >> "$LOG_FILE" 2>&1; then
echo "Download completed, verifying file..." >> "$LOG_FILE"
if [[ ! -f "$run_file" ]]; then
echo "ERROR: File not created after download" >> "$LOG_FILE"
continue
fi
local file_size
file_size=$(stat -c%s "$run_file" 2>/dev/null || stat -f%z "$run_file" 2>/dev/null || echo "0")
echo "Downloaded file size: $file_size bytes" >> "$LOG_FILE"
if [[ $file_size -lt 40000000 ]]; then
echo "ERROR: File too small ($file_size bytes, expected >40MB)" >> "$LOG_FILE"
head -c 200 "$run_file" >> "$LOG_FILE" 2>&1
rm -f "$run_file"
continue
fi
local file_type
file_type=$(file "$run_file" 2>/dev/null)
echo "File type: $file_type" >> "$LOG_FILE"
if echo "$file_type" | grep -q "executable"; then
echo "SUCCESS: Valid executable downloaded" >> "$LOG_FILE"
success=true
break
else
echo "ERROR: Not a valid executable" >> "$LOG_FILE"
head -c 200 "$run_file" | od -c >> "$LOG_FILE" 2>&1
rm -f "$run_file"
fi
else
echo "ERROR: curl failed for $url (exit code: $?)" >> "$LOG_FILE"
rm -f "$run_file"
fi
done
if ! $success; then
msg_error "$(translate 'Download failed for all attempted URLs')" >&2
msg_error "Version $version may not be available for your architecture" >&2
echo "ERROR: All download attempts failed" >> "$LOG_FILE"
return 1
fi
chmod +x "$run_file"
echo "Installation file ready: $run_file" >> "$LOG_FILE"
printf '%s\n' "$run_file"
}
# ==========================================================
# Installation / uninstallation
# ==========================================================
run_nvidia_installer() {
local installer="$1"
msg_info2 "$(translate 'Starting NVIDIA installer. This may take several minutes...')"
echo "" >>"$LOG_FILE"
echo "=== Running NVIDIA installer: $installer ===" >>"$LOG_FILE"
local tmp_extract_dir="$NVIDIA_WORKDIR/tmp_extract"
mkdir -p "$tmp_extract_dir"
sh "$installer" --tmpdir="$tmp_extract_dir" --no-questions --ui=none --disable-nouveau --dkms 2>&1 | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
echo "" >>"$LOG_FILE"
rm -rf "$tmp_extract_dir"
if [[ $rc -ne 0 ]]; then
msg_error "$(translate 'NVIDIA installer reported an error. Check /tmp/nvidia_install.log')"
update_component_status "nvidia_driver" "failed" "" "gpu" '{"patched":false}'
return 1
fi
msg_ok "$(translate 'NVIDIA driver installed successfully.')" | tee -a "$screen_capture"
return 0
}
remove_nvidia_driver() {
complete_nvidia_uninstall
}
install_udev_rules_and_persistenced() {
msg_info "$(translate 'Installing NVIDIA udev rules and persistence service...')"
cat >/etc/udev/rules.d/70-nvidia.rules <<'EOF'
# /etc/udev/rules.d/70-nvidia.rules
KERNEL=="nvidia", RUN+="/bin/bash -c '/usr/bin/nvidia-smi -L'"
KERNEL=="nvidia_uvm", RUN+="/bin/bash -c '/usr/bin/nvidia-modprobe -c0 -u'"
EOF
udevadm control --reload-rules
udevadm trigger --subsystem-match=drm --subsystem-match=pci || true
ensure_workdir
cd "$NVIDIA_WORKDIR" || return 1
if [[ ! -d nvidia-persistenced ]]; then
git clone https://github.com/NVIDIA/nvidia-persistenced.git >>"$LOG_FILE" 2>&1 || true
fi
if [[ -d nvidia-persistenced/init ]]; then
cd nvidia-persistenced/init || return 1
./install.sh >>"$LOG_FILE" 2>&1 || true
fi
msg_ok "$(translate 'NVIDIA udev rules and persistence service installed.')" | tee -a "$screen_capture"
}
apply_nvidia_patch_if_needed() {
if ! whiptail --title "$(translate 'NVIDIA Patch')" --yesno \
"\n$(translate 'Do you want to apply the optional NVIDIA patch to remove some GPU limitations?')" 10 70; then
msg_info2 "$(translate 'NVIDIA patch not applied.')"
update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}'
return 0
fi
msg_info "$(translate 'Cloning and applying NVIDIA patch (keylase/nvidia-patch)...')"
ensure_workdir
cd "$NVIDIA_WORKDIR" || return 1
if [[ ! -d nvidia-patch ]]; then
git clone https://github.com/keylase/nvidia-patch.git >>"$LOG_FILE" 2>&1 || true
fi
if [[ -x nvidia-patch/patch.sh ]]; then
cd nvidia-patch || return 1
./patch.sh >>"$LOG_FILE" 2>&1 || true
msg_ok "$(translate 'NVIDIA patch applied - check README for supported versions.')"
update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":true}'
else
msg_warn "$(translate 'Could not run NVIDIA patch script. Please verify repository and driver version.')"
update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}'
fi
}
restart_prompt() {
if whiptail --title "$(translate 'NVIDIA Drivers')" --yesno \
"\n$(translate 'The installation/changes require a server restart to apply correctly. Do you want to reboot now?')" 10 70; then
msg_success "$(translate 'Installation completed. Press Enter to continue...')"
read -r
msg_warn "$(translate 'Restarting the server...')"
rm -f "$screen_capture"
reboot
else
msg_success "$(translate 'Installation completed. Please reboot the server manually as soon as possible.')"
msg_success "$(translate 'Completed. Press Enter to return to menu...')"
read -r
rm -f "$screen_capture"
fi
}
# ==========================================================
# Dialog menus
# ==========================================================
show_action_menu_if_installed() {
if ! $CURRENT_DRIVER_INSTALLED; then
ACTION="install"
return 0
fi
local menu_choices=(
"install" "$(translate 'Reinstall/Update NVIDIA drivers')"
"remove" "$(translate 'Uninstall NVIDIA drivers and configuration')"
)
ACTION=$(dialog --clear --stdout \
--backtitle "ProxMenux" \
--title "$(translate 'NVIDIA GPU Driver Management')" \
--menu "$(translate 'Choose an action:')" 14 80 8 \
"${menu_choices[@]}") || ACTION="cancel"
}
show_install_overview() {
local overview
overview="\n$(translate 'This installation will:')\n\n"
overview+="$(translate 'Install NVIDIA proprietary drivers')\n"
overview+="$(translate 'Configure GPU passthrough with VFIO')\n"
overview+="$(translate 'Blacklist nouveau driver')\n"
overview+="$(translate 'Enable IOMMU support if not enabled')\n\n"
overview+="$(translate 'Detected GPU(s):')\n"
overview+="\Zb\Z4$DETECTED_GPUS_TEXT\Zn\n"
overview+="\n\Zn$(translate 'Current status: ') "
overview+="\Zb${CURRENT_STATUS_TEXT}\Zn\n\n"
overview+="$(translate 'After confirming, you will be asked to choose the NVIDIA driver version to install.')\n\n"
overview+="$(translate 'Do you want to continue?')"
dialog --colors --backtitle "ProxMenux" \
--title "$(translate 'NVIDIA GPU Driver Installation')" \
--yesno "$overview" 22 90
}
show_version_menu() {
local latest versions_list
local kernel_version
kernel_version=$(uname -r)
latest=$(download_latest_version 2>/dev/null)
versions_list=$(list_available_versions 2>/dev/null)
if [[ -z "$latest" ]] && [[ -z "$versions_list" ]]; then
dialog --backtitle "ProxMenux" --title "$(translate 'Error')" --msgbox \
"$(translate 'Could not retrieve versions list from NVIDIA. Please check your internet connection.')\n\nURL: ${NVIDIA_BASE_URL}" 10 80
DRIVER_VERSION="cancel"
return 1
fi
if [[ -z "$latest" ]] && [[ -n "$versions_list" ]]; then
latest=$(echo "$versions_list" | head -n1)
fi
if [[ -n "$latest" ]] && [[ -z "$versions_list" ]]; then
versions_list="$latest"
fi
# Clean latest version
latest=$(echo "$latest" | tr -d '[:space:]')
local filter=""
local selection
local choices
local current_list
local menu_text
while true; do
current_list="$versions_list"
if [[ -n "$MIN_DRIVER_VERSION" ]]; then
local filtered_list=""
while IFS= read -r ver; do
[[ -z "$ver" ]] && continue
if is_version_compatible "$ver"; then
filtered_list+="$ver"$'\n'
fi
done <<< "$current_list"
current_list="$filtered_list"
fi
if [[ -n "$filter" ]]; then
current_list=$(echo "$current_list" | grep "$filter" || true)
fi
menu_text="$(translate 'Select the NVIDIA driver version to install:')\n\n"
menu_text+="$(translate 'Use the filter entry to narrow the list. Latest available (recommended in most cases), or choose a specific version from the list.')"
choices=()
choices+=("latest" "$(translate 'Latest available') (${latest:-unknown})")
choices+=("" "")
choices+=("filter" "$(translate 'Filter versions')${filter:+: $filter}")
if [[ -n "$current_list" ]]; then
while IFS= read -r ver; do
[[ -z "$ver" ]] && continue
ver=$(echo "$ver" | tr -d '[:space:]')
[[ -z "$ver" ]] && continue
choices+=("$ver" "$ver")
done <<< "$current_list"
else
choices+=("" "$(translate 'No versions match the current filter')")
fi
selection=$(dialog --clear --stdout \
--backtitle "ProxMenux" \
--title "$(translate 'NVIDIA Driver Version')" \
--menu "$menu_text" 26 90 16 \
"${choices[@]}") || { DRIVER_VERSION="cancel"; return 1; }
case "$selection" in
"")
continue
;;
filter)
filter=$(dialog --clear --stdout \
--backtitle "ProxMenux" \
--title "$(translate 'Filter NVIDIA versions')" \
--inputbox "$(translate 'Enter a filter (e.g., 560, 570, 580). Leave empty to show all.')" 10 80 "$filter") || true
;;
latest)
DRIVER_VERSION="$latest"
DRIVER_VERSION=$(echo "$DRIVER_VERSION" | tr -d '[:space:]')
return 0
;;
*)
DRIVER_VERSION="$selection"
DRIVER_VERSION=$(echo "$DRIVER_VERSION" | tr -d '[:space:]')
return 0
;;
esac
done
}
# ==========================================================
# Main flow
# ==========================================================
main() {
: >"$LOG_FILE"
: >"$screen_capture"
detect_nvidia_gpus
detect_driver_status
if ! $NVIDIA_GPU_PRESENT; then
dialog --backtitle "ProxMenux" --title "$(translate 'NVIDIA GPU Driver Installation')" --msgbox \
"\n$(translate 'No NVIDIA GPU has been detected on this system. The installer will now exit.')" 20 70
exit 1
fi
show_action_menu_if_installed
case "$ACTION" in
install)
if ! show_install_overview; then
exit 0
fi
get_kernel_compatibility_info
show_version_menu
if [[ "$DRIVER_VERSION" == "cancel" || -z "$DRIVER_VERSION" ]]; then
exit 0
fi
if $CURRENT_DRIVER_INSTALLED; then
if [[ "$CURRENT_DRIVER_VERSION" == "$DRIVER_VERSION" ]]; then
if ! dialog --colors --backtitle "ProxMenux" --title "$(translate 'Same Version Detected')" --yesno \
"$(printf '\n\n\n%s \Zb%s\Zn\n\n%s' \
"$(translate 'Version')" "$DRIVER_VERSION" \
"$(translate 'is already installed. Do you want to reinstall it? This will perform a clean uninstall first.')")" 14 70; then
exit 0
fi
else
if ! dialog --colors --backtitle "ProxMenux" --title "$(translate 'Version Change Detected')" --yesno \
"$(printf '\n\n%s \Zb%s\Zn\n%s \Zb\Z4%s\Zn\n\n%s' \
"$(translate 'Current version:')" "$CURRENT_DRIVER_VERSION" \
"$(translate 'New version:')" "$DRIVER_VERSION" \
"$(translate 'The current driver will be completely uninstalled before installing the new version. Continue?')")" 20 70; then
exit 0
fi
fi
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
msg_info2 "$(translate 'Uninstalling current NVIDIA driver before installing new version...')"
complete_nvidia_uninstall
sleep 2
CURRENT_DRIVER_INSTALLED=false
CURRENT_DRIVER_VERSION=""
fi
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
ensure_repos_and_headers
blacklist_nouveau
ensure_modules_config
stop_and_disable_nvidia_services
unload_nvidia_modules
msg_info "$(translate 'Downloading NVIDIA driver version:') $DRIVER_VERSION"
local installer
installer=$(download_nvidia_installer "$DRIVER_VERSION" 2>>"$LOG_FILE")
local download_result=$?
if [[ $download_result -ne 0 ]]; then
msg_error "$(translate 'Failed to download NVIDIA installer')"
exit 1
fi
msg_ok "$(translate 'NVIDIA installer downloaded successfully')"
if [[ -z "$installer" || ! -f "$installer" ]]; then
msg_error "$(translate 'Internal error: NVIDIA installer path is empty or file not found.')"
rm -f "$screen_capture"
exit 1
fi
if ! run_nvidia_installer "$installer"; then
rm -f "$screen_capture"
exit 1
fi
sleep 2
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
cat "$screen_capture"
echo -e "${TAB}${GN}📄 $(translate "Log file")${CL}: ${BL}$LOG_FILE${CL}"
install_udev_rules_and_persistenced
msg_info "$(translate 'Updating initramfs for all kernels...')"
update-initramfs -u -k all >>"$LOG_FILE" 2>&1 || true
msg_ok "$(translate 'initramfs updated.')"
msg_info2 "$(translate 'Checking NVIDIA driver status with nvidia-smi')"
if command -v nvidia-smi >/dev/null 2>&1; then
nvidia-smi || true
CURRENT_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1)
CURRENT_DRIVER_INSTALLED=true
else
msg_warn "$(translate 'nvidia-smi not found in PATH. Please verify the driver installation.')"
fi
if [[ -n "$CURRENT_DRIVER_VERSION" ]]; then
msg_ok "$(translate 'NVIDIA driver') $CURRENT_DRIVER_VERSION $(translate 'installed successfully.')"
update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}'
msg_success "$(translate 'Driver installed successfully. Press Enter to continue...')"
read -r
else
msg_error "$(translate 'Failed to detect installed NVIDIA driver version.')"
update_component_status "nvidia_driver" "failed" "" "gpu" '{"patched":false}'
fi
apply_nvidia_patch_if_needed
restart_prompt
;;
remove)
if dialog --backtitle "ProxMenux" --title "$(translate 'NVIDIA Driver Uninstall')" --yesno \
"\n\n\n$(translate 'This will remove NVIDIA drivers and related configuration. Do you want to continue?')" 14 70; then
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
remove_nvidia_driver
msg_info "$(translate 'Updating initramfs for all kernels...')"
update-initramfs -u -k all >>"$LOG_FILE" 2>&1 || true
msg_ok "$(translate 'initramfs updated.')"
restart_prompt
fi
;;
cancel|*)
exit 0
;;
esac
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main
fi
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 29/05/2025
# ==========================================================
+1 -1
View File
@@ -6,7 +6,7 @@
# Author : MacRimi
# Revision : @Blaspt (USB passthrough via udev rule with persistent /dev/coral)
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 16/05/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 19/08/2025
# ==========================================================
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 19/08/2025
# ==========================================================
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 2.0
# Last Updated: 19/08/2025
# ==========================================================
+2 -2
View File
@@ -5,7 +5,7 @@
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# Contributors : cod378
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 04/07/2025
# ==========================================================
@@ -342,7 +342,7 @@ show_version_info() {
fi
local translated_status=$(translate "$status")
case "$status" in
"installed"|"already_installed"|"created"|"already_exists"|"upgraded")
"installed"|"already_installed"|"created"|"already_exists"|"upgraded"|"updated")
info_message+="$component: $translated_status\n"
;;
*)
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 07/05/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 19/08/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 2.0
# Last Updated: 04/04/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 2.0
# Last Updated: 04/04/2025
# ==========================================================
+151 -99
View File
@@ -5,9 +5,9 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.2
# Last Updated: 14/11/2025
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.3
# Last Updated: 14/03/2025
# ==========================================================
# Description:
# This script provides a simple and efficient way to access and execute Proxmox VE scripts
@@ -33,8 +33,9 @@ load_language
initialize_cache
# ==========================================================
# New unified cache — categories and mirror URLs are embedded,
# metadata.json is no longer needed.
HELPERS_JSON_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/refs/heads/main/json/helpers_cache.json"
METADATA_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/frontend/public/json/metadata.json"
for cmd in curl jq dialog; do
if ! command -v "$cmd" >/dev/null; then
@@ -44,63 +45,78 @@ for cmd in curl jq dialog; do
done
CACHE_JSON=$(curl -s "$HELPERS_JSON_URL")
META_JSON=$(curl -s "$METADATA_URL")
# Validate that the JSON loaded correctly
if ! echo "$CACHE_JSON" | jq -e 'if type == "array" and length > 0 then true else false end' >/dev/null 2>&1; then
dialog --title "Helper Scripts" \
--msgbox "Error: Could not load helpers cache.\nCheck your internet connection and try again.\n\nURL: $HELPERS_JSON_URL" 10 70
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
fi
# ---------------------------------------------------------------------------
# Build category map directly from the cache (id → name).
# Uses transpose to pair categories[] and category_names[] arrays — no
# dependency on metadata.json, which no longer exists upstream.
# ---------------------------------------------------------------------------
declare -A CATEGORY_NAMES
while read -r id name; do
CATEGORY_NAMES[$id]="$name"
done < <(echo "$META_JSON" | jq -r '.categories[] | "\(.id)\t\(.name)"')
while IFS=$'\t' read -r id name; do
[[ -n "$id" && -n "$name" ]] && CATEGORY_NAMES["$id"]="$name"
done < <(echo "$CACHE_JSON" | jq -r '
[.[] | [.categories, .category_names] | transpose[] | @tsv]
| unique[]')
# Count scripts per category (deduplicated by slug)
declare -A CATEGORY_COUNT
for id in $(echo "$CACHE_JSON" | jq -r '
group_by(.slug) | map(.[0])[] | .categories[]'); do
while read -r id; do
((CATEGORY_COUNT[$id]++))
done
done < <(echo "$CACHE_JSON" | jq -r '
group_by(.slug) | map(.[0])[] | .categories[]')
# ---------------------------------------------------------------------------
# Type label — updated to match new type values (lxc instead of ct)
# ---------------------------------------------------------------------------
get_type_label() {
local type="$1"
case "$type" in
ct) echo $'\Z1LXC\Zn' ;;
vm) echo $'\Z4VM\Zn' ;;
pve) echo $'\Z3PVE\Zn' ;;
addon) echo $'\Z2ADDON\Zn' ;;
*) echo $'\Z7GEN\Zn' ;;
lxc) echo $'\Z1LXC\Zn' ;;
vm) echo $'\Z4VM\Zn' ;;
pve) echo $'\Z3PVE\Zn' ;;
addon) echo $'\Z2ADDON\Zn' ;;
turnkey) echo $'\Z5TK\Zn' ;;
*) echo $'\Z7GEN\Zn' ;;
esac
}
# ---------------------------------------------------------------------------
# Download and execute a script URL, with optional mirror fallback
# ---------------------------------------------------------------------------
download_script() {
local url="$1"
local fallback_pve="${url/misc\/tools\/pve}"
local fallback_addon="${url/misc\/tools\/addon}"
local fallback_copydata="${url/misc\/tools\/copy-data}"
if curl --silent --head --fail "$url" >/dev/null; then
bash <(curl -s "$url")
elif curl --silent --head --fail "$fallback_pve" >/dev/null; then
bash <(curl -s "$fallback_pve")
elif curl --silent --head --fail "$fallback_addon" >/dev/null; then
bash <(curl -s "$fallback_addon")
elif curl --silent --head --fail "$fallback_copydata" >/dev/null; then
bash <(curl -s "$fallback_copydata")
bash <(curl -s "$url")
else
dialog --title "Helper Scripts" --msgbox "Error: Failed to download the script." 12 70
dialog --title "Helper Scripts" --msgbox "$(translate "Error: Failed to download the script.")" 8 70
fi
}
RETURN_TO_MAIN=false
# ---------------------------------------------------------------------------
# Format default credentials for display
# ---------------------------------------------------------------------------
format_credentials() {
local script_info="$1"
local credentials_info=""
local has_credentials
has_credentials=$(echo "$script_info" | base64 --decode | jq -r 'has("default_credentials")')
if [[ "$has_credentials" == "true" ]]; then
local username password
username=$(echo "$script_info" | base64 --decode | jq -r '.default_credentials.username // empty')
password=$(echo "$script_info" | base64 --decode | jq -r '.default_credentials.password // empty')
if [[ -n "$username" && -n "$password" ]]; then
credentials_info="Username: $username | Password: $password"
elif [[ -n "$username" ]]; then
@@ -109,30 +125,41 @@ format_credentials() {
credentials_info="Password: $password"
fi
fi
echo "$credentials_info"
}
# ---------------------------------------------------------------------------
# Run a script identified by its slug.
#
# A slug can have multiple entries when a script supports several OS variants
# (e.g. Debian + Alpine). Each entry carries its own script_url / mirror and
# the os field already normalised to lowercase by generate_helpers_cache.py.
# The menu lets the user pick OS variant × source (GitHub / Mirror).
# ---------------------------------------------------------------------------
run_script_by_slug() {
local slug="$1"
local -a script_infos
mapfile -t script_infos < <(echo "$CACHE_JSON" | jq -r --arg slug "$slug" '.[] | select(.slug == $slug) | @base64')
mapfile -t script_infos < <(echo "$CACHE_JSON" | jq -r --arg slug "$slug" \
'.[] | select(.slug == $slug) | @base64')
if [[ ${#script_infos[@]} -eq 0 ]]; then
dialog --title "Helper Scripts" --msgbox "Error: No script data found for slug: $slug" 8 60
dialog --title "Helper Scripts" \
--msgbox "$(translate "Error: No script data found for slug:") $slug" 8 60
return
fi
decode() {
echo "$1" | base64 --decode | jq -r "$2"
}
decode() { echo "$1" | base64 --decode | jq -r "$2"; }
local first="${script_infos[0]}"
local name desc notes
local name desc notes port website
name=$(decode "$first" ".name")
desc=$(decode "$first" ".desc")
notes=$(decode "$first" ".notes | join(\"\n\")")
notes=$(decode "$first" '.notes | join("\n")')
port=$(decode "$first" ".port // 0")
website=$(decode "$first" ".website // empty")
# Build notes block
local notes_dialog=""
if [[ -n "$notes" ]]; then
while IFS= read -r line; do
@@ -145,18 +172,21 @@ run_script_by_slug() {
local credentials
credentials=$(format_credentials "$first")
local msg="\Zb\Z4Descripción:\Zn\n$desc"
[[ -n "$notes_dialog" ]] && msg+="\n\n\Zb\Z4Notes:\Zn\n$notes_dialog"
[[ -n "$credentials" ]] && msg+="\n\n\Zb\Z4Default Credentials:\Zn\n$credentials"
# Add separator before menu options
# Build info message
local msg="\Zb\Z4$(translate "Description"):\Zn\n$desc"
[[ -n "$notes_dialog" ]] && msg+="\n\n\Zb\Z4$(translate "Notes"):\Zn\n$notes_dialog"
[[ -n "$credentials" ]] && msg+="\n\n\Zb\Z4$(translate "Default Credentials"):\Zn\n$credentials"
[[ "$port" -gt 0 ]] && msg+="\n\n\Zb\Z4$(translate "Default Port"):\Zn $port"
[[ -n "$website" ]] && msg+="\n\Zb\Z4$(translate "Website"):\Zn $website"
msg+="\n\n$(translate "Choose how to run the script:"):"
# Build menu: one or two entries per script_info (GH + optional Mirror)
declare -a MENU_OPTS=()
local idx=0
for s in "${script_infos[@]}"; do
local os script_url script_url_mirror script_name
os=$(decode "$s" ".os // empty")
os=$(decode "$s" '.os // empty')
[[ -z "$os" ]] && os="$(translate "default")"
script_name=$(decode "$s" ".name")
script_url=$(decode "$s" ".script_url")
@@ -196,7 +226,8 @@ run_script_by_slug() {
if [[ -n "$mirror_url" ]]; then
download_script "$mirror_url"
else
dialog --title "Helper Scripts" --msgbox "$(translate "Mirror URL not available for this script.")" 8 60
dialog --title "Helper Scripts" \
--msgbox "$(translate "Mirror URL not available for this script.")" 8 60
RETURN_TO_MAIN=false
return
fi
@@ -206,10 +237,10 @@ run_script_by_slug() {
echo
if [[ -n "$desc" || -n "$notes" || -n "$credentials" ]]; then
echo -e "$TAB\e[1;36mScript Information:\e[0m"
echo -e "$TAB\e[1;36m$(translate "Script Information"):\e[0m"
if [[ -n "$notes" ]]; then
echo -e "$TAB\e[1;33mNotes:\e[0m"
echo -e "$TAB\e[1;33m$(translate "Notes"):\e[0m"
while IFS= read -r line; do
[[ -z "$line" ]] && continue
echo -e "$TAB$line"
@@ -218,26 +249,30 @@ run_script_by_slug() {
fi
if [[ -n "$credentials" ]]; then
echo -e "$TAB\e[1;32mDefault Credentials:\e[0m"
echo -e "$TAB\e[1;32m$(translate "Default Credentials"):\e[0m"
echo "$TAB$credentials"
echo
fi
fi
msg_success "Press Enter to return to the main menu..."
msg_success "$(translate "Press Enter to return to the main menu...")"
read -r
RETURN_TO_MAIN=true
}
# ---------------------------------------------------------------------------
# Search / filter scripts by name or description
# ---------------------------------------------------------------------------
search_and_filter_scripts() {
local search_term=""
while true; do
search_term=$(dialog --inputbox "Enter search term (leave empty to show all scripts):" \
8 65 "$search_term" 3>&1 1>&2 2>&3)
search_term=$(dialog --inputbox \
"$(translate "Enter search term (leave empty to show all scripts):"):" \
8 65 "$search_term" 3>&1 1>&2 2>&3)
[[ $? -ne 0 ]] && return
local filtered_json
if [[ -z "$search_term" ]]; then
filtered_json="$CACHE_JSON"
@@ -250,12 +285,14 @@ search_and_filter_scripts() {
(.desc | ascii_downcase | contains($term))
)]')
fi
local count
count=$(echo "$filtered_json" | jq 'group_by(.slug) | length')
if [[ $count -eq 0 ]]; then
dialog --msgbox "No scripts found for: '$search_term'\n\nTry a different search term." 8 50
if [[ "$count" -eq 0 ]]; then
dialog --msgbox \
"$(translate "No scripts found for:") '$search_term'\n\n$(translate "Try a different search term.")" \
8 50
continue
fi
@@ -263,43 +300,41 @@ search_and_filter_scripts() {
declare -A index_to_slug
local menu_items=()
local i=1
while IFS=$'\t' read -r slug name type; do
index_to_slug[$i]="$slug"
local label
label=$(get_type_label "$type")
local padded_name
padded_name=$(printf "%-42s" "$name")
local entry="$padded_name $label"
menu_items+=("$i" "$entry")
menu_items+=("$i" "$padded_name $label")
((i++))
done < <(echo "$filtered_json" | jq -r '
group_by(.slug) | map(.[0]) | sort_by(.name)[] | [.slug, .name, .type] | @tsv')
group_by(.slug) | map(.[0]) | sort_by(.name)[]
| [.slug, .name, .type] | @tsv')
menu_items+=("" "")
menu_items+=("new_search" "New Search")
menu_items+=("show_all" "Show All Scripts")
local title="Search Results"
menu_items+=("new_search" "$(translate "New Search")")
menu_items+=("show_all" "$(translate "Show All Scripts")")
local title
if [[ -n "$search_term" ]]; then
title="Search Results for: '$search_term' ($count found)"
title="$(translate "Search Results for:") '$search_term' ($count $(translate "found"))"
else
title="All Available Scripts ($count total)"
title="$(translate "All Available Scripts") ($count $(translate "total"))"
fi
local selected
selected=$(dialog --colors --backtitle "ProxMenux" \
--title "$title" \
--menu "Select a script or action:" \
--menu "$(translate "Select a script or action:"):" \
22 75 15 "${menu_items[@]}" 3>&1 1>&2 2>&3)
if [[ $? -ne 0 ]]; then
return
fi
[[ $? -ne 0 ]] && return
case "$selected" in
"new_search")
break
break
;;
"show_all")
search_term=""
@@ -308,7 +343,7 @@ search_and_filter_scripts() {
continue
;;
"back"|"")
return
return
;;
*)
if [[ -n "${index_to_slug[$selected]}" ]]; then
@@ -321,48 +356,64 @@ search_and_filter_scripts() {
done
}
# ---------------------------------------------------------------------------
# Main loop — category list built from embedded category data.
# We map scriptcatXXXXX IDs to short numeric indices so dialog doesn't show
# the long ID string as the visible tag in the menu column.
# ---------------------------------------------------------------------------
while true; do
MENU_ITEMS=()
MENU_ITEMS+=("search" "Search/Filter Scripts")
MENU_ITEMS+=("search" "$(translate "Search/Filter Scripts")")
MENU_ITEMS+=("" "")
for id in $(printf "%s\n" "${!CATEGORY_COUNT[@]}" | sort -n); do
# Map scriptcatXXXXX IDs to short numeric indices (1, 2, 3…) so dialog
# doesn't render the long ID string as the visible tag column.
declare -A CAT_IDX_TO_ID
local_idx=1
for id in $(printf "%s\n" "${!CATEGORY_COUNT[@]}" | sort); do
CAT_IDX_TO_ID[$local_idx]="$id"
name="${CATEGORY_NAMES[$id]:-Category $id}"
count="${CATEGORY_COUNT[$id]}"
padded_name=$(printf "%-35s" "$name")
padded_count=$(printf "(%2d)" "$count")
MENU_ITEMS+=("$id" "$padded_name $padded_count")
MENU_ITEMS+=("$local_idx" "$padded_name $padded_count")
((local_idx++))
done
SELECTED=$(dialog --backtitle "ProxMenux" --title "Proxmox VE Helper-Scripts" --menu \
"Select a category or search for scripts:" 20 70 14 \
"${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
dialog --clear --title "ProxMenux" \
--msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70
SELECTED_IDX=$(dialog --backtitle "ProxMenux" \
--title "Proxmox VE Helper-Scripts" \
--menu "$(translate "Select a category or search for scripts:"):" \
20 70 14 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
dialog --clear --title "ProxMenux" \
--msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
}
if [[ "$SELECTED" == "search" ]]; then
if [[ "$SELECTED_IDX" == "search" ]]; then
search_and_filter_scripts
continue
fi
# Resolve numeric index back to the real category ID
SELECTED="${CAT_IDX_TO_ID[$SELECTED_IDX]}"
[[ -z "$SELECTED" ]] && continue
# ---- Scripts within the selected category --------------------------------
while true; do
declare -A INDEX_TO_SLUG
SCRIPTS=()
i=1
while IFS=$'\t' read -r slug name type; do
INDEX_TO_SLUG[$i]="$slug"
label=$(get_type_label "$type")
padded_name=$(printf "%-42s" "$name")
entry="$padded_name $label"
SCRIPTS+=("$i" "$entry")
SCRIPTS+=("$i" "$padded_name $label")
((i++))
done < <(echo "$CACHE_JSON" | jq -r --argjson id "$SELECTED" '
done < <(echo "$CACHE_JSON" | jq -r --arg id "$SELECTED" '
[
.[]
| select(.categories | index($id))
.[]
| select(.categories | index($id))
| {slug, name, type}
]
| group_by(.slug)
@@ -371,13 +422,14 @@ while true; do
| [.slug, .name, .type]
| @tsv')
SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" --title "Scripts in ${CATEGORY_NAMES[$SELECTED]}" --menu \
"Choose a script to execute:" 20 70 14 \
"${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break
SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" \
--title "$(translate "Scripts in") ${CATEGORY_NAMES[$SELECTED]}" \
--menu "$(translate "Choose a script to execute:"):" \
20 70 14 "${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break
SCRIPT_SELECTED="${INDEX_TO_SLUG[$SCRIPT_INDEX]}"
run_script_by_slug "$SCRIPT_SELECTED"
[[ "$RETURN_TO_MAIN" == true ]] && { RETURN_TO_MAIN=false; break; }
done
done
+1 -1
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.2
# Last Updated: 06/07/2025
# ==========================================================
+1 -1
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 08/07/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 15/04/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 02/07/2025
# ==========================================================
+1 -1
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 06/07/2025
# ==========================================================
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.3
# Last Updated: 30/06/2025
# ==========================================================
+1 -1
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 06/07/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.5
# Last Updated: 04/08/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
+1 -1
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.2
# Last Updated: 30/07/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 29/05/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 08/04/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# Description : Allows unmounting a previously mounted disk
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 07/05/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 04/06/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 13/03/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 13/03/2025
# ==========================================================
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 13/08/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 04/07/2025
# ==========================================================
+1 -1
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 14/08/2025
# ==========================================================
+1 -1
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 30/07/2025
# ==========================================================
+1 -1
View File
@@ -4,7 +4,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 14/08/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 30/06/2025
# ==========================================================
+231 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# ==========================================================
@@ -42,6 +42,7 @@ CACHE_FILE="$BASE_DIR/cache.json"
LOCAL_VERSION_FILE="$BASE_DIR/version.txt"
MENU_SCRIPT="menu"
VENV_PATH="/opt/googletrans-env"
COMPONENTS_STATUS_FILE="$BASE_DIR/components_status.json"
# Translation context
@@ -112,6 +113,16 @@ cleanup() {
fi
}
stop_spinner() {
if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null 2>&1; then
kill $SPINNER_PID > /dev/null 2>&1
wait $SPINNER_PID 2>/dev/null
fi
printf "\r\033[K"
printf "\e[?25h"
SPINNER_PID=""
}
# Display trnaslate message with spinner
msg_lang() {
local msg="$1"
@@ -398,3 +409,222 @@ echo -e
fi
}
########################################################
ensure_components_status_file() {
mkdir -p "$BASE_DIR"
if [[ ! -f "$COMPONENTS_STATUS_FILE" ]] || ! jq empty "$COMPONENTS_STATUS_FILE" >/dev/null 2>&1; then
echo '{}' > "$COMPONENTS_STATUS_FILE"
fi
}
update_component_status() {
local comp="$1"
local stat="$2"
local ver="$3"
local category="$4"
local extra_json="$5"
if [ -z "$extra_json" ]; then
extra_json="{}"
fi
ensure_components_status_file
local ts
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local tmp_file
tmp_file=$(mktemp)
if jq --arg comp "$comp" \
--arg stat "$stat" \
--arg ver "$ver" \
--arg category "$category" \
--arg time "$ts" \
--argjson extra "$extra_json" \
'.[$comp] = ({status:$stat, version:$ver, category:$category, timestamp:$time} + $extra)' \
"$COMPONENTS_STATUS_FILE" > "$tmp_file" 2>/dev/null; then
mv "$tmp_file" "$COMPONENTS_STATUS_FILE"
else
rm -f "$tmp_file"
echo '{}' > "$COMPONENTS_STATUS_FILE"
fi
}
# ============================================
# Hybrid Dialog Functions (Web/Terminal)
# ============================================
# Detect if running in web mode
is_web_mode() {
[[ "$EXECUTION_MODE" == "web" ]]
}
# Generate unique interaction ID
generate_interaction_id() {
echo "$(date +%s%N)_$$"
}
# Wait for web response with timeout
wait_for_web_response() {
local interaction_id="$1"
local response_file="/tmp/proxmenux_response_${interaction_id}"
local timeout=300 # 5 minutes
local elapsed=0
while [[ ! -f "$response_file" ]] && [[ $elapsed -lt $timeout ]]; do
sleep 0.1
elapsed=$((elapsed + 1))
done
if [[ -f "$response_file" ]]; then
cat "$response_file"
rm -f "$response_file"
return 0
else
echo ""
return 1
fi
}
# Hybrid menu function
hybrid_menu() {
local title="$1"
local text="$2"
local height="${3:-20}"
local width="${4:-70}"
local menu_height="${5:-10}"
shift 5
local items=("$@")
if is_web_mode; then
local interaction_id=$(generate_interaction_id)
local clean_text=$(echo -e "$text" | sed 's/\\Z[0-9bn]//g')
local options_json="["
for ((i=0; i<${#items[@]}; i+=2)); do
if [ $i -gt 0 ]; then options_json+=","; fi
options_json+="{\"value\":\"${items[i]}\",\"label\":\"${items[i+1]}\"}"
done
options_json+="]"
echo "WEB_INTERACTION:menu:${interaction_id}:$(echo -n "$title" | base64 -w0):$(echo -n "$clean_text" | base64 -w0):$options_json" >> "${WEB_LOG:-/tmp/proxmenux_web.log}"
wait_for_web_response "$interaction_id"
else
dialog --colors --title "$title" --menu "$text" "$height" "$width" "$menu_height" "${items[@]}" 3>&1 1>&2 2>&3
fi
}
# Hybrid yes/no prompt
hybrid_yesno() {
local title="$1"
local text="$2"
local height="${3:-10}"
local width="${4:-60}"
if is_web_mode; then
local interaction_id=$(generate_interaction_id)
local clean_text=$(echo -e "$text" | sed 's/\\Z[0-9bn]//g')
echo "WEB_INTERACTION:yesno:${interaction_id}:$(echo -n "$title" | base64 -w0):$(echo -n "$clean_text" | base64 -w0)" >> "${WEB_LOG:-/tmp/proxmenux_web.log}"
local response=$(wait_for_web_response "$interaction_id")
[[ "$response" == "yes" ]] && return 0 || return 1
else
dialog --colors --title "$title" --yesno "$text" "$height" "$width"
fi
}
# Hybrid message box
hybrid_msgbox() {
local title="$1"
local text="$2"
local height="${3:-10}"
local width="${4:-60}"
if is_web_mode; then
local interaction_id=$(generate_interaction_id)
local clean_text=$(echo -e "$text" | sed 's/\\Z[0-9bn]//g')
echo "WEB_INTERACTION:msgbox:${interaction_id}:$(echo -n "$title" | base64 -w0):$(echo -n "$clean_text" | base64 -w0)" >> "${WEB_LOG:-/tmp/proxmenux_web.log}"
wait_for_web_response "$interaction_id" > /dev/null
else
dialog --colors --title "$title" --msgbox "$text" "$height" "$width"
fi
}
# Hybrid input box
hybrid_inputbox() {
local title="$1"
local text="$2"
local height="${3:-10}"
local width="${4:-60}"
local default="${5:-}"
if is_web_mode; then
local interaction_id=$(generate_interaction_id)
echo "WEB_INTERACTION:inputbox:${interaction_id}:$(echo -n "$title" | base64 -w0):$(echo -n "$text" | base64 -w0):$(echo -n "$default" | base64 -w0)" >> "${WEB_LOG:-/tmp/proxmenux_web.log}"
wait_for_web_response "$interaction_id"
else
dialog --title "$title" --inputbox "$text" "$height" "$width" "$default" 3>&1 1>&2 2>&3
fi
}
# Hybrid whiptail menu (used during installation - doesn't hide terminal output)
hybrid_whiptail_menu() {
local title="$1"
local text="$2"
local height="${3:-20}"
local width="${4:-70}"
local menu_height="${5:-10}"
shift 5
local items=("$@")
if is_web_mode; then
local interaction_id=$(generate_interaction_id)
local options_json="["
for ((i=0; i<${#items[@]}; i+=2)); do
if [ $i -gt 0 ]; then options_json+=","; fi
options_json+="{\"value\":\"${items[i]}\",\"label\":\"${items[i+1]}\"}"
done
options_json+="]"
echo "WEB_INTERACTION:menu:${interaction_id}:$(echo -n "$title" | base64 -w0):$(echo -n "$text" | base64 -w0):$options_json" >> "${WEB_LOG:-/tmp/proxmenux_web.log}"
wait_for_web_response "$interaction_id"
else
whiptail --title "$title" --menu "$text" "$height" "$width" "$menu_height" "${items[@]}" 3>&1 1>&2 2>&3
fi
}
# Hybrid whiptail yes/no (used during installation)
hybrid_whiptail_yesno() {
local title="$1"
local text="$2"
local height="${3:-10}"
local width="${4:-70}"
if is_web_mode; then
local interaction_id=$(generate_interaction_id)
echo "WEB_INTERACTION:yesno:${interaction_id}:$(echo -n "$title" | base64 -w0):$(echo -n "$text" | base64 -w0)" >> "${WEB_LOG:-/tmp/proxmenux_web.log}"
local response=$(wait_for_web_response "$interaction_id")
[[ "$response" == "yes" ]] && return 0 || return 1
else
whiptail --title "$title" --yesno "$text" "$height" "$width"
fi
}
# Hybrid whiptail message box (used during installation)
hybrid_whiptail_msgbox() {
local title="$1"
local text="$2"
local height="${3:-10}"
local width="${4:-70}"
if is_web_mode; then
local interaction_id=$(generate_interaction_id)
echo "WEB_INTERACTION:msgbox:${interaction_id}:$(echo -n "$title" | base64 -w0):$(echo -n "$text" | base64 -w0)" >> "${WEB_LOG:-/tmp/proxmenux_web.log}"
wait_for_web_response "$interaction_id" > /dev/null
else
whiptail --title "$title" --msgbox "$text" "$height" "$width"
fi
}
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 07/05/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 07/05/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 07/05/2025
# ==========================================================
+5 -5
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 07/05/2025
# ==========================================================
@@ -41,7 +41,7 @@ function select_nas_iso() {
local NAS_OPTIONS=(
"1" "Synology DSM VM (Loader Linux-based)"
"2" "TrueNAS SCALE VM (Fangtooth)"
"2" "TrueNAS SCALE VM (Goldeye)"
"3" "TrueNAS CORE VM (FreeBSD based)"
"4" "OpenMediaVault VM (Debian based)"
"5" "XigmaNAS VM (FreeBSD based)"
@@ -68,9 +68,9 @@ function select_nas_iso() {
return 1
;;
2)
ISO_NAME="TrueNAS SCALE 25 (Fangtooth)"
ISO_URL="https://download.truenas.com/TrueNAS-SCALE-Fangtooth/25.04.0/TrueNAS-SCALE-25.04.0.iso"
ISO_FILE="TrueNAS-SCALE-25.04.0.iso"
ISO_NAME="TrueNAS SCALE 25 (Goldeye)"
ISO_URL="https://download.sys.truenas.net/TrueNAS-SCALE-Goldeye/25.10.0.1/TrueNAS-SCALE-25.10.0.1.iso"
ISO_FILE="TrueNAS-SCALE-25.10.0.1.iso"
ISO_PATH="$ISO_DIR/$ISO_FILE"
HN="TrueNAS-Scale"
;;
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 07/05/2025
# ==========================================================
+1 -1
View File
@@ -5,7 +5,7 @@
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 13/03/2025
# ==========================================================

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