Compare commits
795 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62b200c5d9 | |||
| c2ed772f34 | |||
| bbce6d4ad0 | |||
| 45f6a0ec02 | |||
| 6b681278e0 | |||
| 2281ff06c7 | |||
| 572a81fd4e | |||
| 5fd7df69fd | |||
| 3401c6305e | |||
| 269f9ac52c | |||
| 4712171d43 | |||
| 632c7b91f4 | |||
| f72dd79dff | |||
| 4d109c0481 | |||
| c046b77223 | |||
| 56ba3b5e5f | |||
| fa99247cb7 | |||
| 653fd37c08 | |||
| 9d053beafb | |||
| f33c451a19 | |||
| 9543148887 | |||
| 029ee4ed2f | |||
| 688e826e9d | |||
| deea0c54d4 | |||
| 028f62aa9c | |||
| d3aef9c1d1 | |||
| 72edce511a | |||
| 37fade8f7a | |||
| 0d2aa6738c | |||
| e3e0e5cba8 | |||
| e40de189c3 | |||
| c9c99f7b2a | |||
| e6400918c8 | |||
| d08557ad0e | |||
| ba84dce7a7 | |||
| 7044725bf1 | |||
| 1812966fe6 | |||
| a9bac25c9b | |||
| 5c967f11f0 | |||
| 1b5f080495 | |||
| 50a27fa3f6 | |||
| f8ed53c1b9 | |||
| 2163830a54 | |||
| cae5e3b99f | |||
| cc0a7941ea | |||
| 18901c0e2d | |||
| d4ea239185 | |||
| 813c6aab13 | |||
| f606e131a7 | |||
| ccefa61b3d | |||
| 606cae411f | |||
| 901e4012cc | |||
| d03b667194 | |||
| d30954167e | |||
| 0ee514ea15 | |||
| 7e60792be8 | |||
| 244a325394 | |||
| 53df16a7ca | |||
| 420576da09 | |||
| d5a9d8ffdb | |||
| 1873ad1a02 | |||
| 9dec238f41 | |||
| b93a018dc1 | |||
| 6b1d5bf7db | |||
| 4580866281 | |||
| 5b743772ac | |||
| 0ecf08e8e6 | |||
| 7ca53d30b2 | |||
| 18a0ba1981 | |||
| 11a35ed589 | |||
| eb5aa12d7f | |||
| 1065b67073 | |||
| 0d7b278003 | |||
| 05093a9d49 | |||
| 87f9b2b72c | |||
| 2d4833d199 | |||
| 610e08e690 | |||
| 1192224c15 | |||
| 4c3a9928e7 | |||
| 433a4359e6 | |||
| f1854b5120 | |||
| 6961b0c2f5 | |||
| 8b26f30e37 | |||
| 071724949f | |||
| 28898aa1db | |||
| 11fae19e33 | |||
| 13b9dd0262 | |||
| b47520c938 | |||
| f6869b9e1c | |||
| 96046a5d1f | |||
| 524d0b278b | |||
| 6e2348eb06 | |||
| e4b57e6ca3 | |||
| 9640e558cd | |||
| 07b13d1374 | |||
| 7e4389abd9 | |||
| 0f424e7f0d | |||
| 455e5735ff | |||
| 6577d2ae3c | |||
| 56ed543dfb | |||
| 5549e3a398 | |||
| 2ff7c111af | |||
| a7af072ca7 | |||
| a4a455f31e | |||
| 99d55b4314 | |||
| 811e2155a6 | |||
| ab7a49351d | |||
| efdfba0575 | |||
| af9b5f6ca4 | |||
| 65144b9a3d | |||
| 621f57d702 | |||
| a0444fbeee | |||
| cff81eea14 | |||
| 5738c90721 | |||
| a229231c0c | |||
| 6bf5bd97b5 | |||
| 35c50a7c60 | |||
| 042e6584eb | |||
| bcce1b7ea8 | |||
| 73181f9e33 | |||
| b0a7b6c7cd | |||
| 09744818dc | |||
| f93b3109b9 | |||
| 48d4836f0a | |||
| 5d4f70e943 | |||
| 9e05197a9a | |||
| 11671e884d | |||
| dcce818678 | |||
| f6c23bc9a0 | |||
| 15eca895fb | |||
| d5d5dd7855 | |||
| c79f5fd8a5 | |||
| 409e40f3b7 | |||
| 67a83cb164 | |||
| 908bdc7c86 | |||
| b1c2bd3d64 | |||
| ffd317aff0 | |||
| 84a10afea1 | |||
| 32036ef64d | |||
| a8b8036311 | |||
| b813716f7c | |||
| 7bd6061a59 | |||
| 7682a6e708 | |||
| fe9c592107 | |||
| 9ff24dc446 | |||
| 3036711fb4 | |||
| 53363f293b | |||
| e9791984ee | |||
| ddca96a60e | |||
| be3607dd4d | |||
| 6000a7a60f | |||
| cc64c9f9d8 | |||
| 5c699d956c | |||
| e1757e5ac5 | |||
| 31167234be | |||
| 807638ca04 | |||
| ff6b78252c | |||
| 93bdcaab7f | |||
| d06c580bbc | |||
| a5b32b356c | |||
| d117e666fd | |||
| 09da94b2ab | |||
| 9ebf5919a2 | |||
| 9c46452a4d | |||
| b34536491b | |||
| a7c1e240c1 | |||
| 44618d3d73 | |||
| 1f92af64f0 | |||
| 5bcd081e88 | |||
| b141622e75 | |||
| f96bdee71d | |||
| 0af70d3298 | |||
| e044c59627 | |||
| cce1c902e5 | |||
| d845474644 | |||
| cd67aba2ad | |||
| 1e47162357 | |||
| 230400846f | |||
| ebb29ad04b | |||
| 2cd603357d | |||
| bee26838e1 | |||
| 5b0572879d | |||
| 6e86275dce | |||
| 03a007b9b6 | |||
| dadb215ce0 | |||
| a1e3e12c6b | |||
| 4274c817d3 | |||
| 5abedc15dc | |||
| 947c9639e8 | |||
| c4cdf4a834 | |||
| 44b9bfee68 | |||
| ac9d43892b | |||
| 9f62f8eff9 | |||
| 13dd400795 | |||
| 37343a4114 | |||
| 3df6a4048a | |||
| c9fb87b571 | |||
| 44ca1d507d | |||
| 30b236548a | |||
| 21b7d1c3fb | |||
| 8d5ea66ecc | |||
| b5ed10689d | |||
| a55cdfd7fa | |||
| 39a4c10ac9 | |||
| c542cd4d7d | |||
| 01de338a65 | |||
| f23f7b1983 | |||
| a349ab62ec | |||
| e620010f10 | |||
| 70509355de | |||
| f25654ead7 | |||
| f1741d4dac | |||
| f3245d092b | |||
| a039a8600e | |||
| ee56f4a7a2 | |||
| c40b6ca7f4 | |||
| 2c0e1e498b | |||
| 0262ea31eb | |||
| 4b671d7fb0 | |||
| d8f9419eb9 | |||
| aa65bab486 | |||
| 849c3967fd | |||
| 83562cf7d8 | |||
| 2631a44410 | |||
| ddfea43b79 | |||
| 68f19ffa5f | |||
| 8800d42c32 | |||
| 416a8a7cb2 | |||
| 7088f249a5 | |||
| a2301cc980 | |||
| af545404e8 | |||
| 2e96f19476 | |||
| e3f26b7f75 | |||
| f45c98a6a7 | |||
| a565a3c909 | |||
| b5e3dd6c06 | |||
| 928f592d9c | |||
| 8ee8edcd36 | |||
| 1e128348e5 | |||
| 6ef5655b7d | |||
| f6ba5329ce | |||
| 93cef0d580 | |||
| 797b088cc8 | |||
| 6d23d3510f | |||
| f2d7d0af43 | |||
| e55c0461db | |||
| 8eca511a53 | |||
| f20e46dee0 | |||
| b79f22f4fe | |||
| 3287dc77e2 | |||
| 78a08b35e7 | |||
| e86196999a | |||
| 4d50339041 | |||
| edc5a2c0f2 | |||
| 598b88b1f0 | |||
| c22b9f8ff5 | |||
| 3c654ab495 | |||
| 2f0fabea7a | |||
| 099b14efc3 | |||
| ee42dee366 | |||
| deae081cb3 | |||
| 6479f14d3d | |||
| 60707c3868 | |||
| e78d8e1ae6 | |||
| fee0d0aed9 | |||
| 178abc77ce | |||
| 7001f97d96 | |||
| 55432e61ff | |||
| 4ee993ef3b | |||
| 64d471bb9b | |||
| 4dfcdcb0b2 | |||
| 8e69a84e7a | |||
| 8ec643d882 | |||
| f4d6192c80 | |||
| c0aa2b85fc | |||
| 61a376fb6d | |||
| 8786cb5180 | |||
| c305ef1360 | |||
| f662ce0b7a | |||
| 71af9345a5 | |||
| a819a19c77 | |||
| c65fad06b7 | |||
| 9e6e1931b1 | |||
| 3b22273f5a | |||
| fc5ff1782b | |||
| 4d3b3d984d | |||
| 514976561f | |||
| fb4998d21b | |||
| 0feec978d3 | |||
| 5f1c39aba5 | |||
| 17973619de | |||
| 478d7a2d2d | |||
| f2af0be1e1 | |||
| d0725f5098 | |||
| 646d614d94 | |||
| 4c337ef5e9 | |||
| 2b633b8566 | |||
| 6b16454217 | |||
| b7086deeac | |||
| f021afb6a4 | |||
| 99622bd3d6 | |||
| 50a76519ea | |||
| 1e806054ab | |||
| 89d7f335fc | |||
| 7a664ec4ec | |||
| a8a4d029f8 | |||
| 501b5dce76 | |||
| e9b3504370 | |||
| 7b20c78e73 | |||
| a343ce69aa | |||
| d52ce400fb | |||
| 0ee574eaaa | |||
| 74c4392b6d | |||
| d844c330e9 | |||
| b50cb78fa6 | |||
| 26c138f42c | |||
| 0ec7e65926 | |||
| 3588cc4c03 | |||
| da8c7749c8 | |||
| 79fe999e77 | |||
| 439c65ad6d | |||
| 81b3aa5ac1 | |||
| c4beb9ae4d | |||
| 9d286d8378 | |||
| 19e7a43fe3 | |||
| ab59e2deac | |||
| 043f22e6ec | |||
| 4abb6af31e | |||
| a17ba4a81f | |||
| bc8a6847e3 | |||
| 18f97f9df2 | |||
| 4a204d8d89 | |||
| 062c6c2364 | |||
| 477716ef67 | |||
| ef973df7c9 | |||
| c40bb6a4d5 | |||
| 20e942dccd | |||
| 598cbc4d11 | |||
| 70a3d5af07 | |||
| 9cabb1afbd | |||
| 4267224f59 | |||
| 02d910f53c | |||
| e81b7b5b9f | |||
| 17e0a8eec1 | |||
| eb322c9d41 | |||
| 751af92c21 | |||
| fb4962a41a | |||
| daf6598599 | |||
| e1e4f71f3a | |||
| a2fa7ec9c4 | |||
| 5cd37b74b4 | |||
| beed7e83f2 | |||
| a7726edca6 | |||
| eed0c21c41 | |||
| 094a43157e | |||
| 4033958bf5 | |||
| f18784ecc1 | |||
| 418494b7f3 | |||
| 343753ee2a | |||
| 1ceda066c4 | |||
| 3c13dea55f | |||
| 6ced2cbf7c | |||
| b41f16a736 | |||
| 42fa02a887 | |||
| 2ce87cdac0 | |||
| e4864d3871 | |||
| b6e0052013 | |||
| 9ef83a59c7 | |||
| 325724ff85 | |||
| 998bfa0656 | |||
| e476df5e7d | |||
| 83a3601cdb | |||
| 996dcc4b23 | |||
| 04304f8283 | |||
| 3418f73390 | |||
| b07b6c8960 | |||
| 4f2cf37d73 | |||
| 408e017f2f | |||
| 62b42266e8 | |||
| 80e9e23965 | |||
| c73154aeb1 | |||
| 66c4786ec2 | |||
| 143f5a2085 | |||
| 699c7df798 | |||
| d3de7b95aa | |||
| 2dc6f76da9 | |||
| 63ccf6b553 | |||
| f49ffe3cb0 | |||
| c1b578350d | |||
| 48e4af41ae | |||
| 5e915f9c40 | |||
| 70b5f91f82 | |||
| 792df08c78 | |||
| 032b5d3580 | |||
| 8f93e43bb3 | |||
| 04f95e648b | |||
| 511b8eb407 | |||
| a6e6dd255d | |||
| c3c53d4056 | |||
| 2f700d9a4c | |||
| e4da9f5afe | |||
| 3f919813f2 | |||
| 4063ffc163 | |||
| 0d08b89853 | |||
| 6c9da364d0 | |||
| c1614e8241 | |||
| 75a458f2be | |||
| 602291736f | |||
| 9f2d15e590 | |||
| 5e88201d47 | |||
| 598b8bd1cd | |||
| 9186a44860 | |||
| 61e3dae708 | |||
| 94d46299d0 | |||
| 7070c05f2f | |||
| 0b7038cc65 | |||
| 4882d04ece | |||
| 4b2d34491e | |||
| 79d8230821 | |||
| c7e3305a76 | |||
| 81feccf0d2 | |||
| 9666bee006 | |||
| 44d54057d0 | |||
| beb4251688 | |||
| 598395cd38 | |||
| 0a6913f5d0 | |||
| fa36458303 | |||
| 3f96f88027 | |||
| 131ab714ba | |||
| 333d0c933a | |||
| 2a5c0e05cc | |||
| 4bda9da860 | |||
| 4c579cf862 | |||
| 1b74ce7ac0 | |||
| 29e3625c7b | |||
| a41b9381a1 | |||
| b4980a968c | |||
| ea91751217 | |||
| 1ceffc3391 | |||
| 3de31427a3 | |||
| 4abb9c2ea6 | |||
| e7bfbe77c2 | |||
| 776282ed6b | |||
| d1621684df | |||
| ba183e71e1 | |||
| aac34d4fad | |||
| 8e28e4ecbf | |||
| 48665aa1ad | |||
| f34968bcf5 | |||
| 4a5c1ed582 | |||
| 6d87ab08e2 | |||
| 5ae18bf4f9 | |||
| 1bac12259d | |||
| 434dc408c3 | |||
| 6601ee3b12 | |||
| fde1731365 | |||
| 7725952776 | |||
| e18ee08b70 | |||
| 5aaaeb426c | |||
| 1f55a0cbd8 | |||
| 4ad026b398 | |||
| d36825da52 | |||
| bf2715c2be | |||
| 80953a0148 | |||
| bb9a08d00d | |||
| 3e8fa7cba7 | |||
| da8b88b6b2 | |||
| 4bd21a1ccb | |||
| 29f7586b93 | |||
| 39b6b725f5 | |||
| 631c68029a | |||
| 93aefc127f | |||
| 19ac88b560 | |||
| 99e775a283 | |||
| 7fc05b96a2 | |||
| 9b811da43d | |||
| 4199b609b4 | |||
| 958e6d8519 | |||
| 9dea22ab05 | |||
| 3dc7c6b36f | |||
| 44e76f36b4 | |||
| 010333a190 | |||
| ba833a265a | |||
| e0bf156272 | |||
| a654e21b27 | |||
| de6f149e3b | |||
| 29893b89b3 | |||
| 7783e9ed20 | |||
| d93d1ed48a | |||
| 32c461e93b | |||
| d88e6153c1 | |||
| 7b980ae4d4 | |||
| b249d37bab | |||
| fa34e081cc | |||
| 9f795d7256 | |||
| c8d7d6be43 | |||
| c31124eb14 | |||
| e999b7a8f8 | |||
| 49353a5ec5 | |||
| 229fbdd306 | |||
| 4562dd08dc | |||
| f24f4ea8f9 | |||
| 6338d38ab6 | |||
| 527d93c6b4 | |||
| 3f5f8d9f57 | |||
| 245c913ba1 | |||
| 7d3ef52f03 | |||
| 6cd7556bc5 | |||
| 123f0594a3 | |||
| 652cebc7d0 | |||
| a4cb9a8923 | |||
| eb954fb10d | |||
| 845eab6f53 | |||
| 4fe20db497 | |||
| c40d503f6e | |||
| 1ea843bde4 | |||
| 9ed5d70250 | |||
| f6209b97e2 | |||
| 5221ad6da7 | |||
| cd0bded428 | |||
| ff5fddf353 | |||
| 28b29ed086 | |||
| 765b2b1d69 | |||
| 599a434faa | |||
| a2abee986d | |||
| d57c0712b0 | |||
| 4166d78e87 | |||
| 5322291402 | |||
| dc2ffd758d | |||
| b73cdb2e7d | |||
| 3a83e5d519 | |||
| 3c0cdcadc0 | |||
| 0a23ef8b5d | |||
| c8a38ac709 | |||
| 31d15fadbe | |||
| 3fc481e302 | |||
| 803605c318 | |||
| 1d138e3b4b | |||
| 5b6c5326b6 | |||
| 60a1c303da | |||
| b9f32da7b8 | |||
| fde0b1d8bf | |||
| cf07004fcd | |||
| b41b52df84 | |||
| 9632dd170a | |||
| 9dc334dea9 | |||
| 0741079450 | |||
| 562df0f48f | |||
| 3b87c078f4 | |||
| fc091665ff | |||
| 3f2842a9a3 | |||
| f2418c81d7 | |||
| 3c85797cc9 | |||
| 9187bf0b83 | |||
| 5a2381d9dd | |||
| 5fb8bb0dac | |||
| 0d55da18d4 | |||
| 73148d65bb | |||
| e81438e49f | |||
| e247d8095e | |||
| 46ddb36c79 | |||
| a87fee906f | |||
| 888d94131e | |||
| 63dd018756 | |||
| db38571646 | |||
| 8111d96a20 | |||
| a1b5b7c03c | |||
| 91e95b1ef2 | |||
| 1491f35f5e | |||
| 9bb127dda7 | |||
| c4348b0cb2 | |||
| a3fc0c7f96 | |||
| c7387068cc | |||
| 658ce390e2 | |||
| f0b6f66be6 | |||
| 304812e14f | |||
| 92d8a05393 | |||
| d96d98b8f4 | |||
| 0d059187ec | |||
| 1b73b0b861 | |||
| 29f8d6b981 | |||
| 7826de9d29 | |||
| 78c56e4f28 | |||
| 3ef7736e85 | |||
| 73e6194551 | |||
| 7ceed3dfbc | |||
| 7a0c2dc261 | |||
| 5807c4d97f | |||
| a689607e98 | |||
| b6b3e27408 | |||
| ac30bd6e51 | |||
| 174fc4f72b | |||
| 047ec982f4 | |||
| e427f37f0e | |||
| 810ac1fcfa | |||
| 5ee3cc6712 | |||
| 5ad3d5697e | |||
| 874ab093d5 | |||
| fb668859b0 | |||
| be7a2d7f41 | |||
| 154b6b9f74 | |||
| 23c91386dc | |||
| 741b6ce0d9 | |||
| 600c2f6061 | |||
| 359de2dbe0 | |||
| 84eec4655a | |||
| 7e8c69a02d | |||
| 730d47f2f7 | |||
| 5afb74e606 | |||
| 8a21547668 | |||
| 3ee3044270 | |||
| 782dc24eba | |||
| 475b96178e | |||
| 4beba53675 | |||
| efb7cad993 | |||
| 7b7705866d | |||
| b7e06d51ea | |||
| 95476276ac | |||
| 2347e10458 | |||
| 85051f1340 | |||
| 42c6e70ebe | |||
| b9fe83e7a8 | |||
| 841108623f | |||
| 8ce221e41b | |||
| f8c41ab39f | |||
| 79e7fd175e | |||
| fbcf755591 | |||
| 6168a47e24 | |||
| d788114be3 | |||
| 497814f80c | |||
| 7297edf16f | |||
| 714407eb46 | |||
| dd3523ddd7 | |||
| 7739de5db9 | |||
| 99c08026ee | |||
| 19f7ea70f0 | |||
| 49050c042d | |||
| 18ccff5759 | |||
| 9f6f646e77 | |||
| b8c0d8ef79 | |||
| 2ccd41bfb9 | |||
| fa64b51d4a | |||
| f5ac194008 | |||
| 816cf0141b | |||
| 7baabc6d2c | |||
| 37d1c7338b | |||
| 404ea9d838 | |||
| 98c5c5827c | |||
| 79525284b1 | |||
| c14ea7afdf | |||
| c437753d64 | |||
| 441cc35e5a | |||
| dc03144773 | |||
| 992921b24c | |||
| 28f38dca46 | |||
| 53155ccef0 | |||
| ba6f0a1aab | |||
| 2d89d06bcb | |||
| 54ff50ce68 | |||
| 22aa8cdd6c | |||
| 06b0195d74 | |||
| a99b4ded7f | |||
| 2405a0e778 | |||
| 84544b1e84 | |||
| 95fce39502 | |||
| 99c5b26241 | |||
| 6e07e49c84 | |||
| 0bcfea9d20 | |||
| 2658331fd2 | |||
| 2ab49cc545 | |||
| a39fe5ff3b | |||
| 01578b4e34 | |||
| 95718c889d | |||
| 6279cc9ec1 | |||
| f7fb9034ef | |||
| 15f3af2020 | |||
| 97288ed6ce | |||
| 5e168c2561 | |||
| 358b3f96ae | |||
| c0d9c3808a | |||
| 7404bb8e64 | |||
| 93eccd7dcf | |||
| 05d9d41860 | |||
| c47c41548f | |||
| 013d1980a3 | |||
| df9f4a23b4 | |||
| c41da47a48 | |||
| e7214ad8df | |||
| d6671de842 | |||
| aad218db5d | |||
| 724ba1e271 | |||
| 97d554f638 | |||
| c5a7655d26 | |||
| 403e896e3e | |||
| 1a15f43cad | |||
| 399b460c53 | |||
| acc0362180 | |||
| 00db93e03f | |||
| d1997794c8 | |||
| aa1ebe69f2 | |||
| 4e7f5f56f1 | |||
| 28cb7359ce | |||
| 91c272d21c | |||
| 3c00125e83 | |||
| f359848a2f | |||
| 989769e5e8 | |||
| 0f2f1b6211 | |||
| ffe8f4acc6 | |||
| edb09777de | |||
| 5262c7863e | |||
| 54256826fe | |||
| 3d3c224b3a | |||
| 049eccb872 | |||
| 269828c79e | |||
| b4e25ae66d | |||
| b20dd74d23 | |||
| bc3e2ec358 | |||
| 6133a6d6d8 | |||
| 46a16c04e6 | |||
| 8469b3b26f | |||
| 2ed04f57fe | |||
| b19bac679a | |||
| 3c33d5982c | |||
| 5b934eeb87 | |||
| 795d96f8d5 | |||
| a8e7119b4a | |||
| 38569ff7fc | |||
| e404557d62 | |||
| 96cbc75a5e | |||
| c989af6cf0 | |||
| 4eac9d03ea | |||
| 6292009b0b | |||
| 3272be967d | |||
| 1c015da440 | |||
| 0d047cc956 | |||
| e682070b85 | |||
| 9f08694d9b | |||
| 70f0db73e5 | |||
| 9dc8f44379 | |||
| 59f7ccd723 | |||
| 0710e95a6d | |||
| 4d1b5e3919 | |||
| 0cc2cb92dd | |||
| dba4d168f7 | |||
| d87ac7843c | |||
| 040535b004 | |||
| c8acd2c0b1 | |||
| d67fecea6e | |||
| 61f80f9ee6 | |||
| 9da8f9a5d1 | |||
| f381468d5a | |||
| 6ae97266e4 | |||
| 66060f345c | |||
| c61f568170 | |||
| dcd108bda3 | |||
| 9d89f98987 | |||
| ca7b959fce | |||
| 4a30793595 | |||
| 35e2d53f0f | |||
| 503efa4572 | |||
| b0c33d9dff | |||
| 012b156b46 | |||
| 25d0d3bf59 | |||
| 0f1babc82b | |||
| e2b93ea785 | |||
| b1cedfa81e | |||
| 701ee36f6a | |||
| 4e5db86434 | |||
| f45e9e657c | |||
| 4936fcdb1e | |||
| 374e05c422 | |||
| 9c00798373 | |||
| db82fce925 | |||
| acaa28e476 | |||
| f297ce5809 | |||
| 3dc3fc5f67 | |||
| 4884fc4418 | |||
| adc17842ec | |||
| daa48b0b7c | |||
| 17c0362df3 | |||
| 29b9a63fc9 | |||
| 2a9fae160e | |||
| 0c49a1e3bd | |||
| e896c41be1 | |||
| 187250fa24 | |||
| 9035b18584 | |||
| 4534d78978 | |||
| f4ab0e982c | |||
| 3e7c6629a6 | |||
| 3ea17331fe | |||
| 1057fcc271 | |||
| 5a31c36097 | |||
| 1677a69bba | |||
| 315c49165d | |||
| aae70e7ec0 |
@@ -0,0 +1,86 @@
|
||||
name: Build ProxMenux Monitor AppImage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths: [ 'AppImage/**' ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
paths: [ 'AppImage/**' ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: AppImage
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
- name: Build Next.js app
|
||||
working-directory: AppImage
|
||||
run: npm run build
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3 python3-pip python3-venv
|
||||
|
||||
- name: Make build script executable
|
||||
working-directory: AppImage
|
||||
run: chmod +x scripts/build_appimage.sh
|
||||
|
||||
- name: Build AppImage
|
||||
working-directory: AppImage
|
||||
run: ./scripts/build_appimage.sh
|
||||
|
||||
- name: Get version from package.json
|
||||
id: version
|
||||
working-directory: AppImage
|
||||
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload AppImage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
|
||||
path: AppImage/dist/*.AppImage
|
||||
retention-days: 30
|
||||
|
||||
- name: Generate SHA256 checksum
|
||||
run: |
|
||||
cd AppImage/dist
|
||||
sha256sum *.AppImage > ProxMenux-Monitor.AppImage.sha256
|
||||
echo "Generated SHA256:"
|
||||
cat ProxMenux-Monitor.AppImage.sha256
|
||||
|
||||
- name: Upload AppImage and checksum to /AppImage folder in main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git fetch origin main
|
||||
git checkout main
|
||||
|
||||
rm -f AppImage/*.AppImage AppImage/*.sha256 || true
|
||||
|
||||
# Copy new files
|
||||
cp AppImage/dist/*.AppImage AppImage/
|
||||
cp AppImage/dist/ProxMenux-Monitor.AppImage.sha256 AppImage/
|
||||
|
||||
git add AppImage/*.AppImage AppImage/*.sha256
|
||||
git commit -m "Update AppImage build ($(date +'%Y-%m-%d %H:%M:%S'))" || echo "No changes to commit"
|
||||
git push origin main
|
||||
@@ -0,0 +1 @@
|
||||
e896eb10de4bf990d31c1d8357289f64cbce481921647f2be53efb850d0b73b2 ProxMenux-1.0.0.AppImage
|
||||
@@ -0,0 +1,41 @@
|
||||
# ProxMenux Monitor
|
||||
|
||||
A modern, responsive dashboard for monitoring Proxmox VE systems built with Next.js and React.
|
||||
|
||||
## Features
|
||||
|
||||
- **System Overview**: Real-time monitoring of CPU, memory, temperature, and active VMs/LXC containers
|
||||
- **Storage Management**: Visual representation of storage distribution and disk performance metrics
|
||||
- **Network Monitoring**: Network interface statistics and performance graphs
|
||||
- **Virtual Machines**: Comprehensive view of VMs and LXC containers with resource usage
|
||||
- **System Logs**: Real-time system log monitoring and filtering
|
||||
- **Dark/Light Theme**: Toggle between themes with Proxmox-inspired design
|
||||
- **Responsive Design**: Works seamlessly on desktop and mobile devices
|
||||
- **Onboarding Experience**: Interactive welcome carousel for first-time users
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Frontend**: Next.js 15, React 19, TypeScript
|
||||
- **Styling**: Tailwind CSS with custom Proxmox-inspired theme
|
||||
- **Charts**: Recharts for data visualization
|
||||
- **UI Components**: Radix UI primitives with shadcn/ui
|
||||
- **Backend**: Flask server for system data collection
|
||||
- **Packaging**: AppImage for easy distribution
|
||||
|
||||
## Onboarding Images
|
||||
|
||||
To customize the onboarding experience, place your screenshot images in `public/images/onboarding/`:
|
||||
|
||||
- `imagen1.png` - Overview section screenshot
|
||||
- `imagen2.png` - Storage section screenshot
|
||||
- `imagen3.png` - Network section screenshot
|
||||
- `imagen4.png` - VMs & LXCs section screenshot
|
||||
- `imagen5.png` - Hardware section screenshot
|
||||
- `imagen6.png` - System Logs section screenshot
|
||||
|
||||
**Recommended image specifications:**
|
||||
- Format: PNG or JPG
|
||||
- Size: 1200x800px or similar 3:2 aspect ratio
|
||||
- Quality: High-quality screenshots with representative data
|
||||
|
||||
The onboarding carousel will automatically show on first visit and can be dismissed or marked as "Don't show again".
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ProxmoxDashboard } from "../../components/proxmox-dashboard"
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-background">
|
||||
<ProxmoxDashboard />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* ===================== */
|
||||
/* Light Mode (default) */
|
||||
/* ===================== */
|
||||
:root {
|
||||
--background: oklch(1 0 0); /* blanco */
|
||||
--foreground: oklch(0.145 0 0); /* casi negro */
|
||||
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: var(--foreground);
|
||||
|
||||
--popover: var(--card);
|
||||
--popover-foreground: var(--foreground);
|
||||
|
||||
--primary: oklch(0.205 0 0); /* gris oscuro */
|
||||
--primary-foreground: oklch(0.985 0 0); /* blanco */
|
||||
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: var(--primary);
|
||||
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0); /* gris medio */
|
||||
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: var(--primary);
|
||||
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.145 0 0);
|
||||
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: var(--border);
|
||||
--ring: oklch(0.708 0 0);
|
||||
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
|
||||
--radius: 0.625rem;
|
||||
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: var(--foreground);
|
||||
--sidebar-primary: var(--primary);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: var(--primary);
|
||||
--sidebar-border: var(--border);
|
||||
--sidebar-ring: var(--ring);
|
||||
}
|
||||
|
||||
/* ===================== */
|
||||
/* Dark Mode (gris) */
|
||||
/* ===================== */
|
||||
.dark {
|
||||
--background: oklch(0.22 0 0); /* gris oscuro */
|
||||
--foreground: oklch(0.97 0 0); /* blanco/gris claro */
|
||||
|
||||
--card: oklch(0.24 0 0);
|
||||
--card-foreground: var(--foreground);
|
||||
|
||||
--popover: var(--card);
|
||||
--popover-foreground: var(--foreground);
|
||||
|
||||
--primary: oklch(0.83 0 0); /* casi blanco */
|
||||
--primary-foreground: var(--background);
|
||||
|
||||
--secondary: oklch(0.28 0 0);
|
||||
--secondary-foreground: oklch(0.92 0 0);
|
||||
|
||||
--muted: oklch(0.26 0 0);
|
||||
--muted-foreground: oklch(0.72 0 0);
|
||||
|
||||
--accent: oklch(0.28 0 0);
|
||||
--accent-foreground: var(--primary);
|
||||
|
||||
--destructive: oklch(0.53 0.25 27);
|
||||
--destructive-foreground: oklch(0.9 0 0);
|
||||
|
||||
--border: oklch(0.34 0 0);
|
||||
--input: var(--border);
|
||||
--ring: oklch(0.55 0 0);
|
||||
|
||||
--chart-1: oklch(0.60 0.20 255);
|
||||
--chart-2: oklch(0.70 0.16 165);
|
||||
--chart-3: oklch(0.76 0.19 70);
|
||||
--chart-4: oklch(0.63 0.25 305);
|
||||
--chart-5: oklch(0.66 0.24 20);
|
||||
|
||||
--sidebar: oklch(0.24 0 0);
|
||||
--sidebar-foreground: var(--foreground);
|
||||
--sidebar-primary: var(--chart-1);
|
||||
--sidebar-primary-foreground: var(--foreground);
|
||||
--sidebar-accent: oklch(0.28 0 0);
|
||||
--sidebar-accent-foreground: var(--foreground);
|
||||
--sidebar-border: var(--border);
|
||||
--sidebar-ring: var(--ring);
|
||||
}
|
||||
|
||||
/* ===================== */
|
||||
/* Base layer */
|
||||
/* ===================== */
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
/* Foco accesible */
|
||||
:is(button,[role="button"],a,input,select,textarea,[tabindex]:not([tabindex="-1"])):focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
:is(button,[role="button"],a,input,select,textarea,[tabindex]:not([tabindex="-1"])):focus-visible {
|
||||
@apply ring-2;
|
||||
--tw-ring-color: var(--ring);
|
||||
--tw-ring-opacity: 0.5; /* equivalente al /50 */
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== */
|
||||
/* Ajustes para Charts */
|
||||
/* ===================== */
|
||||
@layer components {
|
||||
/* Recharts axis */
|
||||
.recharts-cartesian-axis-tick tspan {
|
||||
fill: var(--muted-foreground);
|
||||
}
|
||||
.recharts-cartesian-axis-line,
|
||||
.recharts-cartesian-grid line {
|
||||
stroke: var(--border);
|
||||
}
|
||||
|
||||
/* Chart.js axis */
|
||||
.chartjs-render-monitor text {
|
||||
fill: var(--muted-foreground);
|
||||
}
|
||||
.chartjs-render-monitor line {
|
||||
stroke: var(--border);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import Hardware from "@/components/hardware"
|
||||
|
||||
export default function HardwarePage() {
|
||||
return <Hardware />
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type React from "react"
|
||||
import type { Metadata } from "next"
|
||||
import { GeistSans } from "geist/font/sans"
|
||||
import { GeistMono } from "geist/font/mono"
|
||||
import { ThemeProvider } from "../components/theme-provider"
|
||||
import { Suspense } from "react"
|
||||
import "./globals.css"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "ProxMenux Monitor",
|
||||
description: "Proxmox System Dashboard and Monitor",
|
||||
generator: "v0.app",
|
||||
manifest: "/manifest.json",
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicon.ico", sizes: "any" },
|
||||
{ url: "/icon.svg", type: "image/svg+xml" },
|
||||
{ url: "/icon.png", type: "image/png", sizes: "32x32" },
|
||||
],
|
||||
shortcut: "/favicon.ico",
|
||||
apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
|
||||
},
|
||||
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#2b2f36" },
|
||||
],
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${GeistSans.variable} ${GeistMono.variable} antialiased bg-background text-foreground`}>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</Suspense>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { ProxmoxDashboard } from "../components/proxmox-dashboard"
|
||||
|
||||
export default function Home() {
|
||||
return <ProxmoxDashboard />
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"
|
||||
import { Cpu } from "@/components/icons/cpu" // Added import for Cpu
|
||||
import type { PCIDevice } from "../types/hardware" // Fixed import to use relative path instead of alias
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
|
||||
function GPUCard({ device }: { device: PCIDevice }) {
|
||||
const hasMonitoring = device.gpu_temperature !== undefined || device.gpu_utilization !== undefined
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Cpu className="h-5 w-5" />
|
||||
{device.device}
|
||||
</CardTitle>
|
||||
<CardDescription>{device.vendor}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<div className="text-muted-foreground">Slot</div>
|
||||
<div className="font-medium">{device.slot}</div>
|
||||
</div>
|
||||
{device.driver && (
|
||||
<div>
|
||||
<div className="text-muted-foreground">Driver</div>
|
||||
<div className="font-medium">{device.driver}</div>
|
||||
</div>
|
||||
)}
|
||||
{device.gpu_driver_version && (
|
||||
<div>
|
||||
<div className="text-muted-foreground">Driver Version</div>
|
||||
<div className="font-medium">{device.gpu_driver_version}</div>
|
||||
</div>
|
||||
)}
|
||||
{device.gpu_memory && (
|
||||
<div>
|
||||
<div className="text-muted-foreground">Memory</div>
|
||||
<div className="font-medium">{device.gpu_memory}</div>
|
||||
</div>
|
||||
)}
|
||||
{device.gpu_compute_capability && (
|
||||
<div>
|
||||
<div className="text-muted-foreground">Compute Capability</div>
|
||||
<div className="font-medium">{device.gpu_compute_capability}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasMonitoring && (
|
||||
<div className="space-y-3 pt-4 border-t">
|
||||
<h4 className="text-sm font-semibold">Real-time Monitoring</h4>
|
||||
|
||||
{device.gpu_temperature !== undefined && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Temperature</span>
|
||||
<span className="font-medium">{device.gpu_temperature}°C</span>
|
||||
</div>
|
||||
<Progress value={(device.gpu_temperature / 100) * 100} className="h-2" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{device.gpu_utilization !== undefined && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">GPU Utilization</span>
|
||||
<span className="font-medium">{device.gpu_utilization}%</span>
|
||||
</div>
|
||||
<Progress value={device.gpu_utilization} className="h-2" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{device.gpu_memory_used && device.gpu_memory_total && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Memory Usage</span>
|
||||
<span className="font-medium">
|
||||
{device.gpu_memory_used} / {device.gpu_memory_total}
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={(Number.parseInt(device.gpu_memory_used) / Number.parseInt(device.gpu_memory_total)) * 100}
|
||||
className="h-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{device.gpu_power_draw && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Power Draw</span>
|
||||
<span className="font-medium">{device.gpu_power_draw}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{device.gpu_clock_speed && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">GPU Clock</span>
|
||||
<span className="font-medium">{device.gpu_clock_speed}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{device.gpu_memory_clock && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Memory Clock</span>
|
||||
<span className="font-medium">{device.gpu_memory_clock}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { ArrowLeft, Loader2 } from "lucide-react"
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
||||
|
||||
interface MetricsViewProps {
|
||||
vmid: number
|
||||
vmName: string
|
||||
vmType: "qemu" | "lxc"
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
const TIMEFRAME_OPTIONS = [
|
||||
{ value: "hour", label: "1 Hour" },
|
||||
{ value: "day", label: "24 Hours" },
|
||||
{ value: "week", label: "7 Days" },
|
||||
{ value: "month", label: "30 Days" },
|
||||
{ value: "year", label: "1 Year" },
|
||||
]
|
||||
|
||||
const CustomCPUTooltip = ({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
|
||||
<p className="text-sm font-semibold text-white mb-2">{label}</p>
|
||||
<div className="space-y-1.5">
|
||||
{payload.map((entry: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
|
||||
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
|
||||
<span className="text-sm font-semibold text-white">{entry.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const CustomMemoryTooltip = ({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
|
||||
<p className="text-sm font-semibold text-white mb-2">{label}</p>
|
||||
<div className="space-y-1.5">
|
||||
{payload.map((entry: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
|
||||
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
|
||||
<span className="text-sm font-semibold text-white">{entry.value} GB</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const CustomDiskTooltip = ({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
|
||||
<p className="text-sm font-semibold text-white mb-2">{label}</p>
|
||||
<div className="space-y-1.5">
|
||||
{payload.map((entry: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
|
||||
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
|
||||
<span className="text-sm font-semibold text-white">{entry.value} MB</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const CustomNetworkTooltip = ({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
|
||||
<p className="text-sm font-semibold text-white mb-2">{label}</p>
|
||||
<div className="space-y-1.5">
|
||||
{payload.map((entry: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
|
||||
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
|
||||
<span className="text-sm font-semibold text-white">{entry.value} MB</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function MetricsView({ vmid, vmName, vmType, onBack }: MetricsViewProps) {
|
||||
const [timeframe, setTimeframe] = useState("week")
|
||||
const [data, setData] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [hiddenDiskLines, setHiddenDiskLines] = useState<string[]>([])
|
||||
const [hiddenNetworkLines, setHiddenNetworkLines] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
fetchMetrics()
|
||||
}, [vmid, timeframe])
|
||||
|
||||
const fetchMetrics = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const baseUrl =
|
||||
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||
const apiUrl = `${baseUrl}/api/vms/${vmid}/metrics?timeframe=${timeframe}`
|
||||
|
||||
const response = await fetch(apiUrl)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || "Failed to fetch metrics")
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
const transformedData = result.data.map((item: any) => {
|
||||
const date = new Date(item.time * 1000)
|
||||
let timeLabel = ""
|
||||
|
||||
if (timeframe === "hour") {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
} else if (timeframe === "day") {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
} else if (timeframe === "week") {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
} else if (timeframe === "month") {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
} else {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
time: timeLabel,
|
||||
timestamp: item.time,
|
||||
cpu: item.cpu ? Number((item.cpu * 100).toFixed(2)) : 0,
|
||||
memory: item.mem ? Number(((item.mem / item.maxmem) * 100).toFixed(2)) : 0,
|
||||
memoryGB: item.mem ? Number((item.mem / 1024 / 1024 / 1024).toFixed(2)) : 0,
|
||||
maxMemoryGB: item.maxmem ? Number((item.maxmem / 1024 / 1024 / 1024).toFixed(2)) : 0,
|
||||
netin: item.netin ? Number((item.netin / 1024 / 1024).toFixed(2)) : 0,
|
||||
netout: item.netout ? Number((item.netout / 1024 / 1024).toFixed(2)) : 0,
|
||||
diskread: item.diskread ? Number((item.diskread / 1024 / 1024).toFixed(2)) : 0,
|
||||
diskwrite: item.diskwrite ? Number((item.diskwrite / 1024 / 1024).toFixed(2)) : 0,
|
||||
}
|
||||
})
|
||||
|
||||
setData(transformedData)
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Error loading metrics")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatXAxisTick = (tick: any) => {
|
||||
return tick
|
||||
}
|
||||
|
||||
const renderAllCharts = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[400px]">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[400px]">
|
||||
<p className="text-red-500">{error}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[400px]">
|
||||
<p className="text-muted-foreground">No data available</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const tickInterval = Math.ceil(data.length / 8)
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* CPU Chart */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">CPU Usage</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={data} margin={{ bottom: 80 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor" }}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
interval={tickInterval}
|
||||
tickFormatter={formatXAxisTick}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor" }}
|
||||
label={{ value: "%", angle: -90, position: "insideLeft", fill: "currentColor" }}
|
||||
domain={[0, "dataMax"]}
|
||||
/>
|
||||
<Tooltip content={<CustomCPUTooltip />} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cpu"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={2}
|
||||
fill="#3b82f6"
|
||||
fillOpacity={0.3}
|
||||
name="CPU %"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Memory Chart */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Memory Usage</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={data} margin={{ bottom: 80 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor" }}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
interval={tickInterval}
|
||||
tickFormatter={formatXAxisTick}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor" }}
|
||||
label={{ value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }}
|
||||
domain={[0, "dataMax"]}
|
||||
/>
|
||||
<Tooltip content={<CustomMemoryTooltip />} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="memoryGB"
|
||||
stroke="#10b981"
|
||||
fill="#10b981"
|
||||
fillOpacity={0.3}
|
||||
strokeWidth={2}
|
||||
name="Memory GB"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Disk I/O Chart */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Disk I/O</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={data} margin={{ bottom: 80 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor" }}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
interval={tickInterval}
|
||||
tickFormatter={formatXAxisTick}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor" }}
|
||||
label={{ value: "MB", angle: -90, position: "insideLeft", fill: "currentColor" }}
|
||||
domain={[0, "dataMax"]}
|
||||
/>
|
||||
<Tooltip content={<CustomDiskTooltip />} />
|
||||
<Legend content={renderDiskLegend} verticalAlign="top" />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="diskread"
|
||||
stroke="#10b981"
|
||||
fill="#10b981"
|
||||
fillOpacity={0.3}
|
||||
strokeWidth={2}
|
||||
name="Read"
|
||||
hide={hiddenDiskLines.includes("diskread")}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="diskwrite"
|
||||
stroke="#3b82f6"
|
||||
fill="#3b82f6"
|
||||
fillOpacity={0.3}
|
||||
strokeWidth={2}
|
||||
name="Write"
|
||||
hide={hiddenDiskLines.includes("diskwrite")}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Network I/O Chart */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Network I/O</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={data} margin={{ bottom: 80 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor" }}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
interval={tickInterval}
|
||||
tickFormatter={formatXAxisTick}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor" }}
|
||||
label={{ value: "MB", angle: -90, position: "insideLeft", fill: "currentColor" }}
|
||||
domain={[0, "dataMax"]}
|
||||
/>
|
||||
<Tooltip content={<CustomNetworkTooltip />} />
|
||||
<Legend content={renderNetworkLegend} verticalAlign="top" />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="netin"
|
||||
stroke="#10b981"
|
||||
fill="#10b981"
|
||||
fillOpacity={0.3}
|
||||
strokeWidth={2}
|
||||
name="Download"
|
||||
hide={hiddenNetworkLines.includes("netin")}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="netout"
|
||||
stroke="#3b82f6"
|
||||
fill="#3b82f6"
|
||||
fillOpacity={0.3}
|
||||
strokeWidth={2}
|
||||
name="Upload"
|
||||
hide={hiddenNetworkLines.includes("netout")}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handleDiskLegendClick = (dataKey: string) => {
|
||||
setHiddenDiskLines((prev) => {
|
||||
if (prev.includes(dataKey)) {
|
||||
return prev.filter((key) => key !== dataKey)
|
||||
} else {
|
||||
return [...prev, dataKey]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleNetworkLegendClick = (dataKey: string) => {
|
||||
setHiddenNetworkLines((prev) => {
|
||||
if (prev.includes(dataKey)) {
|
||||
return prev.filter((key) => key !== dataKey)
|
||||
} else {
|
||||
return [...prev, dataKey]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const renderDiskLegend = (props: any) => {
|
||||
const { payload } = props
|
||||
return (
|
||||
<div className="flex justify-center gap-6 pb-2">
|
||||
{payload.map((entry: any) => (
|
||||
<button
|
||||
key={entry.dataKey}
|
||||
onClick={() => handleDiskLegendClick(entry.dataKey)}
|
||||
className={`flex items-center gap-2 cursor-pointer transition-opacity hover:opacity-100 ${
|
||||
hiddenDiskLines.includes(entry.dataKey) ? "opacity-40" : "opacity-100"
|
||||
}`}
|
||||
>
|
||||
<span className="w-3 h-3 rounded-full" style={{ backgroundColor: entry.color }} />
|
||||
<span className="text-sm">{entry.value}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderNetworkLegend = (props: any) => {
|
||||
const { payload } = props
|
||||
return (
|
||||
<div className="flex justify-center gap-6 pb-2">
|
||||
{payload.map((entry: any) => (
|
||||
<button
|
||||
key={entry.dataKey}
|
||||
onClick={() => handleNetworkLegendClick(entry.dataKey)}
|
||||
className={`flex items-center gap-2 cursor-pointer transition-opacity hover:opacity-100 ${
|
||||
hiddenNetworkLines.includes(entry.dataKey) ? "opacity-40" : "opacity-100"
|
||||
}`}
|
||||
>
|
||||
<span className="w-3 h-3 rounded-full" style={{ backgroundColor: entry.color }} />
|
||||
<span className="text-sm">{entry.value}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full max-h-[90vh]">
|
||||
{/* Fixed Header */}
|
||||
<div className="p-6 pb-4 border-b shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="ghost" size="icon" onClick={onBack}>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">Metrics - {vmName}</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
VMID: {vmid} • Type: {vmType.toUpperCase()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Select value={timeframe} onValueChange={setTimeframe}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TIMEFRAME_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrollable Content with all charts */}
|
||||
<div className="flex-1 overflow-y-auto p-6 min-h-0">{renderAllCharts()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent } from "./ui/card"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { Wifi, Zap } from "lucide-react"
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
interface NetworkCardProps {
|
||||
interface_: {
|
||||
name: string
|
||||
type: string
|
||||
status: string
|
||||
speed: number
|
||||
duplex?: string
|
||||
mtu?: number
|
||||
mac_address: string | null
|
||||
addresses: Array<{
|
||||
ip: string
|
||||
netmask: string
|
||||
}>
|
||||
bytes_sent?: number
|
||||
bytes_recv?: number
|
||||
bridge_physical_interface?: string
|
||||
bridge_bond_slaves?: string[]
|
||||
vmid?: number
|
||||
vm_name?: string
|
||||
vm_type?: string
|
||||
}
|
||||
timeframe: "hour" | "day" | "week" | "month" | "year"
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const getInterfaceTypeBadge = (type: string) => {
|
||||
switch (type) {
|
||||
case "physical":
|
||||
return { color: "bg-blue-500/10 text-blue-500 border-blue-500/20", label: "Physical" }
|
||||
case "bridge":
|
||||
return { color: "bg-green-500/10 text-green-500 border-green-500/20", label: "Bridge" }
|
||||
case "bond":
|
||||
return { color: "bg-purple-500/10 text-purple-500 border-purple-500/20", label: "Bond" }
|
||||
case "vlan":
|
||||
return { color: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20", label: "VLAN" }
|
||||
case "vm_lxc":
|
||||
return { color: "bg-orange-500/10 text-orange-500 border-orange-500/20", label: "Virtual" }
|
||||
case "virtual":
|
||||
return { color: "bg-orange-500/10 text-orange-500 border-orange-500/20", label: "Virtual" }
|
||||
default:
|
||||
return { color: "bg-gray-500/10 text-gray-500 border-gray-500/20", label: "Unknown" }
|
||||
}
|
||||
}
|
||||
|
||||
const getVMTypeBadge = (vmType: string | undefined) => {
|
||||
if (vmType === "lxc") {
|
||||
return { color: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20", label: "LXC" }
|
||||
} else if (vmType === "vm") {
|
||||
return { color: "bg-purple-500/10 text-purple-500 border-purple-500/20", label: "VM" }
|
||||
}
|
||||
return { color: "bg-gray-500/10 text-gray-500 border-gray-500/20", label: "Unknown" }
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number | undefined): string => {
|
||||
if (!bytes || bytes === 0) return "0 B"
|
||||
const k = 1024
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"]
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`
|
||||
}
|
||||
|
||||
const formatSpeed = (speed: number): string => {
|
||||
if (speed === 0) return "N/A"
|
||||
if (speed >= 1000) return `${(speed / 1000).toFixed(1)} Gbps`
|
||||
return `${speed} Mbps`
|
||||
}
|
||||
|
||||
const formatStorage = (bytes: number): string => {
|
||||
if (bytes === 0) return "0 B"
|
||||
const k = 1024
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"]
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
const value = bytes / Math.pow(k, i)
|
||||
const decimals = value >= 10 ? 1 : 2
|
||||
return `${value.toFixed(decimals)} ${sizes[i]}`
|
||||
}
|
||||
|
||||
export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps) {
|
||||
const typeBadge = getInterfaceTypeBadge(interface_.type)
|
||||
const vmTypeBadge = interface_.vm_type ? getVMTypeBadge(interface_.vm_type) : null
|
||||
|
||||
const [trafficData, setTrafficData] = useState<{ received: number; sent: number }>({
|
||||
received: 0,
|
||||
sent: 0,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTrafficData = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/network/${interface_.name}/metrics?timeframe=${timeframe}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch traffic data: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Calculate totals from the data points
|
||||
if (data.data && data.data.length > 0) {
|
||||
const lastPoint = data.data[data.data.length - 1]
|
||||
const firstPoint = data.data[0]
|
||||
|
||||
// Calculate the difference between last and first data points
|
||||
const receivedGB = Math.max(0, (lastPoint.netin || 0) - (firstPoint.netin || 0))
|
||||
const sentGB = Math.max(0, (lastPoint.netout || 0) - (firstPoint.netout || 0))
|
||||
|
||||
setTrafficData({
|
||||
received: receivedGB,
|
||||
sent: sentGB,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to fetch traffic data for card:", error)
|
||||
// Keep showing 0 values on error
|
||||
setTrafficData({ received: 0, sent: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
// Only fetch if interface is up and not a VM
|
||||
if (interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm") {
|
||||
fetchTrafficData()
|
||||
|
||||
// Refresh every 60 seconds
|
||||
const interval = setInterval(fetchTrafficData, 60000)
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [interface_.name, interface_.status, interface_.vm_type, timeframe])
|
||||
|
||||
const getTimeframeLabel = () => {
|
||||
switch (timeframe) {
|
||||
case "hour":
|
||||
return "Last Hour"
|
||||
case "day":
|
||||
return "Last 24 Hours"
|
||||
case "week":
|
||||
return "Last 7 Days"
|
||||
case "month":
|
||||
return "Last 30 Days"
|
||||
case "year":
|
||||
return "Last Year"
|
||||
default:
|
||||
return "Last 24 Hours"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-card border-border hover:bg-white/5 transition-colors cursor-pointer" onClick={onClick}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* First row: Icon, Name, Type Badge, Status */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<Wifi className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1 flex-wrap">
|
||||
<div className="font-medium text-foreground">{interface_.name}</div>
|
||||
{vmTypeBadge ? (
|
||||
<Badge variant="outline" className={vmTypeBadge.color}>
|
||||
{vmTypeBadge.label}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className={typeBadge.color}>
|
||||
{typeBadge.label}
|
||||
</Badge>
|
||||
)}
|
||||
{interface_.vm_name && (
|
||||
<div className="text-sm text-muted-foreground truncate">→ {interface_.vm_name}</div>
|
||||
)}
|
||||
{interface_.type === "bridge" && interface_.bridge_physical_interface && (
|
||||
<div className="text-sm text-blue-500 font-medium flex items-center gap-1 flex-wrap break-all">
|
||||
→ {interface_.bridge_physical_interface}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
interface_.status === "up"
|
||||
? "bg-green-500/10 text-green-500 border-green-500/20"
|
||||
: "bg-red-500/10 text-red-500 border-red-500/20"
|
||||
}
|
||||
>
|
||||
{interface_.status.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Second row: Details - Responsive layout */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{interface_.type === "vm_lxc" ? "VMID" : "IP Address"}
|
||||
</div>
|
||||
<div className="font-medium text-foreground font-mono text-sm truncate">
|
||||
{interface_.type === "vm_lxc"
|
||||
? (interface_.vmid ?? "N/A")
|
||||
: interface_.addresses.length > 0
|
||||
? interface_.addresses[0].ip
|
||||
: "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-muted-foreground text-xs">Speed</div>
|
||||
<div className="font-medium text-foreground flex items-center gap-1 text-xs">
|
||||
<Zap className="h-3 w-3" />
|
||||
{formatSpeed(interface_.speed)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<div className="text-muted-foreground text-xs">{getTimeframeLabel()}</div>
|
||||
<div className="font-medium text-foreground text-xs">
|
||||
{interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm" ? (
|
||||
<>
|
||||
<span className="text-green-500">↓ {formatStorage(trafficData.received * 1024 * 1024 * 1024)}</span>
|
||||
{" / "}
|
||||
<span className="text-blue-500">↑ {formatStorage(trafficData.sent * 1024 * 1024 * 1024)}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-green-500">↓ {formatBytes(interface_.bytes_recv)}</span>
|
||||
{" / "}
|
||||
<span className="text-blue-500">↑ {formatBytes(interface_.bytes_sent)}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{interface_.mac_address && (
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<div className="text-muted-foreground text-xs">MAC</div>
|
||||
<div className="font-medium text-foreground font-mono text-xs truncate">{interface_.mac_address}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
||||
import { Loader2 } from "lucide-react"
|
||||
|
||||
interface NetworkMetricsData {
|
||||
time: string
|
||||
timestamp: number
|
||||
netIn: number
|
||||
netOut: number
|
||||
}
|
||||
|
||||
interface NetworkTrafficChartProps {
|
||||
timeframe: string
|
||||
interfaceName?: string
|
||||
onTotalsCalculated?: (totals: { received: number; sent: number }) => void
|
||||
refreshInterval?: number // En milisegundos, por defecto 60000 (60 segundos)
|
||||
}
|
||||
|
||||
const CustomNetworkTooltip = ({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
|
||||
<p className="text-sm font-semibold text-white mb-2">{label}</p>
|
||||
<div className="space-y-1.5">
|
||||
{payload.map((entry: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
|
||||
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
|
||||
<span className="text-sm font-semibold text-white">{entry.value.toFixed(3)} GB</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function NetworkTrafficChart({
|
||||
timeframe,
|
||||
interfaceName,
|
||||
onTotalsCalculated,
|
||||
refreshInterval = 60000,
|
||||
}: NetworkTrafficChartProps) {
|
||||
const [data, setData] = useState<NetworkMetricsData[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true)
|
||||
const [visibleLines, setVisibleLines] = useState({
|
||||
netIn: true,
|
||||
netOut: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setIsInitialLoad(true)
|
||||
fetchMetrics()
|
||||
}, [timeframe, interfaceName])
|
||||
|
||||
useEffect(() => {
|
||||
if (refreshInterval > 0) {
|
||||
const interval = setInterval(() => {
|
||||
fetchMetrics()
|
||||
}, refreshInterval)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [timeframe, interfaceName, refreshInterval])
|
||||
|
||||
const fetchMetrics = async () => {
|
||||
if (isInitialLoad) {
|
||||
setLoading(true)
|
||||
}
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const baseUrl =
|
||||
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||
|
||||
const apiUrl = interfaceName
|
||||
? `${baseUrl}/api/network/${interfaceName}/metrics?timeframe=${timeframe}`
|
||||
: `${baseUrl}/api/node/metrics?timeframe=${timeframe}`
|
||||
|
||||
console.log("[v0] Fetching network metrics from:", apiUrl)
|
||||
|
||||
const response = await fetch(apiUrl)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch network metrics: ${response.status}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!result.data || !Array.isArray(result.data)) {
|
||||
throw new Error("Invalid data format received from server")
|
||||
}
|
||||
|
||||
if (result.data.length === 0) {
|
||||
setData([])
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const transformedData = result.data.map((item: any, index: number) => {
|
||||
const date = new Date(item.time * 1000)
|
||||
let timeLabel = ""
|
||||
|
||||
if (timeframe === "hour") {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
} else if (timeframe === "day") {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
} else if (timeframe === "week") {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
} else if (timeframe === "year") {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})
|
||||
} else {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
}
|
||||
|
||||
let intervalSeconds = 60
|
||||
if (index > 0) {
|
||||
intervalSeconds = item.time - result.data[index - 1].time
|
||||
}
|
||||
|
||||
const netInBytes = (item.netin || 0) * intervalSeconds
|
||||
const netOutBytes = (item.netout || 0) * intervalSeconds
|
||||
|
||||
return {
|
||||
time: timeLabel,
|
||||
timestamp: item.time,
|
||||
netIn: Number((netInBytes / 1024 / 1024 / 1024).toFixed(4)),
|
||||
netOut: Number((netOutBytes / 1024 / 1024 / 1024).toFixed(4)),
|
||||
}
|
||||
})
|
||||
|
||||
setData(transformedData)
|
||||
|
||||
const totalReceived = transformedData.reduce((sum: number, item: NetworkMetricsData) => sum + item.netIn, 0)
|
||||
const totalSent = transformedData.reduce((sum: number, item: NetworkMetricsData) => sum + item.netOut, 0)
|
||||
|
||||
if (onTotalsCalculated) {
|
||||
onTotalsCalculated({ received: totalReceived, sent: totalSent })
|
||||
}
|
||||
|
||||
if (isInitialLoad) {
|
||||
setIsInitialLoad(false)
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("[v0] Error fetching network metrics:", err)
|
||||
setError(err.message || "Error loading metrics")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const tickInterval = Math.ceil(data.length / 8)
|
||||
|
||||
const handleLegendClick = (dataKey: string) => {
|
||||
setVisibleLines((prev) => ({
|
||||
...prev,
|
||||
[dataKey as keyof typeof prev]: !prev[dataKey as keyof typeof prev],
|
||||
}))
|
||||
}
|
||||
|
||||
const renderLegend = (props: any) => {
|
||||
const { payload } = props
|
||||
return (
|
||||
<div className="flex justify-center gap-4 pb-2 flex-wrap">
|
||||
{payload.map((entry: any, index: number) => {
|
||||
const isVisible = visibleLines[entry.dataKey as keyof typeof visibleLines]
|
||||
return (
|
||||
<div
|
||||
key={`legend-${index}`}
|
||||
className="flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={() => handleLegendClick(entry.dataKey)}
|
||||
style={{ opacity: isVisible ? 1 : 0.4 }}
|
||||
>
|
||||
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: entry.color }} />
|
||||
<span className="text-sm text-foreground">{entry.value}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading && isInitialLoad) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[300px]">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[300px] gap-2">
|
||||
<p className="text-muted-foreground text-sm">Network metrics not available yet</p>
|
||||
<p className="text-xs text-red-500">{error}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[300px]">
|
||||
<p className="text-muted-foreground text-sm">No network metrics available</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={data} margin={{ bottom: 80 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor", fontSize: 12 }}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
interval={tickInterval}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor", fontSize: 12 }}
|
||||
label={{ value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }}
|
||||
domain={[0, "auto"]}
|
||||
/>
|
||||
<Tooltip content={<CustomNetworkTooltip />} />
|
||||
<Legend verticalAlign="top" height={36} content={renderLegend} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="netIn"
|
||||
stroke="#10b981"
|
||||
strokeWidth={2}
|
||||
fill="#10b981"
|
||||
fillOpacity={0.3}
|
||||
name="Received"
|
||||
hide={!visibleLines.netIn}
|
||||
isAnimationActive={true}
|
||||
animationDuration={300}
|
||||
animationEasing="ease-in-out"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="netOut"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={2}
|
||||
fill="#3b82f6"
|
||||
fillOpacity={0.3}
|
||||
name="Sent"
|
||||
hide={!visibleLines.netOut}
|
||||
isAnimationActive={true}
|
||||
animationDuration={300}
|
||||
animationEasing="ease-in-out"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
||||
import { Loader2, TrendingUp, MemoryStick } from "lucide-react"
|
||||
|
||||
const TIMEFRAME_OPTIONS = [
|
||||
{ value: "hour", label: "1 Hour" },
|
||||
{ value: "day", label: "24 Hours" },
|
||||
{ value: "week", label: "7 Days" },
|
||||
{ value: "month", label: "30 Days" },
|
||||
]
|
||||
|
||||
interface NodeMetricsData {
|
||||
time: string
|
||||
timestamp: number
|
||||
cpu: number
|
||||
load: number
|
||||
memoryTotal: number
|
||||
memoryUsed: number
|
||||
memoryFree: number
|
||||
memoryZfsArc: number
|
||||
}
|
||||
|
||||
const CustomCpuTooltip = ({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
|
||||
<p className="text-sm font-semibold text-white mb-2">{label}</p>
|
||||
<div className="space-y-1.5">
|
||||
{payload.map((entry: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
|
||||
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
|
||||
<span className="text-sm font-semibold text-white">{entry.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const CustomMemoryTooltip = ({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
|
||||
<p className="text-sm font-semibold text-white mb-2">{label}</p>
|
||||
<div className="space-y-1.5">
|
||||
{payload.map((entry: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
|
||||
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
|
||||
<span className="text-sm font-semibold text-white">{entry.value} GB</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function NodeMetricsCharts() {
|
||||
const [timeframe, setTimeframe] = useState("day")
|
||||
const [data, setData] = useState<NodeMetricsData[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [visibleLines, setVisibleLines] = useState({
|
||||
cpu: { cpu: true, load: true },
|
||||
memory: { memoryTotal: true, memoryUsed: true, memoryZfsArc: true, memoryFree: true },
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
console.log("[v0] NodeMetricsCharts component mounted")
|
||||
fetchMetrics()
|
||||
}, [timeframe])
|
||||
|
||||
const fetchMetrics = async () => {
|
||||
console.log("[v0] fetchMetrics called with timeframe:", timeframe)
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const baseUrl =
|
||||
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||
const apiUrl = `${baseUrl}/api/node/metrics?timeframe=${timeframe}`
|
||||
|
||||
console.log("[v0] Fetching node metrics from:", apiUrl)
|
||||
|
||||
const response = await fetch(apiUrl)
|
||||
|
||||
console.log("[v0] Response status:", response.status)
|
||||
console.log("[v0] Response ok:", response.ok)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.log("[v0] Error response text:", errorText)
|
||||
throw new Error(`Failed to fetch node metrics: ${response.status}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
console.log("[v0] Node metrics result:", result)
|
||||
console.log("[v0] Result keys:", Object.keys(result))
|
||||
console.log("[v0] Data array length:", result.data?.length || 0)
|
||||
|
||||
if (!result.data || !Array.isArray(result.data)) {
|
||||
console.error("[v0] Invalid data format - data is not an array:", result)
|
||||
throw new Error("Invalid data format received from server")
|
||||
}
|
||||
|
||||
if (result.data.length === 0) {
|
||||
console.warn("[v0] No data points received")
|
||||
setData([])
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
console.log("[v0] First data point sample:", result.data[0])
|
||||
console.log("[v0] First data point loadavg field:", result.data[0]?.loadavg)
|
||||
console.log("[v0] loadavg type:", typeof result.data[0]?.loadavg)
|
||||
console.log("[v0] loadavg is array:", Array.isArray(result.data[0]?.loadavg))
|
||||
if (result.data[0]?.loadavg) {
|
||||
console.log("[v0] loadavg length:", result.data[0].loadavg.length)
|
||||
console.log("[v0] loadavg[0]:", result.data[0].loadavg[0])
|
||||
}
|
||||
|
||||
const transformedData = result.data.map((item: any) => {
|
||||
const date = new Date(item.time * 1000)
|
||||
let timeLabel = ""
|
||||
|
||||
if (timeframe === "hour") {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
} else if (timeframe === "day") {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
} else if (timeframe === "week") {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
} else {
|
||||
timeLabel = date.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
time: timeLabel,
|
||||
timestamp: item.time,
|
||||
cpu: item.cpu ? Number((item.cpu * 100).toFixed(2)) : 0,
|
||||
load: item.loadavg
|
||||
? typeof item.loadavg === "number"
|
||||
? Number(item.loadavg.toFixed(2))
|
||||
: Array.isArray(item.loadavg) && item.loadavg.length > 0
|
||||
? Number(item.loadavg[0].toFixed(2))
|
||||
: 0
|
||||
: 0,
|
||||
memoryTotal: item.memtotal ? Number((item.memtotal / 1024 / 1024 / 1024).toFixed(2)) : 0,
|
||||
memoryUsed: item.memused ? Number((item.memused / 1024 / 1024 / 1024).toFixed(2)) : 0,
|
||||
memoryFree: item.memfree ? Number((item.memfree / 1024 / 1024 / 1024).toFixed(2)) : 0,
|
||||
memoryZfsArc: item.zfsarc ? Number((item.zfsarc / 1024 / 1024 / 1024).toFixed(2)) : 0,
|
||||
}
|
||||
})
|
||||
|
||||
setData(transformedData)
|
||||
} catch (err: any) {
|
||||
console.error("[v0] Error fetching node metrics:", err)
|
||||
console.error("[v0] Error message:", err.message)
|
||||
console.error("[v0] Error stack:", err.stack)
|
||||
setError(err.message || "Error loading metrics")
|
||||
} finally {
|
||||
console.log("[v0] fetchMetrics finally block - setting loading to false")
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const tickInterval = Math.ceil(data.length / 8)
|
||||
|
||||
const handleLegendClick = (chartType: "cpu" | "memory", dataKey: string) => {
|
||||
setVisibleLines((prev) => ({
|
||||
...prev,
|
||||
[chartType]: {
|
||||
...prev[chartType],
|
||||
[dataKey as keyof (typeof prev)[typeof chartType]]:
|
||||
!prev[chartType][dataKey as keyof (typeof prev)[typeof chartType]],
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const renderLegend = (chartType: "cpu" | "memory") => (props: any) => {
|
||||
const { payload } = props
|
||||
return (
|
||||
<div className="flex justify-center gap-4 pb-2 flex-wrap">
|
||||
{payload.map((entry: any, index: number) => {
|
||||
const isVisible = visibleLines[chartType][entry.dataKey as keyof (typeof visibleLines)[typeof chartType]]
|
||||
return (
|
||||
<div
|
||||
key={`legend-${index}`}
|
||||
className="flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={() => handleLegendClick(chartType, entry.dataKey)}
|
||||
style={{ opacity: isVisible ? 1 : 0.4 }}
|
||||
>
|
||||
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: entry.color }} />
|
||||
<span className="text-sm text-foreground">{entry.value}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
console.log("[v0] Render state - loading:", loading, "error:", error, "data length:", data.length)
|
||||
|
||||
if (loading) {
|
||||
console.log("[v0] Rendering loading state")
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-center h-[300px]">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-center h-[300px]">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.log("[v0] Rendering error state:", error)
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex flex-col items-center justify-center h-[300px] gap-2">
|
||||
<p className="text-muted-foreground text-sm">Metrics data not available yet</p>
|
||||
<p className="text-xs text-red-500">{error}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex flex-col items-center justify-center h-[300px] gap-2">
|
||||
<p className="text-muted-foreground text-sm">Metrics data not available yet</p>
|
||||
<p className="text-xs text-red-500">{error}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
console.log("[v0] Rendering no data state")
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-center h-[300px]">
|
||||
<p className="text-muted-foreground text-sm">No metrics data available</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-center h-[300px]">
|
||||
<p className="text-muted-foreground text-sm">No metrics data available</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
console.log("[v0] Rendering charts with", data.length, "data points")
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Timeframe Selector */}
|
||||
<div className="flex justify-end">
|
||||
<Select value={timeframe} onValueChange={setTimeframe}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TIMEFRAME_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Charts Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* CPU Usage + Load Average Chart */}
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<TrendingUp className="h-5 w-5 mr-2" />
|
||||
CPU Usage & Load Average
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={data} margin={{ bottom: 60, left: 30, right: 10 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor", fontSize: 12 }}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
interval={tickInterval}
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor", fontSize: 12 }}
|
||||
label={{ value: "CPU %", angle: -90, position: "insideLeft", fill: "currentColor" }}
|
||||
domain={[0, "dataMax"]}
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor", fontSize: 12 }}
|
||||
label={{ value: "Load", angle: 90, position: "insideRight", fill: "currentColor" }}
|
||||
domain={[0, "dataMax"]}
|
||||
/>
|
||||
<Tooltip content={<CustomCpuTooltip />} />
|
||||
<Legend verticalAlign="top" height={36} content={renderLegend("cpu")} />
|
||||
<Area
|
||||
yAxisId="left"
|
||||
type="monotone"
|
||||
dataKey="cpu"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={2}
|
||||
fill="#3b82f6"
|
||||
fillOpacity={0.3}
|
||||
name="CPU %"
|
||||
hide={!visibleLines.cpu.cpu}
|
||||
/>
|
||||
<Area
|
||||
yAxisId="right"
|
||||
type="monotone"
|
||||
dataKey="load"
|
||||
stroke="#10b981"
|
||||
strokeWidth={2}
|
||||
fill="#10b981"
|
||||
fillOpacity={0.3}
|
||||
name="Load Avg"
|
||||
hide={!visibleLines.cpu.load}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Memory Usage Chart */}
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<MemoryStick className="h-5 w-5 mr-2" />
|
||||
Memory Usage
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={data} margin={{ bottom: 60, left: 30, right: 10 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor", fontSize: 12 }}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
interval={tickInterval}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
tick={{ fill: "currentColor", fontSize: 12 }}
|
||||
label={{ value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }}
|
||||
domain={[0, "dataMax"]}
|
||||
/>
|
||||
<Tooltip content={<CustomMemoryTooltip />} />
|
||||
<Legend verticalAlign="top" height={36} content={renderLegend("memory")} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="memoryTotal"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={2}
|
||||
fill="#3b82f6"
|
||||
fillOpacity={0.1}
|
||||
name="Total"
|
||||
hide={!visibleLines.memory.memoryTotal}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="memoryUsed"
|
||||
stroke="#10b981"
|
||||
strokeWidth={2}
|
||||
fill="#10b981"
|
||||
fillOpacity={0.3}
|
||||
name="Used"
|
||||
hide={!visibleLines.memory.memoryUsed}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="memoryZfsArc"
|
||||
stroke="#f59e0b"
|
||||
strokeWidth={2}
|
||||
fill="#f59e0b"
|
||||
fillOpacity={0.3}
|
||||
name="ZFS ARC"
|
||||
hide={!visibleLines.memory.memoryZfsArc}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="memoryFree"
|
||||
stroke="#06b6d4"
|
||||
strokeWidth={2}
|
||||
fill="#06b6d4"
|
||||
fillOpacity={0.3}
|
||||
name="Available"
|
||||
hide={!visibleLines.memory.memoryFree}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "./ui/button"
|
||||
import { Dialog, DialogContent } from "./ui/dialog"
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
X,
|
||||
Sparkles,
|
||||
LayoutDashboard,
|
||||
HardDrive,
|
||||
Network,
|
||||
Box,
|
||||
Cpu,
|
||||
FileText,
|
||||
Rocket,
|
||||
} from "lucide-react"
|
||||
import Image from "next/image"
|
||||
|
||||
interface OnboardingSlide {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
image?: string
|
||||
icon: React.ReactNode
|
||||
gradient: string
|
||||
}
|
||||
|
||||
const slides: OnboardingSlide[] = [
|
||||
{
|
||||
id: 0,
|
||||
title: "Welcome to ProxMenux Monitor!",
|
||||
description:
|
||||
"Your new monitoring tool for Proxmox. Discover all the features that will help you manage and supervise your infrastructure efficiently.",
|
||||
icon: <Sparkles className="h-16 w-16" />,
|
||||
gradient: "from-blue-500 via-purple-500 to-pink-500",
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: "System Overview",
|
||||
description:
|
||||
"Monitor your server's status in real-time: CPU, memory, temperature, system load and more. Everything in an intuitive and easy-to-understand dashboard.",
|
||||
image: "/images/onboarding/imagen1.png",
|
||||
icon: <LayoutDashboard className="h-12 w-12" />,
|
||||
gradient: "from-blue-500 to-cyan-500",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Storage Management",
|
||||
description:
|
||||
"Visualize the status of all your disks and volumes. Detailed information on capacity, usage, SMART health, temperature and performance of each storage device.",
|
||||
image: "/images/onboarding/imagen2.png",
|
||||
icon: <HardDrive className="h-12 w-12" />,
|
||||
gradient: "from-cyan-500 to-teal-500",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Network Metrics",
|
||||
description:
|
||||
"Monitor network traffic in real-time. Bandwidth statistics, active interfaces, transfer speeds and historical usage graphs.",
|
||||
image: "/images/onboarding/imagen3.png",
|
||||
icon: <Network className="h-12 w-12" />,
|
||||
gradient: "from-teal-500 to-green-500",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Virtual Machines & Containers",
|
||||
description:
|
||||
"Manage all your VMs and LXC containers from one place. Status, allocated resources, current usage and quick controls for each virtual machine.",
|
||||
image: "/images/onboarding/imagen4.png",
|
||||
icon: <Box className="h-12 w-12" />,
|
||||
gradient: "from-green-500 to-emerald-500",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Hardware Information",
|
||||
description:
|
||||
"Complete details of your server hardware: CPU, RAM, GPU, disks, network, UPS and more. Technical specifications, models, serial numbers and status of each component.",
|
||||
image: "/images/onboarding/imagen5.png",
|
||||
icon: <Cpu className="h-12 w-12" />,
|
||||
gradient: "from-emerald-500 to-blue-500",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "System Logs",
|
||||
description:
|
||||
"Access system logs in real-time. Filter by event type, search for specific errors and keep complete track of your server activity. Download the displayed logs for further analysis.",
|
||||
image: "/images/onboarding/imagen6.png",
|
||||
icon: <FileText className="h-12 w-12" />,
|
||||
gradient: "from-blue-500 to-indigo-500",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "Ready for the Future!",
|
||||
description:
|
||||
"ProxMenux Monitor is prepared to receive updates and improvements that will be added gradually, improving the user experience and being able to execute ProxMenux functions from the web panel.",
|
||||
icon: <Rocket className="h-16 w-16" />,
|
||||
gradient: "from-indigo-500 via-purple-500 to-pink-500",
|
||||
},
|
||||
]
|
||||
|
||||
export function OnboardingCarousel() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [currentSlide, setCurrentSlide] = useState(0)
|
||||
const [direction, setDirection] = useState<"next" | "prev">("next")
|
||||
|
||||
useEffect(() => {
|
||||
const hasSeenOnboarding = localStorage.getItem("proxmenux-onboarding-seen")
|
||||
if (!hasSeenOnboarding) {
|
||||
setOpen(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentSlide < slides.length - 1) {
|
||||
setDirection("next")
|
||||
setCurrentSlide(currentSlide + 1)
|
||||
} else {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrev = () => {
|
||||
if (currentSlide > 0) {
|
||||
setDirection("prev")
|
||||
setCurrentSlide(currentSlide - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSkip = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleDontShowAgain = () => {
|
||||
localStorage.setItem("proxmenux-onboarding-seen", "true")
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleDotClick = (index: number) => {
|
||||
setDirection(index > currentSlide ? "next" : "prev")
|
||||
setCurrentSlide(index)
|
||||
}
|
||||
|
||||
const slide = slides[currentSlide]
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-4xl p-0 gap-0 overflow-hidden border-0 bg-transparent">
|
||||
<div className="relative bg-card rounded-lg overflow-hidden shadow-2xl">
|
||||
{/* Close button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-4 right-4 z-50 h-8 w-8 rounded-full bg-background/80 backdrop-blur-sm hover:bg-background"
|
||||
onClick={handleSkip}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div
|
||||
className={`relative h-48 md:h-64 bg-gradient-to-br ${slide.gradient} flex items-center justify-center overflow-hidden`}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/10" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,rgba(255,255,255,0.1),transparent)]" />
|
||||
|
||||
{/* Icon or Image */}
|
||||
<div className="relative z-10 text-white">
|
||||
{slide.image ? (
|
||||
<div className="relative w-full h-36 md:h-48 flex items-center justify-center px-4">
|
||||
<Image
|
||||
src={slide.image || "/placeholder.svg"}
|
||||
alt={slide.title}
|
||||
width={600}
|
||||
height={400}
|
||||
className="rounded-lg shadow-2xl object-cover max-h-36 md:max-h-48"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = "none"
|
||||
const fallback = target.parentElement?.querySelector(".fallback-icon")
|
||||
if (fallback) {
|
||||
fallback.classList.remove("hidden")
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="fallback-icon hidden">{slide.icon}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="animate-pulse">{slide.icon}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-2xl" />
|
||||
<div className="absolute bottom-10 right-10 w-32 h-32 bg-white/10 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="p-4 md:p-8 space-y-4 md:space-y-6">
|
||||
<div className="space-y-2 md:space-y-3">
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-foreground text-balance">{slide.title}</h2>
|
||||
<p className="text-base md:text-lg text-muted-foreground leading-relaxed text-pretty">
|
||||
{slide.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Progress dots */}
|
||||
<div className="flex items-center justify-center gap-2 py-2 md:py-4">
|
||||
{slides.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleDotClick(index)}
|
||||
className={`transition-all duration-300 rounded-full ${
|
||||
index === currentSlide
|
||||
? "w-8 h-2.5 bg-blue-500 shadow-lg shadow-blue-500/50"
|
||||
: "w-2.5 h-2.5 bg-muted-foreground/60 hover:bg-muted-foreground/80 border border-muted-foreground/40"
|
||||
}`}
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 md:gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handlePrev}
|
||||
disabled={currentSlide === 0}
|
||||
className="gap-2 w-full sm:w-auto"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
<div className="flex gap-2 w-full sm:w-auto">
|
||||
{currentSlide < slides.length - 1 ? (
|
||||
<>
|
||||
<Button variant="outline" onClick={handleSkip} className="flex-1 sm:flex-none bg-transparent">
|
||||
Skip
|
||||
</Button>
|
||||
<Button onClick={handleNext} className="gap-2 bg-blue-500 hover:bg-blue-600 flex-1 sm:flex-none">
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
className="gap-2 bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 w-full sm:w-auto"
|
||||
>
|
||||
Get Started!
|
||||
<Sparkles className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Don't show again */}
|
||||
{currentSlide === slides.length - 1 && (
|
||||
<div className="text-center pt-2">
|
||||
<button
|
||||
onClick={handleDontShowAgain}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors underline"
|
||||
>
|
||||
Don't show again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from "react"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { Button } from "./ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
|
||||
import { SystemOverview } from "./system-overview"
|
||||
import { StorageOverview } from "./storage-overview"
|
||||
import { NetworkMetrics } from "./network-metrics"
|
||||
import { VirtualMachines } from "./virtual-machines"
|
||||
import Hardware from "./hardware"
|
||||
import { SystemLogs } from "./system-logs"
|
||||
import { OnboardingCarousel } from "./onboarding-carousel"
|
||||
import {
|
||||
RefreshCw,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Server,
|
||||
Menu,
|
||||
LayoutDashboard,
|
||||
HardDrive,
|
||||
NetworkIcon,
|
||||
Box,
|
||||
Cpu,
|
||||
FileText,
|
||||
} from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { ThemeToggle } from "./theme-toggle"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet"
|
||||
|
||||
interface SystemStatus {
|
||||
status: "healthy" | "warning" | "critical"
|
||||
uptime: string
|
||||
lastUpdate: string
|
||||
serverName: string
|
||||
nodeId: string
|
||||
}
|
||||
|
||||
interface FlaskSystemData {
|
||||
hostname: string
|
||||
node_id: string
|
||||
uptime: string
|
||||
cpu_usage: number
|
||||
memory_usage: number
|
||||
temperature: number
|
||||
load_average: number[]
|
||||
}
|
||||
|
||||
export function ProxmoxDashboard() {
|
||||
const [systemStatus, setSystemStatus] = useState<SystemStatus>({
|
||||
status: "healthy",
|
||||
uptime: "Loading...",
|
||||
lastUpdate: new Date().toLocaleTimeString(),
|
||||
serverName: "Loading...",
|
||||
nodeId: "Loading...",
|
||||
})
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [isServerConnected, setIsServerConnected] = useState(true)
|
||||
const [componentKey, setComponentKey] = useState(0)
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState("overview")
|
||||
const [showNavigation, setShowNavigation] = useState(true)
|
||||
const [lastScrollY, setLastScrollY] = useState(0)
|
||||
|
||||
const fetchSystemData = useCallback(async () => {
|
||||
console.log("[v0] Fetching system data from Flask server...")
|
||||
console.log("[v0] Current window location:", window.location.href)
|
||||
|
||||
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||
const apiUrl = `${baseUrl}/api/system`
|
||||
|
||||
console.log("[v0] API URL:", apiUrl)
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
cache: "no-store",
|
||||
})
|
||||
console.log("[v0] Response status:", response.status)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data: FlaskSystemData = await response.json()
|
||||
console.log("[v0] System data received:", data)
|
||||
|
||||
let status: "healthy" | "warning" | "critical" = "healthy"
|
||||
if (data.cpu_usage > 90 || data.memory_usage > 90) {
|
||||
status = "critical"
|
||||
} else if (data.cpu_usage > 75 || data.memory_usage > 75) {
|
||||
status = "warning"
|
||||
}
|
||||
|
||||
setSystemStatus({
|
||||
status,
|
||||
uptime: data.uptime,
|
||||
lastUpdate: new Date().toLocaleTimeString(),
|
||||
serverName: data.hostname,
|
||||
nodeId: data.node_id,
|
||||
})
|
||||
setIsServerConnected(true)
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to fetch system data from Flask server:", error)
|
||||
console.error("[v0] Error details:", {
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
apiUrl,
|
||||
windowLocation: window.location.href,
|
||||
})
|
||||
|
||||
setIsServerConnected(false)
|
||||
setSystemStatus((prev) => ({
|
||||
...prev,
|
||||
status: "critical",
|
||||
serverName: "Server Offline",
|
||||
nodeId: "Server Offline",
|
||||
uptime: "N/A",
|
||||
lastUpdate: new Date().toLocaleTimeString(),
|
||||
}))
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchSystemData()
|
||||
const interval = setInterval(fetchSystemData, 10000)
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchSystemData])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
systemStatus.serverName &&
|
||||
systemStatus.serverName !== "Loading..." &&
|
||||
systemStatus.serverName !== "Server Offline"
|
||||
) {
|
||||
document.title = `${systemStatus.serverName} - ProxMenux Monitor`
|
||||
} else {
|
||||
document.title = "ProxMenux Monitor"
|
||||
}
|
||||
}, [systemStatus.serverName])
|
||||
|
||||
useEffect(() => {
|
||||
let hideTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
let lastPosition = window.scrollY
|
||||
|
||||
const handleScroll = () => {
|
||||
const currentScrollY = window.scrollY
|
||||
const delta = currentScrollY - lastPosition
|
||||
|
||||
if (currentScrollY < 50) {
|
||||
setShowNavigation(true)
|
||||
} else if (delta > 2) {
|
||||
if (hideTimeout) clearTimeout(hideTimeout)
|
||||
hideTimeout = setTimeout(() => setShowNavigation(false), 20)
|
||||
} else if (delta < -2) {
|
||||
if (hideTimeout) clearTimeout(hideTimeout)
|
||||
setShowNavigation(true)
|
||||
}
|
||||
|
||||
lastPosition = currentScrollY
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true })
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll)
|
||||
if (hideTimeout) clearTimeout(hideTimeout)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const refreshData = async () => {
|
||||
setIsRefreshing(true)
|
||||
await fetchSystemData()
|
||||
setComponentKey((prev) => prev + 1)
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
|
||||
const statusIcon = useMemo(() => {
|
||||
switch (systemStatus.status) {
|
||||
case "healthy":
|
||||
return <CheckCircle className="h-4 w-4 text-green-500" />
|
||||
case "warning":
|
||||
return <AlertTriangle className="h-4 w-4 text-yellow-500" />
|
||||
case "critical":
|
||||
return <XCircle className="h-4 w-4 text-red-500" />
|
||||
}
|
||||
}, [systemStatus.status])
|
||||
|
||||
const statusColor = useMemo(() => {
|
||||
switch (systemStatus.status) {
|
||||
case "healthy":
|
||||
return "bg-green-500/10 text-green-500 border-green-500/20"
|
||||
case "warning":
|
||||
return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
|
||||
case "critical":
|
||||
return "bg-red-500/10 text-red-500 border-red-500/20"
|
||||
}
|
||||
}, [systemStatus.status])
|
||||
|
||||
const getActiveTabLabel = () => {
|
||||
switch (activeTab) {
|
||||
case "overview":
|
||||
return "Overview"
|
||||
case "storage":
|
||||
return "Storage"
|
||||
case "network":
|
||||
return "Network"
|
||||
case "vms":
|
||||
return "VMs & LXCs"
|
||||
case "hardware":
|
||||
return "Hardware"
|
||||
case "logs":
|
||||
return "System Logs"
|
||||
default:
|
||||
return "Navigation Menu"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<OnboardingCarousel />
|
||||
|
||||
{!isServerConnected && (
|
||||
<div className="bg-red-500/10 border-b border-red-500/20 px-6 py-3">
|
||||
<div className="container mx-auto">
|
||||
<div className="flex items-center space-x-2 text-red-500 mb-2">
|
||||
<XCircle className="h-5 w-5" />
|
||||
<span className="font-medium">ProxMenux Server Connection Failed</span>
|
||||
</div>
|
||||
<div className="text-sm text-red-500/80 space-y-1 ml-7">
|
||||
<p>• Check that the monitor.service is running correctly.</p>
|
||||
<p>• The ProxMenux server should start automatically on port 8008</p>
|
||||
<p>
|
||||
• Try accessing:{" "}
|
||||
<a
|
||||
href={`http://${typeof window !== "undefined" ? window.location.host : "localhost:8008"}/api/health`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
http://{typeof window !== "undefined" ? window.location.host : "localhost:8008"}/api/health
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<header className="border-b border-border bg-card sticky top-0 z-50 shadow-sm">
|
||||
<div className="container mx-auto px-4 md:px-6 py-4 md:py-4">
|
||||
{/* Logo and Title */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
{/* Logo and Title */}
|
||||
<div className="flex items-center space-x-2 md:space-x-3 min-w-0">
|
||||
<div className="w-16 h-16 md:w-10 md:h-10 relative flex items-center justify-center bg-primary/10 flex-shrink-0">
|
||||
<Image
|
||||
src="/images/proxmenux-logo.png"
|
||||
alt="ProxMenux Logo"
|
||||
width={64}
|
||||
height={64}
|
||||
className="object-contain md:w-10 md:h-10"
|
||||
priority
|
||||
onError={(e) => {
|
||||
console.log("[v0] Logo failed to load, using fallback icon")
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = "none"
|
||||
const fallback = target.parentElement?.querySelector(".fallback-icon")
|
||||
if (fallback) {
|
||||
fallback.classList.remove("hidden")
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Server className="h-8 w-8 md:h-6 md:w-6 text-primary absolute fallback-icon hidden" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-lg md:text-xl font-semibold text-foreground truncate">ProxMenux Monitor</h1>
|
||||
<p className="text-xs md:text-sm text-muted-foreground">Proxmox System Dashboard</p>
|
||||
<div className="lg:hidden flex items-center gap-1 text-xs text-muted-foreground mt-0.5">
|
||||
<Server className="h-3 w-3" />
|
||||
<span className="truncate">Node: {systemStatus.serverName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Actions */}
|
||||
<div className="hidden lg:flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="text-sm">
|
||||
<div className="font-medium text-foreground">Node: {systemStatus.serverName}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Badge variant="outline" className={statusColor}>
|
||||
{statusIcon}
|
||||
<span className="ml-1 capitalize">{systemStatus.status}</span>
|
||||
</Badge>
|
||||
|
||||
<div className="text-sm text-muted-foreground whitespace-nowrap">Uptime: {systemStatus.uptime}</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={refreshData}
|
||||
disabled={isRefreshing}
|
||||
className="border-border/50 bg-transparent hover:bg-secondary"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
Refresh
|
||||
</Button>
|
||||
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
{/* Mobile Actions */}
|
||||
<div className="flex lg:hidden items-center gap-2">
|
||||
<Badge variant="outline" className={`${statusColor} text-xs px-2`}>
|
||||
{statusIcon}
|
||||
<span className="ml-1 capitalize hidden sm:inline">{systemStatus.status}</span>
|
||||
</Badge>
|
||||
|
||||
<Button variant="ghost" size="sm" onClick={refreshData} disabled={isRefreshing} className="h-8 w-8 p-0">
|
||||
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Server Info */}
|
||||
<div className="lg:hidden mt-2 flex items-center justify-end text-xs text-muted-foreground">
|
||||
<span className="whitespace-nowrap">Uptime: {systemStatus.uptime}</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
className={`sticky z-40 bg-background
|
||||
top-[120px] md:top-[76px]
|
||||
transition-all duration-700 ease-[cubic-bezier(0.4,0,0.2,1)]
|
||||
${showNavigation ? "translate-y-0 opacity-100" : "-translate-y-[120%] opacity-0 pointer-events-none"}
|
||||
`}
|
||||
>
|
||||
<div className="container mx-auto px-4 md:px-6 pt-4 md:pt-6">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-0">
|
||||
<TabsList className="hidden md:grid w-full grid-cols-6 bg-card border border-border">
|
||||
<TabsTrigger
|
||||
value="overview"
|
||||
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
||||
>
|
||||
Overview
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="storage"
|
||||
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
||||
>
|
||||
Storage
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="network"
|
||||
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
||||
>
|
||||
Network
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="vms"
|
||||
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
||||
>
|
||||
VMs & LXCs
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="hardware"
|
||||
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
||||
>
|
||||
Hardware
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="logs"
|
||||
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
||||
>
|
||||
System Logs
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
|
||||
<div className="md:hidden">
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`w-full justify-between border-border ${
|
||||
activeTab ? "bg-blue-500/10 text-blue-500" : "bg-card"
|
||||
}`}
|
||||
>
|
||||
<span>{getActiveTabLabel()}</span>
|
||||
<Menu className="h-4 w-4" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
</div>
|
||||
<SheetContent side="top" className="bg-card border-border">
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setActiveTab("overview")
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className={`w-full justify-start gap-3 ${
|
||||
activeTab === "overview"
|
||||
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<LayoutDashboard className="h-5 w-5" />
|
||||
<span>Overview</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setActiveTab("storage")
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className={`w-full justify-start gap-3 ${
|
||||
activeTab === "storage"
|
||||
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<HardDrive className="h-5 w-5" />
|
||||
<span>Storage</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setActiveTab("network")
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className={`w-full justify-start gap-3 ${
|
||||
activeTab === "network"
|
||||
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<NetworkIcon className="h-5 w-5" />
|
||||
<span>Network</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setActiveTab("vms")
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className={`w-full justify-start gap-3 ${
|
||||
activeTab === "vms"
|
||||
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<Box className="h-5 w-5" />
|
||||
<span>VMs & LXCs</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setActiveTab("hardware")
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className={`w-full justify-start gap-3 ${
|
||||
activeTab === "hardware"
|
||||
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<Cpu className="h-5 w-5" />
|
||||
<span>Hardware</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setActiveTab("logs")
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className={`w-full justify-start gap-3 ${
|
||||
activeTab === "logs"
|
||||
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<FileText className="h-5 w-5" />
|
||||
<span>System Logs</span>
|
||||
</Button>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 md:px-6 py-4 md:py-6">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4 md:space-y-6">
|
||||
<TabsContent value="overview" className="space-y-4 md:space-y-6 mt-0">
|
||||
<SystemOverview key={`overview-${componentKey}`} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="storage" className="space-y-4 md:space-y-6 mt-0">
|
||||
<StorageOverview key={`storage-${componentKey}`} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="network" className="space-y-4 md:space-y-6 mt-0">
|
||||
<NetworkMetrics key={`network-${componentKey}`} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="vms" className="space-y-4 md:space-y-6 mt-0">
|
||||
<VirtualMachines key={`vms-${componentKey}`} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="hardware" className="space-y-4 md:space-y-6 mt-0">
|
||||
<Hardware key={`hardware-${componentKey}`} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="logs" className="space-y-4 md:space-y-6 mt-0">
|
||||
<SystemLogs key={`logs-${componentKey}`} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<footer className="mt-8 md:mt-12 pt-4 md:pt-6 border-t border-border text-center text-xs md:text-sm text-muted-foreground">
|
||||
<p className="font-medium mb-2">ProxMenux Monitor v1.0.0</p>
|
||||
<p>
|
||||
<a
|
||||
href="https://ko-fi.com/macrimi"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-600 hover:underline transition-colors"
|
||||
>
|
||||
Support and contribute to the project
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { LayoutDashboard, HardDrive, Network, Server, Cpu, FileText } from "path-to-icons"
|
||||
|
||||
const menuItems = [
|
||||
{ name: "Overview", href: "/", icon: LayoutDashboard },
|
||||
{ name: "Storage", href: "/storage", icon: HardDrive },
|
||||
{ name: "Network", href: "/network", icon: Network },
|
||||
{ name: "Virtual Machines", href: "/virtual-machines", icon: Server },
|
||||
{ name: "Hardware", href: "/hardware", icon: Cpu }, // New Hardware section
|
||||
{ name: "System Logs", href: "/logs", icon: FileText },
|
||||
]
|
||||
@@ -0,0 +1,237 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
||||
import { Progress } from "./ui/progress"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { HardDrive, Database, Archive, AlertTriangle, CheckCircle, Activity, AlertCircle } from "lucide-react"
|
||||
|
||||
interface StorageData {
|
||||
total: number
|
||||
used: number
|
||||
available: number
|
||||
disks: DiskInfo[]
|
||||
}
|
||||
|
||||
interface DiskInfo {
|
||||
name: string
|
||||
mountpoint: string
|
||||
fstype: string
|
||||
total: number
|
||||
used: number
|
||||
available: number
|
||||
usage_percent: number
|
||||
health: string
|
||||
temperature: number
|
||||
}
|
||||
|
||||
const fetchStorageData = async (): Promise<StorageData | null> => {
|
||||
try {
|
||||
console.log("[v0] Fetching storage data from Flask server...")
|
||||
const response = await fetch("/api/storage", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log("[v0] Successfully fetched storage data from Flask:", data)
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to fetch storage data from Flask server:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function StorageMetrics() {
|
||||
const [storageData, setStorageData] = useState<StorageData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const result = await fetchStorageData()
|
||||
|
||||
if (!result) {
|
||||
setError("Flask server not available. Please ensure the server is running.")
|
||||
} else {
|
||||
setStorageData(result)
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
fetchData()
|
||||
const interval = setInterval(fetchData, 60000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center py-8">
|
||||
<div className="text-lg font-medium text-foreground mb-2">Loading storage data...</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !storageData) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="bg-red-500/10 border-red-500/20">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 text-red-600">
|
||||
<AlertCircle className="h-6 w-6" />
|
||||
<div>
|
||||
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
|
||||
<div className="text-sm">
|
||||
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const usagePercent = storageData.total > 0 ? (storageData.used / storageData.total) * 100 : 0
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Storage Overview Cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Total Storage</CardTitle>
|
||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold text-foreground">{storageData.total.toFixed(1)} GB</div>
|
||||
<Progress value={usagePercent} className="mt-2" />
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{storageData.used.toFixed(1)} GB used • {storageData.available.toFixed(1)} GB available
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Used Storage</CardTitle>
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold text-foreground">{storageData.used.toFixed(1)} GB</div>
|
||||
<Progress value={usagePercent} className="mt-2" />
|
||||
<p className="text-xs text-muted-foreground mt-2">{usagePercent.toFixed(1)}% of total space</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Archive className="h-5 w-5 mr-2" />
|
||||
Available
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold text-foreground">{storageData.available.toFixed(1)} GB</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
{((storageData.available / storageData.total) * 100).toFixed(1)}% Free
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Available space</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Activity className="h-5 w-5 mr-2" />
|
||||
Disks
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold text-foreground">{storageData.disks.length}</div>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
{storageData.disks.filter((d) => d.health === "healthy").length} Healthy
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Storage devices</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Disk Details */}
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Database className="h-5 w-5 mr-2" />
|
||||
Storage Devices
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{storageData.disks.map((disk, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-4 rounded-lg border border-border bg-card/50"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<HardDrive className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{disk.name}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{disk.fstype} • {disk.mountpoint}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{disk.used.toFixed(1)} GB / {disk.total.toFixed(1)} GB
|
||||
</div>
|
||||
<Progress value={disk.usage_percent} className="w-24 mt-1" />
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-sm text-muted-foreground">Temp</div>
|
||||
<div className="text-sm font-medium text-foreground">{disk.temperature}°C</div>
|
||||
</div>
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
disk.health === "healthy"
|
||||
? "bg-green-500/10 text-green-500 border-green-500/20"
|
||||
: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
|
||||
}
|
||||
>
|
||||
{disk.health === "healthy" ? (
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
) : (
|
||||
<AlertTriangle className="h-3 w-3 mr-1" />
|
||||
)}
|
||||
{disk.health}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,919 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { HardDrive, Database, AlertTriangle, CheckCircle2, XCircle, Square, Thermometer } from "lucide-react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
|
||||
interface DiskInfo {
|
||||
name: string
|
||||
size?: number // Changed from string to number (KB) for formatMemory()
|
||||
size_formatted?: string // Added formatted size string for display
|
||||
temperature: number
|
||||
health: string
|
||||
power_on_hours?: number
|
||||
smart_status?: string
|
||||
model?: string
|
||||
serial?: string
|
||||
mountpoint?: string
|
||||
fstype?: string
|
||||
total?: number
|
||||
used?: number
|
||||
available?: number
|
||||
usage_percent?: number
|
||||
reallocated_sectors?: number
|
||||
pending_sectors?: number
|
||||
crc_errors?: number
|
||||
rotation_rate?: number
|
||||
power_cycles?: number
|
||||
percentage_used?: number // NVMe: Percentage Used (0-100)
|
||||
media_wearout_indicator?: number // SSD: Media Wearout Indicator
|
||||
wear_leveling_count?: number // SSD: Wear Leveling Count
|
||||
total_lbas_written?: number // SSD/NVMe: Total LBAs Written (GB)
|
||||
ssd_life_left?: number // SSD: SSD Life Left percentage
|
||||
}
|
||||
|
||||
interface ZFSPool {
|
||||
name: string
|
||||
size: string
|
||||
allocated: string
|
||||
free: string
|
||||
health: string
|
||||
}
|
||||
|
||||
interface StorageData {
|
||||
total: number
|
||||
used: number
|
||||
available: number
|
||||
disks: DiskInfo[]
|
||||
zfs_pools: ZFSPool[]
|
||||
disk_count: number
|
||||
healthy_disks: number
|
||||
warning_disks: number
|
||||
critical_disks: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface ProxmoxStorage {
|
||||
name: string
|
||||
type: string
|
||||
status: string
|
||||
total: number
|
||||
used: number
|
||||
available: number
|
||||
percent: number
|
||||
}
|
||||
|
||||
interface ProxmoxStorageData {
|
||||
storage: ProxmoxStorage[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
const formatStorage = (sizeInGB: number): string => {
|
||||
if (sizeInGB < 1) {
|
||||
// Less than 1 GB, show in MB
|
||||
return `${(sizeInGB * 1024).toFixed(1)} MB`
|
||||
} else if (sizeInGB < 1024) {
|
||||
// Less than 1024 GB, show in GB
|
||||
return `${sizeInGB.toFixed(1)} GB`
|
||||
} else {
|
||||
// 1024 GB or more, show in TB
|
||||
return `${(sizeInGB / 1024).toFixed(1)} TB`
|
||||
}
|
||||
}
|
||||
|
||||
export function StorageOverview() {
|
||||
const [storageData, setStorageData] = useState<StorageData | null>(null)
|
||||
const [proxmoxStorage, setProxmoxStorage] = useState<ProxmoxStorageData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [selectedDisk, setSelectedDisk] = useState<DiskInfo | null>(null)
|
||||
const [detailsOpen, setDetailsOpen] = useState(false)
|
||||
|
||||
const fetchStorageData = async () => {
|
||||
try {
|
||||
const baseUrl =
|
||||
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||
|
||||
const [storageResponse, proxmoxResponse] = await Promise.all([
|
||||
fetch(`${baseUrl}/api/storage`),
|
||||
fetch(`${baseUrl}/api/proxmox-storage`),
|
||||
])
|
||||
|
||||
const data = await storageResponse.json()
|
||||
const proxmoxData = await proxmoxResponse.json()
|
||||
|
||||
console.log("[v0] Storage data received:", data)
|
||||
console.log("[v0] Proxmox storage data received:", proxmoxData)
|
||||
|
||||
setStorageData(data)
|
||||
setProxmoxStorage(proxmoxData)
|
||||
} catch (error) {
|
||||
console.error("Error fetching storage data:", error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchStorageData()
|
||||
const interval = setInterval(fetchStorageData, 60000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const getHealthIcon = (health: string) => {
|
||||
switch (health.toLowerCase()) {
|
||||
case "healthy":
|
||||
case "passed":
|
||||
case "online":
|
||||
return <CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
case "warning":
|
||||
return <AlertTriangle className="h-5 w-5 text-yellow-500" />
|
||||
case "critical":
|
||||
case "failed":
|
||||
case "degraded":
|
||||
return <XCircle className="h-5 w-5 text-red-500" />
|
||||
default:
|
||||
return <AlertTriangle className="h-5 w-5 text-gray-500" />
|
||||
}
|
||||
}
|
||||
|
||||
const getHealthBadge = (health: string) => {
|
||||
switch (health.toLowerCase()) {
|
||||
case "healthy":
|
||||
case "passed":
|
||||
case "online":
|
||||
return <Badge className="bg-green-500/10 text-green-500 border-green-500/20">Healthy</Badge>
|
||||
case "warning":
|
||||
return <Badge className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">Warning</Badge>
|
||||
case "critical":
|
||||
case "failed":
|
||||
case "degraded":
|
||||
return <Badge className="bg-red-500/10 text-red-500 border-red-500/20">Critical</Badge>
|
||||
default:
|
||||
return <Badge className="bg-gray-500/10 text-gray-500 border-gray-500/20">Unknown</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
const getTempColor = (temp: number, diskName?: string, rotationRate?: number) => {
|
||||
if (temp === 0) return "text-gray-500"
|
||||
|
||||
// Determinar el tipo de disco
|
||||
let diskType = "HDD" // Por defecto
|
||||
if (diskName) {
|
||||
if (diskName.startsWith("nvme")) {
|
||||
diskType = "NVMe"
|
||||
} else if (!rotationRate || rotationRate === 0) {
|
||||
diskType = "SSD"
|
||||
}
|
||||
}
|
||||
|
||||
// Aplicar rangos de temperatura según el tipo
|
||||
switch (diskType) {
|
||||
case "NVMe":
|
||||
// NVMe: ≤70°C verde, 71-80°C amarillo, >80°C rojo
|
||||
if (temp <= 70) return "text-green-500"
|
||||
if (temp <= 80) return "text-yellow-500"
|
||||
return "text-red-500"
|
||||
|
||||
case "SSD":
|
||||
// SSD: ≤59°C verde, 60-70°C amarillo, >70°C rojo
|
||||
if (temp <= 59) return "text-green-500"
|
||||
if (temp <= 70) return "text-yellow-500"
|
||||
return "text-red-500"
|
||||
|
||||
case "HDD":
|
||||
default:
|
||||
// HDD: ≤45°C verde, 46-55°C amarillo, >55°C rojo
|
||||
if (temp <= 45) return "text-green-500"
|
||||
if (temp <= 55) return "text-yellow-500"
|
||||
return "text-red-500"
|
||||
}
|
||||
}
|
||||
|
||||
const formatHours = (hours: number) => {
|
||||
if (hours === 0) return "N/A"
|
||||
const years = Math.floor(hours / 8760)
|
||||
const days = Math.floor((hours % 8760) / 24)
|
||||
if (years > 0) {
|
||||
return `${years}y ${days}d`
|
||||
}
|
||||
return `${days}d`
|
||||
}
|
||||
|
||||
const formatRotationRate = (rpm: number | undefined) => {
|
||||
if (!rpm || rpm === 0) return "SSD"
|
||||
return `${rpm.toLocaleString()} RPM`
|
||||
}
|
||||
|
||||
const getDiskType = (diskName: string, rotationRate: number | undefined): string => {
|
||||
if (diskName.startsWith("nvme")) {
|
||||
return "NVMe"
|
||||
}
|
||||
if (!rotationRate || rotationRate === 0) {
|
||||
return "SSD"
|
||||
}
|
||||
return "HDD"
|
||||
}
|
||||
|
||||
const getDiskTypeBadge = (diskName: string, rotationRate: number | undefined) => {
|
||||
const diskType = getDiskType(diskName, rotationRate)
|
||||
const badgeStyles: Record<string, { className: string; label: string }> = {
|
||||
NVMe: {
|
||||
className: "bg-purple-500/10 text-purple-500 border-purple-500/20",
|
||||
label: "NVMe",
|
||||
},
|
||||
SSD: {
|
||||
className: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20",
|
||||
label: "SSD",
|
||||
},
|
||||
HDD: {
|
||||
className: "bg-blue-500/10 text-blue-500 border-blue-500/20",
|
||||
label: "HDD",
|
||||
},
|
||||
}
|
||||
return badgeStyles[diskType]
|
||||
}
|
||||
|
||||
const handleDiskClick = (disk: DiskInfo) => {
|
||||
setSelectedDisk(disk)
|
||||
setDetailsOpen(true)
|
||||
}
|
||||
|
||||
const getStorageTypeBadge = (type: string) => {
|
||||
const typeColors: Record<string, string> = {
|
||||
pbs: "bg-purple-500/10 text-purple-500 border-purple-500/20",
|
||||
dir: "bg-blue-500/10 text-blue-500 border-blue-500/20",
|
||||
lvmthin: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20",
|
||||
zfspool: "bg-green-500/10 text-green-500 border-green-500/20",
|
||||
nfs: "bg-orange-500/10 text-orange-500 border-orange-500/20",
|
||||
cifs: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20",
|
||||
}
|
||||
return typeColors[type.toLowerCase()] || "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
||||
}
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case "active":
|
||||
case "online":
|
||||
return <CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
case "inactive":
|
||||
case "offline":
|
||||
return <Square className="h-5 w-5 text-gray-500" />
|
||||
case "error":
|
||||
case "failed":
|
||||
return <AlertTriangle className="h-5 w-5 text-red-500" />
|
||||
default:
|
||||
return <CheckCircle2 className="h-5 w-5 text-gray-500" />
|
||||
}
|
||||
}
|
||||
|
||||
const getWearIndicator = (disk: DiskInfo): { value: number; label: string } | null => {
|
||||
const diskType = getDiskType(disk.name, disk.rotation_rate)
|
||||
|
||||
if (diskType === "NVMe" && disk.percentage_used !== undefined && disk.percentage_used !== null) {
|
||||
return { value: disk.percentage_used, label: "Percentage Used" }
|
||||
}
|
||||
|
||||
if (diskType === "SSD") {
|
||||
// Prioridad: Media Wearout Indicator > Wear Leveling Count > SSD Life Left
|
||||
if (disk.media_wearout_indicator !== undefined && disk.media_wearout_indicator !== null) {
|
||||
return { value: disk.media_wearout_indicator, label: "Media Wearout" }
|
||||
}
|
||||
if (disk.wear_leveling_count !== undefined && disk.wear_leveling_count !== null) {
|
||||
return { value: disk.wear_leveling_count, label: "Wear Level" }
|
||||
}
|
||||
if (disk.ssd_life_left !== undefined && disk.ssd_life_left !== null) {
|
||||
return { value: 100 - disk.ssd_life_left, label: "Life Used" }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const getWearColor = (wearPercent: number): string => {
|
||||
if (wearPercent <= 50) return "text-green-500"
|
||||
if (wearPercent <= 80) return "text-yellow-500"
|
||||
return "text-red-500"
|
||||
}
|
||||
|
||||
const getEstimatedLifeRemaining = (disk: DiskInfo): string | null => {
|
||||
const wearIndicator = getWearIndicator(disk)
|
||||
if (!wearIndicator || !disk.power_on_hours || disk.power_on_hours === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const wearPercent = wearIndicator.value
|
||||
const hoursUsed = disk.power_on_hours
|
||||
|
||||
// Si el desgaste es 0, no podemos calcular
|
||||
if (wearPercent === 0) {
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
// Calcular horas totales estimadas: hoursUsed / (wearPercent / 100)
|
||||
const totalEstimatedHours = hoursUsed / (wearPercent / 100)
|
||||
const remainingHours = totalEstimatedHours - hoursUsed
|
||||
|
||||
// Convertir a años
|
||||
const remainingYears = remainingHours / 8760 // 8760 horas en un año
|
||||
|
||||
if (remainingYears < 1) {
|
||||
const remainingMonths = Math.round(remainingYears * 12)
|
||||
return `~${remainingMonths} months`
|
||||
}
|
||||
|
||||
return `~${remainingYears.toFixed(1)} years`
|
||||
}
|
||||
|
||||
const getDiskHealthBreakdown = () => {
|
||||
if (!storageData || !storageData.disks) {
|
||||
return { normal: 0, warning: 0, critical: 0 }
|
||||
}
|
||||
|
||||
let normal = 0
|
||||
let warning = 0
|
||||
let critical = 0
|
||||
|
||||
storageData.disks.forEach((disk) => {
|
||||
if (disk.temperature === 0) {
|
||||
// Si no hay temperatura, considerarlo normal
|
||||
normal++
|
||||
return
|
||||
}
|
||||
|
||||
const diskType = getDiskType(disk.name, disk.rotation_rate)
|
||||
|
||||
switch (diskType) {
|
||||
case "NVMe":
|
||||
if (disk.temperature <= 70) normal++
|
||||
else if (disk.temperature <= 80) warning++
|
||||
else critical++
|
||||
break
|
||||
case "SSD":
|
||||
if (disk.temperature <= 59) normal++
|
||||
else if (disk.temperature <= 70) warning++
|
||||
else critical++
|
||||
break
|
||||
case "HDD":
|
||||
default:
|
||||
if (disk.temperature <= 45) normal++
|
||||
else if (disk.temperature <= 55) warning++
|
||||
else critical++
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return { normal, warning, critical }
|
||||
}
|
||||
|
||||
const getDiskTypesBreakdown = () => {
|
||||
if (!storageData || !storageData.disks) {
|
||||
return { nvme: 0, ssd: 0, hdd: 0 }
|
||||
}
|
||||
|
||||
let nvme = 0
|
||||
let ssd = 0
|
||||
let hdd = 0
|
||||
|
||||
storageData.disks.forEach((disk) => {
|
||||
const diskType = getDiskType(disk.name, disk.rotation_rate)
|
||||
if (diskType === "NVMe") nvme++
|
||||
else if (diskType === "SSD") ssd++
|
||||
else if (diskType === "HDD") hdd++
|
||||
})
|
||||
|
||||
return { nvme, ssd, hdd }
|
||||
}
|
||||
|
||||
const getWearProgressColor = (wearPercent: number): string => {
|
||||
if (wearPercent < 70) return "[&>div]:bg-blue-500"
|
||||
if (wearPercent < 85) return "[&>div]:bg-yellow-500"
|
||||
return "[&>div]:bg-red-500"
|
||||
}
|
||||
|
||||
const diskHealthBreakdown = getDiskHealthBreakdown()
|
||||
const diskTypesBreakdown = getDiskTypesBreakdown()
|
||||
|
||||
const totalProxmoxUsed =
|
||||
proxmoxStorage && proxmoxStorage.storage
|
||||
? proxmoxStorage.storage
|
||||
.filter(
|
||||
(storage) => storage && storage.total > 0 && storage.status && storage.status.toLowerCase() === "active",
|
||||
)
|
||||
.reduce((sum, storage) => sum + storage.used, 0)
|
||||
: 0
|
||||
|
||||
const usagePercent =
|
||||
storageData && storageData.total > 0 ? ((totalProxmoxUsed / (storageData.total * 1024)) * 100).toFixed(2) : "0.00"
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-muted-foreground">Loading storage information...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!storageData || storageData.error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-red-500">Error loading storage data: {storageData?.error || "Unknown error"}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Storage Summary */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Storage</CardTitle>
|
||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold">{storageData.total.toFixed(1)} TB</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">{storageData.disk_count} physical disks</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Used Storage</CardTitle>
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold">{formatStorage(totalProxmoxUsed)}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">{usagePercent}% used</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Disk Health */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Disk Health</CardTitle>
|
||||
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold">{storageData.disk_count} disks</div>
|
||||
<p className="text-xs mt-1">
|
||||
<span className="text-green-500">{diskHealthBreakdown.normal} normal</span>
|
||||
{diskHealthBreakdown.warning > 0 && (
|
||||
<>
|
||||
{", "}
|
||||
<span className="text-yellow-500">{diskHealthBreakdown.warning} warning</span>
|
||||
</>
|
||||
)}
|
||||
{diskHealthBreakdown.critical > 0 && (
|
||||
<>
|
||||
{", "}
|
||||
<span className="text-red-500">{diskHealthBreakdown.critical} critical</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Disk Types */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Disk Types</CardTitle>
|
||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold">{storageData.disk_count} disks</div>
|
||||
<p className="text-xs mt-1">
|
||||
{diskTypesBreakdown.nvme > 0 && <span className="text-purple-500">{diskTypesBreakdown.nvme} NVMe</span>}
|
||||
{diskTypesBreakdown.ssd > 0 && (
|
||||
<>
|
||||
{diskTypesBreakdown.nvme > 0 && ", "}
|
||||
<span className="text-cyan-500">{diskTypesBreakdown.ssd} SSD</span>
|
||||
</>
|
||||
)}
|
||||
{diskTypesBreakdown.hdd > 0 && (
|
||||
<>
|
||||
{(diskTypesBreakdown.nvme > 0 || diskTypesBreakdown.ssd > 0) && ", "}
|
||||
<span className="text-blue-500">{diskTypesBreakdown.hdd} HDD</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{proxmoxStorage && proxmoxStorage.storage && proxmoxStorage.storage.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
Proxmox Storage
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{proxmoxStorage.storage
|
||||
.filter((storage) => storage && storage.name && storage.total > 0)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((storage) => (
|
||||
<div key={storage.name} className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
{/* Desktop: Icon + Name + Badge tipo alineados horizontalmente */}
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
<Database className="h-5 w-5 text-muted-foreground" />
|
||||
<h3 className="font-semibold text-lg">{storage.name}</h3>
|
||||
<Badge className={getStorageTypeBadge(storage.type)}>{storage.type}</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex md:hidden items-center gap-2 flex-1">
|
||||
<Database className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||
<Badge className={getStorageTypeBadge(storage.type)}>{storage.type}</Badge>
|
||||
<h3 className="font-semibold text-base flex-1 min-w-0 truncate">{storage.name}</h3>
|
||||
{getStatusIcon(storage.status)}
|
||||
</div>
|
||||
|
||||
{/* Desktop: Badge active + Porcentaje */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<Badge
|
||||
className={
|
||||
storage.status === "active"
|
||||
? "bg-green-500/10 text-green-500 border-green-500/20"
|
||||
: "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
||||
}
|
||||
>
|
||||
{storage.status}
|
||||
</Badge>
|
||||
<span className="text-sm font-medium">{storage.percent}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Progress
|
||||
value={storage.percent}
|
||||
className={`h-2 ${
|
||||
storage.percent > 90
|
||||
? "[&>div]:bg-red-500"
|
||||
: storage.percent > 75
|
||||
? "[&>div]:bg-yellow-500"
|
||||
: "[&>div]:bg-blue-500"
|
||||
}`}
|
||||
/>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Total</p>
|
||||
<p className="font-medium">{storage.total.toLocaleString()} GB</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Used</p>
|
||||
<p
|
||||
className={`font-medium ${
|
||||
storage.percent > 90
|
||||
? "text-red-400"
|
||||
: storage.percent > 75
|
||||
? "text-yellow-400"
|
||||
: "text-blue-400"
|
||||
}`}
|
||||
>
|
||||
{storage.used.toLocaleString()} GB
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Available</p>
|
||||
<p className="font-medium text-green-400">{storage.available.toLocaleString()} GB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* ZFS Pools */}
|
||||
{storageData.zfs_pools && storageData.zfs_pools.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
ZFS Pools
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{storageData.zfs_pools.map((pool) => (
|
||||
<div key={pool.name} className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-semibold text-lg">{pool.name}</h3>
|
||||
{getHealthBadge(pool.health)}
|
||||
</div>
|
||||
{getHealthIcon(pool.health)}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Size</p>
|
||||
<p className="font-medium">{pool.size}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Allocated</p>
|
||||
<p className="font-medium">{pool.allocated}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Free</p>
|
||||
<p className="font-medium">{pool.free}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Physical Disks */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<HardDrive className="h-5 w-5" />
|
||||
Physical Disks & SMART Status
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{storageData.disks.map((disk) => (
|
||||
<div key={disk.name}>
|
||||
<div
|
||||
className="sm:hidden border border-white/10 rounded-lg p-4 cursor-pointer bg-white/5 transition-colors"
|
||||
onClick={() => handleDiskClick(disk)}
|
||||
>
|
||||
<div className="space-y-2 mb-3">
|
||||
{/* Row 1: Device name and type badge */}
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||
<h3 className="font-semibold">/dev/{disk.name}</h3>
|
||||
<Badge className={getDiskTypeBadge(disk.name, disk.rotation_rate).className}>
|
||||
{getDiskTypeBadge(disk.name, disk.rotation_rate).label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Model, temperature, and health status */}
|
||||
<div className="flex items-center justify-between gap-3 pl-7">
|
||||
{disk.model && disk.model !== "Unknown" && (
|
||||
<p className="text-sm text-muted-foreground truncate flex-1 min-w-0">{disk.model}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
{disk.temperature > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Thermometer
|
||||
className={`h-4 w-4 ${getTempColor(disk.temperature, disk.name, disk.rotation_rate)}`}
|
||||
/>
|
||||
<span
|
||||
className={`text-sm font-medium ${getTempColor(disk.temperature, disk.name, disk.rotation_rate)}`}
|
||||
>
|
||||
{disk.temperature}°C
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{getHealthBadge(disk.health)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
{disk.size_formatted && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Size</p>
|
||||
<p className="font-medium">{disk.size_formatted}</p>
|
||||
</div>
|
||||
)}
|
||||
{disk.smart_status && disk.smart_status !== "unknown" && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">SMART Status</p>
|
||||
<p className="font-medium capitalize">{disk.smart_status}</p>
|
||||
</div>
|
||||
)}
|
||||
{disk.power_on_hours !== undefined && disk.power_on_hours > 0 && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Power On Time</p>
|
||||
<p className="font-medium">{formatHours(disk.power_on_hours)}</p>
|
||||
</div>
|
||||
)}
|
||||
{disk.serial && disk.serial !== "Unknown" && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Serial</p>
|
||||
<p className="font-medium text-xs">{disk.serial}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="hidden sm:block border border-white/10 rounded-lg p-4 cursor-pointer bg-card hover:bg-white/5 transition-colors"
|
||||
onClick={() => handleDiskClick(disk)}
|
||||
>
|
||||
<div className="space-y-2 mb-3">
|
||||
{/* Row 1: Device name and type badge */}
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||
<h3 className="font-semibold">/dev/{disk.name}</h3>
|
||||
<Badge className={getDiskTypeBadge(disk.name, disk.rotation_rate).className}>
|
||||
{getDiskTypeBadge(disk.name, disk.rotation_rate).label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Model, temperature, and health status */}
|
||||
<div className="flex items-center justify-between gap-3 pl-7">
|
||||
{disk.model && disk.model !== "Unknown" && (
|
||||
<p className="text-sm text-muted-foreground truncate flex-1 min-w-0">{disk.model}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
{disk.temperature > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Thermometer
|
||||
className={`h-4 w-4 ${getTempColor(disk.temperature, disk.name, disk.rotation_rate)}`}
|
||||
/>
|
||||
<span
|
||||
className={`text-sm font-medium ${getTempColor(disk.temperature, disk.name, disk.rotation_rate)}`}
|
||||
>
|
||||
{disk.temperature}°C
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{getHealthBadge(disk.health)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
{disk.size_formatted && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Size</p>
|
||||
<p className="font-medium">{disk.size_formatted}</p>
|
||||
</div>
|
||||
)}
|
||||
{disk.smart_status && disk.smart_status !== "unknown" && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">SMART Status</p>
|
||||
<p className="font-medium capitalize">{disk.smart_status}</p>
|
||||
</div>
|
||||
)}
|
||||
{disk.power_on_hours !== undefined && disk.power_on_hours > 0 && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Power On Time</p>
|
||||
<p className="font-medium">{formatHours(disk.power_on_hours)}</p>
|
||||
</div>
|
||||
)}
|
||||
{disk.serial && disk.serial !== "Unknown" && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Serial</p>
|
||||
<p className="font-medium text-xs">{disk.serial}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Disk Details Dialog */}
|
||||
<Dialog open={detailsOpen} onOpenChange={setDetailsOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<HardDrive className="h-5 w-5" />
|
||||
Disk Details: /dev/{selectedDisk?.name}
|
||||
</DialogTitle>
|
||||
<DialogDescription>Complete SMART information and health status</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedDisk && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Model</p>
|
||||
<p className="font-medium">{selectedDisk.model}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Serial Number</p>
|
||||
<p className="font-medium">{selectedDisk.serial}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Capacity</p>
|
||||
<p className="font-medium">{selectedDisk.size_formatted}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Health Status</p>
|
||||
<div className="mt-1">{getHealthBadge(selectedDisk.health)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wear & Lifetime Section */}
|
||||
{getWearIndicator(selectedDisk) && (
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="font-semibold mb-3">Wear & Lifetime</h4>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-sm text-muted-foreground">{getWearIndicator(selectedDisk)!.label}</p>
|
||||
<p className={`font-medium ${getWearColor(getWearIndicator(selectedDisk)!.value)}`}>
|
||||
{getWearIndicator(selectedDisk)!.value}%
|
||||
</p>
|
||||
</div>
|
||||
<Progress
|
||||
value={getWearIndicator(selectedDisk)!.value}
|
||||
className={`h-2 ${getWearProgressColor(getWearIndicator(selectedDisk)!.value)}`}
|
||||
/>
|
||||
</div>
|
||||
{getEstimatedLifeRemaining(selectedDisk) && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Estimated Life Remaining</p>
|
||||
<p className="font-medium">{getEstimatedLifeRemaining(selectedDisk)}</p>
|
||||
</div>
|
||||
{selectedDisk.total_lbas_written && selectedDisk.total_lbas_written > 0 && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Total Data Written</p>
|
||||
<p className="font-medium">
|
||||
{selectedDisk.total_lbas_written >= 1024
|
||||
? `${(selectedDisk.total_lbas_written / 1024).toFixed(2)} TB`
|
||||
: `${selectedDisk.total_lbas_written.toFixed(2)} GB`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="font-semibold mb-3">SMART Attributes</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Temperature</p>
|
||||
<p
|
||||
className={`font-medium ${getTempColor(selectedDisk.temperature, selectedDisk.name, selectedDisk.rotation_rate)}`}
|
||||
>
|
||||
{selectedDisk.temperature > 0 ? `${selectedDisk.temperature}°C` : "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Power On Hours</p>
|
||||
<p className="font-medium">
|
||||
{selectedDisk.power_on_hours && selectedDisk.power_on_hours > 0
|
||||
? `${selectedDisk.power_on_hours.toLocaleString()}h (${formatHours(selectedDisk.power_on_hours)})`
|
||||
: "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Rotation Rate</p>
|
||||
<p className="font-medium">{formatRotationRate(selectedDisk.rotation_rate)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Power Cycles</p>
|
||||
<p className="font-medium">
|
||||
{selectedDisk.power_cycles && selectedDisk.power_cycles > 0
|
||||
? selectedDisk.power_cycles.toLocaleString()
|
||||
: "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">SMART Status</p>
|
||||
<p className="font-medium capitalize">{selectedDisk.smart_status}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Reallocated Sectors</p>
|
||||
<p
|
||||
className={`font-medium ${selectedDisk.reallocated_sectors && selectedDisk.reallocated_sectors > 0 ? "text-yellow-500" : ""}`}
|
||||
>
|
||||
{selectedDisk.reallocated_sectors ?? 0}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Pending Sectors</p>
|
||||
<p
|
||||
className={`font-medium ${selectedDisk.pending_sectors && selectedDisk.pending_sectors > 0 ? "text-yellow-500" : ""}`}
|
||||
>
|
||||
{selectedDisk.pending_sectors ?? 0}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">CRC Errors</p>
|
||||
<p
|
||||
className={`font-medium ${selectedDisk.crc_errors && selectedDisk.crc_errors > 0 ? "text-yellow-500" : ""}`}
|
||||
>
|
||||
{selectedDisk.crc_errors ?? 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,816 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
||||
import { Progress } from "./ui/progress"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Network } from "lucide-react"
|
||||
import { NodeMetricsCharts } from "./node-metrics-charts"
|
||||
import { NetworkTrafficChart } from "./network-traffic-chart"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||
|
||||
interface SystemData {
|
||||
cpu_usage: number
|
||||
memory_usage: number
|
||||
memory_total: number
|
||||
memory_used: number
|
||||
temperature: number
|
||||
uptime: string
|
||||
load_average: number[]
|
||||
hostname: string
|
||||
node_id: string
|
||||
timestamp: string
|
||||
cpu_cores?: number
|
||||
cpu_threads?: number
|
||||
proxmox_version?: string
|
||||
kernel_version?: string
|
||||
available_updates?: number
|
||||
}
|
||||
|
||||
interface VMData {
|
||||
vmid: number
|
||||
name: string
|
||||
status: string
|
||||
cpu: number
|
||||
mem: number
|
||||
maxmem: number
|
||||
disk: number
|
||||
maxdisk: number
|
||||
uptime: number
|
||||
type?: string
|
||||
}
|
||||
|
||||
interface StorageData {
|
||||
total: number
|
||||
used: number
|
||||
available: number
|
||||
disk_count: number
|
||||
disks: Array<{
|
||||
name: string
|
||||
mountpoint: string
|
||||
total: number
|
||||
used: number
|
||||
available: number
|
||||
usage_percent: number
|
||||
}>
|
||||
}
|
||||
|
||||
interface NetworkData {
|
||||
interfaces: Array<{
|
||||
name: string
|
||||
status: string
|
||||
addresses: Array<{ ip: string; netmask: string }>
|
||||
}>
|
||||
traffic: {
|
||||
bytes_sent: number
|
||||
bytes_recv: number
|
||||
packets_sent: number
|
||||
packets_recv: number
|
||||
}
|
||||
physical_active_count?: number
|
||||
physical_total_count?: number
|
||||
bridge_active_count?: number
|
||||
bridge_total_count?: number
|
||||
physical_interfaces?: Array<{
|
||||
name: string
|
||||
status: string
|
||||
addresses: Array<{ ip: string; netmask: string }>
|
||||
}>
|
||||
bridge_interfaces?: Array<{
|
||||
name: string
|
||||
status: string
|
||||
addresses: Array<{ ip: string; netmask: string }>
|
||||
}>
|
||||
}
|
||||
|
||||
interface ProxmoxStorageData {
|
||||
storage: Array<{
|
||||
name: string
|
||||
type: string
|
||||
status: string
|
||||
total: number
|
||||
used: number
|
||||
available: number
|
||||
percent: number
|
||||
}>
|
||||
}
|
||||
|
||||
const fetchSystemData = async (): Promise<SystemData | null> => {
|
||||
try {
|
||||
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||
const apiUrl = `${baseUrl}/api/system`
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
cache: "no-store",
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to fetch system data:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const fetchVMData = async (): Promise<VMData[]> => {
|
||||
try {
|
||||
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||
const apiUrl = `${baseUrl}/api/vms`
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
cache: "no-store",
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return Array.isArray(data) ? data : data.vms || []
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to fetch VM data:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const fetchStorageData = async (): Promise<StorageData | null> => {
|
||||
try {
|
||||
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||
const apiUrl = `${baseUrl}/api/storage/summary`
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
cache: "no-store",
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.log("[v0] Storage API not available (this is normal if not configured)")
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.log("[v0] Storage data unavailable:", error instanceof Error ? error.message : "Unknown error")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const fetchNetworkData = async (): Promise<NetworkData | null> => {
|
||||
try {
|
||||
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||
const apiUrl = `${baseUrl}/api/network/summary`
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
cache: "no-store",
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.log("[v0] Network API not available (this is normal if not configured)")
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.log("[v0] Network data unavailable:", error instanceof Error ? error.message : "Unknown error")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const fetchProxmoxStorageData = async (): Promise<ProxmoxStorageData | null> => {
|
||||
try {
|
||||
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||
const apiUrl = `${baseUrl}/api/proxmox-storage`
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
cache: "no-store",
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.log("[v0] Proxmox storage API not available")
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.log("[v0] Proxmox storage data unavailable:", error instanceof Error ? error.message : "Unknown error")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function SystemOverview() {
|
||||
const [systemData, setSystemData] = useState<SystemData | null>(null)
|
||||
const [vmData, setVmData] = useState<VMData[]>([])
|
||||
const [storageData, setStorageData] = useState<StorageData | null>(null)
|
||||
const [proxmoxStorageData, setProxmoxStorageData] = useState<ProxmoxStorageData | null>(null)
|
||||
const [networkData, setNetworkData] = useState<NetworkData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [networkTimeframe, setNetworkTimeframe] = useState("day")
|
||||
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const systemResult = await fetchSystemData()
|
||||
|
||||
if (!systemResult) {
|
||||
setError("Flask server not available. Please ensure the server is running.")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setSystemData(systemResult)
|
||||
} catch (err) {
|
||||
console.error("[v0] Error fetching system data:", err)
|
||||
setError("Failed to connect to Flask server. Please check your connection.")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
|
||||
const systemInterval = setInterval(() => {
|
||||
fetchSystemData().then((data) => {
|
||||
if (data) setSystemData(data)
|
||||
})
|
||||
}, 10000)
|
||||
|
||||
return () => {
|
||||
clearInterval(systemInterval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchVMs = async () => {
|
||||
const vmResult = await fetchVMData()
|
||||
setVmData(vmResult)
|
||||
}
|
||||
|
||||
fetchVMs()
|
||||
const vmInterval = setInterval(fetchVMs, 60000)
|
||||
|
||||
return () => {
|
||||
clearInterval(vmInterval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStorage = async () => {
|
||||
const storageResult = await fetchStorageData()
|
||||
setStorageData(storageResult)
|
||||
|
||||
const proxmoxStorageResult = await fetchProxmoxStorageData()
|
||||
setProxmoxStorageData(proxmoxStorageResult)
|
||||
}
|
||||
|
||||
fetchStorage()
|
||||
const storageInterval = setInterval(fetchStorage, 60000)
|
||||
|
||||
return () => {
|
||||
clearInterval(storageInterval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchNetwork = async () => {
|
||||
const networkResult = await fetchNetworkData()
|
||||
setNetworkData(networkResult)
|
||||
}
|
||||
|
||||
fetchNetwork()
|
||||
const networkInterval = setInterval(fetchNetwork, 60000)
|
||||
|
||||
return () => {
|
||||
clearInterval(networkInterval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center py-8">
|
||||
<div className="text-lg font-medium text-foreground mb-2">Connecting to ProxMenux Monitor...</div>
|
||||
<div className="text-sm text-muted-foreground">Fetching real-time system data</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Card key={i} className="bg-card border-border animate-pulse">
|
||||
<CardContent className="p-6">
|
||||
<div className="h-4 bg-muted rounded w-1/2 mb-4"></div>
|
||||
<div className="h-8 bg-muted rounded w-3/4 mb-2"></div>
|
||||
<div className="h-2 bg-muted rounded w-full mb-2"></div>
|
||||
<div className="h-3 bg-muted rounded w-2/3"></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !systemData) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="bg-red-500/10 border-red-500/20">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 text-red-600">
|
||||
<AlertCircle className="h-6 w-6" />
|
||||
<div>
|
||||
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
|
||||
<div className="text-sm">
|
||||
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const vmStats = {
|
||||
total: vmData.length,
|
||||
running: vmData.filter((vm) => vm.status === "running").length,
|
||||
stopped: vmData.filter((vm) => vm.status === "stopped").length,
|
||||
lxc: vmData.filter((vm) => vm.type === "lxc").length,
|
||||
vms: vmData.filter((vm) => vm.type === "qemu" || !vm.type).length,
|
||||
}
|
||||
|
||||
const getTemperatureStatus = (temp: number) => {
|
||||
if (temp === 0) return { status: "N/A", color: "bg-gray-500/10 text-gray-500 border-gray-500/20" }
|
||||
if (temp < 60) return { status: "Normal", color: "bg-green-500/10 text-green-500 border-green-500/20" }
|
||||
if (temp < 75) return { status: "Warm", color: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" }
|
||||
return { status: "Hot", color: "bg-red-500/10 text-red-500 border-red-500/20" }
|
||||
}
|
||||
|
||||
const formatUptime = (seconds: number) => {
|
||||
if (!seconds || seconds === 0) return "Stopped"
|
||||
const days = Math.floor(seconds / 86400)
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
if (days > 0) return `${days}d ${hours}h`
|
||||
if (hours > 0) return `${hours}h ${minutes}m`
|
||||
return `${minutes}m`
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
return (bytes / 1024 ** 3).toFixed(2)
|
||||
}
|
||||
|
||||
const formatStorage = (sizeInGB: number): string => {
|
||||
if (sizeInGB < 1) {
|
||||
// Less than 1 GB, show in MB
|
||||
return `${(sizeInGB * 1024).toFixed(1)} MB`
|
||||
} else if (sizeInGB < 1024) {
|
||||
// Less than 1024 GB, show in GB
|
||||
return `${sizeInGB.toFixed(1)} GB`
|
||||
} else {
|
||||
// 1024 GB or more, show in TB
|
||||
return `${(sizeInGB / 1024).toFixed(2)} TB`
|
||||
}
|
||||
}
|
||||
|
||||
const tempStatus = getTemperatureStatus(systemData.temperature)
|
||||
|
||||
const localStorage = proxmoxStorageData?.storage.find((s) => s.name === "local")
|
||||
|
||||
const vmLxcStorages = proxmoxStorageData?.storage.filter(
|
||||
(s) =>
|
||||
// Include only local storage types that can host VMs/LXCs
|
||||
(s.type === "lvm" || s.type === "lvmthin" || s.type === "zfspool" || s.type === "btrfs" || s.type === "dir") &&
|
||||
// Exclude network storage
|
||||
s.type !== "nfs" &&
|
||||
s.type !== "cifs" &&
|
||||
s.type !== "iscsi" &&
|
||||
// Exclude the "local" storage (used for ISOs/templates)
|
||||
s.name !== "local",
|
||||
)
|
||||
|
||||
const vmLxcStorageTotal = vmLxcStorages?.reduce((acc, s) => acc + s.total, 0) || 0
|
||||
const vmLxcStorageUsed = vmLxcStorages?.reduce((acc, s) => acc + s.used, 0) || 0
|
||||
const vmLxcStorageAvailable = vmLxcStorages?.reduce((acc, s) => acc + s.available, 0) || 0
|
||||
const vmLxcStoragePercent = vmLxcStorageTotal > 0 ? (vmLxcStorageUsed / vmLxcStorageTotal) * 100 : 0
|
||||
|
||||
const getLoadStatus = (load: number, cores: number) => {
|
||||
if (load < cores) {
|
||||
return { status: "Normal", color: "bg-green-500/10 text-green-500 border-green-500/20" }
|
||||
} else if (load < cores * 1.5) {
|
||||
return { status: "Moderate", color: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" }
|
||||
} else {
|
||||
return { status: "High", color: "bg-red-500/10 text-red-500 border-red-500/20" }
|
||||
}
|
||||
}
|
||||
|
||||
const systemAlerts = []
|
||||
if (systemData.available_updates && systemData.available_updates > 0) {
|
||||
systemAlerts.push({
|
||||
type: "warning",
|
||||
message: `${systemData.available_updates} updates available`,
|
||||
})
|
||||
}
|
||||
if (vmStats.stopped > 0) {
|
||||
systemAlerts.push({
|
||||
type: "info",
|
||||
message: `${vmStats.stopped} VM${vmStats.stopped > 1 ? "s" : ""} stopped`,
|
||||
})
|
||||
}
|
||||
if (systemData.temperature > 75) {
|
||||
systemAlerts.push({
|
||||
type: "warning",
|
||||
message: "High temperature detected",
|
||||
})
|
||||
}
|
||||
if (localStorage && localStorage.percent > 90) {
|
||||
systemAlerts.push({
|
||||
type: "warning",
|
||||
message: "System storage almost full",
|
||||
})
|
||||
}
|
||||
|
||||
const loadStatus = getLoadStatus(systemData.load_average[0], systemData.cpu_cores || 8)
|
||||
|
||||
const getTimeframeLabel = (timeframe: string): string => {
|
||||
switch (timeframe) {
|
||||
case "hour":
|
||||
return "1h"
|
||||
case "day":
|
||||
return "24h"
|
||||
case "week":
|
||||
return "7d"
|
||||
case "month":
|
||||
return "30d"
|
||||
case "year":
|
||||
return "1y"
|
||||
default:
|
||||
return timeframe
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Key Metrics Cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">CPU Usage</CardTitle>
|
||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold text-foreground">{systemData.cpu_usage}%</div>
|
||||
<Progress value={systemData.cpu_usage} className="mt-2 [&>div]:bg-blue-500" />
|
||||
<p className="text-xs text-muted-foreground mt-2">Real-time usage</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Memory Usage</CardTitle>
|
||||
<MemoryStick className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold text-foreground">{systemData.memory_used.toFixed(1)} GB</div>
|
||||
<Progress value={systemData.memory_usage} className="mt-2 [&>div]:bg-blue-500" />
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
<span className="text-green-500 font-medium">{systemData.memory_usage.toFixed(1)}%</span> of{" "}
|
||||
{systemData.memory_total} GB
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Temperature</CardTitle>
|
||||
<Thermometer className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold text-foreground">
|
||||
{systemData.temperature === 0 ? "N/A" : `${systemData.temperature}°C`}
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<Badge variant="outline" className={tempStatus.color}>
|
||||
{tempStatus.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{systemData.temperature === 0 ? "No sensor available" : "Live temperature reading"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Active VM & LXC</CardTitle>
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold text-foreground">{vmStats.running}</div>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
{vmStats.running} Running
|
||||
</Badge>
|
||||
{vmStats.stopped > 0 && (
|
||||
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20">
|
||||
{vmStats.stopped} Stopped
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Total: {vmStats.vms} VMs, {vmStats.lxc} LXC
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Node Metrics Charts */}
|
||||
<NodeMetricsCharts />
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Storage Summary */}
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<HardDrive className="h-5 w-5 mr-2" />
|
||||
Storage Overview
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{storageData ? (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2 pb-3 border-b border-border">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Total Capacity:</span>
|
||||
<span className="text-lg font-semibold text-foreground">{storageData.total} TB</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Physical Disks:</span>
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
{storageData.disk_count} disk{storageData.disk_count !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{vmLxcStorages && vmLxcStorages.length > 0 ? (
|
||||
<div className="space-y-2 pb-3 border-b border-border">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">VM/LXC Storage</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">Used:</span>
|
||||
<span className="text-sm font-semibold text-foreground">{formatStorage(vmLxcStorageUsed)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">Available:</span>
|
||||
<span className="text-sm font-semibold text-green-500">
|
||||
{formatStorage(vmLxcStorageAvailable)}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={vmLxcStoragePercent} className="mt-2 [&>div]:bg-blue-500" />
|
||||
<div className="flex justify-between items-center mt-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatStorage(vmLxcStorageUsed)} / {formatStorage(vmLxcStorageTotal)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{vmLxcStoragePercent.toFixed(1)}%</span>
|
||||
</div>
|
||||
{vmLxcStorages.length > 1 && (
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{vmLxcStorages.length} storage volume{vmLxcStorages.length > 1 ? "s" : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 pb-3 border-b border-border">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">VM/LXC Storage</div>
|
||||
<div className="text-center py-4 text-muted-foreground text-sm">No VM/LXC storage configured</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{localStorage && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">Local Storage (System)</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">Used:</span>
|
||||
<span className="text-sm font-semibold text-foreground">{formatStorage(localStorage.used)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">Available:</span>
|
||||
<span className="text-sm font-semibold text-green-500">
|
||||
{formatStorage(localStorage.available)}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={localStorage.percent} className="mt-2 [&>div]:bg-purple-500" />
|
||||
<div className="flex justify-between items-center mt-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatStorage(localStorage.used)} / {formatStorage(localStorage.total)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{localStorage.percent.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground">Storage data not available</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Network Summary */}
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Network className="h-5 w-5 mr-2" />
|
||||
Network Overview
|
||||
</div>
|
||||
<Select value={networkTimeframe} onValueChange={setNetworkTimeframe}>
|
||||
<SelectTrigger className="w-28 h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hour">1 Hour</SelectItem>
|
||||
<SelectItem value="day">24 Hours</SelectItem>
|
||||
<SelectItem value="week">7 Days</SelectItem>
|
||||
<SelectItem value="month">30 Days</SelectItem>
|
||||
<SelectItem value="year">1 Year</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{networkData ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center pb-3 border-b border-border">
|
||||
<span className="text-sm text-muted-foreground">Active Interfaces:</span>
|
||||
<span className="text-lg font-semibold text-foreground">
|
||||
{(networkData.physical_active_count || 0) + (networkData.bridge_active_count || 0)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{networkData.physical_interfaces && networkData.physical_interfaces.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{networkData.physical_interfaces
|
||||
.filter((iface) => iface.status === "up")
|
||||
.map((iface) => (
|
||||
<Badge
|
||||
key={iface.name}
|
||||
variant="outline"
|
||||
className="bg-blue-500/10 text-blue-500 border-blue-500/20"
|
||||
>
|
||||
{iface.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkData.bridge_interfaces && networkData.bridge_interfaces.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{networkData.bridge_interfaces
|
||||
.filter((iface) => iface.status === "up")
|
||||
.map((iface) => (
|
||||
<Badge
|
||||
key={iface.name}
|
||||
variant="outline"
|
||||
className="bg-green-500/10 text-green-500 border-green-500/20"
|
||||
>
|
||||
{iface.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-border space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Received:</span>
|
||||
<span className="text-lg font-semibold text-green-500 flex items-center gap-1">
|
||||
↓ {formatStorage(networkTotals.received)}
|
||||
<span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Sent:</span>
|
||||
<span className="text-lg font-semibold text-blue-500 flex items-center gap-1">
|
||||
↑ {formatStorage(networkTotals.sent)}
|
||||
<span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-3 border-t border-border">
|
||||
<NetworkTrafficChart timeframe={networkTimeframe} onTotalsCalculated={setNetworkTotals} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground">Network data not available</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* System Information */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Server className="h-5 w-5 mr-2" />
|
||||
System Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Uptime:</span>
|
||||
<span className="text-foreground">{systemData.uptime}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Proxmox Version:</span>
|
||||
<span className="text-foreground">{systemData.proxmox_version || "N/A"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Kernel:</span>
|
||||
<span className="text-foreground font-mono text-sm">{systemData.kernel_version || "Linux"}</span>
|
||||
</div>
|
||||
{systemData.available_updates !== undefined && systemData.available_updates > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Available Updates:</span>
|
||||
<Badge variant="outline" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
|
||||
{systemData.available_updates} packages
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* System Health & Alerts */}
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Zap className="h-5 w-5 mr-2" />
|
||||
System Overview
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex justify-between items-center pb-3 border-b border-border">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-muted-foreground">Load Average (1m):</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg font-semibold text-foreground font-mono">
|
||||
{systemData.load_average[0].toFixed(2)}
|
||||
</span>
|
||||
<Badge variant="outline" className={loadStatus.color}>
|
||||
{loadStatus.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pb-3 border-b border-border">
|
||||
<span className="text-sm text-muted-foreground">CPU Threads:</span>
|
||||
<span className="text-lg font-semibold text-foreground">{systemData.cpu_threads || "N/A"}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pb-3 border-b border-border">
|
||||
<span className="text-sm text-muted-foreground">Physical Disks:</span>
|
||||
<span className="text-lg font-semibold text-foreground">{storageData?.disk_count || "N/A"}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Network Interfaces:</span>
|
||||
<span className="text-lg font-semibold text-foreground">
|
||||
{networkData?.physical_total_count || networkData?.physical_interfaces?.length || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
"use client"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import type { ThemeProviderProps } from "next-themes"
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { Button } from "./ui/button"
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
const handleThemeToggle = () => {
|
||||
console.log("[v0] Current theme:", theme)
|
||||
const newTheme = theme === "light" ? "dark" : "light"
|
||||
console.log("[v0] Switching to theme:", newTheme)
|
||||
setTheme(newTheme)
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Button variant="outline" size="sm" className="border-border bg-transparent w-9 h-9">
|
||||
<Sun className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="outline" size="sm" onClick={handleThemeToggle} className="border-border bg-transparent w-9 h-9">
|
||||
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
},
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
|
||||
)
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative h-2 w-full overflow-hidden rounded-full bg-secondary", className)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
@@ -0,0 +1,144 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", className)} {...props} />
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Sheet = DialogPrimitive.Root
|
||||
|
||||
const SheetTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const SheetClose = DialogPrimitive.Close
|
||||
|
||||
const SheetPortal = DialogPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Content>, SheetContentProps>(
|
||||
({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<DialogPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</SheetPortal>
|
||||
),
|
||||
)
|
||||
SheetContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
|
||||
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} />
|
||||
))
|
||||
SheetTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
))
|
||||
SheetDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
trailingSlash: true,
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
experimental: {
|
||||
esmExternals: 'loose',
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
if (!isServer) {
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
fs: false,
|
||||
net: false,
|
||||
tls: false,
|
||||
};
|
||||
}
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "proxmenux-monitor",
|
||||
"version": "1.0.0",
|
||||
"description": "Proxmox System Monitoring Dashboard",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"export": "next build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@radix-ui/react-accordion": "1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "1.1.4",
|
||||
"@radix-ui/react-aspect-ratio": "1.1.1",
|
||||
"@radix-ui/react-avatar": "1.1.2",
|
||||
"@radix-ui/react-checkbox": "1.1.3",
|
||||
"@radix-ui/react-collapsible": "1.1.2",
|
||||
"@radix-ui/react-context-menu": "2.2.4",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.4",
|
||||
"@radix-ui/react-hover-card": "1.1.4",
|
||||
"@radix-ui/react-label": "2.1.1",
|
||||
"@radix-ui/react-menubar": "1.1.4",
|
||||
"@radix-ui/react-navigation-menu": "1.2.3",
|
||||
"@radix-ui/react-popover": "1.1.4",
|
||||
"@radix-ui/react-progress": "1.1.1",
|
||||
"@radix-ui/react-radio-group": "1.2.2",
|
||||
"@radix-ui/react-scroll-area": "1.2.2",
|
||||
"@radix-ui/react-select": "2.1.4",
|
||||
"@radix-ui/react-separator": "1.1.1",
|
||||
"@radix-ui/react-slider": "1.2.2",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-switch": "1.1.2",
|
||||
"@radix-ui/react-tabs": "1.1.2",
|
||||
"@radix-ui/react-toast": "1.2.4",
|
||||
"@radix-ui/react-toggle": "1.1.1",
|
||||
"@radix-ui/react-toggle-group": "1.1.1",
|
||||
"@radix-ui/react-tooltip": "1.1.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"date-fns": "4.1.0",
|
||||
"embla-carousel-react": "8.5.1",
|
||||
"geist": "^1.3.1",
|
||||
"input-otp": "1.4.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
"next": "15.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19",
|
||||
"react-day-picker": "9.8.0",
|
||||
"react-dom": "^19",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "2.15.4",
|
||||
"sonner": "^1.7.4",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.9.9",
|
||||
"zod": "3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 63 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="#0d597f"><path d="M23.25 38.81v-6.745l-4.855 4.864c.474.333.968.635 1.48.906.463.243.87.434 1.303.58s.782.24 1.13.304.66.093.95.096m24.822-.562c.045.037.092.07.142.1a2.77 2.77 0 0 0 .385.203 2.93 2.93 0 0 0 .637.194c.296.06.598.088.9.087.3 0 .608-.03.955-.087a7.24 7.24 0 0 0 1.138-.301 9.96 9.96 0 0 0 1.32-.579c.52-.274 1.02-.58 1.503-.918l-3.685-3.6-12.21-12.258-5.356 5.356-7.23-7.455-18.14 17.935a13.82 13.82 0 0 0 1.5.918c.47.246.91.434 1.317.58a7.18 7.18 0 0 0 1.135.301 5.53 5.53 0 0 0 .955.087c.302.001.604-.028.9-.087a3.29 3.29 0 0 0 .637-.194 2.49 2.49 0 0 0 .385-.197l.145-.104 8.193-8.193 2.924-2.808 8.106 8.106 2.837 2.912a1.29 1.29 0 0 0 .145.101 2.52 2.52 0 0 0 .385.2c.206.085.42.15.637.194.255.052.556.087.903.087.3 0 .608-.03.955-.087a6.89 6.89 0 0 0 1.138-.301 9.95 9.95 0 0 0 1.32-.579c.52-.274 1.02-.58 1.503-.918l-6.508-6.37 1.2-1.2 5.63 5.63 3.283 3.254m-.07-33.96l15.998 27.714L48.003 59.71H15.996L-.002 31.997 15.996 4.283z"/><path d="M38.02 30.65l-4.262-4.256.304-.304 4.3 4.244z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256" version="1.0">
|
||||
<defs>
|
||||
<linearGradient xlink:href="#a" id="d" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-.39377 0 0 .39375 978.34969 416.9815)" x1="541.33502" y1="104.50665" x2="606.91248" y2="303.14029"/>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" id="a" y2="129.3468" x2="112.49853" y1="6.1372099" x1="112.49854" gradientTransform="translate(287 -83)">
|
||||
<stop offset="0" style="stop-color:#fff;stop-opacity:0"/>
|
||||
<stop offset="1" style="stop-color:#fff;stop-opacity:.27450982"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b">
|
||||
<stop style="stop-color:#00bdec" offset="0"/>
|
||||
<stop style="stop-color:#40bfde" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="c">
|
||||
<stop style="stop-color:#6e6e6e" offset="0"/>
|
||||
<stop style="stop-color:#4d4d4d" offset="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path style="fill:#1793d1" d="M128 0c-11.39482 27.937051-18.31337 46.237163-31 73.34375 7.7785 8.245207 17.33826 17.811753 32.84375 28.65625-16.66992-6.859577-28.03357-13.728504-36.53125-20.875C77.076039 115.00489 51.621645 163.24639 0 256c40.562707-23.41756 72.007597-37.86167 101.3125-43.375-1.25376-5.40435-1.923505-11.27752-1.875-17.375l.03125-1.28125c.64379-25.99398 14.16934-45.98224 30.1875-44.625 16.01815 1.35723 28.48754 23.53727 27.84375 49.53125-.12127 4.89622-.6905 9.60082-1.65625 13.96875C184.83328 218.51691 215.98162 232.89667 256 256c-7.89193-14.52962-14.96051-27.61983-21.6875-40.09375-10.59609-8.21269-21.64301-18.89743-44.1875-30.46875 15.4958 4.02645 26.60184 8.6825 35.25 13.875C156.97985 71.972668 151.45422 55.040376 128 0z" transform="matrix(1 0 0 1 -.000002 4e-8)"/>
|
||||
<path style="fill:#fff;fill-opacity:.16568047" d="M818.22607 548.55277c-41.18143-55.89508-50.72685-100.94481-53.14467-111.70015 21.96737 50.6686 21.81733 51.28995 53.14467 111.70015z" transform="matrix(1.34737 0 0 1.34737 -902.40019 -586.944907)"/>
|
||||
<path style="fill:url(#d);fill-opacity:1" d="M765.09805 436.43495c-1.05641 2.59705-2.08559 5.1172-3.06152 7.51465-1.08115 2.65585-2.10928 5.19128-3.13111 7.677-1.02174 2.48575-2.03439 4.91156-3.03833 7.30591-1.00398 2.39446-2.01068 4.76169-3.03833 7.14355-1.02758 2.38177-2.06156 4.78845-3.15429 7.23633-1.09273 2.44796-2.23335 4.94504-3.43262 7.53784-1.19937 2.59282-2.45641 5.27815-3.80371 8.09448-.18662.39008-.41312.83402-.60303 1.22925 5.75521 6.09563 12.84133 13.14976 24.28345 21.15234-12.34021-5.07792-20.76511-10.15751-27.06665-15.44677-.32717.66791-.61387 1.26431-.95093 1.94824-.44365.90024-.97632 1.92315-1.43799 2.85278-.80967 1.66032-1.65574 3.36576-2.52807 5.12574-.33524.66652-.62948 1.24283-.97413 1.92504-5.50733 11.05265-12.33962 24.28304-21.12915 40.72754 24.09557-13.57581 50.08533-33.16242 97.29615-16.30493-2.36708-4.48319-4.54319-8.68756-6.58692-12.64038-2.0437-3.95294-3.94246-7.6555-5.70556-11.15601-1.76297-3.50043-3.39212-6.80069-4.917-9.92675-1.52486-3.12599-2.93832-6.0765-4.26757-8.90625-1.32934-2.8297-2.58106-5.55264-3.75733-8.16407-1.17634-2.6114-2.29708-5.11315-3.36304-7.58422-1.06607-2.4712-2.08657-4.89718-3.08471-7.30591-.99823-2.4088-1.97267-4.81178-2.94556-7.23633-.34772-.86638-.69553-1.7689-1.0437-2.64404-2.66339-6.25269-5.3982-12.73163-8.55835-20.15503z" transform="matrix(1.34737 0 0 1.34737 -902.40019 -586.944907)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 10.0, SVG Export Plug-In . SVG Version: 3.0.0 Build 77) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
|
||||
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
|
||||
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
|
||||
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
|
||||
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
|
||||
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
|
||||
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
|
||||
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
|
||||
<!ENTITY ns_svg "http://www.w3.org/2000/svg">
|
||||
<!ENTITY ns_xlink "http://www.w3.org/1999/xlink">
|
||||
]>
|
||||
<svg
|
||||
xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;" i:viewOrigin="262 450" i:rulerOrigin="0 0" i:pageBounds="0 792 612 0"
|
||||
xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||
width="87.041" height="108.445" viewBox="0 0 87.041 108.445" overflow="visible" enable-background="new 0 0 87.041 108.445"
|
||||
xml:space="preserve">
|
||||
<metadata>
|
||||
<variableSets xmlns="&ns_vars;">
|
||||
<variableSet varSetName="binding1" locked="none">
|
||||
<variables></variables>
|
||||
<v:sampleDataSets xmlns="&ns_custom;" xmlns:v="&ns_vars;"></v:sampleDataSets>
|
||||
</variableSet>
|
||||
</variableSets>
|
||||
<sfw xmlns="&ns_sfw;">
|
||||
<slices></slices>
|
||||
<sliceSourceBounds y="341.555" x="262" width="87.041" height="108.445" bottomLeftOrigin="true"></sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<g id="Layer_1" i:layer="yes" i:dimmedPercent="50" i:rgbTrio="#4F008000FFFF">
|
||||
<g>
|
||||
<path i:knockout="Off" fill="#A80030" d="M51.986,57.297c-1.797,0.025,0.34,0.926,2.686,1.287
|
||||
c0.648-0.506,1.236-1.018,1.76-1.516C54.971,57.426,53.484,57.434,51.986,57.297"/>
|
||||
<path i:knockout="Off" fill="#A80030" d="M61.631,54.893c1.07-1.477,1.85-3.094,2.125-4.766c-0.24,1.192-0.887,2.221-1.496,3.307
|
||||
c-3.359,2.115-0.316-1.256-0.002-2.537C58.646,55.443,61.762,53.623,61.631,54.893"/>
|
||||
<path i:knockout="Off" fill="#A80030" d="M65.191,45.629c0.217-3.236-0.637-2.213-0.924-0.978
|
||||
C64.602,44.825,64.867,46.932,65.191,45.629"/>
|
||||
<path i:knockout="Off" fill="#A80030" d="M45.172,1.399c0.959,0.172,2.072,0.304,1.916,0.533
|
||||
C48.137,1.702,48.375,1.49,45.172,1.399"/>
|
||||
<path i:knockout="Off" fill="#A80030" d="M47.088,1.932l-0.678,0.14l0.631-0.056L47.088,1.932"/>
|
||||
<path i:knockout="Off" fill="#A80030" d="M76.992,46.856c0.107,2.906-0.85,4.316-1.713,6.812l-1.553,0.776
|
||||
c-1.271,2.468,0.123,1.567-0.787,3.53c-1.984,1.764-6.021,5.52-7.313,5.863c-0.943-0.021,0.639-1.113,0.846-1.541
|
||||
c-2.656,1.824-2.131,2.738-6.193,3.846l-0.119-0.264c-10.018,4.713-23.934-4.627-23.751-17.371
|
||||
c-0.107,0.809-0.304,0.607-0.526,0.934c-0.517-6.557,3.028-13.143,9.007-15.832c5.848-2.895,12.704-1.707,16.893,2.197
|
||||
c-2.301-3.014-6.881-6.209-12.309-5.91c-5.317,0.084-10.291,3.463-11.951,7.131c-2.724,1.715-3.04,6.611-4.227,7.507
|
||||
C31.699,56.271,36.3,61.342,44.083,67.307c1.225,0.826,0.345,0.951,0.511,1.58c-2.586-1.211-4.954-3.039-6.901-5.277
|
||||
c1.033,1.512,2.148,2.982,3.589,4.137c-2.438-0.826-5.695-5.908-6.646-6.115c4.203,7.525,17.052,13.197,23.78,10.383
|
||||
c-3.113,0.115-7.068,0.064-10.566-1.229c-1.469-0.756-3.467-2.322-3.11-2.615c9.182,3.43,18.667,2.598,26.612-3.771
|
||||
c2.021-1.574,4.229-4.252,4.867-4.289c-0.961,1.445,0.164,0.695-0.574,1.971c2.014-3.248-0.875-1.322,2.082-5.609l1.092,1.504
|
||||
c-0.406-2.696,3.348-5.97,2.967-10.234c0.861-1.304,0.961,1.403,0.047,4.403c1.268-3.328,0.334-3.863,0.66-6.609
|
||||
c0.352,0.923,0.814,1.904,1.051,2.878c-0.826-3.216,0.848-5.416,1.262-7.285c-0.408-0.181-1.275,1.422-1.473-2.377
|
||||
c0.029-1.65,0.459-0.865,0.625-1.271c-0.324-0.186-1.174-1.451-1.691-3.877c0.375-0.57,1.002,1.478,1.512,1.562
|
||||
c-0.328-1.929-0.893-3.4-0.916-4.88c-1.49-3.114-0.527,0.415-1.736-1.337c-1.586-4.947,1.316-1.148,1.512-3.396
|
||||
c2.404,3.483,3.775,8.881,4.404,11.117c-0.48-2.726-1.256-5.367-2.203-7.922c0.73,0.307-1.176-5.609,0.949-1.691
|
||||
c-2.27-8.352-9.715-16.156-16.564-19.818c0.838,0.767,1.896,1.73,1.516,1.881c-3.406-2.028-2.807-2.186-3.295-3.043
|
||||
c-2.775-1.129-2.957,0.091-4.795,0.002c-5.23-2.774-6.238-2.479-11.051-4.217l0.219,1.023c-3.465-1.154-4.037,0.438-7.782,0.004
|
||||
c-0.228-0.178,1.2-0.644,2.375-0.815c-3.35,0.442-3.193-0.66-6.471,0.122c0.808-0.567,1.662-0.942,2.524-1.424
|
||||
c-2.732,0.166-6.522,1.59-5.352,0.295c-4.456,1.988-12.37,4.779-16.811,8.943l-0.14-0.933c-2.035,2.443-8.874,7.296-9.419,10.46
|
||||
l-0.544,0.127c-1.059,1.793-1.744,3.825-2.584,5.67c-1.385,2.36-2.03,0.908-1.833,1.278c-2.724,5.523-4.077,10.164-5.246,13.97
|
||||
c0.833,1.245,0.02,7.495,0.335,12.497c-1.368,24.704,17.338,48.69,37.785,54.228c2.997,1.072,7.454,1.031,11.245,1.141
|
||||
c-4.473-1.279-5.051-0.678-9.408-2.197c-3.143-1.48-3.832-3.17-6.058-5.102l0.881,1.557c-4.366-1.545-2.539-1.912-6.091-3.037
|
||||
l0.941-1.229c-1.415-0.107-3.748-2.385-4.386-3.646l-1.548,0.061c-1.86-2.295-2.851-3.949-2.779-5.23l-0.5,0.891
|
||||
c-0.567-0.973-6.843-8.607-3.587-6.83c-0.605-0.553-1.409-0.9-2.281-2.484l0.663-0.758c-1.567-2.016-2.884-4.6-2.784-5.461
|
||||
c0.836,1.129,1.416,1.34,1.99,1.533c-3.957-9.818-4.179-0.541-7.176-9.994l0.634-0.051c-0.486-0.732-0.781-1.527-1.172-2.307
|
||||
l0.276-2.75C4.667,58.121,6.719,47.409,7.13,41.534c0.285-2.389,2.378-4.932,3.97-8.92l-0.97-0.167
|
||||
c1.854-3.234,10.586-12.988,14.63-12.486c1.959-2.461-0.389-0.009-0.772-0.629c4.303-4.453,5.656-3.146,8.56-3.947
|
||||
c3.132-1.859-2.688,0.725-1.203-0.709c5.414-1.383,3.837-3.144,10.9-3.846c0.745,0.424-1.729,0.655-2.35,1.205
|
||||
c4.511-2.207,14.275-1.705,20.617,1.225c7.359,3.439,15.627,13.605,15.953,23.17l0.371,0.1
|
||||
c-0.188,3.802,0.582,8.199-0.752,12.238L76.992,46.856"/>
|
||||
<path i:knockout="Off" fill="#A80030" d="M32.372,59.764l-0.252,1.26c1.181,1.604,2.118,3.342,3.626,4.596
|
||||
C34.661,63.502,33.855,62.627,32.372,59.764"/>
|
||||
<path i:knockout="Off" fill="#A80030" d="M35.164,59.654c-0.625-0.691-0.995-1.523-1.409-2.352
|
||||
c0.396,1.457,1.207,2.709,1.962,3.982L35.164,59.654"/>
|
||||
<path i:knockout="Off" fill="#A80030" d="M84.568,48.916l-0.264,0.662c-0.484,3.438-1.529,6.84-3.131,9.994
|
||||
C82.943,56.244,84.088,52.604,84.568,48.916"/>
|
||||
<path i:knockout="Off" fill="#A80030" d="M45.527,0.537C46.742,0.092,48.514,0.293,49.803,0c-1.68,0.141-3.352,0.225-5.003,0.438
|
||||
L45.527,0.537"/>
|
||||
<path i:knockout="Off" fill="#A80030" d="M2.872,23.219c0.28,2.592-1.95,3.598,0.494,1.889
|
||||
C4.676,22.157,2.854,24.293,2.872,23.219"/>
|
||||
<path i:knockout="Off" fill="#A80030" d="M0,35.215c0.563-1.728,0.665-2.766,0.88-3.766C-0.676,33.438,0.164,33.862,0,35.215"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 32 32" preserveAspectRatio="xMidYMid"><path d="M32 16c0 8.836-7.164 16-16 16S0 24.836 0 16 7.164 0 16 0s16 7.164 16 16z" fill="#dd4814"/><path d="M5.12 13.864c-1.18 0-2.137.956-2.137 2.137s.956 2.136 2.137 2.136S7.257 17.18 7.257 16 6.3 13.864 5.12 13.864zm15.252 9.71c-1.022.6-1.372 1.896-.782 2.917s1.895 1.372 2.917.782 1.372-1.895.782-2.917-1.896-1.37-2.917-.782zM9.76 16a6.23 6.23 0 0 1 2.653-5.105L10.852 8.28a9.3 9.3 0 0 0-3.838 5.394C7.69 14.224 8.12 15.06 8.12 16s-.432 1.776-1.106 2.326c.577 2.237 1.968 4.146 3.838 5.395l1.562-2.616A6.23 6.23 0 0 1 9.761 16zM16 9.76a6.24 6.24 0 0 1 6.215 5.687l3.044-.045a9.25 9.25 0 0 0-2.757-6.019c-.812.307-1.75.26-2.56-.208a2.99 2.99 0 0 1-1.461-2.118C17.7 6.84 16.86 6.72 16 6.72c-1.477 0-2.873.347-4.113.96l1.484 2.66c.8-.372 1.69-.58 2.628-.58zm0 12.48c-.94 0-1.83-.21-2.628-.58l-1.484 2.66c1.24.614 2.636.96 4.113.96a9.28 9.28 0 0 0 2.479-.338c.14-.858.65-1.648 1.46-2.118s1.75-.514 2.56-.207a9.25 9.25 0 0 0 2.757-6.019l-3.045-.045A6.24 6.24 0 0 1 16 22.24zm4.372-13.813c1.022.6 2.328.24 2.917-.78s.24-2.328-.78-2.918-2.328-.24-2.918.783-.24 2.327.782 2.917z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,23 @@
|
||||
# Onboarding Images
|
||||
|
||||
Place your screenshot images here with the following names:
|
||||
|
||||
- `imagen1.png` - Overview section screenshot
|
||||
- `imagen2.png` - Storage section screenshot
|
||||
- `imagen3.png` - Network section screenshot
|
||||
- `imagen4.png` - VMs & LXCs section screenshot
|
||||
- `imagen5.png` - Hardware section screenshot
|
||||
- `imagen6.png` - System Logs section screenshot
|
||||
|
||||
## Image Guidelines
|
||||
|
||||
- **Format**: PNG or JPG
|
||||
- **Recommended size**: 1200x800px or similar aspect ratio
|
||||
- **Quality**: High quality screenshots showing the main features of each section
|
||||
- **Content**: Capture the full section with representative data
|
||||
|
||||
## Notes
|
||||
|
||||
- The last slide (Future Updates) doesn't need an image as it uses an icon
|
||||
- If an image fails to load, the component will show a fallback icon
|
||||
- Images should be optimized for web (compressed but still high quality)
|
||||
|
After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 376 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 401 KiB |
|
After Width: | Height: | Size: 372 KiB |
|
After Width: | Height: | Size: 402 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "ProxMenux Monitor",
|
||||
"short_name": "ProxMenux",
|
||||
"description": "Proxmox System Dashboard and Monitor",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#2b2f36",
|
||||
"theme_color": "#2b2f36",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/proxmenux-logo.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ProxMenux Monitor AppImage Entry Point
|
||||
# This script is executed when the AppImage is run
|
||||
|
||||
# Get the directory where this AppImage is mounted
|
||||
APPDIR="$(dirname "$(readlink -f "${0}")")"
|
||||
|
||||
export PATH="${APPDIR}/usr/bin:${PATH}"
|
||||
export LD_LIBRARY_PATH="${APPDIR}/usr/lib/x86_64-linux-gnu:${APPDIR}/usr/lib:${APPDIR}/lib/x86_64-linux-gnu:${APPDIR}/lib:${LD_LIBRARY_PATH}"
|
||||
export PYTHONPATH="${APPDIR}/usr/lib/python3/dist-packages:${APPDIR}/usr/lib/python3/site-packages:${PYTHONPATH}"
|
||||
|
||||
# Change to the AppImage directory
|
||||
cd "${APPDIR}"
|
||||
|
||||
# Check for translation argument
|
||||
if [[ "$1" == "--translate" ]]; then
|
||||
echo "🌐 Starting ProxMenux Translation Service..."
|
||||
exec python3 "${APPDIR}/usr/bin/translate_cli.py" "${@:2}"
|
||||
else
|
||||
echo "🚀 Starting ProxMenux Monitor Dashboard..."
|
||||
echo ""
|
||||
|
||||
echo "🔧 Hardware monitoring tools:"
|
||||
[ -x "${APPDIR}/usr/bin/ipmitool" ] && echo " ✅ ipmitool available" || echo " ⚠️ ipmitool not available"
|
||||
[ -x "${APPDIR}/usr/bin/sensors" ] && echo " ✅ sensors available" || echo " ⚠️ sensors not available"
|
||||
[ -x "${APPDIR}/usr/bin/upsc" ] && echo " ✅ upsc available" || echo " ⚠️ upsc not available"
|
||||
|
||||
if [ -x "${APPDIR}/usr/bin/ipmitool" ]; then
|
||||
if ldd "${APPDIR}/usr/bin/ipmitool" 2>/dev/null | grep -q "libfreeipmi.so.17 => not found"; then
|
||||
echo " ⚠️ libfreeipmi.so.17 not found - ipmitool may not work"
|
||||
elif ldd "${APPDIR}/usr/bin/ipmitool" 2>/dev/null | grep -q "libfreeipmi.so.17"; then
|
||||
echo " ✅ libfreeipmi.so.17 loaded successfully"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Start the Flask server
|
||||
exec python3 "${APPDIR}/usr/bin/flask_server.py"
|
||||
fi
|
||||
@@ -0,0 +1,490 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ProxMenux Monitor AppImage Builder
|
||||
# This script creates a single AppImage with Flask server, Next.js dashboard, and translation support
|
||||
|
||||
set -e
|
||||
|
||||
WORK_DIR="/tmp/proxmenux_build"
|
||||
APP_DIR="$WORK_DIR/ProxMenux.AppDir"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DIST_DIR="$SCRIPT_DIR/../dist"
|
||||
APPIMAGE_ROOT="$SCRIPT_DIR/.."
|
||||
|
||||
VERSION=$(node -p "require('$APPIMAGE_ROOT/package.json').version")
|
||||
APPIMAGE_NAME="ProxMenux-${VERSION}.AppImage"
|
||||
|
||||
echo "🚀 Building ProxMenux Monitor AppImage v${VERSION} with hardware monitoring tools..."
|
||||
|
||||
# Clean and create work directory
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$APP_DIR"
|
||||
mkdir -p "$DIST_DIR"
|
||||
|
||||
# Download appimagetool if not exists
|
||||
if [ ! -f "$WORK_DIR/appimagetool" ]; then
|
||||
echo "📥 Downloading appimagetool..."
|
||||
wget -q "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -O "$WORK_DIR/appimagetool"
|
||||
chmod +x "$WORK_DIR/appimagetool"
|
||||
fi
|
||||
|
||||
# Create directory structure
|
||||
mkdir -p "$APP_DIR/usr/bin"
|
||||
mkdir -p "$APP_DIR/usr/lib/python3/dist-packages"
|
||||
mkdir -p "$APP_DIR/usr/share/applications"
|
||||
mkdir -p "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
|
||||
mkdir -p "$APP_DIR/web"
|
||||
|
||||
echo "🔨 Building Next.js application..."
|
||||
cd "$APPIMAGE_ROOT"
|
||||
if [ ! -f "package.json" ]; then
|
||||
echo "❌ Error: package.json not found in AppImage directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install dependencies if node_modules doesn't exist
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "📦 Installing dependencies..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
echo "🏗️ Building Next.js static export..."
|
||||
npm run export
|
||||
|
||||
echo "🔍 Checking export results..."
|
||||
if [ -d "out" ]; then
|
||||
echo "✅ Export directory found"
|
||||
echo "📁 Contents of out directory:"
|
||||
ls -la out/
|
||||
if [ -f "out/index.html" ]; then
|
||||
echo "✅ index.html found in out directory"
|
||||
else
|
||||
echo "❌ index.html NOT found in out directory"
|
||||
echo "📁 Looking for HTML files:"
|
||||
find out/ -name "*.html" -type f || echo "No HTML files found"
|
||||
fi
|
||||
else
|
||||
echo "❌ Error: Next.js export failed - out directory not found"
|
||||
echo "📁 Current directory contents:"
|
||||
ls -la
|
||||
echo "📁 Looking for any build outputs:"
|
||||
find . -name "*.html" -type f 2>/dev/null || echo "No HTML files found anywhere"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Return to script directory
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Copy Flask server
|
||||
echo "📋 Copying Flask server..."
|
||||
cp "$SCRIPT_DIR/flask_server.py" "$APP_DIR/usr/bin/"
|
||||
|
||||
echo "📋 Adding translation support..."
|
||||
cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF'
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ProxMenux translate CLI
|
||||
stdin JSON -> {"text":"...", "dest_lang":"es", "context":"...", "cache_file":"/usr/local/share/proxmenux/cache.json"}
|
||||
stdout JSON -> {"success":true,"text":"..."} or {"success":false,"error":"..."}
|
||||
"""
|
||||
import sys, json, re
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure embedded site-packages are discoverable
|
||||
HERE = Path(__file__).resolve().parents[2] # .../AppDir
|
||||
DIST = HERE / "usr" / "lib" / "python3" / "dist-packages"
|
||||
SITE = HERE / "usr" / "lib" / "python3" / "site-packages"
|
||||
for p in (str(DIST), str(SITE)):
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
# Python 3.13 compat: inline 'cgi' shim
|
||||
try:
|
||||
import cgi
|
||||
except Exception:
|
||||
import types, html
|
||||
def _parse_header(value: str):
|
||||
value = str(value or "")
|
||||
parts = [p.strip() for p in value.split(";")]
|
||||
if not parts:
|
||||
return "", {}
|
||||
key = parts[0].lower()
|
||||
params = {}
|
||||
for item in parts[1:]:
|
||||
if not item:
|
||||
continue
|
||||
if "=" in item:
|
||||
k, v = item.split("=", 1)
|
||||
k = k.strip().lower()
|
||||
v = v.strip().strip('"').strip("'")
|
||||
params[k] = v
|
||||
else:
|
||||
params[item.strip().lower()] = ""
|
||||
return key, params
|
||||
cgi = types.SimpleNamespace(parse_header=_parse_header, escape=html.escape)
|
||||
|
||||
try:
|
||||
from googletrans import Translator
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": f"ImportError: {e}"}))
|
||||
sys.exit(0)
|
||||
|
||||
def load_json_stdin():
|
||||
try:
|
||||
return json.load(sys.stdin)
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": f"Invalid JSON input: {e}"}))
|
||||
sys.exit(0)
|
||||
|
||||
def ensure_cache(path: Path):
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not path.exists():
|
||||
path.write_text("{}", encoding="utf-8")
|
||||
json.loads(path.read_text(encoding="utf-8") or "{}")
|
||||
except Exception:
|
||||
path.write_text("{}", encoding="utf-8")
|
||||
|
||||
def read_cache(path: Path):
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8") or "{}")
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def write_cache(path: Path, cache: dict):
|
||||
tmp = path.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(cache, ensure_ascii=False), encoding="utf-8")
|
||||
tmp.replace(path)
|
||||
|
||||
def clean_translated(s: str) -> str:
|
||||
s = re.sub(r'^.*?(Translate:|Traducir:|Traduire:|Übersetzen:|Tradurre:|Traduzir:|翻译:|翻訳:)', '', s, flags=re.IGNORECASE | re.DOTALL).strip()
|
||||
s = re.sub(r'^.*?(Context:|Contexto:|Contexte:|Kontext:|Contesto:|上下文:|コンテキスト:).*?:', '', s, flags=re.IGNORECASE | re.DOTALL).strip()
|
||||
return s.strip()
|
||||
|
||||
def main():
|
||||
req = load_json_stdin()
|
||||
text = req.get("text", "")
|
||||
dest = req.get("dest_lang", "en") or "en"
|
||||
context = req.get("context", "")
|
||||
cache_file = Path(req.get("cache_file", "")) if req.get("cache_file") else None
|
||||
|
||||
if dest == "en":
|
||||
print(json.dumps({"success": True, "text": text}))
|
||||
return
|
||||
|
||||
cache = {}
|
||||
if cache_file:
|
||||
ensure_cache(cache_file)
|
||||
cache = read_cache(cache_file)
|
||||
if text in cache and (dest in cache[text] or "notranslate" in cache[text]):
|
||||
found = cache[text].get(dest) or cache[text].get("notranslate")
|
||||
print(json.dumps({"success": True, "text": found}))
|
||||
return
|
||||
|
||||
try:
|
||||
full = (context + " " + text).strip() if context else text
|
||||
tr = Translator()
|
||||
result = tr.translate(full, dest=dest).text
|
||||
result = clean_translated(result)
|
||||
|
||||
if cache_file:
|
||||
cache.setdefault(text, {})
|
||||
cache[text][dest] = result
|
||||
write_cache(cache_file, cache)
|
||||
|
||||
print(json.dumps({"success": True, "text": result}))
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": str(e)}))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
PYEOF
|
||||
|
||||
chmod +x "$APP_DIR/usr/bin/translate_cli.py"
|
||||
|
||||
# Copy Next.js build
|
||||
echo "📋 Copying web dashboard..."
|
||||
if [ -d "$APPIMAGE_ROOT/out" ]; then
|
||||
mkdir -p "$APP_DIR/web"
|
||||
echo "📁 Copying from $APPIMAGE_ROOT/out to $APP_DIR/web"
|
||||
cp -r "$APPIMAGE_ROOT/out"/* "$APP_DIR/web/"
|
||||
|
||||
if [ -f "$APP_DIR/web/index.html" ]; then
|
||||
echo "✅ index.html copied successfully to $APP_DIR/web/"
|
||||
else
|
||||
echo "❌ index.html NOT found after copying"
|
||||
echo "📁 Contents of $APP_DIR/web:"
|
||||
ls -la "$APP_DIR/web/" || echo "Directory is empty or doesn't exist"
|
||||
fi
|
||||
|
||||
if [ -d "$APPIMAGE_ROOT/public" ]; then
|
||||
cp -r "$APPIMAGE_ROOT/public"/* "$APP_DIR/web/" 2>/dev/null || true
|
||||
fi
|
||||
cp "$APPIMAGE_ROOT/package.json" "$APP_DIR/web/"
|
||||
|
||||
echo "✅ Next.js static export copied successfully"
|
||||
else
|
||||
echo "❌ Error: Next.js export not found even after building"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy AppRun script
|
||||
echo "📋 Copying AppRun script..."
|
||||
if [ -f "$SCRIPT_DIR/AppRun" ]; then
|
||||
cp "$SCRIPT_DIR/AppRun" "$APP_DIR/AppRun"
|
||||
chmod +x "$APP_DIR/AppRun"
|
||||
echo "✅ AppRun script copied successfully"
|
||||
else
|
||||
echo "❌ Error: AppRun script not found at $SCRIPT_DIR/AppRun"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create desktop file
|
||||
cat > "$APP_DIR/proxmenux-monitor.desktop" << EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=ProxMenux Monitor
|
||||
Comment=Proxmox System Monitoring Dashboard with Translation Support
|
||||
Exec=AppRun
|
||||
Icon=proxmenux-monitor
|
||||
Categories=System;Monitor;
|
||||
Terminal=false
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
# Copy desktop file to applications directory
|
||||
cp "$APP_DIR/proxmenux-monitor.desktop" "$APP_DIR/usr/share/applications/"
|
||||
|
||||
# Download and set icon
|
||||
echo "🎨 Setting up icon..."
|
||||
if [ -f "$APPIMAGE_ROOT/public/images/proxmenux-logo.png" ]; then
|
||||
cp "$APPIMAGE_ROOT/public/images/proxmenux-logo.png" "$APP_DIR/proxmenux-monitor.png"
|
||||
else
|
||||
wget -q "https://raw.githubusercontent.com/MacRimi/ProxMenux/main/images/logo.png" -O "$APP_DIR/proxmenux-monitor.png" || {
|
||||
echo "⚠️ Could not download logo, creating placeholder..."
|
||||
convert -size 256x256 xc:blue -fill white -gravity center -pointsize 24 -annotate +0+0 "PM" "$APP_DIR/proxmenux-monitor.png" 2>/dev/null || {
|
||||
echo "⚠️ ImageMagick not available, skipping icon creation"
|
||||
}
|
||||
}
|
||||
fi
|
||||
|
||||
if [ -f "$APP_DIR/proxmenux-monitor.png" ]; then
|
||||
cp "$APP_DIR/proxmenux-monitor.png" "$APP_DIR/usr/share/icons/hicolor/256x256/apps/"
|
||||
fi
|
||||
|
||||
echo "📦 Installing Python dependencies..."
|
||||
pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" \
|
||||
flask \
|
||||
flask-cors \
|
||||
psutil \
|
||||
requests \
|
||||
googletrans==4.0.0-rc1 \
|
||||
httpx==0.13.3 \
|
||||
httpcore==0.9.1 \
|
||||
beautifulsoup4
|
||||
|
||||
cat > "$APP_DIR/usr/lib/python3/dist-packages/cgi.py" << 'PYEOF'
|
||||
from typing import Tuple, Dict
|
||||
try:
|
||||
from html import escape as _html_escape
|
||||
except Exception:
|
||||
def _html_escape(s, quote=True): return s
|
||||
|
||||
__all__ = ["parse_header", "escape"]
|
||||
|
||||
def escape(s, quote=True):
|
||||
return _html_escape(s, quote=quote)
|
||||
|
||||
def parse_header(value: str) -> Tuple[str, Dict[str, str]]:
|
||||
if not isinstance(value, str):
|
||||
value = str(value or "")
|
||||
parts = [p.strip() for p in value.split(";")]
|
||||
if not parts:
|
||||
return "", {}
|
||||
key = parts[0].lower()
|
||||
params: Dict[str, str] = {}
|
||||
for item in parts[1:]:
|
||||
if not item:
|
||||
continue
|
||||
if "=" in item:
|
||||
k, v = item.split("=", 1)
|
||||
k = k.strip().lower()
|
||||
v = v.strip().strip('"').strip("'")
|
||||
params[k] = v
|
||||
else:
|
||||
params[item.strip().lower()] = ""
|
||||
return key, params
|
||||
PYEOF
|
||||
|
||||
echo "🔧 Installing hardware monitoring tools..."
|
||||
mkdir -p "$WORK_DIR/debs"
|
||||
cd "$WORK_DIR/debs"
|
||||
|
||||
|
||||
# ==============================================================
|
||||
|
||||
|
||||
echo "📥 Downloading hardware monitoring tools (dynamic via APT)..."
|
||||
|
||||
dl_pkg() {
|
||||
local out="$1"; shift
|
||||
local pkg deb_file
|
||||
for pkg in "$@"; do
|
||||
echo " - trying: $pkg"
|
||||
if apt-get download -y "$pkg" >/dev/null 2>&1; then
|
||||
deb_file="$(ls -1 ${pkg}_*.deb 2>/dev/null | head -n1)"
|
||||
if [ -n "$deb_file" ] && [ -f "$deb_file" ]; then
|
||||
mv "$deb_file" "$out"
|
||||
echo " ✅ downloaded: $pkg -> $out"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
|
||||
echo " ↻ retry with sudo apt-get update && download"
|
||||
sudo apt-get update -qq || true
|
||||
for pkg in "$@"; do
|
||||
echo " - trying (sudo): $pkg"
|
||||
if sudo apt-get download -y "$pkg" >/dev/null 2>&1; then
|
||||
deb_file="$(ls -1 ${pkg}_*.deb 2>/dev/null | head -n1)"
|
||||
if [ -n "$deb_file" ] && [ -f "$deb_file" ]; then
|
||||
mv "$deb_file" "$out"
|
||||
echo " ✅ downloaded (sudo): $pkg -> $out"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo " ⚠️ none of the candidates could be downloaded for $out"
|
||||
return 1
|
||||
}
|
||||
|
||||
mkdir -p "$WORK_DIR/debs"
|
||||
cd "$WORK_DIR/debs"
|
||||
|
||||
|
||||
dl_pkg "ipmitool.deb" "ipmitool" || true
|
||||
dl_pkg "libfreeipmi17.deb" "libfreeipmi17" || true
|
||||
dl_pkg "lm-sensors.deb" "lm-sensors" || true
|
||||
dl_pkg "nut-client.deb" "nut-client" || true
|
||||
dl_pkg "libupsclient.deb" "libupsclient6" "libupsclient5" "libupsclient4" || true
|
||||
|
||||
|
||||
# dl_pkg "nvidia-smi.deb" "nvidia-smi" "nvidia-utils" "nvidia-utils-535" "nvidia-utils-550" || true
|
||||
# dl_pkg "intel-gpu-tools.deb" "intel-gpu-tools" || true
|
||||
# dl_pkg "radeontop.deb" "radeontop" || true
|
||||
|
||||
echo "📦 Extracting .deb packages into AppDir..."
|
||||
extracted_count=0
|
||||
shopt -s nullglob
|
||||
for deb in *.deb; do
|
||||
echo " -> $deb"
|
||||
if file "$deb" | grep -q "Debian binary package"; then
|
||||
dpkg-deb -x "$deb" "$APP_DIR" && extracted_count=$((extracted_count + 1))
|
||||
else
|
||||
echo " ⚠️ $deb is not a valid .deb, skipping"
|
||||
fi
|
||||
done
|
||||
shopt -u nullglob
|
||||
|
||||
if [ $extracted_count -eq 0 ]; then
|
||||
echo "⚠️ No packages extracted; hardware/GPU monitoring may be unavailable"
|
||||
else
|
||||
echo "✅ Extracted $extracted_count package(s)"
|
||||
fi
|
||||
|
||||
|
||||
if [ -d "$APP_DIR/bin" ]; then
|
||||
echo "📋 Normalizing /bin -> /usr/bin"
|
||||
mkdir -p "$APP_DIR/usr/bin"
|
||||
cp -r "$APP_DIR/bin/"* "$APP_DIR/usr/bin/" 2>/dev/null || true
|
||||
rm -rf "$APP_DIR/bin"
|
||||
fi
|
||||
|
||||
|
||||
echo "🔍 Sanity check (ldd + presence of libfreeipmi)"
|
||||
export LD_LIBRARY_PATH="$APP_DIR/lib:$APP_DIR/lib/x86_64-linux-gnu:$APP_DIR/usr/lib:$APP_DIR/usr/lib/x86_64-linux-gnu"
|
||||
|
||||
|
||||
if ! find "$APP_DIR/usr/lib" "$APP_DIR/lib" -maxdepth 3 -name 'libfreeipmi.so.17*' | grep -q .; then
|
||||
echo "❌ libfreeipmi.so.17 not found inside AppDir (ipmitool will fail)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
if [ -x "$APP_DIR/usr/bin/ipmitool" ] && ldd "$APP_DIR/usr/bin/ipmitool" | grep -q 'not found'; then
|
||||
echo "❌ ipmitool has unresolved libs:"
|
||||
ldd "$APP_DIR/usr/bin/ipmitool" | grep 'not found' || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
if [ -x "$APP_DIR/usr/bin/upsc" ] && ldd "$APP_DIR/usr/bin/upsc" | grep -q 'not found'; then
|
||||
echo "⚠️ upsc has unresolved libs, trying to auto-fix..."
|
||||
missing="$(ldd "$APP_DIR/usr/bin/upsc" | awk '/not found/{print $1}' | tr -d ' ')"
|
||||
echo " missing: $missing"
|
||||
case "$missing" in
|
||||
libupsclient.so.6) need_pkg="libupsclient6" ;;
|
||||
libupsclient.so.5) need_pkg="libupsclient5" ;;
|
||||
libupsclient.so.4) need_pkg="libupsclient4" ;;
|
||||
*) need_pkg="" ;;
|
||||
esac
|
||||
|
||||
if [ -n "$need_pkg" ]; then
|
||||
echo " downloading: $need_pkg"
|
||||
dl_pkg "libupsclient_autofix.deb" "$need_pkg" || true
|
||||
if [ -f "libupsclient_autofix.deb" ]; then
|
||||
dpkg-deb -x "libupsclient_autofix.deb" "$APP_DIR"
|
||||
echo " re-checking ldd for upsc..."
|
||||
if ldd "$APP_DIR/usr/bin/upsc" | grep -q 'not found'; then
|
||||
echo "❌ upsc still has unresolved libs:"
|
||||
ldd "$APP_DIR/usr/bin/upsc" | grep 'not found' || true
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ could not download $need_pkg automatically"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ unknown missing library for upsc: $missing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ Sanity check OK (ipmitool/upsc ready; libfreeipmi present)"
|
||||
|
||||
# Info rápida
|
||||
[ -x "$APP_DIR/usr/bin/sensors" ] && echo " • sensors: OK" || echo " • sensors: missing"
|
||||
[ -x "$APP_DIR/usr/bin/ipmitool" ] && echo " • ipmitool: OK" || echo " • ipmitool: missing"
|
||||
[ -x "$APP_DIR/usr/bin/upsc" ] && echo " • upsc: OK" || echo " • upsc: missing"
|
||||
[ -x "$APP_DIR/usr/bin/nvidia-smi" ] && echo " • nvidia-smi: OK" || echo " • nvidia-smi: missing"
|
||||
[ -x "$APP_DIR/usr/bin/intel_gpu_top" ] && echo " • intel-gpu-tools: OK" || echo " • intel-gpu-tools: missing"
|
||||
[ -x "$APP_DIR/usr/bin/radeontop" ] && echo " • radeontop: OK" || echo " • radeontop: missing"
|
||||
|
||||
|
||||
|
||||
# ==============================================================
|
||||
|
||||
|
||||
|
||||
# Build AppImage
|
||||
echo "🔨 Building unified AppImage v${VERSION}..."
|
||||
cd "$WORK_DIR"
|
||||
export NO_CLEANUP=1
|
||||
export APPIMAGE_EXTRACT_AND_RUN=1
|
||||
ARCH=x86_64 ./appimagetool --no-appstream --verbose "$APP_DIR" "$APPIMAGE_NAME"
|
||||
|
||||
# Move to dist directory
|
||||
mv "$APPIMAGE_NAME" "$DIST_DIR/"
|
||||
|
||||
echo "✅ Unified AppImage created: $DIST_DIR/$APPIMAGE_NAME"
|
||||
echo ""
|
||||
echo "📋 Usage:"
|
||||
echo " Dashboard: ./$APPIMAGE_NAME"
|
||||
echo " Translation: ./$APPIMAGE_NAME --translate"
|
||||
echo ""
|
||||
echo "🚀 Installation:"
|
||||
echo " sudo cp $DIST_DIR/$APPIMAGE_NAME /usr/local/bin/proxmenux-monitor"
|
||||
echo " sudo chmod +x /usr/local/bin/proxmenux-monitor"
|
||||
@@ -0,0 +1,369 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import subprocess
|
||||
import re
|
||||
import os
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
def run_command(cmd: List[str]) -> str:
|
||||
"""Run a command and return its output."""
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
|
||||
return result.stdout
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def get_nvidia_gpu_info() -> List[Dict[str, Any]]:
|
||||
"""Get detailed NVIDIA GPU information using nvidia-smi."""
|
||||
gpus = []
|
||||
|
||||
# Check if nvidia-smi is available
|
||||
if not os.path.exists('/usr/bin/nvidia-smi'):
|
||||
return gpus
|
||||
|
||||
try:
|
||||
# Query all GPU metrics at once
|
||||
query_fields = [
|
||||
'index',
|
||||
'name',
|
||||
'driver_version',
|
||||
'memory.total',
|
||||
'memory.used',
|
||||
'memory.free',
|
||||
'temperature.gpu',
|
||||
'utilization.gpu',
|
||||
'utilization.memory',
|
||||
'power.draw',
|
||||
'power.limit',
|
||||
'clocks.current.graphics',
|
||||
'clocks.current.memory',
|
||||
'pcie.link.gen.current',
|
||||
'pcie.link.width.current'
|
||||
]
|
||||
|
||||
cmd = ['nvidia-smi', '--query-gpu=' + ','.join(query_fields), '--format=csv,noheader,nounits']
|
||||
output = run_command(cmd)
|
||||
|
||||
if not output:
|
||||
return gpus
|
||||
|
||||
for line in output.strip().split('\n'):
|
||||
if not line:
|
||||
continue
|
||||
|
||||
values = [v.strip() for v in line.split(',')]
|
||||
if len(values) < len(query_fields):
|
||||
continue
|
||||
|
||||
gpu_info = {
|
||||
'index': values[0],
|
||||
'name': values[1],
|
||||
'driver_version': values[2],
|
||||
'memory_total': f"{values[3]} MiB",
|
||||
'memory_used': f"{values[4]} MiB",
|
||||
'memory_free': f"{values[5]} MiB",
|
||||
'temperature': values[6],
|
||||
'utilization_gpu': values[7],
|
||||
'utilization_memory': values[8],
|
||||
'power_draw': f"{values[9]} W",
|
||||
'power_limit': f"{values[10]} W",
|
||||
'clock_graphics': f"{values[11]} MHz",
|
||||
'clock_memory': f"{values[12]} MHz",
|
||||
'pcie_gen': values[13],
|
||||
'pcie_width': f"x{values[14]}"
|
||||
}
|
||||
|
||||
# Get CUDA version if available
|
||||
cuda_output = run_command(['nvidia-smi', '--query-gpu=compute_cap', '--format=csv,noheader', '-i', values[0]])
|
||||
if cuda_output:
|
||||
gpu_info['compute_capability'] = cuda_output.strip()
|
||||
|
||||
gpus.append(gpu_info)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting NVIDIA GPU info: {e}", file=sys.stderr)
|
||||
|
||||
return gpus
|
||||
|
||||
def get_amd_gpu_info() -> List[Dict[str, Any]]:
|
||||
"""Get AMD GPU information using rocm-smi."""
|
||||
gpus = []
|
||||
|
||||
# Check if rocm-smi is available
|
||||
if not os.path.exists('/opt/rocm/bin/rocm-smi'):
|
||||
return gpus
|
||||
|
||||
try:
|
||||
# Get basic GPU info
|
||||
output = run_command(['/opt/rocm/bin/rocm-smi', '--showid', '--showtemp', '--showuse', '--showmeminfo', 'vram'])
|
||||
|
||||
if not output:
|
||||
return gpus
|
||||
|
||||
# Parse rocm-smi output (format varies, this is a basic parser)
|
||||
current_gpu = None
|
||||
for line in output.split('\n'):
|
||||
if 'GPU[' in line:
|
||||
if current_gpu:
|
||||
gpus.append(current_gpu)
|
||||
current_gpu = {'index': line.split('[')[1].split(']')[0]}
|
||||
elif current_gpu:
|
||||
if 'Temperature' in line:
|
||||
temp_match = re.search(r'(\d+\.?\d*)', line)
|
||||
if temp_match:
|
||||
current_gpu['temperature'] = temp_match.group(1)
|
||||
elif 'GPU use' in line:
|
||||
use_match = re.search(r'(\d+)%', line)
|
||||
if use_match:
|
||||
current_gpu['utilization_gpu'] = use_match.group(1)
|
||||
elif 'VRAM' in line:
|
||||
mem_match = re.search(r'(\d+)MB / (\d+)MB', line)
|
||||
if mem_match:
|
||||
current_gpu['memory_used'] = f"{mem_match.group(1)} MiB"
|
||||
current_gpu['memory_total'] = f"{mem_match.group(2)} MiB"
|
||||
|
||||
if current_gpu:
|
||||
gpus.append(current_gpu)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting AMD GPU info: {e}", file=sys.stderr)
|
||||
|
||||
return gpus
|
||||
|
||||
def get_temperatures() -> List[Dict[str, Any]]:
|
||||
"""Get temperature readings from sensors."""
|
||||
temps = []
|
||||
output = run_command(['sensors', '-A', '-u'])
|
||||
|
||||
current_adapter = None
|
||||
current_sensor = None
|
||||
|
||||
for line in output.split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
if line.endswith(':') and not line.startswith(' '):
|
||||
current_adapter = line[:-1]
|
||||
elif '_input:' in line and current_adapter:
|
||||
parts = line.split(':')
|
||||
if len(parts) == 2:
|
||||
sensor_name = parts[0].replace('_input', '').replace('_', ' ').title()
|
||||
try:
|
||||
temp_value = float(parts[1].strip())
|
||||
temps.append({
|
||||
'name': sensor_name,
|
||||
'current': round(temp_value, 1),
|
||||
'adapter': current_adapter
|
||||
})
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return temps
|
||||
|
||||
def get_fans() -> List[Dict[str, Any]]:
|
||||
"""Get fan speed readings."""
|
||||
fans = []
|
||||
output = run_command(['sensors', '-A', '-u'])
|
||||
|
||||
current_adapter = None
|
||||
|
||||
for line in output.split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
if line.endswith(':') and not line.startswith(' '):
|
||||
current_adapter = line[:-1]
|
||||
elif 'fan' in line.lower() and '_input:' in line and current_adapter:
|
||||
parts = line.split(':')
|
||||
if len(parts) == 2:
|
||||
fan_name = parts[0].replace('_input', '').replace('_', ' ').title()
|
||||
try:
|
||||
speed = float(parts[1].strip())
|
||||
fans.append({
|
||||
'name': fan_name,
|
||||
'speed': int(speed),
|
||||
'unit': 'RPM'
|
||||
})
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return fans
|
||||
|
||||
def get_network_cards() -> List[Dict[str, Any]]:
|
||||
"""Get network interface information."""
|
||||
cards = []
|
||||
output = run_command(['ip', '-o', 'link', 'show'])
|
||||
|
||||
for line in output.split('\n'):
|
||||
if not line or 'lo:' in line:
|
||||
continue
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
name = parts[1].rstrip(':')
|
||||
state = 'UP' if 'UP' in line else 'DOWN'
|
||||
|
||||
# Get interface type
|
||||
iface_type = 'Unknown'
|
||||
if 'ether' in line:
|
||||
iface_type = 'Ethernet'
|
||||
elif 'wlan' in name or 'wifi' in name:
|
||||
iface_type = 'WiFi'
|
||||
|
||||
# Try to get speed
|
||||
speed = None
|
||||
speed_output = run_command(['ethtool', name])
|
||||
speed_match = re.search(r'Speed: (\d+\w+)', speed_output)
|
||||
if speed_match:
|
||||
speed = speed_match.group(1)
|
||||
|
||||
cards.append({
|
||||
'name': name,
|
||||
'type': iface_type,
|
||||
'status': state,
|
||||
'speed': speed
|
||||
})
|
||||
|
||||
return cards
|
||||
|
||||
def get_storage_devices() -> List[Dict[str, Any]]:
|
||||
"""Get storage device information."""
|
||||
devices = []
|
||||
output = run_command(['lsblk', '-d', '-o', 'NAME,TYPE,SIZE,MODEL', '-n'])
|
||||
|
||||
for line in output.split('\n'):
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parts = line.split(None, 3)
|
||||
if len(parts) >= 3:
|
||||
name = parts[0]
|
||||
dev_type = parts[1]
|
||||
size = parts[2]
|
||||
model = parts[3] if len(parts) > 3 else 'Unknown'
|
||||
|
||||
if dev_type in ['disk', 'nvme']:
|
||||
devices.append({
|
||||
'name': name,
|
||||
'type': dev_type,
|
||||
'size': size,
|
||||
'model': model.strip()
|
||||
})
|
||||
|
||||
return devices
|
||||
|
||||
def get_pci_devices() -> List[Dict[str, Any]]:
|
||||
"""Get PCI device information including GPUs."""
|
||||
devices = []
|
||||
output = run_command(['lspci', '-vmm'])
|
||||
|
||||
current_device = {}
|
||||
|
||||
for line in output.split('\n'):
|
||||
line = line.strip()
|
||||
|
||||
if not line:
|
||||
if current_device:
|
||||
devices.append(current_device)
|
||||
current_device = {}
|
||||
continue
|
||||
|
||||
if ':' in line:
|
||||
key, value = line.split(':', 1)
|
||||
key = key.strip().lower().replace(' ', '_')
|
||||
value = value.strip()
|
||||
current_device[key] = value
|
||||
|
||||
if current_device:
|
||||
devices.append(current_device)
|
||||
|
||||
# Enhance GPU devices with monitoring data
|
||||
nvidia_gpus = get_nvidia_gpu_info()
|
||||
amd_gpus = get_amd_gpu_info()
|
||||
|
||||
nvidia_idx = 0
|
||||
amd_idx = 0
|
||||
|
||||
for device in devices:
|
||||
# Check if it's a GPU
|
||||
device_class = device.get('class', '').lower()
|
||||
vendor = device.get('vendor', '').lower()
|
||||
|
||||
if 'vga' in device_class or 'display' in device_class or '3d' in device_class:
|
||||
device['type'] = 'GPU'
|
||||
|
||||
# Add NVIDIA GPU monitoring data
|
||||
if 'nvidia' in vendor and nvidia_idx < len(nvidia_gpus):
|
||||
gpu_data = nvidia_gpus[nvidia_idx]
|
||||
device['gpu_memory'] = gpu_data.get('memory_total')
|
||||
device['gpu_driver_version'] = gpu_data.get('driver_version')
|
||||
device['gpu_compute_capability'] = gpu_data.get('compute_capability')
|
||||
device['gpu_power_draw'] = gpu_data.get('power_draw')
|
||||
device['gpu_temperature'] = float(gpu_data.get('temperature', 0))
|
||||
device['gpu_utilization'] = float(gpu_data.get('utilization_gpu', 0))
|
||||
device['gpu_memory_used'] = gpu_data.get('memory_used')
|
||||
device['gpu_memory_total'] = gpu_data.get('memory_total')
|
||||
device['gpu_clock_speed'] = gpu_data.get('clock_graphics')
|
||||
device['gpu_memory_clock'] = gpu_data.get('clock_memory')
|
||||
nvidia_idx += 1
|
||||
|
||||
# Add AMD GPU monitoring data
|
||||
elif 'amd' in vendor and amd_idx < len(amd_gpus):
|
||||
gpu_data = amd_gpus[amd_idx]
|
||||
device['gpu_temperature'] = float(gpu_data.get('temperature', 0))
|
||||
device['gpu_utilization'] = float(gpu_data.get('utilization_gpu', 0))
|
||||
device['gpu_memory_used'] = gpu_data.get('memory_used')
|
||||
device['gpu_memory_total'] = gpu_data.get('memory_total')
|
||||
amd_idx += 1
|
||||
elif 'network' in device_class or 'ethernet' in device_class:
|
||||
device['type'] = 'Network'
|
||||
elif 'storage' in device_class or 'sata' in device_class or 'nvme' in device_class:
|
||||
device['type'] = 'Storage'
|
||||
else:
|
||||
device['type'] = 'Other'
|
||||
|
||||
return devices
|
||||
|
||||
def get_power_info() -> Optional[Dict[str, Any]]:
|
||||
"""Get power consumption information if available."""
|
||||
# Try to get system power from RAPL (Running Average Power Limit)
|
||||
rapl_path = '/sys/class/powercap/intel-rapl/intel-rapl:0/energy_uj'
|
||||
|
||||
if os.path.exists(rapl_path):
|
||||
try:
|
||||
with open(rapl_path, 'r') as f:
|
||||
energy_uj = int(f.read().strip())
|
||||
|
||||
# This is cumulative energy, would need to track over time for watts
|
||||
# For now, just indicate power monitoring is available
|
||||
return {
|
||||
'name': 'System Power',
|
||||
'watts': 0, # Would need time-based calculation
|
||||
'adapter': 'RAPL'
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def main():
|
||||
"""Main function to gather all hardware information."""
|
||||
data = {
|
||||
'temperatures': get_temperatures(),
|
||||
'fans': get_fans(),
|
||||
'network_cards': get_network_cards(),
|
||||
'storage_devices': get_storage_devices(),
|
||||
'pci_devices': get_pci_devices(),
|
||||
}
|
||||
|
||||
power_info = get_power_info()
|
||||
if power_info:
|
||||
data['power_meter'] = power_info
|
||||
|
||||
print(json.dumps(data, indent=2))
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
main()
|
||||
@@ -0,0 +1,65 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./app/**/*.{ts,tsx,js,jsx}",
|
||||
"./components/**/*.{ts,tsx,js,jsx}",
|
||||
"./pages/**/*.{ts,tsx,js,jsx}",
|
||||
"./src/**/*.{ts,tsx,js,jsx}",
|
||||
],
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
|
||||
card: "var(--card)",
|
||||
"card-foreground": "var(--card-foreground)",
|
||||
|
||||
popover: "var(--popover)",
|
||||
"popover-foreground": "var(--popover-foreground)",
|
||||
|
||||
primary: "var(--primary)",
|
||||
"primary-foreground": "var(--primary-foreground)",
|
||||
|
||||
secondary: "var(--secondary)",
|
||||
"secondary-foreground": "var(--secondary-foreground)",
|
||||
|
||||
muted: "var(--muted)",
|
||||
"muted-foreground": "var(--muted-foreground)",
|
||||
|
||||
accent: "var(--accent)",
|
||||
"accent-foreground": "var(--accent-foreground)",
|
||||
|
||||
destructive: "var(--destructive)",
|
||||
"destructive-foreground": "var(--destructive-foreground)",
|
||||
|
||||
border: "var(--border)",
|
||||
input: "var(--input)",
|
||||
ring: "var(--ring)",
|
||||
|
||||
"chart-1": "var(--chart-1)",
|
||||
"chart-2": "var(--chart-2)",
|
||||
"chart-3": "var(--chart-3)",
|
||||
"chart-4": "var(--chart-4)",
|
||||
"chart-5": "var(--chart-5)",
|
||||
|
||||
sidebar: "var(--sidebar)",
|
||||
"sidebar-foreground": "var(--sidebar-foreground)",
|
||||
"sidebar-primary": "var(--sidebar-primary)",
|
||||
"sidebar-primary-foreground": "var(--sidebar-primary-foreground)",
|
||||
"sidebar-accent": "var(--sidebar-accent)",
|
||||
"sidebar-accent-foreground": "var(--sidebar-accent-foreground)",
|
||||
"sidebar-border": "var(--sidebar-border)",
|
||||
"sidebar-ring": "var(--sidebar-ring)",
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
xl: "calc(var(--radius) + 4px)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "es6"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
export interface Temperature {
|
||||
name: string
|
||||
original_name?: string
|
||||
current: number
|
||||
high?: number
|
||||
critical?: number
|
||||
adapter?: string
|
||||
}
|
||||
|
||||
export interface PowerMeter {
|
||||
name: string
|
||||
watts: number
|
||||
adapter?: string
|
||||
}
|
||||
|
||||
export interface NetworkInterface {
|
||||
name: string
|
||||
type: string
|
||||
speed?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface StorageDevice {
|
||||
name: string
|
||||
type: string
|
||||
size?: string
|
||||
model?: string
|
||||
driver?: string
|
||||
interface?: string
|
||||
serial?: string
|
||||
family?: string
|
||||
firmware?: string
|
||||
rotation_rate?: number | string
|
||||
form_factor?: string
|
||||
sata_version?: string
|
||||
}
|
||||
|
||||
export interface PCIDevice {
|
||||
slot: string
|
||||
type: string
|
||||
device: string
|
||||
vendor: string
|
||||
class: string
|
||||
driver?: string
|
||||
kernel_module?: string
|
||||
irq?: string
|
||||
memory_address?: string
|
||||
link_speed?: string
|
||||
capabilities?: string[]
|
||||
gpu_memory?: string
|
||||
gpu_driver_version?: string
|
||||
gpu_cuda_version?: string
|
||||
gpu_compute_capability?: string
|
||||
gpu_power_draw?: string
|
||||
gpu_temperature?: number
|
||||
gpu_utilization?: number
|
||||
gpu_memory_used?: string
|
||||
gpu_memory_total?: string
|
||||
gpu_clock_speed?: string
|
||||
gpu_memory_clock?: string
|
||||
}
|
||||
|
||||
export interface Fan {
|
||||
name: string
|
||||
original_name?: string
|
||||
speed: number
|
||||
unit: string
|
||||
adapter?: string
|
||||
}
|
||||
|
||||
export interface PowerSupply {
|
||||
name: string
|
||||
watts: number
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface UPS {
|
||||
name: string
|
||||
host?: string
|
||||
is_remote?: boolean
|
||||
connection_type?: string
|
||||
status: string
|
||||
model?: string
|
||||
manufacturer?: string
|
||||
serial?: string
|
||||
device_type?: string
|
||||
firmware?: string
|
||||
driver?: string
|
||||
battery_charge?: string
|
||||
battery_charge_raw?: number
|
||||
battery_voltage?: string
|
||||
battery_date?: string
|
||||
time_left?: string
|
||||
time_left_seconds?: number
|
||||
load_percent?: string
|
||||
load_percent_raw?: number
|
||||
input_voltage?: string
|
||||
input_frequency?: string
|
||||
output_voltage?: string
|
||||
output_frequency?: string
|
||||
real_power?: string
|
||||
apparent_power?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface GPU {
|
||||
slot: string
|
||||
name: string
|
||||
vendor: string
|
||||
type: string
|
||||
pci_class?: string
|
||||
pci_driver?: string
|
||||
pci_kernel_module?: string
|
||||
driver_version?: string
|
||||
memory_total?: string
|
||||
memory_used?: string
|
||||
memory_free?: string
|
||||
temperature?: number
|
||||
power_draw?: string
|
||||
power_limit?: string
|
||||
utilization_gpu?: number
|
||||
utilization_memory?: number
|
||||
clock_graphics?: string
|
||||
clock_memory?: string
|
||||
engine_render?: number
|
||||
engine_blitter?: number
|
||||
engine_video?: number
|
||||
engine_video_enhance?: number
|
||||
pcie_gen?: string
|
||||
pcie_width?: string
|
||||
fan_speed?: number
|
||||
fan_unit?: string
|
||||
processes?: Array<{
|
||||
pid: string
|
||||
name: string
|
||||
memory: string
|
||||
}>
|
||||
has_monitoring_tool?: boolean
|
||||
note?: string
|
||||
}
|
||||
|
||||
export interface DiskHardwareInfo {
|
||||
type?: string
|
||||
driver?: string
|
||||
interface?: string
|
||||
model?: string
|
||||
serial?: string
|
||||
family?: string
|
||||
firmware?: string
|
||||
rotation_rate?: string
|
||||
form_factor?: string
|
||||
sata_version?: string
|
||||
}
|
||||
|
||||
export interface NetworkHardwareInfo {
|
||||
driver?: string
|
||||
kernel_modules?: string
|
||||
subsystem?: string
|
||||
max_link_speed?: string
|
||||
max_link_width?: string
|
||||
current_link_speed?: string
|
||||
current_link_width?: string
|
||||
interface_name?: string
|
||||
interface_speed?: string
|
||||
mac_address?: string
|
||||
}
|
||||
|
||||
export interface HardwareData {
|
||||
cpu?: {
|
||||
model?: string
|
||||
cores_per_socket?: number
|
||||
sockets?: number
|
||||
total_threads?: number
|
||||
l3_cache?: string
|
||||
virtualization?: string
|
||||
}
|
||||
motherboard?: {
|
||||
manufacturer?: string
|
||||
model?: string
|
||||
bios?: {
|
||||
vendor?: string
|
||||
version?: string
|
||||
date?: string
|
||||
}
|
||||
}
|
||||
memory_modules?: Array<{
|
||||
slot: string
|
||||
size?: string
|
||||
type?: string
|
||||
speed?: string
|
||||
manufacturer?: string
|
||||
}>
|
||||
temperatures?: Temperature[]
|
||||
power_meter?: PowerMeter
|
||||
network_cards?: NetworkInterface[]
|
||||
storage_devices?: StorageDevice[]
|
||||
pci_devices?: PCIDevice[]
|
||||
gpus?: GPU[]
|
||||
fans?: Fan[]
|
||||
power_supplies?: PowerSupply[]
|
||||
ups?: UPS | UPS[]
|
||||
}
|
||||
|
||||
export const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
||||
@@ -1,25 +1,73 @@
|
||||
## 2025-09-04
|
||||
|
||||
### New version v1.1.7
|
||||
|
||||
### Added
|
||||
|
||||
- **ProxMenux Monitor**
|
||||
Your new monitoring tool for Proxmox. Discover all the features that will help you manage and supervise your infrastructure efficiently.
|
||||
|
||||
ProxMenux Monitor is designed to support future updates where **actions can be triggered without using the terminal**, and managed through a **user-friendly interface** accessible across multiple formats and devices.
|
||||
|
||||

|
||||
|
||||
- **New Banner Removal Method**
|
||||
A new function to disable the Proxmox subscription message with improved safety:
|
||||
- Creates a full backup before modifying any files
|
||||
- Shows a clear warning that breaking changes may occur with future GUI updates
|
||||
- If the GUI fails to load, the user can revert changes via SSH from the post-install menu using the **"Uninstall Options → Restore Banner"** tool
|
||||
|
||||
Special thanks to **@eryonki** for providing the improved method.
|
||||
|
||||
---
|
||||
|
||||
### Improved
|
||||
|
||||
- **CORAL TPU Installer Updated for PVE 9**
|
||||
The CORAL TPU driver installer now supports both **Proxmox VE 8 and VE 9**, ensuring compatibility with the latest kernels and udev rules.
|
||||
|
||||
- **Log2RAM Installation & Integration**
|
||||
- Log2RAM installation is now idempotent and can be safely run multiple times.
|
||||
- Automatically adjusts `journald` configuration to align with the size and behavior of Log2RAM.
|
||||
- Ensures journaling is correctly tuned to avoid overflows or RAM exhaustion on low-memory systems.
|
||||
|
||||
- **Network Optimization Function (LXC + NFS)**
|
||||
Improved to prevent “martian source” warnings in setups where **LXC containers share storage with VMs** over NFS within the same server.
|
||||
|
||||
- **APT Upgrade Progress**
|
||||
When running full system upgrades via ProxMenux, a **real-time progress bar** is now displayed, giving the user clear visibility into the update process.
|
||||
|
||||
---
|
||||
|
||||
### Fixed
|
||||
|
||||
- Other small improvements and fixes to optimize runtime performance and eliminate minor bugs.
|
||||
|
||||
|
||||
|
||||
## 2025-01-10
|
||||
|
||||
### New version v1.1.6
|
||||
|
||||

|
||||
|
||||
|
||||
### Added
|
||||
|
||||
- **New Menu: Mount and Share Manager**
|
||||
Introduced a comprehensive new menu for managing shared resources between Proxmox host and LXC containers:
|
||||
|
||||
**Host Configuration Options:**
|
||||
1. **Configure NFS Shared on Host** - Add, view, and remove NFS shared resources on the Proxmox server with automatic export management
|
||||
2. **Configure Samba Shared on Host** - Add, view, and remove Samba/CIFS shared resources on the Proxmox server with share configuration
|
||||
3. **Configure Local Shared on Host** - Create and manage local shared directories with proper permissions on the Proxmox host
|
||||
- **Configure NFS Shared on Host** - Add, view, and remove NFS shared resources on the Proxmox server with automatic export management
|
||||
- **Configure Samba Shared on Host** - Add, view, and remove Samba/CIFS shared resources on the Proxmox server with share configuration
|
||||
- **Configure Local Shared on Host** - Create and manage local shared directories with proper permissions on the Proxmox host
|
||||
|
||||
**LXC Integration Options:**
|
||||
4. **Configure LXC Mount Points (Host ↔ Container)** - **Core feature** that enables mounting host directories into LXC containers with automatic permission handling. Includes the ability to **view existing mount points** for each container in a clear, organized way and **remove mount points** with proper verification that the process completed successfully. Especially optimized for **unprivileged containers** where UID/GID mapping is critical.
|
||||
5. **Configure NFS Client in LXC** - Set up NFS client inside privileged containers
|
||||
6. **Configure Samba Client in LXC** - Set up Samba client inside privileged containers
|
||||
7. **Configure NFS Server in LXC** - Install NFS server inside privileged containers
|
||||
8. **Configure Samba Server in LXC** - Install Samba server inside privileged containers
|
||||
- **Configure LXC Mount Points (Host ↔ Container)** - **Core feature** that enables mounting host directories into LXC containers with automatic permission handling. Includes the ability to **view existing mount points** for each container in a clear, organized way and **remove mount points** with proper verification that the process completed successfully. Especially optimized for **unprivileged containers** where UID/GID mapping is critical.
|
||||
- **Configure NFS Client in LXC** - Set up NFS client inside privileged containers
|
||||
- **Configure Samba Client in LXC** - Set up Samba client inside privileged containers
|
||||
- **Configure NFS Server in LXC** - Install NFS server inside privileged containers
|
||||
- **Configure Samba Server in LXC** - Install Samba server inside privileged containers
|
||||
|
||||
**Documentation & Support:**
|
||||
- **Help & Info (commands)** - Comprehensive guides with step-by-step manual instructions for all sharing scenarios
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<img src="https://github.com/MacRimi/ProxMenux/blob/main/images/main.png"
|
||||
alt="ProxMenu Logo"
|
||||
alt="ProxMenux Logo"
|
||||
style="max-width: 100%; height: auto;" >
|
||||
|
||||
</div>
|
||||
@@ -70,6 +70,12 @@ Then, follow the on-screen options to manage your Proxmox server efficiently.
|
||||
## ⭐ Support the Project!
|
||||
If you find **ProxMenux** useful, consider giving it a ⭐ on GitHub to help others discover it!
|
||||
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#MacRimi/ProxMenux&Date)
|
||||
|
||||
|
||||
<div style="display: flex; justify-content: center; align-items: center;">
|
||||
<a href="https://ko-fi.com/G2G313ECAN" target="_blank" style="display: flex; align-items: center; text-decoration: none;">
|
||||
<img src="https://raw.githubusercontent.com/MacRimi/HWEncoderX/main/images/kofi.png" alt="Support me on Ko-fi" style="width:140px; margin-right:40px;"/>
|
||||
@@ -78,4 +84,10 @@ If you find **ProxMenux** useful, consider giving it a ⭐ on GitHub to help oth
|
||||
|
||||
Support the project on Ko-fi!
|
||||
|
||||
## Contributors
|
||||
<a href="https://github.com/MacRimi/ProxMenux/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=MacRimi/ProxMenux" />
|
||||
</a>
|
||||
|
||||
[contrib.rocks](https://contrib.rocks).
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
@@ -41,11 +41,16 @@ BASE_DIR="/usr/local/share/proxmenux"
|
||||
CONFIG_FILE="$BASE_DIR/config.json"
|
||||
CACHE_FILE="$BASE_DIR/cache.json"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
#EMERGENCY_FILE="$BASE_DIR/emergency_repair.sh"
|
||||
LOCAL_VERSION_FILE="$BASE_DIR/version.txt"
|
||||
MENU_SCRIPT="menu"
|
||||
VENV_PATH="/opt/googletrans-env"
|
||||
|
||||
MONITOR_APPIMAGE_URL="https://github.com/MacRimi/ProxMenux/raw/refs/heads/main/AppImage/ProxMenux-1.0.0.AppImage"
|
||||
MONITOR_SHA256_URL="https://github.com/MacRimi/ProxMenux/raw/refs/heads/main/AppImage/ProxMenux-Monitor.AppImage.sha256"
|
||||
MONITOR_INSTALL_PATH="$BASE_DIR/ProxMenux-Monitor.AppImage"
|
||||
MONITOR_SERVICE_FILE="/etc/systemd/system/proxmenux-monitor.service"
|
||||
MONITOR_PORT=8008
|
||||
|
||||
if ! source <(curl -sSf "$UTILS_URL"); then
|
||||
echo "Error: Could not load utils.sh from $UTILS_URL"
|
||||
exit 1
|
||||
@@ -101,7 +106,7 @@ check_existing_installation() {
|
||||
fi
|
||||
}
|
||||
|
||||
uninstall_proxmenu() {
|
||||
uninstall_proxmenux() {
|
||||
local install_type="$1"
|
||||
local force_clean="$2"
|
||||
|
||||
@@ -168,7 +173,7 @@ handle_installation_change() {
|
||||
if whiptail --title "Installation Type Change" \
|
||||
--yesno "Switch from Translation to Normal Version?\n\nThis will remove translation components." 10 60; then
|
||||
echo "Preparing for installation type change..."
|
||||
uninstall_proxmenu "translation" "force" >/dev/null 2>&1
|
||||
uninstall_proxmenux "translation" "force" >/dev/null 2>&1
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
@@ -193,7 +198,7 @@ update_config() {
|
||||
local status="$2"
|
||||
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
local tracked_components=("dialog" "curl" "jq" "python3" "python3-venv" "python3-pip" "virtual_environment" "pip" "googletrans")
|
||||
local tracked_components=("dialog" "curl" "jq" "python3" "python3-venv" "python3-pip" "virtual_environment" "pip" "googletrans" "proxmenux_monitor")
|
||||
|
||||
if [[ " ${tracked_components[@]} " =~ " ${component} " ]]; then
|
||||
mkdir -p "$(dirname "$CONFIG_FILE")"
|
||||
@@ -274,7 +279,7 @@ show_installation_confirmation() {
|
||||
case "$install_type" in
|
||||
"1")
|
||||
if whiptail --title "ProxMenux - Normal Version Installation" \
|
||||
--yesno "ProxMenux Normal Version will install:\n\n• dialog (interactive menus) - Official Debian package\n• curl (file downloads) - Official Debian package\n• jq (JSON processing) - Official Debian package\n• ProxMenux core files (/usr/local/share/proxmenux)\n\nThis is a lightweight installation with minimal dependencies.\n\nProceed with installation?" 18 70; then
|
||||
--yesno "ProxMenux Normal Version will install:\n\n• dialog (interactive menus) - Official Debian package\n• curl (file downloads) - Official Debian package\n• jq (JSON processing) - Official Debian package\n• ProxMenux core files (/usr/local/share/proxmenux)\n• ProxMenux Monitor (Web dashboard on port 8008)\n\nThis is a lightweight installation with minimal dependencies.\n\nProceed with installation?" 20 70; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
@@ -282,7 +287,7 @@ show_installation_confirmation() {
|
||||
;;
|
||||
"2")
|
||||
if whiptail --title "ProxMenux - Translation Version Installation" \
|
||||
--yesno "ProxMenux Translation Version will install:\n\n• dialog (interactive menus)\n• curl (file downloads)\n• jq (JSON processing)\n• python3 + python3-venv + python3-pip\n• Google Translate library (googletrans)\n• Virtual environment (/opt/googletrans-env)\n• Translation cache system\n• ProxMenux core files\n\nThis version requires more dependencies for translation support.\n\nProceed with installation?" 18 70; then
|
||||
--yesno "ProxMenux Translation Version will install:\n\n• dialog (interactive menus)\n• curl (file downloads)\n• jq (JSON processing)\n• python3 + python3-venv + python3-pip\n• Google Translate library (googletrans)\n• Virtual environment (/opt/googletrans-env)\n• Translation cache system\n• ProxMenux core files\n• ProxMenux Monitor (Web dashboard on port 8008)\n\nThis version requires more dependencies for translation support.\n\nProceed with installation?" 20 70; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
@@ -291,9 +296,112 @@ show_installation_confirmation() {
|
||||
esac
|
||||
}
|
||||
|
||||
get_server_ip() {
|
||||
local ip
|
||||
# Try to get the primary IP address
|
||||
ip=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+')
|
||||
|
||||
if [ -z "$ip" ]; then
|
||||
# Fallback: get first non-loopback IP
|
||||
ip=$(hostname -I | awk '{print $1}')
|
||||
fi
|
||||
|
||||
if [ -z "$ip" ]; then
|
||||
# Last resort: use localhost
|
||||
ip="localhost"
|
||||
fi
|
||||
|
||||
echo "$ip"
|
||||
}
|
||||
|
||||
install_proxmenux_monitor() {
|
||||
# Check if URL is accessible
|
||||
if ! wget --spider -q "$MONITOR_APPIMAGE_URL" 2>/dev/null; then
|
||||
msg_warn "ProxMenux Monitor AppImage not available at: $MONITOR_APPIMAGE_URL"
|
||||
msg_info "The monitor will be available in future releases."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Download AppImage silently
|
||||
if ! wget -q -O "$MONITOR_INSTALL_PATH" "$MONITOR_APPIMAGE_URL" 2>&1; then
|
||||
msg_warn "Failed to download ProxMenux Monitor from GitHub."
|
||||
msg_info "You can install it manually later when available."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Download SHA256 checksum silently
|
||||
local sha256_file="/tmp/proxmenux-monitor.sha256"
|
||||
if ! wget -q -O "$sha256_file" "$MONITOR_SHA256_URL" 2>/dev/null; then
|
||||
msg_warn "SHA256 checksum file not available. Skipping verification."
|
||||
msg_info "AppImage downloaded but integrity cannot be verified."
|
||||
rm -f "$sha256_file"
|
||||
else
|
||||
# Verify SHA256 silently
|
||||
local expected_hash=$(cat "$sha256_file" | awk '{print $1}')
|
||||
local actual_hash=$(sha256sum "$MONITOR_INSTALL_PATH" | awk '{print $1}')
|
||||
|
||||
if [ "$expected_hash" != "$actual_hash" ]; then
|
||||
msg_error "SHA256 verification failed! AppImage may be corrupted."
|
||||
msg_info "Expected: $expected_hash"
|
||||
msg_info "Got: $actual_hash"
|
||||
rm -f "$MONITOR_INSTALL_PATH" "$sha256_file"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$sha256_file"
|
||||
fi
|
||||
|
||||
# Make executable
|
||||
chmod +x "$MONITOR_INSTALL_PATH"
|
||||
|
||||
# Show single success message at the end
|
||||
msg_ok "ProxMenux Monitor installed and activated successfully."
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
create_monitor_service() {
|
||||
msg_info "Creating ProxMenux Monitor service..."
|
||||
|
||||
cat > "$MONITOR_SERVICE_FILE" << EOF
|
||||
[Unit]
|
||||
Description=ProxMenux Monitor - Web Dashboard
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=$BASE_DIR
|
||||
ExecStart=$MONITOR_INSTALL_PATH
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
Environment="PORT=$MONITOR_PORT"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Reload systemd, enable and start service
|
||||
systemctl daemon-reload
|
||||
systemctl enable proxmenux-monitor.service > /dev/null 2>&1
|
||||
systemctl start proxmenux-monitor.service > /dev/null 2>&1
|
||||
|
||||
# Wait a moment for service to start
|
||||
sleep 2
|
||||
|
||||
# Check if service is running
|
||||
if systemctl is-active --quiet proxmenux-monitor.service; then
|
||||
msg_ok "ProxMenux Monitor service started successfully."
|
||||
update_config "proxmenux_monitor" "installed"
|
||||
return 0
|
||||
else
|
||||
msg_warn "ProxMenux Monitor service failed to start. Check logs with: journalctl -u proxmenux-monitor"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
####################################################
|
||||
install_normal_version() {
|
||||
local total_steps=3
|
||||
local total_steps=4 # Increased from 3 to 4 for monitor installation
|
||||
local current_step=1
|
||||
|
||||
show_progress $current_step $total_steps "Installing basic dependencies"
|
||||
@@ -350,7 +458,6 @@ install_normal_version() {
|
||||
|
||||
FILES=(
|
||||
"$UTILS_FILE $REPO_URL/scripts/utils.sh"
|
||||
# "$EMERGENCY_FILE $REPO_URL/scripts/emergency_repair.sh"
|
||||
"$INSTALL_DIR/$MENU_SCRIPT $REPO_URL/$MENU_SCRIPT"
|
||||
"$LOCAL_VERSION_FILE $REPO_URL/version.txt"
|
||||
)
|
||||
@@ -368,12 +475,18 @@ install_normal_version() {
|
||||
done
|
||||
|
||||
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
|
||||
# chmod +x "$EMERGENCY_FILE"
|
||||
|
||||
((current_step++))
|
||||
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
|
||||
|
||||
if install_proxmenux_monitor; then
|
||||
create_monitor_service
|
||||
fi
|
||||
}
|
||||
|
||||
####################################################
|
||||
install_translation_version() {
|
||||
local total_steps=4
|
||||
local total_steps=5 # Increased from 4 to 5 for monitor installation
|
||||
local current_step=1
|
||||
|
||||
show_progress $current_step $total_steps "Language selection"
|
||||
@@ -470,7 +583,6 @@ install_translation_version() {
|
||||
FILES=(
|
||||
"$CACHE_FILE $REPO_URL/json/cache.json"
|
||||
"$UTILS_FILE $REPO_URL/scripts/utils.sh"
|
||||
# "$EMERGENCY_FILE $REPO_URL/scripts/emergency_repair.sh"
|
||||
"$INSTALL_DIR/$MENU_SCRIPT $REPO_URL/$MENU_SCRIPT"
|
||||
"$LOCAL_VERSION_FILE $REPO_URL/version.txt"
|
||||
)
|
||||
@@ -491,7 +603,13 @@ install_translation_version() {
|
||||
done
|
||||
|
||||
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
|
||||
#chmod +x "$EMERGENCY_FILE"
|
||||
|
||||
((current_step++))
|
||||
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
|
||||
|
||||
if install_proxmenux_monitor; then
|
||||
create_monitor_service
|
||||
fi
|
||||
}
|
||||
|
||||
####################################################
|
||||
@@ -518,9 +636,6 @@ show_installation_options() {
|
||||
esac
|
||||
fi
|
||||
|
||||
|
||||
|
||||
|
||||
if [[ "$pve_version" -ge 9 ]]; then
|
||||
INSTALL_TYPE=$(whiptail --backtitle "ProxMenux" --title "$menu_title" --menu "\n$menu_text" 14 70 2 \
|
||||
"1" "Normal Version (English only)" 3>&1 1>&2 2>&3)
|
||||
@@ -541,8 +656,6 @@ show_installation_options() {
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
|
||||
if [ -z "$INSTALL_TYPE" ]; then
|
||||
show_proxmenux_logo
|
||||
@@ -587,6 +700,13 @@ install_proxmenu() {
|
||||
esac
|
||||
|
||||
msg_title "$(translate "ProxMenux has been installed successfully")"
|
||||
|
||||
if systemctl is-active --quiet proxmenux-monitor.service; then
|
||||
local server_ip=$(get_server_ip)
|
||||
echo -e "${GN}🌐 $(translate "ProxMenux Monitor activated")${CL}: ${BL}http://${server_ip}:${MONITOR_PORT}${CL}"
|
||||
echo
|
||||
fi
|
||||
|
||||
echo -ne "${GN}"
|
||||
type_text "$(translate "To run ProxMenux, simply execute this command in the console or terminal:")"
|
||||
echo -e "${YWB} menu${CL}"
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
"it": "Giapponese",
|
||||
"pt": "Japonês"
|
||||
},
|
||||
"Thank you for using ProxMenu. Goodbye!": {
|
||||
"Thank you for using ProxMenux. Goodbye!": {
|
||||
"es": "Gracias por usar ProxMenu. ¡Adiós!",
|
||||
"fr": "Merci d'avoir utilisé ProxMenu. Au revoir!",
|
||||
"de": "Danke für die Nutzung von ProxMenu. Auf Wiedersehen!",
|
||||
@@ -900,7 +900,11 @@
|
||||
"it": "Trova il tuo dispositivo usando https://finds.synology.com"
|
||||
},
|
||||
"Help and Info Commands": {
|
||||
"es": "Comandos de ayuda e información"
|
||||
"es": "Comandos de ayuda e información",
|
||||
"fr": "Aide et Informations (commandes)",
|
||||
"de": "Hilfe & Informationen (Befehle)",
|
||||
"it": "Aiuto e Informazioni (comandi)",
|
||||
"pt": "Ajuda e Informações (comandos)"
|
||||
},
|
||||
"Create VM from template or script": {
|
||||
"es": "Crear VM a partir de plantilla o script",
|
||||
@@ -2566,5 +2570,103 @@
|
||||
"de": "Notfallwiederherstellung:",
|
||||
"it": "Ripristino di emergenza:",
|
||||
"pt": "Recuperação de emergência:"
|
||||
},
|
||||
"Mount and Share Manager": {
|
||||
"es": "Montajes y Recursos Compartidos",
|
||||
"fr": "Gestionnaire de Montage et Partage",
|
||||
"de": "Mount- und Share-Manager",
|
||||
"it": "Gestore di Mount e Condivisioni",
|
||||
"pt": "Gerenciador de Montagem e Compartilhamento"
|
||||
},
|
||||
"HOST": {
|
||||
"es": "HOST",
|
||||
"fr": "HÔTE",
|
||||
"de": "HOST",
|
||||
"it": "HOST",
|
||||
"pt": "HOST"
|
||||
},
|
||||
"Configure NFS shared on Host": {
|
||||
"es": "Configurar recursos NFS compartidos en el Host",
|
||||
"fr": "Configurer les partages NFS sur l'hôte",
|
||||
"de": "NFS-Freigaben auf Host konfigurieren",
|
||||
"it": "Configurare condivisioni NFS su Host",
|
||||
"pt": "Configurar compartilhamentos NFS no Host"
|
||||
},
|
||||
"Configure Samba shared on Host": {
|
||||
"es": "Configurar recursos Samba compartidos en el Host",
|
||||
"fr": "Configurer les partages Samba sur l'hôte",
|
||||
"de": "Samba-Freigaben auf Host konfigurieren",
|
||||
"it": "Configurare condivisioni Samba su Host",
|
||||
"pt": "Configurar compartilhamentos Samba no Host"
|
||||
},
|
||||
"Configure Local Shared on Host": {
|
||||
"es": "Configurar directorios locales compartidos en el Host",
|
||||
"fr": "Configurer les répertoires locaux partagés sur l'hôte",
|
||||
"de": "Lokale geteilte Verzeichnisse auf Host konfigurieren",
|
||||
"it": "Configurare directory locali condivise su Host",
|
||||
"pt": "Configurar diretórios locais compartilhados no Host"
|
||||
},
|
||||
"LXC": {
|
||||
"es": "LXC",
|
||||
"fr": "LXC",
|
||||
"de": "LXC",
|
||||
"it": "LXC",
|
||||
"pt": "LXC"
|
||||
},
|
||||
"Configure LXC Mount Points (Host ↔ Container)": {
|
||||
"es": "Configurar puntos de montaje LXC (Host ↔ LXC)",
|
||||
"fr": "Configurer les points de montage LXC (Hôte ↔ LXC)",
|
||||
"de": "LXC-Mount-Punkte konfigurieren (Host ↔ LXC)",
|
||||
"it": "Configurare punti di mount LXC (Host ↔ LXC)",
|
||||
"pt": "Configurar pontos de montagem LXC (Host ↔ LXC)"
|
||||
},
|
||||
"Configure NFS Client in LXC (only privileged)": {
|
||||
"es": "Configurar cliente NFS en LXC (solo privilegiados)",
|
||||
"fr": "Configurer le client NFS dans LXC (privilégiés uniquement)",
|
||||
"de": "NFS-Client in LXC konfigurieren (nur privilegiert)",
|
||||
"it": "Configurare client NFS in LXC (solo privilegiati)",
|
||||
"pt": "Configurar cliente NFS em LXC (apenas privilegiados)"
|
||||
},
|
||||
"Configure Samba Client in LXC (only privileged)": {
|
||||
"es": "Configurar cliente Samba en LXC (solo privilegiados)",
|
||||
"fr": "Configurer le client Samba dans LXC (privilégiés uniquement)",
|
||||
"de": "Samba-Client in LXC konfigurieren (nur privilegiert)",
|
||||
"it": "Configurare client Samba in LXC (solo privilegiati)",
|
||||
"pt": "Configurar cliente Samba em LXC (apenas privilegiados)"
|
||||
},
|
||||
"Configure NFS Server in LXC (only privileged)": {
|
||||
"es": "Configurar servidor NFS en LXC (solo privilegiados)",
|
||||
"fr": "Configurer le serveur NFS dans LXC (privilégiés uniquement)",
|
||||
"de": "NFS-Server in LXC konfigurieren (nur privilegiert)",
|
||||
"it": "Configurare server NFS in LXC (solo privilegiati)",
|
||||
"pt": "Configurar servidor NFS em LXC (apenas privilegiados)"
|
||||
},
|
||||
"configure Samba Server in LXC (only privileged)": {
|
||||
"es": "Configurar servidor Samba en LXC (solo privilegiados)",
|
||||
"fr": "Configurer le serveur Samba dans LXC (privilégiés uniquement)",
|
||||
"de": "Samba-Server in LXC konfigurieren (nur privilegiert)",
|
||||
"it": "Configurare server Samba in LXC (solo privilegiati)",
|
||||
"pt": "Configurar servidor Samba em LXC (apenas privilegiados)"
|
||||
},
|
||||
"Help & Info (commands)": {
|
||||
"es": "Comandos de ayuda e información",
|
||||
"fr": "Aide et Informations (commandes)",
|
||||
"de": "Hilfe & Informationen (Befehle)",
|
||||
"it": "Aiuto e Informazioni (comandi)",
|
||||
"pt": "Ajuda e Informações (comandos)"
|
||||
},
|
||||
"English": {
|
||||
"es": "Inglés",
|
||||
"fr": "Anglais",
|
||||
"de": "Englisch",
|
||||
"it": "Inglese",
|
||||
"pt": "Inglês"
|
||||
},
|
||||
"Language Change": {
|
||||
"es": "Cambio de Idioma",
|
||||
"fr": "Changement de Langue",
|
||||
"de": "Sprachänderung",
|
||||
"it": "Cambio Lingua",
|
||||
"pt": "Mudança de Idioma"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
@@ -13,13 +13,13 @@
|
||||
# This script serves as the main entry point for ProxMenux,
|
||||
# a menu-driven tool designed for Proxmox VE management.
|
||||
#
|
||||
# - Displays the ProxMenu logo on startup.
|
||||
# - Displays the ProxMenux logo on startup.
|
||||
# - Loads necessary configurations and language settings.
|
||||
# - Checks for available updates and installs them if confirmed.
|
||||
# - Downloads and executes the latest main menu script.
|
||||
#
|
||||
# Key Features:
|
||||
# - Ensures ProxMenu is always up-to-date by fetching the latest version.
|
||||
# - Ensures ProxMenux is always up-to-date by fetching the latest version.
|
||||
# - Uses whiptail for interactive menus and language selection.
|
||||
# - Loads utility functions and translation support.
|
||||
# - Maintains a cache system to improve performance.
|
||||
@@ -67,7 +67,7 @@ check_updates() {
|
||||
if whiptail --title "$(translate "Update Available")" \
|
||||
--yesno "$(translate "New version available") ($REMOTE_VERSION)\n\n$(translate "Do you want to update now?")" \
|
||||
10 60 --defaultno; then
|
||||
msg_warn "$(translate "Starting ProxMenu update...")"
|
||||
msg_warn "$(translate "Starting ProxMenux update...")"
|
||||
|
||||
if wget -qO "$INSTALL_SCRIPT" "$REPO_URL/install_proxmenux.sh"; then
|
||||
chmod +x "$INSTALL_SCRIPT"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu - Network Management and Repair Tool
|
||||
# ProxMenux - Network Management and Repair Tool
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# Remove Subscription Banner - Proxmox VE (v3 - Minimal Intrusive)
|
||||
# ==========================================================
|
||||
# This version makes a surgical change to the checked_command function
|
||||
# by changing the condition to 'if (false)' and commenting out the banner logic.
|
||||
# Also patches the mobile UI to remove the subscription dialog.
|
||||
# ==========================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Source utilities if available
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
TOOLS_JSON="/usr/local/share/proxmenux/installed_tools.json"
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
# File paths
|
||||
JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||||
GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
|
||||
MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
|
||||
MOBILE_UI_FILE="/usr/share/pve-yew-mobile-gui/index.html.tpl"
|
||||
BACKUP_DIR="$BASE_DIR/backups"
|
||||
APT_HOOK="/etc/apt/apt.conf.d/no-nag-script"
|
||||
PATCH_BIN="/usr/local/bin/pve-remove-nag-v3.sh"
|
||||
MARK="/* PROXMENUX_NAG_PATCH_V3 */"
|
||||
MOBILE_MARK="<!-- PROXMENUX_MOBILE_NAG_PATCH -->"
|
||||
|
||||
# Ensure tools JSON exists
|
||||
ensure_tools_json() {
|
||||
[ -f "$TOOLS_JSON" ] || echo "{}" > "$TOOLS_JSON"
|
||||
}
|
||||
|
||||
# Register tool in JSON
|
||||
register_tool() {
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
local tool="$1" state="$2"
|
||||
ensure_tools_json
|
||||
jq --arg t "$tool" --argjson v "$state" '.[$t]=$v' "$TOOLS_JSON" \
|
||||
> "$TOOLS_JSON.tmp" && mv "$TOOLS_JSON.tmp" "$TOOLS_JSON"
|
||||
}
|
||||
|
||||
# Verify JS file integrity
|
||||
verify_js_integrity() {
|
||||
local file="$1"
|
||||
[ -f "$file" ] || return 1
|
||||
[ -s "$file" ] || return 1
|
||||
grep -Eq 'Ext|function|var|const|let' "$file" || return 1
|
||||
if LC_ALL=C grep -qP '\x00' "$file" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Create timestamped backup
|
||||
create_backup() {
|
||||
local file="$1"
|
||||
local timestamp
|
||||
timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
local backup_file="$BACKUP_DIR/$(basename "$file").backup.$timestamp"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
if [ -f "$file" ]; then
|
||||
rm -f "$BACKUP_DIR"/"$(basename "$file")".backup.* 2>/dev/null || true
|
||||
|
||||
cp -a "$file" "$backup_file"
|
||||
echo "$backup_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create the patch script that will be called by APT hook
|
||||
create_patch_script() {
|
||||
cat > "$PATCH_BIN" <<'EOFPATCH'
|
||||
#!/usr/bin/env bash
|
||||
# ==========================================================
|
||||
# Proxmox Subscription Banner Patch (v3 - Minimal)
|
||||
# ==========================================================
|
||||
set -euo pipefail
|
||||
|
||||
JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||||
GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
|
||||
MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
|
||||
MOBILE_UI_FILE="/usr/share/pve-yew-mobile-gui/index.html.tpl"
|
||||
BACKUP_DIR="/usr/local/share/proxmenux/backups"
|
||||
MARK="/* PROXMENUX_NAG_PATCH_V3 */"
|
||||
MOBILE_MARK="<!-- PROXMENUX_MOBILE_NAG_PATCH -->"
|
||||
|
||||
verify_js_integrity() {
|
||||
local file="$1"
|
||||
[ -f "$file" ] && [ -s "$file" ] && grep -Eq 'Ext|function' "$file" && ! LC_ALL=C grep -qP '\x00' "$file" 2>/dev/null
|
||||
}
|
||||
|
||||
patch_checked_command() {
|
||||
[ -f "$JS_FILE" ] || return 0
|
||||
|
||||
# Check if already patched
|
||||
grep -q "$MARK" "$JS_FILE" && return 0
|
||||
|
||||
# Create backup
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
local backup="$BACKUP_DIR/$(basename "$JS_FILE").backup.$(date +%Y%m%d_%H%M%S)"
|
||||
cp -a "$JS_FILE" "$backup"
|
||||
|
||||
# Set trap to restore on error
|
||||
trap "cp -a '$backup' '$JS_FILE' 2>/dev/null || true" ERR
|
||||
|
||||
# Add patch marker at the beginning
|
||||
sed -i "1s|^|$MARK\n|" "$JS_FILE"
|
||||
|
||||
# Surgical patch: Change the condition in checked_command function
|
||||
# This changes the if condition to 'if (false)' making the banner never show
|
||||
if grep -q "res\.data\.status\.toLowerCase() !== 'active'" "$JS_FILE"; then
|
||||
# Pattern for newer versions (8.4.5+)
|
||||
sed -i "/checked_command: function/,/},$/s/res === null || res === undefined || !res || res\.data\.status\.toLowerCase() !== 'active'/false/g" "$JS_FILE"
|
||||
elif grep -q "res\.data\.status !== 'Active'" "$JS_FILE"; then
|
||||
# Pattern for older versions
|
||||
sed -i "/checked_command: function/,/},$/s/res === null || res === undefined || !res || res\.data\.status !== 'Active'/false/g" "$JS_FILE"
|
||||
fi
|
||||
|
||||
# Also handle the NoMoreNagging pattern if present
|
||||
if grep -q "res\.data\.status\.toLowerCase() !== 'NoMoreNagging'" "$JS_FILE"; then
|
||||
sed -i "/checked_command: function/,/},$/s/res === null || res === undefined || !res || res\.data\.status\.toLowerCase() !== 'NoMoreNagging'/false/g" "$JS_FILE"
|
||||
fi
|
||||
|
||||
# Verify integrity after patch
|
||||
if ! verify_js_integrity "$JS_FILE"; then
|
||||
cp -a "$backup" "$JS_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Clean up generated files
|
||||
rm -f "$MIN_JS_FILE" "$GZ_FILE" 2>/dev/null || true
|
||||
find /var/cache/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
find /var/lib/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
find /var/cache/nginx/ -type f -delete 2>/dev/null || true
|
||||
|
||||
trap - ERR
|
||||
return 0
|
||||
}
|
||||
|
||||
patch_mobile_ui() {
|
||||
[ -f "$MOBILE_UI_FILE" ] || return 0
|
||||
|
||||
# Check if already patched
|
||||
grep -q "$MOBILE_MARK" "$MOBILE_UI_FILE" && return 0
|
||||
|
||||
# Create backup
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
local backup="$BACKUP_DIR/$(basename "$MOBILE_UI_FILE").backup.$(date +%Y%m%d_%H%M%S)"
|
||||
cp -a "$MOBILE_UI_FILE" "$backup"
|
||||
|
||||
# Set trap to restore on error
|
||||
trap "cp -a '$backup' '$MOBILE_UI_FILE' 2>/dev/null || true" ERR
|
||||
|
||||
# Insert the script before </head> tag
|
||||
sed -i "/<\/head>/i\\
|
||||
$MOBILE_MARK\\
|
||||
<!-- Script to remove subscription banner from mobile UI -->\\
|
||||
<script>\\
|
||||
function removeNoSubDialog() {\\
|
||||
const observer = new MutationObserver(() => {\\
|
||||
const diag = document.querySelector('dialog[aria-label=\"No valid subscription\"]');\\
|
||||
if (diag) {\\
|
||||
diag.remove();\\
|
||||
}\\
|
||||
});\\
|
||||
observer.observe(document.body, { childList: true, subtree: true });\\
|
||||
}\\
|
||||
window.addEventListener('load', () => {\\
|
||||
setTimeout(removeNoSubDialog, 200);\\
|
||||
});\\
|
||||
</script>" "$MOBILE_UI_FILE"
|
||||
|
||||
trap - ERR
|
||||
return 0
|
||||
}
|
||||
|
||||
reload_services() {
|
||||
systemctl is-active --quiet pveproxy 2>/dev/null && {
|
||||
systemctl reload pveproxy 2>/dev/null || systemctl restart pveproxy 2>/dev/null || true
|
||||
}
|
||||
systemctl is-active --quiet nginx 2>/dev/null && {
|
||||
systemctl reload nginx 2>/dev/null || true
|
||||
}
|
||||
systemctl is-active --quiet pvedaemon 2>/dev/null && {
|
||||
systemctl reload pvedaemon 2>/dev/null || true
|
||||
}
|
||||
}
|
||||
|
||||
main() {
|
||||
patch_checked_command || return 1
|
||||
patch_mobile_ui || true
|
||||
reload_services
|
||||
}
|
||||
|
||||
main
|
||||
EOFPATCH
|
||||
|
||||
chmod 755 "$PATCH_BIN"
|
||||
}
|
||||
|
||||
# Create APT hook to reapply patch after updates
|
||||
create_apt_hook() {
|
||||
cat > "$APT_HOOK" <<'EOFAPT'
|
||||
/* ProxMenux: reapply minimal nag patch after upgrades */
|
||||
DPkg::Post-Invoke { "/usr/local/bin/pve-remove-nag-v3.sh || true"; };
|
||||
EOFAPT
|
||||
|
||||
chmod 644 "$APT_HOOK"
|
||||
|
||||
# Verify APT hook syntax
|
||||
apt-config dump >/dev/null 2>&1 || {
|
||||
rm -f "$APT_HOOK"
|
||||
}
|
||||
}
|
||||
|
||||
# Main function to remove subscription banner
|
||||
remove_subscription_banner_v3() {
|
||||
local pve_version
|
||||
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||
|
||||
msg_info "$(translate "Detected Proxmox VE") ${pve_version} - $(translate "applying banner patch")"
|
||||
|
||||
|
||||
|
||||
# Remove old APT hooks
|
||||
for f in /etc/apt/apt.conf.d/*nag*; do
|
||||
[[ -e "$f" ]] && rm -f "$f"
|
||||
done
|
||||
|
||||
# Create backup for desktop UI
|
||||
local backup_file
|
||||
backup_file=$(create_backup "$JS_FILE")
|
||||
if [ -n "$backup_file" ]; then
|
||||
# msg_ok "$(translate "Desktop UI backup created"): $backup_file"
|
||||
:
|
||||
fi
|
||||
|
||||
if [ -f "$MOBILE_UI_FILE" ]; then
|
||||
local mobile_backup
|
||||
mobile_backup=$(create_backup "$MOBILE_UI_FILE")
|
||||
if [ -n "$mobile_backup" ]; then
|
||||
# msg_ok "$(translate "Mobile UI backup created"): $mobile_backup"
|
||||
:
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create patch script and APT hook
|
||||
create_patch_script
|
||||
create_apt_hook
|
||||
|
||||
# Apply the patch
|
||||
if ! "$PATCH_BIN"; then
|
||||
msg_error "$(translate "Error applying patch. Backups preserved at"): $BACKUP_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Register tool as applied
|
||||
register_tool "subscription_banner" true
|
||||
|
||||
msg_ok "$(translate "Subscription banner removed successfully")"
|
||||
msg_ok "$(translate "Desktop and Mobile UI patched")"
|
||||
msg_ok "$(translate "Refresh your browser (Ctrl+Shift+R) to see changes")"
|
||||
|
||||
}
|
||||
|
||||
# Run if executed directly
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
remove_subscription_banner_v3
|
||||
fi
|
||||
@@ -0,0 +1,257 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# Remove Subscription Banner - Proxmox VE 9.x (Clean Version)
|
||||
# ==========================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
TOOLS_JSON="/usr/local/share/proxmenux/installed_tools.json"
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
|
||||
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
ensure_tools_json() {
|
||||
[ -f "$TOOLS_JSON" ] || echo "{}" > "$TOOLS_JSON"
|
||||
}
|
||||
|
||||
register_tool() {
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
local tool="$1" state="$2"
|
||||
ensure_tools_json
|
||||
jq --arg t "$tool" --argjson v "$state" '.[$t]=$v' "$TOOLS_JSON" \
|
||||
> "$TOOLS_JSON.tmp" && mv "$TOOLS_JSON.tmp" "$TOOLS_JSON"
|
||||
}
|
||||
|
||||
JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||||
MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
|
||||
GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
|
||||
MOBILE_TPL="/usr/share/pve-yew-mobile-gui/index.html.tpl"
|
||||
APT_HOOK="/etc/apt/apt.conf.d/no-nag-script"
|
||||
PATCH_BIN="/usr/local/bin/pve-remove-nag.sh"
|
||||
|
||||
MARK_JS="PROXMENUX_NAG_REMOVED_v2"
|
||||
MARK_MOBILE="<!-- PROXMENUX: MOBILE NAG PATCH v2 -->"
|
||||
|
||||
|
||||
verify_js_integrity() {
|
||||
local file="$1"
|
||||
[ -f "$file" ] || return 1
|
||||
[ -s "$file" ] || return 1
|
||||
grep -Eq 'Ext|function|var|const|let' "$file" || return 1
|
||||
if LC_ALL=C grep -qP '\x00' "$file" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
create_backup() {
|
||||
local file="$1"
|
||||
local backup_dir="$BASE_DIR/backups"
|
||||
local timestamp
|
||||
timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
local backup_file="$backup_dir/$(basename "$file").backup.$timestamp"
|
||||
mkdir -p "$backup_dir"
|
||||
if [ -f "$file" ]; then
|
||||
cp -a "$file" "$backup_file"
|
||||
ls -t "$backup_dir"/"$(basename "$file")".backup.* 2>/dev/null | tail -n +6 | xargs -r rm -f 2>/dev/null || true
|
||||
echo "$backup_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# ----------------------------------------------------
|
||||
|
||||
create_patch_script() {
|
||||
cat > "$PATCH_BIN" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||||
MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
|
||||
GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
|
||||
MOBILE_TPL="/usr/share/pve-yew-mobile-gui/index.html.tpl"
|
||||
MARK_JS="PROXMENUX_NAG_REMOVED_v2"
|
||||
MARK_MOBILE="<!-- PROXMENUX: MOBILE NAG PATCH v2 -->"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
|
||||
verify_js_integrity() {
|
||||
local file="$1"
|
||||
[ -f "$file" ] && [ -s "$file" ] && grep -Eq 'Ext|function' "$file" && ! LC_ALL=C grep -qP '\x00' "$file" 2>/dev/null
|
||||
}
|
||||
|
||||
patch_web() {
|
||||
[ -f "$JS_FILE" ] || return 0
|
||||
grep -q "$MARK_JS" "$JS_FILE" && return 0
|
||||
|
||||
local backup_dir="$BASE_DIR/backups"
|
||||
mkdir -p "$backup_dir"
|
||||
local backup="$backup_dir/$(basename "$JS_FILE").backup.$(date +%Y%m%d_%H%M%S)"
|
||||
cp -a "$JS_FILE" "$backup"
|
||||
trap "cp -a '$backup' '$JS_FILE' 2>/dev/null || true" ERR
|
||||
|
||||
sed -i '1s|^|/* '"$MARK_JS"' */\n|' "$JS_FILE"
|
||||
|
||||
local patterns_found=0
|
||||
|
||||
if grep -q "res\.data\.status\.toLowerCase() !== 'active'" "$JS_FILE"; then
|
||||
sed -i "s/res\.data\.status\.toLowerCase() !== 'active'/false/g" "$JS_FILE"
|
||||
patterns_found=$((patterns_found + 1))
|
||||
fi
|
||||
|
||||
if grep -q "subscriptionActive: ''" "$JS_FILE"; then
|
||||
sed -i "s/subscriptionActive: ''/subscriptionActive: true/g" "$JS_FILE"
|
||||
patterns_found=$((patterns_found + 1))
|
||||
fi
|
||||
|
||||
if grep -q "title: gettext('No valid subscription')" "$JS_FILE"; then
|
||||
sed -i "s/title: gettext('No valid subscription')/title: gettext('Community Edition')/g" "$JS_FILE"
|
||||
patterns_found=$((patterns_found + 1))
|
||||
fi
|
||||
|
||||
if grep -q "icon: Ext\.Msg\.WARNING" "$JS_FILE"; then
|
||||
sed -i "s/icon: Ext\.Msg\.WARNING/icon: Ext.Msg.INFO/g" "$JS_FILE"
|
||||
patterns_found=$((patterns_found + 1))
|
||||
fi
|
||||
|
||||
if grep -q "subscription = !(" "$JS_FILE"; then
|
||||
sed -i "s/subscription = !(/subscription = false \&\& (/g" "$JS_FILE"
|
||||
patterns_found=$((patterns_found + 1))
|
||||
fi
|
||||
|
||||
# Si nada coincidió (cambio upstream), restaura y sal limpio
|
||||
if [ "${patterns_found:-0}" -eq 0 ]; then
|
||||
cp -a "$backup" "$JS_FILE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Verificación final
|
||||
if ! verify_js_integrity "$JS_FILE"; then
|
||||
cp -a "$backup" "$JS_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Limpiar artefactos/cachés
|
||||
rm -f "$MIN_JS_FILE" "$GZ_FILE" 2>/dev/null || true
|
||||
find /var/cache/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
find /var/lib/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
find /var/cache/nginx/ -type f -delete 2>/dev/null || true
|
||||
|
||||
trap - ERR
|
||||
}
|
||||
|
||||
patch_mobile() {
|
||||
[ -f "$MOBILE_TPL" ] || return 0
|
||||
grep -q "$MARK_MOBILE" "$MOBILE_TPL" && return 0
|
||||
|
||||
local backup_dir="$BASE_DIR/backups"
|
||||
mkdir -p "$backup_dir"
|
||||
cp -a "$MOBILE_TPL" "$backup_dir/$(basename "$MOBILE_TPL").backup.$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
cat >> "$MOBILE_TPL" <<EOM
|
||||
$MARK_MOBILE
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
function removeSubscriptionElements() {
|
||||
try {
|
||||
const dialogs = document.querySelectorAll('dialog.pwt-outer-dialog');
|
||||
dialogs.forEach(d => {
|
||||
const text = (d.textContent || '').toLowerCase();
|
||||
if (text.includes('subscription') || text.includes('no valid')) { d.remove(); }
|
||||
});
|
||||
const cards = document.querySelectorAll('.pwt-card.pwt-p-2.pwt-d-flex.pwt-interactive.pwt-justify-content-center');
|
||||
cards.forEach(c => {
|
||||
const text = (c.textContent || '').toLowerCase();
|
||||
const hasButton = c.querySelector('button');
|
||||
if (!hasButton && (text.includes('subscription') || text.includes('no valid'))) { c.remove(); }
|
||||
});
|
||||
const alerts = document.querySelectorAll('[class*="alert"], [class*="warning"], [class*="notice"]');
|
||||
alerts.forEach(a => {
|
||||
const text = (a.textContent || '').toLowerCase();
|
||||
if (text.includes('subscription') || text.includes('no valid')) { a.remove(); }
|
||||
});
|
||||
} catch (e) { console.warn('Error removing subscription elements:', e); }
|
||||
}
|
||||
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', removeSubscriptionElements); }
|
||||
else { removeSubscriptionElements(); }
|
||||
const observer = new MutationObserver(removeSubscriptionElements);
|
||||
if (document.body) {
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
const interval = setInterval(removeSubscriptionElements, 500);
|
||||
setTimeout(() => { try { observer.disconnect(); clearInterval(interval); } catch(e){} }, 30000);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
EOM
|
||||
}
|
||||
|
||||
reload_services() {
|
||||
systemctl is-active --quiet pveproxy 2>/dev/null && {
|
||||
systemctl reload pveproxy 2>/dev/null || systemctl restart pveproxy 2>/dev/null || true
|
||||
}
|
||||
systemctl is-active --quiet nginx 2>/dev/null && {
|
||||
systemctl reload nginx 2>/dev/null || true
|
||||
}
|
||||
systemctl is-active --quiet pvedaemon 2>/dev/null && {
|
||||
systemctl reload pvedaemon 2>/dev/null || true
|
||||
}
|
||||
find /var/cache/pve-manager/ -type f -delete 2>/dev/null || true
|
||||
find /var/lib/pve-manager/ -type f -delete 2>/dev/null || true
|
||||
}
|
||||
|
||||
main() {
|
||||
patch_web || return 1
|
||||
patch_mobile
|
||||
reload_services
|
||||
}
|
||||
|
||||
main
|
||||
EOF
|
||||
chmod 755 "$PATCH_BIN"
|
||||
}
|
||||
# ----------------------------------------------------
|
||||
|
||||
|
||||
create_apt_hook() {
|
||||
cat > "$APT_HOOK" <<'EOF'
|
||||
/* ProxMenux: reapply nag patch after upgrades */
|
||||
DPkg::Post-Invoke { "/usr/local/bin/pve-remove-nag.sh || true"; };
|
||||
EOF
|
||||
chmod 644 "$APT_HOOK"
|
||||
apt-config dump >/dev/null 2>&1 || { msg_warn "APT hook syntax issue"; rm -f "$APT_HOOK"; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
remove_subscription_banner_pve9() {
|
||||
local pve_version
|
||||
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1 || true)
|
||||
local pve_major="${pve_version%%.*}"
|
||||
|
||||
msg_info "$(translate "Detected Proxmox VE ${pve_version:-9.x} – removing subscription banner")"
|
||||
|
||||
create_patch_script
|
||||
create_apt_hook
|
||||
|
||||
if ! "$PATCH_BIN"; then
|
||||
msg_error "$(translate "Error applying patches")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
register_tool "subscription_banner" true
|
||||
msg_ok "$(translate "Subscription banner removed successfully.")"
|
||||
msg_ok "$(translate "Refresh your browser to see changes.")"
|
||||
}
|
||||
|
||||
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
remove_subscription_banner_pve9
|
||||
fi
|
||||
@@ -35,6 +35,7 @@ download_common_functions() {
|
||||
}
|
||||
|
||||
update_pve9() {
|
||||
local pve_version=$(pveversion | awk -F'/' '{print $2}' | cut -d'-' -f1)
|
||||
local start_time=$(date +%s)
|
||||
local log_file="/var/log/proxmox-update-$(date +%Y%m%d-%H%M%S).log"
|
||||
local changes_made=false
|
||||
@@ -48,8 +49,8 @@ update_pve9() {
|
||||
download_common_functions
|
||||
|
||||
|
||||
msg_info2 "$(translate "Detected: Proxmox VE 9.x (Current: $OS_CODENAME, Target: $TARGET_CODENAME)")"
|
||||
echo
|
||||
msg_info2 "$(translate "Detected: Proxmox VE $pve_version (Current: $OS_CODENAME, Target: $TARGET_CODENAME)")"
|
||||
echo -e
|
||||
|
||||
local available_space=$(df /var/cache/apt/archives | awk 'NR==2 {print int($4/1024)}')
|
||||
if [ "$available_space" -lt 1024 ]; then
|
||||
@@ -137,13 +138,13 @@ EOF
|
||||
Types: deb
|
||||
URIs: http://deb.debian.org/debian/
|
||||
Suites: ${TARGET_CODENAME} ${TARGET_CODENAME}-updates
|
||||
Components: main contrib non-free-firmware
|
||||
Components: main contrib non-free non-free-firmware
|
||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
|
||||
Types: deb
|
||||
URIs: http://security.debian.org/debian-security/
|
||||
Suites: ${TARGET_CODENAME}-security
|
||||
Components: main contrib non-free-firmware
|
||||
Components: main contrib non-free non-free-firmware
|
||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
EOF
|
||||
|
||||
@@ -158,8 +159,6 @@ EOF
|
||||
msg_ok "$(translate "Non-free firmware warnings disabled")"
|
||||
fi
|
||||
|
||||
cleanup_duplicate_repos
|
||||
|
||||
update_output=$(apt-get update 2>&1)
|
||||
update_exit_code=$?
|
||||
|
||||
@@ -315,7 +314,7 @@ EOF
|
||||
lvm_repair_check
|
||||
cleanup_duplicate_repos
|
||||
|
||||
msg_info "$(translate "Performing system cleanup...")"
|
||||
#msg_info "$(translate "Performing system cleanup...")"
|
||||
apt-get -y autoremove > /dev/null 2>&1 || true
|
||||
apt-get -y autoclean > /dev/null 2>&1 || true
|
||||
msg_ok "$(translate "Cleanup finished")"
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# Proxmox VE Update Script - Improved Version
|
||||
# ==========================================================
|
||||
|
||||
# Configuration
|
||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
VENV_PATH="/opt/googletrans-env"
|
||||
TOOLS_JSON="/usr/local/share/proxmenux/installed_tools.json"
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
ensure_tools_json() {
|
||||
[ -f "$TOOLS_JSON" ] || echo "{}" > "$TOOLS_JSON"
|
||||
}
|
||||
|
||||
register_tool() {
|
||||
local tool="$1"
|
||||
local state="$2"
|
||||
ensure_tools_json
|
||||
jq --arg t "$tool" --argjson v "$state" '.[$t]=$v' "$TOOLS_JSON" > "$TOOLS_JSON.tmp" && mv "$TOOLS_JSON.tmp" "$TOOLS_JSON"
|
||||
}
|
||||
|
||||
download_common_functions() {
|
||||
if ! source <(curl -s "$REPO_URL/scripts/global/common-functions.sh"); then
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
update_pve9() {
|
||||
local pve_version=$(pveversion | awk -F'/' '{print $2}' | cut -d'-' -f1)
|
||||
local start_time=$(date +%s)
|
||||
local log_file="/var/log/proxmox-update-$(date +%Y%m%d-%H%M%S).log"
|
||||
local changes_made=false
|
||||
local OS_CODENAME="$(grep "VERSION_CODENAME=" /etc/os-release | cut -d"=" -f 2 | xargs)"
|
||||
local TARGET_CODENAME="trixie"
|
||||
|
||||
local screen_capture="/tmp/proxmenux_screen_capture_$$.txt"
|
||||
|
||||
if [ -z "$OS_CODENAME" ]; then
|
||||
OS_CODENAME=$(lsb_release -cs 2>/dev/null || echo "trixie")
|
||||
fi
|
||||
|
||||
download_common_functions
|
||||
|
||||
{
|
||||
msg_info2 "$(translate "Detected: Proxmox VE $pve_version (Current: $OS_CODENAME, Target: $TARGET_CODENAME)")"
|
||||
} | tee -a "$screen_capture"
|
||||
|
||||
|
||||
local available_space=$(df /var/cache/apt/archives | awk 'NR==2 {print int($4/1024)}')
|
||||
if [ "$available_space" -lt 1024 ]; then
|
||||
msg_error "$(translate "Insufficient disk space. Available: ${available_space}MB")"
|
||||
echo -e
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! ping -c 1 download.proxmox.com >/dev/null 2>&1; then
|
||||
msg_error "$(translate "Cannot reach Proxmox repositories")"
|
||||
echo -e
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
return 1
|
||||
fi
|
||||
|
||||
disable_sources_repo() {
|
||||
local file="$1"
|
||||
if [[ -f "$file" ]]; then
|
||||
sed -i ':a;/^\n*$/{$d;N;ba}' "$file"
|
||||
|
||||
if grep -q "^Enabled:" "$file"; then
|
||||
sed -i 's/^Enabled:.*$/Enabled: false/' "$file"
|
||||
else
|
||||
echo "Enabled: false" >> "$file"
|
||||
fi
|
||||
|
||||
if ! grep -q "^Types: " "$file"; then
|
||||
msg_warn "$(translate "Malformed .sources file detected, removing: $(basename "$file")")"
|
||||
rm -f "$file"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if disable_sources_repo "/etc/apt/sources.list.d/pve-enterprise.sources"; then
|
||||
msg_ok "$(translate "Enterprise Proxmox repository disabled")" | tee -a "$screen_capture"
|
||||
changes_made=true
|
||||
fi
|
||||
|
||||
if disable_sources_repo "/etc/apt/sources.list.d/ceph.sources"; then
|
||||
msg_ok "$(translate "Enterprise Proxmox Ceph repository disabled")" | tee -a "$screen_capture"
|
||||
changes_made=true
|
||||
fi
|
||||
|
||||
for legacy_file in /etc/apt/sources.list.d/pve-public-repo.list \
|
||||
/etc/apt/sources.list.d/pve-install-repo.list \
|
||||
/etc/apt/sources.list.d/debian.list; do
|
||||
if [[ -f "$legacy_file" ]]; then
|
||||
rm -f "$legacy_file"
|
||||
msg_ok "$(translate "Removed legacy repository: $(basename "$legacy_file")")" | tee -a "$screen_capture"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -f /etc/apt/sources.list.d/debian.sources ]]; then
|
||||
rm -f /etc/apt/sources.list.d/debian.sources
|
||||
msg_ok "$(translate "Old debian.sources file removed to prevent duplication")" | tee -a "$screen_capture"
|
||||
fi
|
||||
|
||||
msg_info "$(translate "Creating Proxmox VE 9.x no-subscription repository...")"
|
||||
cat > /etc/apt/sources.list.d/proxmox.sources << EOF
|
||||
Enabled: true
|
||||
Types: deb
|
||||
URIs: http://download.proxmox.com/debian/pve
|
||||
Suites: ${TARGET_CODENAME}
|
||||
Components: pve-no-subscription
|
||||
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||
EOF
|
||||
msg_ok "$(translate "Proxmox VE 9.x no-subscription repository created")" | tee -a "$screen_capture"
|
||||
changes_made=true
|
||||
|
||||
msg_info "$(translate "Creating Debian ${TARGET_CODENAME} sources file...")"
|
||||
cat > /etc/apt/sources.list.d/debian.sources << EOF
|
||||
Types: deb
|
||||
URIs: http://deb.debian.org/debian/
|
||||
Suites: ${TARGET_CODENAME} ${TARGET_CODENAME}-updates
|
||||
Components: main contrib non-free non-free-firmware
|
||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
|
||||
Types: deb
|
||||
URIs: http://security.debian.org/debian-security/
|
||||
Suites: ${TARGET_CODENAME}-security
|
||||
Components: main contrib non-free non-free-firmware
|
||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
EOF
|
||||
|
||||
msg_ok "$(translate "Debian repositories configured for $TARGET_CODENAME")"
|
||||
|
||||
local firmware_conf="/etc/apt/apt.conf.d/no-firmware-warnings.conf"
|
||||
if [ ! -f "$firmware_conf" ]; then
|
||||
msg_info "$(translate "Disabling non-free firmware warnings...")"
|
||||
echo 'APT::Get::Update::SourceListWarnings::NonFreeFirmware "false";' > "$firmware_conf"
|
||||
msg_ok "$(translate "Non-free firmware warnings disabled")"
|
||||
fi
|
||||
|
||||
#update_output=$(apt-get update 2>&1)
|
||||
update_output=$(apt-get -o Dpkg::Progress-Fancy=1 update 2>&1)
|
||||
update_exit_code=$?
|
||||
|
||||
if [ $update_exit_code -eq 0 ]; then
|
||||
msg_ok "$(translate "Package lists updated successfully")" | tee -a "$screen_capture"
|
||||
else
|
||||
if echo "$update_output" | grep -q "NO_PUBKEY\|GPG error"; then
|
||||
msg_info "$(translate "Fixing GPG key issues...")"
|
||||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $(echo "$update_output" | grep "NO_PUBKEY" | sed 's/.*NO_PUBKEY //' | head -1) 2>/dev/null
|
||||
if apt-get update > "$log_file" 2>&1; then
|
||||
msg_ok "$(translate "Package lists updated after GPG fix")" | tee -a "$screen_capture"
|
||||
else
|
||||
msg_error "$(translate "Failed to update package lists. Check log: $log_file")"
|
||||
return 1
|
||||
fi
|
||||
elif echo "$update_output" | grep -q "404\|Failed to fetch"; then
|
||||
msg_warn "$(translate "Some repositories are not available, continuing with available ones...")"
|
||||
else
|
||||
msg_error "$(translate "Failed to update package lists. Check log: $log_file")"
|
||||
echo "Error details: $update_output"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if apt policy 2>/dev/null | grep -q "${TARGET_CODENAME}.*pve-no-subscription"; then
|
||||
msg_ok "$(translate "Proxmox VE 9.x repositories verified")" | tee -a "$screen_capture"
|
||||
else
|
||||
msg_warn "$(translate "Proxmox VE 9.x repositories verification inconclusive, continuing...")"
|
||||
fi
|
||||
|
||||
local current_pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
local available_pve_version=$(apt-cache policy pve-manager 2>/dev/null | grep -oP 'Candidate: \K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
local upgradable=$(apt list --upgradable 2>/dev/null | grep -c "upgradable")
|
||||
local security_updates=$(apt list --upgradable 2>/dev/null | grep -c "security")
|
||||
|
||||
show_update_menu() {
|
||||
local current_version="$1"
|
||||
local target_version="$2"
|
||||
local upgradable_count="$3"
|
||||
local security_count="$4"
|
||||
|
||||
local menu_text="$(translate "System Update Information")\n\n"
|
||||
menu_text+="$(translate "Current PVE Version"): $current_version\n"
|
||||
if [ -n "$target_version" ] && [ "$target_version" != "$current_version" ]; then
|
||||
menu_text+="$(translate "Available PVE Version"): $target_version\n"
|
||||
fi
|
||||
menu_text+="\n$(translate "Package Updates Available"): $upgradable_count\n"
|
||||
menu_text+="$(translate "Security Updates"): $security_count\n\n"
|
||||
|
||||
if [ "$upgradable_count" -eq 0 ]; then
|
||||
menu_text+="$(translate "System is already up to date")"
|
||||
whiptail --title "$(translate "Update Status")" --msgbox "$menu_text" 15 70
|
||||
return 2
|
||||
else
|
||||
menu_text+="$(translate "Do you want to proceed with the system update?")"
|
||||
if whiptail --title "$(translate "Proxmox Update")" --yesno "$menu_text" 18 70; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
show_update_menu "$current_pve_version" "$available_pve_version" "$upgradable" "$security_updates"
|
||||
MENU_RESULT=$?
|
||||
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "$SCRIPT_TITLE")"
|
||||
cat "$screen_capture"
|
||||
|
||||
|
||||
if [[ $MENU_RESULT -eq 1 ]]; then
|
||||
msg_info2 "$(translate "Update cancelled by user")"
|
||||
apt-get -y autoremove > /dev/null 2>&1 || true
|
||||
apt-get -y autoclean > /dev/null 2>&1 || true
|
||||
rm -f "$screen_capture"
|
||||
return 0
|
||||
elif [[ $MENU_RESULT -eq 2 ]]; then
|
||||
msg_ok "$(translate "System is already up to date. No update needed.")"
|
||||
apt-get -y autoremove > /dev/null 2>&1 || true
|
||||
apt-get -y autoclean > /dev/null 2>&1 || true
|
||||
rm -f "$screen_capture"
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg_info "$(translate "Cleaning up unused time synchronization services...")"
|
||||
if /usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' purge ntp openntpd systemd-timesyncd > /dev/null 2>&1; then
|
||||
msg_ok "$(translate "Old time services removed successfully")"
|
||||
else
|
||||
msg_warn "$(translate "Some old time services could not be removed (not installed)")"
|
||||
fi
|
||||
|
||||
echo -e
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y \
|
||||
-o Dpkg::Options::='--force-confdef' \
|
||||
-o Dpkg::Options::='--force-confold' \
|
||||
dist-upgrade 2>&1 | tee -a "$log_file"
|
||||
|
||||
upgrade_exit_code=${PIPESTATUS[0]}
|
||||
echo -e
|
||||
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "$SCRIPT_TITLE")"
|
||||
cat "$screen_capture"
|
||||
|
||||
|
||||
if [ $upgrade_exit_code -ne 0 ]; then
|
||||
msg_error "$(translate "System upgrade failed. Check log: $log_file")"
|
||||
rm -f "$screen_capture"
|
||||
return 1
|
||||
fi
|
||||
|
||||
msg_info "$(translate "Installing essential Proxmox packages...")"
|
||||
local additional_packages="zfsutils-linux proxmox-backup-restore-image chrony"
|
||||
|
||||
if /usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' install $additional_packages >> "$log_file" 2>&1; then
|
||||
msg_ok "$(translate "Essential Proxmox packages installed")"
|
||||
else
|
||||
msg_warn "$(translate "Some essential Proxmox packages may not have been installed")"
|
||||
fi
|
||||
|
||||
lvm_repair_check
|
||||
cleanup_duplicate_repos
|
||||
|
||||
apt-get -y autoremove > /dev/null 2>&1 || true
|
||||
apt-get -y autoclean > /dev/null 2>&1 || true
|
||||
msg_ok "$(translate "Cleanup finished")"
|
||||
|
||||
local end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
local minutes=$((duration / 60))
|
||||
local seconds=$((duration % 60))
|
||||
|
||||
echo -e "${TAB}${BGN}$(translate "====== PVE UPDATE COMPLETED ======")${CL}"
|
||||
echo -e "${TAB}${GN}⏱️ $(translate "Duration")${CL}: ${BL}${minutes}m ${seconds}s${CL}"
|
||||
echo -e "${TAB}${GN}📄 $(translate "Log file")${CL}: ${BL}$log_file${CL}"
|
||||
echo -e "${TAB}${GN}📦 $(translate "Packages upgraded")${CL}: ${BL}$upgradable${CL}"
|
||||
echo -e "${TAB}${GN}🖥️ $(translate "Proxmox VE")${CL}: ${BL}$available_pve_version (Debian $OS_CODENAME)${CL}"
|
||||
|
||||
msg_ok "$(translate "Proxmox VE 9.x configuration completed.")"
|
||||
|
||||
rm -f "$screen_capture"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
update_pve9
|
||||
fi
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/bin/bash
|
||||
# ProxMenux - Coral TPU Installer (PVE 9.x)
|
||||
# =========================================
|
||||
# Author : MacRimi
|
||||
# License : MIT
|
||||
# Version : 1.3 (PVE9, silent build)
|
||||
# Last Updated: 25/09/2025
|
||||
# =========================================
|
||||
|
||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
LOG_FILE="/tmp/coral_install.log"
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
|
||||
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
|
||||
|
||||
|
||||
ensure_apex_group_and_udev() {
|
||||
msg_info "Ensuring apex group and udev rules..."
|
||||
|
||||
|
||||
if ! getent group apex >/dev/null; then
|
||||
groupadd --system apex || true
|
||||
msg_ok "System group 'apex' created"
|
||||
else
|
||||
msg_ok "System group 'apex' already exists"
|
||||
fi
|
||||
|
||||
|
||||
cat >/etc/udev/rules.d/99-coral-apex.rules <<'EOF'
|
||||
# Coral / Google APEX TPU (M.2 / PCIe)
|
||||
# Assign group "apex" and safe permissions to device nodes
|
||||
KERNEL=="apex_*", GROUP="apex", MODE="0660"
|
||||
SUBSYSTEM=="apex", GROUP="apex", MODE="0660"
|
||||
EOF
|
||||
|
||||
|
||||
if [[ -f /usr/lib/udev/rules.d/60-gasket-dkms.rules ]]; then
|
||||
sed -i 's/GROUP="[^"]*"/GROUP="apex"/g' /usr/lib/udev/rules.d/60-gasket-dkms.rules || true
|
||||
fi
|
||||
|
||||
|
||||
udevadm control --reload-rules
|
||||
udevadm trigger --subsystem-match=apex || true
|
||||
|
||||
msg_ok "apex group and udev rules are in place"
|
||||
|
||||
|
||||
if ls -l /dev/apex_* 2>/dev/null | grep -q ' apex '; then
|
||||
msg_ok "Coral TPU device nodes detected with correct group (apex)"
|
||||
else
|
||||
msg_warn "apex device node not found yet; a reboot may be required"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
pre_install_prompt() {
|
||||
if ! dialog --title "$(translate 'Coral TPU Installation')" --yesno \
|
||||
"\n$(translate 'Installing Coral TPU drivers requires rebooting the server after installation. Do you want to proceed?')" 10 70; then
|
||||
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
install_coral_host() {
|
||||
show_proxmenux_logo
|
||||
: >"$LOG_FILE"
|
||||
|
||||
|
||||
|
||||
msg_info "$(translate 'Installing build dependencies...')"
|
||||
apt-get update -qq >>"$LOG_FILE" 2>&1
|
||||
apt-get install -y git devscripts dh-dkms dkms proxmox-headers-$(uname -r) >>"$LOG_FILE" 2>&1
|
||||
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Error installing build dependencies. Check /tmp/coral_install.log')"; exit 1; fi
|
||||
msg_ok "$(translate 'Build dependencies installed.')"
|
||||
|
||||
|
||||
|
||||
cd /tmp || exit 1
|
||||
rm -rf gasket-driver >>"$LOG_FILE" 2>&1
|
||||
msg_info "$(translate 'Cloning Google Coral driver repository...')"
|
||||
git clone https://github.com/google/gasket-driver.git >>"$LOG_FILE" 2>&1
|
||||
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Could not clone the repository. Check /tmp/coral_install.log')"; exit 1; fi
|
||||
msg_ok "$(translate 'Repository cloned successfully.')"
|
||||
|
||||
|
||||
|
||||
cd /tmp/gasket-driver || exit 1
|
||||
msg_info "$(translate 'Patching source for kernel compatibility...')"
|
||||
|
||||
|
||||
sed -i 's/\.llseek = no_llseek/\.llseek = noop_llseek/' src/gasket_core.c
|
||||
|
||||
sed -i 's/^MODULE_IMPORT_NS(DMA_BUF);/MODULE_IMPORT_NS("DMA_BUF");/' src/gasket_page_table.c
|
||||
|
||||
sed -i "s/\(linux-headers-686-pae | linux-headers-amd64 | linux-headers-generic | linux-headers\)/\1 | proxmox-headers-$(uname -r) | pve-headers-$(uname -r)/" debian/control
|
||||
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Patching failed. Check /tmp/coral_install.log')"; exit 1; fi
|
||||
msg_ok "$(translate 'Source patched successfully.')"
|
||||
|
||||
|
||||
|
||||
msg_info "$(translate 'Building DKMS package...')"
|
||||
debuild -us -uc -tc -b >>"$LOG_FILE" 2>&1
|
||||
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Failed to build DKMS package. Check /tmp/coral_install.log')"; exit 1; fi
|
||||
msg_ok "$(translate 'DKMS package built successfully.')"
|
||||
|
||||
|
||||
|
||||
msg_info "$(translate 'Installing DKMS package...')"
|
||||
dpkg -i ../gasket-dkms_*.deb >>"$LOG_FILE" 2>&1 || true
|
||||
if ! dpkg -s gasket-dkms >/dev/null 2>&1; then
|
||||
msg_error "$(translate 'Failed to install DKMS package. Check /tmp/coral_install.log')"; exit 1
|
||||
fi
|
||||
msg_ok "$(translate 'DKMS package installed.')"
|
||||
|
||||
|
||||
|
||||
msg_info "$(translate 'Compiling Coral TPU drivers for current kernel...')"
|
||||
dkms remove -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1 || true
|
||||
dkms add -m gasket -v 1.0 >>"$LOG_FILE" 2>&1 || true
|
||||
dkms build -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
sed -n '1,200p' /var/lib/dkms/gasket/1.0/build/make.log >>"$LOG_FILE" 2>&1 || true
|
||||
msg_error "$(translate 'DKMS build failed. Check /tmp/coral_install.log')"; exit 1
|
||||
fi
|
||||
dkms install -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1
|
||||
if [[ $? -ne 0 ]]; then msg_error "$(translate 'DKMS install failed. Check /tmp/coral_install.log')"; exit 1; fi
|
||||
msg_ok "$(translate 'Drivers compiled and installed via DKMS.')"
|
||||
|
||||
|
||||
ensure_apex_group_and_udev
|
||||
|
||||
msg_info "$(translate 'Loading modules...')"
|
||||
modprobe gasket >>"$LOG_FILE" 2>&1 || true
|
||||
modprobe apex >>"$LOG_FILE" 2>&1 || true
|
||||
if lsmod | grep -q '\bapex\b'; then
|
||||
msg_ok "$(translate 'Modules loaded.')"
|
||||
msg_success "$(translate 'Coral TPU drivers installed and loaded successfully.')"
|
||||
else
|
||||
msg_warn "$(translate 'Installation finished but drivers are not loaded. Please check dmesg and /tmp/coral_install.log')"
|
||||
fi
|
||||
|
||||
|
||||
|
||||
echo "---- dmesg | grep -i apex (last lines) ----" >>"$LOG_FILE"
|
||||
dmesg | grep -i apex | tail -n 20 >>"$LOG_FILE" 2>&1
|
||||
}
|
||||
|
||||
restart_prompt() {
|
||||
if whiptail --title "$(translate 'Coral TPU Installation')" --yesno \
|
||||
"$(translate 'The installation requires a server restart to apply changes. Do you want to restart now?')" 10 70; then
|
||||
msg_warn "$(translate 'Restarting the server...')"
|
||||
reboot
|
||||
else
|
||||
msg_success "$(translate 'Completed. Press Enter to return to menu...')"
|
||||
read -r
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
pre_install_prompt
|
||||
install_coral_host
|
||||
restart_prompt
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Revision : @Blaspt (USB passthrough via udev rule with persistent /dev/coral)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script para instalar JDownloader en un contenedor LXC desde el host Proxmox
|
||||
# Autor: MacRimi
|
||||
|
||||
# Mostrar lista de CTs
|
||||
CT_LIST=$(pct list | awk 'NR>1 {print $1, $3}')
|
||||
if [ -z "$CT_LIST" ]; then
|
||||
whiptail --title "Error" --msgbox "No hay contenedores LXC disponibles en el sistema." 8 50
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Seleccionar CT
|
||||
CTID=$(whiptail --title "Instalación de JDownloader" --menu "Selecciona el contenedor donde instalar JDownloader:" 20 60 10 $CT_LIST 3>&1 1>&2 2>&3)
|
||||
if [ -z "$CTID" ]; then
|
||||
whiptail --title "Cancelado" --msgbox "No se ha seleccionado ningún contenedor." 8 40
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Solicitar email
|
||||
EMAIL=$(whiptail --title "Cuenta My JDownloader" --inputbox "Introduce tu correo electrónico para vincular JDownloader:" 10 60 3>&1 1>&2 2>&3)
|
||||
if [ -z "$EMAIL" ]; then
|
||||
whiptail --title "Error" --msgbox "No se ha introducido ningún correo." 8 40
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Solicitar contraseña con confirmación
|
||||
while true; do
|
||||
PASSWORD=$(whiptail --title "Cuenta My JDownloader" --passwordbox "Introduce tu contraseña de My JDownloader:" 10 60 3>&1 1>&2 2>&3)
|
||||
[ -z "$PASSWORD" ] && whiptail --title "Error" --msgbox "No se ha introducido ninguna contraseña." 8 40 && exit 1
|
||||
|
||||
CONFIRM=$(whiptail --title "Confirmación de contraseña" --passwordbox "Repite tu contraseña para confirmar:" 10 60 3>&1 1>&2 2>&3)
|
||||
[ "$PASSWORD" = "$CONFIRM" ] && break
|
||||
whiptail --title "Error" --msgbox "Las contraseñas no coinciden. Intenta de nuevo." 8 50
|
||||
done
|
||||
|
||||
# Confirmación final
|
||||
whiptail --title "Confirmar datos" --yesno "¿Deseas continuar con los siguientes datos?\n\nCorreo: $EMAIL\nContraseña: (oculta)\n\nEsta información se usará para vincular el contenedor con tu cuenta de My.JDownloader." 14 60
|
||||
[ $? -ne 0 ] && whiptail --title "Cancelado" --msgbox "Instalación cancelada por el usuario." 8 40 && exit 1
|
||||
|
||||
clear
|
||||
echo "🔍 Detectando sistema operativo dentro del CT $CTID..."
|
||||
OS_ID=$(pct exec "$CTID" -- awk -F= '/^ID=/{gsub("\"",""); print $2}' /etc/os-release)
|
||||
|
||||
echo "Sistema detectado: $OS_ID"
|
||||
echo "🧰 Preparando entorno..."
|
||||
|
||||
case "$OS_ID" in
|
||||
debian)
|
||||
# Repositorio adicional para Java 8
|
||||
pct exec "$CTID" -- wget -q http://www.mirbsd.org/~tg/Debs/sources.txt/wtf-bookworm.sources
|
||||
pct exec "$CTID" -- mv wtf-bookworm.sources /etc/apt/sources.list.d/
|
||||
pct exec "$CTID" -- apt update -y
|
||||
pct exec "$CTID" -- apt install -y openjdk-8-jdk wget
|
||||
JAVA_PATH="/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java"
|
||||
;;
|
||||
ubuntu)
|
||||
pct exec "$CTID" -- apt update -y
|
||||
pct exec "$CTID" -- apt install -y openjdk-8-jdk wget
|
||||
JAVA_PATH="/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java"
|
||||
;;
|
||||
alpine)
|
||||
pct exec "$CTID" -- apk update
|
||||
pct exec "$CTID" -- apk add openjdk8 wget
|
||||
JAVA_PATH="/usr/lib/jvm/java-1.8-openjdk/bin/java"
|
||||
;;
|
||||
*)
|
||||
echo "❌ Sistema operativo no soportado: $OS_ID"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Crear carpeta de instalación
|
||||
pct exec "$CTID" -- mkdir -p /opt/jdownloader
|
||||
pct exec "$CTID" -- bash -lc '
|
||||
set -e
|
||||
mkdir -p /opt/jdownloader
|
||||
cd /opt/jdownloader
|
||||
if [ ! -f JDownloader.jar ]; then
|
||||
if ls JDownloader.jar.backup.* >/dev/null 2>&1; then
|
||||
cp -a "$(ls -t JDownloader.jar.backup.* | head -1)" JDownloader.jar
|
||||
else
|
||||
curl -fSLo JDownloader.jar https://installer.jdownloader.org/JDownloader.jar
|
||||
fi
|
||||
fi
|
||||
chown root:root JDownloader.jar
|
||||
chmod 0644 JDownloader.jar
|
||||
'
|
||||
|
||||
|
||||
|
||||
# Crear archivo de configuración JSON para My JDownloader
|
||||
pct exec "$CTID" -- bash -c "mkdir -p /opt/jdownloader/cfg && cat > /opt/jdownloader/cfg/org.jdownloader.api.myjdownloader.MyJDownloaderSettings.json" <<EOF
|
||||
{
|
||||
"email" : "$EMAIL",
|
||||
"password" : "$PASSWORD",
|
||||
"enabled" : true
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
# Crear servicio según sistema
|
||||
if [[ "$OS_ID" == "alpine" ]]; then
|
||||
# Servicio OpenRC para Alpine
|
||||
pct exec "$CTID" -- bash -c 'cat > /etc/init.d/jdownloader <<EOF
|
||||
#!/sbin/openrc-run
|
||||
|
||||
command="/usr/bin/java"
|
||||
command_args="-jar /opt/jdownloader/JDownloader.jar -norestart"
|
||||
pidfile="/var/run/jdownloader.pid"
|
||||
name="JDownloader"
|
||||
|
||||
depend() {
|
||||
need net
|
||||
}
|
||||
EOF'
|
||||
|
||||
pct exec "$CTID" -- chmod +x /etc/init.d/jdownloader
|
||||
pct exec "$CTID" -- rc-update add jdownloader default
|
||||
pct exec "$CTID" -- rc-service jdownloader start
|
||||
|
||||
else
|
||||
# Servicio systemd para Debian/Ubuntu
|
||||
pct exec "$CTID" -- bash -lc 'cat > /etc/systemd/system/jdownloader.service <<'"'"'EOF'"'"'
|
||||
[Unit]
|
||||
Description=JDownloader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/jdownloader
|
||||
ExecStartPre=/usr/bin/test -s /opt/jdownloader/JDownloader.jar
|
||||
ExecStart=/usr/bin/java -jar /opt/jdownloader/JDownloader.jar -norestart
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable jdownloader
|
||||
systemctl restart jdownloader
|
||||
systemctl status jdownloader --no-pager || true
|
||||
'
|
||||
|
||||
pct exec "$CTID" -- systemctl daemon-reexec
|
||||
pct exec "$CTID" -- systemctl daemon-reload
|
||||
pct exec "$CTID" -- systemctl enable jdownloader
|
||||
pct exec "$CTID" -- systemctl start jdownloader
|
||||
fi
|
||||
|
||||
pct exec "$CTID" -- reboot
|
||||
|
||||
echo -e "\n\033[1;32m✅ JDownloader se ha instalado correctamente en el CT $CTID y está funcionando como servicio.\033[0m"
|
||||
echo -e "\n➡️ Accede a \033[1;34mhttps://my.jdownloader.org\033[0m con tu cuenta para gestionarlo.\n"
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
@@ -19,6 +19,7 @@ LOCAL_VERSION_FILE="$BASE_DIR/version.txt"
|
||||
INSTALL_DIR="/usr/local/bin"
|
||||
MENU_SCRIPT="menu"
|
||||
VENV_PATH="/opt/googletrans-env"
|
||||
MONITOR_SERVICE="proxmenux-monitor.service"
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
@@ -33,12 +34,12 @@ detect_installation_type() {
|
||||
local has_venv=false
|
||||
local has_language=false
|
||||
|
||||
|
||||
# Check if virtual environment exists
|
||||
if [ -d "$VENV_PATH" ] && [ -f "$VENV_PATH/bin/activate" ]; then
|
||||
has_venv=true
|
||||
fi
|
||||
|
||||
|
||||
# Check if language is configured
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
local current_language=$(jq -r '.language // empty' "$CONFIG_FILE" 2>/dev/null)
|
||||
if [[ -n "$current_language" && "$current_language" != "null" && "$current_language" != "empty" ]]; then
|
||||
@@ -53,6 +54,89 @@ detect_installation_type() {
|
||||
fi
|
||||
}
|
||||
|
||||
check_monitor_status() {
|
||||
if systemctl list-unit-files | grep -q "$MONITOR_SERVICE"; then
|
||||
if systemctl is-active --quiet "$MONITOR_SERVICE"; then
|
||||
echo "active"
|
||||
else
|
||||
echo "inactive"
|
||||
fi
|
||||
else
|
||||
echo "not_installed"
|
||||
fi
|
||||
}
|
||||
|
||||
toggle_monitor_service() {
|
||||
local status=$(check_monitor_status)
|
||||
|
||||
if [ "$status" = "not_installed" ]; then
|
||||
dialog --clear --backtitle "ProxMenux Configuration" \
|
||||
--title "$(translate "ProxMenux Monitor")" \
|
||||
--msgbox "\n\n$(translate "ProxMenux Monitor is not installed.")" 10 50
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$status" = "active" ]; then
|
||||
if dialog --clear --backtitle "ProxMenux Configuration" \
|
||||
--title "$(translate "Deactivate Monitor")" \
|
||||
--yesno "\n$(translate "Do you want to deactivate ProxMenux Monitor?")" 8 60; then
|
||||
systemctl stop "$MONITOR_SERVICE" 2>/dev/null
|
||||
systemctl disable "$MONITOR_SERVICE" 2>/dev/null
|
||||
dialog --clear --backtitle "ProxMenux Configuration" \
|
||||
--title "$(translate "Monitor Deactivated")" \
|
||||
--msgbox "\n\n$(translate "ProxMenux Monitor has been deactivated.")" 10 50
|
||||
fi
|
||||
else
|
||||
if dialog --clear --backtitle "ProxMenux Configuration" \
|
||||
--title "$(translate "Activate Monitor")" \
|
||||
--yesno "\n$(translate "Do you want to activate ProxMenux Monitor?")" 8 60; then
|
||||
systemctl enable "$MONITOR_SERVICE" 2>/dev/null
|
||||
systemctl start "$MONITOR_SERVICE" 2>/dev/null
|
||||
dialog --clear --backtitle "ProxMenux Configuration" \
|
||||
--title "$(translate "Monitor Activated")" \
|
||||
--msgbox "\n\n$(translate "ProxMenux Monitor has been activated.")" 10 50
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
show_monitor_status() {
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "ProxMenux Monitor Service Verification")"
|
||||
echo ""
|
||||
|
||||
local status=$(check_monitor_status)
|
||||
|
||||
if [ "$status" = "not_installed" ]; then
|
||||
msg_warn "$(translate "ProxMenux Monitor is not installed")"
|
||||
echo ""
|
||||
msg_info2 "$(translate "To install the monitor, reinstall ProxMenux with the latest version")"
|
||||
else
|
||||
msg_info2 "$(translate "Service Status"): $MONITOR_SERVICE"
|
||||
echo ""
|
||||
|
||||
if [ "$status" = "active" ]; then
|
||||
msg_ok "$(translate "Service is active and running")"
|
||||
|
||||
local server_ip=$(hostname -I | awk '{print $1}')
|
||||
if [ -n "$server_ip" ]; then
|
||||
echo -e "${TAB}${GN}🌐 $(translate "Monitor URL")${CL}: ${BL}http://${server_ip}:8008${CL}"
|
||||
fi
|
||||
else
|
||||
msg_warn "$(translate "Service is inactive")"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
msg_info2 "$(translate "Detailed service information"):"
|
||||
echo ""
|
||||
systemctl status "$MONITOR_SERVICE" --no-pager -l
|
||||
fi
|
||||
|
||||
echo ""
|
||||
msg_success "$(translate "Press Enter to continue...")"
|
||||
read -r
|
||||
}
|
||||
|
||||
# ==========================================================
|
||||
show_config_menu() {
|
||||
local install_type
|
||||
@@ -62,39 +146,68 @@ show_config_menu() {
|
||||
local menu_options=()
|
||||
local option_actions=()
|
||||
|
||||
|
||||
if [ "$install_type" = "translation" ]; then
|
||||
menu_options+=("1" "$(translate "Change Language")")
|
||||
option_actions[1]="change_language"
|
||||
local monitor_status=$(check_monitor_status)
|
||||
local option_num=1
|
||||
|
||||
if [ "$monitor_status" != "not_installed" ]; then
|
||||
if [ "$monitor_status" = "active" ]; then
|
||||
menu_options+=("$option_num" "$(translate "Deactivate ProxMenux Monitor")")
|
||||
option_actions[$option_num]="toggle_monitor"
|
||||
else
|
||||
menu_options+=("$option_num" "$(translate "Activate ProxMenux Monitor")")
|
||||
option_actions[$option_num]="toggle_monitor"
|
||||
fi
|
||||
((option_num++))
|
||||
|
||||
menu_options+=("2" "$(translate "Show Version Information")")
|
||||
option_actions[2]="show_version_info"
|
||||
|
||||
menu_options+=("3" "$(translate "Uninstall ProxMenux")")
|
||||
option_actions[3]="uninstall_proxmenu"
|
||||
|
||||
menu_options+=("4" "$(translate "Return to Main Menu")")
|
||||
option_actions[4]="return_main"
|
||||
else
|
||||
|
||||
menu_options+=("1" "Show Version Information")
|
||||
option_actions[1]="show_version_info"
|
||||
|
||||
menu_options+=("2" "Uninstall ProxMenux")
|
||||
option_actions[2]="uninstall_proxmenu"
|
||||
|
||||
menu_options+=("3" "Return to Main Menu")
|
||||
option_actions[3]="return_main"
|
||||
menu_options+=("$option_num" "$(translate "Show Monitor Service Status")")
|
||||
option_actions[$option_num]="show_monitor_status"
|
||||
((option_num++))
|
||||
fi
|
||||
|
||||
|
||||
# Build menu based on installation type
|
||||
if [ "$install_type" = "translation" ]; then
|
||||
menu_options+=("$option_num" "$(translate "Change Language")")
|
||||
option_actions[$option_num]="change_language"
|
||||
((option_num++))
|
||||
|
||||
menu_options+=("$option_num" "$(translate "Show Version Information")")
|
||||
option_actions[$option_num]="show_version_info"
|
||||
((option_num++))
|
||||
|
||||
menu_options+=("$option_num" "$(translate "Uninstall ProxMenux")")
|
||||
option_actions[$option_num]="uninstall_proxmenu"
|
||||
((option_num++))
|
||||
|
||||
menu_options+=("$option_num" "$(translate "Return to Main Menu")")
|
||||
option_actions[$option_num]="return_main"
|
||||
else
|
||||
# Normal version (English only)
|
||||
menu_options+=("$option_num" "Show Version Information")
|
||||
option_actions[$option_num]="show_version_info"
|
||||
((option_num++))
|
||||
|
||||
menu_options+=("$option_num" "Uninstall ProxMenux")
|
||||
option_actions[$option_num]="uninstall_proxmenu"
|
||||
((option_num++))
|
||||
|
||||
menu_options+=("$option_num" "Return to Main Menu")
|
||||
option_actions[$option_num]="return_main"
|
||||
fi
|
||||
|
||||
# Show menu
|
||||
OPTION=$(dialog --clear --backtitle "ProxMenux Configuration" \
|
||||
--title "$(translate "Configuration Menu")" \
|
||||
--menu "$(translate "Select an option:")" 20 70 10 \
|
||||
"${menu_options[@]}" 3>&1 1>&2 2>&3)
|
||||
|
||||
|
||||
# Execute selected action
|
||||
case "${option_actions[$OPTION]}" in
|
||||
"toggle_monitor")
|
||||
toggle_monitor_service
|
||||
;;
|
||||
"show_monitor_status")
|
||||
show_monitor_status
|
||||
;;
|
||||
"change_language")
|
||||
change_language
|
||||
;;
|
||||
@@ -131,7 +244,7 @@ change_language() {
|
||||
return
|
||||
fi
|
||||
|
||||
|
||||
# Update language in config file
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
tmp=$(mktemp)
|
||||
jq --arg lang "$new_language" '.language = $lang' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE"
|
||||
@@ -143,7 +256,7 @@ change_language() {
|
||||
--title "$(translate "Language Change")" \
|
||||
--msgbox "\n\n$(translate "Language changed to") $new_language" 10 50
|
||||
|
||||
|
||||
# Reload menu with new language
|
||||
TMP_FILE=$(mktemp)
|
||||
curl -s "$REPO_URL/scripts/menus/config_menu.sh" > "$TMP_FILE"
|
||||
chmod +x "$TMP_FILE"
|
||||
@@ -164,7 +277,7 @@ show_version_info() {
|
||||
|
||||
info_message+="$(translate "Current ProxMenux version:") $version\n\n"
|
||||
|
||||
|
||||
# Show installation type
|
||||
info_message+="$(translate "Installation type:")\n"
|
||||
if [ "$install_type" = "translation" ]; then
|
||||
info_message+="✓ $(translate "Translation Version (Multi-language support)")\n"
|
||||
@@ -197,13 +310,13 @@ show_version_info() {
|
||||
info_message+="$(translate "No installation information available.")\n"
|
||||
fi
|
||||
|
||||
info_message+="\n$(translate "ProxMenu files:")\n"
|
||||
info_message+="\n$(translate "ProxMenux files:")\n"
|
||||
[ -f "$INSTALL_DIR/$MENU_SCRIPT" ] && info_message+="✓ $MENU_SCRIPT → $INSTALL_DIR/$MENU_SCRIPT\n" || info_message+="✗ $MENU_SCRIPT\n"
|
||||
[ -f "$UTILS_FILE" ] && info_message+="✓ utils.sh → $UTILS_FILE\n" || info_message+="✗ utils.sh\n"
|
||||
[ -f "$CONFIG_FILE" ] && info_message+="✓ config.json → $CONFIG_FILE\n" || info_message+="✗ config.json\n"
|
||||
[ -f "$LOCAL_VERSION_FILE" ] && info_message+="✓ version.txt → $LOCAL_VERSION_FILE\n" || info_message+="✗ version.txt\n"
|
||||
|
||||
|
||||
# Show translation-specific files
|
||||
if [ "$install_type" = "translation" ]; then
|
||||
[ -f "$CACHE_FILE" ] && info_message+="✓ cache.json → $CACHE_FILE\n" || info_message+="✗ cache.json\n"
|
||||
|
||||
@@ -222,7 +335,7 @@ show_version_info() {
|
||||
info_message+="\n$(translate "Language:")\nEnglish (Fixed)\n"
|
||||
fi
|
||||
|
||||
|
||||
# Display information in a scrollable text box
|
||||
tmpfile=$(mktemp)
|
||||
echo -e "$info_message" > "$tmpfile"
|
||||
dialog --clear --backtitle "ProxMenux Configuration" \
|
||||
@@ -237,14 +350,14 @@ uninstall_proxmenu() {
|
||||
install_type=$(detect_installation_type)
|
||||
|
||||
if ! dialog --clear --backtitle "ProxMenux Configuration" \
|
||||
--title "Uninstall ProxMenu" \
|
||||
--yesno "\n$(translate "Are you sure you want to uninstall ProxMenu?")" 8 60; then
|
||||
--title "Uninstall ProxMenux" \
|
||||
--yesno "\n$(translate "Are you sure you want to uninstall ProxMenux?")" 8 60; then
|
||||
return
|
||||
fi
|
||||
|
||||
local deps_to_remove=""
|
||||
|
||||
|
||||
# Show different dependency options based on installation type
|
||||
if [ "$install_type" = "translation" ]; then
|
||||
deps_to_remove=$(dialog --clear --backtitle "ProxMenux Configuration" \
|
||||
--title "Remove Dependencies" \
|
||||
@@ -263,12 +376,12 @@ uninstall_proxmenu() {
|
||||
3>&1 1>&2 2>&3)
|
||||
fi
|
||||
|
||||
|
||||
# Perform uninstallation with progress bar
|
||||
(
|
||||
echo "10" ; echo "Removing ProxMenu files..."
|
||||
sleep 1
|
||||
|
||||
|
||||
# Remove googletrans and virtual environment if exists
|
||||
if [ -f "$VENV_PATH/bin/activate" ]; then
|
||||
echo "30" ; echo "Removing googletrans and virtual environment..."
|
||||
source "$VENV_PATH/bin/activate"
|
||||
@@ -281,7 +394,7 @@ uninstall_proxmenu() {
|
||||
rm -f "$INSTALL_DIR/$MENU_SCRIPT"
|
||||
rm -rf "$BASE_DIR"
|
||||
|
||||
|
||||
# Remove selected dependencies
|
||||
if [ -n "$deps_to_remove" ]; then
|
||||
echo "70" ; echo "Removing selected dependencies..."
|
||||
read -r -a DEPS_ARRAY <<< "$(echo "$deps_to_remove" | tr -d '"')"
|
||||
@@ -293,7 +406,7 @@ uninstall_proxmenu() {
|
||||
fi
|
||||
|
||||
echo "90" ; echo "Restoring system files..."
|
||||
|
||||
# Restore .bashrc and motd
|
||||
[ -f /root/.bashrc.bak ] && mv /root/.bashrc.bak /root/.bashrc
|
||||
if [ -f /etc/motd.bak ]; then
|
||||
mv /etc/motd.bak /etc/motd
|
||||
@@ -308,7 +421,7 @@ uninstall_proxmenu() {
|
||||
--title "Uninstalling ProxMenux" \
|
||||
--gauge "Starting uninstallation..." 10 60 0
|
||||
|
||||
|
||||
# Show completion message
|
||||
local final_message="ProxMenux has been uninstalled successfully.\n\n"
|
||||
if [ -n "$deps_to_remove" ]; then
|
||||
final_message+="The following dependencies were removed:\n$deps_to_remove\n\n"
|
||||
@@ -324,4 +437,4 @@ uninstall_proxmenu() {
|
||||
|
||||
# ==========================================================
|
||||
# Main execution
|
||||
show_config_menu
|
||||
show_config_menu
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
@@ -47,7 +47,7 @@ initialize_cache
|
||||
fi
|
||||
;;
|
||||
3)
|
||||
bash <(curl -s "$REPO_URL/scripts/install_coral_pve.sh")
|
||||
bash <(curl -s "$REPO_URL/scripts/gpu_tpu/install_coral_pve9.sh")
|
||||
if [ $? -ne 0 ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - LXC Conversion Management Menu
|
||||
# ProxMenux - LXC Conversion Management Menu
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
@@ -99,14 +99,15 @@ show_menu() {
|
||||
--title "$(translate "$menu_title")" \
|
||||
--menu "$(translate "Select an option:")" 20 70 10 \
|
||||
1 "$(translate "Settings post-install Proxmox")" \
|
||||
2 "$(translate "Help and Info Commands")" \
|
||||
3 "$(translate "Hardware: GPUs and Coral-TPU")" \
|
||||
4 "$(translate "Create VM from template or script")" \
|
||||
5 "$(translate "Disk and Storage Manager")" \
|
||||
2 "$(translate "Hardware: GPUs and Coral-TPU")" \
|
||||
3 "$(translate "Create VM from template or script")" \
|
||||
4 "$(translate "Disk and Storage Manager")" \
|
||||
5 "$(translate "Mount and Share Manager")" \
|
||||
6 "$(translate "Proxmox VE Helper Scripts")" \
|
||||
7 "$(translate "Network Management")" \
|
||||
8 "$(translate "Utilities and Tools")" \
|
||||
9 "$(translate "Settings")" \
|
||||
h "$(translate "Help and Info Commands")" \
|
||||
s "$(translate "Settings")" \
|
||||
0 "$(translate "Exit")" 2>"$TEMP_FILE"
|
||||
|
||||
local EXIT_STATUS=$?
|
||||
@@ -122,15 +123,16 @@ show_menu() {
|
||||
|
||||
case $OPTION in
|
||||
1) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_post_install.sh") ;;
|
||||
2) bash <(curl -s "$REPO_URL/scripts/help_info_menu.sh") ;;
|
||||
3) exec bash <(curl -s "$REPO_URL/scripts/menus/hw_grafics_menu.sh") ;;
|
||||
4) exec bash <(curl -s "$REPO_URL/scripts/menus/create_vm_menu.sh") ;;
|
||||
5) exec bash <(curl -s "$REPO_URL/scripts/menus/storage_menu.sh") ;;
|
||||
2) exec bash <(curl -s "$REPO_URL/scripts/menus/hw_grafics_menu.sh") ;;
|
||||
3) exec bash <(curl -s "$REPO_URL/scripts/menus/create_vm_menu.sh") ;;
|
||||
4) exec bash <(curl -s "$REPO_URL/scripts/menus/storage_menu.sh") ;;
|
||||
5) exec bash <(curl -s "$REPO_URL/scripts/menus/share_menu.sh") ;;
|
||||
6) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_Helper_Scripts.sh") ;;
|
||||
7) exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh") ;;
|
||||
8) exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh") ;;
|
||||
9) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;;
|
||||
0) clear; msg_ok "$(translate "Thank you for using ProxMenu. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
|
||||
h) bash <(curl -s "$REPO_URL/scripts/help_info_menu.sh") ;;
|
||||
s) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;;
|
||||
0) clear; msg_ok "$(translate "Thank you for using ProxMenux. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
|
||||
*) msg_warn "$(translate "Invalid option")"; sleep 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
@@ -71,7 +71,7 @@ show_menu() {
|
||||
7) exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh") ;;
|
||||
8) exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh") ;;
|
||||
9) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;;
|
||||
0) clear; msg_ok "$(translate "Thank you for using ProxMenu. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
|
||||
0) clear; msg_ok "$(translate "Thank you for using ProxMenux. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
|
||||
*) msg_warn "$(translate "Invalid option")"; sleep 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu - Network Management and Repair Tool
|
||||
# ProxMenux - Network Management and Repair Tool
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -53,7 +53,7 @@ initialize_cache
|
||||
OS_CODENAME="$(grep "VERSION_CODENAME=" /etc/os-release | cut -d"=" -f 2 | xargs)"
|
||||
RAM_SIZE_GB=$(( $(vmstat -s | grep -i "total memory" | xargs | cut -d" " -f 1) / 1024 / 1000))
|
||||
NECESSARY_REBOOT=0
|
||||
SCRIPT_TITLE="Customizable post-installation optimization script"
|
||||
export SCRIPT_TITLE="ProxMenux Optimization Post-Installation"
|
||||
|
||||
# ==========================================================
|
||||
# Tool registration system
|
||||
@@ -107,7 +107,7 @@ apt_upgrade() {
|
||||
|
||||
if [[ "$pve_version" -ge 9 ]]; then
|
||||
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve.sh")
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve9_2.sh")
|
||||
else
|
||||
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve8.sh")
|
||||
@@ -132,18 +132,22 @@ remove_subscription_banner() {
|
||||
local pve_version
|
||||
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
|
||||
|
||||
|
||||
if [[ -z "$pve_version" ]]; then
|
||||
msg_error "Unable to detect Proxmox version."
|
||||
return 1
|
||||
fi
|
||||
|
||||
kill -TERM "$SPINNER_PID" 2>/dev/null
|
||||
sleep 1
|
||||
|
||||
if [[ "$pve_version" -ge 9 ]]; then
|
||||
if ! whiptail --title "Proxmox VE 9.x Subscription Banner Removal" \
|
||||
--yesno "Do you want to remove the Proxmox subscription banner from the web interface for PVE $pve_version?" 10 70; then
|
||||
if ! whiptail --title "Proxmox VE ${pve_version} Subscription Banner Removal" \
|
||||
--yesno "$(translate "Do you want to remove the Proxmox subscription banner from the web interface for PVE $pve_version?")\n\n$(translate "Attention: Removing the subscription banner may cause issues in the web interface after a future update.")\n\n$(translate "If this happens, you can restore the backup from the 'Subscription Banner Removal' option in 'Uninstall optimizations'.")\n\n$(translate "Are you sure you want to continue?")" 14 75; then
|
||||
msg_warn "Banner removal cancelled by user."
|
||||
return 1
|
||||
fi
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve9.sh")
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve-v3.sh")
|
||||
else
|
||||
if ! whiptail --title "Proxmox VE 8.x Subscription Banner Removal" \
|
||||
--yesno "Do you want to remove the Proxmox subscription banner from the web interface for PVE $pve_version?" 10 70; then
|
||||
@@ -158,51 +162,11 @@ remove_subscription_banner() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ==========================================================
|
||||
|
||||
|
||||
configure_time_sync_() {
|
||||
msg_info2 "$(translate "Configuring system time settings...")"
|
||||
|
||||
|
||||
# Get public IP address
|
||||
this_ip=$(dig +short myip.opendns.com @resolver1.opendns.com)
|
||||
if [ -z "$this_ip" ]; then
|
||||
msg_warn "$(translate "Failed to obtain public IP address")"
|
||||
timezone="UTC"
|
||||
else
|
||||
# Get timezone based on IP
|
||||
timezone=$(curl -s "https://ipapi.co/${this_ip}/timezone")
|
||||
if [ -z "$timezone" ]; then
|
||||
msg_warn "$(translate "Failed to determine timezone from IP address")"
|
||||
timezone="UTC"
|
||||
else
|
||||
msg_ok "$(translate "Found timezone $timezone for IP $this_ip")"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set the timezone
|
||||
if timedatectl set-timezone "$timezone"; then
|
||||
msg_ok "$(translate "Timezone set to $timezone")"
|
||||
else
|
||||
msg_error "$(translate "Failed to set timezone to $timezone")"
|
||||
fi
|
||||
|
||||
# Configure time synchronization
|
||||
msg_info "$(translate "Enabling automatic time synchronization...")"
|
||||
if timedatectl set-ntp true; then
|
||||
systemctl restart postfix 2>/dev/null || true
|
||||
msg_ok "$(translate "Automatic time synchronization enabled")"
|
||||
register_tool "time_sync" true
|
||||
else
|
||||
msg_error "$(translate "Failed to enable automatic time synchronization")"
|
||||
fi
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -243,36 +207,29 @@ configure_time_sync() {
|
||||
|
||||
|
||||
# ==========================================================
|
||||
|
||||
skip_apt_languages() {
|
||||
msg_info "$(translate "Configuring APT to skip downloading additional languages...")"
|
||||
local default_locale=""
|
||||
|
||||
if [ -f /etc/default/locale ]; then
|
||||
default_locale=$(grep '^LANG=' /etc/default/locale | cut -d= -f2 | tr -d '"')
|
||||
elif [ -f /etc/environment ]; then
|
||||
default_locale=$(grep '^LANG=' /etc/environment | cut -d= -f2 | tr -d '"')
|
||||
fi
|
||||
|
||||
default_locale="${default_locale:-en_US.UTF-8}"
|
||||
local normalized_locale=$(echo "$default_locale" | tr 'A-Z' 'a-z' | sed 's/utf-8/utf8/;s/-/_/')
|
||||
|
||||
if ! locale -a | grep -qi "^$normalized_locale$"; then
|
||||
if ! grep -qE "^${default_locale}[[:space:]]+UTF-8" /etc/locale.gen; then
|
||||
echo "$default_locale UTF-8" >> /etc/locale.gen
|
||||
fi
|
||||
locale-gen "$default_locale" > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
echo 'Acquire::Languages "none";' > /etc/apt/apt.conf.d/99-disable-translations
|
||||
|
||||
msg_ok "$(translate "APT configured to skip additional languages")"
|
||||
register_tool "apt_languages" true
|
||||
msg_info "$(translate "Configuring APT to skip downloading additional languages...")"
|
||||
cat > /etc/apt/apt.conf.d/99-disable-translations <<'EOF'
|
||||
Acquire::Languages "none";
|
||||
EOF
|
||||
msg_ok "$(translate "APT configured to skip additional languages")"
|
||||
register_tool "apt_languages" true
|
||||
}
|
||||
|
||||
# ==========================================================
|
||||
optimize_journald() {
|
||||
|
||||
if [ -f /etc/log2ram.conf ] || [ -d /var/log.hdd ]; then
|
||||
return 0
|
||||
fi
|
||||
msg_info "$(translate "Limiting size and optimizing journald...")"
|
||||
NECESSARY_REBOOT=1
|
||||
|
||||
local jf="/etc/systemd/journald.conf"
|
||||
if ! grep -q "ProxMenux optimized journald" "$jf" 2>/dev/null; then
|
||||
cp -a "$jf" "${jf}.bak" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
cat <<EOF > /etc/systemd/journald.conf
|
||||
[Journal]
|
||||
@@ -310,14 +267,16 @@ optimize_logrotate() {
|
||||
if ! grep -q "# ProxMenux optimized configuration" "$logrotate_conf"; then
|
||||
cp "$logrotate_conf" "$backup_conf"
|
||||
cat <<EOF > "$logrotate_conf"
|
||||
# ProxMenux optimized configuration
|
||||
# ProxMenux optimized configuration (Log2RAM-friendly)
|
||||
daily
|
||||
su root adm
|
||||
rotate 7
|
||||
create
|
||||
compress
|
||||
size=10M
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
create 0640 root adm
|
||||
copytruncate
|
||||
include /etc/logrotate.d
|
||||
EOF
|
||||
@@ -469,10 +428,10 @@ force_apt_ipv4() {
|
||||
# ==========================================================
|
||||
|
||||
apply_network_optimizations() {
|
||||
msg_info "$(translate "Optimizing network settings...")"
|
||||
NECESSARY_REBOOT=1
|
||||
msg_info "$(translate "Optimizing network settings...")"
|
||||
NECESSARY_REBOOT=1
|
||||
|
||||
cat <<'EOF' > /etc/sysctl.d/99-network.conf
|
||||
cat <<'EOF' > /etc/sysctl.d/99-network.conf
|
||||
# ==========================================================
|
||||
# ProxMenux - Network tuning (PVE 9 compatible)
|
||||
# ==========================================================
|
||||
@@ -483,20 +442,20 @@ net.core.rmem_max = 16777216
|
||||
net.core.wmem_max = 16777216
|
||||
net.core.somaxconn = 8192
|
||||
|
||||
# IPv4
|
||||
# IPv4 hardening
|
||||
net.ipv4.conf.all.accept_redirects = 0
|
||||
net.ipv4.conf.all.accept_source_route = 0
|
||||
net.ipv4.conf.all.secure_redirects = 0
|
||||
net.ipv4.conf.all.send_redirects = 0
|
||||
net.ipv4.conf.all.log_martians = 1
|
||||
net.ipv4.conf.all.log_martians = 0
|
||||
|
||||
net.ipv4.conf.default.accept_redirects = 0
|
||||
net.ipv4.conf.default.accept_source_route = 0
|
||||
net.ipv4.conf.default.secure_redirects = 0
|
||||
net.ipv4.conf.default.send_redirects = 0
|
||||
net.ipv4.conf.default.log_martians = 1
|
||||
net.ipv4.conf.default.log_martians = 0
|
||||
|
||||
# rp_filter: loose multi-homed/bridges
|
||||
# rp_filter:
|
||||
net.ipv4.conf.all.rp_filter = 2
|
||||
net.ipv4.conf.default.rp_filter = 2
|
||||
|
||||
@@ -516,30 +475,41 @@ net.ipv4.tcp_wmem = 8192 65536 16777216
|
||||
net.unix.max_dgram_qlen = 4096
|
||||
EOF
|
||||
|
||||
|
||||
sysctl --system > /dev/null 2>&1
|
||||
sysctl --system > /dev/null 2>&1
|
||||
|
||||
|
||||
local interfaces_file="/etc/network/interfaces"
|
||||
if ! grep -q 'source /etc/network/interfaces.d/*' "$interfaces_file"; then
|
||||
echo "source /etc/network/interfaces.d/*" >> "$interfaces_file"
|
||||
fi
|
||||
cat >/etc/systemd/system/proxmenux-fwbr-tune.service <<'EOF'
|
||||
[Unit]
|
||||
Description=ProxMenux - Tune rp_filter/log_martians on virtual fw bridges
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
msg_ok "$(translate "Network optimization completed")"
|
||||
register_tool "network_optimization" true
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash -c 'for i in /proc/sys/net/ipv4/conf/*; do n=${i##*/}; case "$n" in fwbr*|fwln*|fwpr*|tap*) echo 0 > /proc/sys/net/ipv4/conf/$n/rp_filter; echo 0 > /proc/sys/net/ipv4/conf/$n/log_martians; esac; done'
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now proxmenux-fwbr-tune.service >/dev/null 2>&1 || true
|
||||
|
||||
|
||||
local interfaces_file="/etc/network/interfaces"
|
||||
if ! grep -q 'source /etc/network/interfaces.d/*' "$interfaces_file"; then
|
||||
echo "source /etc/network/interfaces.d/*" >> "$interfaces_file"
|
||||
fi
|
||||
|
||||
msg_ok "$(translate "Network optimization completed")"
|
||||
register_tool "network_optimization" true
|
||||
}
|
||||
|
||||
|
||||
# ==========================================================
|
||||
disable_rpc() {
|
||||
msg_info "$(translate "Disabling portmapper/rpcbind for security...")"
|
||||
|
||||
systemctl disable rpcbind > /dev/null 2>&1
|
||||
systemctl stop rpcbind > /dev/null 2>&1
|
||||
|
||||
msg_ok "$(translate "portmapper/rpcbind has been disabled and removed")"
|
||||
register_tool "disable_rpc" true
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ==========================================================
|
||||
customize_bashrc_() {
|
||||
@@ -624,18 +594,20 @@ EOF
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
install_log2ram_auto() {
|
||||
|
||||
msg_info "$(translate "Checking if system disk is SSD or M.2...")"
|
||||
msg_info "$(translate "Checking if system disk is SSD or M.2...")"
|
||||
|
||||
local is_ssd=false
|
||||
local pool disks disk byid_path dev rot
|
||||
|
||||
if grep -qE '^root=ZFS=' /etc/kernel/cmdline 2>/dev/null || mount | grep -q 'on / type zfs'; then
|
||||
|
||||
pool=$(zfs list -Ho name,mountpoint 2>/dev/null | awk '$2=="/"{print $1}' | cut -d/ -f1)
|
||||
disks=$(zpool status "$pool" 2>/dev/null | awk '/ONLINE/ && $1 !~ /:|mirror|raidz|log|spare|config|NAME|rpool|state/ {print $1}' | sort -u)
|
||||
|
||||
is_ssd=true
|
||||
for disk in $disks; do
|
||||
byid_path=$(readlink -f /dev/disk/by-id/*$disk* 2>/dev/null) || continue
|
||||
@@ -644,9 +616,7 @@ install_log2ram_auto() {
|
||||
[[ "$rot" != "0" ]] && is_ssd=false && break
|
||||
done
|
||||
else
|
||||
|
||||
ROOT_PART=$(lsblk -no NAME,MOUNTPOINT | grep ' /$' | awk '{print $1}')
|
||||
#SYSTEM_DISK=$(lsblk -no PKNAME /dev/$ROOT_PART 2>/dev/null)
|
||||
SYSTEM_DISK=$(lsblk -no PKNAME /dev/$ROOT_PART 2>/dev/null | grep -E '^[a-z]+' | head -n1)
|
||||
SYSTEM_DISK=${SYSTEM_DISK:-sda}
|
||||
if [[ "$SYSTEM_DISK" == nvme* || "$(cat /sys/block/$SYSTEM_DISK/queue/rotational 2>/dev/null)" == "0" ]]; then
|
||||
@@ -657,64 +627,60 @@ install_log2ram_auto() {
|
||||
if [[ "$is_ssd" == true ]]; then
|
||||
msg_ok "$(translate "System disk is SSD or M.2. Proceeding with Log2RAM setup.")"
|
||||
else
|
||||
if whiptail --yesno "$(translate "Do you want to install Log2RAM anyway to reduce log write load?")" \
|
||||
10 70 --title "Log2RAM"; then
|
||||
:
|
||||
kill -TERM "$SPINNER_PID" 2>/dev/null
|
||||
sleep 1
|
||||
if whiptail --yesno "$(translate "Do you want to install Log2RAM anyway to reduce log write load?")" 10 70 --title "Log2RAM"; then
|
||||
msg_ok "$(translate "Proceeding with Log2RAM setup on non-SSD disk as requested by user.")"
|
||||
else
|
||||
msg_info2 "$(translate "Log2RAM installation cancelled by user")"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
msg_info "$(translate "Cleaning previous Log2RAM installation...")"
|
||||
|
||||
systemctl stop log2ram log2ram-daily.timer >/dev/null 2>&1 || true
|
||||
systemctl disable log2ram log2ram-daily.timer >/dev/null 2>&1 || true
|
||||
|
||||
if [[ -f /etc/log2ram.conf ]] && command -v log2ram >/dev/null 2>&1 && systemctl list-units --all | grep -q log2ram; then
|
||||
msg_ok "$(translate "Log2RAM is already installed and configured correctly.")"
|
||||
register_tool "log2ram" true
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg_info "$(translate "Log2RAM proceeding with installation...")"
|
||||
|
||||
rm -f /etc/cron.d/log2ram /etc/cron.d/log2ram-auto-sync \
|
||||
/etc/cron.hourly/log2ram /etc/cron.daily/log2ram \
|
||||
/etc/cron.weekly/log2ram /etc/cron.monthly/log2ram 2>/dev/null || true
|
||||
rm -f /usr/local/bin/log2ram-check.sh /usr/local/bin/log2ram /usr/sbin/log2ram 2>/dev/null || true
|
||||
rm -f /etc/systemd/system/log2ram.service \
|
||||
/etc/systemd/system/log2ram-daily.timer \
|
||||
/etc/systemd/system/log2ram-daily.service \
|
||||
/etc/systemd/system/sysinit.target.wants/log2ram.service 2>/dev/null || true
|
||||
rm -rf /etc/systemd/system/log2ram.service.d 2>/dev/null || true
|
||||
rm -f /etc/log2ram.conf* 2>/dev/null || true
|
||||
rm -rf /etc/logrotate.d/log2ram /var/log.hdd /tmp/log2ram 2>/dev/null || true
|
||||
|
||||
if [[ -d /tmp/log2ram ]]; then
|
||||
rm -rf /tmp/log2ram
|
||||
fi
|
||||
|
||||
|
||||
[[ -f /etc/systemd/system/log2ram.service ]] && rm -f /etc/systemd/system/log2ram*
|
||||
[[ -f /etc/systemd/system/log2ram-daily.service ]] && rm -f /etc/systemd/system/log2ram-daily.*
|
||||
[[ -f /etc/cron.d/log2ram ]] && rm -f /etc/cron.d/log2ram*
|
||||
[[ -f /usr/sbin/log2ram ]] && rm -f /usr/sbin/log2ram
|
||||
[[ -f /etc/log2ram.conf ]] && rm -f /etc/log2ram.conf
|
||||
[[ -f /usr/local/bin/log2ram-check.sh ]] && rm -f /usr/local/bin/log2ram-check.sh
|
||||
[[ -d /var/log.hdd ]] && rm -rf /var/log.hdd
|
||||
|
||||
systemctl daemon-reexec >/dev/null 2>&1 || true
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
|
||||
|
||||
systemctl restart cron >/dev/null 2>&1 || true
|
||||
|
||||
msg_ok "$(translate "Previous installation cleaned")"
|
||||
msg_info "$(translate "Installing Log2RAM from source...")"
|
||||
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
apt-get update -qq >/dev/null 2>&1
|
||||
apt-get install -y git >/dev/null 2>&1
|
||||
#msg_ok "$(translate "Git installed successfully")"
|
||||
fi
|
||||
|
||||
|
||||
rm -rf /tmp/log2ram 2>/dev/null || true
|
||||
if ! git clone https://github.com/azlux/log2ram.git /tmp/log2ram >/dev/null 2>>/tmp/log2ram_install.log; then
|
||||
msg_error "$(translate "Failed to clone log2ram repository. Check /tmp/log2ram_install.log")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cd /tmp/log2ram || {
|
||||
msg_error "$(translate "Failed to access log2ram directory")"
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
cd /tmp/log2ram || { msg_error "$(translate "Failed to access log2ram directory")"; return 1; }
|
||||
|
||||
if ! bash install.sh >>/tmp/log2ram_install.log 2>&1; then
|
||||
msg_error "$(translate "Failed to run log2ram installer. Check /tmp/log2ram_install.log")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
systemctl enable --now log2ram >/dev/null 2>&1 || true
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
|
||||
if [[ -f /etc/log2ram.conf ]] && command -v log2ram >/dev/null 2>&1; then
|
||||
msg_ok "$(translate "Log2RAM installed successfully")"
|
||||
@@ -722,66 +688,141 @@ install_log2ram_auto() {
|
||||
msg_error "$(translate "Log2RAM installation verification failed. Check /tmp/log2ram_install.log")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
RAM_SIZE_GB=$(free -g | awk '/^Mem:/{print $2}')
|
||||
[[ -z "$RAM_SIZE_GB" || "$RAM_SIZE_GB" -eq 0 ]] && RAM_SIZE_GB=4
|
||||
|
||||
|
||||
if (( RAM_SIZE_GB <= 8 )); then
|
||||
LOG2RAM_SIZE="128M"
|
||||
CRON_HOURS=1
|
||||
LOG2RAM_SIZE="128M"; CRON_HOURS=1
|
||||
elif (( RAM_SIZE_GB <= 16 )); then
|
||||
LOG2RAM_SIZE="256M"
|
||||
CRON_HOURS=3
|
||||
LOG2RAM_SIZE="256M"; CRON_HOURS=3
|
||||
else
|
||||
LOG2RAM_SIZE="512M"
|
||||
CRON_HOURS=6
|
||||
LOG2RAM_SIZE="512M"; CRON_HOURS=6
|
||||
fi
|
||||
|
||||
|
||||
msg_ok "$(translate "Detected RAM:") $RAM_SIZE_GB GB — $(translate "Log2RAM size set to:") $LOG2RAM_SIZE"
|
||||
|
||||
|
||||
sed -i "s/^SIZE=.*/SIZE=$LOG2RAM_SIZE/" /etc/log2ram.conf
|
||||
LOG2RAM_BIN="$(command -v log2ram || echo /usr/local/bin/log2ram)"
|
||||
rm -f /etc/cron.daily/log2ram /etc/cron.weekly/log2ram /etc/cron.monthly/log2ram 2>/dev/null || true
|
||||
rm -f /etc/cron.hourly/log2ram
|
||||
|
||||
{
|
||||
echo 'MAILTO=""'
|
||||
echo "0 */$CRON_HOURS * * * root $LOG2RAM_BIN write >/dev/null 2>&1"
|
||||
} > /etc/cron.d/log2ram
|
||||
|
||||
LOG2RAM_BIN="$(command -v log2ram || echo /usr/sbin/log2ram)"
|
||||
|
||||
cat > /etc/cron.d/log2ram <<EOF
|
||||
# Log2RAM periodic sync - Created by ProxMenux
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
MAILTO=""
|
||||
0 */$CRON_HOURS * * * root $LOG2RAM_BIN write >/dev/null 2>&1
|
||||
EOF
|
||||
chmod 0644 /etc/cron.d/log2ram
|
||||
chown root:root /etc/cron.d/log2ram
|
||||
msg_ok "$(translate "Log2RAM write scheduled every") $CRON_HOURS $(translate "hour(s)")"
|
||||
|
||||
|
||||
cat << 'EOF' > /usr/local/bin/log2ram-check.sh
|
||||
#!/bin/bash
|
||||
cat > /usr/local/bin/log2ram-check.sh <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
CONF_FILE="/etc/log2ram.conf"
|
||||
LIMIT_KB=$(grep '^SIZE=' "$CONF_FILE" | cut -d'=' -f2 | tr -d 'M')000
|
||||
USED_KB=$(df /var/log --output=used | tail -1)
|
||||
THRESHOLD=$(( LIMIT_KB * 90 / 100 ))
|
||||
L2R_BIN="$(command -v log2ram || true)"
|
||||
[[ -z "$L2R_BIN" && -x /usr/sbin/log2ram ]] && L2R_BIN="/usr/sbin/log2ram"
|
||||
[[ -z "$L2R_BIN" ]] && exit 0
|
||||
|
||||
if (( USED_KB > THRESHOLD )); then
|
||||
$(command -v log2ram) write
|
||||
SIZE_MiB="$(grep -E '^SIZE=' "$CONF_FILE" 2>/dev/null | cut -d'=' -f2 | tr -dc '0-9')"
|
||||
[[ -z "$SIZE_MiB" ]] && SIZE_MiB=128
|
||||
LIMIT_BYTES=$(( SIZE_MiB * 1024 * 1024 ))
|
||||
THRESHOLD_BYTES=$(( LIMIT_BYTES * 90 / 100 ))
|
||||
|
||||
USED_BYTES="$(df -B1 --output=used /var/log 2>/dev/null | tail -1 | tr -dc '0-9')"
|
||||
[[ -z "$USED_BYTES" ]] && exit 0
|
||||
|
||||
LOCK="/run/log2ram-check.lock"
|
||||
exec 9>"$LOCK" 2>/dev/null || exit 0
|
||||
flock -n 9 || exit 0
|
||||
|
||||
if (( USED_BYTES > THRESHOLD_BYTES )); then
|
||||
"$L2R_BIN" write 2>/dev/null || true
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x /usr/local/bin/log2ram-check.sh
|
||||
{
|
||||
echo 'MAILTO=""'
|
||||
echo "*/5 * * * * root /usr/local/bin/log2ram-check.sh >/dev/null 2>&1"
|
||||
} > /etc/cron.d/log2ram-auto-sync
|
||||
|
||||
cat > /etc/cron.d/log2ram-auto-sync <<'EOF'
|
||||
# Log2RAM auto-sync based on /var/log usage - Created by ProxMenux
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
MAILTO=""
|
||||
*/5 * * * * root /usr/local/bin/log2ram-check.sh >/dev/null 2>&1
|
||||
EOF
|
||||
chmod 0644 /etc/cron.d/log2ram-auto-sync
|
||||
chown root:root /etc/cron.d/log2ram-auto-sync
|
||||
|
||||
systemctl restart cron >/dev/null 2>&1 || true
|
||||
msg_ok "$(translate "Auto-sync enabled when /var/log exceeds 90% of") $LOG2RAM_SIZE"
|
||||
|
||||
|
||||
|
||||
msg_info "$(translate "Adjusting systemd-journald limits to match Log2RAM size...")"
|
||||
|
||||
|
||||
if [[ -f /etc/systemd/journald.conf ]]; then
|
||||
cp -n /etc/systemd/journald.conf /etc/systemd/journald.conf.bak.$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
fi
|
||||
|
||||
SIZE_MB=$(echo "$LOG2RAM_SIZE" | tr -dc '0-9')
|
||||
|
||||
|
||||
USE_MB=$(( SIZE_MB * 55 / 100 ))
|
||||
KEEP_MB=$(( SIZE_MB * 10 / 100 ))
|
||||
RUNTIME_MB=$(( SIZE_MB * 25 / 100 ))
|
||||
|
||||
|
||||
[ "$USE_MB" -lt 80 ] && USE_MB=80
|
||||
[ "$RUNTIME_MB" -lt 32 ] && RUNTIME_MB=32
|
||||
[ "$KEEP_MB" -lt 8 ] && KEEP_MB=8
|
||||
|
||||
|
||||
sed -i '/^\[Journal\]/,$d' /etc/systemd/journald.conf 2>/dev/null || true
|
||||
tee -a /etc/systemd/journald.conf >/dev/null <<EOF
|
||||
[Journal]
|
||||
Storage=persistent
|
||||
SplitMode=none
|
||||
RateLimitIntervalSec=30s
|
||||
RateLimitBurst=1000
|
||||
ForwardToSyslog=no
|
||||
ForwardToWall=no
|
||||
Seal=no
|
||||
Compress=yes
|
||||
SystemMaxUse=${USE_MB}M
|
||||
SystemKeepFree=${KEEP_MB}M
|
||||
RuntimeMaxUse=${RUNTIME_MB}M
|
||||
MaxLevelStore=warning
|
||||
MaxLevelSyslog=warning
|
||||
MaxLevelKMsg=warning
|
||||
MaxLevelConsole=notice
|
||||
MaxLevelWall=crit
|
||||
EOF
|
||||
|
||||
|
||||
mkdir -p /var/log/pveproxy
|
||||
chown -R www-data:www-data /var/log/pveproxy
|
||||
chmod 0750 /var/log/pveproxy
|
||||
|
||||
mkdir -p /var/log.hdd/pveproxy
|
||||
chown -R www-data:www-data /var/log.hdd/pveproxy
|
||||
chmod 0750 /var/log.hdd/pveproxy
|
||||
|
||||
systemctl restart systemd-journald >/dev/null 2>&1 || true
|
||||
#msg_ok "$(translate "Backup created:") /etc/systemd/journald.conf.bak.$(date +%Y%m%d-%H%M%S)"
|
||||
msg_ok "$(translate "Journald configuration adjusted to") ${USE_MB}M (Log2RAM ${LOG2RAM_SIZE})"
|
||||
|
||||
|
||||
register_tool "log2ram" true
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ==========================================================
|
||||
|
||||
|
||||
@@ -836,28 +877,27 @@ EOF
|
||||
# ==========================================================
|
||||
|
||||
run_complete_optimization() {
|
||||
clear
|
||||
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "ProxMenux Optimization Post-Installation")"
|
||||
msg_title "$(translate "$SCRIPT_TITLE")"
|
||||
|
||||
ensure_tools_json
|
||||
|
||||
apt_upgrade
|
||||
cleanup
|
||||
remove_subscription_banner
|
||||
force_apt_ipv4
|
||||
#configure_time_sync
|
||||
skip_apt_languages
|
||||
optimize_journald
|
||||
optimize_logrotate
|
||||
increase_system_limits
|
||||
configure_entropy
|
||||
optimize_memory_settings
|
||||
configure_kernel_panic
|
||||
force_apt_ipv4
|
||||
apply_network_optimizations
|
||||
#disable_rpc
|
||||
customize_bashrc
|
||||
install_log2ram_auto
|
||||
optimize_journald
|
||||
optimize_logrotate
|
||||
setup_persistent_network
|
||||
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ apt_upgrade() {
|
||||
|
||||
if [[ "$pve_version" -ge 9 ]]; then
|
||||
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve.sh")
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve9_2.sh")
|
||||
else
|
||||
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve8.sh")
|
||||
@@ -795,7 +795,7 @@ EOF
|
||||
)
|
||||
|
||||
local selected
|
||||
selected=$(dialog --clear --backtitle "ProxMenu - $(translate "System Utilities")" \
|
||||
selected=$(dialog --clear --backtitle "ProxMenux - $(translate "System Utilities")" \
|
||||
--title "$(translate "Select utilities to install")" \
|
||||
--checklist "$(translate "Use SPACE to select/deselect, ENTER to confirm")" \
|
||||
20 70 12 "${utilities[@]}" 2>&1 >/dev/tty)
|
||||
@@ -807,7 +807,7 @@ EOF
|
||||
local selected="$1"
|
||||
|
||||
if [ -z "$selected" ]; then
|
||||
dialog --clear --backtitle "ProxMenu" \
|
||||
dialog --clear --backtitle "ProxMenux" \
|
||||
--title "$(translate "No Selection")" \
|
||||
--msgbox "$(translate "No utilities were selected")" 8 40
|
||||
return
|
||||
@@ -975,7 +975,7 @@ EOF
|
||||
|
||||
local selected
|
||||
selected=$(
|
||||
dialog --clear --backtitle "ProxMenu - $(translate "System Utilities")" \
|
||||
dialog --clear --backtitle "ProxMenux - $(translate "System Utilities")" \
|
||||
--title "$(translate "Select utilities to install")" \
|
||||
--checklist "$(translate "Use SPACE to select/deselect, ENTER to confirm")" \
|
||||
20 70 12 "${utilities[@]}" 3>&1 1>&2 2>&3
|
||||
@@ -992,7 +992,7 @@ EOF
|
||||
|
||||
|
||||
if [[ -z "$selected" ]]; then
|
||||
dialog --clear --backtitle "ProxMenu" \
|
||||
dialog --clear --backtitle "ProxMenux" \
|
||||
--title "$(translate "No Selection")" \
|
||||
--msgbox "$(translate "No utilities were selected")" 8 40
|
||||
return 0
|
||||
@@ -1051,7 +1051,7 @@ EOF
|
||||
local dlg_status=$?
|
||||
|
||||
if [[ $dlg_status -ne 0 ]]; then
|
||||
dialog --clear --backtitle "ProxMenu" \
|
||||
dialog --clear --backtitle "ProxMenux" \
|
||||
--title "$(translate "Canceled")" \
|
||||
--msgbox "$(translate "Action canceled by user")" 8 40
|
||||
|
||||
@@ -2902,7 +2902,7 @@ remove_subscription_banner() {
|
||||
|
||||
if [[ "$pve_version" -ge 9 ]]; then
|
||||
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve9.sh")
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve-v3.sh")
|
||||
else
|
||||
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve8.sh")
|
||||
@@ -3436,7 +3436,7 @@ EOF
|
||||
chmod +x "$profile_script"
|
||||
|
||||
|
||||
ensure_aliases() {
|
||||
ensure_aliases_() {
|
||||
local bashrc="/root/.bashrc"
|
||||
[[ -f "$bashrc" ]] || touch "$bashrc"
|
||||
|
||||
@@ -3473,6 +3473,44 @@ EOF
|
||||
awk '!seen[$0]++' "$bashrc" > "${bashrc}.tmp" && mv "${bashrc}.tmp" "$bashrc"
|
||||
}
|
||||
|
||||
|
||||
ensure_aliases() {
|
||||
local bashrc="/root/.bashrc"
|
||||
[[ -f "$bashrc" ]] || touch "$bashrc"
|
||||
|
||||
if ! grep -q "shopt -s expand_aliases" "$bashrc" 2>/dev/null; then
|
||||
echo "shopt -s expand_aliases" >> "$bashrc"
|
||||
fi
|
||||
|
||||
local -a ALIASES=(
|
||||
"aptup=apt update && apt dist-upgrade"
|
||||
"lxcclean=bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/clean-lxcs.sh)\""
|
||||
"lxcupdate=bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/update-lxcs.sh)\""
|
||||
"kernelclean=bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/kernel-clean.sh)\""
|
||||
"cpugov=bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/scaling-governor.sh)\""
|
||||
"lxctrim=bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/fstrim.sh)\""
|
||||
"updatecerts=pvecm updatecerts"
|
||||
"seqwrite=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4M --size=32G --readwrite=write --ramp_time=4"
|
||||
"seqread=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4M --size=32G --readwrite=read --ramp_time=4"
|
||||
"ranwrite=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4k --size=4G --readwrite=randwrite --ramp_time=4"
|
||||
"ranread=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4k --size=4G --readwrite=randread --ramp_time=4"
|
||||
)
|
||||
|
||||
for entry in "${ALIASES[@]}"; do
|
||||
local name="${entry%%=*}"
|
||||
local cmd="${entry#*=}"
|
||||
|
||||
local safe_cmd=${cmd//\'/\'\\\'\'}
|
||||
|
||||
sed -i -E "/^[[:space:]]*alias[[:space:]]+${name}=.*/d" "$bashrc"
|
||||
|
||||
printf "alias %s='%s'\n" "$name" "$safe_cmd" >> "$bashrc"
|
||||
done
|
||||
|
||||
. "$bashrc"
|
||||
}
|
||||
|
||||
|
||||
ensure_aliases
|
||||
msg_ok "$(translate "Aliases added to .bashrc")"
|
||||
|
||||
@@ -3514,19 +3552,10 @@ update_pve_appliance_manager() {
|
||||
|
||||
|
||||
|
||||
|
||||
configure_log2ram() {
|
||||
|
||||
configure_log2ram_() {
|
||||
msg_info2 "$(translate "Preparing Log2RAM configuration")"
|
||||
sleep 2
|
||||
|
||||
|
||||
if [[ -f /etc/log2ram.conf ]] && command -v log2ram >/dev/null 2>&1 && systemctl list-units --all | grep -q log2ram; then
|
||||
msg_ok "$(translate "Log2RAM is already installed and configured correctly.")"
|
||||
register_tool "log2ram" true
|
||||
return 0
|
||||
fi
|
||||
|
||||
RAM_SIZE_GB=$(free -g | awk '/^Mem:/{print $2}')
|
||||
[[ -z "$RAM_SIZE_GB" || "$RAM_SIZE_GB" -eq 0 ]] && RAM_SIZE_GB=4
|
||||
|
||||
@@ -3541,11 +3570,9 @@ configure_log2ram() {
|
||||
DEFAULT_HOURS="6"
|
||||
fi
|
||||
|
||||
|
||||
USER_SIZE=$(whiptail --title "Log2RAM" --inputbox "$(translate "Enter the maximum size (in MB) to allocate for /var/log in RAM (e.g. 128, 256, 512):")\n\n$(translate "Recommended for $RAM_SIZE_GB GB RAM:") ${DEFAULT_SIZE}M" 12 70 "$DEFAULT_SIZE" 3>&1 1>&2 2>&3) || return 0
|
||||
LOG2RAM_SIZE="${USER_SIZE}M"
|
||||
|
||||
|
||||
CRON_HOURS=$(whiptail --title "Log2RAM" --radiolist "$(translate "Select the sync interval (in hours):")\n\n$(translate "Suggested interval: every $DEFAULT_HOURS hour(s)")" 15 70 5 \
|
||||
"1" "$(translate "Every hour")" OFF \
|
||||
"3" "$(translate "Every 3 hours")" OFF \
|
||||
@@ -3553,17 +3580,34 @@ configure_log2ram() {
|
||||
"12" "$(translate "Every 12 hours")" OFF \
|
||||
3>&1 1>&2 2>&3) || return 0
|
||||
|
||||
|
||||
if whiptail --title "Log2RAM" --yesno "$(translate "Enable auto-sync if /var/log exceeds 90% of its size?")" 10 60; then
|
||||
ENABLE_AUTOSYNC=true
|
||||
else
|
||||
ENABLE_AUTOSYNC=false
|
||||
fi
|
||||
|
||||
msg_info "$(translate "Cleaning previous Log2RAM installation...")"
|
||||
|
||||
systemctl stop log2ram log2ram-daily.timer >/dev/null 2>&1 || true
|
||||
systemctl disable log2ram log2ram-daily.timer >/dev/null 2>&1 || true
|
||||
|
||||
rm -f /etc/cron.d/log2ram /etc/cron.d/log2ram-auto-sync \
|
||||
/etc/cron.hourly/log2ram /etc/cron.daily/log2ram \
|
||||
/etc/cron.weekly/log2ram /etc/cron.monthly/log2ram 2>/dev/null || true
|
||||
rm -f /usr/local/bin/log2ram-check.sh /usr/local/bin/log2ram /usr/sbin/log2ram 2>/dev/null || true
|
||||
rm -f /etc/systemd/system/log2ram.service \
|
||||
/etc/systemd/system/log2ram-daily.timer \
|
||||
/etc/systemd/system/log2ram-daily.service \
|
||||
/etc/systemd/system/sysinit.target.wants/log2ram.service 2>/dev/null || true
|
||||
rm -rf /etc/systemd/system/log2ram.service.d 2>/dev/null || true
|
||||
rm -f /etc/log2ram.conf* 2>/dev/null || true
|
||||
rm -rf /etc/logrotate.d/log2ram /var/log.hdd /tmp/log2ram 2>/dev/null || true
|
||||
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl restart cron >/dev/null 2>&1 || true
|
||||
|
||||
msg_ok "$(translate "Previous installation cleaned")"
|
||||
msg_info "$(translate "Installing Log2RAM from GitHub...")"
|
||||
rm -rf /tmp/log2ram
|
||||
|
||||
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
msg_info "$(translate "Installing required package: git")"
|
||||
@@ -3571,64 +3615,92 @@ configure_log2ram() {
|
||||
apt-get install -y git >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
git clone https://github.com/azlux/log2ram.git /tmp/log2ram >/dev/null 2>&1
|
||||
cd /tmp/log2ram || return 1
|
||||
bash install.sh >/dev/null 2>&1
|
||||
|
||||
if [[ -f /etc/log2ram.conf ]] && systemctl list-units --all | grep -q log2ram; then
|
||||
msg_ok "$(translate "Log2RAM installed successfully")"
|
||||
else
|
||||
msg_error "$(translate "Failed to install Log2RAM.")"
|
||||
rm -rf /tmp/log2ram 2>/dev/null || true
|
||||
if ! git clone https://github.com/azlux/log2ram.git /tmp/log2ram >/dev/null 2>>/tmp/log2ram_install.log; then
|
||||
msg_error "$(translate "Failed to clone log2ram repository. Check /tmp/log2ram_install.log")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cd /tmp/log2ram || { msg_error "$(translate "Failed to access log2ram directory")"; return 1; }
|
||||
|
||||
if ! bash install.sh >>/tmp/log2ram_install.log 2>&1; then
|
||||
msg_error "$(translate "Failed to run log2ram installer. Check /tmp/log2ram_install.log")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl enable --now log2ram >/dev/null 2>&1 || true
|
||||
|
||||
if [[ -f /etc/log2ram.conf ]] && command -v log2ram >/dev/null 2>&1; then
|
||||
msg_ok "$(translate "Log2RAM installed successfully")"
|
||||
else
|
||||
msg_error "$(translate "Log2RAM installation verification failed. Check /tmp/log2ram_install.log")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sed -i "s/^SIZE=.*/SIZE=$LOG2RAM_SIZE/" /etc/log2ram.conf
|
||||
LOG2RAM_BIN="$(command -v log2ram || echo /usr/local/bin/log2ram)"
|
||||
rm -f /etc/cron.daily/log2ram /etc/cron.weekly/log2ram /etc/cron.monthly/log2ram 2>/dev/null || true
|
||||
rm -f /etc/cron.hourly/log2ram
|
||||
LOG2RAM_BIN="$(command -v log2ram || echo /usr/sbin/log2ram)"
|
||||
|
||||
{
|
||||
echo 'MAILTO=""'
|
||||
echo "0 */$CRON_HOURS * * * root $LOG2RAM_BIN write >/dev/null 2>&1"
|
||||
} > /etc/cron.d/log2ram
|
||||
|
||||
cat > /etc/cron.d/log2ram <<EOF
|
||||
# Log2RAM periodic sync - Created by ProxMenux
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
MAILTO=""
|
||||
0 */$CRON_HOURS * * * root $LOG2RAM_BIN write >/dev/null 2>&1
|
||||
EOF
|
||||
chmod 0644 /etc/cron.d/log2ram
|
||||
chown root:root /etc/cron.d/log2ram
|
||||
msg_ok "$(translate "Log2RAM write scheduled every") $CRON_HOURS $(translate "hour(s)")"
|
||||
|
||||
|
||||
if [[ "$ENABLE_AUTOSYNC" == true ]]; then
|
||||
cat << 'EOF' > /usr/local/bin/log2ram-check.sh
|
||||
#!/bin/bash
|
||||
cat > /usr/local/bin/log2ram-check.sh <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
CONF_FILE="/etc/log2ram.conf"
|
||||
LIMIT_KB=$(grep '^SIZE=' "$CONF_FILE" | cut -d'=' -f2 | tr -d 'M')000
|
||||
USED_KB=$(df /var/log --output=used | tail -1)
|
||||
THRESHOLD=$(( LIMIT_KB * 90 / 100 ))
|
||||
if (( USED_KB > THRESHOLD )); then
|
||||
$(command -v log2ram) write
|
||||
L2R_BIN="$(command -v log2ram || true)"
|
||||
[[ -z "$L2R_BIN" && -x /usr/sbin/log2ram ]] && L2R_BIN="/usr/sbin/log2ram"
|
||||
[[ -z "$L2R_BIN" ]] && exit 0
|
||||
|
||||
SIZE_MiB="$(grep -E '^SIZE=' "$CONF_FILE" 2>/dev/null | cut -d'=' -f2 | tr -dc '0-9')"
|
||||
[[ -z "$SIZE_MiB" ]] && SIZE_MiB=128
|
||||
LIMIT_BYTES=$(( SIZE_MiB * 1024 * 1024 ))
|
||||
THRESHOLD_BYTES=$(( LIMIT_BYTES * 90 / 100 ))
|
||||
|
||||
USED_BYTES="$(df -B1 --output=used /var/log 2>/dev/null | tail -1 | tr -dc '0-9')"
|
||||
[[ -z "$USED_BYTES" ]] && exit 0
|
||||
|
||||
LOCK="/run/log2ram-check.lock"
|
||||
exec 9>"$LOCK" 2>/dev/null || exit 0
|
||||
flock -n 9 || exit 0
|
||||
|
||||
if (( USED_BYTES > THRESHOLD_BYTES )); then
|
||||
"$L2R_BIN" write 2>/dev/null || true
|
||||
fi
|
||||
EOF
|
||||
chmod +x /usr/local/bin/log2ram-check.sh
|
||||
|
||||
{
|
||||
echo 'MAILTO=""'
|
||||
echo "*/5 * * * * root /usr/local/bin/log2ram-check.sh >/dev/null 2>&1"
|
||||
} > /etc/cron.d/log2ram-auto-sync
|
||||
cat > /etc/cron.d/log2ram-auto-sync <<'EOF'
|
||||
# Log2RAM auto-sync based on /var/log usage - Created by ProxMenux
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
MAILTO=""
|
||||
*/5 * * * * root /usr/local/bin/log2ram-check.sh >/dev/null 2>&1
|
||||
EOF
|
||||
chmod 0644 /etc/cron.d/log2ram-auto-sync
|
||||
chown root:root /etc/cron.d/log2ram-auto-sync
|
||||
|
||||
msg_ok "$(translate "Auto-sync enabled when /var/log exceeds 90% of") $LOG2RAM_SIZE"
|
||||
else
|
||||
rm -f /usr/local/bin/log2ram-check.sh /etc/cron.d/log2ram-auto-sync
|
||||
rm -f /usr/local/bin/log2ram-check.sh /etc/cron.d/log2ram-auto-sync 2>/dev/null || true
|
||||
msg_info2 "$(translate "Auto-sync was not enabled")"
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
systemctl restart log2ram
|
||||
systemctl restart cron >/dev/null 2>&1 || true
|
||||
systemctl restart log2ram >/dev/null 2>&1 || true
|
||||
|
||||
msg_success "$(translate "Log2RAM installation and configuration completed successfully.")"
|
||||
register_tool "log2ram" true
|
||||
# NECESSARY_REBOOT=1
|
||||
}
|
||||
|
||||
|
||||
@@ -3637,6 +3709,237 @@ EOF
|
||||
|
||||
|
||||
|
||||
|
||||
configure_log2ram() {
|
||||
msg_info2 "$(translate "Preparing Log2RAM configuration")"
|
||||
sleep 1
|
||||
|
||||
|
||||
RAM_SIZE_GB=$(free -g | awk '/^Mem:/{print $2}')
|
||||
[[ -z "$RAM_SIZE_GB" || "$RAM_SIZE_GB" -eq 0 ]] && RAM_SIZE_GB=4
|
||||
|
||||
if (( RAM_SIZE_GB <= 8 )); then
|
||||
DEFAULT_SIZE="128" # MiB
|
||||
DEFAULT_HOURS="1"
|
||||
elif (( RAM_SIZE_GB <= 16 )); then
|
||||
DEFAULT_SIZE="256"
|
||||
DEFAULT_HOURS="3"
|
||||
else
|
||||
DEFAULT_SIZE="512"
|
||||
DEFAULT_HOURS="6"
|
||||
fi
|
||||
|
||||
|
||||
USER_SIZE=$(whiptail --title "Log2RAM" --inputbox \
|
||||
"$(translate "Enter the maximum size (in MB) to allocate for /var/log in RAM (e.g. 128, 256, 512):")\n\n$(translate "Recommended for $RAM_SIZE_GB GB RAM:") ${DEFAULT_SIZE}M" \
|
||||
12 70 "$DEFAULT_SIZE" 3>&1 1>&2 2>&3) || return 0
|
||||
|
||||
if ! [[ "$USER_SIZE" =~ ^[0-9]+$ ]]; then
|
||||
msg_error "$(translate "Invalid size. Please enter a number in MB (e.g., 128, 256, 512).")"
|
||||
return 1
|
||||
fi
|
||||
(( USER_SIZE < 64 )) && USER_SIZE=64 # mínimo razonable
|
||||
(( USER_SIZE > 8192 )) && USER_SIZE=8192 # límite de seguridad
|
||||
LOG2RAM_SIZE="${USER_SIZE}M"
|
||||
|
||||
|
||||
CRON_HOURS=$(whiptail --title "Log2RAM" --radiolist \
|
||||
"$(translate "Select the sync interval (in hours):")\n\n$(translate "Suggested interval: every $DEFAULT_HOURS hour(s)")" \
|
||||
15 70 5 \
|
||||
"1" "$(translate "Every hour")" $([[ "$DEFAULT_HOURS" = "1" ]] && echo ON || echo OFF) \
|
||||
"3" "$(translate "Every 3 hours")" $([[ "$DEFAULT_HOURS" = "3" ]] && echo ON || echo OFF) \
|
||||
"6" "$(translate "Every 6 hours")" $([[ "$DEFAULT_HOURS" = "6" ]] && echo ON || echo OFF) \
|
||||
"12" "$(translate "Every 12 hours")" OFF \
|
||||
3>&1 1>&2 2>&3) || return 0
|
||||
|
||||
|
||||
if whiptail --title "Log2RAM" --yesno "$(translate "Enable auto-sync if /var/log exceeds 90% of its size?")" 10 60; then
|
||||
ENABLE_AUTOSYNC=true
|
||||
else
|
||||
ENABLE_AUTOSYNC=false
|
||||
fi
|
||||
|
||||
|
||||
msg_info "$(translate "Cleaning previous Log2RAM installation...")"
|
||||
systemctl stop log2ram log2ram-daily.timer >/dev/null 2>&1 || true
|
||||
systemctl disable log2ram log2ram-daily.timer >/dev/null 2>&1 || true
|
||||
|
||||
rm -f /etc/cron.d/log2ram /etc/cron.d/log2ram-auto-sync \
|
||||
/etc/cron.hourly/log2ram /etc/cron.daily/log2ram \
|
||||
/etc/cron.weekly/log2ram /etc/cron.monthly/log2ram 2>/dev/null || true
|
||||
rm -f /usr/local/bin/log2ram-check.sh /usr/local/bin/log2ram /usr/sbin/log2ram 2>/dev/null || true
|
||||
rm -f /etc/systemd/system/log2ram.service \
|
||||
/etc/systemd/system/log2ram-daily.timer \
|
||||
/etc/systemd/system/log2ram-daily.service \
|
||||
/etc/systemd/system/sysinit.target.wants/log2ram.service 2>/dev/null || true
|
||||
rm -rf /etc/systemd/system/log2ram.service.d 2>/dev/null || true
|
||||
rm -f /etc/log2ram.conf* 2>/dev/null || true
|
||||
rm -rf /etc/logrotate.d/log2ram /var/log.hdd /tmp/log2ram 2>/dev/null || true
|
||||
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl restart cron >/dev/null 2>&1 || true
|
||||
msg_ok "$(translate "Previous installation cleaned")"
|
||||
|
||||
|
||||
msg_info "$(translate "Installing Log2RAM from GitHub...")"
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
msg_info "$(translate "Installing required package: git")"
|
||||
apt-get update -qq >/dev/null 2>&1
|
||||
apt-get install -y git >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
rm -rf /tmp/log2ram 2>/dev/null || true
|
||||
if ! git clone https://github.com/azlux/log2ram.git /tmp/log2ram >/dev/null 2>>/tmp/log2ram_install.log; then
|
||||
msg_error "$(translate "Failed to clone log2ram repository. Check /tmp/log2ram_install.log")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cd /tmp/log2ram || { msg_error "$(translate "Failed to access log2ram directory")"; return 1; }
|
||||
if ! bash install.sh >>/tmp/log2ram_install.log 2>&1; then
|
||||
msg_error "$(translate "Failed to run log2ram installer. Check /tmp/log2ram_install.log")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl enable --now log2ram >/dev/null 2>&1 || true
|
||||
|
||||
if [[ -f /etc/log2ram.conf ]] && command -v log2ram >/dev/null 2>&1; then
|
||||
msg_ok "$(translate "Log2RAM installed successfully")"
|
||||
else
|
||||
msg_error "$(translate "Log2RAM installation verification failed. Check /tmp/log2ram_install.log")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
sed -i "s/^SIZE=.*/SIZE=$LOG2RAM_SIZE/" /etc/log2ram.conf
|
||||
LOG2RAM_BIN="$(command -v log2ram || echo /usr/sbin/log2ram)"
|
||||
|
||||
cat > /etc/cron.d/log2ram <<EOF
|
||||
# Log2RAM periodic sync - Created by ProxMenux
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
MAILTO=""
|
||||
0 */$CRON_HOURS * * * root $LOG2RAM_BIN write >/dev/null 2>&1
|
||||
EOF
|
||||
chmod 0644 /etc/cron.d/log2ram
|
||||
chown root:root /etc/cron.d/log2ram
|
||||
msg_ok "$(translate "Log2RAM write scheduled every") $CRON_HOURS $(translate "hour(s)")"
|
||||
|
||||
|
||||
if [[ "$ENABLE_AUTOSYNC" == true ]]; then
|
||||
cat > /usr/local/bin/log2ram-check.sh <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
CONF_FILE="/etc/log2ram.conf"
|
||||
L2R_BIN="$(command -v log2ram || true)"
|
||||
[[ -z "$L2R_BIN" && -x /usr/sbin/log2ram ]] && L2R_BIN="/usr/sbin/log2ram"
|
||||
[[ -z "$L2R_BIN" ]] && exit 0
|
||||
|
||||
SIZE_MiB="$(grep -E '^SIZE=' "$CONF_FILE" 2>/dev/null | cut -d'=' -f2 | tr -dc '0-9')"
|
||||
[[ -z "$SIZE_MiB" ]] && SIZE_MiB=128
|
||||
LIMIT_BYTES=$(( SIZE_MiB * 1024 * 1024 ))
|
||||
THRESHOLD_BYTES=$(( LIMIT_BYTES * 90 / 100 ))
|
||||
|
||||
USED_BYTES="$(df -B1 --output=used /var/log 2>/dev/null | tail -1 | tr -dc '0-9')"
|
||||
[[ -z "$USED_BYTES" ]] && exit 0
|
||||
|
||||
LOCK="/run/log2ram-check.lock"
|
||||
exec 9>"$LOCK" 2>/dev/null || exit 0
|
||||
flock -n 9 || exit 0
|
||||
|
||||
if (( USED_BYTES > THRESHOLD_BYTES )); then
|
||||
"$L2R_BIN" write 2>/dev/null || true
|
||||
fi
|
||||
EOF
|
||||
chmod +x /usr/local/bin/log2ram-check.sh
|
||||
|
||||
cat > /etc/cron.d/log2ram-auto-sync <<'EOF'
|
||||
# Log2RAM auto-sync based on /var/log usage - Created by ProxMenux
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
MAILTO=""
|
||||
*/5 * * * * root /usr/local/bin/log2ram-check.sh >/dev/null 2>&1
|
||||
EOF
|
||||
chmod 0644 /etc/cron.d/log2ram-auto-sync
|
||||
chown root:root /etc/cron.d/log2ram-auto-sync
|
||||
msg_ok "$(translate "Auto-sync enabled when /var/log exceeds 90% of") $LOG2RAM_SIZE"
|
||||
else
|
||||
rm -f /usr/local/bin/log2ram-check.sh /etc/cron.d/log2ram-auto-sync 2>/dev/null || true
|
||||
msg_info2 "$(translate "Auto-sync was not enabled")"
|
||||
fi
|
||||
|
||||
# --- Ajuste de systemd-journald proporcional al tamaño de Log2RAM ---
|
||||
msg_info "$(translate "Adjusting systemd-journald limits to match Log2RAM size...")"
|
||||
|
||||
if [[ -f /etc/systemd/journald.conf ]]; then
|
||||
cp -n /etc/systemd/journald.conf "/etc/systemd/journald.conf.bak.$(date +%Y%m%d-%H%M%S)"
|
||||
BAK_OK=$?
|
||||
fi
|
||||
|
||||
SIZE_MB=$(echo "$LOG2RAM_SIZE" | tr -dc '0-9')
|
||||
# Repartos: 55% persistente / 10% libre / 25% runtime (pisos mínimos)
|
||||
USE_MB=$(( SIZE_MB * 55 / 100 ))
|
||||
KEEP_MB=$(( SIZE_MB * 10 / 100 ))
|
||||
RUNTIME_MB=$(( SIZE_MB * 25 / 100 ))
|
||||
[ "$USE_MB" -lt 80 ] && USE_MB=80
|
||||
[ "$RUNTIME_MB" -lt 32 ] && RUNTIME_MB=32
|
||||
[ "$KEEP_MB" -lt 8 ] && KEEP_MB=8
|
||||
|
||||
# Reescribir bloque [Journal] de forma segura
|
||||
sed -i '/^\[Journal\]/,$d' /etc/systemd/journald.conf 2>/dev/null || true
|
||||
tee -a /etc/systemd/journald.conf >/dev/null <<EOF
|
||||
[Journal]
|
||||
Storage=persistent
|
||||
SplitMode=none
|
||||
RateLimitIntervalSec=30s
|
||||
RateLimitBurst=1000
|
||||
ForwardToSyslog=no
|
||||
ForwardToWall=no
|
||||
Seal=no
|
||||
Compress=yes
|
||||
SystemMaxUse=${USE_MB}M
|
||||
SystemKeepFree=${KEEP_MB}M
|
||||
RuntimeMaxUse=${RUNTIME_MB}M
|
||||
MaxLevelStore=warning
|
||||
MaxLevelSyslog=warning
|
||||
MaxLevelKMsg=warning
|
||||
MaxLevelConsole=notice
|
||||
MaxLevelWall=crit
|
||||
EOF
|
||||
|
||||
systemctl restart systemd-journald >/dev/null 2>&1 || true
|
||||
[[ "$BAK_OK" = "0" ]] && msg_ok "$(translate "Backup created:") /etc/systemd/journald.conf.bak.$(date +%Y%m%d-%H%M%S)"
|
||||
msg_ok "$(translate "Journald configuration adjusted to") ${USE_MB}M (Log2RAM ${LOG2RAM_SIZE})"
|
||||
|
||||
mkdir -p /var/log/pveproxy
|
||||
chown -R www-data:www-data /var/log/pveproxy
|
||||
chmod 0750 /var/log/pveproxy
|
||||
|
||||
mkdir -p /var/log.hdd/pveproxy
|
||||
chown -R www-data:www-data /var/log.hdd/pveproxy
|
||||
chmod 0750 /var/log.hdd/pveproxy
|
||||
|
||||
systemctl restart cron >/dev/null 2>&1 || true
|
||||
systemctl restart log2ram >/dev/null 2>&1 || true
|
||||
|
||||
|
||||
log2ram write >/dev/null 2>&1 || true
|
||||
log2ram clean >/dev/null 2>&1 || true
|
||||
systemctl restart rsyslog >/dev/null 2>&1 || true
|
||||
|
||||
msg_success "$(translate "Log2RAM installation and configuration completed successfully.")"
|
||||
register_tool "log2ram" true
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ==========================================================
|
||||
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ uninstall_apt_upgrade() {
|
||||
|
||||
################################################################
|
||||
|
||||
uninstall_subscription_banner() {
|
||||
uninstall_subscription_banner_() {
|
||||
msg_info "$(translate "Restoring subscription banner...")"
|
||||
|
||||
# Remove APT hook
|
||||
@@ -163,6 +163,214 @@ uninstall_subscription_banner() {
|
||||
register_tool "subscription_banner" false
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
uninstall_subscription_banner__() {
|
||||
msg_info "$(translate "Restoring subscription banner...")"
|
||||
|
||||
local JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||||
local MOBILE_TPL="/usr/share/pve-manager/templates/index.html.tpl"
|
||||
local PATCH_BIN="/usr/local/bin/pve-remove-nag.sh"
|
||||
local BASE_DIR="/opt/pve-nag-buster"
|
||||
local MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
|
||||
local GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
|
||||
local MARK_MOBILE=" PVE9 Mobile Subscription Banner Removal "
|
||||
|
||||
local restored=false
|
||||
|
||||
# Remove APT hook (both old and new versions)
|
||||
for hook in /etc/apt/apt.conf.d/*nag* /etc/apt/apt.conf.d/no-nag-script; do
|
||||
if [[ -e "$hook" ]]; then
|
||||
rm -f "$hook"
|
||||
msg_ok "$(translate "Removed APT hook: $hook")"
|
||||
fi
|
||||
done
|
||||
|
||||
# Remove patch script
|
||||
if [[ -f "$PATCH_BIN" ]]; then
|
||||
rm -f "$PATCH_BIN"
|
||||
msg_ok "$(translate "Removed patch script: $PATCH_BIN")"
|
||||
fi
|
||||
|
||||
# Restore JavaScript file from backups (new script method)
|
||||
if [[ -d "$BASE_DIR/backups" ]]; then
|
||||
local latest_backup
|
||||
latest_backup=$(ls -t "$BASE_DIR/backups"/proxmoxlib.js.backup.* 2>/dev/null | head -1)
|
||||
if [[ -n "$latest_backup" && -f "$latest_backup" ]]; then
|
||||
if [[ -s "$latest_backup" ]] && grep -q "Ext\|function" "$latest_backup" && ! grep -q $'\0' "$latest_backup"; then
|
||||
cp -a "$latest_backup" "$JS_FILE"
|
||||
msg_ok "$(translate "Restored from backup: $latest_backup")"
|
||||
restored=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Restore from old script backups (if new method didn't work)
|
||||
if [[ "$restored" == false ]]; then
|
||||
local old_backup
|
||||
old_backup=$(ls -t "${JS_FILE}".backup.pve9.* "${JS_FILE}".backup.* 2>/dev/null | head -1)
|
||||
if [[ -n "$old_backup" && -f "$old_backup" ]]; then
|
||||
if [[ -s "$old_backup" ]] && grep -q "Ext\|function" "$old_backup" && ! grep -q $'\0' "$old_backup"; then
|
||||
cp -a "$old_backup" "$JS_FILE"
|
||||
msg_ok "$(translate "Restored from old backup: $old_backup")"
|
||||
restored=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Restore mobile template if patched
|
||||
if [[ -f "$MOBILE_TPL" ]] && grep -q "$MARK_MOBILE" "$MOBILE_TPL"; then
|
||||
local mobile_backup
|
||||
mobile_backup=$(ls -t "$BASE_DIR/backups"/index.html.tpl.backup.* 2>/dev/null | head -1)
|
||||
if [[ -n "$mobile_backup" && -f "$mobile_backup" ]]; then
|
||||
cp -a "$mobile_backup" "$MOBILE_TPL"
|
||||
msg_ok "$(translate "Restored mobile template from backup")"
|
||||
else
|
||||
# Remove the patch manually if no backup available
|
||||
sed -i "/^$MARK_MOBILE$/,\$d" "$MOBILE_TPL"
|
||||
msg_ok "$(translate "Removed mobile template patches")"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If no valid backups found, reinstall packages
|
||||
if [[ "$restored" == false ]]; then
|
||||
msg_info "$(translate "No valid backups found, reinstalling packages...")"
|
||||
|
||||
if apt --reinstall install proxmox-widget-toolkit -y >/dev/null 2>&1; then
|
||||
msg_ok "$(translate "Reinstalled proxmox-widget-toolkit")"
|
||||
restored=true
|
||||
else
|
||||
msg_error "$(translate "Failed to reinstall proxmox-widget-toolkit")"
|
||||
fi
|
||||
|
||||
# Also try to reinstall pve-manager if mobile template was patched
|
||||
if [[ -f "$MOBILE_TPL" ]] && grep -q "$MARK_MOBILE" "$MOBILE_TPL"; then
|
||||
apt --reinstall install pve-manager -y >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$MIN_JS_FILE" "$GZ_FILE" 2>/dev/null || true
|
||||
find /var/cache/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
find /var/lib/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
find /var/cache/nginx/ -type f -delete 2>/dev/null || true
|
||||
|
||||
register_tool "subscription_banner" false
|
||||
|
||||
if [[ "$restored" == true ]]; then
|
||||
msg_ok "$(translate "Subscription banner restored successfully")"
|
||||
msg_ok "$(translate "Refresh your browser to see changes (server restart may be required)")"
|
||||
else
|
||||
msg_error "$(translate "Failed to restore subscription banner completely")"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
uninstall_subscription_banner() {
|
||||
msg_info "$(translate "Restoring subscription banner...")"
|
||||
|
||||
local JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||||
local MOBILE_UI_FILE="/usr/share/pve-yew-mobile-gui/index.html.tpl"
|
||||
local PATCH_BIN="/usr/local/bin/pve-remove-nag-v3.sh"
|
||||
local BASE_DIR="/usr/local/share/proxmenux"
|
||||
local MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
|
||||
local GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
|
||||
|
||||
local restored=false
|
||||
|
||||
|
||||
for hook in /etc/apt/apt.conf.d/*nag*; do
|
||||
if [[ -e "$hook" ]]; then
|
||||
rm -f "$hook"
|
||||
msg_ok "$(translate "Removed APT hook: $hook")"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -f "$PATCH_BIN" ]]; then
|
||||
rm -f "$PATCH_BIN"
|
||||
msg_ok "$(translate "Removed patch script: $PATCH_BIN")"
|
||||
fi
|
||||
|
||||
if [[ -d "$BASE_DIR/backups" ]]; then
|
||||
local backup_file
|
||||
backup_file=$(ls -t "$BASE_DIR/backups"/proxmoxlib.js.backup.* 2>/dev/null | head -1)
|
||||
|
||||
if [[ -n "$backup_file" && -f "$backup_file" ]]; then
|
||||
# Verify backup integrity before restoring
|
||||
if [[ -s "$backup_file" ]] && grep -q "Ext\|function" "$backup_file" && ! grep -q $'\0' "$backup_file"; then
|
||||
cp -a "$backup_file" "$JS_FILE"
|
||||
msg_ok "$(translate "Restored desktop UI from backup: $backup_file")"
|
||||
restored=true
|
||||
else
|
||||
msg_warn "$(translate "Backup file appears corrupted, will reinstall packages")"
|
||||
fi
|
||||
else
|
||||
msg_warn "$(translate "No desktop UI backup found, will reinstall packages")"
|
||||
fi
|
||||
|
||||
local mobile_backup
|
||||
mobile_backup=$(ls -t "$BASE_DIR/backups"/index.html.tpl.backup.* 2>/dev/null | head -1)
|
||||
|
||||
if [[ -n "$mobile_backup" && -f "$mobile_backup" && -f "$MOBILE_UI_FILE" ]]; then
|
||||
if [[ -s "$mobile_backup" ]]; then
|
||||
cp -a "$mobile_backup" "$MOBILE_UI_FILE"
|
||||
msg_ok "$(translate "Restored mobile UI from backup: $mobile_backup")"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$restored" == false ]]; then
|
||||
msg_info "$(translate "Performing complete package reinstallation...")"
|
||||
|
||||
# Update package lists
|
||||
apt-get update >/dev/null 2>&1
|
||||
|
||||
# Reinstall packages with force-confnew to restore original configs
|
||||
if apt-get --reinstall -o Dpkg::Options::="--force-confnew" install \
|
||||
pve-manager proxmox-widget-toolkit libjs-extjs libpve-http-server-perl -y >/dev/null 2>&1; then
|
||||
msg_ok "$(translate "Reinstalled Proxmox packages successfully")"
|
||||
restored=true
|
||||
else
|
||||
msg_error "$(translate "Failed to reinstall packages")"
|
||||
fi
|
||||
|
||||
# Clean package update cache
|
||||
rm -rf /var/lib/pve-manager/pkgupdates /var/cache/pve-manager 2>/dev/null || true
|
||||
|
||||
# Second pass reinstallation to ensure everything is clean
|
||||
apt-get update >/dev/null 2>&1
|
||||
apt-get --reinstall install proxmox-widget-toolkit pve-manager libjs-extjs libpve-http-server-perl -y >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
msg_info "$(translate "Cleaning cached files...")"
|
||||
rm -f "$MIN_JS_FILE" "$GZ_FILE" 2>/dev/null || true
|
||||
rm -rf /var/lib/pve-manager/pkgupdates /var/cache/pve-manager 2>/dev/null || true
|
||||
find /var/cache/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
find /var/lib/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
find /var/cache/nginx/ -type f -delete 2>/dev/null || true
|
||||
|
||||
|
||||
systemctl restart pveproxy pvedaemon pvestatd 2>/dev/null || true
|
||||
|
||||
register_tool "subscription_banner" false
|
||||
|
||||
if [[ "$restored" == true ]]; then
|
||||
msg_ok "$(translate "Subscription banner restored successfully (desktop and mobile)")"
|
||||
msg_ok "$(translate "Clear your browser cache and refresh to see the subscription banner again")"
|
||||
else
|
||||
msg_error "$(translate "Failed to restore subscription banner completely")"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
################################################################
|
||||
|
||||
uninstall_time_sync() {
|
||||
@@ -245,8 +453,9 @@ EOF
|
||||
|
||||
uninstall_logrotate() {
|
||||
msg_info "$(translate "Restoring original logrotate configuration...")"
|
||||
|
||||
# Restore from backup if it exists
|
||||
|
||||
[ -f /etc/logrotate.d/pveproxy ] && rm -f /etc/logrotate.d/pveproxy
|
||||
|
||||
if [ -f /etc/logrotate.conf.bak ]; then
|
||||
mv /etc/logrotate.conf.bak /etc/logrotate.conf
|
||||
systemctl restart logrotate >/dev/null 2>&1
|
||||
@@ -364,37 +573,30 @@ uninstall_apt_ipv4() {
|
||||
uninstall_network_optimization() {
|
||||
msg_info "$(translate "Removing network optimizations...")"
|
||||
|
||||
# Remove ProxMenux network configuration
|
||||
rm -f /etc/sysctl.d/99-network.conf
|
||||
|
||||
# Remove interfaces.d source line if we added it
|
||||
|
||||
local interfaces_file="/etc/network/interfaces"
|
||||
if [ -f "$interfaces_file" ]; then
|
||||
# Only remove if it's the last line and looks like our addition
|
||||
if tail -1 "$interfaces_file" | grep -q "^source /etc/network/interfaces.d/\*$"; then
|
||||
sed -i '$d' "$interfaces_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Reload sysctl
|
||||
sysctl --system >/dev/null 2>&1
|
||||
rm -f /etc/sysctl.d/97-proxmenux-fwbr.conf \
|
||||
/etc/sysctl.d/98-proxmenux-rpf.conf
|
||||
|
||||
systemctl disable --now proxmenux-fwbr-tune.service >/dev/null 2>&1 || true
|
||||
rm -f /etc/systemd/system/proxmenux-fwbr-tune.service
|
||||
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
sysctl --system >/dev/null 2>&1 || true
|
||||
|
||||
|
||||
msg_ok "$(translate "Network optimizations removed")"
|
||||
register_tool "network_optimization" false
|
||||
}
|
||||
|
||||
################################################################
|
||||
|
||||
uninstall_disable_rpc() {
|
||||
msg_info "$(translate "Re-enabling RPC services...")"
|
||||
|
||||
# Re-enable and start rpcbind
|
||||
systemctl enable rpcbind >/dev/null 2>&1
|
||||
systemctl start rpcbind >/dev/null 2>&1
|
||||
|
||||
msg_ok "$(translate "RPC services re-enabled")"
|
||||
register_tool "disable_rpc" false
|
||||
}
|
||||
|
||||
################################################################
|
||||
|
||||
@@ -425,48 +627,81 @@ uninstall_bashrc_custom() {
|
||||
################################################################
|
||||
|
||||
uninstall_log2ram() {
|
||||
if [[ ! -f /etc/log2ram.conf ]] && ! systemctl list-units --all | grep -q log2ram; then
|
||||
msg_warn "$(translate "log2ram is not installed.")"
|
||||
return 0
|
||||
fi
|
||||
msg_info "$(translate "Uninstalling log2ram (all versions)...")"
|
||||
|
||||
msg_info "$(translate "Uninstalling log2ram...")"
|
||||
systemctl stop log2ram log2ram-daily.timer log2ram-daily.service >/dev/null 2>&1 || true
|
||||
systemctl disable log2ram log2ram-daily.timer log2ram-daily.service >/dev/null 2>&1 || true
|
||||
|
||||
# Stop and disable services and timers
|
||||
systemctl stop log2ram >/dev/null 2>&1
|
||||
systemctl disable log2ram >/dev/null 2>&1
|
||||
systemctl stop log2ram-daily.timer >/dev/null 2>&1
|
||||
systemctl disable log2ram-daily.timer >/dev/null 2>&1
|
||||
rm -f /etc/cron.d/log2ram \
|
||||
/etc/cron.d/log2ram-auto-sync \
|
||||
/etc/cron.d/log2ram-sync \
|
||||
/etc/cron.hourly/log2ram \
|
||||
/etc/cron.daily/log2ram \
|
||||
/etc/cron.weekly/log2ram \
|
||||
/etc/cron.monthly/log2ram 2>/dev/null || true
|
||||
|
||||
# Remove cron jobs
|
||||
rm -f /etc/cron.d/log2ram
|
||||
rm -f /etc/cron.d/log2ram-auto-sync
|
||||
rm -f /usr/local/bin/log2ram-check.sh \
|
||||
/usr/local/bin/log2ram \
|
||||
/usr/local/bin/log2ram-sync \
|
||||
/usr/sbin/log2ram \
|
||||
/usr/bin/log2ram 2>/dev/null || true
|
||||
|
||||
# Remove config and binaries
|
||||
rm -f /usr/local/bin/log2ram-check.sh
|
||||
rm -f /usr/sbin/log2ram
|
||||
rm -f /etc/log2ram.conf*
|
||||
rm -f /etc/systemd/system/log2ram.service
|
||||
rm -f /etc/systemd/system/log2ram-daily.timer
|
||||
rm -f /etc/systemd/system/log2ram-daily.service
|
||||
rm -f /etc/systemd/system/log2ram.service \
|
||||
/etc/systemd/system/log2ram-daily.timer \
|
||||
/etc/systemd/system/log2ram-daily.service \
|
||||
/etc/systemd/system/sysinit.target.wants/log2ram.service \
|
||||
/etc/systemd/system/timers.target.wants/log2ram-daily.timer \
|
||||
/lib/systemd/system/log2ram.service \
|
||||
/lib/systemd/system/log2ram-daily.timer \
|
||||
/lib/systemd/system/log2ram-daily.service 2>/dev/null || true
|
||||
rm -rf /etc/systemd/system/log2ram.service.d 2>/dev/null || true
|
||||
|
||||
# Clean up log2ram mount if active
|
||||
if [ -d /var/log.hdd ]; then
|
||||
if [ -d /var/log ] && mountpoint -q /var/log; then
|
||||
rsync -a /var/log/ /var/log.hdd/ >/dev/null 2>&1
|
||||
umount /var/log >/dev/null 2>&1
|
||||
rm -f /etc/log2ram.conf \
|
||||
/etc/log2ram.conf.dpkg-old \
|
||||
/etc/log2ram.conf.bak \
|
||||
/etc/log2ram.conf.save 2>/dev/null || true
|
||||
|
||||
rm -rf /etc/logrotate.d/log2ram 2>/dev/null || true
|
||||
|
||||
if mountpoint -q /var/log 2>/dev/null; then
|
||||
if [[ -d /var/log.hdd ]]; then
|
||||
msg_info "$(translate "Preserving logs to /var/log.hdd before unmounting...")"
|
||||
rsync -a /var/log/ /var/log.hdd/ >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -rf /var/log.hdd
|
||||
umount /var/log >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
systemctl daemon-reexec >/dev/null 2>&1
|
||||
systemctl daemon-reload >/dev/null 2>&1
|
||||
[[ -d /var/log.hdd ]] && rm -rf /var/log.hdd
|
||||
[[ -d /tmp/log2ram ]] && rm -rf /tmp/log2ram
|
||||
[[ -d /var/hdd.log ]] && rm -rf /var/hdd.log
|
||||
[[ -f /tmp/log2ram_install.log ]] && rm -f /tmp/log2ram_install.log
|
||||
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl reset-failed >/dev/null 2>&1 || true
|
||||
systemctl restart cron >/dev/null 2>&1 || true
|
||||
|
||||
if dpkg -l 2>/dev/null | grep -q '^ii log2ram'; then
|
||||
msg_info "$(translate "Purging log2ram apt package...")"
|
||||
apt-get purge -y log2ram >/dev/null 2>&1 || true
|
||||
apt-get autoremove -y >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [[ -f /etc/log2ram.conf ]] || \
|
||||
command -v log2ram >/dev/null 2>&1 || \
|
||||
systemctl list-units --all 2>/dev/null | grep -q log2ram || \
|
||||
[[ -f /etc/cron.d/log2ram-auto-sync ]]; then
|
||||
msg_warn "$(translate "Some log2ram files may still exist. Manual cleanup may be required.")"
|
||||
else
|
||||
msg_ok "$(translate "log2ram completely removed from system")"
|
||||
fi
|
||||
|
||||
msg_ok "$(translate "log2ram completely removed")"
|
||||
register_tool "log2ram" false
|
||||
NECESSARY_REBOOT=1
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
################################################################
|
||||
|
||||
uninstall_persistent_network() {
|
||||
@@ -485,6 +720,7 @@ uninstall_persistent_network() {
|
||||
msg_ok "$(translate "Removed all .link files from") $LINK_DIR"
|
||||
msg_info "$(translate "Interface names will return to default systemd behavior.")"
|
||||
register_tool "persistent_network" false
|
||||
NECESSARY_REBOOT=1
|
||||
}
|
||||
|
||||
|
||||
@@ -583,7 +819,6 @@ migrate_installed_tools() {
|
||||
return
|
||||
fi
|
||||
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_info "$(translate 'Detecting previous optimizations...')"
|
||||
|
||||
@@ -673,7 +908,7 @@ show_uninstall_menu() {
|
||||
case "$tool" in
|
||||
lvm_repair) desc="LVM PV Headers Repair";;
|
||||
repo_cleanup) desc="Repository Cleanup";;
|
||||
apt_upgrade) desc="APT Upgrade & Repository Config";;
|
||||
#apt_upgrade) desc="APT Upgrade & Repository Config";;
|
||||
subscription_banner) desc="Subscription Banner Removal";;
|
||||
time_sync) desc="Time Synchronization";;
|
||||
apt_languages) desc="APT Language Skip";;
|
||||
@@ -686,7 +921,6 @@ show_uninstall_menu() {
|
||||
apt_ipv4) desc="APT IPv4 Force";;
|
||||
kexec) desc="kexec for quick reboots";;
|
||||
network_optimization) desc="Network Optimizations";;
|
||||
disable_rpc) desc="RPC/rpcbind Disable";;
|
||||
bashrc_custom) desc="Bashrc Customization";;
|
||||
figurine) desc="Figurine";;
|
||||
fastfetch) desc="Fastfetch";;
|
||||
@@ -729,6 +963,9 @@ show_uninstall_menu() {
|
||||
|
||||
msg_success "$(translate "Selected optimizations have been uninstalled.")"
|
||||
msg_warn "$(translate "A system reboot is recommended to ensure all changes take effect.")"
|
||||
echo -e
|
||||
msg_success "$(translate "Press Enter to continue...")"
|
||||
read -r
|
||||
|
||||
if dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Reboot Recommended")" \
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu CT - NFS Client Manager for Proxmox LXC
|
||||
# ProxMenux CT - NFS Client Manager for Proxmox LXC
|
||||
# ==========================================================
|
||||
# Based on ProxMenux by MacRimi
|
||||
# ==========================================================
|
||||
|
||||