Compare commits
319 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be03035574 | |||
| 619f3ca700 | |||
| 8553e63338 | |||
| be4d9fe24b | |||
| 497f727b08 | |||
| 74a7569f4c | |||
| 66185e3b91 | |||
| 1b2beda695 | |||
| feb3b5ef5f | |||
| b2439331b3 | |||
| f1000afc27 | |||
| 5fc2a82423 | |||
| a27f884418 | |||
| cae4b73226 | |||
| 50ed293de2 | |||
| 616b772a45 | |||
| c1d00e21db | |||
| 2e8e2b61d3 | |||
| ba595c9719 | |||
| e392f6a2b7 | |||
| 7457e71776 | |||
| 982d0dd72e | |||
| d345f96518 | |||
| 469874e975 | |||
| 6ba817cd43 | |||
| 42f2e69e3a | |||
| 12442b4bd3 | |||
| 305d37a13b | |||
| 4baf60174f | |||
| 8cd1ac6a4b | |||
| c65fef638e | |||
| a030cd7e28 | |||
| 59a3b7eac5 | |||
| 2faac48adf | |||
| 3883039764 | |||
| 307ed0c637 | |||
| 96ffdb65d0 | |||
| 5ca55798b2 | |||
| cd32e11c6d | |||
| 774cbe4c9d | |||
| 1d0bb20506 | |||
| 8064e107f4 | |||
| c1d1121ed1 | |||
| 07603f11db | |||
| ec22c857d5 | |||
| 364e808261 | |||
| 1d47ad0c4b | |||
| c9d0eac6cc | |||
| 97fc72b78a | |||
| 7b1111430b | |||
| 4f3306cd0f | |||
| d3f7056ece | |||
| 9f3286c570 | |||
| 16fc737b2d | |||
| 3e0ae709d9 | |||
| 39ddb7c8f9 | |||
| 3c509ce0e4 | |||
| 048cf2fb8f | |||
| 0a20821c41 | |||
| e0eaf6267f | |||
| 3ddf98277f | |||
| 85294bcd33 | |||
| acff4523f3 | |||
| bf71e1f9b8 | |||
| f0bcdc1c25 | |||
| 43526c58bd | |||
| ce3c7a545e | |||
| 9498e4e7eb | |||
| 4ec7c207f4 | |||
| 000479463f | |||
| 6b2065e43c | |||
| e97e1363ae | |||
| 697a1f8e31 | |||
| 035f43311a | |||
| c597f1252e | |||
| cc1e7a715c | |||
| 80057e3014 | |||
| 79ffba873f | |||
| 673e1cf212 | |||
| 7e878ecff2 | |||
| 88cf51a602 | |||
| 1860fffe07 | |||
| fa925543db | |||
| 825e99c59b | |||
| 955bed80fb | |||
| 03b9ac3ec4 | |||
| c255d9a5d8 | |||
| 401d973a51 | |||
| a507d559e1 | |||
| 9225982ca5 | |||
| 6f831530cc | |||
| e6b4443074 | |||
| 1c800cbd8f | |||
| a65924799e | |||
| adbfa1e73e | |||
| 44a4226ad2 | |||
| 07ca3f13a0 | |||
| 87a052b89c | |||
| 2216543ac3 | |||
| 4254d57d12 | |||
| 30d93898d8 | |||
| 4c7ed2c2c5 | |||
| 4fb327cef8 | |||
| 588af3613b | |||
| 5b5f325a4e | |||
| ae62196dff | |||
| 27e66ee770 | |||
| 8fb8134898 | |||
| a59489f804 | |||
| cbf3938784 | |||
| c45ebfe598 | |||
| a75aad1fdc | |||
| a0635a1026 | |||
| 27353e160f | |||
| b9619efbbf | |||
| 1712d32ef7 | |||
| 014deb2118 | |||
| 6077cf81f2 | |||
| 0422c38096 | |||
| 8c902ae04d | |||
| 0a0b916067 | |||
| 6822635a0b | |||
| f9b15fd110 | |||
| 131a458e69 | |||
| 7260807d78 | |||
| df83d8a3e5 | |||
| 0f45424458 | |||
| 60f92d019b | |||
| 2189487982 | |||
| 3a44997795 | |||
| 1f04134aac | |||
| ce44538240 | |||
| 5fd53883be | |||
| f064cc89ba | |||
| 5dd8b3ee36 | |||
| f2f9c37ee2 | |||
| 6836777629 | |||
| beefdd280f | |||
| 4b9ad0da7a | |||
| f9fdd1686c | |||
| 25fc3d931e | |||
| fc7d0f2cd5 | |||
| 60c91d9fe4 | |||
| cc2d6849a8 | |||
| 4a5379ea42 | |||
| ba84c644df | |||
| 37217b4219 | |||
| 41dab03a5f | |||
| 6e48bf2a71 | |||
| 1c1c6f513c | |||
| 49c54f5593 | |||
| d083e49d0b | |||
| 8dc2b833f4 | |||
| 7d5726be50 | |||
| 246c1674d1 | |||
| 06b81f2b64 | |||
| ee57797890 | |||
| a94000e114 | |||
| e6655b35f3 | |||
| 696ffde184 | |||
| 9e74e99923 | |||
| bc5c6dadfb | |||
| 0d173a0bfe | |||
| cd78920edd | |||
| 9a7ec62cf9 | |||
| 2b4580cfe8 | |||
| b790c06294 | |||
| 61d87b46d9 | |||
| 143cb4cbab | |||
| 22709dac36 | |||
| d97be93449 | |||
| 5864de7dea | |||
| 4ea5890e92 | |||
| 876d51b009 | |||
| 5b0d55c1a2 | |||
| 3ddb1421c3 | |||
| 58f9a7bc02 | |||
| e8e4b728ce | |||
| 0a4868192d | |||
| 9d81ffffe8 | |||
| e6fe4a09e5 | |||
| 77c5ad7b09 | |||
| b850e9615a | |||
| c2ea307821 | |||
| fb588c0d60 | |||
| fecbdf6190 | |||
| bbbbf6892f | |||
| e1a11053a6 | |||
| f0a62191ea | |||
| a8311923fb | |||
| cd1d88760d | |||
| 004949d3a0 | |||
| f6d26042da | |||
| 270a73a470 | |||
| 018e80e59d | |||
| cb5cb1e594 | |||
| 6c5eb156a1 | |||
| 8abef33840 | |||
| 1d6b8951e8 | |||
| 711d57d91f | |||
| 65fd847251 | |||
| 73a170a5f1 | |||
| 9a32d1c0f7 | |||
| 59918032c6 | |||
| 55394cbf09 | |||
| 83dcc0c4f2 | |||
| b4b93f0572 | |||
| ab0e59215c | |||
| 5669ce207c | |||
| 37f6cd96a4 | |||
| c0ec74fb12 | |||
| 226dc45190 | |||
| 11e3f53a2f | |||
| 31d7f7e3e9 | |||
| 128edc08e2 | |||
| 5158c5f359 | |||
| a70b33ce13 | |||
| d787c3caa0 | |||
| a554af939e | |||
| 06604ff0d1 | |||
| 9490f79c6d | |||
| 311a624698 | |||
| 3e2e77f9fb | |||
| b2e02cd0e7 | |||
| 87ead71766 | |||
| b8517a5b3e | |||
| c29cdf44fb | |||
| 4b2ab2894a | |||
| c9a01ab5ad | |||
| 90d1046312 | |||
| 14e749a18d | |||
| a4be1af0ef | |||
| f4185d0a2a | |||
| ffb8324b5a | |||
| 6df44f1632 | |||
| 9570819f59 | |||
| f2afc94ed2 | |||
| 050b95946c | |||
| e33ef92334 | |||
| 0d7ff46aec | |||
| 042913e080 | |||
| 98bc8be642 | |||
| 2c6d2f4255 | |||
| 6293556837 | |||
| 641721d199 | |||
| 036a2b9014 | |||
| 9ad092d340 | |||
| d24884f651 | |||
| fa1c498716 | |||
| 25635239d4 | |||
| c816688de3 | |||
| 43d79bd1e9 | |||
| ba88c7b0f6 | |||
| 4359d92ffe | |||
| 16c7513e82 | |||
| 4572478ad8 | |||
| 02b5cd61bd | |||
| bff07311b2 | |||
| 44cc89b9d5 | |||
| fa1e6c6c64 | |||
| 4ebbdb284b | |||
| 51302a7c5a | |||
| ba984592ed | |||
| 60a97e5815 | |||
| 3275a1ecb4 | |||
| af72c7a2d3 | |||
| c07ada1fc4 | |||
| c19c8f9c5d | |||
| 43fe7ae7db | |||
| 22916868df | |||
| 7d00ff8869 | |||
| 4ea2088485 | |||
| e421b40093 | |||
| a9dd7562ac | |||
| 8f62ed67d3 | |||
| cfd89a14f7 | |||
| 55011842f5 | |||
| 3079a3f51c | |||
| c4ec390ca0 | |||
| f99b7f3589 | |||
| 887b170c0e | |||
| c696cfd8d8 | |||
| 25966973a2 | |||
| d0a57d4b7c | |||
| 9341b49fd1 | |||
| bbc3c922a6 | |||
| 17b8d63e6c | |||
| c751a8168a | |||
| f2509dbe5d | |||
| 6d44c22982 | |||
| 4bed489610 | |||
| 8edf488636 | |||
| 8fe7d249f8 | |||
| 6ed14e1d3c | |||
| a5459acdaf | |||
| 61cd198d35 | |||
| 49ea2b304d | |||
| 27231d1764 | |||
| 8744620220 | |||
| 4590be6d42 | |||
| fa93b43c32 | |||
| 3c47f84a24 | |||
| 8a371c26de | |||
| 088a594468 | |||
| c551913551 | |||
| 05e81053e0 | |||
| 981c0ab980 | |||
| 1f083b335f | |||
| 10603900df | |||
| c22e36d219 | |||
| 26fc2ae9db | |||
| 2a0b298ae5 | |||
| 96f0a9bc5d | |||
| 5054e78864 | |||
| f1fa6b03d5 | |||
| 8f15bf9668 | |||
| 4b8e7b19a3 | |||
| 67bba1dd09 | |||
| b826dec79d |
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Report a problem in the project
|
||||||
|
title: "[BUG] Describe the issue"
|
||||||
|
labels: bug
|
||||||
|
assignees: 'MacRimi'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Describe the bug clearly and concisely.
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
1. ...
|
||||||
|
2. ...
|
||||||
|
3. ...
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
What should happen?
|
||||||
|
|
||||||
|
## Screenshots (Required)
|
||||||
|
Add images to help illustrate the issue.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
- Operating system:
|
||||||
|
- Software version:
|
||||||
|
- Other relevant details:
|
||||||
|
|
||||||
|
## Additional Information
|
||||||
|
Add any other context about the problem here.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Soporte General
|
||||||
|
url: https://github.com/MacRimi/ProxMenux/discussions
|
||||||
|
about: If your request is neither a bug nor a feature, please use Discussions.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest a new feature or improvement
|
||||||
|
title: "[FEATURE] Describe your proposal"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: 'MacRimi'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Explain the feature you are proposing.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
Why is this improvement important? What problem does it solve?
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
Are there other solutions you have thought about?
|
||||||
|
|
||||||
|
## Additional Information
|
||||||
|
Add any extra details that help understand your proposal.
|
||||||
@@ -1,76 +1,177 @@
|
|||||||
import requests, json
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# GitHub API URL to fetch all .json files describing scripts
|
import requests
|
||||||
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
|
|
||||||
|
|
||||||
# Base path to build the full URL for the installable scripts
|
# ---------- Config ----------
|
||||||
|
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
|
||||||
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||||
|
|
||||||
# Output file where the consolidated helper scripts cache will be stored
|
# Escribimos siempre en <raiz_repo>/json/helpers_cache.json, independientemente del cwd
|
||||||
OUTPUT_FILE = Path("json/helpers_cache.json")
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
OUTPUT_FILE = REPO_ROOT / "json" / "helpers_cache.json"
|
||||||
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
res = requests.get(API_URL)
|
|
||||||
data = res.json()
|
|
||||||
cache = []
|
|
||||||
|
|
||||||
# Loop over each file in the JSON directory
|
def to_mirror_url(raw_url: str) -> str:
|
||||||
for item in data:
|
"""
|
||||||
url = item.get("download_url")
|
Convierte una URL raw de GitHub al raw del mirror.
|
||||||
if not url or not url.endswith(".json"):
|
GH : https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/docker.sh
|
||||||
continue
|
MIR: https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/main/ct/docker.sh
|
||||||
|
"""
|
||||||
|
m = re.match(r"^https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)$", raw_url or "")
|
||||||
|
if not m:
|
||||||
|
return ""
|
||||||
|
org, repo, branch, path = m.groups()
|
||||||
|
if org.lower() != "community-scripts" or repo != "ProxmoxVE":
|
||||||
|
return ""
|
||||||
|
return f"https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/{branch}/{path}"
|
||||||
|
|
||||||
|
|
||||||
|
def guess_os_from_script_path(script_path: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Heurística suave cuando el JSON no publica resources.os:
|
||||||
|
- tools/pve/* -> proxmox
|
||||||
|
- ct/alpine-* -> alpine
|
||||||
|
- tools/addon/* -> generic (suele ejecutarse sobre LXC existente)
|
||||||
|
- ct/* -> debian (por defecto para CTs)
|
||||||
|
"""
|
||||||
|
if not script_path:
|
||||||
|
return None
|
||||||
|
if script_path.startswith("tools/pve/") or script_path == "tools/pve/host-backup.sh" or script_path.startswith("vm/"):
|
||||||
|
return "proxmox"
|
||||||
|
if "/alpine-" in script_path or script_path.startswith("ct/alpine-"):
|
||||||
|
return "alpine"
|
||||||
|
if script_path.startswith("tools/addon/"):
|
||||||
|
return "generic"
|
||||||
|
if script_path.startswith("ct/"):
|
||||||
|
return "debian"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_directory_json(api_url: str) -> list[dict]:
|
||||||
|
r = requests.get(api_url, timeout=30)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
if not isinstance(data, list):
|
||||||
|
raise RuntimeError("GitHub API no devolvió una lista.")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
try:
|
try:
|
||||||
raw = requests.get(url).json()
|
directory = fetch_directory_json(API_URL)
|
||||||
if not isinstance(raw, dict):
|
except Exception as e:
|
||||||
|
print(f"ERROR: No se pudo leer el índice de JSONs: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
cache: list[dict] = []
|
||||||
|
seen: set[tuple[str, str]] = set() # (slug, script) para evitar duplicados
|
||||||
|
|
||||||
|
total_items = len(directory)
|
||||||
|
processed = 0
|
||||||
|
kept = 0
|
||||||
|
|
||||||
|
for item in directory:
|
||||||
|
url = item.get("download_url")
|
||||||
|
name_in_dir = item.get("name", "")
|
||||||
|
if not url or not url.endswith(".json"):
|
||||||
continue
|
continue
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Extract fields required to identify a valid helper script
|
try:
|
||||||
name = raw.get("name", "")
|
raw = requests.get(url, timeout=30).json()
|
||||||
slug = raw.get("slug")
|
if not isinstance(raw, dict):
|
||||||
type_ = raw.get("type", "")
|
continue
|
||||||
script = raw.get("install_methods", [{}])[0].get("script", "")
|
except Exception:
|
||||||
if not slug or not script:
|
print(f"❌ Error al obtener/parsing {name_in_dir}", file=sys.stderr)
|
||||||
continue # Skip if it's not a valid script
|
continue
|
||||||
|
|
||||||
desc = raw.get("description", "")
|
processed += 1
|
||||||
categories = raw.get("categories", [])
|
|
||||||
notes = [note.get("text", "") for note in raw.get("notes", []) if isinstance(note, dict)]
|
name = raw.get("name", "")
|
||||||
full_script_url = f"{SCRIPT_BASE}/{script}"
|
slug = raw.get("slug")
|
||||||
|
type_ = raw.get("type", "")
|
||||||
|
desc = raw.get("description", "")
|
||||||
|
categories = raw.get("categories", [])
|
||||||
|
notes = [n.get("text", "") for n in raw.get("notes", []) if isinstance(n, dict)]
|
||||||
|
|
||||||
|
# Credenciales (si existen, se copian tal cual)
|
||||||
|
credentials = raw.get("default_credentials", {})
|
||||||
|
cred_username = credentials.get("username") if isinstance(credentials, dict) else None
|
||||||
|
cred_password = credentials.get("password") if isinstance(credentials, dict) else None
|
||||||
|
add_credentials = any([
|
||||||
|
cred_username not in (None, ""),
|
||||||
|
cred_password not in (None, "")
|
||||||
|
])
|
||||||
|
|
||||||
|
install_methods = raw.get("install_methods", [])
|
||||||
|
if not isinstance(install_methods, list) or not install_methods:
|
||||||
|
# Sin install_methods válidos -> continuamos
|
||||||
|
continue
|
||||||
|
|
||||||
|
for im in install_methods:
|
||||||
|
if not isinstance(im, dict):
|
||||||
|
continue
|
||||||
|
script = im.get("script", "")
|
||||||
|
if not script:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# OS desde resources u heurística
|
||||||
|
resources = im.get("resources", {}) if isinstance(im, dict) else {}
|
||||||
|
os_name = resources.get("os") if isinstance(resources, dict) else None
|
||||||
|
if not os_name:
|
||||||
|
os_name = guess_os_from_script_path(script)
|
||||||
|
if isinstance(os_name, str):
|
||||||
|
os_name = os_name.strip().lower()
|
||||||
|
|
||||||
|
full_script_url = f"{SCRIPT_BASE}/{script}"
|
||||||
|
script_url_mirror = to_mirror_url(full_script_url)
|
||||||
|
|
||||||
|
key = (slug or "", script)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
"name": name,
|
||||||
|
"slug": slug,
|
||||||
|
"desc": desc,
|
||||||
|
"script": script,
|
||||||
|
"script_url": full_script_url,
|
||||||
|
"script_url_mirror": script_url_mirror, # nuevo
|
||||||
|
"os": os_name, # nuevo
|
||||||
|
"categories": categories,
|
||||||
|
"notes": notes,
|
||||||
|
"type": type_,
|
||||||
|
}
|
||||||
|
if add_credentials:
|
||||||
|
entry["default_credentials"] = {
|
||||||
|
"username": cred_username,
|
||||||
|
"password": cred_password,
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.append(entry)
|
||||||
|
kept += 1
|
||||||
|
|
||||||
|
# Progreso ligero
|
||||||
|
print(f"[{kept:03d}] {slug or name:<24} → {script:<28} os={os_name or 'n/a'} src={'GH+MR' if script_url_mirror else 'GH'}")
|
||||||
|
|
||||||
|
# Orden estable para commits reproducibles
|
||||||
|
cache.sort(key=lambda x: (x.get("slug") or "", x.get("script") or ""))
|
||||||
|
|
||||||
|
with OUTPUT_FILE.open("w", encoding="utf-8") as f:
|
||||||
|
json.dump(cache, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
print(f"\n✅ helpers_cache.json → {OUTPUT_FILE}")
|
||||||
|
print(f" Total JSON en índice: {total_items}")
|
||||||
|
print(f" Procesados: {processed} | Guardados: {kept} | Únicos (slug,script): {len(seen)}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
credentials = raw.get("default_credentials", {})
|
if __name__ == "__main__":
|
||||||
cred_username = credentials.get("username")
|
sys.exit(main())
|
||||||
cred_password = credentials.get("password")
|
|
||||||
|
|
||||||
add_credentials = (
|
|
||||||
(cred_username is not None and str(cred_username).strip() != "") or
|
|
||||||
(cred_password is not None and str(cred_password).strip() != "")
|
|
||||||
)
|
|
||||||
|
|
||||||
entry = {
|
|
||||||
"name": name,
|
|
||||||
"slug": slug,
|
|
||||||
"desc": desc,
|
|
||||||
"script": script,
|
|
||||||
"script_url": full_script_url,
|
|
||||||
"categories": categories,
|
|
||||||
"notes": notes,
|
|
||||||
"type": type_
|
|
||||||
}
|
|
||||||
if add_credentials:
|
|
||||||
entry["default_credentials"] = {
|
|
||||||
"username": cred_username,
|
|
||||||
"password": cred_password
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.append(entry)
|
|
||||||
|
|
||||||
|
|
||||||
# Write the JSON cache to disk
|
|
||||||
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(cache, f, indent=2)
|
|
||||||
|
|
||||||
print(f"✅ helpers_cache.json created at {OUTPUT_FILE} with {len(cache)} valid scripts.")
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import requests, json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# GitHub API URL to fetch all .json files describing scripts
|
||||||
|
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
|
||||||
|
|
||||||
|
# Base path to build the full URL for the installable scripts
|
||||||
|
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||||
|
|
||||||
|
# Output file where the consolidated helper scripts cache will be stored
|
||||||
|
OUTPUT_FILE = Path("json/helpers_cache.json")
|
||||||
|
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
res = requests.get(API_URL)
|
||||||
|
data = res.json()
|
||||||
|
cache = []
|
||||||
|
|
||||||
|
# Loop over each file in the JSON directory
|
||||||
|
for item in data:
|
||||||
|
url = item.get("download_url")
|
||||||
|
if not url or not url.endswith(".json"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
raw = requests.get(url).json()
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extract fields required to identify a valid helper script
|
||||||
|
name = raw.get("name", "")
|
||||||
|
slug = raw.get("slug")
|
||||||
|
type_ = raw.get("type", "")
|
||||||
|
script = raw.get("install_methods", [{}])[0].get("script", "")
|
||||||
|
if not slug or not script:
|
||||||
|
continue # Skip if it's not a valid script
|
||||||
|
|
||||||
|
desc = raw.get("description", "")
|
||||||
|
categories = raw.get("categories", [])
|
||||||
|
notes = [note.get("text", "") for note in raw.get("notes", []) if isinstance(note, dict)]
|
||||||
|
full_script_url = f"{SCRIPT_BASE}/{script}"
|
||||||
|
|
||||||
|
|
||||||
|
credentials = raw.get("default_credentials", {})
|
||||||
|
cred_username = credentials.get("username")
|
||||||
|
cred_password = credentials.get("password")
|
||||||
|
|
||||||
|
add_credentials = (
|
||||||
|
(cred_username is not None and str(cred_username).strip() != "") or
|
||||||
|
(cred_password is not None and str(cred_password).strip() != "")
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
"name": name,
|
||||||
|
"slug": slug,
|
||||||
|
"desc": desc,
|
||||||
|
"script": script,
|
||||||
|
"script_url": full_script_url,
|
||||||
|
"categories": categories,
|
||||||
|
"notes": notes,
|
||||||
|
"type": type_
|
||||||
|
}
|
||||||
|
if add_credentials:
|
||||||
|
entry["default_credentials"] = {
|
||||||
|
"username": cred_username,
|
||||||
|
"password": cred_password
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.append(entry)
|
||||||
|
|
||||||
|
|
||||||
|
# Write the JSON cache to disk
|
||||||
|
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(cache, f, indent=2)
|
||||||
|
|
||||||
|
print(f"✅ helpers_cache.json created at {OUTPUT_FILE} with {len(cache)} valid scripts.")
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
name: Build ProxMenux Monitor AppImage
|
||||||
|
|
||||||
|
on:
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: AppImage
|
||||||
|
run: npm install --legacy-peer-deps
|
||||||
|
|
||||||
|
- name: Build Next.js app
|
||||||
|
working-directory: AppImage
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y python3 python3-pip python3-venv
|
||||||
|
|
||||||
|
- name: Make build script executable
|
||||||
|
working-directory: AppImage
|
||||||
|
run: chmod +x scripts/build_appimage.sh
|
||||||
|
|
||||||
|
- name: Build AppImage
|
||||||
|
working-directory: AppImage
|
||||||
|
run: ./scripts/build_appimage.sh
|
||||||
|
|
||||||
|
- name: Get version from package.json
|
||||||
|
id: version
|
||||||
|
working-directory: AppImage
|
||||||
|
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload AppImage artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
|
||||||
|
path: AppImage/dist/*.AppImage
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Generate SHA256 checksum
|
||||||
|
run: |
|
||||||
|
cd AppImage/dist
|
||||||
|
sha256sum *.AppImage > ProxMenux-Monitor.AppImage.sha256
|
||||||
|
echo "Generated SHA256:"
|
||||||
|
cat ProxMenux-Monitor.AppImage.sha256
|
||||||
|
|
||||||
|
- name: Upload AppImage and checksum to /AppImage folder in main
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
git fetch origin main
|
||||||
|
git checkout main
|
||||||
|
|
||||||
|
rm -f AppImage/*.AppImage AppImage/*.sha256 || true
|
||||||
|
|
||||||
|
# Copy new files
|
||||||
|
cp AppImage/dist/*.AppImage AppImage/
|
||||||
|
cp AppImage/dist/ProxMenux-Monitor.AppImage.sha256 AppImage/
|
||||||
|
|
||||||
|
git add AppImage/*.AppImage AppImage/*.sha256
|
||||||
|
git commit -m "Update AppImage build ($(date +'%Y-%m-%d %H:%M:%S'))" || echo "No changes to commit"
|
||||||
|
git push origin main
|
||||||
@@ -8,10 +8,7 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
paths: [ 'AppImage/**' ]
|
paths: [ 'AppImage/**' ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
@@ -57,30 +54,3 @@ jobs:
|
|||||||
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
|
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
|
||||||
path: AppImage/dist/*.AppImage
|
path: AppImage/dist/*.AppImage
|
||||||
retention-days: 30
|
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
|
|
||||||
|
|||||||
@@ -51,3 +51,5 @@ Thumbs.db
|
|||||||
!guides/
|
!guides/
|
||||||
!web/
|
!web/
|
||||||
|
|
||||||
|
# GitHub authentication
|
||||||
|
.github/auth.sh
|
||||||
|
|||||||
Binary file not shown.
@@ -1 +1 @@
|
|||||||
e896eb10de4bf990d31c1d8357289f64cbce481921647f2be53efb850d0b73b2 ProxMenux-1.0.0.AppImage
|
f35de512c1a19843d15a9a3263a5104759d041ffc9d01249450babe0b0c3f889 ProxMenux-1.0.1.AppImage
|
||||||
|
|||||||
+794
-23
@@ -2,40 +2,811 @@
|
|||||||
|
|
||||||
A modern, responsive dashboard for monitoring Proxmox VE systems built with Next.js and React.
|
A modern, responsive dashboard for monitoring Proxmox VE systems built with Next.js and React.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Technology Stack](#technology-stack)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Authentication & Security](#authentication--security)
|
||||||
|
- [Setup Authentication](#setup-authentication)
|
||||||
|
- [Two-Factor Authentication (2FA)](#two-factor-authentication-2fa)
|
||||||
|
- [Security Best Practices for API Tokens](#security-best-practices-for-api-tokens)
|
||||||
|
- [API Documentation](#api-documentation)
|
||||||
|
- [API Authentication](#api-authentication)
|
||||||
|
- [Generating API Tokens](#generating-api-tokens)
|
||||||
|
- [Available Endpoints](#available-endpoints)
|
||||||
|
- [Integration Examples](#integration-examples)
|
||||||
|
- [Homepage Integration](#homepage-integration)
|
||||||
|
- [Home Assistant Integration](#home-assistant-integration)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**ProxMenux Monitor** is a comprehensive, real-time monitoring dashboard for Proxmox VE environments. Built with modern web technologies, it provides an intuitive interface to monitor system resources, virtual machines, containers, storage, network traffic, and system logs.
|
||||||
|
|
||||||
|
The application runs as a standalone AppImage on your Proxmox server and serves a web interface accessible from any device on your network.
|
||||||
|
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
Get a quick overview of ProxMenux Monitor's main features:
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="public/images/onboarding/imagen1.png" alt="Overview Dashboard" width="800"/>
|
||||||
|
<br/>
|
||||||
|
<em>System Overview - Monitor CPU, memory, temperature, and uptime in real-time</em>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="public/images/onboarding/imagen2.png" alt="Storage Management" width="800"/>
|
||||||
|
<br/>
|
||||||
|
<em>Storage Management - Visual representation of disk usage and health</em>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="public/images/onboarding/imagen3.png" alt="Network Monitoring" width="800"/>
|
||||||
|
<br/>
|
||||||
|
<em>Network Monitoring - Real-time traffic graphs and interface statistics</em>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="public/images/onboarding/imagen4.png" alt="Virtual Machines & LXC" width="800"/>
|
||||||
|
<br/>
|
||||||
|
<em>VMs & LXC Containers - Comprehensive view with resource usage and controls</em>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="public/images/onboarding/imagen5.png" alt="Hardware Information" width="800"/>
|
||||||
|
<br/>
|
||||||
|
<em>Hardware Information - Detailed specs for CPU, GPU, and PCIe devices</em>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="public/images/onboarding/imagen6.png" alt="System Logs" width="800"/>
|
||||||
|
<br/>
|
||||||
|
<em>System Logs - Real-time monitoring with filtering and search</em>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **System Overview**: Real-time monitoring of CPU, memory, temperature, and active VMs/LXC containers
|
- **System Overview**: Real-time monitoring of CPU, memory, temperature, and system uptime
|
||||||
- **Storage Management**: Visual representation of storage distribution and disk performance metrics
|
- **Storage Management**: Visual representation of storage distribution, disk health, and SMART data
|
||||||
- **Network Monitoring**: Network interface statistics and performance graphs
|
- **Network Monitoring**: Network interface statistics, real-time traffic graphs, and bandwidth usage
|
||||||
- **Virtual Machines**: Comprehensive view of VMs and LXC containers with resource usage
|
- **Virtual Machines & LXC**: Comprehensive view of all VMs and containers with resource usage and controls
|
||||||
- **System Logs**: Real-time system log monitoring and filtering
|
- **Hardware Information**: Detailed hardware specifications including CPU, GPU, PCIe devices, and disks
|
||||||
|
- **System Logs**: Real-time system log monitoring with filtering and search capabilities
|
||||||
|
- **Health Monitoring**: Proactive system health checks with persistent error tracking
|
||||||
|
- **Authentication & 2FA**: Optional password protection with TOTP-based two-factor authentication
|
||||||
|
- **RESTful API**: Complete API access for integrations with Homepage, Home Assistant, and custom dashboards
|
||||||
- **Dark/Light Theme**: Toggle between themes with Proxmox-inspired design
|
- **Dark/Light Theme**: Toggle between themes with Proxmox-inspired design
|
||||||
- **Responsive Design**: Works seamlessly on desktop and mobile devices
|
- **Responsive Design**: Works seamlessly on desktop, tablet, and mobile devices
|
||||||
- **Onboarding Experience**: Interactive welcome carousel for first-time users
|
- **Release Notes**: Automatic notifications of new features and improvements
|
||||||
|
|
||||||
## Technology Stack
|
## Technology Stack
|
||||||
|
|
||||||
- **Frontend**: Next.js 15, React 19, TypeScript
|
- **Frontend**: Next.js 15, React 19, TypeScript
|
||||||
- **Styling**: Tailwind CSS with custom Proxmox-inspired theme
|
- **Styling**: Tailwind CSS v4 with custom Proxmox-inspired theme
|
||||||
- **Charts**: Recharts for data visualization
|
- **Charts**: Recharts for data visualization
|
||||||
- **UI Components**: Radix UI primitives with shadcn/ui
|
- **UI Components**: Radix UI primitives with shadcn/ui
|
||||||
- **Backend**: Flask server for system data collection
|
- **Backend**: Flask (Python) server for system data collection
|
||||||
- **Packaging**: AppImage for easy distribution
|
- **Packaging**: AppImage for easy distribution and deployment
|
||||||
|
|
||||||
## Onboarding Images
|
## Installation
|
||||||
|
|
||||||
To customize the onboarding experience, place your screenshot images in `public/images/onboarding/`:
|
**ProxMenux Monitor is integrated into [ProxMenux](https://proxmenux.com) and comes enabled by default.** No manual installation is required if you're using ProxMenux.
|
||||||
|
|
||||||
- `imagen1.png` - Overview section screenshot
|
The monitor automatically starts when ProxMenux is installed and runs as a systemd service on your Proxmox server.
|
||||||
- `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:**
|
### Accessing the Dashboard
|
||||||
- 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".
|
You can access ProxMenux Monitor in two ways:
|
||||||
|
|
||||||
|
1. **Direct Access**: `http://your-proxmox-ip:8008`
|
||||||
|
2. **Via Proxy** (Recommended): `https://your-domain.com/proxmenux-monitor/`
|
||||||
|
|
||||||
|
**Note**: All API endpoints work seamlessly with both direct access and proxy configurations. When using a reverse proxy, the application automatically detects and adapts to the proxied environment.
|
||||||
|
|
||||||
|
### Proxy Configuration
|
||||||
|
|
||||||
|
ProxMenux Monitor includes built-in support for reverse proxy configurations. If you're using Nginx, Caddy, or Traefik, the application will automatically:
|
||||||
|
|
||||||
|
- Detect the proxy headers (`X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`)
|
||||||
|
- Adjust API endpoints to work correctly through the proxy
|
||||||
|
- Maintain full functionality for all features including authentication and API access
|
||||||
|
|
||||||
|
**Example Nginx configuration:**
|
||||||
|
```nginx
|
||||||
|
location /proxmenux-monitor/ {
|
||||||
|
proxy_pass http://localhost:8008/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Authentication & Security
|
||||||
|
|
||||||
|
ProxMenux Monitor includes an optional authentication system to protect your dashboard with a password and two-factor authentication.
|
||||||
|
|
||||||
|
### Setup Authentication
|
||||||
|
|
||||||
|
On first launch, you'll be presented with three options:
|
||||||
|
|
||||||
|
1. **Set up authentication** - Create a username and password to protect your dashboard
|
||||||
|
2. **Enable 2FA** - Add TOTP-based two-factor authentication for enhanced security
|
||||||
|
3. **Skip** - Continue without authentication (not recommended for production environments)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Two-Factor Authentication (2FA)
|
||||||
|
|
||||||
|
After setting up your password, you can enable 2FA using any TOTP authenticator app (Google Authenticator, Authy, 1Password, etc.):
|
||||||
|
|
||||||
|
1. Navigate to **Settings > Authentication**
|
||||||
|
2. Click **Enable 2FA**
|
||||||
|
3. Scan the QR code with your authenticator app
|
||||||
|
4. Enter the 6-digit code to verify
|
||||||
|
5. Save your backup codes in a secure location
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Security Best Practices for API Tokens
|
||||||
|
|
||||||
|
**IMPORTANT**: Never hardcode your API tokens directly in configuration files or scripts. Instead, use environment variables or secrets management.
|
||||||
|
|
||||||
|
**Option 1: Environment Variables**
|
||||||
|
|
||||||
|
Store your token in an environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux/macOS - Add to ~/.bashrc or ~/.zshrc
|
||||||
|
export PROXMENUX_API_TOKEN="your_actual_token_here"
|
||||||
|
|
||||||
|
# Windows PowerShell - Add to profile
|
||||||
|
$env:PROXMENUX_API_TOKEN = "your_actual_token_here"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reference it in your scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux/macOS
|
||||||
|
curl -H "Authorization: Bearer $PROXMENUX_API_TOKEN" \
|
||||||
|
http://your-proxmox-ip:8008/api/system
|
||||||
|
|
||||||
|
# Windows PowerShell
|
||||||
|
curl -H "Authorization: Bearer $env:PROXMENUX_API_TOKEN" `
|
||||||
|
http://your-proxmox-ip:8008/api/system
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Secrets File**
|
||||||
|
|
||||||
|
Create a dedicated secrets file (make sure to add it to `.gitignore`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create secrets file
|
||||||
|
echo "PROXMENUX_API_TOKEN=your_actual_token_here" > ~/.proxmenux_secrets
|
||||||
|
|
||||||
|
# Secure the file (Linux/macOS only)
|
||||||
|
chmod 600 ~/.proxmenux_secrets
|
||||||
|
|
||||||
|
# Load in your script
|
||||||
|
source ~/.proxmenux_secrets
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 3: Homepage Secrets (Recommended)**
|
||||||
|
|
||||||
|
Homepage supports secrets management. Create a `secrets.yaml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# secrets.yaml (add to .gitignore!)
|
||||||
|
proxmenux_token: "your_actual_token_here"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reference it in your `services.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- ProxMenux Monitor:
|
||||||
|
widget:
|
||||||
|
type: customapi
|
||||||
|
url: http://proxmox.example.tld:8008/api/system
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 4: Home Assistant Secrets**
|
||||||
|
|
||||||
|
Home Assistant has built-in secrets support. Edit `secrets.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# secrets.yaml
|
||||||
|
proxmenux_api_token: "your_actual_token_here"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reference it in `configuration.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sensor:
|
||||||
|
- platform: rest
|
||||||
|
name: ProxMenux CPU
|
||||||
|
resource: http://proxmox.example.tld:8008/api/system
|
||||||
|
headers:
|
||||||
|
Authorization: !secret proxmenux_api_token
|
||||||
|
```
|
||||||
|
|
||||||
|
**Token Security Checklist:**
|
||||||
|
- ✅ Store tokens in environment variables or secrets files
|
||||||
|
- ✅ Add secrets files to `.gitignore`
|
||||||
|
- ✅ Set proper file permissions (chmod 600 on Linux/macOS)
|
||||||
|
- ✅ Rotate tokens periodically (every 3-6 months)
|
||||||
|
- ✅ Use different tokens for different integrations
|
||||||
|
- ✅ Delete tokens you no longer use
|
||||||
|
- ❌ Never commit tokens to version control
|
||||||
|
- ❌ Never share tokens in screenshots or logs
|
||||||
|
- ❌ Never hardcode tokens in configuration files
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
ProxMenux Monitor provides a comprehensive RESTful API for integrating with external services like Homepage, Home Assistant, or custom dashboards.
|
||||||
|
|
||||||
|
### API Authentication
|
||||||
|
|
||||||
|
When authentication is enabled on ProxMenux Monitor, all API endpoints (except `/api/health` and `/api/auth/*`) require a valid JWT token in the `Authorization` header.
|
||||||
|
|
||||||
|
### API Endpoint Base URL
|
||||||
|
|
||||||
|
**Direct Access:**
|
||||||
|
```
|
||||||
|
http://your-proxmox-ip:8008/api/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Via Proxy:**
|
||||||
|
```
|
||||||
|
https://your-domain.com/proxmenux-monitor/api/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: All API examples in this documentation work with both direct and proxied URLs. Simply replace the base URL with your preferred access method.
|
||||||
|
|
||||||
|
### Generating API Tokens
|
||||||
|
|
||||||
|
To use the API with authentication enabled, you need to generate a long-lived API token.
|
||||||
|
|
||||||
|
#### Option 1: Generate via Web Panel (Recommended)
|
||||||
|
|
||||||
|
The easiest way to generate an API token is through the ProxMenux Monitor web interface:
|
||||||
|
|
||||||
|
1. Navigate to **Settings** tab in the dashboard
|
||||||
|
2. Scroll to the **API Access Tokens** section
|
||||||
|
3. Enter your password
|
||||||
|
4. If 2FA is enabled, enter your 6-digit code
|
||||||
|
5. Provide a name for the token (e.g., "Homepage Integration")
|
||||||
|
6. Click **Generate Token**
|
||||||
|
7. Copy the token immediately - it will not be shown again
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The token will be valid for **365 days** (1 year) and can be used for integrations with Homepage, Home Assistant, or any custom application.
|
||||||
|
|
||||||
|
#### Option 2: Generate via API Call
|
||||||
|
|
||||||
|
For advanced users or automation, you can generate tokens programmatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"username": "your-username",
|
||||||
|
"password": "your-password",
|
||||||
|
"totp_token": "123456",
|
||||||
|
"token_name": "Homepage Integration"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"token_name": "Homepage Integration",
|
||||||
|
"expires_in": "365 days",
|
||||||
|
"message": "API token generated successfully. Store this token securely, it will not be shown again."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- If 2FA is enabled, include the `totp_token` field with your 6-digit code
|
||||||
|
- If 2FA is not enabled, omit the `totp_token` field
|
||||||
|
- The token is valid for **365 days** (1 year)
|
||||||
|
- Store the token securely - it cannot be retrieved again
|
||||||
|
|
||||||
|
#### Option 3: Generate via cURL (without 2FA)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Without 2FA
|
||||||
|
curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"pedro","password":"your-password","token_name":"Homepage"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using API Tokens
|
||||||
|
|
||||||
|
Once you have your API token, include it in the `Authorization` header of all API requests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer YOUR_API_TOKEN_HERE" \
|
||||||
|
http://your-proxmox-ip:8008/api/system
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Available Endpoints
|
||||||
|
|
||||||
|
Below is a complete list of all API endpoints with descriptions and example responses.
|
||||||
|
|
||||||
|
#### System & Metrics
|
||||||
|
|
||||||
|
| Endpoint | Method | Auth Required | Description |
|
||||||
|
|----------|--------|---------------|-------------|
|
||||||
|
| `/api/system` | GET | Yes | Complete system information (CPU, memory, temperature, uptime) |
|
||||||
|
| `/api/system-info` | GET | No | Lightweight system info for header (hostname, uptime, health) |
|
||||||
|
| `/api/node/metrics` | GET | Yes | Historical metrics data (RRD) for CPU, memory, disk I/O |
|
||||||
|
| `/api/prometheus` | GET | Yes | Export metrics in Prometheus format |
|
||||||
|
|
||||||
|
**Example `/api/system` Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hostname": "pve",
|
||||||
|
"cpu_usage": 15.2,
|
||||||
|
"memory_usage": 45.8,
|
||||||
|
"temperature": 42.5,
|
||||||
|
"uptime": 345600,
|
||||||
|
"kernel": "6.2.16-3-pve",
|
||||||
|
"pve_version": "8.0.3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Storage
|
||||||
|
|
||||||
|
| Endpoint | Method | Auth Required | Description |
|
||||||
|
|----------|--------|---------------|-------------|
|
||||||
|
| `/api/storage` | GET | Yes | Complete storage information with SMART data |
|
||||||
|
| `/api/storage/summary` | GET | Yes | Optimized storage summary (without SMART) |
|
||||||
|
| `/api/proxmox-storage` | GET | Yes | Proxmox storage pools information |
|
||||||
|
| `/api/backups` | GET | Yes | List of all backup files |
|
||||||
|
|
||||||
|
**Example `/api/storage/summary` Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total_capacity": 1431894917120,
|
||||||
|
"used_space": 197414092800,
|
||||||
|
"free_space": 1234480824320,
|
||||||
|
"usage_percentage": 13.8,
|
||||||
|
"disks": [
|
||||||
|
{
|
||||||
|
"device": "/dev/sda",
|
||||||
|
"model": "Samsung SSD 970",
|
||||||
|
"size": "476.94 GB",
|
||||||
|
"type": "SSD"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Network
|
||||||
|
|
||||||
|
| Endpoint | Method | Auth Required | Description |
|
||||||
|
|----------|--------|---------------|-------------|
|
||||||
|
| `/api/network` | GET | Yes | Complete network information for all interfaces |
|
||||||
|
| `/api/network/summary` | GET | Yes | Optimized network summary |
|
||||||
|
| `/api/network/<interface>/metrics` | GET | Yes | Historical metrics (RRD) for specific interface |
|
||||||
|
|
||||||
|
**Example `/api/network/summary` Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"name": "vmbr0",
|
||||||
|
"ip": "192.168.1.100",
|
||||||
|
"state": "up",
|
||||||
|
"rx_bytes": 1234567890,
|
||||||
|
"tx_bytes": 987654321
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Virtual Machines & Containers
|
||||||
|
|
||||||
|
| Endpoint | Method | Auth Required | Description |
|
||||||
|
|----------|--------|---------------|-------------|
|
||||||
|
| `/api/vms` | GET | Yes | List of all VMs and LXC containers |
|
||||||
|
| `/api/vms/<vmid>` | GET | Yes | Detailed configuration for specific VM/LXC |
|
||||||
|
| `/api/vms/<vmid>/metrics` | GET | Yes | Historical metrics (RRD) for specific VM/LXC |
|
||||||
|
| `/api/vms/<vmid>/logs` | GET | Yes | Download real logs for specific VM/LXC |
|
||||||
|
| `/api/vms/<vmid>/control` | POST | Yes | Control VM/LXC (start, stop, shutdown, reboot) |
|
||||||
|
| `/api/vms/<vmid>/config` | PUT | Yes | Update VM/LXC configuration (description/notes) |
|
||||||
|
|
||||||
|
**Example `/api/vms` Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"vms": [
|
||||||
|
{
|
||||||
|
"vmid": "100",
|
||||||
|
"name": "ubuntu-server",
|
||||||
|
"type": "qemu",
|
||||||
|
"status": "running",
|
||||||
|
"cpu": 2,
|
||||||
|
"maxcpu": 4,
|
||||||
|
"mem": 2147483648,
|
||||||
|
"maxmem": 4294967296,
|
||||||
|
"uptime": 86400
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Hardware
|
||||||
|
|
||||||
|
| Endpoint | Method | Auth Required | Description |
|
||||||
|
|----------|--------|---------------|-------------|
|
||||||
|
| `/api/hardware` | GET | Yes | Complete hardware information (CPU, GPU, PCIe, disks) |
|
||||||
|
| `/api/gpu/<slot>/realtime` | GET | Yes | Real-time monitoring for specific GPU |
|
||||||
|
|
||||||
|
**Example `/api/hardware` Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cpu": {
|
||||||
|
"model": "AMD Ryzen 9 5950X",
|
||||||
|
"cores": 16,
|
||||||
|
"threads": 32,
|
||||||
|
"frequency": "3.4 GHz"
|
||||||
|
},
|
||||||
|
"gpus": [
|
||||||
|
{
|
||||||
|
"slot": "0000:01:00.0",
|
||||||
|
"vendor": "NVIDIA",
|
||||||
|
"model": "GeForce RTX 3080",
|
||||||
|
"driver": "nvidia"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Logs, Events & Notifications
|
||||||
|
|
||||||
|
| Endpoint | Method | Auth Required | Description |
|
||||||
|
|----------|--------|---------------|-------------|
|
||||||
|
| `/api/logs` | GET | Yes | System logs (journalctl) with filters |
|
||||||
|
| `/api/logs/download` | GET | Yes | Download logs as text file |
|
||||||
|
| `/api/notifications` | GET | Yes | Proxmox notification history |
|
||||||
|
| `/api/notifications/download` | GET | Yes | Download full notification log |
|
||||||
|
| `/api/events` | GET | Yes | Recent Proxmox tasks and events |
|
||||||
|
| `/api/task-log/<upid>` | GET | Yes | Full log for specific task using UPID |
|
||||||
|
|
||||||
|
**Example `/api/logs` Query Parameters:**
|
||||||
|
```
|
||||||
|
/api/logs?severity=error&since=1h&search=failed
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Health Monitoring
|
||||||
|
|
||||||
|
| Endpoint | Method | Auth Required | Description |
|
||||||
|
|----------|--------|---------------|-------------|
|
||||||
|
| `/api/health` | GET | No | Basic health check (for external monitoring) |
|
||||||
|
| `/api/health/status` | GET | Yes | Summary of system health status |
|
||||||
|
| `/api/health/details` | GET | Yes | Detailed health check results |
|
||||||
|
| `/api/health/acknowledge` | POST | Yes | Dismiss/acknowledge health warnings |
|
||||||
|
| `/api/health/active-errors` | GET | Yes | Get active persistent errors |
|
||||||
|
|
||||||
|
#### ProxMenux Optimizations
|
||||||
|
|
||||||
|
| Endpoint | Method | Auth Required | Description |
|
||||||
|
|----------|--------|---------------|-------------|
|
||||||
|
| `/api/proxmenux/installed-tools` | GET | Yes | List of installed ProxMenux optimizations |
|
||||||
|
|
||||||
|
#### Authentication
|
||||||
|
|
||||||
|
| Endpoint | Method | Auth Required | Description |
|
||||||
|
|----------|--------|---------------|-------------|
|
||||||
|
| `/api/auth/status` | GET | No | Current authentication status |
|
||||||
|
| `/api/auth/login` | POST | No | Authenticate and receive JWT token |
|
||||||
|
| `/api/auth/generate-api-token` | POST | No | Generate long-lived API token (365 days) |
|
||||||
|
| `/api/auth/setup` | POST | No | Initial setup of username/password |
|
||||||
|
| `/api/auth/enable` | POST | No | Enable authentication |
|
||||||
|
| `/api/auth/disable` | POST | Yes | Disable authentication |
|
||||||
|
| `/api/auth/change-password` | POST | No | Change password |
|
||||||
|
| `/api/auth/totp/setup` | POST | Yes | Initialize 2FA setup |
|
||||||
|
| `/api/auth/totp/enable` | POST | Yes | Enable 2FA after verification |
|
||||||
|
| `/api/auth/totp/disable` | POST | Yes | Disable 2FA |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration Examples
|
||||||
|
|
||||||
|
### Homepage Integration
|
||||||
|
|
||||||
|
[Homepage](https://gethomepage.dev/) is a modern, fully static, fast, secure fully proxied, highly customizable application dashboard.
|
||||||
|
|
||||||
|
#### Basic Configuration (No Authentication)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- ProxMenux Monitor:
|
||||||
|
href: http://proxmox.example.tld:8008/
|
||||||
|
icon: lucide:flask-round
|
||||||
|
widget:
|
||||||
|
type: customapi
|
||||||
|
url: http://proxmox.example.tld:8008/api/system
|
||||||
|
refreshInterval: 10000
|
||||||
|
mappings:
|
||||||
|
- field: uptime
|
||||||
|
label: Uptime
|
||||||
|
icon: lucide:clock-4
|
||||||
|
format: text
|
||||||
|
- field: cpu_usage
|
||||||
|
label: CPU
|
||||||
|
icon: lucide:cpu
|
||||||
|
format: percent
|
||||||
|
- field: memory_usage
|
||||||
|
label: RAM
|
||||||
|
icon: lucide:memory-stick
|
||||||
|
format: percent
|
||||||
|
- field: temperature
|
||||||
|
label: Temp
|
||||||
|
icon: lucide:thermometer-sun
|
||||||
|
format: number
|
||||||
|
suffix: °C
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With Authentication Enabled (Using Secrets)
|
||||||
|
|
||||||
|
First, generate an API token via the web interface (Settings > API Access Tokens) or via API.
|
||||||
|
|
||||||
|
Then, store your token securely in Homepage's `secrets.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# secrets.yaml (add to .gitignore!)
|
||||||
|
proxmenux_token: "your_actual_api_token_here"
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, reference the secret in your `services.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- ProxMenux Monitor:
|
||||||
|
href: http://proxmox.example.tld:8008/
|
||||||
|
icon: lucide:flask-round
|
||||||
|
widget:
|
||||||
|
type: customapi
|
||||||
|
url: http://proxmox.example.tld:8008/api/system
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
|
||||||
|
refreshInterval: 10000
|
||||||
|
mappings:
|
||||||
|
- field: uptime
|
||||||
|
label: Uptime
|
||||||
|
icon: lucide:clock-4
|
||||||
|
format: text
|
||||||
|
- field: cpu_usage
|
||||||
|
label: CPU
|
||||||
|
icon: lucide:cpu
|
||||||
|
format: percent
|
||||||
|
- field: memory_usage
|
||||||
|
label: RAM
|
||||||
|
icon: lucide:memory-stick
|
||||||
|
format: percent
|
||||||
|
- field: temperature
|
||||||
|
label: Temp
|
||||||
|
icon: lucide:thermometer-sun
|
||||||
|
format: number
|
||||||
|
suffix: °C
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Advanced Multi-Widget Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Store token in secrets.yaml
|
||||||
|
# proxmenux_token: "your_actual_api_token_here"
|
||||||
|
|
||||||
|
- ProxMenux System:
|
||||||
|
href: http://proxmox.example.tld:8008/
|
||||||
|
icon: lucide:server
|
||||||
|
description: Proxmox VE Host
|
||||||
|
widget:
|
||||||
|
type: customapi
|
||||||
|
url: http://proxmox.example.tld:8008/api/system
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
|
||||||
|
refreshInterval: 5000
|
||||||
|
mappings:
|
||||||
|
- field: cpu_usage
|
||||||
|
label: CPU
|
||||||
|
icon: lucide:cpu
|
||||||
|
format: percent
|
||||||
|
- field: memory_usage
|
||||||
|
label: RAM
|
||||||
|
icon: lucide:memory-stick
|
||||||
|
format: percent
|
||||||
|
- field: temperature
|
||||||
|
label: Temp
|
||||||
|
icon: lucide:thermometer-sun
|
||||||
|
format: number
|
||||||
|
suffix: °C
|
||||||
|
|
||||||
|
- ProxMenux Storage:
|
||||||
|
href: http://proxmox.example.tld:8008/#/storage
|
||||||
|
icon: lucide:hard-drive
|
||||||
|
description: Storage Overview
|
||||||
|
widget:
|
||||||
|
type: customapi
|
||||||
|
url: http://proxmox.example.tld:8008/api/storage/summary
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
|
||||||
|
refreshInterval: 30000
|
||||||
|
mappings:
|
||||||
|
- field: usage_percentage
|
||||||
|
label: Used
|
||||||
|
icon: lucide:database
|
||||||
|
format: percent
|
||||||
|
- field: used_space
|
||||||
|
label: Space
|
||||||
|
icon: lucide:folder
|
||||||
|
format: bytes
|
||||||
|
|
||||||
|
- ProxMenux Network:
|
||||||
|
href: http://proxmox.example.tld:8008/#/network
|
||||||
|
icon: lucide:network
|
||||||
|
description: Network Stats
|
||||||
|
widget:
|
||||||
|
type: customapi
|
||||||
|
url: http://proxmox.example.tld:8008/api/network/summary
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
|
||||||
|
refreshInterval: 5000
|
||||||
|
mappings:
|
||||||
|
- field: interfaces[0].rx_bytes
|
||||||
|
label: Received
|
||||||
|
icon: lucide:download
|
||||||
|
format: bytes
|
||||||
|
- field: interfaces[0].tx_bytes
|
||||||
|
label: Sent
|
||||||
|
icon: lucide:upload
|
||||||
|
format: bytes
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Home Assistant Integration
|
||||||
|
|
||||||
|
[Home Assistant](https://www.home-assistant.io/) is an open-source home automation platform.
|
||||||
|
|
||||||
|
#### Store Token Securely
|
||||||
|
|
||||||
|
First, add your API token to Home Assistant's `secrets.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# secrets.yaml
|
||||||
|
proxmenux_api_token: "Bearer your_actual_api_token_here"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Include "Bearer " prefix in the secrets file for Home Assistant.
|
||||||
|
|
||||||
|
#### Configuration.yaml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ProxMenux Monitor Sensors
|
||||||
|
sensor:
|
||||||
|
- platform: rest
|
||||||
|
name: ProxMenux CPU
|
||||||
|
resource: http://proxmox.example.tld:8008/api/system
|
||||||
|
headers:
|
||||||
|
Authorization: !secret proxmenux_api_token
|
||||||
|
value_template: "{{ value_json.cpu_usage }}"
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
scan_interval: 30
|
||||||
|
|
||||||
|
- platform: rest
|
||||||
|
name: ProxMenux Memory
|
||||||
|
resource: http://proxmox.example.tld:8008/api/system
|
||||||
|
headers:
|
||||||
|
Authorization: !secret proxmenux_api_token
|
||||||
|
value_template: "{{ value_json.memory_usage }}"
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
scan_interval: 30
|
||||||
|
|
||||||
|
- platform: rest
|
||||||
|
name: ProxMenux Temperature
|
||||||
|
resource: http://proxmox.example.tld:8008/api/system
|
||||||
|
headers:
|
||||||
|
Authorization: !secret proxmenux_api_token
|
||||||
|
value_template: "{{ value_json.temperature }}"
|
||||||
|
unit_of_measurement: "°C"
|
||||||
|
device_class: temperature
|
||||||
|
scan_interval: 30
|
||||||
|
|
||||||
|
- platform: rest
|
||||||
|
name: ProxMenux Uptime
|
||||||
|
resource: http://proxmox.example.tld:8008/api/system
|
||||||
|
headers:
|
||||||
|
Authorization: !secret proxmenux_api_token
|
||||||
|
value_template: >
|
||||||
|
{% set uptime_seconds = value_json.uptime | int %}
|
||||||
|
{% set days = (uptime_seconds / 86400) | int %}
|
||||||
|
{% set hours = ((uptime_seconds % 86400) / 3600) | int %}
|
||||||
|
{% set minutes = ((uptime_seconds % 3600) / 60) | int %}
|
||||||
|
{{ days }}d {{ hours }}h {{ minutes }}m
|
||||||
|
scan_interval: 60
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Lovelace Card Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: entities
|
||||||
|
title: Proxmox Monitor
|
||||||
|
entities:
|
||||||
|
- entity: sensor.proxmenux_cpu
|
||||||
|
name: CPU Usage
|
||||||
|
icon: mdi:cpu-64-bit
|
||||||
|
- entity: sensor.proxmenux_memory
|
||||||
|
name: Memory Usage
|
||||||
|
icon: mdi:memory
|
||||||
|
- entity: sensor.proxmenux_temperature
|
||||||
|
name: Temperature
|
||||||
|
icon: mdi:thermometer
|
||||||
|
- entity: sensor.proxmenux_uptime
|
||||||
|
name: Uptime
|
||||||
|
icon: mdi:clock-outline
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.
|
||||||
|
|
||||||
|
### Development Setup
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. Install dependencies: `npm install`
|
||||||
|
3. Run development server: `npm run dev`
|
||||||
|
4. Build AppImage: `./build_appimage.sh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the **Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0)**.
|
||||||
|
|
||||||
|
You are free to:
|
||||||
|
- Share — copy and redistribute the material in any medium or format
|
||||||
|
- Adapt — remix, transform, and build upon the material
|
||||||
|
|
||||||
|
Under the following terms:
|
||||||
|
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made
|
||||||
|
- NonCommercial — You may not use the material for commercial purposes
|
||||||
|
|
||||||
|
For more details, see the [full license](https://creativecommons.org/licenses/by-nc/4.0/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For support, feature requests, or bug reports, please visit:
|
||||||
|
- GitHub Issues: [github.com/your-repo/issues](https://github.com/your-repo/issues)
|
||||||
|
- Documentation: [github.com/your-repo/wiki](https://github.com/your-repo/wiki)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**ProxMenux Monitor** - Made with ❤️ for the Proxmox community
|
||||||
|
|||||||
+79
-1
@@ -1,7 +1,85 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
import { ProxmoxDashboard } from "../components/proxmox-dashboard"
|
import { ProxmoxDashboard } from "../components/proxmox-dashboard"
|
||||||
|
import { Login } from "../components/login"
|
||||||
|
import { AuthSetup } from "../components/auth-setup"
|
||||||
|
import { getApiUrl } from "../lib/api-config"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return <ProxmoxDashboard />
|
const [authStatus, setAuthStatus] = useState<{
|
||||||
|
loading: boolean
|
||||||
|
authEnabled: boolean
|
||||||
|
authConfigured: boolean
|
||||||
|
authenticated: boolean
|
||||||
|
}>({
|
||||||
|
loading: true,
|
||||||
|
authEnabled: false,
|
||||||
|
authConfigured: false,
|
||||||
|
authenticated: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuthStatus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const checkAuthStatus = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("proxmenux-auth-token")
|
||||||
|
const response = await fetch(getApiUrl("/api/auth/status"), {
|
||||||
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
console.log("[v0] Auth status:", data)
|
||||||
|
|
||||||
|
const authenticated = data.auth_enabled ? data.authenticated : true
|
||||||
|
|
||||||
|
setAuthStatus({
|
||||||
|
loading: false,
|
||||||
|
authEnabled: data.auth_enabled,
|
||||||
|
authConfigured: data.auth_configured,
|
||||||
|
authenticated,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[v0] Failed to check auth status:", error)
|
||||||
|
setAuthStatus({
|
||||||
|
loading: false,
|
||||||
|
authEnabled: false,
|
||||||
|
authConfigured: false,
|
||||||
|
authenticated: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAuthComplete = () => {
|
||||||
|
checkAuthStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoginSuccess = () => {
|
||||||
|
checkAuthStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authStatus.loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
|
||||||
|
<p className="text-muted-foreground">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authStatus.authEnabled && !authStatus.authenticated) {
|
||||||
|
return <Login onLogin={handleLoginSuccess} />
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show dashboard in all other cases
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!authStatus.authConfigured && <AuthSetup onComplete={handleAuthComplete} />}
|
||||||
|
<ProxmoxDashboard />
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { Button } from "./ui/button"
|
||||||
|
import { Dialog, DialogContent } from "./ui/dialog"
|
||||||
|
import { Input } from "./ui/input"
|
||||||
|
import { Label } from "./ui/label"
|
||||||
|
import { Shield, Lock, User, AlertCircle } from "lucide-react"
|
||||||
|
import { getApiUrl } from "../lib/api-config"
|
||||||
|
|
||||||
|
interface AuthSetupProps {
|
||||||
|
onComplete: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthSetup({ onComplete }: AuthSetupProps) {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [step, setStep] = useState<"choice" | "setup">("choice")
|
||||||
|
const [username, setUsername] = useState("")
|
||||||
|
const [password, setPassword] = useState("")
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("")
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkOnboardingStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(getApiUrl("/api/auth/status"))
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
console.log("[v0] Auth status for modal check:", data)
|
||||||
|
|
||||||
|
// Show modal if auth is not configured and not declined
|
||||||
|
if (!data.auth_configured) {
|
||||||
|
setTimeout(() => setOpen(true), 500)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[v0] Failed to check auth status:", error)
|
||||||
|
// Fail-safe: show modal if we can't check status
|
||||||
|
setTimeout(() => setOpen(true), 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkOnboardingStatus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSkipAuth = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError("")
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("[v0] Skipping authentication setup...")
|
||||||
|
const response = await fetch(getApiUrl("/api/auth/skip"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
console.log("[v0] Auth skip response:", data)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || "Failed to skip authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.auth_declined) {
|
||||||
|
console.log("[v0] Authentication skipped successfully - APIs should be accessible without token")
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[v0] Authentication skipped successfully")
|
||||||
|
localStorage.setItem("proxmenux-auth-declined", "true")
|
||||||
|
localStorage.removeItem("proxmenux-auth-token") // Remove any old token
|
||||||
|
setOpen(false)
|
||||||
|
onComplete()
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[v0] Auth skip error:", err)
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to save preference")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetupAuth = async () => {
|
||||||
|
setError("")
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
setError("Please fill in all fields")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError("Passwords do not match")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
setError("Password must be at least 6 characters")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("[v0] Setting up authentication...")
|
||||||
|
const response = await fetch(getApiUrl("/api/auth/setup"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
console.log("[v0] Auth setup response:", data)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || "Failed to setup authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.token) {
|
||||||
|
localStorage.setItem("proxmenux-auth-token", data.token)
|
||||||
|
localStorage.removeItem("proxmenux-auth-declined")
|
||||||
|
console.log("[v0] Authentication setup successful")
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false)
|
||||||
|
onComplete()
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[v0] Auth setup error:", err)
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to setup authentication")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
|
||||||
|
{step === "choice" ? (
|
||||||
|
<div className="space-y-6 py-2">
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<div className="mx-auto w-16 h-16 bg-blue-500/10 rounded-full flex items-center justify-center">
|
||||||
|
<Shield className="h-8 w-8 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold">Protect Your Dashboard?</h2>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Add an extra layer of security to protect your Proxmox data when accessing from non-private networks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Button onClick={() => setStep("setup")} className="w-full bg-blue-500 hover:bg-blue-600" size="lg">
|
||||||
|
<Lock className="h-4 w-4 mr-2" />
|
||||||
|
Yes, Setup Password
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSkipAuth}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full bg-transparent"
|
||||||
|
size="lg"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
No, Continue Without Protection
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-center text-muted-foreground">You can always enable this later in Settings</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6 py-2">
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<div className="mx-auto w-16 h-16 bg-blue-500/10 rounded-full flex items-center justify-center">
|
||||||
|
<Lock className="h-8 w-8 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold">Setup Authentication</h2>
|
||||||
|
<p className="text-muted-foreground text-sm">Create a username and password to protect your dashboard</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-red-500">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="username" className="text-sm">
|
||||||
|
Username
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className="pl-10 text-base"
|
||||||
|
disabled={loading}
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password" className="text-sm">
|
||||||
|
Password
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="pl-10 text-base"
|
||||||
|
disabled={loading}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="confirm-password" className="text-sm">
|
||||||
|
Confirm Password
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="confirm-password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
className="pl-10 text-base"
|
||||||
|
disabled={loading}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Button onClick={handleSetupAuth} className="w-full bg-blue-500 hover:bg-blue-600" disabled={loading}>
|
||||||
|
{loading ? "Setting up..." : "Setup Authentication"}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setStep("choice")} variant="ghost" className="w-full" disabled={loading}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
+354
-158
@@ -20,7 +20,14 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { type HardwareData, type GPU, type PCIDevice, type StorageDevice, fetcher } from "../types/hardware"
|
import {
|
||||||
|
type HardwareData,
|
||||||
|
type GPU,
|
||||||
|
type PCIDevice,
|
||||||
|
type StorageDevice,
|
||||||
|
fetcher as swrFetcher,
|
||||||
|
} from "../types/hardware"
|
||||||
|
import { fetchApi } from "@/lib/api-config"
|
||||||
|
|
||||||
const parseLsblkSize = (sizeStr: string | undefined): number => {
|
const parseLsblkSize = (sizeStr: string | undefined): number => {
|
||||||
if (!sizeStr) return 0
|
if (!sizeStr) return 0
|
||||||
@@ -64,9 +71,12 @@ const formatMemory = (memoryKB: number | string): string => {
|
|||||||
return `${tb.toFixed(1)} TB`
|
return `${tb.toFixed(1)} TB`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to GB if >= 1024 MB
|
|
||||||
if (mb >= 1024) {
|
if (mb >= 1024) {
|
||||||
const gb = mb / 1024
|
const gb = mb / 1024
|
||||||
|
// If GB value is greater than 999, convert to TB
|
||||||
|
if (gb > 999) {
|
||||||
|
return `${(gb / 1024).toFixed(2)} TB`
|
||||||
|
}
|
||||||
return `${gb.toFixed(1)} GB`
|
return `${gb.toFixed(1)} GB`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,14 +170,65 @@ const groupAndSortTemperatures = (temperatures: any[]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Hardware() {
|
export default function Hardware() {
|
||||||
|
// Static data - load once without refresh
|
||||||
const {
|
const {
|
||||||
data: hardwareData,
|
data: staticHardwareData,
|
||||||
error,
|
error: staticError,
|
||||||
isLoading,
|
isLoading: staticLoading,
|
||||||
} = useSWR<HardwareData>("/api/hardware", fetcher, {
|
} = useSWR<HardwareData>("/api/hardware", swrFetcher, {
|
||||||
refreshInterval: 5000,
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
refreshInterval: 0, // No auto-refresh for static data
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Dynamic data - refresh every 5 seconds for temperatures, fans, power, ups
|
||||||
|
const {
|
||||||
|
data: dynamicHardwareData,
|
||||||
|
error: dynamicError,
|
||||||
|
isLoading: dynamicLoading,
|
||||||
|
} = useSWR<HardwareData>("/api/hardware", swrFetcher, {
|
||||||
|
refreshInterval: 7000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Merge static and dynamic data, preferring static for CPU/memory/PCI/disks
|
||||||
|
const hardwareData = staticHardwareData
|
||||||
|
? {
|
||||||
|
...dynamicHardwareData,
|
||||||
|
// Keep static data from initial load
|
||||||
|
cpu: staticHardwareData.cpu,
|
||||||
|
motherboard: staticHardwareData.motherboard,
|
||||||
|
memory_modules: staticHardwareData.memory_modules,
|
||||||
|
pci_devices: staticHardwareData.pci_devices,
|
||||||
|
storage_devices: staticHardwareData.storage_devices,
|
||||||
|
gpus: staticHardwareData.gpus,
|
||||||
|
// Use dynamic data for these
|
||||||
|
temperatures: dynamicHardwareData?.temperatures,
|
||||||
|
fans: dynamicHardwareData?.fans,
|
||||||
|
power_meter: dynamicHardwareData?.power_meter,
|
||||||
|
power_supplies: dynamicHardwareData?.power_supplies,
|
||||||
|
ups: dynamicHardwareData?.ups,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const error = staticError || dynamicError
|
||||||
|
const isLoading = staticLoading
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hardwareData?.storage_devices) {
|
||||||
|
console.log("[v0] Storage devices data from backend:", hardwareData.storage_devices)
|
||||||
|
hardwareData.storage_devices.forEach((device) => {
|
||||||
|
if (device.name.startsWith("nvme")) {
|
||||||
|
console.log(`[v0] NVMe device ${device.name}:`, {
|
||||||
|
pcie_gen: device.pcie_gen,
|
||||||
|
pcie_width: device.pcie_width,
|
||||||
|
pcie_max_gen: device.pcie_max_gen,
|
||||||
|
pcie_max_width: device.pcie_max_width,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [hardwareData])
|
||||||
|
|
||||||
const [selectedGPU, setSelectedGPU] = useState<GPU | null>(null)
|
const [selectedGPU, setSelectedGPU] = useState<GPU | null>(null)
|
||||||
const [realtimeGPUData, setRealtimeGPUData] = useState<any>(null)
|
const [realtimeGPUData, setRealtimeGPUData] = useState<any>(null)
|
||||||
const [detailsLoading, setDetailsLoading] = useState(false)
|
const [detailsLoading, setDetailsLoading] = useState(false)
|
||||||
@@ -176,6 +237,21 @@ export default function Hardware() {
|
|||||||
const [selectedNetwork, setSelectedNetwork] = useState<PCIDevice | null>(null)
|
const [selectedNetwork, setSelectedNetwork] = useState<PCIDevice | null>(null)
|
||||||
const [selectedUPS, setSelectedUPS] = useState<any>(null)
|
const [selectedUPS, setSelectedUPS] = useState<any>(null)
|
||||||
|
|
||||||
|
const fetcher = async (url: string) => {
|
||||||
|
const data = await fetchApi(url)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: hardwareDataSWR,
|
||||||
|
error: swrError,
|
||||||
|
isLoading: swrLoading,
|
||||||
|
mutate,
|
||||||
|
} = useSWR<HardwareData>("/api/hardware", fetcher, {
|
||||||
|
refreshInterval: 30000,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedGPU) return
|
if (!selectedGPU) return
|
||||||
|
|
||||||
@@ -188,25 +264,10 @@ export default function Hardware() {
|
|||||||
|
|
||||||
const fetchRealtimeData = async () => {
|
const fetchRealtimeData = async () => {
|
||||||
try {
|
try {
|
||||||
const apiUrl = `http://${window.location.hostname}:8008/api/gpu/${fullSlot}/realtime`
|
const data = await fetchApi(`/api/gpu/${fullSlot}/realtime`)
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
signal: abortController.signal,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
setRealtimeGPUData(data)
|
setRealtimeGPUData(data)
|
||||||
setDetailsLoading(false)
|
setDetailsLoading(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Only log non-abort errors
|
|
||||||
if (error instanceof Error && error.name !== "AbortError") {
|
if (error instanceof Error && error.name !== "AbortError") {
|
||||||
console.error("[v0] Error fetching GPU realtime data:", error)
|
console.error("[v0] Error fetching GPU realtime data:", error)
|
||||||
}
|
}
|
||||||
@@ -215,10 +276,7 @@ export default function Hardware() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial fetch
|
|
||||||
fetchRealtimeData()
|
fetchRealtimeData()
|
||||||
|
|
||||||
// Poll every 3 seconds
|
|
||||||
const interval = setInterval(fetchRealtimeData, 3000)
|
const interval = setInterval(fetchRealtimeData, 3000)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -234,14 +292,14 @@ export default function Hardware() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const findPCIDeviceForGPU = (gpu: GPU): PCIDevice | null => {
|
const findPCIDeviceForGPU = (gpu: GPU): PCIDevice | null => {
|
||||||
if (!hardwareData?.pci_devices || !gpu.slot) return null
|
if (!hardwareDataSWR?.pci_devices || !gpu.slot) return null
|
||||||
|
|
||||||
// Try to find exact match first (e.g., "00:02.0")
|
// Try to find exact match first (e.g., "00:02.0")
|
||||||
let pciDevice = hardwareData.pci_devices.find((d) => d.slot === gpu.slot)
|
let pciDevice = hardwareDataSWR.pci_devices.find((d) => d.slot === gpu.slot)
|
||||||
|
|
||||||
// If not found, try to match by partial slot (e.g., "00" matches "00:02.0")
|
// If not found, try to match by partial slot (e.g., "00" matches "00:02.0")
|
||||||
if (!pciDevice && gpu.slot.length <= 2) {
|
if (!pciDevice && gpu.slot.length <= 2) {
|
||||||
pciDevice = hardwareData.pci_devices.find(
|
pciDevice = hardwareDataSWR.pci_devices.find(
|
||||||
(d) =>
|
(d) =>
|
||||||
d.slot.startsWith(gpu.slot + ":") &&
|
d.slot.startsWith(gpu.slot + ":") &&
|
||||||
(d.type.toLowerCase().includes("vga") ||
|
(d.type.toLowerCase().includes("vga") ||
|
||||||
@@ -260,7 +318,7 @@ export default function Hardware() {
|
|||||||
return realtimeGPUData.has_monitoring_tool === true
|
return realtimeGPUData.has_monitoring_tool === true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (swrLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
@@ -273,7 +331,7 @@ export default function Hardware() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* System Information - CPU & Motherboard */}
|
{/* System Information - CPU & Motherboard */}
|
||||||
{(hardwareData?.cpu || hardwareData?.motherboard) && (
|
{(hardwareDataSWR?.cpu || hardwareDataSWR?.motherboard) && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Cpu className="h-5 w-5 text-primary" />
|
<Cpu className="h-5 w-5 text-primary" />
|
||||||
@@ -282,44 +340,44 @@ export default function Hardware() {
|
|||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
{/* CPU Info */}
|
{/* CPU Info */}
|
||||||
{hardwareData?.cpu && Object.keys(hardwareData.cpu).length > 0 && (
|
{hardwareDataSWR?.cpu && Object.keys(hardwareDataSWR.cpu).length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
<CpuIcon className="h-4 w-4 text-muted-foreground" />
|
<CpuIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">CPU</h3>
|
<h3 className="text-sm font-semibold">CPU</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{hardwareData.cpu.model && (
|
{hardwareDataSWR.cpu.model && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Model</span>
|
<span className="text-muted-foreground">Model</span>
|
||||||
<span className="font-medium text-right">{hardwareData.cpu.model}</span>
|
<span className="font-medium text-right">{hardwareDataSWR.cpu.model}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.cpu.cores_per_socket && hardwareData.cpu.sockets && (
|
{hardwareDataSWR.cpu.cores_per_socket && hardwareDataSWR.cpu.sockets && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Cores</span>
|
<span className="text-muted-foreground">Cores</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{hardwareData.cpu.sockets} × {hardwareData.cpu.cores_per_socket} ={" "}
|
{hardwareDataSWR.cpu.sockets} × {hardwareDataSWR.cpu.cores_per_socket} ={" "}
|
||||||
{hardwareData.cpu.sockets * hardwareData.cpu.cores_per_socket} cores
|
{hardwareDataSWR.cpu.sockets * hardwareDataSWR.cpu.cores_per_socket} cores
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.cpu.total_threads && (
|
{hardwareDataSWR.cpu.total_threads && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Threads</span>
|
<span className="text-muted-foreground">Threads</span>
|
||||||
<span className="font-medium">{hardwareData.cpu.total_threads}</span>
|
<span className="font-medium">{hardwareDataSWR.cpu.total_threads}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.cpu.l3_cache && (
|
{hardwareDataSWR.cpu.l3_cache && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">L3 Cache</span>
|
<span className="text-muted-foreground">L3 Cache</span>
|
||||||
<span className="font-medium">{hardwareData.cpu.l3_cache}</span>
|
<span className="font-medium">{hardwareDataSWR.cpu.l3_cache}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.cpu.virtualization && (
|
{hardwareDataSWR.cpu.virtualization && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Virtualization</span>
|
<span className="text-muted-foreground">Virtualization</span>
|
||||||
<span className="font-medium">{hardwareData.cpu.virtualization}</span>
|
<span className="font-medium">{hardwareDataSWR.cpu.virtualization}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -327,41 +385,41 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Motherboard Info */}
|
{/* Motherboard Info */}
|
||||||
{hardwareData?.motherboard && Object.keys(hardwareData.motherboard).length > 0 && (
|
{hardwareDataSWR?.motherboard && Object.keys(hardwareDataSWR.motherboard).length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">Motherboard</h3>
|
<h3 className="text-sm font-semibold">Motherboard</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{hardwareData.motherboard.manufacturer && (
|
{hardwareDataSWR.motherboard.manufacturer && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Manufacturer</span>
|
<span className="text-muted-foreground">Manufacturer</span>
|
||||||
<span className="font-medium text-right">{hardwareData.motherboard.manufacturer}</span>
|
<span className="font-medium text-right">{hardwareDataSWR.motherboard.manufacturer}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.motherboard.model && (
|
{hardwareDataSWR.motherboard.model && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Model</span>
|
<span className="text-muted-foreground">Model</span>
|
||||||
<span className="font-medium text-right">{hardwareData.motherboard.model}</span>
|
<span className="font-medium text-right">{hardwareDataSWR.motherboard.model}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.motherboard.bios?.vendor && (
|
{hardwareDataSWR.motherboard.bios?.vendor && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">BIOS</span>
|
<span className="text-muted-foreground">BIOS</span>
|
||||||
<span className="font-medium text-right">{hardwareData.motherboard.bios.vendor}</span>
|
<span className="font-medium text-right">{hardwareDataSWR.motherboard.bios.vendor}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.motherboard.bios?.version && (
|
{hardwareDataSWR.motherboard.bios?.version && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Version</span>
|
<span className="text-muted-foreground">Version</span>
|
||||||
<span className="font-medium">{hardwareData.motherboard.bios.version}</span>
|
<span className="font-medium">{hardwareDataSWR.motherboard.bios.version}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.motherboard.bios?.date && (
|
{hardwareDataSWR.motherboard.bios?.date && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Date</span>
|
<span className="text-muted-foreground">Date</span>
|
||||||
<span className="font-medium">{hardwareData.motherboard.bios.date}</span>
|
<span className="font-medium">{hardwareDataSWR.motherboard.bios.date}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -372,18 +430,18 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Memory Modules */}
|
{/* Memory Modules */}
|
||||||
{hardwareData?.memory_modules && hardwareData.memory_modules.length > 0 && (
|
{hardwareDataSWR?.memory_modules && hardwareDataSWR.memory_modules.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<MemoryStick className="h-5 w-5 text-primary" />
|
<MemoryStick className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">Memory Modules</h2>
|
<h2 className="text-lg font-semibold">Memory Modules</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.memory_modules.length} installed
|
{hardwareDataSWR.memory_modules.length} installed
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{hardwareData.memory_modules.map((module, index) => (
|
{hardwareDataSWR.memory_modules.map((module, index) => (
|
||||||
<div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4">
|
<div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4">
|
||||||
<div className="mb-2 font-medium text-sm">{module.slot}</div>
|
<div className="mb-2 font-medium text-sm">{module.slot}</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -419,29 +477,29 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Thermal Monitoring */}
|
{/* Thermal Monitoring */}
|
||||||
{hardwareData?.temperatures && hardwareData.temperatures.length > 0 && (
|
{hardwareDataSWR?.temperatures && hardwareDataSWR.temperatures.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Thermometer className="h-5 w-5 text-primary" />
|
<Thermometer className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">Thermal Monitoring</h2>
|
<h2 className="text-lg font-semibold">Thermal Monitoring</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.temperatures.length} sensors
|
{hardwareDataSWR.temperatures.length} sensors
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
{/* CPU Sensors */}
|
{/* CPU Sensors */}
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).CPU.length > 0 && (
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.length > 0 && (
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<CpuIcon className="h-4 w-4 text-muted-foreground" />
|
<CpuIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">CPU</h3>
|
<h3 className="text-sm font-semibold">CPU</h3>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).CPU.length}
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).CPU.map((temp, index) => {
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.map((temp, index) => {
|
||||||
const percentage =
|
const percentage =
|
||||||
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
||||||
const isHot = temp.current > (temp.high || 80)
|
const isHot = temp.current > (temp.high || 80)
|
||||||
@@ -472,21 +530,21 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* GPU Sensors */}
|
{/* GPU Sensors */}
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).GPU.length > 0 && (
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className={groupAndSortTemperatures(hardwareData.temperatures).GPU.length > 1 ? "md:col-span-2" : ""}
|
className={groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 1 ? "md:col-span-2" : ""}
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<Gpu className="h-4 w-4 text-muted-foreground" />
|
<Gpu className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">GPU</h3>
|
<h3 className="text-sm font-semibold">GPU</h3>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).GPU.length}
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).GPU.length > 1 ? "md:grid-cols-2" : ""}`}
|
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 1 ? "md:grid-cols-2" : ""}`}
|
||||||
>
|
>
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).GPU.map((temp, index) => {
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.map((temp, index) => {
|
||||||
const percentage =
|
const percentage =
|
||||||
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
||||||
const isHot = temp.current > (temp.high || 80)
|
const isHot = temp.current > (temp.high || 80)
|
||||||
@@ -517,21 +575,23 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* NVME Sensors */}
|
{/* NVME Sensors */}
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).NVME.length > 0 && (
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className={groupAndSortTemperatures(hardwareData.temperatures).NVME.length > 1 ? "md:col-span-2" : ""}
|
className={
|
||||||
|
groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 1 ? "md:col-span-2" : ""
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">NVME</h3>
|
<h3 className="text-sm font-semibold">NVME</h3>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).NVME.length}
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).NVME.length > 1 ? "md:grid-cols-2" : ""}`}
|
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 1 ? "md:grid-cols-2" : ""}`}
|
||||||
>
|
>
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).NVME.map((temp, index) => {
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.map((temp, index) => {
|
||||||
const percentage =
|
const percentage =
|
||||||
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
||||||
const isHot = temp.current > (temp.high || 80)
|
const isHot = temp.current > (temp.high || 80)
|
||||||
@@ -562,21 +622,21 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* PCI Sensors */}
|
{/* PCI Sensors */}
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).PCI.length > 0 && (
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className={groupAndSortTemperatures(hardwareData.temperatures).PCI.length > 1 ? "md:col-span-2" : ""}
|
className={groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 1 ? "md:col-span-2" : ""}
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<CpuIcon className="h-4 w-4 text-muted-foreground" />
|
<CpuIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">PCI</h3>
|
<h3 className="text-sm font-semibold">PCI</h3>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).PCI.length}
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).PCI.length > 1 ? "md:grid-cols-2" : ""}`}
|
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 1 ? "md:grid-cols-2" : ""}`}
|
||||||
>
|
>
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).PCI.map((temp, index) => {
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.map((temp, index) => {
|
||||||
const percentage =
|
const percentage =
|
||||||
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
||||||
const isHot = temp.current > (temp.high || 80)
|
const isHot = temp.current > (temp.high || 80)
|
||||||
@@ -607,21 +667,23 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* OTHER Sensors */}
|
{/* OTHER Sensors */}
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).OTHER.length > 0 && (
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className={groupAndSortTemperatures(hardwareData.temperatures).OTHER.length > 1 ? "md:col-span-2" : ""}
|
className={
|
||||||
|
groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 1 ? "md:col-span-2" : ""
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<Thermometer className="h-4 w-4 text-muted-foreground" />
|
<Thermometer className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">OTHER</h3>
|
<h3 className="text-sm font-semibold">OTHER</h3>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).OTHER.length}
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).OTHER.length > 1 ? "md:grid-cols-2" : ""}`}
|
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 1 ? "md:grid-cols-2" : ""}`}
|
||||||
>
|
>
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).OTHER.map((temp, index) => {
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.map((temp, index) => {
|
||||||
const percentage =
|
const percentage =
|
||||||
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
||||||
const isHot = temp.current > (temp.high || 80)
|
const isHot = temp.current > (temp.high || 80)
|
||||||
@@ -655,18 +717,18 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* GPU Information - Enhanced with on-demand data fetching */}
|
{/* GPU Information - Enhanced with on-demand data fetching */}
|
||||||
{hardwareData?.gpus && hardwareData.gpus.length > 0 && (
|
{hardwareDataSWR?.gpus && hardwareDataSWR.gpus.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Gpu className="h-5 w-5 text-primary" />
|
<Gpu className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">Graphics Cards</h2>
|
<h2 className="text-lg font-semibold">Graphics Cards</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.gpus.length} GPU{hardwareData.gpus.length > 1 ? "s" : ""}
|
{hardwareDataSWR.gpus.length} GPU{hardwareDataSWR.gpus.length > 1 ? "s" : ""}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
{hardwareData.gpus.map((gpu, index) => {
|
{hardwareDataSWR.gpus.map((gpu, index) => {
|
||||||
const pciDevice = findPCIDeviceForGPU(gpu)
|
const pciDevice = findPCIDeviceForGPU(gpu)
|
||||||
const fullSlot = pciDevice?.slot || gpu.slot
|
const fullSlot = pciDevice?.slot || gpu.slot
|
||||||
|
|
||||||
@@ -1044,18 +1106,18 @@ export default function Hardware() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* PCI Devices - Changed to modal */}
|
{/* PCI Devices - Changed to modal */}
|
||||||
{hardwareData?.pci_devices && hardwareData.pci_devices.length > 0 && (
|
{hardwareDataSWR?.pci_devices && hardwareDataSWR.pci_devices.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<CpuIcon className="h-5 w-5 text-primary" />
|
<CpuIcon className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">PCI Devices</h2>
|
<h2 className="text-lg font-semibold">PCI Devices</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.pci_devices.length} devices
|
{hardwareDataSWR.pci_devices.length} devices
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{hardwareData.pci_devices.map((device, index) => (
|
{hardwareDataSWR.pci_devices.map((device, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => setSelectedPCIDevice(device)}
|
onClick={() => setSelectedPCIDevice(device)}
|
||||||
@@ -1130,7 +1192,7 @@ export default function Hardware() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Power Consumption */}
|
{/* Power Consumption */}
|
||||||
{hardwareData?.power_meter && (
|
{hardwareDataSWR?.power_meter && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Zap className="h-5 w-5 text-blue-500" />
|
<Zap className="h-5 w-5 text-blue-500" />
|
||||||
@@ -1140,13 +1202,13 @@ export default function Hardware() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between rounded-lg border border-border/30 bg-background/60 p-4">
|
<div className="flex items-center justify-between rounded-lg border border-border/30 bg-background/60 p-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-medium">{hardwareData.power_meter.name}</p>
|
<p className="text-sm font-medium">{hardwareDataSWR.power_meter.name}</p>
|
||||||
{hardwareData.power_meter.adapter && (
|
{hardwareDataSWR.power_meter.adapter && (
|
||||||
<p className="text-xs text-muted-foreground">{hardwareData.power_meter.adapter}</p>
|
<p className="text-xs text-muted-foreground">{hardwareDataSWR.power_meter.adapter}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-2xl font-bold text-blue-500">{hardwareData.power_meter.watts.toFixed(1)} W</p>
|
<p className="text-2xl font-bold text-blue-500">{hardwareDataSWR.power_meter.watts.toFixed(1)} W</p>
|
||||||
<p className="text-xs text-muted-foreground">Current Draw</p>
|
<p className="text-xs text-muted-foreground">Current Draw</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1155,18 +1217,18 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Power Supplies */}
|
{/* Power Supplies */}
|
||||||
{hardwareData?.power_supplies && hardwareData.power_supplies.length > 0 && (
|
{hardwareDataSWR?.power_supplies && hardwareDataSWR.power_supplies.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<PowerIcon className="h-5 w-5 text-green-500" />
|
<PowerIcon className="h-5 w-5 text-green-500" />
|
||||||
<h2 className="text-lg font-semibold">Power Supplies</h2>
|
<h2 className="text-lg font-semibold">Power Supplies</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.power_supplies.length} PSUs
|
{hardwareDataSWR.power_supplies.length} PSUs
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
{hardwareData.power_supplies.map((psu, index) => (
|
{hardwareDataSWR.power_supplies.map((psu, index) => (
|
||||||
<div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4">
|
<div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-medium">{psu.name}</span>
|
<span className="text-sm font-medium">{psu.name}</span>
|
||||||
@@ -1183,18 +1245,18 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Fans */}
|
{/* Fans */}
|
||||||
{hardwareData?.fans && hardwareData.fans.length > 0 && (
|
{hardwareDataSWR?.fans && hardwareDataSWR.fans.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<FanIcon className="h-5 w-5 text-primary" />
|
<FanIcon className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">System Fans</h2>
|
<h2 className="text-lg font-semibold">System Fans</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.fans.length} fans
|
{hardwareDataSWR.fans.length} fans
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{hardwareData.fans.map((fan, index) => {
|
{hardwareDataSWR.fans.map((fan, index) => {
|
||||||
const isPercentage = fan.unit === "percent" || fan.unit === "%"
|
const isPercentage = fan.unit === "percent" || fan.unit === "%"
|
||||||
const percentage = isPercentage ? fan.speed : Math.min((fan.speed / 5000) * 100, 100)
|
const percentage = isPercentage ? fan.speed : Math.min((fan.speed / 5000) * 100, 100)
|
||||||
|
|
||||||
@@ -1218,18 +1280,18 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* UPS */}
|
{/* UPS */}
|
||||||
{hardwareData?.ups && Array.isArray(hardwareData.ups) && hardwareData.ups.length > 0 && (
|
{hardwareDataSWR?.ups && Array.isArray(hardwareDataSWR.ups) && hardwareDataSWR.ups.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Battery className="h-5 w-5 text-primary" />
|
<Battery className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">UPS Status</h2>
|
<h2 className="text-lg font-semibold">UPS Status</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.ups.length} UPS
|
{hardwareDataSWR.ups.length} UPS
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{hardwareData.ups.map((ups: any, index: number) => {
|
{hardwareDataSWR.ups.map((ups: any, index: number) => {
|
||||||
const batteryCharge =
|
const batteryCharge =
|
||||||
ups.battery_charge_raw || Number.parseFloat(ups.battery_charge?.replace("%", "") || "0")
|
ups.battery_charge_raw || Number.parseFloat(ups.battery_charge?.replace("%", "") || "0")
|
||||||
const loadPercent = ups.load_percent_raw || Number.parseFloat(ups.load_percent?.replace("%", "") || "0")
|
const loadPercent = ups.load_percent_raw || Number.parseFloat(ups.load_percent?.replace("%", "") || "0")
|
||||||
@@ -1500,19 +1562,19 @@ export default function Hardware() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Network Summary - Clickable */}
|
{/* Network Summary - Clickable */}
|
||||||
{hardwareData?.pci_devices &&
|
{hardwareDataSWR?.pci_devices &&
|
||||||
hardwareData.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length > 0 && (
|
hardwareDataSWR.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Network className="h-5 w-5 text-primary" />
|
<Network className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">Network Summary</h2>
|
<h2 className="text-lg font-semibold">Network Summary</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length} interfaces
|
{hardwareDataSWR.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length} interfaces
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{hardwareData.pci_devices
|
{hardwareDataSWR.pci_devices
|
||||||
.filter((d) => d.type.toLowerCase().includes("network"))
|
.filter((d) => d.type.toLowerCase().includes("network"))
|
||||||
.map((device, index) => (
|
.map((device, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -1592,14 +1654,14 @@ export default function Hardware() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Storage Summary - Clickable */}
|
{/* Storage Summary - Clickable */}
|
||||||
{hardwareData?.storage_devices && hardwareData.storage_devices.length > 0 && (
|
{hardwareDataSWR?.storage_devices && hardwareDataSWR.storage_devices.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<HardDrive className="h-5 w-5 text-primary" />
|
<HardDrive className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">Storage Summary</h2>
|
<h2 className="text-lg font-semibold">Storage Summary</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{
|
{
|
||||||
hardwareData.storage_devices.filter(
|
hardwareDataSWR.storage_devices.filter(
|
||||||
(device) =>
|
(device) =>
|
||||||
device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"),
|
device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"),
|
||||||
).length
|
).length
|
||||||
@@ -1609,7 +1671,7 @@ export default function Hardware() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{hardwareData.storage_devices
|
{hardwareDataSWR.storage_devices
|
||||||
.filter(
|
.filter(
|
||||||
(device) => device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"),
|
(device) => device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"),
|
||||||
)
|
)
|
||||||
@@ -1653,6 +1715,61 @@ export default function Hardware() {
|
|||||||
|
|
||||||
const diskBadge = getDiskTypeBadge(device.name, device.rotation_rate)
|
const diskBadge = getDiskTypeBadge(device.name, device.rotation_rate)
|
||||||
|
|
||||||
|
const getLinkSpeedInfo = (device: StorageDevice) => {
|
||||||
|
// NVMe PCIe information
|
||||||
|
if (device.name.startsWith("nvme") && (device.pcie_gen || device.pcie_width)) {
|
||||||
|
const current = `${device.pcie_gen || ""} ${device.pcie_width || ""}`.trim()
|
||||||
|
const max =
|
||||||
|
device.pcie_max_gen && device.pcie_max_width
|
||||||
|
? `${device.pcie_max_gen} ${device.pcie_max_width}`.trim()
|
||||||
|
: null
|
||||||
|
|
||||||
|
const isLowerSpeed = max && current !== max
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: current || null,
|
||||||
|
maxText: max,
|
||||||
|
isWarning: isLowerSpeed,
|
||||||
|
color: isLowerSpeed ? "text-orange-500" : "text-blue-500",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SATA information
|
||||||
|
if (device.sata_version) {
|
||||||
|
return {
|
||||||
|
text: device.sata_version,
|
||||||
|
maxText: null,
|
||||||
|
isWarning: false,
|
||||||
|
color: "text-blue-500",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAS information
|
||||||
|
if (device.sas_version || device.sas_speed) {
|
||||||
|
const text = [device.sas_version, device.sas_speed].filter(Boolean).join(" ")
|
||||||
|
return {
|
||||||
|
text: text || null,
|
||||||
|
maxText: null,
|
||||||
|
isWarning: false,
|
||||||
|
color: "text-blue-500",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic link speed
|
||||||
|
if (device.link_speed) {
|
||||||
|
return {
|
||||||
|
text: device.link_speed,
|
||||||
|
maxText: null,
|
||||||
|
isWarning: false,
|
||||||
|
color: "text-blue-500",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkSpeed = getLinkSpeedInfo(device)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
@@ -1667,6 +1784,14 @@ export default function Hardware() {
|
|||||||
{device.model && (
|
{device.model && (
|
||||||
<p className="text-xs text-muted-foreground line-clamp-2 break-words">{device.model}</p>
|
<p className="text-xs text-muted-foreground line-clamp-2 break-words">{device.model}</p>
|
||||||
)}
|
)}
|
||||||
|
{linkSpeed && (
|
||||||
|
<div className="mt-1 flex items-center gap-1">
|
||||||
|
<span className={`text-xs font-medium ${linkSpeed.color}`}>{linkSpeed.text}</span>
|
||||||
|
{linkSpeed.maxText && linkSpeed.isWarning && (
|
||||||
|
<span className="text-xs font-medium text-blue-500">(max: {linkSpeed.maxText})</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -1690,46 +1815,44 @@ export default function Hardware() {
|
|||||||
<span className="font-mono text-sm">{selectedDisk.name}</span>
|
<span className="font-mono text-sm">{selectedDisk.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedDisk.name && (
|
<div className="flex justify-between border-b border-border/50 pb-2">
|
||||||
<div className="flex justify-between border-b border-border/50 pb-2">
|
<span className="text-sm font-medium text-muted-foreground">Type</span>
|
||||||
<span className="text-sm font-medium text-muted-foreground">Type</span>
|
{(() => {
|
||||||
{(() => {
|
const getDiskTypeBadge = (diskName: string, rotationRate: number | string | undefined) => {
|
||||||
const getDiskTypeBadge = (diskName: string, rotationRate: number | string | undefined) => {
|
let diskType = "HDD"
|
||||||
let diskType = "HDD"
|
|
||||||
|
|
||||||
if (diskName.startsWith("nvme")) {
|
if (diskName.startsWith("nvme")) {
|
||||||
diskType = "NVMe"
|
diskType = "NVMe"
|
||||||
} else if (rotationRate !== undefined && rotationRate !== null) {
|
} else if (rotationRate !== undefined && rotationRate !== null) {
|
||||||
const rateNum = typeof rotationRate === "string" ? Number.parseInt(rotationRate) : rotationRate
|
const rateNum = typeof rotationRate === "string" ? Number.parseInt(rotationRate) : rotationRate
|
||||||
if (rateNum === 0 || isNaN(rateNum)) {
|
if (rateNum === 0 || isNaN(rateNum)) {
|
||||||
diskType = "SSD"
|
|
||||||
}
|
|
||||||
} else if (typeof rotationRate === "string" && rotationRate.includes("Solid State")) {
|
|
||||||
diskType = "SSD"
|
diskType = "SSD"
|
||||||
}
|
}
|
||||||
|
} else if (typeof rotationRate === "string" && rotationRate.includes("Solid State")) {
|
||||||
const badgeStyles: Record<string, { className: string; label: string }> = {
|
diskType = "SSD"
|
||||||
NVMe: {
|
|
||||||
className: "bg-purple-500/10 text-purple-500 border-purple-500/20",
|
|
||||||
label: "NVMe SSD",
|
|
||||||
},
|
|
||||||
SSD: {
|
|
||||||
className: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20",
|
|
||||||
label: "SSD",
|
|
||||||
},
|
|
||||||
HDD: {
|
|
||||||
className: "bg-blue-500/10 text-blue-500 border-blue-500/20",
|
|
||||||
label: "HDD",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return badgeStyles[diskType]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const diskBadge = getDiskTypeBadge(selectedDisk.name, selectedDisk.rotation_rate)
|
const badgeStyles: Record<string, { className: string; label: string }> = {
|
||||||
return <Badge className={diskBadge.className}>{diskBadge.label}</Badge>
|
NVMe: {
|
||||||
})()}
|
className: "bg-purple-500/10 text-purple-500 border-purple-500/20",
|
||||||
</div>
|
label: "NVMe SSD",
|
||||||
)}
|
},
|
||||||
|
SSD: {
|
||||||
|
className: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20",
|
||||||
|
label: "SSD",
|
||||||
|
},
|
||||||
|
HDD: {
|
||||||
|
className: "bg-blue-500/10 text-blue-500 border-blue-500/20",
|
||||||
|
label: "HDD",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return badgeStyles[diskType]
|
||||||
|
}
|
||||||
|
|
||||||
|
const diskBadge = getDiskTypeBadge(selectedDisk.name, selectedDisk.rotation_rate)
|
||||||
|
return <Badge className={diskBadge.className}>{diskBadge.label}</Badge>
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
{selectedDisk.size && (
|
{selectedDisk.size && (
|
||||||
<div className="flex justify-between border-b border-border/50 pb-2">
|
<div className="flex justify-between border-b border-border/50 pb-2">
|
||||||
@@ -1738,6 +1861,84 @@ export default function Hardware() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<h3 className="text-sm font-semibold text-muted-foreground mb-2 uppercase tracking-wide">
|
||||||
|
Interface Information
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* NVMe PCIe Information */}
|
||||||
|
{selectedDisk.name.startsWith("nvme") && (
|
||||||
|
<>
|
||||||
|
{selectedDisk.pcie_gen || selectedDisk.pcie_width ? (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between border-b border-border/50 pb-2">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Current Link Speed</span>
|
||||||
|
<span
|
||||||
|
className={`text-sm font-medium ${
|
||||||
|
selectedDisk.pcie_max_gen &&
|
||||||
|
selectedDisk.pcie_max_width &&
|
||||||
|
`${selectedDisk.pcie_gen} ${selectedDisk.pcie_width}` !==
|
||||||
|
`${selectedDisk.pcie_max_gen} ${selectedDisk.pcie_max_width}`
|
||||||
|
? "text-orange-500"
|
||||||
|
: "text-blue-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{selectedDisk.pcie_gen || "PCIe"} {selectedDisk.pcie_width || ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{selectedDisk.pcie_max_gen && selectedDisk.pcie_max_width && (
|
||||||
|
<div className="flex justify-between border-b border-border/50 pb-2">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Maximum Link Speed</span>
|
||||||
|
<span className="text-sm font-medium text-blue-500">
|
||||||
|
{selectedDisk.pcie_max_gen} {selectedDisk.pcie_max_width}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-between border-b border-border/50 pb-2">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">PCIe Link Speed</span>
|
||||||
|
<span className="text-sm text-muted-foreground italic">Detecting...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SATA Information */}
|
||||||
|
{!selectedDisk.name.startsWith("nvme") && selectedDisk.sata_version && (
|
||||||
|
<div className="flex justify-between border-b border-border/50 pb-2">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">SATA Version</span>
|
||||||
|
<span className="text-sm font-medium text-blue-500">{selectedDisk.sata_version}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SAS Information */}
|
||||||
|
{!selectedDisk.name.startsWith("nvme") && selectedDisk.sas_version && (
|
||||||
|
<div className="flex justify-between border-b border-border/50 pb-2">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">SAS Version</span>
|
||||||
|
<span className="text-sm font-medium text-blue-500">{selectedDisk.sas_version}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!selectedDisk.name.startsWith("nvme") && selectedDisk.sas_speed && (
|
||||||
|
<div className="flex justify-between border-b border-border/50 pb-2">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">SAS Speed</span>
|
||||||
|
<span className="text-sm font-medium text-blue-500">{selectedDisk.sas_speed}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Generic Link Speed - only show if no specific interface info */}
|
||||||
|
{!selectedDisk.name.startsWith("nvme") &&
|
||||||
|
selectedDisk.link_speed &&
|
||||||
|
!selectedDisk.pcie_gen &&
|
||||||
|
!selectedDisk.sata_version &&
|
||||||
|
!selectedDisk.sas_version && (
|
||||||
|
<div className="flex justify-between border-b border-border/50 pb-2">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Link Speed</span>
|
||||||
|
<span className="text-sm font-medium text-blue-500">{selectedDisk.link_speed}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedDisk.model && (
|
{selectedDisk.model && (
|
||||||
<div className="flex justify-between border-b border-border/50 pb-2">
|
<div className="flex justify-between border-b border-border/50 pb-2">
|
||||||
<span className="text-sm font-medium text-muted-foreground">Model</span>
|
<span className="text-sm font-medium text-muted-foreground">Model</span>
|
||||||
@@ -1783,13 +1984,15 @@ export default function Hardware() {
|
|||||||
{selectedDisk.rotation_rate !== undefined && selectedDisk.rotation_rate !== null && (
|
{selectedDisk.rotation_rate !== undefined && selectedDisk.rotation_rate !== null && (
|
||||||
<div className="flex justify-between border-b border-border/50 pb-2">
|
<div className="flex justify-between border-b border-border/50 pb-2">
|
||||||
<span className="text-sm font-medium text-muted-foreground">Rotation Rate</span>
|
<span className="text-sm font-medium text-muted-foreground">Rotation Rate</span>
|
||||||
<span className="text-sm">
|
<div className="text-sm">
|
||||||
{typeof selectedDisk.rotation_rate === "number" && selectedDisk.rotation_rate > 0
|
{typeof selectedDisk.rotation_rate === "number" && selectedDisk.rotation_rate === -1
|
||||||
? `${selectedDisk.rotation_rate} rpm`
|
? "N/A"
|
||||||
: typeof selectedDisk.rotation_rate === "string"
|
: typeof selectedDisk.rotation_rate === "number" && selectedDisk.rotation_rate > 0
|
||||||
? selectedDisk.rotation_rate
|
? `${selectedDisk.rotation_rate} rpm`
|
||||||
: "Solid State Device"}
|
: typeof selectedDisk.rotation_rate === "string"
|
||||||
</span>
|
? selectedDisk.rotation_rate
|
||||||
|
: "Solid State Device"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1799,13 +2002,6 @@ export default function Hardware() {
|
|||||||
<span className="text-sm">{selectedDisk.form_factor}</span>
|
<span className="text-sm">{selectedDisk.form_factor}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedDisk.sata_version && (
|
|
||||||
<div className="flex justify-between border-b border-border/50 pb-2">
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">SATA Version</span>
|
|
||||||
<span className="text-sm">{selectedDisk.sata_version}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -0,0 +1,363 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type React from "react"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Loader2,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertTriangle,
|
||||||
|
XCircle,
|
||||||
|
Activity,
|
||||||
|
Cpu,
|
||||||
|
MemoryStick,
|
||||||
|
HardDrive,
|
||||||
|
Disc,
|
||||||
|
Network,
|
||||||
|
Box,
|
||||||
|
Settings,
|
||||||
|
FileText,
|
||||||
|
RefreshCw,
|
||||||
|
Shield,
|
||||||
|
X,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
interface CategoryCheck {
|
||||||
|
status: string
|
||||||
|
reason?: string
|
||||||
|
details?: any
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HealthDetails {
|
||||||
|
overall: string
|
||||||
|
summary: string
|
||||||
|
details: {
|
||||||
|
cpu: CategoryCheck
|
||||||
|
memory: CategoryCheck
|
||||||
|
storage: CategoryCheck
|
||||||
|
disks: CategoryCheck
|
||||||
|
network: CategoryCheck
|
||||||
|
vms: CategoryCheck
|
||||||
|
services: CategoryCheck
|
||||||
|
logs: CategoryCheck
|
||||||
|
updates: CategoryCheck
|
||||||
|
security: CategoryCheck
|
||||||
|
}
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HealthStatusModalProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
getApiUrl: (path: string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
const CATEGORIES = [
|
||||||
|
{ key: "cpu", label: "CPU Usage & Temperature", Icon: Cpu },
|
||||||
|
{ key: "memory", label: "Memory & Swap", Icon: MemoryStick },
|
||||||
|
{ key: "storage", label: "Storage Mounts & Space", Icon: HardDrive },
|
||||||
|
{ key: "disks", label: "Disk I/O & Errors", Icon: Disc },
|
||||||
|
{ key: "network", label: "Network Interfaces", Icon: Network },
|
||||||
|
{ key: "vms", label: "VMs & Containers", Icon: Box },
|
||||||
|
{ key: "services", label: "PVE Services", Icon: Settings },
|
||||||
|
{ key: "logs", label: "System Logs", Icon: FileText },
|
||||||
|
{ key: "updates", label: "System Updates", Icon: RefreshCw },
|
||||||
|
{ key: "security", label: "Security & Certificates", Icon: Shield },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatusModalProps) {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [healthData, setHealthData] = useState<HealthDetails | null>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
fetchHealthDetails()
|
||||||
|
}
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
const fetchHealthDetails = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(getApiUrl("/api/health/details"))
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch health details")
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
console.log("[v0] Health data received:", data)
|
||||||
|
setHealthData(data)
|
||||||
|
|
||||||
|
const event = new CustomEvent("healthStatusUpdated", {
|
||||||
|
detail: { status: data.overall },
|
||||||
|
})
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[v0] Error fetching health data:", err)
|
||||||
|
setError(err instanceof Error ? err.message : "Unknown error")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
const statusUpper = status?.toUpperCase()
|
||||||
|
switch (statusUpper) {
|
||||||
|
case "OK":
|
||||||
|
return <CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||||
|
case "WARNING":
|
||||||
|
return <AlertTriangle className="h-5 w-5 text-yellow-500" />
|
||||||
|
case "CRITICAL":
|
||||||
|
return <XCircle className="h-5 w-5 text-red-500" />
|
||||||
|
default:
|
||||||
|
return <Activity className="h-5 w-5 text-gray-500" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string) => {
|
||||||
|
const statusUpper = status?.toUpperCase()
|
||||||
|
switch (statusUpper) {
|
||||||
|
case "OK":
|
||||||
|
return <Badge className="bg-green-500 text-white hover:bg-green-500">OK</Badge>
|
||||||
|
case "WARNING":
|
||||||
|
return <Badge className="bg-yellow-500 text-white hover:bg-yellow-500">Warning</Badge>
|
||||||
|
case "CRITICAL":
|
||||||
|
return <Badge className="bg-red-500 text-white hover:bg-red-500">Critical</Badge>
|
||||||
|
default:
|
||||||
|
return <Badge>Unknown</Badge>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHealthStats = () => {
|
||||||
|
if (!healthData?.details) {
|
||||||
|
return { total: 0, healthy: 0, warnings: 0, critical: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
let healthy = 0
|
||||||
|
let warnings = 0
|
||||||
|
let critical = 0
|
||||||
|
|
||||||
|
CATEGORIES.forEach(({ key }) => {
|
||||||
|
const categoryData = healthData.details[key as keyof typeof healthData.details]
|
||||||
|
if (categoryData) {
|
||||||
|
const status = categoryData.status?.toUpperCase()
|
||||||
|
if (status === "OK") healthy++
|
||||||
|
else if (status === "WARNING") warnings++
|
||||||
|
else if (status === "CRITICAL") critical++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { total: CATEGORIES.length, healthy, warnings, critical }
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = getHealthStats()
|
||||||
|
|
||||||
|
const handleCategoryClick = (categoryKey: string, status: string) => {
|
||||||
|
if (status === "OK") return // No navegar si está OK
|
||||||
|
|
||||||
|
onOpenChange(false) // Cerrar el modal
|
||||||
|
|
||||||
|
// Mapear categorías a tabs
|
||||||
|
const categoryToTab: Record<string, string> = {
|
||||||
|
storage: "storage",
|
||||||
|
disks: "storage",
|
||||||
|
network: "network",
|
||||||
|
vms: "vms",
|
||||||
|
logs: "logs",
|
||||||
|
hardware: "hardware",
|
||||||
|
services: "hardware",
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetTab = categoryToTab[categoryKey]
|
||||||
|
if (targetTab) {
|
||||||
|
// Disparar evento para cambiar tab
|
||||||
|
const event = new CustomEvent("changeTab", { detail: { tab: targetTab } })
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAcknowledge = async (errorKey: string, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation() // Prevent navigation
|
||||||
|
|
||||||
|
console.log("[v0] Dismissing error:", errorKey)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(getApiUrl("/api/health/acknowledge"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ error_key: errorKey }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json()
|
||||||
|
console.error("[v0] Acknowledge failed:", errorData)
|
||||||
|
throw new Error(errorData.error || "Failed to acknowledge error")
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
console.log("[v0] Acknowledge success:", result)
|
||||||
|
|
||||||
|
// Refresh health data
|
||||||
|
await fetchHealthDetails()
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[v0] Error acknowledging:", err)
|
||||||
|
alert("Failed to dismiss error. Please try again.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[85vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<DialogTitle className="flex items-center gap-2 flex-1">
|
||||||
|
<Activity className="h-6 w-6" />
|
||||||
|
System Health Status
|
||||||
|
{healthData && <div className="ml-2">{getStatusBadge(healthData.overall)}</div>}
|
||||||
|
</DialogTitle>
|
||||||
|
</div>
|
||||||
|
<DialogDescription>Detailed health checks for all system components</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-lg border border-red-200 bg-red-50 p-4 text-red-800 dark:bg-red-950 dark:border-red-800 dark:text-red-200">
|
||||||
|
<p className="font-medium">Error loading health status</p>
|
||||||
|
<p className="text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{healthData && !loading && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Overall Stats Summary */}
|
||||||
|
<div className="grid grid-cols-4 gap-3 p-4 rounded-lg bg-muted/30 border">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold">{stats.total}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Total Checks</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-500">{stats.healthy}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Healthy</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-yellow-500">{stats.warnings}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Warnings</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-red-500">{stats.critical}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Critical</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{healthData.summary && healthData.summary !== "All systems operational" && (
|
||||||
|
<div className="text-sm p-3 rounded-lg bg-muted/20 border">
|
||||||
|
<span className="font-medium text-foreground">{healthData.summary}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{CATEGORIES.map(({ key, label, Icon }) => {
|
||||||
|
const categoryData = healthData.details[key as keyof typeof healthData.details]
|
||||||
|
const status = categoryData?.status || "UNKNOWN"
|
||||||
|
const reason = categoryData?.reason
|
||||||
|
const details = categoryData?.details
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
onClick={() => handleCategoryClick(key, status)}
|
||||||
|
className={`flex items-start gap-3 p-3 rounded-lg border transition-colors ${
|
||||||
|
status === "OK"
|
||||||
|
? "bg-card border-border hover:bg-muted/30"
|
||||||
|
: status === "WARNING"
|
||||||
|
? "bg-yellow-500/5 border-yellow-500/20 hover:bg-yellow-500/10 cursor-pointer"
|
||||||
|
: status === "CRITICAL"
|
||||||
|
? "bg-red-500/5 border-red-500/20 hover:bg-red-500/10 cursor-pointer"
|
||||||
|
: "bg-muted/30 hover:bg-muted/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="mt-0.5 flex-shrink-0 flex items-center gap-2">
|
||||||
|
<Icon className="h-4 w-4 text-blue-500" />
|
||||||
|
{getStatusIcon(status)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between gap-2 mb-1">
|
||||||
|
<p className="font-medium text-sm">{label}</p>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`shrink-0 text-xs ${
|
||||||
|
status === "OK"
|
||||||
|
? "border-green-500 text-green-500 bg-transparent"
|
||||||
|
: status === "WARNING"
|
||||||
|
? "border-yellow-500 text-yellow-500 bg-yellow-500/5"
|
||||||
|
: status === "CRITICAL"
|
||||||
|
? "border-red-500 text-red-500 bg-red-500/5"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{reason && <p className="text-xs text-muted-foreground mt-1">{reason}</p>}
|
||||||
|
{details && typeof details === "object" && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{Object.entries(details).map(([detailKey, detailValue]: [string, any]) => {
|
||||||
|
if (typeof detailValue === "object" && detailValue !== null) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={detailKey}
|
||||||
|
className="flex items-start justify-between gap-2 text-xs pl-3 border-l-2 border-muted py-1"
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<span className="font-medium">{detailKey}:</span>
|
||||||
|
{detailValue.reason && (
|
||||||
|
<span className="ml-1 text-muted-foreground">{detailValue.reason}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{(status === "WARNING" || status === "CRITICAL") && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="h-6 px-2 shrink-0 hover:bg-red-500/10 hover:border-red-500/50 bg-transparent"
|
||||||
|
onClick={(e) => handleAcknowledge(detailKey, e)}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3 mr-1" />
|
||||||
|
<span className="text-xs">Dismiss</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{healthData.timestamp && (
|
||||||
|
<div className="text-xs text-muted-foreground text-center pt-2">
|
||||||
|
Last updated: {new Date(healthData.timestamp).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type React from "react"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { Button } from "./ui/button"
|
||||||
|
import { Input } from "./ui/input"
|
||||||
|
import { Label } from "./ui/label"
|
||||||
|
import { Checkbox } from "./ui/checkbox"
|
||||||
|
import { Lock, User, AlertCircle, Server, Shield } from "lucide-react"
|
||||||
|
import { getApiUrl } from "../lib/api-config"
|
||||||
|
import Image from "next/image"
|
||||||
|
|
||||||
|
interface LoginProps {
|
||||||
|
onLogin: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Login({ onLogin }: LoginProps) {
|
||||||
|
const [username, setUsername] = useState("")
|
||||||
|
const [password, setPassword] = useState("")
|
||||||
|
const [totpCode, setTotpCode] = useState("")
|
||||||
|
const [requiresTotp, setRequiresTotp] = useState(false)
|
||||||
|
const [rememberMe, setRememberMe] = useState(false)
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedUsername = localStorage.getItem("proxmenux-saved-username")
|
||||||
|
const savedPassword = localStorage.getItem("proxmenux-saved-password")
|
||||||
|
|
||||||
|
if (savedUsername && savedPassword) {
|
||||||
|
setUsername(savedUsername)
|
||||||
|
setPassword(savedPassword)
|
||||||
|
setRememberMe(true)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError("")
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
setError("Please enter username and password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiresTotp && !totpCode) {
|
||||||
|
setError("Please enter your 2FA code")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(getApiUrl("/api/auth/login"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
totp_token: totpCode || undefined, // Include 2FA code if provided
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.requires_totp) {
|
||||||
|
setRequiresTotp(true)
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || "Login failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("proxmenux-auth-token", data.token)
|
||||||
|
|
||||||
|
if (rememberMe) {
|
||||||
|
localStorage.setItem("proxmenux-saved-username", username)
|
||||||
|
localStorage.setItem("proxmenux-saved-password", password)
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem("proxmenux-saved-username")
|
||||||
|
localStorage.removeItem("proxmenux-saved-password")
|
||||||
|
}
|
||||||
|
|
||||||
|
onLogin()
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Login failed")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-md space-y-8">
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="w-20 h-20 relative flex items-center justify-center bg-primary/10 rounded-lg">
|
||||||
|
<Image
|
||||||
|
src="/images/proxmenux-logo.png"
|
||||||
|
alt="ProxMenux Logo"
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
className="object-contain"
|
||||||
|
priority
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement
|
||||||
|
target.style.display = "none"
|
||||||
|
const fallback = target.parentElement?.querySelector(".fallback-icon")
|
||||||
|
if (fallback) {
|
||||||
|
fallback.classList.remove("hidden")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Server className="h-12 w-12 text-primary absolute fallback-icon hidden" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">ProxMenux Monitor</h1>
|
||||||
|
<p className="text-muted-foreground mt-2">Sign in to access your dashboard</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 shadow-lg">
|
||||||
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-red-500">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!requiresTotp ? (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="login-username" className="text-sm">
|
||||||
|
Username
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="login-username"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter your username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className="pl-10 text-base"
|
||||||
|
disabled={loading}
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="login-password" className="text-sm">
|
||||||
|
Password
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="login-password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="pl-10 text-base"
|
||||||
|
disabled={loading}
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="remember-me"
|
||||||
|
checked={rememberMe}
|
||||||
|
onCheckedChange={(checked) => setRememberMe(checked as boolean)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="remember-me" className="text-sm font-normal cursor-pointer select-none">
|
||||||
|
Remember me
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<Shield className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-blue-500">Two-Factor Authentication</p>
|
||||||
|
<p className="text-xs text-blue-500 mt-1">Enter the 6-digit code from your authentication app</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="totp-code" className="text-sm">
|
||||||
|
Authentication Code
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="totp-code"
|
||||||
|
type="text"
|
||||||
|
placeholder="000000"
|
||||||
|
value={totpCode}
|
||||||
|
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
|
||||||
|
className="text-center text-lg tracking-widest font-mono text-base"
|
||||||
|
maxLength={6}
|
||||||
|
disabled={loading}
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground text-center">
|
||||||
|
You can also use a backup code (format: XXXX-XXXX)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setRequiresTotp(false)
|
||||||
|
setTotpCode("")
|
||||||
|
setError("")
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Back to login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full bg-blue-500 hover:bg-blue-600" disabled={loading}>
|
||||||
|
{loading ? "Signing in..." : requiresTotp ? "Verify Code" : "Sign In"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-center text-sm text-muted-foreground">ProxMenux Monitor v1.0.1</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { ArrowLeft, Loader2 } from "lucide-react"
|
import { ArrowLeft, Loader2 } from "lucide-react"
|
||||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
||||||
|
import { fetchApi } from "@/lib/api-config"
|
||||||
|
|
||||||
interface MetricsViewProps {
|
interface MetricsViewProps {
|
||||||
vmid: number
|
vmid: number
|
||||||
@@ -118,18 +119,7 @@ export function MetricsView({ vmid, vmName, vmType, onBack }: MetricsViewProps)
|
|||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baseUrl =
|
const result = await fetchApi<any>(`/api/vms/${vmid}/metrics?timeframe=${timeframe}`)
|
||||||
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 transformedData = result.data.map((item: any) => {
|
||||||
const date = new Date(item.time * 1000)
|
const date = new Date(item.time * 1000)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Card, CardContent } from "./ui/card"
|
|||||||
import { Badge } from "./ui/badge"
|
import { Badge } from "./ui/badge"
|
||||||
import { Wifi, Zap } from "lucide-react"
|
import { Wifi, Zap } from "lucide-react"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
|
import { fetchApi } from "../lib/api-config"
|
||||||
|
|
||||||
interface NetworkCardProps {
|
interface NetworkCardProps {
|
||||||
interface_: {
|
interface_: {
|
||||||
@@ -94,26 +95,12 @@ export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTrafficData = async () => {
|
const fetchTrafficData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/network/${interface_.name}/metrics?timeframe=${timeframe}`, {
|
const data = await fetchApi(`/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) {
|
if (data.data && data.data.length > 0) {
|
||||||
const lastPoint = data.data[data.data.length - 1]
|
const lastPoint = data.data[data.data.length - 1]
|
||||||
const firstPoint = data.data[0]
|
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 receivedGB = Math.max(0, (lastPoint.netin || 0) - (firstPoint.netin || 0))
|
||||||
const sentGB = Math.max(0, (lastPoint.netout || 0) - (firstPoint.netout || 0))
|
const sentGB = Math.max(0, (lastPoint.netout || 0) - (firstPoint.netout || 0))
|
||||||
|
|
||||||
@@ -124,16 +111,13 @@ export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[v0] Failed to fetch traffic data for card:", error)
|
console.error("[v0] Failed to fetch traffic data for card:", error)
|
||||||
// Keep showing 0 values on error
|
|
||||||
setTrafficData({ received: 0, sent: 0 })
|
setTrafficData({ received: 0, sent: 0 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only fetch if interface is up and not a VM
|
|
||||||
if (interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm") {
|
if (interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm") {
|
||||||
fetchTrafficData()
|
fetchTrafficData()
|
||||||
|
|
||||||
// Refresh every 60 seconds
|
|
||||||
const interval = setInterval(fetchTrafficData, 60000)
|
const interval = setInterval(fetchTrafficData, 60000)
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
||||||
import { Badge } from "./ui/badge"
|
import { Badge } from "./ui/badge"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog"
|
||||||
import { Wifi, Activity, Network, Router, AlertCircle, Zap } from "lucide-react"
|
import { Wifi, Activity, Network, Router, AlertCircle, Zap } from "lucide-react"
|
||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
import { NetworkTrafficChart } from "./network-traffic-chart"
|
import { NetworkTrafficChart } from "./network-traffic-chart"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||||
|
import { fetchApi } from "../lib/api-config"
|
||||||
|
|
||||||
interface NetworkData {
|
interface NetworkData {
|
||||||
interfaces: NetworkInterface[]
|
interfaces: NetworkInterface[]
|
||||||
@@ -128,19 +129,7 @@ const formatSpeed = (speed: number): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetcher = async (url: string): Promise<NetworkData> => {
|
const fetcher = async (url: string): Promise<NetworkData> => {
|
||||||
const response = await fetch(url, {
|
return fetchApi<NetworkData>(url)
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(5000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NetworkMetrics() {
|
export function NetworkMetrics() {
|
||||||
@@ -149,7 +138,7 @@ export function NetworkMetrics() {
|
|||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useSWR<NetworkData>("/api/network", fetcher, {
|
} = useSWR<NetworkData>("/api/network", fetcher, {
|
||||||
refreshInterval: 60000, // Refresh every 60 seconds
|
refreshInterval: 53000,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
revalidateOnReconnect: true,
|
revalidateOnReconnect: true,
|
||||||
})
|
})
|
||||||
@@ -161,13 +150,13 @@ export function NetworkMetrics() {
|
|||||||
const [interfaceTotals, setInterfaceTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
|
const [interfaceTotals, setInterfaceTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
|
||||||
|
|
||||||
const { data: modalNetworkData } = useSWR<NetworkData>(selectedInterface ? "/api/network" : null, fetcher, {
|
const { data: modalNetworkData } = useSWR<NetworkData>(selectedInterface ? "/api/network" : null, fetcher, {
|
||||||
refreshInterval: 15000, // Refresh every 15 seconds when modal is open
|
refreshInterval: 17000,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
revalidateOnReconnect: true,
|
revalidateOnReconnect: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: interfaceHistoricalData } = useSWR<any>(`/api/node/metrics?timeframe=${timeframe}`, fetcher, {
|
const { data: interfaceHistoricalData } = useSWR<any>(`/api/node/metrics?timeframe=${timeframe}`, fetcher, {
|
||||||
refreshInterval: 30000,
|
refreshInterval: 29000,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -688,6 +677,9 @@ export function NetworkMetrics() {
|
|||||||
<Router className="h-5 w-5" />
|
<Router className="h-5 w-5" />
|
||||||
{selectedInterface?.name} - Interface Details
|
{selectedInterface?.name} - Interface Details
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
View detailed information and network traffic statistics for this interface
|
||||||
|
</DialogDescription>
|
||||||
{selectedInterface?.status.toLowerCase() === "up" && selectedInterface?.vm_type !== "vm" && (
|
{selectedInterface?.status.toLowerCase() === "up" && selectedInterface?.vm_type !== "vm" && (
|
||||||
<div className="flex justify-end pt-2">
|
<div className="flex justify-end pt-2">
|
||||||
<Select value={modalTimeframe} onValueChange={(value: any) => setModalTimeframe(value)}>
|
<Select value={modalTimeframe} onValueChange={(value: any) => setModalTimeframe(value)}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
||||||
import { Loader2 } from "lucide-react"
|
import { Loader2 } from "lucide-react"
|
||||||
|
import { fetchApi } from "@/lib/api-config"
|
||||||
|
|
||||||
interface NetworkMetricsData {
|
interface NetworkMetricsData {
|
||||||
time: string
|
time: string
|
||||||
@@ -75,22 +76,13 @@ export function NetworkTrafficChart({
|
|||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baseUrl =
|
const apiPath = interfaceName
|
||||||
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
? `/api/network/${interfaceName}/metrics?timeframe=${timeframe}`
|
||||||
|
: `/api/node/metrics?timeframe=${timeframe}`
|
||||||
|
|
||||||
const apiUrl = interfaceName
|
console.log("[v0] Fetching network metrics from:", apiPath)
|
||||||
? `${baseUrl}/api/network/${interfaceName}/metrics?timeframe=${timeframe}`
|
|
||||||
: `${baseUrl}/api/node/metrics?timeframe=${timeframe}`
|
|
||||||
|
|
||||||
console.log("[v0] Fetching network metrics from:", apiUrl)
|
const result = await fetchApi<any>(apiPath)
|
||||||
|
|
||||||
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)) {
|
if (!result.data || !Array.isArray(result.data)) {
|
||||||
throw new Error("Invalid data format received from server")
|
throw new Error("Invalid data format received from server")
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
||||||
import { Loader2, TrendingUp, MemoryStick } from "lucide-react"
|
import { Loader2, TrendingUp, MemoryStick } from "lucide-react"
|
||||||
|
import { useIsMobile } from "../hooks/use-mobile"
|
||||||
|
import { fetchApi } from "@/lib/api-config"
|
||||||
|
|
||||||
const TIMEFRAME_OPTIONS = [
|
const TIMEFRAME_OPTIONS = [
|
||||||
{ value: "hour", label: "1 Hour" },
|
{ value: "hour", label: "1 Hour" },
|
||||||
@@ -69,6 +71,7 @@ export function NodeMetricsCharts() {
|
|||||||
const [data, setData] = useState<NodeMetricsData[]>([])
|
const [data, setData] = useState<NodeMetricsData[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
const [visibleLines, setVisibleLines] = useState({
|
const [visibleLines, setVisibleLines] = useState({
|
||||||
cpu: { cpu: true, load: true },
|
cpu: { cpu: true, load: true },
|
||||||
@@ -86,24 +89,8 @@ export function NodeMetricsCharts() {
|
|||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baseUrl =
|
const result = await fetchApi<any>(`/api/node/metrics?timeframe=${timeframe}`)
|
||||||
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] Node metrics result:", result)
|
||||||
console.log("[v0] Result keys:", Object.keys(result))
|
console.log("[v0] Result keys:", Object.keys(result))
|
||||||
console.log("[v0] Data array length:", result.data?.length || 0)
|
console.log("[v0] Data array length:", result.data?.length || 0)
|
||||||
@@ -318,15 +305,15 @@ export function NodeMetricsCharts() {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{/* CPU Usage + Load Average Chart */}
|
{/* CPU Usage + Load Average Chart */}
|
||||||
<Card className="bg-card border-border">
|
<Card className="bg-card border-border">
|
||||||
<CardHeader>
|
<CardHeader className="px-4 md:px-6">
|
||||||
<CardTitle className="text-foreground flex items-center">
|
<CardTitle className="text-foreground flex items-center">
|
||||||
<TrendingUp className="h-5 w-5 mr-2" />
|
<TrendingUp className="h-5 w-5 mr-2" />
|
||||||
CPU Usage & Load Average
|
CPU Usage & Load Average
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="px-0 md:px-6">
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<AreaChart data={data} margin={{ bottom: 60, left: 30, right: 10 }}>
|
<AreaChart data={data} margin={{ bottom: 60, left: 0, right: 0 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
|
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="time"
|
dataKey="time"
|
||||||
@@ -343,7 +330,9 @@ export function NodeMetricsCharts() {
|
|||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
className="text-foreground"
|
className="text-foreground"
|
||||||
tick={{ fill: "currentColor", fontSize: 12 }}
|
tick={{ fill: "currentColor", fontSize: 12 }}
|
||||||
label={{ value: "CPU %", angle: -90, position: "insideLeft", fill: "currentColor" }}
|
label={
|
||||||
|
isMobile ? undefined : { value: "CPU %", angle: -90, position: "insideLeft", fill: "currentColor" }
|
||||||
|
}
|
||||||
domain={[0, "dataMax"]}
|
domain={[0, "dataMax"]}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
@@ -352,7 +341,9 @@ export function NodeMetricsCharts() {
|
|||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
className="text-foreground"
|
className="text-foreground"
|
||||||
tick={{ fill: "currentColor", fontSize: 12 }}
|
tick={{ fill: "currentColor", fontSize: 12 }}
|
||||||
label={{ value: "Load", angle: 90, position: "insideRight", fill: "currentColor" }}
|
label={
|
||||||
|
isMobile ? undefined : { value: "Load", angle: 90, position: "insideRight", fill: "currentColor" }
|
||||||
|
}
|
||||||
domain={[0, "dataMax"]}
|
domain={[0, "dataMax"]}
|
||||||
/>
|
/>
|
||||||
<Tooltip content={<CustomCpuTooltip />} />
|
<Tooltip content={<CustomCpuTooltip />} />
|
||||||
@@ -386,15 +377,15 @@ export function NodeMetricsCharts() {
|
|||||||
|
|
||||||
{/* Memory Usage Chart */}
|
{/* Memory Usage Chart */}
|
||||||
<Card className="bg-card border-border">
|
<Card className="bg-card border-border">
|
||||||
<CardHeader>
|
<CardHeader className="px-4 md:px-6">
|
||||||
<CardTitle className="text-foreground flex items-center">
|
<CardTitle className="text-foreground flex items-center">
|
||||||
<MemoryStick className="h-5 w-5 mr-2" />
|
<MemoryStick className="h-5 w-5 mr-2" />
|
||||||
Memory Usage
|
Memory Usage
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="px-0 pr-2 md:px-6">
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<AreaChart data={data} margin={{ bottom: 60, left: 30, right: 10 }}>
|
<AreaChart data={data} margin={{ bottom: 60, left: 0, right: 0 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
|
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="time"
|
dataKey="time"
|
||||||
@@ -410,7 +401,9 @@ export function NodeMetricsCharts() {
|
|||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
className="text-foreground"
|
className="text-foreground"
|
||||||
tick={{ fill: "currentColor", fontSize: 12 }}
|
tick={{ fill: "currentColor", fontSize: 12 }}
|
||||||
label={{ value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }}
|
label={
|
||||||
|
isMobile ? undefined : { value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }
|
||||||
|
}
|
||||||
domain={[0, "dataMax"]}
|
domain={[0, "dataMax"]}
|
||||||
/>
|
/>
|
||||||
<Tooltip content={<CustomMemoryTooltip />} />
|
<Tooltip content={<CustomMemoryTooltip />} />
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Rocket,
|
Rocket,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
|
import { Checkbox } from "./ui/checkbox"
|
||||||
|
|
||||||
interface OnboardingSlide {
|
interface OnboardingSlide {
|
||||||
id: number
|
id: number
|
||||||
@@ -106,6 +107,7 @@ export function OnboardingCarousel() {
|
|||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [currentSlide, setCurrentSlide] = useState(0)
|
const [currentSlide, setCurrentSlide] = useState(0)
|
||||||
const [direction, setDirection] = useState<"next" | "prev">("next")
|
const [direction, setDirection] = useState<"next" | "prev">("next")
|
||||||
|
const [dontShowAgain, setDontShowAgain] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hasSeenOnboarding = localStorage.getItem("proxmenux-onboarding-seen")
|
const hasSeenOnboarding = localStorage.getItem("proxmenux-onboarding-seen")
|
||||||
@@ -119,6 +121,9 @@ export function OnboardingCarousel() {
|
|||||||
setDirection("next")
|
setDirection("next")
|
||||||
setCurrentSlide(currentSlide + 1)
|
setCurrentSlide(currentSlide + 1)
|
||||||
} else {
|
} else {
|
||||||
|
if (dontShowAgain) {
|
||||||
|
localStorage.setItem("proxmenux-onboarding-seen", "true")
|
||||||
|
}
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,11 +136,16 @@ export function OnboardingCarousel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSkip = () => {
|
const handleSkip = () => {
|
||||||
|
if (dontShowAgain) {
|
||||||
|
localStorage.setItem("proxmenux-onboarding-seen", "true")
|
||||||
|
}
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDontShowAgain = () => {
|
const handleClose = () => {
|
||||||
localStorage.setItem("proxmenux-onboarding-seen", "true")
|
if (dontShowAgain) {
|
||||||
|
localStorage.setItem("proxmenux-onboarding-seen", "true")
|
||||||
|
}
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,15 +157,14 @@ export function OnboardingCarousel() {
|
|||||||
const slide = slides[currentSlide]
|
const slide = slides[currentSlide]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={handleClose}>
|
||||||
<DialogContent className="max-w-4xl p-0 gap-0 overflow-hidden border-0 bg-transparent">
|
<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">
|
<div className="relative bg-card rounded-lg overflow-hidden shadow-2xl">
|
||||||
{/* Close button */}
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
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"
|
className="absolute top-4 right-4 z-50 h-8 w-8 rounded-full bg-background/80 backdrop-blur-sm hover:bg-background"
|
||||||
onClick={handleSkip}
|
onClick={handleClose}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -166,7 +175,6 @@ export function OnboardingCarousel() {
|
|||||||
<div className="absolute inset-0 bg-black/10" />
|
<div className="absolute inset-0 bg-black/10" />
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,rgba(255,255,255,0.1),transparent)]" />
|
<div className="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">
|
<div className="relative z-10 text-white">
|
||||||
{slide.image ? (
|
{slide.image ? (
|
||||||
<div className="relative w-full h-36 md:h-48 flex items-center justify-center px-4">
|
<div className="relative w-full h-36 md:h-48 flex items-center justify-center px-4">
|
||||||
@@ -192,20 +200,18 @@ export function OnboardingCarousel() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Decorative elements */}
|
|
||||||
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-2xl" />
|
<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 className="absolute bottom-10 right-10 w-32 h-32 bg-white/10 rounded-full blur-3xl" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 md:p-8 space-y-4 md:space-y-6">
|
<div className="p-4 md:p-8 space-y-3 md:space-y-6 max-h-[60vh] md:max-h-none overflow-y-auto">
|
||||||
<div className="space-y-2 md:space-y-3">
|
<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>
|
<h2 className="text-xl 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">
|
<p className="text-sm md:text-lg text-muted-foreground leading-relaxed text-pretty">
|
||||||
{slide.description}
|
{slide.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress dots */}
|
|
||||||
<div className="flex items-center justify-center gap-2 py-2 md:py-4">
|
<div className="flex items-center justify-center gap-2 py-2 md:py-4">
|
||||||
{slides.map((_, index) => (
|
{slides.map((_, index) => (
|
||||||
<button
|
<button
|
||||||
@@ -221,12 +227,12 @@ export function OnboardingCarousel() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 md:gap-4">
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-2 md:gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={handlePrev}
|
onClick={handlePrev}
|
||||||
disabled={currentSlide === 0}
|
disabled={currentSlide === 0}
|
||||||
className="gap-2 w-full sm:w-auto"
|
className="gap-2 w-full sm:w-auto text-sm"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
Previous
|
Previous
|
||||||
@@ -235,10 +241,17 @@ export function OnboardingCarousel() {
|
|||||||
<div className="flex gap-2 w-full sm:w-auto">
|
<div className="flex gap-2 w-full sm:w-auto">
|
||||||
{currentSlide < slides.length - 1 ? (
|
{currentSlide < slides.length - 1 ? (
|
||||||
<>
|
<>
|
||||||
<Button variant="outline" onClick={handleSkip} className="flex-1 sm:flex-none bg-transparent">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleSkip}
|
||||||
|
className="flex-1 sm:flex-none bg-transparent text-sm"
|
||||||
|
>
|
||||||
Skip
|
Skip
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleNext} className="gap-2 bg-blue-500 hover:bg-blue-600 flex-1 sm:flex-none">
|
<Button
|
||||||
|
onClick={handleNext}
|
||||||
|
className="gap-2 bg-blue-500 hover:bg-blue-600 flex-1 sm:flex-none text-sm"
|
||||||
|
>
|
||||||
Next
|
Next
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -246,7 +259,7 @@ export function OnboardingCarousel() {
|
|||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
className="gap-2 bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 w-full sm:w-auto"
|
className="gap-2 bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 w-full sm:w-auto text-sm"
|
||||||
>
|
>
|
||||||
Get Started!
|
Get Started!
|
||||||
<Sparkles className="h-4 w-4" />
|
<Sparkles className="h-4 w-4" />
|
||||||
@@ -255,17 +268,19 @@ export function OnboardingCarousel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Don't show again */}
|
<div className="flex items-center justify-center gap-2 pt-2 pb-1">
|
||||||
{currentSlide === slides.length - 1 && (
|
<Checkbox
|
||||||
<div className="text-center pt-2">
|
id="dont-show-again"
|
||||||
<button
|
checked={dontShowAgain}
|
||||||
onClick={handleDontShowAgain}
|
onCheckedChange={(checked) => setDontShowAgain(checked as boolean)}
|
||||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors underline"
|
/>
|
||||||
>
|
<label
|
||||||
Don't show again
|
htmlFor="dont-show-again"
|
||||||
</button>
|
className="text-xs md:text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer select-none"
|
||||||
</div>
|
>
|
||||||
)}
|
Don't show this again
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import { NetworkMetrics } from "./network-metrics"
|
|||||||
import { VirtualMachines } from "./virtual-machines"
|
import { VirtualMachines } from "./virtual-machines"
|
||||||
import Hardware from "./hardware"
|
import Hardware from "./hardware"
|
||||||
import { SystemLogs } from "./system-logs"
|
import { SystemLogs } from "./system-logs"
|
||||||
|
import { Settings } from "./settings"
|
||||||
import { OnboardingCarousel } from "./onboarding-carousel"
|
import { OnboardingCarousel } from "./onboarding-carousel"
|
||||||
|
import { HealthStatusModal } from "./health-status-modal"
|
||||||
|
import { ReleaseNotesModal, useVersionCheck } from "./release-notes-modal"
|
||||||
|
import { getApiUrl, fetchApi } from "../lib/api-config"
|
||||||
import {
|
import {
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
@@ -24,6 +28,7 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Cpu,
|
Cpu,
|
||||||
FileText,
|
FileText,
|
||||||
|
SettingsIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import { ThemeToggle } from "./theme-toggle"
|
import { ThemeToggle } from "./theme-toggle"
|
||||||
@@ -47,11 +52,20 @@ interface FlaskSystemData {
|
|||||||
load_average: number[]
|
load_average: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FlaskSystemInfo {
|
||||||
|
hostname: string
|
||||||
|
node_id: string
|
||||||
|
uptime: string
|
||||||
|
health: {
|
||||||
|
status: "healthy" | "warning" | "critical"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function ProxmoxDashboard() {
|
export function ProxmoxDashboard() {
|
||||||
const [systemStatus, setSystemStatus] = useState<SystemStatus>({
|
const [systemStatus, setSystemStatus] = useState<SystemStatus>({
|
||||||
status: "healthy",
|
status: "healthy",
|
||||||
uptime: "Loading...",
|
uptime: "Loading...",
|
||||||
lastUpdate: new Date().toLocaleTimeString(),
|
lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }),
|
||||||
serverName: "Loading...",
|
serverName: "Loading...",
|
||||||
nodeId: "Loading...",
|
nodeId: "Loading...",
|
||||||
})
|
})
|
||||||
@@ -62,55 +76,37 @@ export function ProxmoxDashboard() {
|
|||||||
const [activeTab, setActiveTab] = useState("overview")
|
const [activeTab, setActiveTab] = useState("overview")
|
||||||
const [showNavigation, setShowNavigation] = useState(true)
|
const [showNavigation, setShowNavigation] = useState(true)
|
||||||
const [lastScrollY, setLastScrollY] = useState(0)
|
const [lastScrollY, setLastScrollY] = useState(0)
|
||||||
|
const [showHealthModal, setShowHealthModal] = useState(false)
|
||||||
|
const { showReleaseNotes, setShowReleaseNotes } = useVersionCheck()
|
||||||
|
|
||||||
const fetchSystemData = useCallback(async () => {
|
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 {
|
try {
|
||||||
const response = await fetch(apiUrl, {
|
const data: FlaskSystemInfo = await fetchApi("/api/system-info")
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
cache: "no-store",
|
|
||||||
})
|
|
||||||
console.log("[v0] Response status:", response.status)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
const uptimeValue =
|
||||||
throw new Error(`Server responded with status: ${response.status}`)
|
data.uptime && typeof data.uptime === "string" && data.uptime.trim() !== "" ? data.uptime : "N/A"
|
||||||
}
|
|
||||||
|
|
||||||
const data: FlaskSystemData = await response.json()
|
const backendStatus = data.health?.status?.toUpperCase() || "OK"
|
||||||
console.log("[v0] System data received:", data)
|
let healthStatus: "healthy" | "warning" | "critical"
|
||||||
|
|
||||||
let status: "healthy" | "warning" | "critical" = "healthy"
|
if (backendStatus === "CRITICAL") {
|
||||||
if (data.cpu_usage > 90 || data.memory_usage > 90) {
|
healthStatus = "critical"
|
||||||
status = "critical"
|
} else if (backendStatus === "WARNING") {
|
||||||
} else if (data.cpu_usage > 75 || data.memory_usage > 75) {
|
healthStatus = "warning"
|
||||||
status = "warning"
|
} else {
|
||||||
|
healthStatus = "healthy"
|
||||||
}
|
}
|
||||||
|
|
||||||
setSystemStatus({
|
setSystemStatus({
|
||||||
status,
|
status: healthStatus,
|
||||||
uptime: data.uptime,
|
uptime: uptimeValue,
|
||||||
lastUpdate: new Date().toLocaleTimeString(),
|
lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }),
|
||||||
serverName: data.hostname,
|
serverName: data.hostname || "Unknown",
|
||||||
nodeId: data.node_id,
|
nodeId: data.node_id || "Unknown",
|
||||||
})
|
})
|
||||||
setIsServerConnected(true)
|
setIsServerConnected(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[v0] Failed to fetch system data from Flask server:", 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)
|
setIsServerConnected(false)
|
||||||
setSystemStatus((prev) => ({
|
setSystemStatus((prev) => ({
|
||||||
@@ -119,16 +115,67 @@ export function ProxmoxDashboard() {
|
|||||||
serverName: "Server Offline",
|
serverName: "Server Offline",
|
||||||
nodeId: "Server Offline",
|
nodeId: "Server Offline",
|
||||||
uptime: "N/A",
|
uptime: "N/A",
|
||||||
lastUpdate: new Date().toLocaleTimeString(),
|
lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Siempre fetch inicial
|
||||||
fetchSystemData()
|
fetchSystemData()
|
||||||
const interval = setInterval(fetchSystemData, 10000)
|
|
||||||
return () => clearInterval(interval)
|
// En overview: cada 30 segundos para actualización frecuente del estado de salud
|
||||||
}, [fetchSystemData])
|
// En otras tabs: cada 60 segundos para reducir carga
|
||||||
|
let interval: ReturnType<typeof setInterval> | null = null
|
||||||
|
if (activeTab === "overview") {
|
||||||
|
interval = setInterval(fetchSystemData, 30000) // 30 segundos
|
||||||
|
} else {
|
||||||
|
interval = setInterval(fetchSystemData, 60000) // 60 segundos
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (interval) clearInterval(interval)
|
||||||
|
}
|
||||||
|
}, [fetchSystemData, activeTab])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleChangeTab = (event: CustomEvent) => {
|
||||||
|
const { tab } = event.detail
|
||||||
|
if (tab) {
|
||||||
|
setActiveTab(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("changeTab", handleChangeTab as EventListener)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("changeTab", handleChangeTab as EventListener)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleHealthStatusUpdate = (event: CustomEvent) => {
|
||||||
|
const { status } = event.detail
|
||||||
|
let healthStatus: "healthy" | "warning" | "critical"
|
||||||
|
|
||||||
|
if (status === "CRITICAL") {
|
||||||
|
healthStatus = "critical"
|
||||||
|
} else if (status === "WARNING") {
|
||||||
|
healthStatus = "warning"
|
||||||
|
} else {
|
||||||
|
healthStatus = "healthy"
|
||||||
|
}
|
||||||
|
|
||||||
|
setSystemStatus((prev) => ({
|
||||||
|
...prev,
|
||||||
|
status: healthStatus,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("healthStatusUpdated", handleHealthStatusUpdate as EventListener)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("healthStatusUpdated", handleHealthStatusUpdate as EventListener)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -214,6 +261,8 @@ export function ProxmoxDashboard() {
|
|||||||
return "Hardware"
|
return "Hardware"
|
||||||
case "logs":
|
case "logs":
|
||||||
return "System Logs"
|
return "System Logs"
|
||||||
|
case "settings":
|
||||||
|
return "Settings"
|
||||||
default:
|
default:
|
||||||
return "Navigation Menu"
|
return "Navigation Menu"
|
||||||
}
|
}
|
||||||
@@ -222,6 +271,7 @@ export function ProxmoxDashboard() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<OnboardingCarousel />
|
<OnboardingCarousel />
|
||||||
|
<ReleaseNotesModal open={showReleaseNotes} onClose={() => setShowReleaseNotes(false)} />
|
||||||
|
|
||||||
{!isServerConnected && (
|
{!isServerConnected && (
|
||||||
<div className="bg-red-500/10 border-b border-red-500/20 px-6 py-3">
|
<div className="bg-red-500/10 border-b border-red-500/20 px-6 py-3">
|
||||||
@@ -235,13 +285,8 @@ export function ProxmoxDashboard() {
|
|||||||
<p>• The ProxMenux server should start automatically on port 8008</p>
|
<p>• The ProxMenux server should start automatically on port 8008</p>
|
||||||
<p>
|
<p>
|
||||||
• Try accessing:{" "}
|
• Try accessing:{" "}
|
||||||
<a
|
<a href={getApiUrl("/api/health")} target="_blank" rel="noopener noreferrer" className="underline">
|
||||||
href={`http://${typeof window !== "undefined" ? window.location.host : "localhost:8008"}/api/health`}
|
{getApiUrl("/api/health")}
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="underline"
|
|
||||||
>
|
|
||||||
http://{typeof window !== "undefined" ? window.location.host : "localhost:8008"}/api/health
|
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -249,7 +294,10 @@ export function ProxmoxDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<header className="border-b border-border bg-card sticky top-0 z-50 shadow-sm">
|
<header
|
||||||
|
className="border-b border-border bg-card sticky top-0 z-50 shadow-sm cursor-pointer hover:bg-accent/5 transition-colors"
|
||||||
|
onClick={() => setShowHealthModal(true)}
|
||||||
|
>
|
||||||
<div className="container mx-auto px-4 md:px-6 py-4 md:py-4">
|
<div className="container mx-auto px-4 md:px-6 py-4 md:py-4">
|
||||||
{/* Logo and Title */}
|
{/* Logo and Title */}
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
@@ -299,12 +347,17 @@ export function ProxmoxDashboard() {
|
|||||||
<span className="ml-1 capitalize">{systemStatus.status}</span>
|
<span className="ml-1 capitalize">{systemStatus.status}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<div className="text-sm text-muted-foreground whitespace-nowrap">Uptime: {systemStatus.uptime}</div>
|
<div className="text-sm text-muted-foreground whitespace-nowrap">
|
||||||
|
Uptime: {systemStatus.uptime || "N/A"}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={refreshData}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
refreshData()
|
||||||
|
}}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
className="border-border/50 bg-transparent hover:bg-secondary"
|
className="border-border/50 bg-transparent hover:bg-secondary"
|
||||||
>
|
>
|
||||||
@@ -312,7 +365,9 @@ export function ProxmoxDashboard() {
|
|||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<ThemeToggle />
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Actions */}
|
{/* Mobile Actions */}
|
||||||
@@ -322,17 +377,28 @@ export function ProxmoxDashboard() {
|
|||||||
<span className="ml-1 capitalize hidden sm:inline">{systemStatus.status}</span>
|
<span className="ml-1 capitalize hidden sm:inline">{systemStatus.status}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<Button variant="ghost" size="sm" onClick={refreshData} disabled={isRefreshing} className="h-8 w-8 p-0">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
refreshData()
|
||||||
|
}}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<ThemeToggle />
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Server Info */}
|
{/* Mobile Server Info */}
|
||||||
<div className="lg:hidden mt-2 flex items-center justify-end text-xs text-muted-foreground">
|
<div className="lg:hidden mt-2 flex items-center justify-end text-xs text-muted-foreground">
|
||||||
<span className="whitespace-nowrap">Uptime: {systemStatus.uptime}</span>
|
<span className="whitespace-nowrap">Uptime: {systemStatus.uptime || "N/A"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -346,7 +412,7 @@ export function ProxmoxDashboard() {
|
|||||||
>
|
>
|
||||||
<div className="container mx-auto px-4 md:px-6 pt-4 md:pt-6">
|
<div className="container mx-auto px-4 md:px-6 pt-4 md:pt-6">
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-0">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-0">
|
||||||
<TabsList className="hidden md:grid w-full grid-cols-6 bg-card border border-border">
|
<TabsList className="hidden md:grid w-full grid-cols-7 bg-card border border-border">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="overview"
|
value="overview"
|
||||||
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
||||||
@@ -383,6 +449,12 @@ export function ProxmoxDashboard() {
|
|||||||
>
|
>
|
||||||
System Logs
|
System Logs
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="settings"
|
||||||
|
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
|
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
|
||||||
@@ -491,6 +563,21 @@ export function ProxmoxDashboard() {
|
|||||||
<FileText className="h-5 w-5" />
|
<FileText className="h-5 w-5" />
|
||||||
<span>System Logs</span>
|
<span>System Logs</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveTab("settings")
|
||||||
|
setMobileMenuOpen(false)
|
||||||
|
}}
|
||||||
|
className={`w-full justify-start gap-3 ${
|
||||||
|
activeTab === "settings"
|
||||||
|
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<SettingsIcon className="h-5 w-5" />
|
||||||
|
<span>Settings</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
@@ -523,10 +610,14 @@ export function ProxmoxDashboard() {
|
|||||||
<TabsContent value="logs" className="space-y-4 md:space-y-6 mt-0">
|
<TabsContent value="logs" className="space-y-4 md:space-y-6 mt-0">
|
||||||
<SystemLogs key={`logs-${componentKey}`} />
|
<SystemLogs key={`logs-${componentKey}`} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="settings" className="space-y-4 md:space-y-6 mt-0">
|
||||||
|
<Settings />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</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">
|
<footer className="mt-8 md:mt-12 pt-4 md:pt-6 border-t border-border text-center text-xs md:text-sm text-muted-foreground">
|
||||||
<p className="font-medium mb-2">ProxMenux Monitor v1.0.0</p>
|
<p className="font-medium mb-2">ProxMenux Monitor v1.0.1</p>
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="https://ko-fi.com/macrimi"
|
href="https://ko-fi.com/macrimi"
|
||||||
@@ -539,6 +630,8 @@ export function ProxmoxDashboard() {
|
|||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<HealthStatusModal open={showHealthModal} onOpenChange={setShowHealthModal} getApiUrl={getApiUrl} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { Button } from "./ui/button"
|
||||||
|
import { Dialog, DialogContent } from "./ui/dialog"
|
||||||
|
import { X, Sparkles, Link2, Shield, Zap, HardDrive, Gauge, Wrench, Settings } from "lucide-react"
|
||||||
|
import { Checkbox } from "./ui/checkbox"
|
||||||
|
|
||||||
|
const APP_VERSION = "1.0.1" // Sync with AppImage/package.json
|
||||||
|
|
||||||
|
interface ReleaseNote {
|
||||||
|
date: string
|
||||||
|
changes: {
|
||||||
|
added?: string[]
|
||||||
|
changed?: string[]
|
||||||
|
fixed?: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CHANGELOG: Record<string, ReleaseNote> = {
|
||||||
|
"1.0.1": {
|
||||||
|
date: "November 11, 2025",
|
||||||
|
changes: {
|
||||||
|
added: [
|
||||||
|
"Proxy Support - Access ProxMenux through reverse proxies with full functionality",
|
||||||
|
"Authentication System - Secure your dashboard with password protection",
|
||||||
|
"PCIe Link Speed Detection - View NVMe drive connection speeds and detect performance issues",
|
||||||
|
"Enhanced Storage Display - Better formatting for disk sizes (auto-converts GB to TB when needed)",
|
||||||
|
"SATA/SAS Information - View detailed interface information for all storage devices",
|
||||||
|
"Two-Factor Authentication (2FA) - Enhanced security with TOTP support",
|
||||||
|
"Health Monitoring System - Comprehensive system health checks with dismissible warnings",
|
||||||
|
"Release Notes Modal - Automatic notification of new features and improvements",
|
||||||
|
],
|
||||||
|
changed: [
|
||||||
|
"Optimized VM & LXC page - Reduced CPU usage by 85% through intelligent caching",
|
||||||
|
"Storage metrics now separate local and remote storage for clarity",
|
||||||
|
"Update warnings now appear only after 365 days instead of 30 days",
|
||||||
|
"API intervals staggered to distribute server load (23s and 37s)",
|
||||||
|
],
|
||||||
|
fixed: [
|
||||||
|
"Fixed dark mode text contrast issues in various components",
|
||||||
|
"Corrected storage calculation discrepancies between Overview and Storage pages",
|
||||||
|
"Resolved JSON stringify error in VM control actions",
|
||||||
|
"Improved IP address fetching for LXC containers",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"1.0.0": {
|
||||||
|
date: "October 15, 2025",
|
||||||
|
changes: {
|
||||||
|
added: [
|
||||||
|
"Initial release of ProxMenux Monitor",
|
||||||
|
"Real-time system monitoring dashboard",
|
||||||
|
"Storage management with SMART health monitoring",
|
||||||
|
"Network metrics and bandwidth tracking",
|
||||||
|
"VM & LXC container management",
|
||||||
|
"Hardware information display",
|
||||||
|
"System logs viewer with filtering",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const CURRENT_VERSION_FEATURES = [
|
||||||
|
{
|
||||||
|
icon: <Link2 className="h-5 w-5" />,
|
||||||
|
text: "Proxy Support - Access ProxMenux through reverse proxies with full functionality",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Shield className="h-5 w-5" />,
|
||||||
|
text: "Two-Factor Authentication (2FA) - Enhanced security with TOTP support for login protection",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Zap className="h-5 w-5" />,
|
||||||
|
text: "Performance Improvements - Optimized loading times and reduced CPU usage across the application",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <HardDrive className="h-5 w-5" />,
|
||||||
|
text: "Storage Enhancements - Improved disk space consumption display with local and remote storage separation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Gauge className="h-5 w-5" />,
|
||||||
|
text: "PCIe Link Speed Detection - View NVMe drive connection speeds and identify performance bottlenecks",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Wrench className="h-5 w-5" />,
|
||||||
|
text: "Hardware Page Improvements - Enhanced hardware information display with detailed PCIe and interface data",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Settings className="h-5 w-5" />,
|
||||||
|
text: "New Settings Page - Centralized configuration for authentication, optimizations, and system preferences",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
interface ReleaseNotesModalProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReleaseNotesModal({ open, onClose }: ReleaseNotesModalProps) {
|
||||||
|
const [dontShowAgain, setDontShowAgain] = useState(false)
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (dontShowAgain) {
|
||||||
|
localStorage.setItem("proxmenux-last-seen-version", APP_VERSION)
|
||||||
|
}
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleClose}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[85vh] p-0 gap-0 border-0 bg-transparent">
|
||||||
|
<div className="relative bg-card rounded-lg shadow-2xl h-full flex flex-col max-h-[85vh]">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute top-4 right-4 z-50 h-8 w-8 rounded-full bg-background/80 backdrop-blur-sm hover:bg-background"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="relative h-32 md:h-40 bg-gradient-to-br from-amber-500 via-orange-500 to-red-500 flex items-center justify-center overflow-hidden flex-shrink-0">
|
||||||
|
<div className="absolute inset-0 bg-black/10" />
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,rgba(255,255,255,0.1),transparent)]" />
|
||||||
|
|
||||||
|
<div className="relative z-10 text-white animate-pulse">
|
||||||
|
<Sparkles className="h-12 w-12 md:h-14 md:w-14" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-2xl" />
|
||||||
|
<div className="absolute bottom-10 right-10 w-32 h-32 bg-white/10 rounded-full blur-3xl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 md:p-8 space-y-4 md:space-y-6 min-h-0">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="text-xl md:text-2xl font-bold text-foreground text-balance">
|
||||||
|
What's New in Version {APP_VERSION}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
We've added exciting new features and improvements to make ProxMenux Monitor even better!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{CURRENT_VERSION_FEATURES.map((feature, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-start gap-2 md:gap-3 p-3 rounded-lg bg-muted/50 border border-border/50 hover:bg-muted/70 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="text-orange-500 mt-0.5 flex-shrink-0">{feature.icon}</div>
|
||||||
|
<p className="text-xs md:text-sm text-foreground leading-relaxed">{feature.text}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0 p-6 md:p-8 pt-4 border-t border-border/50 bg-card">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="w-full bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600"
|
||||||
|
>
|
||||||
|
<Sparkles className="h-4 w-4 mr-2" />
|
||||||
|
Got it!
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id="dont-show-version-again"
|
||||||
|
checked={dontShowAgain}
|
||||||
|
onCheckedChange={(checked) => setDontShowAgain(checked as boolean)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="dont-show-version-again"
|
||||||
|
className="text-xs md:text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer select-none"
|
||||||
|
>
|
||||||
|
Don't show again for this version
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVersionCheck() {
|
||||||
|
const [showReleaseNotes, setShowReleaseNotes] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const lastSeenVersion = localStorage.getItem("proxmenux-last-seen-version")
|
||||||
|
|
||||||
|
if (lastSeenVersion !== APP_VERSION) {
|
||||||
|
setShowReleaseNotes(true)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return { showReleaseNotes, setShowReleaseNotes }
|
||||||
|
}
|
||||||
|
|
||||||
|
export { APP_VERSION }
|
||||||
@@ -0,0 +1,910 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { Button } from "./ui/button"
|
||||||
|
import { Input } from "./ui/input"
|
||||||
|
import { Label } from "./ui/label"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
||||||
|
import {
|
||||||
|
Shield,
|
||||||
|
Lock,
|
||||||
|
User,
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle,
|
||||||
|
Info,
|
||||||
|
LogOut,
|
||||||
|
Wrench,
|
||||||
|
Package,
|
||||||
|
Key,
|
||||||
|
Copy,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { APP_VERSION } from "./release-notes-modal"
|
||||||
|
import { getApiUrl, fetchApi } from "../lib/api-config"
|
||||||
|
import { TwoFactorSetup } from "./two-factor-setup"
|
||||||
|
|
||||||
|
interface ProxMenuxTool {
|
||||||
|
key: string
|
||||||
|
name: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Settings() {
|
||||||
|
const [authEnabled, setAuthEnabled] = useState(false)
|
||||||
|
const [totpEnabled, setTotpEnabled] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const [success, setSuccess] = useState("")
|
||||||
|
|
||||||
|
// Setup form state
|
||||||
|
const [showSetupForm, setShowSetupForm] = useState(false)
|
||||||
|
const [username, setUsername] = useState("")
|
||||||
|
const [password, setPassword] = useState("")
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("")
|
||||||
|
|
||||||
|
// Change password form state
|
||||||
|
const [showChangePassword, setShowChangePassword] = useState(false)
|
||||||
|
const [currentPassword, setCurrentPassword] = useState("")
|
||||||
|
const [newPassword, setNewPassword] = useState("")
|
||||||
|
const [confirmNewPassword, setConfirmNewPassword] = useState("")
|
||||||
|
|
||||||
|
const [show2FASetup, setShow2FASetup] = useState(false)
|
||||||
|
const [show2FADisable, setShow2FADisable] = useState(false)
|
||||||
|
const [disable2FAPassword, setDisable2FAPassword] = useState("")
|
||||||
|
|
||||||
|
const [proxmenuxTools, setProxmenuxTools] = useState<ProxMenuxTool[]>([])
|
||||||
|
const [loadingTools, setLoadingTools] = useState(true)
|
||||||
|
const [expandedVersions, setExpandedVersions] = useState<Record<string, boolean>>({
|
||||||
|
[APP_VERSION]: true, // Current version expanded by default
|
||||||
|
})
|
||||||
|
|
||||||
|
// API Token state management
|
||||||
|
const [showApiTokenSection, setShowApiTokenSection] = useState(false)
|
||||||
|
const [apiToken, setApiToken] = useState("")
|
||||||
|
const [apiTokenVisible, setApiTokenVisible] = useState(false)
|
||||||
|
const [tokenPassword, setTokenPassword] = useState("")
|
||||||
|
const [tokenTotpCode, setTokenTotpCode] = useState("")
|
||||||
|
const [generatingToken, setGeneratingToken] = useState(false)
|
||||||
|
const [tokenCopied, setTokenCopied] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuthStatus()
|
||||||
|
loadProxmenuxTools()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const checkAuthStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(getApiUrl("/api/auth/status"))
|
||||||
|
const data = await response.json()
|
||||||
|
setAuthEnabled(data.auth_enabled || false)
|
||||||
|
setTotpEnabled(data.totp_enabled || false) // Get 2FA status
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to check auth status:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadProxmenuxTools = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(getApiUrl("/api/proxmenux/installed-tools"))
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setProxmenuxTools(data.installed_tools || [])
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load ProxMenux tools:", err)
|
||||||
|
} finally {
|
||||||
|
setLoadingTools(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEnableAuth = async () => {
|
||||||
|
setError("")
|
||||||
|
setSuccess("")
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
setError("Please fill in all fields")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError("Passwords do not match")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
setError("Password must be at least 6 characters")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(getApiUrl("/api/auth/setup"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
enable_auth: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || "Failed to enable authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save token
|
||||||
|
localStorage.setItem("proxmenux-auth-token", data.token)
|
||||||
|
localStorage.setItem("proxmenux-auth-setup-complete", "true")
|
||||||
|
|
||||||
|
setSuccess("Authentication enabled successfully!")
|
||||||
|
setAuthEnabled(true)
|
||||||
|
setShowSetupForm(false)
|
||||||
|
setUsername("")
|
||||||
|
setPassword("")
|
||||||
|
setConfirmPassword("")
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to enable authentication")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDisableAuth = async () => {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
"Are you sure you want to disable authentication? This will remove password protection from your dashboard.",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setError("")
|
||||||
|
setSuccess("")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("proxmenux-auth-token")
|
||||||
|
const response = await fetch(getApiUrl("/api/auth/disable"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || "Failed to disable authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem("proxmenux-auth-token")
|
||||||
|
localStorage.removeItem("proxmenux-auth-setup-complete")
|
||||||
|
|
||||||
|
setSuccess("Authentication disabled successfully! Reloading...")
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 1000)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to disable authentication. Please try again.")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangePassword = async () => {
|
||||||
|
setError("")
|
||||||
|
setSuccess("")
|
||||||
|
|
||||||
|
if (!currentPassword || !newPassword) {
|
||||||
|
setError("Please fill in all fields")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword !== confirmNewPassword) {
|
||||||
|
setError("New passwords do not match")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
setError("Password must be at least 6 characters")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(getApiUrl("/api/auth/change-password"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("proxmenux-auth-token")}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
current_password: currentPassword,
|
||||||
|
new_password: newPassword,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || "Failed to change password")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update token if provided
|
||||||
|
if (data.token) {
|
||||||
|
localStorage.setItem("proxmenux-auth-token", data.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuccess("Password changed successfully!")
|
||||||
|
setShowChangePassword(false)
|
||||||
|
setCurrentPassword("")
|
||||||
|
setNewPassword("")
|
||||||
|
setConfirmNewPassword("")
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to change password")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDisable2FA = async () => {
|
||||||
|
setError("")
|
||||||
|
setSuccess("")
|
||||||
|
|
||||||
|
if (!disable2FAPassword) {
|
||||||
|
setError("Please enter your password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("proxmenux-auth-token")
|
||||||
|
const response = await fetch(getApiUrl("/api/auth/totp/disable"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ password: disable2FAPassword }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || "Failed to disable 2FA")
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuccess("2FA disabled successfully!")
|
||||||
|
setTotpEnabled(false)
|
||||||
|
setShow2FADisable(false)
|
||||||
|
setDisable2FAPassword("")
|
||||||
|
checkAuthStatus()
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to disable 2FA")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem("proxmenux-auth-token")
|
||||||
|
localStorage.removeItem("proxmenux-auth-setup-complete")
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGenerateApiToken = async () => {
|
||||||
|
setError("")
|
||||||
|
setSuccess("")
|
||||||
|
|
||||||
|
if (!tokenPassword) {
|
||||||
|
setError("Please enter your password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totpEnabled && !tokenTotpCode) {
|
||||||
|
setError("Please enter your 2FA code")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setGeneratingToken(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchApi("/api/auth/generate-api-token", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
password: tokenPassword,
|
||||||
|
totp_token: totpEnabled ? tokenTotpCode : undefined,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
setError(data.message || data.error || "Failed to generate API token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.token) {
|
||||||
|
setError("No token received from server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setApiToken(data.token)
|
||||||
|
setSuccess("API token generated successfully! Make sure to copy it now as you won't be able to see it again.")
|
||||||
|
setTokenPassword("")
|
||||||
|
setTokenTotpCode("")
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to generate API token. Please try again.")
|
||||||
|
} finally {
|
||||||
|
setGeneratingToken(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyApiToken = () => {
|
||||||
|
navigator.clipboard.writeText(apiToken)
|
||||||
|
setTokenCopied(true)
|
||||||
|
setTimeout(() => setTokenCopied(false), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleVersion = (version: string) => {
|
||||||
|
setExpandedVersions((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[version]: !prev[version],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Settings</h1>
|
||||||
|
<p className="text-muted-foreground mt-2">Manage your dashboard security and preferences</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Authentication Settings */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Shield className="h-5 w-5 text-blue-500" />
|
||||||
|
<CardTitle>Authentication</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription>Protect your dashboard with username and password authentication</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-red-500">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-green-500">{success}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className={`w-10 h-10 rounded-full flex items-center justify-center ${authEnabled ? "bg-green-500/10" : "bg-gray-500/10"}`}
|
||||||
|
>
|
||||||
|
<Lock className={`h-5 w-5 ${authEnabled ? "text-green-500" : "text-gray-500"}`} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Authentication Status</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{authEnabled ? "Password protection is enabled" : "No password protection"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`px-3 py-1 rounded-full text-sm font-medium ${authEnabled ? "bg-green-500/10 text-green-500" : "bg-gray-500/10 text-gray-500"}`}
|
||||||
|
>
|
||||||
|
{authEnabled ? "Enabled" : "Disabled"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!authEnabled && !showSetupForm && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-blue-500">
|
||||||
|
Enable authentication to protect your dashboard when accessing from non-private networks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => setShowSetupForm(true)} className="w-full bg-blue-500 hover:bg-blue-600">
|
||||||
|
<Shield className="h-4 w-4 mr-2" />
|
||||||
|
Enable Authentication
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!authEnabled && showSetupForm && (
|
||||||
|
<div className="space-y-4 border border-border rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold">Setup Authentication</h3>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="setup-username">Username</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="setup-username"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="setup-password">Password</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="setup-password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter password (min 6 characters)"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="setup-confirm-password">Confirm Password</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="setup-confirm-password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={handleEnableAuth} className="flex-1 bg-blue-500 hover:bg-blue-600" disabled={loading}>
|
||||||
|
{loading ? "Enabling..." : "Enable"}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setShowSetupForm(false)} variant="outline" className="flex-1" disabled={loading}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{authEnabled && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Button onClick={handleLogout} variant="outline" className="w-full bg-transparent">
|
||||||
|
<LogOut className="h-4 w-4 mr-2" />
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!showChangePassword && (
|
||||||
|
<Button onClick={() => setShowChangePassword(true)} variant="outline" className="w-full">
|
||||||
|
<Lock className="h-4 w-4 mr-2" />
|
||||||
|
Change Password
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showChangePassword && (
|
||||||
|
<div className="space-y-4 border border-border rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold">Change Password</h3>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="current-password">Current Password</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="current-password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter current password"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="new-password">New Password</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="new-password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter new password (min 6 characters)"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="confirm-new-password">Confirm New Password</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="confirm-new-password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm new password"
|
||||||
|
value={confirmNewPassword}
|
||||||
|
onChange={(e) => setConfirmNewPassword(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleChangePassword}
|
||||||
|
className="flex-1 bg-blue-500 hover:bg-blue-600"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Changing..." : "Change Password"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowChangePassword(false)}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!totpEnabled && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-blue-400">
|
||||||
|
<p className="font-medium mb-1">Two-Factor Authentication (2FA)</p>
|
||||||
|
<p className="text-blue-300">
|
||||||
|
Add an extra layer of security by requiring a code from your authenticator app in addition to
|
||||||
|
your password.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={() => setShow2FASetup(true)} variant="outline" className="w-full">
|
||||||
|
<Shield className="h-4 w-4 mr-2" />
|
||||||
|
Enable Two-Factor Authentication
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totpEnabled && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-3 flex items-center gap-2">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500" />
|
||||||
|
<p className="text-sm text-green-500 font-medium">2FA is enabled</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!show2FADisable && (
|
||||||
|
<Button onClick={() => setShow2FADisable(true)} variant="outline" className="w-full">
|
||||||
|
<Shield className="h-4 w-4 mr-2" />
|
||||||
|
Disable 2FA
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{show2FADisable && (
|
||||||
|
<div className="space-y-4 border border-border rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold">Disable Two-Factor Authentication</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Enter your password to confirm</p>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="disable-2fa-password">Password</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="disable-2fa-password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
value={disable2FAPassword}
|
||||||
|
onChange={(e) => setDisable2FAPassword(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={handleDisable2FA} variant="destructive" className="flex-1" disabled={loading}>
|
||||||
|
{loading ? "Disabling..." : "Disable 2FA"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShow2FADisable(false)
|
||||||
|
setDisable2FAPassword("")
|
||||||
|
setError("")
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button onClick={handleDisableAuth} variant="destructive" className="w-full" disabled={loading}>
|
||||||
|
Disable Authentication
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* API Access Tokens */}
|
||||||
|
{authEnabled && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Key className="h-5 w-5 text-purple-500" />
|
||||||
|
<CardTitle>API Access Tokens</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
Generate long-lived API tokens for external integrations like Homepage and Home Assistant
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-red-500">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-green-500">{success}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="space-y-2 text-sm text-blue-400">
|
||||||
|
<p className="font-medium">About API Tokens</p>
|
||||||
|
<ul className="list-disc list-inside space-y-1 text-blue-300">
|
||||||
|
<li>Tokens are valid for 1 year</li>
|
||||||
|
<li>Use them to access APIs from external services</li>
|
||||||
|
<li>Include in Authorization header: Bearer YOUR_TOKEN</li>
|
||||||
|
<li>See README.md for complete integration examples</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!showApiTokenSection && !apiToken && (
|
||||||
|
<Button onClick={() => setShowApiTokenSection(true)} className="w-full bg-purple-500 hover:bg-purple-600">
|
||||||
|
<Key className="h-4 w-4 mr-2" />
|
||||||
|
Generate New API Token
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showApiTokenSection && !apiToken && (
|
||||||
|
<div className="space-y-4 border border-border rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold">Generate API Token</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Enter your credentials to generate a new long-lived API token
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="token-password">Password</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="token-password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
value={tokenPassword}
|
||||||
|
onChange={(e) => setTokenPassword(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
disabled={generatingToken}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totpEnabled && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="token-totp">2FA Code</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Shield className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
id="token-totp"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter 6-digit code"
|
||||||
|
value={tokenTotpCode}
|
||||||
|
onChange={(e) => setTokenTotpCode(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
maxLength={6}
|
||||||
|
disabled={generatingToken}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleGenerateApiToken}
|
||||||
|
className="flex-1 bg-purple-500 hover:bg-purple-600"
|
||||||
|
disabled={generatingToken}
|
||||||
|
>
|
||||||
|
{generatingToken ? "Generating..." : "Generate Token"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShowApiTokenSection(false)
|
||||||
|
setTokenPassword("")
|
||||||
|
setTokenTotpCode("")
|
||||||
|
setError("")
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
disabled={generatingToken}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{apiToken && (
|
||||||
|
<div className="space-y-4 border border-green-500/20 bg-green-500/5 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 text-green-500">
|
||||||
|
<CheckCircle className="h-5 w-5" />
|
||||||
|
<h3 className="font-semibold">Your API Token</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-amber-500/10 border border-amber-500/30 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-amber-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-amber-600 dark:text-amber-400 font-semibold">
|
||||||
|
⚠️ Important: Save this token now!
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-amber-600/80 dark:text-amber-400/80">
|
||||||
|
You won't be able to see it again. Store it securely.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Token</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
value={apiToken}
|
||||||
|
readOnly
|
||||||
|
type={apiTokenVisible ? "text" : "password"}
|
||||||
|
className="pr-20 font-mono text-sm"
|
||||||
|
/>
|
||||||
|
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex gap-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setApiTokenVisible(!apiTokenVisible)}
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
>
|
||||||
|
{apiTokenVisible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost" onClick={copyApiToken} className="h-7 w-7 p-0">
|
||||||
|
<Copy className={`h-4 w-4 ${tokenCopied ? "text-green-500" : ""}`} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{tokenCopied && (
|
||||||
|
<p className="text-xs text-green-500 flex items-center gap-1">
|
||||||
|
<CheckCircle className="h-3 w-3" />
|
||||||
|
Copied to clipboard!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium">How to use this token:</p>
|
||||||
|
<div className="bg-muted/50 rounded p-3 text-xs font-mono">
|
||||||
|
<p className="text-muted-foreground mb-2"># Add to request headers:</p>
|
||||||
|
<p>Authorization: Bearer YOUR_TOKEN_HERE</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
See the README documentation for complete integration examples with Homepage and Home Assistant.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setApiToken("")
|
||||||
|
setShowApiTokenSection(false)
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ProxMenux Optimizations */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Wrench className="h-5 w-5 text-orange-500" />
|
||||||
|
<CardTitle>ProxMenux Optimizations</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription>System optimizations and utilities installed via ProxMenux</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{loadingTools ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="animate-spin h-8 w-8 border-4 border-orange-500 border-t-transparent rounded-full" />
|
||||||
|
</div>
|
||||||
|
) : proxmenuxTools.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Package className="h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50" />
|
||||||
|
<p className="text-muted-foreground">No ProxMenux optimizations installed yet</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">Run ProxMenux to configure system optimizations</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between mb-4 pb-2 border-b border-border">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Installed Tools</span>
|
||||||
|
<span className="text-sm font-semibold text-orange-500">{proxmenuxTools.length} active</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
{proxmenuxTools.map((tool) => (
|
||||||
|
<div
|
||||||
|
key={tool.key}
|
||||||
|
className="flex items-center gap-2 p-3 bg-muted/50 rounded-lg border border-border hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
|
||||||
|
<span className="text-sm font-medium">{tool.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<TwoFactorSetup
|
||||||
|
open={show2FASetup}
|
||||||
|
onClose={() => setShow2FASetup(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setSuccess("2FA enabled successfully!")
|
||||||
|
checkAuthStatus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { LayoutDashboard, HardDrive, Network, Server, Cpu, FileText } from "path-to-icons"
|
import { LayoutDashboard, HardDrive, Network, Server, Cpu, FileText, SettingsIcon } from "lucide-react"
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ name: "Overview", href: "/", icon: LayoutDashboard },
|
{ name: "Overview", href: "/", icon: LayoutDashboard },
|
||||||
{ name: "Storage", href: "/storage", icon: HardDrive },
|
{ name: "Storage", href: "/storage", icon: HardDrive },
|
||||||
{ name: "Network", href: "/network", icon: Network },
|
{ name: "Network", href: "/network", icon: Network },
|
||||||
{ name: "Virtual Machines", href: "/virtual-machines", icon: Server },
|
{ name: "Virtual Machines", href: "/virtual-machines", icon: Server },
|
||||||
{ name: "Hardware", href: "/hardware", icon: Cpu }, // New Hardware section
|
{ name: "Hardware", href: "/hardware", icon: Cpu },
|
||||||
{ name: "System Logs", href: "/logs", icon: FileText },
|
{ name: "System Logs", href: "/logs", icon: FileText },
|
||||||
|
{ name: "Settings", href: "/settings", icon: SettingsIcon },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// ... existing code ...
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
|||||||
import { Progress } from "./ui/progress"
|
import { Progress } from "./ui/progress"
|
||||||
import { Badge } from "./ui/badge"
|
import { Badge } from "./ui/badge"
|
||||||
import { HardDrive, Database, Archive, AlertTriangle, CheckCircle, Activity, AlertCircle } from "lucide-react"
|
import { HardDrive, Database, Archive, AlertTriangle, CheckCircle, Activity, AlertCircle } from "lucide-react"
|
||||||
|
import { formatStorage } from "@/lib/utils"
|
||||||
|
|
||||||
interface StorageData {
|
interface StorageData {
|
||||||
total: number
|
total: number
|
||||||
@@ -116,10 +117,10 @@ export function StorageMetrics() {
|
|||||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-xl lg:text-2xl font-bold text-foreground">{storageData.total.toFixed(1)} GB</div>
|
<div className="text-xl lg:text-2xl font-bold text-foreground">{formatStorage(storageData.total)}</div>
|
||||||
<Progress value={usagePercent} className="mt-2" />
|
<Progress value={usagePercent} className="mt-2" />
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
{storageData.used.toFixed(1)} GB used • {storageData.available.toFixed(1)} GB available
|
{formatStorage(storageData.used)} used • {formatStorage(storageData.available)} available
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -130,7 +131,7 @@ export function StorageMetrics() {
|
|||||||
<Database className="h-4 w-4 text-muted-foreground" />
|
<Database className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-xl lg:text-2xl font-bold text-foreground">{storageData.used.toFixed(1)} GB</div>
|
<div className="text-xl lg:text-2xl font-bold text-foreground">{formatStorage(storageData.used)}</div>
|
||||||
<Progress value={usagePercent} className="mt-2" />
|
<Progress value={usagePercent} className="mt-2" />
|
||||||
<p className="text-xs text-muted-foreground mt-2">{usagePercent.toFixed(1)}% of total space</p>
|
<p className="text-xs text-muted-foreground mt-2">{usagePercent.toFixed(1)}% of total space</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -144,7 +145,7 @@ export function StorageMetrics() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-xl lg:text-2xl font-bold text-foreground">{storageData.available.toFixed(1)} GB</div>
|
<div className="text-xl lg:text-2xl font-bold text-foreground">{formatStorage(storageData.available)}</div>
|
||||||
<div className="flex items-center mt-2">
|
<div className="flex items-center mt-2">
|
||||||
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||||
{((storageData.available / storageData.total) * 100).toFixed(1)}% Free
|
{((storageData.available / storageData.total) * 100).toFixed(1)}% Free
|
||||||
@@ -201,7 +202,7 @@ export function StorageMetrics() {
|
|||||||
<div className="flex items-center space-x-6">
|
<div className="flex items-center space-x-6">
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-sm font-medium text-foreground">
|
<div className="text-sm font-medium text-foreground">
|
||||||
{disk.used.toFixed(1)} GB / {disk.total.toFixed(1)} GB
|
{formatStorage(disk.used)} / {formatStorage(disk.total)}
|
||||||
</div>
|
</div>
|
||||||
<Progress value={disk.usage_percent} className="w-24 mt-1" />
|
<Progress value={disk.usage_percent} className="w-24 mt-1" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { HardDrive, Database, AlertTriangle, CheckCircle2, XCircle, Square, Thermometer } from "lucide-react"
|
import { HardDrive, Database, AlertTriangle, CheckCircle2, XCircle, Square, Thermometer, Archive } from "lucide-react"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Progress } from "@/components/ui/progress"
|
import { Progress } from "@/components/ui/progress"
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
|
import { fetchApi } from "../lib/api-config"
|
||||||
|
|
||||||
interface DiskInfo {
|
interface DiskInfo {
|
||||||
name: string
|
name: string
|
||||||
@@ -64,6 +65,7 @@ interface ProxmoxStorage {
|
|||||||
used: number
|
used: number
|
||||||
available: number
|
available: number
|
||||||
percent: number
|
percent: number
|
||||||
|
node: string // Added node property for detailed debug logging
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProxmoxStorageData {
|
interface ProxmoxStorageData {
|
||||||
@@ -75,12 +77,11 @@ const formatStorage = (sizeInGB: number): string => {
|
|||||||
if (sizeInGB < 1) {
|
if (sizeInGB < 1) {
|
||||||
// Less than 1 GB, show in MB
|
// Less than 1 GB, show in MB
|
||||||
return `${(sizeInGB * 1024).toFixed(1)} MB`
|
return `${(sizeInGB * 1024).toFixed(1)} MB`
|
||||||
} else if (sizeInGB < 1024) {
|
} else if (sizeInGB > 999) {
|
||||||
// Less than 1024 GB, show in GB
|
return `${(sizeInGB / 1024).toFixed(2)} TB`
|
||||||
return `${sizeInGB.toFixed(1)} GB`
|
|
||||||
} else {
|
} else {
|
||||||
// 1024 GB or more, show in TB
|
// Between 1 and 999 GB, show in GB
|
||||||
return `${(sizeInGB / 1024).toFixed(1)} TB`
|
return `${sizeInGB.toFixed(2)} GB`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,20 +94,11 @@ export function StorageOverview() {
|
|||||||
|
|
||||||
const fetchStorageData = async () => {
|
const fetchStorageData = async () => {
|
||||||
try {
|
try {
|
||||||
const baseUrl =
|
const [data, proxmoxData] = await Promise.all([
|
||||||
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
fetchApi<StorageData>("/api/storage"),
|
||||||
|
fetchApi<ProxmoxStorageData>("/api/proxmox-storage"),
|
||||||
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)
|
setStorageData(data)
|
||||||
setProxmoxStorage(proxmoxData)
|
setProxmoxStorage(proxmoxData)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -211,6 +203,12 @@ export function StorageOverview() {
|
|||||||
if (diskName.startsWith("nvme")) {
|
if (diskName.startsWith("nvme")) {
|
||||||
return "NVMe"
|
return "NVMe"
|
||||||
}
|
}
|
||||||
|
// rotation_rate = -1 means HDD but RPM is unknown (detected via kernel rotational flag)
|
||||||
|
// rotation_rate = 0 or undefined means SSD
|
||||||
|
// rotation_rate > 0 means HDD with known RPM
|
||||||
|
if (rotationRate === -1) {
|
||||||
|
return "HDD"
|
||||||
|
}
|
||||||
if (!rotationRate || rotationRate === 0) {
|
if (!rotationRate || rotationRate === 0) {
|
||||||
return "SSD"
|
return "SSD"
|
||||||
}
|
}
|
||||||
@@ -393,20 +391,88 @@ export function StorageOverview() {
|
|||||||
return "[&>div]:bg-red-500"
|
return "[&>div]:bg-red-500"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUsageColor = (percent: number): string => {
|
||||||
|
if (percent < 70) return "text-blue-500"
|
||||||
|
if (percent < 85) return "text-yellow-500"
|
||||||
|
if (percent < 95) return "text-orange-500"
|
||||||
|
return "text-red-500"
|
||||||
|
}
|
||||||
|
|
||||||
const diskHealthBreakdown = getDiskHealthBreakdown()
|
const diskHealthBreakdown = getDiskHealthBreakdown()
|
||||||
const diskTypesBreakdown = getDiskTypesBreakdown()
|
const diskTypesBreakdown = getDiskTypesBreakdown()
|
||||||
|
|
||||||
const totalProxmoxUsed =
|
const localStorageTypes = ["dir", "lvmthin", "lvm", "zfspool", "btrfs"]
|
||||||
proxmoxStorage && proxmoxStorage.storage
|
const remoteStorageTypes = ["pbs", "nfs", "cifs", "smb", "glusterfs", "iscsi", "iscsidirect", "rbd", "cephfs"]
|
||||||
? proxmoxStorage.storage
|
|
||||||
.filter(
|
|
||||||
(storage) => storage && storage.total > 0 && storage.status && storage.status.toLowerCase() === "active",
|
|
||||||
)
|
|
||||||
.reduce((sum, storage) => sum + storage.used, 0)
|
|
||||||
: 0
|
|
||||||
|
|
||||||
const usagePercent =
|
const totalLocalUsed =
|
||||||
storageData && storageData.total > 0 ? ((totalProxmoxUsed / (storageData.total * 1024)) * 100).toFixed(2) : "0.00"
|
proxmoxStorage?.storage
|
||||||
|
.filter(
|
||||||
|
(storage) =>
|
||||||
|
storage &&
|
||||||
|
storage.name &&
|
||||||
|
storage.status === "active" &&
|
||||||
|
storage.total > 0 &&
|
||||||
|
storage.used >= 0 &&
|
||||||
|
storage.available >= 0 &&
|
||||||
|
localStorageTypes.includes(storage.type.toLowerCase()),
|
||||||
|
)
|
||||||
|
.reduce((sum, storage) => sum + storage.used, 0) || 0
|
||||||
|
|
||||||
|
const totalLocalCapacity =
|
||||||
|
proxmoxStorage?.storage
|
||||||
|
.filter(
|
||||||
|
(storage) =>
|
||||||
|
storage &&
|
||||||
|
storage.name &&
|
||||||
|
storage.status === "active" &&
|
||||||
|
storage.total > 0 &&
|
||||||
|
storage.used >= 0 &&
|
||||||
|
storage.available >= 0 &&
|
||||||
|
localStorageTypes.includes(storage.type.toLowerCase()),
|
||||||
|
)
|
||||||
|
.reduce((sum, storage) => sum + storage.total, 0) || 0
|
||||||
|
|
||||||
|
const localUsagePercent = totalLocalCapacity > 0 ? ((totalLocalUsed / totalLocalCapacity) * 100).toFixed(2) : "0.00"
|
||||||
|
|
||||||
|
const totalRemoteUsed =
|
||||||
|
proxmoxStorage?.storage
|
||||||
|
.filter(
|
||||||
|
(storage) =>
|
||||||
|
storage &&
|
||||||
|
storage.name &&
|
||||||
|
storage.status === "active" &&
|
||||||
|
storage.total > 0 &&
|
||||||
|
storage.used >= 0 &&
|
||||||
|
storage.available >= 0 &&
|
||||||
|
remoteStorageTypes.includes(storage.type.toLowerCase()),
|
||||||
|
)
|
||||||
|
.reduce((sum, storage) => sum + storage.used, 0) || 0
|
||||||
|
|
||||||
|
const totalRemoteCapacity =
|
||||||
|
proxmoxStorage?.storage
|
||||||
|
.filter(
|
||||||
|
(storage) =>
|
||||||
|
storage &&
|
||||||
|
storage.name &&
|
||||||
|
storage.status === "active" &&
|
||||||
|
storage.total > 0 &&
|
||||||
|
storage.used >= 0 &&
|
||||||
|
storage.available >= 0 &&
|
||||||
|
remoteStorageTypes.includes(storage.type.toLowerCase()),
|
||||||
|
)
|
||||||
|
.reduce((sum, storage) => sum + storage.total, 0) || 0
|
||||||
|
|
||||||
|
const remoteUsagePercent =
|
||||||
|
totalRemoteCapacity > 0 ? ((totalRemoteUsed / totalRemoteCapacity) * 100).toFixed(2) : "0.00"
|
||||||
|
|
||||||
|
const remoteStorageCount =
|
||||||
|
proxmoxStorage?.storage.filter(
|
||||||
|
(storage) =>
|
||||||
|
storage &&
|
||||||
|
storage.name &&
|
||||||
|
storage.status === "active" &&
|
||||||
|
remoteStorageTypes.includes(storage.type.toLowerCase()),
|
||||||
|
).length || 0
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -441,64 +507,81 @@ export function StorageOverview() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">Used Storage</CardTitle>
|
<CardTitle className="text-sm font-medium">Local Used</CardTitle>
|
||||||
<Database className="h-4 w-4 text-muted-foreground" />
|
<Database className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-xl lg:text-2xl font-bold">{formatStorage(totalProxmoxUsed)}</div>
|
<div className="text-xl lg:text-2xl font-bold">{formatStorage(totalLocalUsed)}</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">{usagePercent}% used</p>
|
<p className="text-xs mt-1">
|
||||||
|
<span className={getUsageColor(Number.parseFloat(localUsagePercent))}>{localUsagePercent}%</span>
|
||||||
|
<span className="text-muted-foreground"> of </span>
|
||||||
|
<span className="text-green-500">{formatStorage(totalLocalCapacity)}</span>
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Disk Health */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">Disk Health</CardTitle>
|
<CardTitle className="text-sm font-medium">Remote Used</CardTitle>
|
||||||
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
|
<Archive className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-xl lg:text-2xl font-bold">{storageData.disk_count} disks</div>
|
<div className="text-xl lg:text-2xl font-bold">
|
||||||
|
{remoteStorageCount > 0 ? formatStorage(totalRemoteUsed) : "None"}
|
||||||
|
</div>
|
||||||
<p className="text-xs mt-1">
|
<p className="text-xs mt-1">
|
||||||
<span className="text-green-500">{diskHealthBreakdown.normal} normal</span>
|
{remoteStorageCount > 0 ? (
|
||||||
{diskHealthBreakdown.warning > 0 && (
|
|
||||||
<>
|
<>
|
||||||
{", "}
|
<span className={getUsageColor(Number.parseFloat(remoteUsagePercent))}>{remoteUsagePercent}%</span>
|
||||||
<span className="text-yellow-500">{diskHealthBreakdown.warning} warning</span>
|
<span className="text-muted-foreground"> of </span>
|
||||||
</>
|
<span className="text-green-500">{formatStorage(totalRemoteCapacity)}</span>
|
||||||
)}
|
|
||||||
{diskHealthBreakdown.critical > 0 && (
|
|
||||||
<>
|
|
||||||
{", "}
|
|
||||||
<span className="text-red-500">{diskHealthBreakdown.critical} critical</span>
|
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">No remote storage</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Disk Types */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">Disk Types</CardTitle>
|
<CardTitle className="text-sm font-medium">Physical Disks</CardTitle>
|
||||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-xl lg:text-2xl font-bold">{storageData.disk_count} disks</div>
|
<div className="text-xl lg:text-2xl font-bold">{storageData.disk_count} disks</div>
|
||||||
<p className="text-xs mt-1">
|
<div className="space-y-1 mt-1">
|
||||||
{diskTypesBreakdown.nvme > 0 && <span className="text-purple-500">{diskTypesBreakdown.nvme} NVMe</span>}
|
<p className="text-xs">
|
||||||
{diskTypesBreakdown.ssd > 0 && (
|
{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.nvme > 0 && ", "}
|
||||||
</>
|
<span className="text-cyan-500">{diskTypesBreakdown.ssd} SSD</span>
|
||||||
)}
|
</>
|
||||||
{diskTypesBreakdown.hdd > 0 && (
|
)}
|
||||||
<>
|
{diskTypesBreakdown.hdd > 0 && (
|
||||||
{(diskTypesBreakdown.nvme > 0 || diskTypesBreakdown.ssd > 0) && ", "}
|
<>
|
||||||
<span className="text-blue-500">{diskTypesBreakdown.hdd} HDD</span>
|
{(diskTypesBreakdown.nvme > 0 || diskTypesBreakdown.ssd > 0) && ", "}
|
||||||
</>
|
<span className="text-blue-500">{diskTypesBreakdown.hdd} HDD</span>
|
||||||
)}
|
</>
|
||||||
</p>
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs">
|
||||||
|
<span className="text-green-500">{diskHealthBreakdown.normal} normal</span>
|
||||||
|
{diskHealthBreakdown.warning > 0 && (
|
||||||
|
<>
|
||||||
|
{", "}
|
||||||
|
<span className="text-yellow-500">{diskHealthBreakdown.warning} warning</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{diskHealthBreakdown.critical > 0 && (
|
||||||
|
<>
|
||||||
|
{", "}
|
||||||
|
<span className="text-red-500">{diskHealthBreakdown.critical} critical</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -514,7 +597,10 @@ export function StorageOverview() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{proxmoxStorage.storage
|
{proxmoxStorage.storage
|
||||||
.filter((storage) => storage && storage.name && storage.total > 0)
|
.filter(
|
||||||
|
(storage) =>
|
||||||
|
storage && storage.name && storage.total > 0 && storage.used >= 0 && storage.available >= 0,
|
||||||
|
)
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map((storage) => (
|
.map((storage) => (
|
||||||
<div key={storage.name} className="border rounded-lg p-4">
|
<div key={storage.name} className="border rounded-lg p-4">
|
||||||
@@ -562,7 +648,7 @@ export function StorageOverview() {
|
|||||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">Total</p>
|
<p className="text-muted-foreground">Total</p>
|
||||||
<p className="font-medium">{storage.total.toLocaleString()} GB</p>
|
<p className="font-medium">{formatStorage(storage.total)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">Used</p>
|
<p className="text-muted-foreground">Used</p>
|
||||||
@@ -575,12 +661,12 @@ export function StorageOverview() {
|
|||||||
: "text-blue-400"
|
: "text-blue-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{storage.used.toLocaleString()} GB
|
{formatStorage(storage.used)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">Available</p>
|
<p className="text-muted-foreground">Available</p>
|
||||||
<p className="font-medium text-green-400">{storage.available.toLocaleString()} GB</p>
|
<p className="font-medium text-green-400">{formatStorage(storage.available)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+258
-222
@@ -27,7 +27,8 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
Terminal,
|
Terminal,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect, useMemo } from "react"
|
||||||
|
import { API_PORT, fetchApi } from "@/lib/api-config"
|
||||||
|
|
||||||
interface Log {
|
interface Log {
|
||||||
timestamp: string
|
timestamp: string
|
||||||
@@ -125,9 +126,20 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
const getApiUrl = (endpoint: string) => {
|
const getApiUrl = (endpoint: string) => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
return `${window.location.protocol}//${window.location.hostname}:8008${endpoint}`
|
const { protocol, hostname, port } = window.location
|
||||||
|
const isStandardPort = port === "" || port === "80" || port === "443"
|
||||||
|
|
||||||
|
if (isStandardPort) {
|
||||||
|
return endpoint
|
||||||
|
} else {
|
||||||
|
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return `http://localhost:8008${endpoint}`
|
// This part might not be strictly necessary if only running client-side, but good for SSR safety
|
||||||
|
// In a real SSR scenario, you'd need to handle API_PORT differently
|
||||||
|
const protocol = typeof window !== "undefined" ? window.location.protocol : "http:" // Defaulting to http for SSR safety
|
||||||
|
const hostname = typeof window !== "undefined" ? window.location.hostname : "localhost" // Defaulting to localhost for SSR safety
|
||||||
|
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -186,27 +198,15 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
const [logsRes, backupsRes, eventsRes, notificationsRes] = await Promise.all([
|
const [logsRes, backupsRes, eventsRes, notificationsRes] = await Promise.all([
|
||||||
fetchSystemLogs(),
|
fetchSystemLogs(),
|
||||||
fetch(getApiUrl("/api/backups")),
|
fetchApi("/api/backups"),
|
||||||
fetch(getApiUrl("/api/events?limit=50")),
|
fetchApi("/api/events?limit=50"),
|
||||||
fetch(getApiUrl("/api/notifications")),
|
fetchApi("/api/notifications"),
|
||||||
])
|
])
|
||||||
|
|
||||||
setLogs(logsRes)
|
setLogs(logsRes)
|
||||||
|
setBackups(backupsRes.backups || [])
|
||||||
if (backupsRes.ok) {
|
setEvents(eventsRes.events || [])
|
||||||
const backupsData = await backupsRes.json()
|
setNotifications(notificationsRes.notifications || [])
|
||||||
setBackups(backupsData.backups || [])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventsRes.ok) {
|
|
||||||
const eventsData = await eventsRes.json()
|
|
||||||
setEvents(eventsData.events || [])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notificationsRes.ok) {
|
|
||||||
const notificationsData = await notificationsRes.json()
|
|
||||||
setNotifications(notificationsData.notifications || [])
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[v0] Error fetching system logs data:", err)
|
console.error("[v0] Error fetching system logs data:", err)
|
||||||
setError("Failed to connect to server")
|
setError("Failed to connect to server")
|
||||||
@@ -217,7 +217,7 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
const fetchSystemLogs = async (): Promise<SystemLog[]> => {
|
const fetchSystemLogs = async (): Promise<SystemLog[]> => {
|
||||||
try {
|
try {
|
||||||
let apiUrl = getApiUrl("/api/logs")
|
let apiUrl = "/api/logs"
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
// CHANGE: Always add since_days parameter (no more "now" option)
|
// CHANGE: Always add since_days parameter (no more "now" option)
|
||||||
@@ -250,22 +250,7 @@ export function SystemLogs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log("[v0] Making fetch request to:", apiUrl)
|
console.log("[v0] Making fetch request to:", apiUrl)
|
||||||
const response = await fetch(apiUrl, {
|
const data = await fetchApi(apiUrl)
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
cache: "no-store",
|
|
||||||
signal: AbortSignal.timeout(30000), // 30 second timeout
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log("[v0] Response status:", response.status, "OK:", response.ok)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
console.log("[v0] Received logs data, count:", data.logs?.length || 0)
|
console.log("[v0] Received logs data, count:", data.logs?.length || 0)
|
||||||
|
|
||||||
const logsArray = Array.isArray(data) ? data : data.logs || []
|
const logsArray = Array.isArray(data) ? data : data.logs || []
|
||||||
@@ -356,37 +341,33 @@ export function SystemLogs() {
|
|||||||
if (upid) {
|
if (upid) {
|
||||||
// Try to fetch the complete task log from Proxmox
|
// Try to fetch the complete task log from Proxmox
|
||||||
try {
|
try {
|
||||||
const response = await fetch(getApiUrl(`/api/task-log/${encodeURIComponent(upid)}`))
|
const taskLog = await fetchApi(`/api/task-log/${encodeURIComponent(upid)}`, {}, "text")
|
||||||
|
|
||||||
if (response.ok) {
|
// Download the complete task log
|
||||||
const taskLog = await response.text()
|
const blob = new Blob(
|
||||||
|
[
|
||||||
|
`Proxmox Task Log\n`,
|
||||||
|
`================\n\n`,
|
||||||
|
`UPID: ${upid}\n`,
|
||||||
|
`Timestamp: ${notification.timestamp}\n`,
|
||||||
|
`Service: ${notification.service}\n`,
|
||||||
|
`Source: ${notification.source}\n\n`,
|
||||||
|
`Complete Task Log:\n`,
|
||||||
|
`${"-".repeat(80)}\n`,
|
||||||
|
`${taskLog}\n`,
|
||||||
|
],
|
||||||
|
{ type: "text/plain" },
|
||||||
|
)
|
||||||
|
|
||||||
// Download the complete task log
|
const url = window.URL.createObjectURL(blob)
|
||||||
const blob = new Blob(
|
const a = document.createElement("a")
|
||||||
[
|
a.href = url
|
||||||
`Proxmox Task Log\n`,
|
a.download = `task_log_${upid.replace(/:/g, "_")}_${notification.timestamp.replace(/[:\s]/g, "_")}.txt`
|
||||||
`================\n\n`,
|
document.body.appendChild(a)
|
||||||
`UPID: ${upid}\n`,
|
a.click()
|
||||||
`Timestamp: ${notification.timestamp}\n`,
|
window.URL.revokeObjectURL(url)
|
||||||
`Service: ${notification.service}\n`,
|
document.body.removeChild(a)
|
||||||
`Source: ${notification.source}\n\n`,
|
return
|
||||||
`Complete Task Log:\n`,
|
|
||||||
`${"-".repeat(80)}\n`,
|
|
||||||
`${taskLog}\n`,
|
|
||||||
],
|
|
||||||
{ type: "text/plain" },
|
|
||||||
)
|
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement("a")
|
|
||||||
a.href = url
|
|
||||||
a.download = `task_log_${upid.replace(/:/g, "_")}_${notification.timestamp.replace(/[:\s]/g, "_")}.txt`
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
document.body.removeChild(a)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[v0] Failed to fetch task log from Proxmox:", error)
|
console.error("[v0] Failed to fetch task log from Proxmox:", error)
|
||||||
// Fall through to download notification message
|
// Fall through to download notification message
|
||||||
@@ -421,39 +402,61 @@ export function SystemLogs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const logsOnly: CombinedLogEntry[] = logs
|
const safeToLowerCase = (value: any): string => {
|
||||||
.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() }))
|
if (value === null || value === undefined) return ""
|
||||||
.sort((a, b) => b.sortTimestamp - a.sortTimestamp)
|
return String(value).toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
const eventsOnly: CombinedLogEntry[] = events
|
const memoizedLogs = useMemo(() => logs, [logs])
|
||||||
.map((event) => ({
|
const memoizedEvents = useMemo(() => events, [events])
|
||||||
timestamp: event.starttime,
|
const memoizedBackups = useMemo(() => backups, [backups])
|
||||||
level: event.level,
|
const memoizedNotifications = useMemo(() => notifications, [notifications])
|
||||||
service: event.type,
|
|
||||||
message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`,
|
const logsOnly: CombinedLogEntry[] = useMemo(
|
||||||
source: `Node: ${event.node} • User: ${event.user}`,
|
() =>
|
||||||
isEvent: true,
|
memoizedLogs
|
||||||
eventData: event,
|
.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() }))
|
||||||
sortTimestamp: new Date(event.starttime).getTime(),
|
.sort((a, b) => b.sortTimestamp - a.sortTimestamp),
|
||||||
}))
|
[memoizedLogs],
|
||||||
.sort((a, b) => b.sortTimestamp - a.sortTimestamp)
|
)
|
||||||
|
|
||||||
|
const eventsOnly: CombinedLogEntry[] = useMemo(
|
||||||
|
() =>
|
||||||
|
memoizedEvents
|
||||||
|
.map((event) => ({
|
||||||
|
timestamp: event.starttime,
|
||||||
|
level: event.level,
|
||||||
|
service: event.type,
|
||||||
|
message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`,
|
||||||
|
source: `Node: ${event.node} • User: ${event.user}`,
|
||||||
|
isEvent: true,
|
||||||
|
eventData: event,
|
||||||
|
sortTimestamp: new Date(event.starttime).getTime(),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.sortTimestamp - a.sortTimestamp),
|
||||||
|
[memoizedEvents],
|
||||||
|
)
|
||||||
|
|
||||||
// Filter logs only
|
|
||||||
const filteredLogsOnly = logsOnly.filter((log) => {
|
const filteredLogsOnly = logsOnly.filter((log) => {
|
||||||
|
const message = log.message || ""
|
||||||
|
const service = log.service || ""
|
||||||
|
const searchTermLower = safeToLowerCase(searchTerm)
|
||||||
|
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
log.message.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower)
|
||||||
log.service.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
const matchesLevel = levelFilter === "all" || log.level === levelFilter
|
const matchesLevel = levelFilter === "all" || log.level === levelFilter
|
||||||
const matchesService = serviceFilter === "all" || log.service === serviceFilter
|
const matchesService = serviceFilter === "all" || log.service === serviceFilter
|
||||||
|
|
||||||
return matchesSearch && matchesLevel && matchesService
|
return matchesSearch && matchesLevel && matchesService
|
||||||
})
|
})
|
||||||
|
|
||||||
// Filter events only
|
|
||||||
const filteredEventsOnly = eventsOnly.filter((event) => {
|
const filteredEventsOnly = eventsOnly.filter((event) => {
|
||||||
|
const message = event.message || ""
|
||||||
|
const service = event.service || ""
|
||||||
|
const searchTermLower = safeToLowerCase(searchTerm)
|
||||||
|
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
event.message.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower)
|
||||||
event.service.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
const matchesLevel = levelFilter === "all" || event.level === levelFilter
|
const matchesLevel = levelFilter === "all" || event.level === levelFilter
|
||||||
const matchesService = serviceFilter === "all" || event.service === serviceFilter
|
const matchesService = serviceFilter === "all" || event.service === serviceFilter
|
||||||
|
|
||||||
@@ -463,30 +466,40 @@ export function SystemLogs() {
|
|||||||
const displayedLogsOnly = filteredLogsOnly.slice(0, displayedLogsCount)
|
const displayedLogsOnly = filteredLogsOnly.slice(0, displayedLogsCount)
|
||||||
const displayedEventsOnly = filteredEventsOnly.slice(0, displayedLogsCount)
|
const displayedEventsOnly = filteredEventsOnly.slice(0, displayedLogsCount)
|
||||||
|
|
||||||
const combinedLogs: CombinedLogEntry[] = [
|
const combinedLogs: CombinedLogEntry[] = useMemo(
|
||||||
...logs.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() })),
|
() =>
|
||||||
...events.map((event) => ({
|
[
|
||||||
timestamp: event.starttime,
|
...memoizedLogs.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() })),
|
||||||
level: event.level,
|
...memoizedEvents.map((event) => ({
|
||||||
service: event.type,
|
timestamp: event.starttime,
|
||||||
message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`,
|
level: event.level,
|
||||||
source: `Node: ${event.node} • User: ${event.user}`,
|
service: event.type,
|
||||||
isEvent: true,
|
message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`,
|
||||||
eventData: event,
|
source: `Node: ${event.node} • User: ${event.user}`,
|
||||||
sortTimestamp: new Date(event.starttime).getTime(),
|
isEvent: true,
|
||||||
})),
|
eventData: event,
|
||||||
].sort((a, b) => b.sortTimestamp - a.sortTimestamp) // Sort by timestamp descending
|
sortTimestamp: new Date(event.starttime).getTime(),
|
||||||
|
})),
|
||||||
|
].sort((a, b) => b.sortTimestamp - a.sortTimestamp),
|
||||||
|
[memoizedLogs, memoizedEvents],
|
||||||
|
)
|
||||||
|
|
||||||
// Filter combined logs
|
const filteredCombinedLogs = useMemo(
|
||||||
const filteredCombinedLogs = combinedLogs.filter((log) => {
|
() =>
|
||||||
const matchesSearch =
|
combinedLogs.filter((log) => {
|
||||||
log.message.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const message = log.message || ""
|
||||||
log.service.toLowerCase().includes(searchTerm.toLowerCase())
|
const service = log.service || ""
|
||||||
const matchesLevel = levelFilter === "all" || log.level === levelFilter
|
const searchTermLower = safeToLowerCase(searchTerm)
|
||||||
const matchesService = serviceFilter === "all" || log.service === serviceFilter
|
|
||||||
|
|
||||||
return matchesSearch && matchesLevel && matchesService
|
const matchesSearch =
|
||||||
})
|
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower)
|
||||||
|
const matchesLevel = levelFilter === "all" || log.level === levelFilter
|
||||||
|
const matchesService = serviceFilter === "all" || log.service === serviceFilter
|
||||||
|
|
||||||
|
return matchesSearch && matchesLevel && matchesService
|
||||||
|
}),
|
||||||
|
[combinedLogs, searchTerm, levelFilter, serviceFilter],
|
||||||
|
)
|
||||||
|
|
||||||
// CHANGE: Re-assigning displayedLogs to use the filteredCombinedLogs
|
// CHANGE: Re-assigning displayedLogs to use the filteredCombinedLogs
|
||||||
const displayedLogs = filteredCombinedLogs.slice(0, displayedLogsCount)
|
const displayedLogs = filteredCombinedLogs.slice(0, displayedLogsCount)
|
||||||
@@ -548,7 +561,9 @@ export function SystemLogs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getNotificationTypeColor = (type: string) => {
|
const getNotificationTypeColor = (type: string) => {
|
||||||
switch (type.toLowerCase()) {
|
if (!type) return "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
||||||
|
|
||||||
|
switch (safeToLowerCase(type)) {
|
||||||
case "error":
|
case "error":
|
||||||
return "bg-red-500/10 text-red-500 border-red-500/20"
|
return "bg-red-500/10 text-red-500 border-red-500/20"
|
||||||
case "warning":
|
case "warning":
|
||||||
@@ -564,7 +579,9 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
// ADDED: New function for notification source colors
|
// ADDED: New function for notification source colors
|
||||||
const getNotificationSourceColor = (source: string) => {
|
const getNotificationSourceColor = (source: string) => {
|
||||||
switch (source.toLowerCase()) {
|
if (!source) return "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
||||||
|
|
||||||
|
switch (safeToLowerCase(source)) {
|
||||||
case "task-log":
|
case "task-log":
|
||||||
return "bg-purple-500/10 text-purple-500 border-purple-500/20"
|
return "bg-purple-500/10 text-purple-500 border-purple-500/20"
|
||||||
case "journal":
|
case "journal":
|
||||||
@@ -583,7 +600,7 @@ export function SystemLogs() {
|
|||||||
info: logs.filter((log) => ["info", "notice", "debug"].includes(log.level)).length,
|
info: logs.filter((log) => ["info", "notice", "debug"].includes(log.level)).length,
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueServices = [...new Set(logs.map((log) => log.service))]
|
const uniqueServices = useMemo(() => [...new Set(memoizedLogs.map((log) => log.service))], [memoizedLogs])
|
||||||
|
|
||||||
const getBackupType = (volid: string): "vm" | "lxc" => {
|
const getBackupType = (volid: string): "vm" | "lxc" => {
|
||||||
if (volid.includes("/vm/") || volid.includes("vzdump-qemu")) {
|
if (volid.includes("/vm/") || volid.includes("vzdump-qemu")) {
|
||||||
@@ -908,9 +925,11 @@ export function SystemLogs() {
|
|||||||
<SelectValue placeholder="Filter by service" />
|
<SelectValue placeholder="Filter by service" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">All Services</SelectItem>
|
<SelectItem key="service-all" value="all">
|
||||||
{uniqueServices.slice(0, 20).map((service) => (
|
All Services
|
||||||
<SelectItem key={service} value={service}>
|
</SelectItem>
|
||||||
|
{uniqueServices.slice(0, 20).map((service, idx) => (
|
||||||
|
<SelectItem key={`service-${service}-${idx}`} value={service}>
|
||||||
{service}
|
{service}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -925,51 +944,59 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
<ScrollArea className="h-[600px] w-full rounded-md border border-border overflow-x-hidden">
|
<ScrollArea className="h-[600px] w-full rounded-md border border-border overflow-x-hidden">
|
||||||
<div className="space-y-2 p-4 w-full box-border">
|
<div className="space-y-2 p-4 w-full box-border">
|
||||||
{displayedLogs.map((log, index) => (
|
{displayedLogs.map((log, index) => {
|
||||||
<div
|
// Generate a more stable unique key
|
||||||
key={index}
|
const timestampMs = new Date(log.timestamp).getTime()
|
||||||
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden box-border"
|
const uniqueKey = log.eventData
|
||||||
onClick={() => {
|
? `event-${log.eventData.upid.replace(/:/g, "-")}-${timestampMs}`
|
||||||
if (log.eventData) {
|
: `log-${timestampMs}-${log.service?.substring(0, 10) || "unknown"}-${log.pid || "nopid"}-${index}`
|
||||||
setSelectedEvent(log.eventData)
|
|
||||||
setIsEventModalOpen(true)
|
|
||||||
} else {
|
|
||||||
setSelectedLog(log as SystemLog)
|
|
||||||
setIsLogModalOpen(true)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex-shrink-0 flex gap-2 flex-wrap">
|
|
||||||
<Badge variant="outline" className={getLevelColor(log.level)}>
|
|
||||||
{getLevelIcon(log.level)}
|
|
||||||
{log.level.toUpperCase()}
|
|
||||||
</Badge>
|
|
||||||
{log.eventData && (
|
|
||||||
<Badge variant="outline" className="bg-purple-500/10 text-purple-500 border-purple-500/20">
|
|
||||||
<Activity className="h-3 w-3 mr-1" />
|
|
||||||
EVENT
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0 overflow-hidden box-border">
|
return (
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-1 gap-1">
|
<div
|
||||||
<div className="text-sm font-medium text-foreground truncate min-w-0">{log.service}</div>
|
key={uniqueKey}
|
||||||
<div className="text-xs text-muted-foreground font-mono truncate sm:ml-2 sm:flex-shrink-0">
|
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden box-border"
|
||||||
{log.timestamp}
|
onClick={() => {
|
||||||
|
if (log.eventData) {
|
||||||
|
setSelectedEvent(log.eventData)
|
||||||
|
setIsEventModalOpen(true)
|
||||||
|
} else {
|
||||||
|
setSelectedLog(log as SystemLog)
|
||||||
|
setIsLogModalOpen(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0 flex gap-2 flex-wrap">
|
||||||
|
<Badge variant="outline" className={getLevelColor(log.level)}>
|
||||||
|
{getLevelIcon(log.level)}
|
||||||
|
{log.level.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
{log.eventData && (
|
||||||
|
<Badge variant="outline" className="bg-purple-500/10 text-purple-500 border-purple-500/20">
|
||||||
|
<Activity className="h-3 w-3 mr-1" />
|
||||||
|
EVENT
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0 overflow-hidden box-border">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-1 gap-1">
|
||||||
|
<div className="text-sm font-medium text-foreground truncate min-w-0">{log.service}</div>
|
||||||
|
<div className="text-xs text-muted-foreground font-mono truncate sm:ml-2 sm:flex-shrink-0">
|
||||||
|
{log.timestamp}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-foreground mb-1 line-clamp-2 break-all overflow-hidden">
|
||||||
|
{log.message}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground truncate break-all overflow-hidden">
|
||||||
|
{log.source}
|
||||||
|
{log.pid && ` • PID: ${log.pid}`}
|
||||||
|
{log.hostname && ` • Host: ${log.hostname}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-foreground mb-1 line-clamp-2 break-all overflow-hidden">
|
|
||||||
{log.message}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground truncate break-all overflow-hidden">
|
|
||||||
{log.source}
|
|
||||||
{log.pid && ` • PID: ${log.pid}`}
|
|
||||||
{log.hostname && ` • Host: ${log.hostname}`}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
))}
|
})}
|
||||||
|
|
||||||
{displayedLogs.length === 0 && (
|
{displayedLogs.length === 0 && (
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
@@ -1030,44 +1057,48 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
<ScrollArea className="h-[500px] w-full rounded-md border border-border">
|
<ScrollArea className="h-[500px] w-full rounded-md border border-border">
|
||||||
<div className="space-y-2 p-4">
|
<div className="space-y-2 p-4">
|
||||||
{backups.map((backup, index) => (
|
{memoizedBackups.map((backup, index) => {
|
||||||
<div
|
const uniqueKey = `backup-${backup.volid.replace(/[/:]/g, "-")}-${backup.timestamp || index}`
|
||||||
key={index}
|
|
||||||
className="flex items-start space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedBackup(backup)
|
|
||||||
setIsBackupModalOpen(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<HardDrive className="h-5 w-5 text-blue-500" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
return (
|
||||||
<div className="flex items-center justify-between mb-1 gap-2 flex-wrap">
|
<div
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
key={uniqueKey}
|
||||||
<Badge variant="outline" className={getBackupTypeColor(backup.volid)}>
|
className="flex items-start space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer"
|
||||||
{getBackupTypeLabel(backup.volid)}
|
onClick={() => {
|
||||||
</Badge>
|
setSelectedBackup(backup)
|
||||||
<Badge variant="outline" className={getBackupStorageColor(backup.volid)}>
|
setIsBackupModalOpen(true)
|
||||||
{getBackupStorageLabel(backup.volid)}
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<HardDrive className="h-5 w-5 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between mb-1 gap-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<Badge variant="outline" className={getBackupTypeColor(backup.volid)}>
|
||||||
|
{getBackupTypeLabel(backup.volid)}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className={getBackupStorageColor(backup.volid)}>
|
||||||
|
{getBackupStorageLabel(backup.volid)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="bg-green-500/10 text-green-500 border-green-500/20 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{backup.size_human}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<div className="text-xs text-muted-foreground mb-1 truncate">Storage: {backup.storage}</div>
|
||||||
variant="outline"
|
<div className="text-xs text-muted-foreground flex items-center">
|
||||||
className="bg-green-500/10 text-green-500 border-green-500/20 whitespace-nowrap"
|
<Calendar className="h-3 w-3 mr-1 flex-shrink-0" />
|
||||||
>
|
<span className="truncate">{backup.created}</span>
|
||||||
{backup.size_human}
|
</div>
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground mb-1 truncate">Storage: {backup.storage}</div>
|
|
||||||
<div className="text-xs text-muted-foreground flex items-center">
|
|
||||||
<Calendar className="h-3 w-3 mr-1 flex-shrink-0" />
|
|
||||||
<span className="truncate">{backup.created}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
))}
|
})}
|
||||||
|
|
||||||
{backups.length === 0 && (
|
{backups.length === 0 && (
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
@@ -1083,42 +1114,47 @@ export function SystemLogs() {
|
|||||||
<TabsContent value="notifications" className="space-y-4">
|
<TabsContent value="notifications" className="space-y-4">
|
||||||
<ScrollArea className="h-[600px] w-full rounded-md border border-border">
|
<ScrollArea className="h-[600px] w-full rounded-md border border-border">
|
||||||
<div className="space-y-2 p-4">
|
<div className="space-y-2 p-4">
|
||||||
{notifications.map((notification, index) => (
|
{memoizedNotifications.map((notification, index) => {
|
||||||
<div
|
const timestampMs = new Date(notification.timestamp).getTime()
|
||||||
key={index}
|
const uniqueKey = `notification-${timestampMs}-${notification.service?.substring(0, 10) || "unknown"}-${notification.source?.substring(0, 10) || "unknown"}-${index}`
|
||||||
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden w-full"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedNotification(notification)
|
|
||||||
setIsNotificationModalOpen(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex-shrink-0 flex gap-2 flex-wrap">
|
|
||||||
<Badge variant="outline" className={getNotificationTypeColor(notification.type)}>
|
|
||||||
{notification.type.toUpperCase()}
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="outline" className={getNotificationSourceColor(notification.source)}>
|
|
||||||
{notification.source === "task-log" && <Activity className="h-3 w-3 mr-1" />}
|
|
||||||
{notification.source === "journal" && <FileText className="h-3 w-3 mr-1" />}
|
|
||||||
{notification.source.toUpperCase()}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0 overflow-hidden">
|
return (
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-1 gap-1">
|
<div
|
||||||
<div className="text-sm font-medium text-foreground truncate">{notification.service}</div>
|
key={uniqueKey}
|
||||||
<div className="text-xs text-muted-foreground font-mono truncate">
|
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden w-full"
|
||||||
{notification.timestamp}
|
onClick={() => {
|
||||||
|
setSelectedNotification(notification)
|
||||||
|
setIsNotificationModalOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0 flex gap-2 flex-wrap">
|
||||||
|
<Badge variant="outline" className={getNotificationTypeColor(notification.type)}>
|
||||||
|
{notification.type.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className={getNotificationSourceColor(notification.source)}>
|
||||||
|
{notification.source === "task-log" && <Activity className="h-3 w-3 mr-1" />}
|
||||||
|
{notification.source === "journal" && <FileText className="h-3 w-3 mr-1" />}
|
||||||
|
{notification.source.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0 overflow-hidden">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-1 gap-1">
|
||||||
|
<div className="text-sm font-medium text-foreground truncate">{notification.service}</div>
|
||||||
|
<div className="text-xs text-muted-foreground font-mono truncate">
|
||||||
|
{notification.timestamp}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-foreground mb-1 line-clamp-2 break-all overflow-hidden">
|
||||||
|
{notification.message}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground break-words overflow-hidden">
|
||||||
|
Service: {notification.service} • Source: {notification.source}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-foreground mb-1 line-clamp-2 break-all overflow-hidden">
|
|
||||||
{notification.message}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground break-words overflow-hidden">
|
|
||||||
Service: {notification.service} • Source: {notification.source}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
))}
|
})}
|
||||||
|
|
||||||
{notifications.length === 0 && (
|
{notifications.length === 0 && (
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Net
|
|||||||
import { NodeMetricsCharts } from "./node-metrics-charts"
|
import { NodeMetricsCharts } from "./node-metrics-charts"
|
||||||
import { NetworkTrafficChart } from "./network-traffic-chart"
|
import { NetworkTrafficChart } from "./network-traffic-chart"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||||
|
import { fetchApi } from "../lib/api-config"
|
||||||
|
|
||||||
interface SystemData {
|
interface SystemData {
|
||||||
cpu_usage: number
|
cpu_usage: number
|
||||||
@@ -97,22 +98,7 @@ interface ProxmoxStorageData {
|
|||||||
|
|
||||||
const fetchSystemData = async (): Promise<SystemData | null> => {
|
const fetchSystemData = async (): Promise<SystemData | null> => {
|
||||||
try {
|
try {
|
||||||
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
const data = await fetchApi<SystemData>("/api/system")
|
||||||
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
|
return data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[v0] Failed to fetch system data:", error)
|
console.error("[v0] Failed to fetch system data:", error)
|
||||||
@@ -122,22 +108,7 @@ const fetchSystemData = async (): Promise<SystemData | null> => {
|
|||||||
|
|
||||||
const fetchVMData = async (): Promise<VMData[]> => {
|
const fetchVMData = async (): Promise<VMData[]> => {
|
||||||
try {
|
try {
|
||||||
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
const data = await fetchApi<any>("/api/vms")
|
||||||
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 || []
|
return Array.isArray(data) ? data : data.vms || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[v0] Failed to fetch VM data:", error)
|
console.error("[v0] Failed to fetch VM data:", error)
|
||||||
@@ -147,78 +118,30 @@ const fetchVMData = async (): Promise<VMData[]> => {
|
|||||||
|
|
||||||
const fetchStorageData = async (): Promise<StorageData | null> => {
|
const fetchStorageData = async (): Promise<StorageData | null> => {
|
||||||
try {
|
try {
|
||||||
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
const data = await fetchApi<StorageData>("/api/storage/summary")
|
||||||
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
|
return data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[v0] Storage data unavailable:", error instanceof Error ? error.message : "Unknown error")
|
console.log("[v0] Storage API not available (this is normal if not configured)")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchNetworkData = async (): Promise<NetworkData | null> => {
|
const fetchNetworkData = async (): Promise<NetworkData | null> => {
|
||||||
try {
|
try {
|
||||||
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
const data = await fetchApi<NetworkData>("/api/network/summary")
|
||||||
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
|
return data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[v0] Network data unavailable:", error instanceof Error ? error.message : "Unknown error")
|
console.log("[v0] Network API not available (this is normal if not configured)")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchProxmoxStorageData = async (): Promise<ProxmoxStorageData | null> => {
|
const fetchProxmoxStorageData = async (): Promise<ProxmoxStorageData | null> => {
|
||||||
try {
|
try {
|
||||||
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
const data = await fetchApi<ProxmoxStorageData>("/api/proxmox-storage")
|
||||||
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
|
return data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[v0] Proxmox storage data unavailable:", error instanceof Error ? error.message : "Unknown error")
|
console.log("[v0] Proxmox storage API not available")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,100 +152,87 @@ export function SystemOverview() {
|
|||||||
const [storageData, setStorageData] = useState<StorageData | null>(null)
|
const [storageData, setStorageData] = useState<StorageData | null>(null)
|
||||||
const [proxmoxStorageData, setProxmoxStorageData] = useState<ProxmoxStorageData | null>(null)
|
const [proxmoxStorageData, setProxmoxStorageData] = useState<ProxmoxStorageData | null>(null)
|
||||||
const [networkData, setNetworkData] = useState<NetworkData | null>(null)
|
const [networkData, setNetworkData] = useState<NetworkData | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loadingStates, setLoadingStates] = useState({
|
||||||
|
system: true,
|
||||||
|
vms: true,
|
||||||
|
storage: true,
|
||||||
|
network: true,
|
||||||
|
})
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [networkTimeframe, setNetworkTimeframe] = useState("day")
|
const [networkTimeframe, setNetworkTimeframe] = useState("day")
|
||||||
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
|
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchAllData = async () => {
|
||||||
try {
|
const [systemResult, vmResult, storageResults, networkResult] = await Promise.all([
|
||||||
setLoading(true)
|
fetchSystemData().finally(() => setLoadingStates((prev) => ({ ...prev, system: false }))),
|
||||||
setError(null)
|
fetchVMData().finally(() => setLoadingStates((prev) => ({ ...prev, vms: false }))),
|
||||||
|
Promise.all([fetchStorageData(), fetchProxmoxStorageData()]).finally(() =>
|
||||||
|
setLoadingStates((prev) => ({ ...prev, storage: false })),
|
||||||
|
),
|
||||||
|
fetchNetworkData().finally(() => setLoadingStates((prev) => ({ ...prev, network: false }))),
|
||||||
|
])
|
||||||
|
|
||||||
const systemResult = await fetchSystemData()
|
if (!systemResult) {
|
||||||
|
setError("Flask server not available. Please ensure the server is running.")
|
||||||
if (!systemResult) {
|
return
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSystemData(systemResult)
|
||||||
|
setVmData(vmResult)
|
||||||
|
setStorageData(storageResults[0])
|
||||||
|
setProxmoxStorageData(storageResults[1])
|
||||||
|
setNetworkData(networkResult)
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
const refreshedSystemData = await fetchSystemData()
|
||||||
|
if (refreshedSystemData) {
|
||||||
|
setSystemData(refreshedSystemData)
|
||||||
|
}
|
||||||
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchData()
|
fetchAllData()
|
||||||
|
|
||||||
const systemInterval = setInterval(() => {
|
const systemInterval = setInterval(async () => {
|
||||||
fetchSystemData().then((data) => {
|
const data = await fetchSystemData()
|
||||||
if (data) setSystemData(data)
|
if (data) setSystemData(data)
|
||||||
})
|
}, 9000)
|
||||||
}, 10000)
|
|
||||||
|
const vmInterval = setInterval(async () => {
|
||||||
|
const data = await fetchVMData()
|
||||||
|
setVmData(data)
|
||||||
|
}, 59000)
|
||||||
|
|
||||||
|
const storageInterval = setInterval(async () => {
|
||||||
|
const [storage, proxmoxStorage] = await Promise.all([fetchStorageData(), fetchProxmoxStorageData()])
|
||||||
|
if (storage) setStorageData(storage)
|
||||||
|
if (proxmoxStorage) setProxmoxStorageData(proxmoxStorage)
|
||||||
|
}, 59000)
|
||||||
|
|
||||||
|
const networkInterval = setInterval(async () => {
|
||||||
|
const data = await fetchNetworkData()
|
||||||
|
if (data) setNetworkData(data)
|
||||||
|
}, 59000)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(systemInterval)
|
clearInterval(systemInterval)
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchVMs = async () => {
|
|
||||||
const vmResult = await fetchVMData()
|
|
||||||
setVmData(vmResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchVMs()
|
|
||||||
const vmInterval = setInterval(fetchVMs, 60000)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(vmInterval)
|
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)
|
clearInterval(storageInterval)
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchNetwork = async () => {
|
|
||||||
const networkResult = await fetchNetworkData()
|
|
||||||
setNetworkData(networkResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchNetwork()
|
|
||||||
const networkInterval = setInterval(fetchNetwork, 60000)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(networkInterval)
|
clearInterval(networkInterval)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (loading) {
|
const isInitialLoading = loadingStates.system && !systemData
|
||||||
|
|
||||||
|
if (isInitialLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="text-lg font-medium text-foreground mb-2">Connecting to ProxMenux Monitor...</div>
|
<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 className="text-sm text-muted-foreground">Fetching real-time system data</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
|
||||||
{[...Array(4)].map((_, i) => (
|
{[...Array(4)].map((_, i) => (
|
||||||
<Card key={i} className="bg-card border-border animate-pulse">
|
<Card key={i} className="bg-card border-border animate-pulse">
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
@@ -390,14 +300,11 @@ export function SystemOverview() {
|
|||||||
|
|
||||||
const formatStorage = (sizeInGB: number): string => {
|
const formatStorage = (sizeInGB: number): string => {
|
||||||
if (sizeInGB < 1) {
|
if (sizeInGB < 1) {
|
||||||
// Less than 1 GB, show in MB
|
|
||||||
return `${(sizeInGB * 1024).toFixed(1)} MB`
|
return `${(sizeInGB * 1024).toFixed(1)} MB`
|
||||||
} else if (sizeInGB < 1024) {
|
} else if (sizeInGB > 999) {
|
||||||
// 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`
|
return `${(sizeInGB / 1024).toFixed(2)} TB`
|
||||||
|
} else {
|
||||||
|
return `${sizeInGB.toFixed(2)} GB`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,13 +314,10 @@ export function SystemOverview() {
|
|||||||
|
|
||||||
const vmLxcStorages = proxmoxStorageData?.storage.filter(
|
const vmLxcStorages = proxmoxStorageData?.storage.filter(
|
||||||
(s) =>
|
(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") &&
|
(s.type === "lvm" || s.type === "lvmthin" || s.type === "zfspool" || s.type === "btrfs" || s.type === "dir") &&
|
||||||
// Exclude network storage
|
|
||||||
s.type !== "nfs" &&
|
s.type !== "nfs" &&
|
||||||
s.type !== "cifs" &&
|
s.type !== "cifs" &&
|
||||||
s.type !== "iscsi" &&
|
s.type !== "iscsi" &&
|
||||||
// Exclude the "local" storage (used for ISOs/templates)
|
|
||||||
s.name !== "local",
|
s.name !== "local",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -479,7 +383,6 @@ export function SystemOverview() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Key Metrics Cards */}
|
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
|
||||||
<Card className="bg-card border-border">
|
<Card className="bg-card border-border">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
@@ -529,34 +432,44 @@ export function SystemOverview() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="bg-card border-border">
|
<Card className="bg-card border-border">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader>
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">Active VM & LXC</CardTitle>
|
<CardTitle className="text-foreground flex items-center">
|
||||||
<Server className="h-4 w-4 text-muted-foreground" />
|
<Server className="h-5 w-5 mr-2" />
|
||||||
|
Active VM & LXC
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-xl lg:text-2xl font-bold text-foreground">{vmStats.running}</div>
|
{loadingStates.vms ? (
|
||||||
<div className="mt-2 flex flex-wrap gap-1">
|
<div className="space-y-2 animate-pulse">
|
||||||
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
<div className="h-8 bg-muted rounded w-12"></div>
|
||||||
{vmStats.running} Running
|
<div className="h-5 bg-muted rounded w-24"></div>
|
||||||
</Badge>
|
<div className="h-4 bg-muted rounded w-32"></div>
|
||||||
{vmStats.stopped > 0 && (
|
</div>
|
||||||
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20">
|
) : (
|
||||||
{vmStats.stopped} Stopped
|
<>
|
||||||
</Badge>
|
<div className="text-xl lg:text-2xl font-bold text-foreground">{vmStats.running}</div>
|
||||||
)}
|
<div className="mt-2 flex flex-wrap gap-1">
|
||||||
</div>
|
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
{vmStats.running} Running
|
||||||
Total: {vmStats.vms} VMs, {vmStats.lxc} LXC
|
</Badge>
|
||||||
</p>
|
{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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Node Metrics Charts */}
|
|
||||||
<NodeMetricsCharts />
|
<NodeMetricsCharts />
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{/* Storage Summary */}
|
|
||||||
<Card className="bg-card border-border">
|
<Card className="bg-card border-border">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-foreground flex items-center">
|
<CardTitle className="text-foreground flex items-center">
|
||||||
@@ -565,8 +478,45 @@ export function SystemOverview() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{storageData ? (
|
{loadingStates.storage ? (
|
||||||
|
<div className="space-y-4 animate-pulse">
|
||||||
|
<div className="h-6 bg-muted rounded w-full"></div>
|
||||||
|
<div className="h-4 bg-muted rounded w-3/4"></div>
|
||||||
|
<div className="h-4 bg-muted rounded w-2/3"></div>
|
||||||
|
</div>
|
||||||
|
) : storageData ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{(() => {
|
||||||
|
const totalCapacity = (vmLxcStorageTotal || 0) + (localStorage?.total || 0)
|
||||||
|
const totalUsed = (vmLxcStorageUsed || 0) + (localStorage?.used || 0)
|
||||||
|
const totalAvailable = (vmLxcStorageAvailable || 0) + (localStorage?.available || 0)
|
||||||
|
const totalPercent = totalCapacity > 0 ? (totalUsed / totalCapacity) * 100 : 0
|
||||||
|
|
||||||
|
return totalCapacity > 0 ? (
|
||||||
|
<div className="space-y-2 pb-4 border-b-2 border-border">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium text-foreground">Total Node Capacity:</span>
|
||||||
|
<span className="text-lg font-bold text-foreground">{formatStorage(totalCapacity)}</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
value={totalPercent}
|
||||||
|
className="mt-2 h-3 [&>div]:bg-gradient-to-r [&>div]:from-blue-500 [&>div]:to-purple-500"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between items-center mt-1">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Used: <span className="font-semibold text-foreground">{formatStorage(totalUsed)}</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Free: <span className="font-semibold text-green-500">{formatStorage(totalAvailable)}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-semibold text-muted-foreground">{totalPercent.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
})()}
|
||||||
|
|
||||||
<div className="space-y-2 pb-3 border-b border-border">
|
<div className="space-y-2 pb-3 border-b border-border">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm text-muted-foreground">Total Capacity:</span>
|
<span className="text-sm text-muted-foreground">Total Capacity:</span>
|
||||||
@@ -642,7 +592,6 @@ export function SystemOverview() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Network Summary */}
|
|
||||||
<Card className="bg-card border-border">
|
<Card className="bg-card border-border">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-foreground flex items-center justify-between">
|
<CardTitle className="text-foreground flex items-center justify-between">
|
||||||
@@ -665,7 +614,13 @@ export function SystemOverview() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{networkData ? (
|
{loadingStates.network ? (
|
||||||
|
<div className="space-y-4 animate-pulse">
|
||||||
|
<div className="h-6 bg-muted rounded w-full"></div>
|
||||||
|
<div className="h-4 bg-muted rounded w-3/4"></div>
|
||||||
|
<div className="h-4 bg-muted rounded w-2/3"></div>
|
||||||
|
</div>
|
||||||
|
) : networkData ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center pb-3 border-b border-border">
|
<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-sm text-muted-foreground">Active Interfaces:</span>
|
||||||
@@ -736,7 +691,6 @@ export function SystemOverview() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* System Information */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<Card className="bg-card border-border">
|
<Card className="bg-card border-border">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -769,7 +723,6 @@ export function SystemOverview() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* System Health & Alerts */}
|
|
||||||
<Card className="bg-card border-border">
|
<Card className="bg-card border-border">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-foreground flex items-center">
|
<CardTitle className="text-foreground flex items-center">
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import { Button } from "./ui/button"
|
||||||
|
import { Input } from "./ui/input"
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"
|
||||||
|
import { AlertCircle, CheckCircle, Copy, Shield, Check } from "lucide-react"
|
||||||
|
import { getApiUrl } from "../lib/api-config"
|
||||||
|
|
||||||
|
interface TwoFactorSetupProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TwoFactorSetup({ open, onClose, onSuccess }: TwoFactorSetupProps) {
|
||||||
|
const [step, setStep] = useState(1)
|
||||||
|
const [qrCode, setQrCode] = useState("")
|
||||||
|
const [secret, setSecret] = useState("")
|
||||||
|
const [backupCodes, setBackupCodes] = useState<string[]>([])
|
||||||
|
const [verificationCode, setVerificationCode] = useState("")
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [copiedSecret, setCopiedSecret] = useState(false)
|
||||||
|
const [copiedCodes, setCopiedCodes] = useState(false)
|
||||||
|
|
||||||
|
const handleSetupStart = async () => {
|
||||||
|
setError("")
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("proxmenux-auth-token")
|
||||||
|
const response = await fetch(getApiUrl("/api/auth/totp/setup"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || "Failed to setup 2FA")
|
||||||
|
}
|
||||||
|
|
||||||
|
setQrCode(data.qr_code)
|
||||||
|
setSecret(data.secret)
|
||||||
|
setBackupCodes(data.backup_codes)
|
||||||
|
setStep(2)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to setup 2FA")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVerify = async () => {
|
||||||
|
if (!verificationCode || verificationCode.length !== 6) {
|
||||||
|
setError("Please enter a 6-digit code")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setError("")
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("proxmenux-auth-token")
|
||||||
|
const response = await fetch(getApiUrl("/api/auth/totp/enable"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ token: verificationCode }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || "Invalid verification code")
|
||||||
|
}
|
||||||
|
|
||||||
|
setStep(3)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Verification failed")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyToClipboard = (text: string, type: "secret" | "codes") => {
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
if (type === "secret") {
|
||||||
|
setCopiedSecret(true)
|
||||||
|
setTimeout(() => setCopiedSecret(false), 2000)
|
||||||
|
} else {
|
||||||
|
setCopiedCodes(true)
|
||||||
|
setTimeout(() => setCopiedCodes(false), 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setStep(1)
|
||||||
|
setQrCode("")
|
||||||
|
setSecret("")
|
||||||
|
setBackupCodes([])
|
||||||
|
setVerificationCode("")
|
||||||
|
setError("")
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFinish = () => {
|
||||||
|
handleClose()
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleClose}>
|
||||||
|
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Shield className="h-5 w-5 text-blue-500" />
|
||||||
|
Setup Two-Factor Authentication
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>Add an extra layer of security to your account</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-red-500">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 1 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
|
||||||
|
<p className="text-sm text-blue-500">
|
||||||
|
Two-factor authentication (2FA) adds an extra layer of security by requiring a code from your
|
||||||
|
authentication app in addition to your password.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium">You will need:</h4>
|
||||||
|
<ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
|
||||||
|
<li>An authentication app (Google Authenticator, Authy, etc.)</li>
|
||||||
|
<li>Scan a QR code or enter a key manually</li>
|
||||||
|
<li>Store backup codes securely</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={handleSetupStart} className="w-full bg-blue-500 hover:bg-blue-600" disabled={loading}>
|
||||||
|
{loading ? "Starting..." : "Start Setup"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium">1. Scan the QR code</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">Open your authentication app and scan this QR code</p>
|
||||||
|
{qrCode && (
|
||||||
|
<div className="flex justify-center p-4 bg-white rounded-lg">
|
||||||
|
<img src={qrCode || "/placeholder.svg"} alt="QR Code" width={200} height={200} className="rounded" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium">Or enter the key manually:</h4>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input value={secret} readOnly className="font-mono text-sm" />
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => copyToClipboard(secret, "secret")}
|
||||||
|
title="Copy key"
|
||||||
|
>
|
||||||
|
{copiedSecret ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium">2. Enter the verification code</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">Enter the 6-digit code that appears in your app</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="000000"
|
||||||
|
value={verificationCode}
|
||||||
|
onChange={(e) => setVerificationCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
|
||||||
|
className="text-center text-lg tracking-widest font-mono text-base"
|
||||||
|
maxLength={6}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={handleVerify} className="flex-1 bg-blue-500 hover:bg-blue-600" disabled={loading}>
|
||||||
|
{loading ? "Verifying..." : "Verify and Enable"}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleClose} variant="outline" className="flex-1 bg-transparent" disabled={loading}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 3 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4 flex items-start gap-2">
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-green-500">2FA Enabled Successfully</p>
|
||||||
|
<p className="text-sm text-green-500 mt-1">
|
||||||
|
Your account is now protected with two-factor authentication
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium text-orange-500">Important: Save your backup codes</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
These codes will allow you to access your account if you lose access to your authentication app. Store
|
||||||
|
them in a safe place.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-muted/50 rounded-lg p-4 space-y-2">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="text-sm font-medium">Backup Codes</span>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => copyToClipboard(backupCodes.join("\n"), "codes")}>
|
||||||
|
{copiedCodes ? (
|
||||||
|
<Check className="h-4 w-4 text-green-500 mr-2" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4 mr-2" />
|
||||||
|
)}
|
||||||
|
Copy All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{backupCodes.map((code, index) => (
|
||||||
|
<div key={index} className="bg-background rounded px-3 py-2 font-mono text-sm text-center">
|
||||||
|
{code}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={handleFinish} className="w-full bg-blue-500 hover:bg-blue-600">
|
||||||
|
Finish
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { Check } from "lucide-react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Checkbox = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
))
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
@@ -41,6 +41,7 @@ const DialogContent = React.forwardRef<
|
|||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
|
"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,
|
className,
|
||||||
)}
|
)}
|
||||||
|
aria-describedby={props["aria-describedby"] || undefined}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type,
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-10 w-full rounded-lg border border-input bg-background px-4 py-2 text-sm shadow-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 hover:border-ring/50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
|
||||||
|
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70")
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||||
|
))
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Label }
|
||||||
@@ -25,6 +25,8 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
import { MetricsView } from "./metrics-dialog"
|
import { MetricsView } from "./metrics-dialog"
|
||||||
|
import { formatStorage } from "@/lib/utils" // Import formatStorage utility
|
||||||
|
import { fetchApi } from "../lib/api-config"
|
||||||
|
|
||||||
interface VMData {
|
interface VMData {
|
||||||
vmid: number
|
vmid: number
|
||||||
@@ -123,7 +125,6 @@ interface VMDetails extends VMData {
|
|||||||
gpu_passthrough?: string[]
|
gpu_passthrough?: string[]
|
||||||
devices?: string[]
|
devices?: string[]
|
||||||
}
|
}
|
||||||
lxc_ip?: string
|
|
||||||
lxc_ip_info?: {
|
lxc_ip_info?: {
|
||||||
all_ips: string[]
|
all_ips: string[]
|
||||||
real_ips: string[]
|
real_ips: string[]
|
||||||
@@ -133,20 +134,7 @@ interface VMDetails extends VMData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetcher = async (url: string) => {
|
const fetcher = async (url: string) => {
|
||||||
const response = await fetch(url, {
|
return fetchApi(url)
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(5000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatBytes = (bytes: number | undefined): string => {
|
const formatBytes = (bytes: number | undefined): string => {
|
||||||
@@ -194,18 +182,18 @@ const extractIPFromConfig = (config?: VMConfig, lxcIPInfo?: VMDetails["lxc_ip_in
|
|||||||
return "DHCP"
|
return "DHCP"
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatStorage = (sizeInGB: number): string => {
|
// const formatStorage = (sizeInGB: number): string => {
|
||||||
if (sizeInGB < 1) {
|
// if (sizeInGB < 1) {
|
||||||
// Less than 1 GB, show in MB
|
// // Less than 1 GB, show in MB
|
||||||
return `${(sizeInGB * 1024).toFixed(1)} MB`
|
// return `${(sizeInGB * 1024).toFixed(1)} MB`
|
||||||
} else if (sizeInGB < 1024) {
|
// } else if (sizeInGB < 1024) {
|
||||||
// Less than 1024 GB, show in GB
|
// // Less than 1024 GB, show in GB
|
||||||
return `${sizeInGB.toFixed(1)} GB`
|
// return `${sizeInGB.toFixed(1)} GB`
|
||||||
} else {
|
// } else {
|
||||||
// 1024 GB or more, show in TB
|
// // 1024 GB or more, show in TB
|
||||||
return `${(sizeInGB / 1024).toFixed(1)} TB`
|
// return `${(sizeInGB / 1024).toFixed(1)} TB`
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const getUsageColor = (percent: number): string => {
|
const getUsageColor = (percent: number): string => {
|
||||||
if (percent >= 95) return "text-red-500"
|
if (percent >= 95) return "text-red-500"
|
||||||
@@ -263,9 +251,11 @@ export function VirtualMachines() {
|
|||||||
isLoading,
|
isLoading,
|
||||||
mutate,
|
mutate,
|
||||||
} = useSWR<VMData[]>("/api/vms", fetcher, {
|
} = useSWR<VMData[]>("/api/vms", fetcher, {
|
||||||
refreshInterval: 30000,
|
refreshInterval: 23000,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
revalidateOnReconnect: true,
|
revalidateOnReconnect: true,
|
||||||
|
dedupingInterval: 10000,
|
||||||
|
errorRetryCount: 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [selectedVM, setSelectedVM] = useState<VMData | null>(null)
|
const [selectedVM, setSelectedVM] = useState<VMData | null>(null)
|
||||||
@@ -280,37 +270,59 @@ export function VirtualMachines() {
|
|||||||
const [editedNotes, setEditedNotes] = useState("")
|
const [editedNotes, setEditedNotes] = useState("")
|
||||||
const [savingNotes, setSavingNotes] = useState(false)
|
const [savingNotes, setSavingNotes] = useState(false)
|
||||||
const [selectedMetric, setSelectedMetric] = useState<string | null>(null)
|
const [selectedMetric, setSelectedMetric] = useState<string | null>(null)
|
||||||
|
const [ipsLoaded, setIpsLoaded] = useState(false)
|
||||||
|
const [loadingIPs, setLoadingIPs] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLXCIPs = async () => {
|
const fetchLXCIPs = async () => {
|
||||||
if (!vmData) return
|
// Only fetch if data exists, not already loaded, and not currently loading
|
||||||
|
if (!vmData || ipsLoaded || loadingIPs) return
|
||||||
|
|
||||||
const lxcs = vmData.filter((vm) => vm.type === "lxc")
|
const lxcs = vmData.filter((vm) => vm.type === "lxc")
|
||||||
|
|
||||||
|
if (lxcs.length === 0) {
|
||||||
|
setIpsLoaded(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingIPs(true)
|
||||||
const configs: Record<number, string> = {}
|
const configs: Record<number, string> = {}
|
||||||
|
|
||||||
await Promise.all(
|
const batchSize = 5
|
||||||
lxcs.map(async (lxc) => {
|
for (let i = 0; i < lxcs.length; i += batchSize) {
|
||||||
try {
|
const batch = lxcs.slice(i, i + batchSize)
|
||||||
const response = await fetch(`/api/vms/${lxc.vmid}`)
|
|
||||||
if (response.ok) {
|
await Promise.all(
|
||||||
const details = await response.json()
|
batch.map(async (lxc) => {
|
||||||
|
try {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 10000)
|
||||||
|
|
||||||
|
const details = await fetchApi(`/api/vms/${lxc.vmid}`)
|
||||||
|
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
if (details.lxc_ip_info?.primary_ip) {
|
if (details.lxc_ip_info?.primary_ip) {
|
||||||
configs[lxc.vmid] = details.lxc_ip_info.primary_ip
|
configs[lxc.vmid] = details.lxc_ip_info.primary_ip
|
||||||
} else if (details.config) {
|
} else if (details.config) {
|
||||||
configs[lxc.vmid] = extractIPFromConfig(details.config, details.lxc_ip_info)
|
configs[lxc.vmid] = extractIPFromConfig(details.config, details.lxc_ip_info)
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`[v0] Could not fetch IP for LXC ${lxc.vmid}`)
|
||||||
|
configs[lxc.vmid] = "N/A"
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}),
|
||||||
console.error(`Error fetching config for LXC ${lxc.vmid}:`, error)
|
)
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
setVmConfigs(configs)
|
setVmConfigs((prev) => ({ ...prev, ...configs }))
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingIPs(false)
|
||||||
|
setIpsLoaded(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchLXCIPs()
|
fetchLXCIPs()
|
||||||
}, [vmData])
|
}, [vmData, ipsLoaded, loadingIPs])
|
||||||
|
|
||||||
const handleVMClick = async (vm: VMData) => {
|
const handleVMClick = async (vm: VMData) => {
|
||||||
setSelectedVM(vm)
|
setSelectedVM(vm)
|
||||||
@@ -321,11 +333,8 @@ export function VirtualMachines() {
|
|||||||
setEditedNotes("")
|
setEditedNotes("")
|
||||||
setDetailsLoading(true)
|
setDetailsLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/vms/${vm.vmid}`)
|
const details = await fetchApi(`/api/vms/${vm.vmid}`)
|
||||||
if (response.ok) {
|
setVMDetails(details)
|
||||||
const details = await response.json()
|
|
||||||
setVMDetails(details)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching VM details:", error)
|
console.error("Error fetching VM details:", error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -344,23 +353,16 @@ export function VirtualMachines() {
|
|||||||
const handleVMControl = async (vmid: number, action: string) => {
|
const handleVMControl = async (vmid: number, action: string) => {
|
||||||
setControlLoading(true)
|
setControlLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/vms/${vmid}/control`, {
|
await fetchApi(`/api/vms/${vmid}/control`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ action }),
|
body: JSON.stringify({ action }),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok) {
|
mutate()
|
||||||
mutate()
|
setSelectedVM(null)
|
||||||
setSelectedVM(null)
|
setVMDetails(null)
|
||||||
setVMDetails(null)
|
|
||||||
} else {
|
|
||||||
console.error("Failed to control VM")
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error controlling VM:", error)
|
console.error("Failed to control VM")
|
||||||
} finally {
|
} finally {
|
||||||
setControlLoading(false)
|
setControlLoading(false)
|
||||||
}
|
}
|
||||||
@@ -368,36 +370,33 @@ export function VirtualMachines() {
|
|||||||
|
|
||||||
const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/vms/${vmid}/logs`)
|
const data = await fetchApi(`/api/vms/${vmid}/logs`)
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
// Format logs as plain text
|
// Format logs as plain text
|
||||||
let logText = `=== Logs for ${vmName} (VMID: ${vmid}) ===\n`
|
let logText = `=== Logs for ${vmName} (VMID: ${vmid}) ===\n`
|
||||||
logText += `Node: ${data.node}\n`
|
logText += `Node: ${data.node}\n`
|
||||||
logText += `Type: ${data.type}\n`
|
logText += `Type: ${data.type}\n`
|
||||||
logText += `Total lines: ${data.log_lines}\n`
|
logText += `Total lines: ${data.log_lines}\n`
|
||||||
logText += `Generated: ${new Date().toISOString()}\n`
|
logText += `Generated: ${new Date().toISOString()}\n`
|
||||||
logText += `\n${"=".repeat(80)}\n\n`
|
logText += `\n${"=".repeat(80)}\n\n`
|
||||||
|
|
||||||
if (data.logs && Array.isArray(data.logs)) {
|
if (data.logs && Array.isArray(data.logs)) {
|
||||||
data.logs.forEach((log: any) => {
|
data.logs.forEach((log: any) => {
|
||||||
if (typeof log === "object" && log.t) {
|
if (typeof log === "object" && log.t) {
|
||||||
logText += `${log.t}\n`
|
logText += `${log.t}\n`
|
||||||
} else if (typeof log === "string") {
|
} else if (typeof log === "string") {
|
||||||
logText += `${log}\n`
|
logText += `${log}\n`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([logText], { type: "text/plain" })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement("a")
|
|
||||||
a.href = url
|
|
||||||
a.download = `${vmName}-${vmid}-logs.txt`
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([logText], { type: "text/plain" })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement("a")
|
||||||
|
a.href = url
|
||||||
|
a.download = `${vmName}-${vmid}-logs.txt`
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error downloading logs:", error)
|
console.error("Error downloading logs:", error)
|
||||||
}
|
}
|
||||||
@@ -450,7 +449,7 @@ export function VirtualMachines() {
|
|||||||
"/api/system",
|
"/api/system",
|
||||||
fetcher,
|
fetcher,
|
||||||
{
|
{
|
||||||
refreshInterval: 30000,
|
refreshInterval: 37000,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -592,29 +591,21 @@ export function VirtualMachines() {
|
|||||||
|
|
||||||
setSavingNotes(true)
|
setSavingNotes(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/vms/${selectedVM.vmid}/config`, {
|
await fetchApi(`/api/vms/${selectedVM.vmid}/config`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
description: editedNotes, // Send as-is, pvesh will handle encoding
|
description: editedNotes, // Send as-is, pvesh will handle encoding
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok) {
|
setVMDetails({
|
||||||
setVMDetails({
|
...vmDetails,
|
||||||
...vmDetails,
|
config: {
|
||||||
config: {
|
...vmDetails.config,
|
||||||
...vmDetails.config,
|
description: editedNotes, // Store unencoded
|
||||||
description: editedNotes, // Store unencoded
|
},
|
||||||
},
|
})
|
||||||
})
|
setIsEditingNotes(false)
|
||||||
setIsEditingNotes(false)
|
|
||||||
} else {
|
|
||||||
console.error("Failed to save notes")
|
|
||||||
alert("Failed to save notes. Please try again.")
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving notes:", error)
|
console.error("Error saving notes:", error)
|
||||||
alert("Error saving notes. Please try again.")
|
alert("Error saving notes. Please try again.")
|
||||||
@@ -1041,11 +1032,15 @@ export function VirtualMachines() {
|
|||||||
setEditedNotes("")
|
setEditedNotes("")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className="max-w-4xl h-[95vh] sm:h-[90vh] flex flex-col p-0 overflow-hidden">
|
<DialogContent
|
||||||
|
className="max-w-4xl h-[95vh] sm:h-[90vh] flex flex-col p-0 overflow-hidden"
|
||||||
|
key={selectedVM?.vmid || "no-vm"}
|
||||||
|
>
|
||||||
{currentView === "main" ? (
|
{currentView === "main" ? (
|
||||||
<>
|
<>
|
||||||
<DialogHeader className="pb-4 border-b border-border px-6 pt-6">
|
<DialogHeader className="pb-4 border-b border-border px-6 pt-6">
|
||||||
<DialogTitle className="flex flex-col gap-3">
|
<DialogTitle className="flex flex-col gap-3">
|
||||||
|
{/* Desktop layout: Uptime now appears after status badge */}
|
||||||
<div className="hidden sm:flex items-center gap-3 flex-wrap">
|
<div className="hidden sm:flex items-center gap-3 flex-wrap">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Server className="h-5 w-5 flex-shrink-0" />
|
<Server className="h-5 w-5 flex-shrink-0" />
|
||||||
@@ -1062,15 +1057,16 @@ export function VirtualMachines() {
|
|||||||
<Badge variant="outline" className={`${getStatusColor(selectedVM.status)} flex-shrink-0`}>
|
<Badge variant="outline" className={`${getStatusColor(selectedVM.status)} flex-shrink-0`}>
|
||||||
{selectedVM.status.toUpperCase()}
|
{selectedVM.status.toUpperCase()}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{selectedVM.status === "running" && (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Uptime: {formatUptime(selectedVM.uptime)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{selectedVM.status === "running" && (
|
|
||||||
<span className="text-sm text-muted-foreground ml-auto">
|
|
||||||
Uptime: {formatUptime(selectedVM.uptime)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Mobile layout unchanged */}
|
||||||
<div className="sm:hidden flex flex-col gap-2">
|
<div className="sm:hidden flex flex-col gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Server className="h-5 w-5 flex-shrink-0" />
|
<Server className="h-5 w-5 flex-shrink-0" />
|
||||||
@@ -1101,7 +1097,7 @@ export function VirtualMachines() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{selectedVM && (
|
{selectedVM && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div key={`metrics-${selectedVM.vmid}`}>
|
||||||
<Card
|
<Card
|
||||||
className="cursor-pointer rounded-lg border border-black/10 dark:border-white/10 sm:border-border max-sm:bg-black/5 max-sm:dark:bg-white/5 sm:bg-card sm:hover:bg-black/5 sm:dark:hover:bg-white/5 transition-colors group"
|
className="cursor-pointer rounded-lg border border-black/10 dark:border-white/10 sm:border-border max-sm:bg-black/5 max-sm:dark:bg-white/5 sm:bg-card sm:hover:bg-black/5 sm:dark:hover:bg-white/5 transition-colors group"
|
||||||
onClick={handleMetricsClick}
|
onClick={handleMetricsClick}
|
||||||
@@ -1192,7 +1188,7 @@ export function VirtualMachines() {
|
|||||||
<div className="text-center py-8 text-muted-foreground">Loading configuration...</div>
|
<div className="text-center py-8 text-muted-foreground">Loading configuration...</div>
|
||||||
) : vmDetails?.config ? (
|
) : vmDetails?.config ? (
|
||||||
<>
|
<>
|
||||||
<Card className="border border-border bg-card/50">
|
<Card className="border border-border bg-card/50" key={`config-${selectedVM.vmid}`}>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||||
@@ -1258,26 +1254,25 @@ export function VirtualMachines() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* IP Addresses with proper keys */}
|
||||||
{selectedVM?.type === "lxc" && vmDetails?.lxc_ip_info && (
|
{selectedVM?.type === "lxc" && vmDetails?.lxc_ip_info && (
|
||||||
<div className="mt-4 lg:mt-6 pt-4 lg:pt-6 border-t border-border">
|
<div className="mt-4 lg:mt-6 pt-4 lg:pt-6 border-t border-border">
|
||||||
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
|
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
|
||||||
IP Addresses
|
IP Addresses
|
||||||
</h4>
|
</h4>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{/* Real IPs (green, without "Real" label) */}
|
|
||||||
{vmDetails.lxc_ip_info.real_ips.map((ip, index) => (
|
{vmDetails.lxc_ip_info.real_ips.map((ip, index) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={`real-${index}`}
|
key={`real-ip-${selectedVM.vmid}-${ip.replace(/[.:/]/g, "-")}-${index}`}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="bg-green-500/10 text-green-500 border-green-500/20"
|
className="bg-green-500/10 text-green-500 border-green-500/20"
|
||||||
>
|
>
|
||||||
{ip}
|
{ip}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
{/* Docker bridge IPs (yellow, with "Bridge" label) */}
|
|
||||||
{vmDetails.lxc_ip_info.docker_ips.map((ip, index) => (
|
{vmDetails.lxc_ip_info.docker_ips.map((ip, index) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={`docker-${index}`}
|
key={`docker-ip-${selectedVM.vmid}-${ip.replace(/[.:/]/g, "-")}-${index}`}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
|
className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
|
||||||
>
|
>
|
||||||
@@ -1387,7 +1382,7 @@ export function VirtualMachines() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* GPU Passthrough */}
|
{/* GPU Passthrough with proper keys */}
|
||||||
{vmDetails.hardware_info.gpu_passthrough &&
|
{vmDetails.hardware_info.gpu_passthrough &&
|
||||||
vmDetails.hardware_info.gpu_passthrough.length > 0 && (
|
vmDetails.hardware_info.gpu_passthrough.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
@@ -1395,7 +1390,7 @@ export function VirtualMachines() {
|
|||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{vmDetails.hardware_info.gpu_passthrough.map((gpu, index) => (
|
{vmDetails.hardware_info.gpu_passthrough.map((gpu, index) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={index}
|
key={`gpu-${selectedVM.vmid}-${index}-${gpu.replace(/[^a-zA-Z0-9]/g, "-").substring(0, 30)}`}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={
|
className={
|
||||||
gpu.includes("NVIDIA")
|
gpu.includes("NVIDIA")
|
||||||
@@ -1410,7 +1405,7 @@ export function VirtualMachines() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Other Hardware Devices */}
|
{/* Hardware Devices with proper keys */}
|
||||||
{vmDetails.hardware_info.devices &&
|
{vmDetails.hardware_info.devices &&
|
||||||
vmDetails.hardware_info.devices.length > 0 && (
|
vmDetails.hardware_info.devices.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
@@ -1418,7 +1413,7 @@ export function VirtualMachines() {
|
|||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{vmDetails.hardware_info.devices.map((device, index) => (
|
{vmDetails.hardware_info.devices.map((device, index) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={index}
|
key={`device-${selectedVM.vmid}-${index}-${device.replace(/[^a-zA-Z0-9]/g, "-").substring(0, 30)}`}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="bg-blue-500/10 text-blue-500 border-blue-500/20"
|
className="bg-blue-500/10 text-blue-500 border-blue-500/20"
|
||||||
>
|
>
|
||||||
@@ -1540,7 +1535,7 @@ export function VirtualMachines() {
|
|||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{vmDetails.config.rootfs && (
|
{vmDetails.config.rootfs && (
|
||||||
<div>
|
<div key="rootfs">
|
||||||
<div className="text-xs text-muted-foreground mb-1">Root Filesystem</div>
|
<div className="text-xs text-muted-foreground mb-1">Root Filesystem</div>
|
||||||
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
|
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
|
||||||
{vmDetails.config.rootfs}
|
{vmDetails.config.rootfs}
|
||||||
@@ -1548,15 +1543,16 @@ export function VirtualMachines() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{vmDetails.config.scsihw && (
|
{vmDetails.config.scsihw && (
|
||||||
<div>
|
<div key="scsihw">
|
||||||
<div className="text-xs text-muted-foreground mb-1">SCSI Controller</div>
|
<div className="text-xs text-muted-foreground mb-1">SCSI Controller</div>
|
||||||
<div className="font-medium text-foreground">{vmDetails.config.scsihw}</div>
|
<div className="font-medium text-foreground">{vmDetails.config.scsihw}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* Disk Storage with proper keys */}
|
||||||
{Object.keys(vmDetails.config)
|
{Object.keys(vmDetails.config)
|
||||||
.filter((key) => key.match(/^(scsi|sata|ide|virtio)\d+$/))
|
.filter((key) => key.match(/^(scsi|sata|ide|virtio)\d+$/))
|
||||||
.map((diskKey) => (
|
.map((diskKey) => (
|
||||||
<div key={diskKey}>
|
<div key={`disk-${selectedVM.vmid}-${diskKey}`}>
|
||||||
<div className="text-xs text-muted-foreground mb-1">
|
<div className="text-xs text-muted-foreground mb-1">
|
||||||
{diskKey.toUpperCase().replace(/(\d+)/, " $1")}
|
{diskKey.toUpperCase().replace(/(\d+)/, " $1")}
|
||||||
</div>
|
</div>
|
||||||
@@ -1566,7 +1562,7 @@ export function VirtualMachines() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{vmDetails.config.efidisk0 && (
|
{vmDetails.config.efidisk0 && (
|
||||||
<div>
|
<div key="efidisk0">
|
||||||
<div className="text-xs text-muted-foreground mb-1">EFI Disk</div>
|
<div className="text-xs text-muted-foreground mb-1">EFI Disk</div>
|
||||||
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
|
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
|
||||||
{vmDetails.config.efidisk0}
|
{vmDetails.config.efidisk0}
|
||||||
@@ -1574,18 +1570,18 @@ export function VirtualMachines() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{vmDetails.config.tpmstate0 && (
|
{vmDetails.config.tpmstate0 && (
|
||||||
<div>
|
<div key="tpmstate0">
|
||||||
<div className="text-xs text-muted-foreground mb-1">TPM State</div>
|
<div className="text-xs text-muted-foreground mb-1">TPM State</div>
|
||||||
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
|
<div className="font-medium text-foreground text-sm break-all font-mono bg-muted/50 p-2 rounded">
|
||||||
{vmDetails.config.tpmstate0}
|
{vmDetails.config.tpmstate0}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Mount points for LXC */}
|
{/* Mount Points with proper keys */}
|
||||||
{Object.keys(vmDetails.config)
|
{Object.keys(vmDetails.config)
|
||||||
.filter((key) => key.match(/^mp\d+$/))
|
.filter((key) => key.match(/^mp\d+$/))
|
||||||
.map((mpKey) => (
|
.map((mpKey) => (
|
||||||
<div key={mpKey}>
|
<div key={`mp-${selectedVM.vmid}-${mpKey}`}>
|
||||||
<div className="text-xs text-muted-foreground mb-1">
|
<div className="text-xs text-muted-foreground mb-1">
|
||||||
Mount Point {mpKey.replace("mp", "")}
|
Mount Point {mpKey.replace("mp", "")}
|
||||||
</div>
|
</div>
|
||||||
@@ -1603,10 +1599,11 @@ export function VirtualMachines() {
|
|||||||
Network
|
Network
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
{/* Network Interfaces with proper keys */}
|
||||||
{Object.keys(vmDetails.config)
|
{Object.keys(vmDetails.config)
|
||||||
.filter((key) => key.match(/^net\d+$/))
|
.filter((key) => key.match(/^net\d+$/))
|
||||||
.map((netKey) => (
|
.map((netKey) => (
|
||||||
<div key={netKey}>
|
<div key={`net-${selectedVM.vmid}-${netKey}`}>
|
||||||
<div className="text-xs text-muted-foreground mb-1">
|
<div className="text-xs text-muted-foreground mb-1">
|
||||||
Network Interface {netKey.replace("net", "")}
|
Network Interface {netKey.replace("net", "")}
|
||||||
</div>
|
</div>
|
||||||
@@ -1644,7 +1641,7 @@ export function VirtualMachines() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* PCI Devices Section */}
|
{/* PCI Devices with proper keys */}
|
||||||
{Object.keys(vmDetails.config).some((key) => key.match(/^hostpci\d+$/)) && (
|
{Object.keys(vmDetails.config).some((key) => key.match(/^hostpci\d+$/)) && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
|
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
|
||||||
@@ -1654,7 +1651,7 @@ export function VirtualMachines() {
|
|||||||
{Object.keys(vmDetails.config)
|
{Object.keys(vmDetails.config)
|
||||||
.filter((key) => key.match(/^hostpci\d+$/))
|
.filter((key) => key.match(/^hostpci\d+$/))
|
||||||
.map((pciKey) => (
|
.map((pciKey) => (
|
||||||
<div key={pciKey}>
|
<div key={`pci-${selectedVM.vmid}-${pciKey}`}>
|
||||||
<div className="text-xs text-muted-foreground mb-1">
|
<div className="text-xs text-muted-foreground mb-1">
|
||||||
{pciKey.toUpperCase().replace(/(\d+)/, " $1")}
|
{pciKey.toUpperCase().replace(/(\d+)/, " $1")}
|
||||||
</div>
|
</div>
|
||||||
@@ -1667,7 +1664,7 @@ export function VirtualMachines() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* USB Devices Section */}
|
{/* USB Devices with proper keys */}
|
||||||
{Object.keys(vmDetails.config).some((key) => key.match(/^usb\d+$/)) && (
|
{Object.keys(vmDetails.config).some((key) => key.match(/^usb\d+$/)) && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
|
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
|
||||||
@@ -1677,7 +1674,7 @@ export function VirtualMachines() {
|
|||||||
{Object.keys(vmDetails.config)
|
{Object.keys(vmDetails.config)
|
||||||
.filter((key) => key.match(/^usb\d+$/))
|
.filter((key) => key.match(/^usb\d+$/))
|
||||||
.map((usbKey) => (
|
.map((usbKey) => (
|
||||||
<div key={usbKey}>
|
<div key={`usb-${selectedVM.vmid}-${usbKey}`}>
|
||||||
<div className="text-xs text-muted-foreground mb-1">
|
<div className="text-xs text-muted-foreground mb-1">
|
||||||
{usbKey.toUpperCase().replace(/(\d+)/, " $1")}
|
{usbKey.toUpperCase().replace(/(\d+)/, " $1")}
|
||||||
</div>
|
</div>
|
||||||
@@ -1690,7 +1687,7 @@ export function VirtualMachines() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Serial Devices Section */}
|
{/* Serial Ports with proper keys */}
|
||||||
{Object.keys(vmDetails.config).some((key) => key.match(/^serial\d+$/)) && (
|
{Object.keys(vmDetails.config).some((key) => key.match(/^serial\d+$/)) && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
|
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
|
||||||
@@ -1700,7 +1697,7 @@ export function VirtualMachines() {
|
|||||||
{Object.keys(vmDetails.config)
|
{Object.keys(vmDetails.config)
|
||||||
.filter((key) => key.match(/^serial\d+$/))
|
.filter((key) => key.match(/^serial\d+$/))
|
||||||
.map((serialKey) => (
|
.map((serialKey) => (
|
||||||
<div key={serialKey}>
|
<div key={`serial-${selectedVM.vmid}-${serialKey}`}>
|
||||||
<div className="text-xs text-muted-foreground mb-1">
|
<div className="text-xs text-muted-foreground mb-1">
|
||||||
{serialKey.toUpperCase().replace(/(\d+)/, " $1")}
|
{serialKey.toUpperCase().replace(/(\d+)/, " $1")}
|
||||||
</div>
|
</div>
|
||||||
@@ -1712,91 +1709,6 @@ export function VirtualMachines() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Options Section */}
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
|
|
||||||
Options
|
|
||||||
</h4>
|
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{vmDetails.config.onboot !== undefined && (
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">Start on Boot</div>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={
|
|
||||||
vmDetails.config.onboot
|
|
||||||
? "bg-green-500/10 text-green-500 border-green-500/20"
|
|
||||||
: "bg-red-500/10 text-red-500 border-red-500/20"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{vmDetails.config.onboot ? "Yes" : "No"}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{vmDetails.config.ostype && (
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">OS Type</div>
|
|
||||||
<div className="font-medium text-foreground">{vmDetails.config.ostype}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{vmDetails.config.arch && (
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">Architecture</div>
|
|
||||||
<div className="font-medium text-foreground">{vmDetails.config.arch}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{vmDetails.config.boot && (
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">Boot Order</div>
|
|
||||||
<div className="font-medium text-foreground">{vmDetails.config.boot}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{vmDetails.config.features && (
|
|
||||||
<div className="col-span-2 lg:grid-cols-3">
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">Features</div>
|
|
||||||
<div className="font-medium text-foreground text-sm">
|
|
||||||
{vmDetails.config.features}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Advanced Section */}
|
|
||||||
{(vmDetails.config.vmgenid || vmDetails.config.smbios1 || vmDetails.config.meta) && (
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
|
|
||||||
Advanced
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{vmDetails.config.vmgenid && (
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">VM Generation ID</div>
|
|
||||||
<div className="font-medium text-muted-foreground text-sm font-mono">
|
|
||||||
{vmDetails.config.vmgenid}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{vmDetails.config.smbios1 && (
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">SMBIOS</div>
|
|
||||||
<div className="font-medium text-muted-foreground text-sm font-mono break-all">
|
|
||||||
{vmDetails.config.smbios1}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{vmDetails.config.meta && (
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">Metadata</div>
|
|
||||||
<div className="font-medium text-muted-foreground text-sm font-mono">
|
|
||||||
{vmDetails.config.meta}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
export function useIsMobile() {
|
||||||
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMobile = () => {
|
||||||
|
setIsMobile(window.innerWidth < 768)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check on mount
|
||||||
|
checkMobile()
|
||||||
|
|
||||||
|
// Listen for resize
|
||||||
|
window.addEventListener("resize", checkMobile)
|
||||||
|
|
||||||
|
return () => window.removeEventListener("resize", checkMobile)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return isMobile
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* API Configuration for ProxMenux Monitor
|
||||||
|
* Handles API URL generation with automatic proxy detection
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Server Port Configuration
|
||||||
|
* Default: 8008 (production)
|
||||||
|
* Can be changed to 8009 for beta testing
|
||||||
|
* This can also be set via NEXT_PUBLIC_API_PORT environment variable
|
||||||
|
*/
|
||||||
|
export const API_PORT = process.env.NEXT_PUBLIC_API_PORT || "8008"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the base URL for API calls
|
||||||
|
* Automatically detects if running behind a proxy by checking if we're on a standard port
|
||||||
|
*
|
||||||
|
* @returns Base URL for API endpoints
|
||||||
|
*/
|
||||||
|
export function getApiBaseUrl(): string {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
console.log("[v0] getApiBaseUrl: Running on server (SSR)")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const { protocol, hostname, port } = window.location
|
||||||
|
|
||||||
|
console.log("[v0] getApiBaseUrl - protocol:", protocol, "hostname:", hostname, "port:", port)
|
||||||
|
|
||||||
|
// If accessing via standard ports (80/443) or no port, assume we're behind a proxy
|
||||||
|
// In this case, use relative URLs so the proxy handles routing
|
||||||
|
const isStandardPort = port === "" || port === "80" || port === "443"
|
||||||
|
|
||||||
|
console.log("[v0] getApiBaseUrl - isStandardPort:", isStandardPort)
|
||||||
|
|
||||||
|
if (isStandardPort) {
|
||||||
|
// Behind a proxy - use relative URL
|
||||||
|
console.log("[v0] getApiBaseUrl: Detected proxy access, using relative URLs")
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
// Direct access - use explicit API port
|
||||||
|
const baseUrl = `${protocol}//${hostname}:${API_PORT}`
|
||||||
|
console.log("[v0] getApiBaseUrl: Direct access detected, using:", baseUrl)
|
||||||
|
return baseUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a full API URL
|
||||||
|
*
|
||||||
|
* @param endpoint - API endpoint path (e.g., '/api/system')
|
||||||
|
* @returns Full API URL
|
||||||
|
*/
|
||||||
|
export function getApiUrl(endpoint: string): string {
|
||||||
|
const baseUrl = getApiBaseUrl()
|
||||||
|
|
||||||
|
// Ensure endpoint starts with /
|
||||||
|
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`
|
||||||
|
|
||||||
|
return `${baseUrl}${normalizedEndpoint}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the JWT token from localStorage
|
||||||
|
*
|
||||||
|
* @returns JWT token or null if not authenticated
|
||||||
|
*/
|
||||||
|
export function getAuthToken(): string | null {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const token = localStorage.getItem("proxmenux-auth-token")
|
||||||
|
console.log(
|
||||||
|
"[v0] getAuthToken called:",
|
||||||
|
token ? `Token found (length: ${token.length})` : "No token found in localStorage",
|
||||||
|
)
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches data from an API endpoint with error handling
|
||||||
|
*
|
||||||
|
* @param endpoint - API endpoint path
|
||||||
|
* @param options - Fetch options
|
||||||
|
* @returns Promise with the response data
|
||||||
|
*/
|
||||||
|
export async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
||||||
|
const url = getApiUrl(endpoint)
|
||||||
|
|
||||||
|
const token = getAuthToken()
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(options?.headers as Record<string, string>),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers["Authorization"] = `Bearer ${token}`
|
||||||
|
console.log("[v0] fetchApi:", endpoint, "- Authorization header ADDED")
|
||||||
|
} else {
|
||||||
|
console.log("[v0] fetchApi:", endpoint, "- NO TOKEN - Request will fail if endpoint is protected")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
cache: "no-store",
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("[v0] fetchApi:", endpoint, "- Response status:", response.status)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
console.error("[v0] fetchApi: 401 UNAUTHORIZED -", endpoint, "- Token present:", !!token)
|
||||||
|
throw new Error(`Unauthorized: ${endpoint}`)
|
||||||
|
}
|
||||||
|
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[v0] fetchApi error for", endpoint, ":", error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,3 +4,18 @@ import { twMerge } from "tailwind-merge"
|
|||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatStorage(sizeInGB: number): string {
|
||||||
|
if (sizeInGB < 1) {
|
||||||
|
// Less than 1 GB, show in MB
|
||||||
|
const mb = sizeInGB * 1024
|
||||||
|
return `${mb % 1 === 0 ? mb.toFixed(0) : mb.toFixed(1)} MB`
|
||||||
|
} else if (sizeInGB < 1024) {
|
||||||
|
// Less than 1024 GB, show in GB
|
||||||
|
return `${sizeInGB % 1 === 0 ? sizeInGB.toFixed(0) : sizeInGB.toFixed(1)} GB`
|
||||||
|
} else {
|
||||||
|
// 1024 GB or more, show in TB
|
||||||
|
const tb = sizeInGB / 1024
|
||||||
|
return `${tb % 1 === 0 ? tb.toFixed(0) : tb.toFixed(1)} TB`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "proxmenux-monitor",
|
"name": "ProxMenux-Monitor",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Proxmox System Monitoring Dashboard",
|
"description": "Proxmox System Monitoring Dashboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,504 @@
|
|||||||
|
"""
|
||||||
|
Authentication Manager Module
|
||||||
|
Handles all authentication-related operations including:
|
||||||
|
- Loading/saving auth configuration
|
||||||
|
- Password hashing and verification
|
||||||
|
- JWT token generation and validation
|
||||||
|
- Auth status checking
|
||||||
|
- Two-Factor Authentication (2FA/TOTP)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jwt
|
||||||
|
JWT_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
JWT_AVAILABLE = False
|
||||||
|
print("Warning: PyJWT not available. Authentication features will be limited.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyotp
|
||||||
|
import segno
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
TOTP_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
TOTP_AVAILABLE = False
|
||||||
|
print("Warning: pyotp/segno not available. 2FA features will be disabled.")
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
CONFIG_DIR = Path.home() / ".config" / "proxmenux-monitor"
|
||||||
|
AUTH_CONFIG_FILE = CONFIG_DIR / "auth.json"
|
||||||
|
JWT_SECRET = "proxmenux-monitor-secret-key-change-in-production"
|
||||||
|
JWT_ALGORITHM = "HS256"
|
||||||
|
TOKEN_EXPIRATION_HOURS = 24
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_config_dir():
|
||||||
|
"""Ensure the configuration directory exists"""
|
||||||
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def load_auth_config():
|
||||||
|
"""
|
||||||
|
Load authentication configuration from file
|
||||||
|
Returns dict with structure:
|
||||||
|
{
|
||||||
|
"enabled": bool,
|
||||||
|
"username": str,
|
||||||
|
"password_hash": str,
|
||||||
|
"declined": bool,
|
||||||
|
"configured": bool,
|
||||||
|
"totp_enabled": bool, # 2FA enabled flag
|
||||||
|
"totp_secret": str, # TOTP secret key
|
||||||
|
"backup_codes": list # List of backup codes
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if not AUTH_CONFIG_FILE.exists():
|
||||||
|
return {
|
||||||
|
"enabled": False,
|
||||||
|
"username": None,
|
||||||
|
"password_hash": None,
|
||||||
|
"declined": False,
|
||||||
|
"configured": False,
|
||||||
|
"totp_enabled": False,
|
||||||
|
"totp_secret": None,
|
||||||
|
"backup_codes": []
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(AUTH_CONFIG_FILE, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
# Ensure all required fields exist
|
||||||
|
config.setdefault("declined", False)
|
||||||
|
config.setdefault("configured", config.get("enabled", False) or config.get("declined", False))
|
||||||
|
config.setdefault("totp_enabled", False)
|
||||||
|
config.setdefault("totp_secret", None)
|
||||||
|
config.setdefault("backup_codes", [])
|
||||||
|
return config
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading auth config: {e}")
|
||||||
|
return {
|
||||||
|
"enabled": False,
|
||||||
|
"username": None,
|
||||||
|
"password_hash": None,
|
||||||
|
"declined": False,
|
||||||
|
"configured": False,
|
||||||
|
"totp_enabled": False,
|
||||||
|
"totp_secret": None,
|
||||||
|
"backup_codes": []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def save_auth_config(config):
|
||||||
|
"""Save authentication configuration to file"""
|
||||||
|
ensure_config_dir()
|
||||||
|
try:
|
||||||
|
with open(AUTH_CONFIG_FILE, 'w') as f:
|
||||||
|
json.dump(config, f, indent=2)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving auth config: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password):
|
||||||
|
"""Hash a password using SHA-256"""
|
||||||
|
return hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(password, password_hash):
|
||||||
|
"""Verify a password against its hash"""
|
||||||
|
return hash_password(password) == password_hash
|
||||||
|
|
||||||
|
|
||||||
|
def generate_token(username):
|
||||||
|
"""Generate a JWT token for the given username"""
|
||||||
|
if not JWT_AVAILABLE:
|
||||||
|
return None
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'username': username,
|
||||||
|
'exp': datetime.utcnow() + timedelta(hours=TOKEN_EXPIRATION_HOURS),
|
||||||
|
'iat': datetime.utcnow()
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||||
|
return token
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating token: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def verify_token(token):
|
||||||
|
"""
|
||||||
|
Verify a JWT token
|
||||||
|
Returns username if valid, None otherwise
|
||||||
|
"""
|
||||||
|
if not JWT_AVAILABLE or not token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
||||||
|
return payload.get('username')
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
print("Token has expired")
|
||||||
|
return None
|
||||||
|
except jwt.InvalidTokenError as e:
|
||||||
|
print(f"Invalid token: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_status():
|
||||||
|
"""
|
||||||
|
Get current authentication status
|
||||||
|
Returns dict with:
|
||||||
|
{
|
||||||
|
"auth_enabled": bool,
|
||||||
|
"auth_configured": bool,
|
||||||
|
"declined": bool,
|
||||||
|
"username": str or None,
|
||||||
|
"authenticated": bool,
|
||||||
|
"totp_enabled": bool # 2FA status
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
return {
|
||||||
|
"auth_enabled": config.get("enabled", False),
|
||||||
|
"auth_configured": config.get("configured", False),
|
||||||
|
"declined": config.get("declined", False),
|
||||||
|
"username": config.get("username") if config.get("enabled") else None,
|
||||||
|
"authenticated": False,
|
||||||
|
"totp_enabled": config.get("totp_enabled", False) # Include 2FA status
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def setup_auth(username, password):
|
||||||
|
"""
|
||||||
|
Set up authentication with username and password
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
if not username or not password:
|
||||||
|
return False, "Username and password are required"
|
||||||
|
|
||||||
|
if len(password) < 6:
|
||||||
|
return False, "Password must be at least 6 characters"
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"enabled": True,
|
||||||
|
"username": username,
|
||||||
|
"password_hash": hash_password(password),
|
||||||
|
"declined": False,
|
||||||
|
"configured": True,
|
||||||
|
"totp_enabled": False,
|
||||||
|
"totp_secret": None,
|
||||||
|
"backup_codes": []
|
||||||
|
}
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, "Authentication configured successfully"
|
||||||
|
else:
|
||||||
|
return False, "Failed to save authentication configuration"
|
||||||
|
|
||||||
|
|
||||||
|
def decline_auth():
|
||||||
|
"""
|
||||||
|
Mark authentication as declined by user
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
config["enabled"] = False
|
||||||
|
config["declined"] = True
|
||||||
|
config["configured"] = True
|
||||||
|
config["username"] = None
|
||||||
|
config["password_hash"] = None
|
||||||
|
config["totp_enabled"] = False
|
||||||
|
config["totp_secret"] = None
|
||||||
|
config["backup_codes"] = []
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, "Authentication declined"
|
||||||
|
else:
|
||||||
|
return False, "Failed to save configuration"
|
||||||
|
|
||||||
|
|
||||||
|
def disable_auth():
|
||||||
|
"""
|
||||||
|
Disable authentication (different from decline - can be re-enabled)
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
config["enabled"] = False
|
||||||
|
config["username"] = None
|
||||||
|
config["password_hash"] = None
|
||||||
|
config["declined"] = False
|
||||||
|
config["configured"] = False
|
||||||
|
config["totp_enabled"] = False
|
||||||
|
config["totp_secret"] = None
|
||||||
|
config["backup_codes"] = []
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, "Authentication disabled"
|
||||||
|
else:
|
||||||
|
return False, "Failed to save configuration"
|
||||||
|
|
||||||
|
|
||||||
|
def enable_auth():
|
||||||
|
"""
|
||||||
|
Enable authentication (must already be configured)
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
|
||||||
|
if not config.get("username") or not config.get("password_hash"):
|
||||||
|
return False, "Authentication not configured. Please set up username and password first."
|
||||||
|
|
||||||
|
config["enabled"] = True
|
||||||
|
config["declined"] = False
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, "Authentication enabled"
|
||||||
|
else:
|
||||||
|
return False, "Failed to save configuration"
|
||||||
|
|
||||||
|
|
||||||
|
def change_password(old_password, new_password):
|
||||||
|
"""
|
||||||
|
Change the authentication password
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
|
||||||
|
if not config.get("enabled"):
|
||||||
|
return False, "Authentication is not enabled"
|
||||||
|
|
||||||
|
if not verify_password(old_password, config.get("password_hash", "")):
|
||||||
|
return False, "Current password is incorrect"
|
||||||
|
|
||||||
|
if len(new_password) < 6:
|
||||||
|
return False, "New password must be at least 6 characters"
|
||||||
|
|
||||||
|
config["password_hash"] = hash_password(new_password)
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, "Password changed successfully"
|
||||||
|
else:
|
||||||
|
return False, "Failed to save new password"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_totp_secret():
|
||||||
|
"""Generate a new TOTP secret key"""
|
||||||
|
if not TOTP_AVAILABLE:
|
||||||
|
return None
|
||||||
|
return pyotp.random_base32()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_totp_qr(username, secret):
|
||||||
|
"""
|
||||||
|
Generate a QR code for TOTP setup
|
||||||
|
Returns base64 encoded SVG image
|
||||||
|
"""
|
||||||
|
if not TOTP_AVAILABLE:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create TOTP URI
|
||||||
|
totp = pyotp.TOTP(secret)
|
||||||
|
uri = totp.provisioning_uri(
|
||||||
|
name=username,
|
||||||
|
issuer_name="ProxMenux Monitor"
|
||||||
|
)
|
||||||
|
|
||||||
|
qr = segno.make(uri)
|
||||||
|
|
||||||
|
# Convert to SVG string
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
qr.save(buffer, kind='svg', scale=4, border=2)
|
||||||
|
svg_bytes = buffer.getvalue()
|
||||||
|
svg_content = svg_bytes.decode('utf-8')
|
||||||
|
|
||||||
|
# Return as data URL
|
||||||
|
svg_base64 = base64.b64encode(svg_content.encode()).decode('utf-8')
|
||||||
|
return f"data:image/svg+xml;base64,{svg_base64}"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating QR code: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def generate_backup_codes(count=8):
|
||||||
|
"""Generate backup codes for 2FA recovery"""
|
||||||
|
codes = []
|
||||||
|
for _ in range(count):
|
||||||
|
# Generate 8-character alphanumeric code
|
||||||
|
code = ''.join(secrets.choice('ABCDEFGHJKLMNPQRSTUVWXYZ23456789') for _ in range(8))
|
||||||
|
# Format as XXXX-XXXX for readability
|
||||||
|
formatted = f"{code[:4]}-{code[4:]}"
|
||||||
|
codes.append({
|
||||||
|
"code": hashlib.sha256(formatted.encode()).hexdigest(),
|
||||||
|
"used": False
|
||||||
|
})
|
||||||
|
return codes
|
||||||
|
|
||||||
|
|
||||||
|
def setup_totp(username):
|
||||||
|
"""
|
||||||
|
Set up TOTP for a user
|
||||||
|
Returns (success: bool, secret: str, qr_code: str, backup_codes: list, message: str)
|
||||||
|
"""
|
||||||
|
if not TOTP_AVAILABLE:
|
||||||
|
return False, None, None, None, "2FA is not available (pyotp/segno not installed)"
|
||||||
|
|
||||||
|
config = load_auth_config()
|
||||||
|
|
||||||
|
if not config.get("enabled"):
|
||||||
|
return False, None, None, None, "Authentication must be enabled first"
|
||||||
|
|
||||||
|
if config.get("username") != username:
|
||||||
|
return False, None, None, None, "Invalid username"
|
||||||
|
|
||||||
|
# Generate new secret and backup codes
|
||||||
|
secret = generate_totp_secret()
|
||||||
|
qr_code = generate_totp_qr(username, secret)
|
||||||
|
backup_codes_plain = []
|
||||||
|
backup_codes_hashed = generate_backup_codes()
|
||||||
|
|
||||||
|
# Generate plain text backup codes for display (only returned once)
|
||||||
|
for i in range(8):
|
||||||
|
code = ''.join(secrets.choice('ABCDEFGHJKLMNPQRSTUVWXYZ23456789') for _ in range(8))
|
||||||
|
formatted = f"{code[:4]}-{code[4:]}"
|
||||||
|
backup_codes_plain.append(formatted)
|
||||||
|
backup_codes_hashed[i]["code"] = hashlib.sha256(formatted.encode()).hexdigest()
|
||||||
|
|
||||||
|
# Store secret and hashed backup codes (not enabled yet until verified)
|
||||||
|
config["totp_secret"] = secret
|
||||||
|
config["backup_codes"] = backup_codes_hashed
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, secret, qr_code, backup_codes_plain, "2FA setup initiated"
|
||||||
|
else:
|
||||||
|
return False, None, None, None, "Failed to save 2FA configuration"
|
||||||
|
|
||||||
|
|
||||||
|
def verify_totp(username, token, use_backup=False):
|
||||||
|
"""
|
||||||
|
Verify a TOTP token or backup code
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
if not TOTP_AVAILABLE and not use_backup:
|
||||||
|
return False, "2FA is not available"
|
||||||
|
|
||||||
|
config = load_auth_config()
|
||||||
|
|
||||||
|
if not config.get("totp_enabled"):
|
||||||
|
return False, "2FA is not enabled"
|
||||||
|
|
||||||
|
if config.get("username") != username:
|
||||||
|
return False, "Invalid username"
|
||||||
|
|
||||||
|
# Check backup code
|
||||||
|
if use_backup:
|
||||||
|
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||||
|
for backup_code in config.get("backup_codes", []):
|
||||||
|
if backup_code["code"] == token_hash and not backup_code["used"]:
|
||||||
|
backup_code["used"] = True
|
||||||
|
save_auth_config(config)
|
||||||
|
return True, "Backup code accepted"
|
||||||
|
return False, "Invalid or already used backup code"
|
||||||
|
|
||||||
|
# Check TOTP token
|
||||||
|
totp = pyotp.TOTP(config.get("totp_secret"))
|
||||||
|
if totp.verify(token, valid_window=1): # Allow 1 time step tolerance
|
||||||
|
return True, "2FA verification successful"
|
||||||
|
else:
|
||||||
|
return False, "Invalid 2FA code"
|
||||||
|
|
||||||
|
|
||||||
|
def enable_totp(username, verification_token):
|
||||||
|
"""
|
||||||
|
Enable TOTP after successful verification
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
if not TOTP_AVAILABLE:
|
||||||
|
return False, "2FA is not available"
|
||||||
|
|
||||||
|
config = load_auth_config()
|
||||||
|
|
||||||
|
if not config.get("totp_secret"):
|
||||||
|
return False, "2FA has not been set up. Please set up 2FA first."
|
||||||
|
|
||||||
|
if config.get("username") != username:
|
||||||
|
return False, "Invalid username"
|
||||||
|
|
||||||
|
# Verify the token before enabling
|
||||||
|
totp = pyotp.TOTP(config.get("totp_secret"))
|
||||||
|
if not totp.verify(verification_token, valid_window=1):
|
||||||
|
return False, "Invalid verification code. Please try again."
|
||||||
|
|
||||||
|
config["totp_enabled"] = True
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, "2FA enabled successfully"
|
||||||
|
else:
|
||||||
|
return False, "Failed to enable 2FA"
|
||||||
|
|
||||||
|
|
||||||
|
def disable_totp(username, password):
|
||||||
|
"""
|
||||||
|
Disable TOTP (requires password confirmation)
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
|
||||||
|
if config.get("username") != username:
|
||||||
|
return False, "Invalid username"
|
||||||
|
|
||||||
|
if not verify_password(password, config.get("password_hash", "")):
|
||||||
|
return False, "Invalid password"
|
||||||
|
|
||||||
|
config["totp_enabled"] = False
|
||||||
|
config["totp_secret"] = None
|
||||||
|
config["backup_codes"] = []
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, "2FA disabled successfully"
|
||||||
|
else:
|
||||||
|
return False, "Failed to disable 2FA"
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate(username, password, totp_token=None):
|
||||||
|
"""
|
||||||
|
Authenticate a user with username, password, and optional TOTP
|
||||||
|
Returns (success: bool, token: str or None, requires_totp: bool, message: str)
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
|
||||||
|
if not config.get("enabled"):
|
||||||
|
return False, None, False, "Authentication is not enabled"
|
||||||
|
|
||||||
|
if username != config.get("username"):
|
||||||
|
return False, None, False, "Invalid username or password"
|
||||||
|
|
||||||
|
if not verify_password(password, config.get("password_hash", "")):
|
||||||
|
return False, None, False, "Invalid username or password"
|
||||||
|
|
||||||
|
if config.get("totp_enabled"):
|
||||||
|
if not totp_token:
|
||||||
|
return False, None, True, "2FA code required"
|
||||||
|
|
||||||
|
# Verify TOTP token or backup code
|
||||||
|
success, message = verify_totp(username, totp_token, use_backup=len(totp_token) == 9) # Backup codes are formatted XXXX-XXXX
|
||||||
|
if not success:
|
||||||
|
return False, None, True, message
|
||||||
|
|
||||||
|
token = generate_token(username)
|
||||||
|
if token:
|
||||||
|
return True, token, False, "Authentication successful"
|
||||||
|
else:
|
||||||
|
return False, None, False, "Failed to generate authentication token"
|
||||||
@@ -78,6 +78,13 @@ cd "$SCRIPT_DIR"
|
|||||||
# Copy Flask server
|
# Copy Flask server
|
||||||
echo "📋 Copying Flask server..."
|
echo "📋 Copying Flask server..."
|
||||||
cp "$SCRIPT_DIR/flask_server.py" "$APP_DIR/usr/bin/"
|
cp "$SCRIPT_DIR/flask_server.py" "$APP_DIR/usr/bin/"
|
||||||
|
cp "$SCRIPT_DIR/flask_auth_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_auth_routes.py not found"
|
||||||
|
cp "$SCRIPT_DIR/auth_manager.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ auth_manager.py not found"
|
||||||
|
cp "$SCRIPT_DIR/jwt_middleware.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ jwt_middleware.py not found"
|
||||||
|
cp "$SCRIPT_DIR/health_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ health_monitor.py not found"
|
||||||
|
cp "$SCRIPT_DIR/health_persistence.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ health_persistence.py not found"
|
||||||
|
cp "$SCRIPT_DIR/flask_health_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_health_routes.py not found"
|
||||||
|
cp "$SCRIPT_DIR/flask_proxmenux_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_proxmenux_routes.py not found"
|
||||||
|
|
||||||
echo "📋 Adding translation support..."
|
echo "📋 Adding translation support..."
|
||||||
cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF'
|
cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF'
|
||||||
@@ -279,6 +286,9 @@ pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" \
|
|||||||
flask-cors \
|
flask-cors \
|
||||||
psutil \
|
psutil \
|
||||||
requests \
|
requests \
|
||||||
|
PyJWT \
|
||||||
|
pyotp \
|
||||||
|
segno \
|
||||||
googletrans==4.0.0-rc1 \
|
googletrans==4.0.0-rc1 \
|
||||||
httpx==0.13.3 \
|
httpx==0.13.3 \
|
||||||
httpcore==0.9.1 \
|
httpcore==0.9.1 \
|
||||||
@@ -321,10 +331,6 @@ echo "🔧 Installing hardware monitoring tools..."
|
|||||||
mkdir -p "$WORK_DIR/debs"
|
mkdir -p "$WORK_DIR/debs"
|
||||||
cd "$WORK_DIR/debs"
|
cd "$WORK_DIR/debs"
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================
|
|
||||||
|
|
||||||
|
|
||||||
echo "📥 Downloading hardware monitoring tools (dynamic via APT)..."
|
echo "📥 Downloading hardware monitoring tools (dynamic via APT)..."
|
||||||
|
|
||||||
dl_pkg() {
|
dl_pkg() {
|
||||||
@@ -361,21 +367,12 @@ dl_pkg() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdir -p "$WORK_DIR/debs"
|
|
||||||
cd "$WORK_DIR/debs"
|
|
||||||
|
|
||||||
|
|
||||||
dl_pkg "ipmitool.deb" "ipmitool" || true
|
dl_pkg "ipmitool.deb" "ipmitool" || true
|
||||||
dl_pkg "libfreeipmi17.deb" "libfreeipmi17" || true
|
dl_pkg "libfreeipmi17.deb" "libfreeipmi17" || true
|
||||||
dl_pkg "lm-sensors.deb" "lm-sensors" || true
|
dl_pkg "lm-sensors.deb" "lm-sensors" || true
|
||||||
dl_pkg "nut-client.deb" "nut-client" || true
|
dl_pkg "nut-client.deb" "nut-client" || true
|
||||||
dl_pkg "libupsclient.deb" "libupsclient6" "libupsclient5" "libupsclient4" || 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..."
|
echo "📦 Extracting .deb packages into AppDir..."
|
||||||
extracted_count=0
|
extracted_count=0
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
@@ -395,7 +392,6 @@ else
|
|||||||
echo "✅ Extracted $extracted_count package(s)"
|
echo "✅ Extracted $extracted_count package(s)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
if [ -d "$APP_DIR/bin" ]; then
|
if [ -d "$APP_DIR/bin" ]; then
|
||||||
echo "📋 Normalizing /bin -> /usr/bin"
|
echo "📋 Normalizing /bin -> /usr/bin"
|
||||||
mkdir -p "$APP_DIR/usr/bin"
|
mkdir -p "$APP_DIR/usr/bin"
|
||||||
@@ -403,24 +399,20 @@ if [ -d "$APP_DIR/bin" ]; then
|
|||||||
rm -rf "$APP_DIR/bin"
|
rm -rf "$APP_DIR/bin"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
echo "🔍 Sanity check (ldd + presence of libfreeipmi)"
|
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"
|
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
|
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)"
|
echo "❌ libfreeipmi.so.17 not found inside AppDir (ipmitool will fail)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
if [ -x "$APP_DIR/usr/bin/ipmitool" ] && ldd "$APP_DIR/usr/bin/ipmitool" | grep -q 'not found'; then
|
if [ -x "$APP_DIR/usr/bin/ipmitool" ] && ldd "$APP_DIR/usr/bin/ipmitool" | grep -q 'not found'; then
|
||||||
echo "❌ ipmitool has unresolved libs:"
|
echo "❌ ipmitool has unresolved libs:"
|
||||||
ldd "$APP_DIR/usr/bin/ipmitool" | grep 'not found' || true
|
ldd "$APP_DIR/usr/bin/ipmitool" | grep 'not found' || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
if [ -x "$APP_DIR/usr/bin/upsc" ] && ldd "$APP_DIR/usr/bin/upsc" | grep -q 'not found'; then
|
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..."
|
echo "⚠️ upsc has unresolved libs, trying to auto-fix..."
|
||||||
missing="$(ldd "$APP_DIR/usr/bin/upsc" | awk '/not found/{print $1}' | tr -d ' ')"
|
missing="$(ldd "$APP_DIR/usr/bin/upsc" | awk '/not found/{print $1}' | tr -d ' ')"
|
||||||
@@ -463,12 +455,6 @@ echo "✅ Sanity check OK (ipmitool/upsc ready; libfreeipmi present)"
|
|||||||
[ -x "$APP_DIR/usr/bin/intel_gpu_top" ] && echo " • intel-gpu-tools: OK" || echo " • intel-gpu-tools: missing"
|
[ -x "$APP_DIR/usr/bin/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"
|
[ -x "$APP_DIR/usr/bin/radeontop" ] && echo " • radeontop: OK" || echo " • radeontop: missing"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build AppImage
|
# Build AppImage
|
||||||
echo "🔨 Building unified AppImage v${VERSION}..."
|
echo "🔨 Building unified AppImage v${VERSION}..."
|
||||||
cd "$WORK_DIR"
|
cd "$WORK_DIR"
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
"""
|
||||||
|
Flask Authentication Routes
|
||||||
|
Provides REST API endpoints for authentication management
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
import auth_manager
|
||||||
|
import jwt
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
auth_bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/status', methods=['GET'])
|
||||||
|
def auth_status():
|
||||||
|
"""Get current authentication status"""
|
||||||
|
try:
|
||||||
|
status = auth_manager.get_auth_status()
|
||||||
|
|
||||||
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||||
|
if token:
|
||||||
|
username = auth_manager.verify_token(token)
|
||||||
|
if username:
|
||||||
|
status['authenticated'] = True
|
||||||
|
|
||||||
|
return jsonify(status)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/setup', methods=['POST'])
|
||||||
|
def auth_setup():
|
||||||
|
"""Set up authentication with username and password"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
|
||||||
|
success, message = auth_manager.setup_auth(username, password)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "message": message})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/decline', methods=['POST'])
|
||||||
|
def auth_decline():
|
||||||
|
"""Decline authentication setup"""
|
||||||
|
try:
|
||||||
|
success, message = auth_manager.decline_auth()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "message": message})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/login', methods=['POST'])
|
||||||
|
def auth_login():
|
||||||
|
"""Authenticate user and return JWT token"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
totp_token = data.get('totp_token') # Optional 2FA token
|
||||||
|
|
||||||
|
success, token, requires_totp, message = auth_manager.authenticate(username, password, totp_token)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "token": token, "message": message})
|
||||||
|
elif requires_totp:
|
||||||
|
return jsonify({"success": False, "requires_totp": True, "message": message}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 401
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/enable', methods=['POST'])
|
||||||
|
def auth_enable():
|
||||||
|
"""Enable authentication"""
|
||||||
|
try:
|
||||||
|
success, message = auth_manager.enable_auth()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "message": message})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/disable', methods=['POST'])
|
||||||
|
def auth_disable():
|
||||||
|
"""Disable authentication"""
|
||||||
|
try:
|
||||||
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||||
|
if not token or not auth_manager.verify_token(token):
|
||||||
|
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
||||||
|
|
||||||
|
success, message = auth_manager.disable_auth()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "message": message})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/change-password', methods=['POST'])
|
||||||
|
def auth_change_password():
|
||||||
|
"""Change authentication password"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
old_password = data.get('old_password')
|
||||||
|
new_password = data.get('new_password')
|
||||||
|
|
||||||
|
success, message = auth_manager.change_password(old_password, new_password)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "message": message})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/skip', methods=['POST'])
|
||||||
|
def auth_skip():
|
||||||
|
"""Skip authentication setup (same as decline)"""
|
||||||
|
try:
|
||||||
|
success, message = auth_manager.decline_auth()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# Return success with clear indication that APIs should be accessible
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": message,
|
||||||
|
"auth_declined": True # Add explicit flag for frontend
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/totp/setup', methods=['POST'])
|
||||||
|
def totp_setup():
|
||||||
|
"""Initialize TOTP setup for a user"""
|
||||||
|
try:
|
||||||
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||||
|
username = auth_manager.verify_token(token)
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
||||||
|
|
||||||
|
success, secret, qr_code, backup_codes, message = auth_manager.setup_totp(username)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"secret": secret,
|
||||||
|
"qr_code": qr_code,
|
||||||
|
"backup_codes": backup_codes,
|
||||||
|
"message": message
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/totp/enable', methods=['POST'])
|
||||||
|
def totp_enable():
|
||||||
|
"""Enable TOTP after verification"""
|
||||||
|
try:
|
||||||
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||||
|
username = auth_manager.verify_token(token)
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
||||||
|
|
||||||
|
data = request.json
|
||||||
|
verification_token = data.get('token')
|
||||||
|
|
||||||
|
if not verification_token:
|
||||||
|
return jsonify({"success": False, "message": "Verification token required"}), 400
|
||||||
|
|
||||||
|
success, message = auth_manager.enable_totp(username, verification_token)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "message": message})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/totp/disable', methods=['POST'])
|
||||||
|
def totp_disable():
|
||||||
|
"""Disable TOTP (requires password confirmation)"""
|
||||||
|
try:
|
||||||
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||||
|
username = auth_manager.verify_token(token)
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
||||||
|
|
||||||
|
data = request.json
|
||||||
|
password = data.get('password')
|
||||||
|
|
||||||
|
if not password:
|
||||||
|
return jsonify({"success": False, "message": "Password required"}), 400
|
||||||
|
|
||||||
|
success, message = auth_manager.disable_totp(username, password)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "message": message})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/generate-api-token', methods=['POST'])
|
||||||
|
def generate_api_token():
|
||||||
|
"""Generate a long-lived API token for external integrations (Homepage, Home Assistant, etc.)"""
|
||||||
|
try:
|
||||||
|
auth_header = request.headers.get('Authorization', '')
|
||||||
|
token = auth_header.replace('Bearer ', '')
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
return jsonify({"success": False, "message": "Unauthorized. Please log in first."}), 401
|
||||||
|
|
||||||
|
username = auth_manager.verify_token(token)
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return jsonify({"success": False, "message": "Invalid or expired session. Please log in again."}), 401
|
||||||
|
|
||||||
|
data = request.json
|
||||||
|
password = data.get('password')
|
||||||
|
totp_token = data.get('totp_token') # Optional 2FA token
|
||||||
|
token_name = data.get('token_name', 'API Token') # Optional token description
|
||||||
|
|
||||||
|
if not password:
|
||||||
|
return jsonify({"success": False, "message": "Password is required"}), 400
|
||||||
|
|
||||||
|
# Authenticate user with password and optional 2FA
|
||||||
|
success, _, requires_totp, message = auth_manager.authenticate(username, password, totp_token)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# Generate a long-lived token (1 year expiration)
|
||||||
|
api_token = jwt.encode({
|
||||||
|
'username': username,
|
||||||
|
'token_name': token_name,
|
||||||
|
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=365),
|
||||||
|
'iat': datetime.datetime.utcnow()
|
||||||
|
}, auth_manager.JWT_SECRET, algorithm='HS256')
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"token": api_token,
|
||||||
|
"token_name": token_name,
|
||||||
|
"expires_in": "365 days",
|
||||||
|
"message": "API token generated successfully. Store this token securely, it will not be shown again."
|
||||||
|
})
|
||||||
|
elif requires_totp:
|
||||||
|
return jsonify({"success": False, "requires_totp": True, "message": message}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 401
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] generate_api_token: {str(e)}") # Log error for debugging
|
||||||
|
return jsonify({"success": False, "message": f"Internal error: {str(e)}"}), 500
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"""
|
||||||
|
Flask routes for health monitoring with persistence support
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
from health_monitor import health_monitor
|
||||||
|
from health_persistence import health_persistence
|
||||||
|
|
||||||
|
health_bp = Blueprint('health', __name__)
|
||||||
|
|
||||||
|
@health_bp.route('/api/health/status', methods=['GET'])
|
||||||
|
def get_health_status():
|
||||||
|
"""Get overall health status summary"""
|
||||||
|
try:
|
||||||
|
status = health_monitor.get_overall_status()
|
||||||
|
return jsonify(status)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@health_bp.route('/api/health/details', methods=['GET'])
|
||||||
|
def get_health_details():
|
||||||
|
"""Get detailed health status with all checks"""
|
||||||
|
try:
|
||||||
|
details = health_monitor.get_detailed_status()
|
||||||
|
return jsonify(details)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@health_bp.route('/api/system-info', methods=['GET'])
|
||||||
|
def get_system_info():
|
||||||
|
"""
|
||||||
|
Get lightweight system info for header display.
|
||||||
|
Returns: hostname, uptime, and health status with proper structure.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
info = health_monitor.get_system_info()
|
||||||
|
|
||||||
|
if 'health' in info:
|
||||||
|
status_map = {
|
||||||
|
'OK': 'healthy',
|
||||||
|
'WARNING': 'warning',
|
||||||
|
'CRITICAL': 'critical',
|
||||||
|
'UNKNOWN': 'warning'
|
||||||
|
}
|
||||||
|
current_status = info['health'].get('status', 'OK').upper()
|
||||||
|
info['health']['status'] = status_map.get(current_status, 'healthy')
|
||||||
|
|
||||||
|
return jsonify(info)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@health_bp.route('/api/health/acknowledge', methods=['POST'])
|
||||||
|
def acknowledge_error():
|
||||||
|
"""Acknowledge an error manually (user dismissed it)"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
if not data or 'error_key' not in data:
|
||||||
|
return jsonify({'error': 'error_key is required'}), 400
|
||||||
|
|
||||||
|
error_key = data['error_key']
|
||||||
|
health_persistence.acknowledge_error(error_key)
|
||||||
|
return jsonify({'success': True, 'message': 'Error acknowledged'})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@health_bp.route('/api/health/active-errors', methods=['GET'])
|
||||||
|
def get_active_errors():
|
||||||
|
"""Get all active persistent errors"""
|
||||||
|
try:
|
||||||
|
category = request.args.get('category')
|
||||||
|
errors = health_persistence.get_active_errors(category)
|
||||||
|
return jsonify({'errors': errors})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
proxmenux_bp = Blueprint('proxmenux', __name__)
|
||||||
|
|
||||||
|
# Tool descriptions mapping
|
||||||
|
TOOL_DESCRIPTIONS = {
|
||||||
|
'lvm_repair': 'LVM PV Headers Repair',
|
||||||
|
'repo_cleanup': 'Repository Cleanup',
|
||||||
|
'subscription_banner': 'Subscription Banner Removal',
|
||||||
|
'time_sync': 'Time Synchronization',
|
||||||
|
'apt_languages': 'APT Language Skip',
|
||||||
|
'journald': 'Journald Optimization',
|
||||||
|
'logrotate': 'Logrotate Optimization',
|
||||||
|
'system_limits': 'System Limits Increase',
|
||||||
|
'entropy': 'Entropy Generation (haveged)',
|
||||||
|
'memory_settings': 'Memory Settings Optimization',
|
||||||
|
'kernel_panic': 'Kernel Panic Configuration',
|
||||||
|
'apt_ipv4': 'APT IPv4 Force',
|
||||||
|
'kexec': 'kexec for quick reboots',
|
||||||
|
'network_optimization': 'Network Optimizations',
|
||||||
|
'bashrc_custom': 'Bashrc Customization',
|
||||||
|
'figurine': 'Figurine',
|
||||||
|
'fastfetch': 'Fastfetch',
|
||||||
|
'log2ram': 'Log2ram (SSD Protection)',
|
||||||
|
'amd_fixes': 'AMD CPU (Ryzen/EPYC) fixes',
|
||||||
|
'persistent_network': 'Setting persistent network interfaces'
|
||||||
|
}
|
||||||
|
|
||||||
|
@proxmenux_bp.route('/api/proxmenux/installed-tools', methods=['GET'])
|
||||||
|
def get_installed_tools():
|
||||||
|
"""Get list of installed ProxMenux tools/optimizations"""
|
||||||
|
installed_tools_path = '/usr/local/share/proxmenux/installed_tools.json'
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not os.path.exists(installed_tools_path):
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'installed_tools': [],
|
||||||
|
'message': 'No ProxMenux optimizations installed yet'
|
||||||
|
})
|
||||||
|
|
||||||
|
with open(installed_tools_path, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# Convert to list format with descriptions
|
||||||
|
tools = []
|
||||||
|
for tool_key, enabled in data.items():
|
||||||
|
if enabled: # Only include enabled tools
|
||||||
|
tools.append({
|
||||||
|
'key': tool_key,
|
||||||
|
'name': TOOL_DESCRIPTIONS.get(tool_key, tool_key.replace('_', ' ').title()),
|
||||||
|
'enabled': enabled
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort alphabetically by name
|
||||||
|
tools.sort(key=lambda x: x['name'])
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'installed_tools': tools,
|
||||||
|
'total_count': len(tools)
|
||||||
|
})
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Invalid JSON format in installed_tools.json'
|
||||||
|
}), 500
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
@@ -12,6 +12,7 @@ import psutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import socket
|
import socket
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@@ -22,10 +23,29 @@ import xml.etree.ElementTree as ET # Added for XML parsing
|
|||||||
import math # Imported math for format_bytes function
|
import math # Imported math for format_bytes function
|
||||||
import urllib.parse # Added for URL encoding
|
import urllib.parse # Added for URL encoding
|
||||||
import platform # Added for platform.release()
|
import platform # Added for platform.release()
|
||||||
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
import jwt
|
||||||
|
from functools import wraps
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from flask_health_routes import health_bp
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from flask_auth_routes import auth_bp
|
||||||
|
from flask_proxmenux_routes import proxmenux_bp
|
||||||
|
from jwt_middleware import require_auth
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app) # Enable CORS for Next.js frontend
|
CORS(app) # Enable CORS for Next.js frontend
|
||||||
|
|
||||||
|
app.register_blueprint(auth_bp)
|
||||||
|
app.register_blueprint(health_bp)
|
||||||
|
app.register_blueprint(proxmenux_bp)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def identify_gpu_type(name, vendor=None, bus=None, driver=None):
|
def identify_gpu_type(name, vendor=None, bus=None, driver=None):
|
||||||
"""
|
"""
|
||||||
Returns: 'Integrated' or 'PCI' (discrete)
|
Returns: 'Integrated' or 'PCI' (discrete)
|
||||||
@@ -395,18 +415,18 @@ def get_intel_gpu_processes_from_text():
|
|||||||
processes.append(process_info)
|
processes.append(process_info)
|
||||||
|
|
||||||
except (ValueError, IndexError) as e:
|
except (ValueError, IndexError) as e:
|
||||||
# print(f"[v0] Error parsing process line: {e}", flush=True)
|
# print(f"[v0] Error parsing process line: {e}")
|
||||||
pass
|
pass
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
|
|
||||||
if not header_found:
|
if not header_found:
|
||||||
# print(f"[v0] No process table found in intel_gpu_top output", flush=True)
|
# print(f"[v0] No process table found in intel_gpu_top output")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return processes
|
return processes
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# print(f"[v0] Error getting processes from intel_gpu_top text: {e}", flush=True)
|
# print(f"[v0] Error getting processes from intel_gpu_top text: {e}")
|
||||||
pass
|
pass
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -917,6 +937,210 @@ def get_disk_hardware_info(disk_name):
|
|||||||
"""Placeholder for disk hardware info - to be populated by lsblk later."""
|
"""Placeholder for disk hardware info - to be populated by lsblk later."""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
def get_pcie_link_speed(disk_name):
|
||||||
|
"""Get PCIe link speed information for NVMe drives"""
|
||||||
|
pcie_info = {
|
||||||
|
'pcie_gen': None,
|
||||||
|
'pcie_width': None,
|
||||||
|
'pcie_max_gen': None,
|
||||||
|
'pcie_max_width': None
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# For NVMe drives, get PCIe information from sysfs
|
||||||
|
if disk_name.startswith('nvme'):
|
||||||
|
# Extract controller name properly using regex
|
||||||
|
import re
|
||||||
|
match = re.match(r'(nvme\d+)n\d+', disk_name)
|
||||||
|
if not match:
|
||||||
|
# print(f"[v0] Could not extract controller from {disk_name}")
|
||||||
|
pass
|
||||||
|
return pcie_info
|
||||||
|
|
||||||
|
controller = match.group(1) # nvme0n1 -> nvme0
|
||||||
|
# print(f"[v0] Getting PCIe info for {disk_name}, controller: {controller}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Path to PCIe device in sysfs
|
||||||
|
sys_path = f'/sys/class/nvme/{controller}/device'
|
||||||
|
|
||||||
|
# print(f"[v0] Checking sys_path: {sys_path}, exists: {os.path.exists(sys_path)}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
if os.path.exists(sys_path):
|
||||||
|
try:
|
||||||
|
pci_address = os.path.basename(os.readlink(sys_path))
|
||||||
|
# print(f"[v0] PCI address for {disk_name}: {pci_address}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Use lspci to get detailed PCIe information
|
||||||
|
result = subprocess.run(['lspci', '-vvv', '-s', pci_address],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
# print(f"[v0] lspci output for {pci_address}:")
|
||||||
|
pass
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
# Look for "LnkSta:" line which shows current link status
|
||||||
|
if 'LnkSta:' in line:
|
||||||
|
# print(f"[v0] Found LnkSta: {line}")
|
||||||
|
pass
|
||||||
|
# Example: "LnkSta: Speed 8GT/s, Width x4"
|
||||||
|
if 'Speed' in line:
|
||||||
|
speed_match = re.search(r'Speed\s+([\d.]+)GT/s', line)
|
||||||
|
if speed_match:
|
||||||
|
gt_s = float(speed_match.group(1))
|
||||||
|
if gt_s <= 2.5:
|
||||||
|
pcie_info['pcie_gen'] = '1.0'
|
||||||
|
elif gt_s <= 5.0:
|
||||||
|
pcie_info['pcie_gen'] = '2.0'
|
||||||
|
elif gt_s <= 8.0:
|
||||||
|
pcie_info['pcie_gen'] = '3.0'
|
||||||
|
elif gt_s <= 16.0:
|
||||||
|
pcie_info['pcie_gen'] = '4.0'
|
||||||
|
else:
|
||||||
|
pcie_info['pcie_gen'] = '5.0'
|
||||||
|
# print(f"[v0] Current PCIe gen: {pcie_info['pcie_gen']}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'Width' in line:
|
||||||
|
width_match = re.search(r'Width\s+x(\d+)', line)
|
||||||
|
if width_match:
|
||||||
|
pcie_info['pcie_width'] = f'x{width_match.group(1)}'
|
||||||
|
# print(f"[v0] Current PCIe width: {pcie_info['pcie_width']}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Look for "LnkCap:" line which shows maximum capabilities
|
||||||
|
elif 'LnkCap:' in line:
|
||||||
|
# print(f"[v0] Found LnkCap: {line}")
|
||||||
|
pass
|
||||||
|
if 'Speed' in line:
|
||||||
|
speed_match = re.search(r'Speed\s+([\d.]+)GT/s', line)
|
||||||
|
if speed_match:
|
||||||
|
gt_s = float(speed_match.group(1))
|
||||||
|
if gt_s <= 2.5:
|
||||||
|
pcie_info['pcie_max_gen'] = '1.0'
|
||||||
|
elif gt_s <= 5.0:
|
||||||
|
pcie_info['pcie_max_gen'] = '2.0'
|
||||||
|
elif gt_s <= 8.0:
|
||||||
|
pcie_info['pcie_max_gen'] = '3.0'
|
||||||
|
elif gt_s <= 16.0:
|
||||||
|
pcie_info['pcie_max_gen'] = '4.0'
|
||||||
|
else:
|
||||||
|
pcie_info['pcie_max_gen'] = '5.0'
|
||||||
|
# print(f"[v0] Max PCIe gen: {pcie_info['pcie_max_gen']}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'Width' in line:
|
||||||
|
width_match = re.search(r'Width\s+x(\d+)', line)
|
||||||
|
if width_match:
|
||||||
|
pcie_info['pcie_max_width'] = f'x{width_match.group(1)}'
|
||||||
|
# print(f"[v0] Max PCIe width: {pcie_info['pcie_max_width']}")
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# print(f"[v0] lspci failed with return code: {result.returncode}")
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
# print(f"[v0] Error getting PCIe info via lspci: {e}")
|
||||||
|
pass
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
else:
|
||||||
|
# print(f"[v0] sys_path does not exist: {sys_path}")
|
||||||
|
pass
|
||||||
|
alt_sys_path = f'/sys/block/{disk_name}/device/device'
|
||||||
|
# print(f"[v0] Trying alternative path: {alt_sys_path}, exists: {os.path.exists(alt_sys_path)}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
if os.path.exists(alt_sys_path):
|
||||||
|
try:
|
||||||
|
# Get PCI address from the alternative path
|
||||||
|
pci_address = os.path.basename(os.readlink(alt_sys_path))
|
||||||
|
# print(f"[v0] PCI address from alt path for {disk_name}: {pci_address}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Use lspci to get detailed PCIe information
|
||||||
|
result = subprocess.run(['lspci', '-vvv', '-s', pci_address],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
# print(f"[v0] lspci output for {pci_address} (from alt path):")
|
||||||
|
pass
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
# Look for "LnkSta:" line which shows current link status
|
||||||
|
if 'LnkSta:' in line:
|
||||||
|
# print(f"[v0] Found LnkSta: {line}")
|
||||||
|
pass
|
||||||
|
if 'Speed' in line:
|
||||||
|
speed_match = re.search(r'Speed\s+([\d.]+)GT/s', line)
|
||||||
|
if speed_match:
|
||||||
|
gt_s = float(speed_match.group(1))
|
||||||
|
if gt_s <= 2.5:
|
||||||
|
pcie_info['pcie_gen'] = '1.0'
|
||||||
|
elif gt_s <= 5.0:
|
||||||
|
pcie_info['pcie_gen'] = '2.0'
|
||||||
|
elif gt_s <= 8.0:
|
||||||
|
pcie_info['pcie_gen'] = '3.0'
|
||||||
|
elif gt_s <= 16.0:
|
||||||
|
pcie_info['pcie_gen'] = '4.0'
|
||||||
|
else:
|
||||||
|
pcie_info['pcie_gen'] = '5.0'
|
||||||
|
# print(f"[v0] Current PCIe gen: {pcie_info['pcie_gen']}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'Width' in line:
|
||||||
|
width_match = re.search(r'Width\s+x(\d+)', line)
|
||||||
|
if width_match:
|
||||||
|
pcie_info['pcie_width'] = f'x{width_match.group(1)}'
|
||||||
|
# print(f"[v0] Current PCIe width: {pcie_info['pcie_width']}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Look for "LnkCap:" line which shows maximum capabilities
|
||||||
|
elif 'LnkCap:' in line:
|
||||||
|
# print(f"[v0] Found LnkCap: {line}")
|
||||||
|
pass
|
||||||
|
if 'Speed' in line:
|
||||||
|
speed_match = re.search(r'Speed\s+([\d.]+)GT/s', line)
|
||||||
|
if speed_match:
|
||||||
|
gt_s = float(speed_match.group(1))
|
||||||
|
if gt_s <= 2.5:
|
||||||
|
pcie_info['pcie_max_gen'] = '1.0'
|
||||||
|
elif gt_s <= 5.0:
|
||||||
|
pcie_info['pcie_max_gen'] = '2.0'
|
||||||
|
elif gt_s <= 8.0:
|
||||||
|
pcie_info['pcie_max_gen'] = '3.0'
|
||||||
|
elif gt_s <= 16.0:
|
||||||
|
pcie_info['pcie_max_gen'] = '4.0'
|
||||||
|
else:
|
||||||
|
pcie_info['pcie_max_gen'] = '5.0'
|
||||||
|
# print(f"[v0] Max PCIe gen: {pcie_info['pcie_max_gen']}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'Width' in line:
|
||||||
|
width_match = re.search(r'Width\s+x(\d+)', line)
|
||||||
|
if width_match:
|
||||||
|
pcie_info['pcie_max_width'] = f'x{width_match.group(1)}'
|
||||||
|
# print(f"[v0] Max PCIe width: {pcie_info['pcie_max_width']}")
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# print(f"[v0] lspci failed with return code: {result.returncode}")
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
# print(f"[v0] Error getting PCIe info from alt path: {e}")
|
||||||
|
pass
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# print(f"[v0] Error in get_pcie_link_speed for {disk_name}: {e}")
|
||||||
|
pass
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# print(f"[v0] Final PCIe info for {disk_name}: {pcie_info}")
|
||||||
|
pass
|
||||||
|
return pcie_info
|
||||||
|
|
||||||
|
# get_pcie_link_speed function definition ends here
|
||||||
|
|
||||||
def get_smart_data(disk_name):
|
def get_smart_data(disk_name):
|
||||||
"""Get SMART data for a specific disk - Enhanced with multiple device type attempts"""
|
"""Get SMART data for a specific disk - Enhanced with multiple device type attempts"""
|
||||||
smart_data = {
|
smart_data = {
|
||||||
@@ -947,8 +1171,8 @@ def get_smart_data(disk_name):
|
|||||||
try:
|
try:
|
||||||
commands_to_try = [
|
commands_to_try = [
|
||||||
['smartctl', '-a', '-j', f'/dev/{disk_name}'], # JSON output (preferred)
|
['smartctl', '-a', '-j', f'/dev/{disk_name}'], # JSON output (preferred)
|
||||||
['smartctl', '-a', '-j', '-d', 'ata', f'/dev/{disk_name}'], # JSON with ATA device type
|
['smartctl', '-a', '-d', 'ata', f'/dev/{disk_name}'], # JSON with ATA device type
|
||||||
['smartctl', '-a', '-j', '-d', 'sat', f'/dev/{disk_name}'], # JSON with SAT device type
|
['smartctl', '-a', '-d', 'sat', f'/dev/{disk_name}'], # JSON with SAT device type
|
||||||
['smartctl', '-a', f'/dev/{disk_name}'], # Text output (fallback)
|
['smartctl', '-a', f'/dev/{disk_name}'], # Text output (fallback)
|
||||||
['smartctl', '-a', '-d', 'ata', f'/dev/{disk_name}'], # Text with ATA device type
|
['smartctl', '-a', '-d', 'ata', f'/dev/{disk_name}'], # Text with ATA device type
|
||||||
['smartctl', '-a', '-d', 'sat', f'/dev/{disk_name}'], # Text with SAT device type
|
['smartctl', '-a', '-d', 'sat', f'/dev/{disk_name}'], # Text with SAT device type
|
||||||
@@ -1387,6 +1611,23 @@ def get_smart_data(disk_name):
|
|||||||
smart_data['health'] = 'warning'
|
smart_data['health'] = 'warning'
|
||||||
# print(f"[v0] Health: WARNING (temperature {smart_data['temperature']}°C)")
|
# print(f"[v0] Health: WARNING (temperature {smart_data['temperature']}°C)")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# CHANGE: Use -1 to indicate HDD with unknown RPM instead of inventing 7200 RPM
|
||||||
|
# Fallback: Check kernel's rotational flag if smartctl didn't provide rotation_rate
|
||||||
|
# This fixes detection for older disks that don't report RPM via smartctl
|
||||||
|
if smart_data['rotation_rate'] == 0:
|
||||||
|
try:
|
||||||
|
rotational_path = f"/sys/block/{disk_name}/queue/rotational"
|
||||||
|
if os.path.exists(rotational_path):
|
||||||
|
with open(rotational_path, 'r') as f:
|
||||||
|
rotational = int(f.read().strip())
|
||||||
|
if rotational == 1:
|
||||||
|
# Disk is rotational (HDD), use -1 to indicate "HDD but RPM unknown"
|
||||||
|
smart_data['rotation_rate'] = -1
|
||||||
|
# If rotational == 0, it's an SSD, keep rotation_rate as 0
|
||||||
|
except Exception as e:
|
||||||
|
pass # If we can't read the file, leave rotation_rate as is
|
||||||
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# print(f"[v0] ERROR: smartctl not found - install smartmontools for disk monitoring.")
|
# print(f"[v0] ERROR: smartctl not found - install smartmontools for disk monitoring.")
|
||||||
@@ -1500,6 +1741,7 @@ def get_proxmox_storage():
|
|||||||
# END OF CHANGES FOR get_proxmox_storage
|
# END OF CHANGES FOR get_proxmox_storage
|
||||||
|
|
||||||
@app.route('/api/storage/summary', methods=['GET'])
|
@app.route('/api/storage/summary', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_storage_summary():
|
def api_storage_summary():
|
||||||
"""Get storage summary without SMART data (optimized for Overview page)"""
|
"""Get storage summary without SMART data (optimized for Overview page)"""
|
||||||
try:
|
try:
|
||||||
@@ -1598,8 +1840,15 @@ def get_interface_type(interface_name):
|
|||||||
if '.' in interface_name:
|
if '.' in interface_name:
|
||||||
return 'vlan'
|
return 'vlan'
|
||||||
|
|
||||||
# Check if it's a physical interface
|
# Check if interface has a real device symlink in /sys/class/net
|
||||||
if interface_name.startswith(('enp', 'eth', 'eno', 'ens', 'wlan', 'wlp')):
|
# This catches all physical interfaces including USB, regardless of naming
|
||||||
|
sys_path = f'/sys/class/net/{interface_name}/device'
|
||||||
|
if os.path.exists(sys_path):
|
||||||
|
# It's a physical interface (PCI, USB, etc.)
|
||||||
|
return 'physical'
|
||||||
|
|
||||||
|
# This handles cases where /sys might not be available
|
||||||
|
if interface_name.startswith(('enp', 'eth', 'eno', 'ens', 'enx', 'wlan', 'wlp', 'wlo', 'usb')):
|
||||||
return 'physical'
|
return 'physical'
|
||||||
|
|
||||||
# Default to skip for unknown types
|
# Default to skip for unknown types
|
||||||
@@ -1996,7 +2245,7 @@ def get_proxmox_vms():
|
|||||||
# print(f"[v0] Error getting VM/LXC info: {e}")
|
# print(f"[v0] Error getting VM/LXC info: {e}")
|
||||||
pass
|
pass
|
||||||
return {
|
return {
|
||||||
'error': f'Unable to access VM information: {str(e)}',
|
'error': 'Unable to access VM information: {str(e)}',
|
||||||
'vms': []
|
'vms': []
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -2531,7 +2780,7 @@ def get_detailed_gpu_info(gpu):
|
|||||||
if 'clients' in json_data:
|
if 'clients' in json_data:
|
||||||
client_count = len(json_data['clients'])
|
client_count = len(json_data['clients'])
|
||||||
|
|
||||||
for client_id, client_data in json_data['clients'].items():
|
for client_id, client_data in json_data['clients']:
|
||||||
client_name = client_data.get('name', 'Unknown')
|
client_name = client_data.get('name', 'Unknown')
|
||||||
client_pid = client_data.get('pid', 'Unknown')
|
client_pid = client_data.get('pid', 'Unknown')
|
||||||
|
|
||||||
@@ -3064,6 +3313,9 @@ def get_detailed_gpu_info(gpu):
|
|||||||
|
|
||||||
data_retrieved = False
|
data_retrieved = False
|
||||||
|
|
||||||
|
# CHANGE: Initialize sensors variable to None to avoid UnboundLocalError
|
||||||
|
sensors = None
|
||||||
|
|
||||||
# Parse temperature (Edge Temperature from sensors)
|
# Parse temperature (Edge Temperature from sensors)
|
||||||
if 'sensors' in device:
|
if 'sensors' in device:
|
||||||
sensors = device['sensors']
|
sensors = device['sensors']
|
||||||
@@ -3074,22 +3326,23 @@ def get_detailed_gpu_info(gpu):
|
|||||||
# print(f"[v0] Temperature: {detailed_info['temperature']}°C", flush=True)
|
# print(f"[v0] Temperature: {detailed_info['temperature']}°C", flush=True)
|
||||||
pass
|
pass
|
||||||
data_retrieved = True
|
data_retrieved = True
|
||||||
|
|
||||||
# Parse power draw (GFX Power or average_socket_power)
|
# CHANGE: Added check to ensure sensors is not None before accessing
|
||||||
if 'GFX Power' in sensors:
|
# Parse power draw (GFX Power or average_socket_power)
|
||||||
gfx_power = sensors['GFX Power']
|
if sensors and 'GFX Power' in sensors:
|
||||||
if 'value' in gfx_power:
|
gfx_power = sensors['GFX Power']
|
||||||
detailed_info['power_draw'] = f"{gfx_power['value']:.2f} W"
|
if 'value' in gfx_power:
|
||||||
# print(f"[v0] Power Draw: {detailed_info['power_draw']}", flush=True)
|
detailed_info['power_draw'] = f"{gfx_power['value']:.2f} W"
|
||||||
pass
|
# print(f"[v0] Power Draw: {detailed_info['power_draw']}", flush=True)
|
||||||
data_retrieved = True
|
pass
|
||||||
elif 'average_socket_power' in sensors:
|
data_retrieved = True
|
||||||
socket_power = sensors['average_socket_power']
|
elif sensors and 'average_socket_power' in sensors:
|
||||||
if 'value' in socket_power:
|
socket_power = sensors['average_socket_power']
|
||||||
detailed_info['power_draw'] = f"{socket_power['value']:.2f} W"
|
if 'value' in socket_power:
|
||||||
# print(f"[v0] Power Draw: {detailed_info['power_draw']}", flush=True)
|
detailed_info['power_draw'] = f"{socket_power['value']:.2f} W"
|
||||||
pass
|
# print(f"[v0] Power Draw: {detailed_info['power_draw']}", flush=True)
|
||||||
data_retrieved = True
|
pass
|
||||||
|
data_retrieved = True
|
||||||
|
|
||||||
# Parse clocks (GFX_SCLK for graphics, GFX_MCLK for memory)
|
# Parse clocks (GFX_SCLK for graphics, GFX_MCLK for memory)
|
||||||
if 'Clocks' in device:
|
if 'Clocks' in device:
|
||||||
@@ -3098,7 +3351,7 @@ def get_detailed_gpu_info(gpu):
|
|||||||
gfx_clock = clocks['GFX_SCLK']
|
gfx_clock = clocks['GFX_SCLK']
|
||||||
if 'value' in gfx_clock:
|
if 'value' in gfx_clock:
|
||||||
detailed_info['clock_graphics'] = f"{gfx_clock['value']} MHz"
|
detailed_info['clock_graphics'] = f"{gfx_clock['value']} MHz"
|
||||||
# print(f"[v0] Graphics Clock: {detailed_info['clock_graphics']}", flush=True)
|
# print(f"[v0] Graphics Clock: {detailed_info['clock_graphics']} MHz", flush=True)
|
||||||
pass
|
pass
|
||||||
data_retrieved = True
|
data_retrieved = True
|
||||||
|
|
||||||
@@ -3106,7 +3359,7 @@ def get_detailed_gpu_info(gpu):
|
|||||||
mem_clock = clocks['GFX_MCLK']
|
mem_clock = clocks['GFX_MCLK']
|
||||||
if 'value' in mem_clock:
|
if 'value' in mem_clock:
|
||||||
detailed_info['clock_memory'] = f"{mem_clock['value']} MHz"
|
detailed_info['clock_memory'] = f"{mem_clock['value']} MHz"
|
||||||
# print(f"[v0] Memory Clock: {detailed_info['clock_memory']}", flush=True)
|
# print(f"[v0] Memory Clock: {detailed_info['clock_memory']} MHz", flush=True)
|
||||||
pass
|
pass
|
||||||
data_retrieved = True
|
data_retrieved = True
|
||||||
|
|
||||||
@@ -3223,7 +3476,7 @@ def get_detailed_gpu_info(gpu):
|
|||||||
'shared': 0,
|
'shared': 0,
|
||||||
'resident': int(vram_mb * 1024 * 1024)
|
'resident': int(vram_mb * 1024 * 1024)
|
||||||
}
|
}
|
||||||
# print(f"[v0] VRAM: {vram_mb} MB", flush=True)
|
# print(f"[v0] VRAM: {vram_mb} MB", flush=True)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Parse GTT (Graphics Translation Table) usage (está dentro de usage.usage)
|
# Parse GTT (Graphics Translation Table) usage (está dentro de usage.usage)
|
||||||
@@ -3237,7 +3490,7 @@ def get_detailed_gpu_info(gpu):
|
|||||||
else:
|
else:
|
||||||
# Add GTT to existing VRAM
|
# Add GTT to existing VRAM
|
||||||
process_info['memory']['total'] += int(gtt_mb * 1024 * 1024)
|
process_info['memory']['total'] += int(gtt_mb * 1024 * 1024)
|
||||||
# print(f"[v0] GTT: {gtt_mb} MB", flush=True)
|
# print(f"[v0] GTT: {gtt_mb} MB", flush=True)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Parse engine utilization for this process (están dentro de usage.usage)
|
# Parse engine utilization for this process (están dentro de usage.usage)
|
||||||
@@ -3343,7 +3596,6 @@ def get_detailed_gpu_info(gpu):
|
|||||||
else:
|
else:
|
||||||
# print(f"[v0] No fdinfo section found in device data", flush=True)
|
# print(f"[v0] No fdinfo section found in device data", flush=True)
|
||||||
pass
|
pass
|
||||||
detailed_info['processes'] = []
|
|
||||||
|
|
||||||
if data_retrieved:
|
if data_retrieved:
|
||||||
detailed_info['has_monitoring_tool'] = True
|
detailed_info['has_monitoring_tool'] = True
|
||||||
@@ -3869,6 +4121,10 @@ def get_hardware_info():
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
pcie_info = {}
|
||||||
|
if disk_name.startswith('nvme'):
|
||||||
|
pcie_info = get_pcie_link_speed(disk_name)
|
||||||
|
|
||||||
# Build storage device with all available information
|
# Build storage device with all available information
|
||||||
storage_device = {
|
storage_device = {
|
||||||
'name': disk_name,
|
'name': disk_name,
|
||||||
@@ -3884,6 +4140,9 @@ def get_hardware_info():
|
|||||||
'sata_version': sata_version,
|
'sata_version': sata_version,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pcie_info:
|
||||||
|
storage_device.update(pcie_info)
|
||||||
|
|
||||||
# Add family if available (from smartctl)
|
# Add family if available (from smartctl)
|
||||||
try:
|
try:
|
||||||
result_smart = subprocess.run(['smartctl', '-i', f'/dev/{disk_name}'],
|
result_smart = subprocess.run(['smartctl', '-i', f'/dev/{disk_name}'],
|
||||||
@@ -3905,7 +4164,7 @@ def get_hardware_info():
|
|||||||
# print(f"[v0] Error getting storage info: {e}")
|
# print(f"[v0] Error getting storage info: {e}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Graphics Cards (from lspci - will be duplicated by new PCI device listing, but kept for now)
|
# Graphics Cards
|
||||||
try:
|
try:
|
||||||
# Try nvidia-smi first
|
# Try nvidia-smi first
|
||||||
result = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total,memory.used,temperature.gpu,power.draw,utilization.gpu,utilization.memory,clocks.graphics,clocks.memory', '--format=csv,noheader,nounits'],
|
result = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total,memory.used,temperature.gpu,power.draw,utilization.gpu,utilization.memory,clocks.graphics,clocks.memory', '--format=csv,noheader,nounits'],
|
||||||
@@ -4262,6 +4521,7 @@ def get_hardware_info():
|
|||||||
|
|
||||||
|
|
||||||
@app.route('/api/system', methods=['GET'])
|
@app.route('/api/system', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_system():
|
def api_system():
|
||||||
"""Get system information including CPU, memory, and temperature"""
|
"""Get system information including CPU, memory, and temperature"""
|
||||||
try:
|
try:
|
||||||
@@ -4318,21 +4578,25 @@ def api_system():
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/storage', methods=['GET'])
|
@app.route('/api/storage', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_storage():
|
def api_storage():
|
||||||
"""Get storage information"""
|
"""Get storage information"""
|
||||||
return jsonify(get_storage_info())
|
return jsonify(get_storage_info())
|
||||||
|
|
||||||
@app.route('/api/proxmox-storage', methods=['GET'])
|
@app.route('/api/proxmox-storage', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_proxmox_storage():
|
def api_proxmox_storage():
|
||||||
"""Get Proxmox storage information"""
|
"""Get Proxmox storage information"""
|
||||||
return jsonify(get_proxmox_storage())
|
return jsonify(get_proxmox_storage())
|
||||||
|
|
||||||
@app.route('/api/network', methods=['GET'])
|
@app.route('/api/network', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_network():
|
def api_network():
|
||||||
"""Get network information"""
|
"""Get network information"""
|
||||||
return jsonify(get_network_info())
|
return jsonify(get_network_info())
|
||||||
|
|
||||||
@app.route('/api/network/summary', methods=['GET'])
|
@app.route('/api/network/summary', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_network_summary():
|
def api_network_summary():
|
||||||
"""Optimized network summary endpoint - returns basic network info without detailed analysis"""
|
"""Optimized network summary endpoint - returns basic network info without detailed analysis"""
|
||||||
try:
|
try:
|
||||||
@@ -4411,6 +4675,7 @@ def api_network_summary():
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/network/<interface_name>/metrics', methods=['GET'])
|
@app.route('/api/network/<interface_name>/metrics', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_network_interface_metrics(interface_name):
|
def api_network_interface_metrics(interface_name):
|
||||||
"""Get historical metrics (RRD data) for a specific network interface"""
|
"""Get historical metrics (RRD data) for a specific network interface"""
|
||||||
try:
|
try:
|
||||||
@@ -4450,7 +4715,7 @@ def api_network_interface_metrics(interface_name):
|
|||||||
for point in all_data:
|
for point in all_data:
|
||||||
filtered_point = {'time': point.get('time')}
|
filtered_point = {'time': point.get('time')}
|
||||||
# Add network fields if they exist
|
# Add network fields if they exist
|
||||||
for key in ['netin', 'netout', 'diskread', 'diskwrite']:
|
for key in ['netin', 'netout']:
|
||||||
if key in point:
|
if key in point:
|
||||||
filtered_point[key] = point[key]
|
filtered_point[key] = point[key]
|
||||||
rrd_data.append(filtered_point)
|
rrd_data.append(filtered_point)
|
||||||
@@ -4493,12 +4758,13 @@ def api_network_interface_metrics(interface_name):
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/vms', methods=['GET'])
|
@app.route('/api/vms', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_vms():
|
def api_vms():
|
||||||
"""Get virtual machine information"""
|
"""Get virtual machine information"""
|
||||||
return jsonify(get_proxmox_vms())
|
return jsonify(get_proxmox_vms())
|
||||||
|
|
||||||
# Add the new api_vm_metrics endpoint here
|
|
||||||
@app.route('/api/vms/<int:vmid>/metrics', methods=['GET'])
|
@app.route('/api/vms/<int:vmid>/metrics', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_vm_metrics(vmid):
|
def api_vm_metrics(vmid):
|
||||||
"""Get historical metrics (RRD data) for a specific VM/LXC"""
|
"""Get historical metrics (RRD data) for a specific VM/LXC"""
|
||||||
try:
|
try:
|
||||||
@@ -4565,6 +4831,7 @@ def api_vm_metrics(vmid):
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/node/metrics', methods=['GET'])
|
@app.route('/api/node/metrics', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_node_metrics():
|
def api_node_metrics():
|
||||||
"""Get historical metrics (RRD data) for the node"""
|
"""Get historical metrics (RRD data) for the node"""
|
||||||
try:
|
try:
|
||||||
@@ -4608,6 +4875,7 @@ def api_node_metrics():
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/logs', methods=['GET'])
|
@app.route('/api/logs', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_logs():
|
def api_logs():
|
||||||
"""Get system logs"""
|
"""Get system logs"""
|
||||||
try:
|
try:
|
||||||
@@ -4666,7 +4934,7 @@ def api_logs():
|
|||||||
'pid': log_entry.get('_PID', ''),
|
'pid': log_entry.get('_PID', ''),
|
||||||
'hostname': log_entry.get('_HOSTNAME', '')
|
'hostname': log_entry.get('_HOSTNAME', '')
|
||||||
})
|
})
|
||||||
except (json.JSONDecodeError, ValueError) as e:
|
except (json.JSONDecodeError, ValueError):
|
||||||
continue
|
continue
|
||||||
return jsonify({'logs': logs, 'total': len(logs)})
|
return jsonify({'logs': logs, 'total': len(logs)})
|
||||||
else:
|
else:
|
||||||
@@ -4685,6 +4953,7 @@ def api_logs():
|
|||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/api/logs/download', methods=['GET'])
|
@app.route('/api/logs/download', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_logs_download():
|
def api_logs_download():
|
||||||
"""Download system logs as a text file"""
|
"""Download system logs as a text file"""
|
||||||
try:
|
try:
|
||||||
@@ -4743,6 +5012,7 @@ def api_logs_download():
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/notifications', methods=['GET'])
|
@app.route('/api/notifications', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_notifications():
|
def api_notifications():
|
||||||
"""Get Proxmox notification history"""
|
"""Get Proxmox notification history"""
|
||||||
try:
|
try:
|
||||||
@@ -4859,6 +5129,7 @@ def api_notifications():
|
|||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/api/notifications/download', methods=['GET'])
|
@app.route('/api/notifications/download', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_notifications_download():
|
def api_notifications_download():
|
||||||
"""Download complete log for a specific notification"""
|
"""Download complete log for a specific notification"""
|
||||||
try:
|
try:
|
||||||
@@ -4914,6 +5185,7 @@ def api_notifications_download():
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/backups', methods=['GET'])
|
@app.route('/api/backups', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_backups():
|
def api_backups():
|
||||||
"""Get list of all backup files from Proxmox storage"""
|
"""Get list of all backup files from Proxmox storage"""
|
||||||
try:
|
try:
|
||||||
@@ -5002,6 +5274,7 @@ def api_backups():
|
|||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/api/events', methods=['GET'])
|
@app.route('/api/events', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_events():
|
def api_events():
|
||||||
"""Get recent Proxmox events and tasks"""
|
"""Get recent Proxmox events and tasks"""
|
||||||
try:
|
try:
|
||||||
@@ -5078,6 +5351,7 @@ def api_events():
|
|||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/api/task-log/<path:upid>')
|
@app.route('/api/task-log/<path:upid>')
|
||||||
|
@require_auth
|
||||||
def get_task_log(upid):
|
def get_task_log(upid):
|
||||||
"""Get complete task log from Proxmox using UPID"""
|
"""Get complete task log from Proxmox using UPID"""
|
||||||
try:
|
try:
|
||||||
@@ -5175,15 +5449,17 @@ def get_task_log(upid):
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/health', methods=['GET'])
|
@app.route('/api/health', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_health():
|
def api_health():
|
||||||
"""Health check endpoint"""
|
"""Health check endpoint"""
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'healthy',
|
'status': 'healthy',
|
||||||
'timestamp': datetime.now().isoformat(),
|
'timestamp': datetime.now().isoformat(),
|
||||||
'version': '1.0.0'
|
'version': '1.0.1'
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/api/prometheus', methods=['GET'])
|
@app.route('/api/prometheus', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_prometheus():
|
def api_prometheus():
|
||||||
"""Export metrics in Prometheus format"""
|
"""Export metrics in Prometheus format"""
|
||||||
try:
|
try:
|
||||||
@@ -5362,10 +5638,6 @@ def api_prometheus():
|
|||||||
mem_used_bytes = mem_used * 1024 * 1024 # Convert MiB to bytes
|
mem_used_bytes = mem_used * 1024 * 1024 # Convert MiB to bytes
|
||||||
mem_total_bytes = mem_total * 1024 * 1024
|
mem_total_bytes = mem_total * 1024 * 1024
|
||||||
|
|
||||||
metrics.append(f'# HELP proxmox_gpu_memory_used_bytes GPU memory used in bytes')
|
|
||||||
metrics.append(f'# TYPE proxmox_gpu_memory_used_bytes gauge')
|
|
||||||
metrics.append(f'proxmox_gpu_memory_used_bytes{{node="{node}",gpu="{gpu_name}",vendor="{gpu_vendor}",slot="{gpu_slot}"}} {mem_used_bytes} {timestamp}')
|
|
||||||
|
|
||||||
metrics.append(f'# HELP proxmox_gpu_memory_total_bytes GPU memory total in bytes')
|
metrics.append(f'# HELP proxmox_gpu_memory_total_bytes GPU memory total in bytes')
|
||||||
metrics.append(f'# TYPE proxmox_gpu_memory_total_bytes gauge')
|
metrics.append(f'# TYPE proxmox_gpu_memory_total_bytes gauge')
|
||||||
metrics.append(f'proxmox_gpu_memory_total_bytes{{node="{node}",gpu="{gpu_name}",vendor="{gpu_vendor}",slot="{gpu_slot}"}} {mem_total_bytes} {timestamp}')
|
metrics.append(f'proxmox_gpu_memory_total_bytes{{node="{node}",gpu="{gpu_name}",vendor="{gpu_vendor}",slot="{gpu_slot}"}} {mem_total_bytes} {timestamp}')
|
||||||
@@ -5442,64 +5714,14 @@ def api_prometheus():
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return f'# Error generating metrics: {str(e)}\n', 500, {'Content-Type': 'text/plain; charset=utf-8'}
|
return f'# Error generating metrics: {str(e)}\n', 500, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
|
|
||||||
@app.route('/api/system-info', methods=['GET'])
|
|
||||||
def api_system_info():
|
|
||||||
"""Get system and node information for dashboard header"""
|
|
||||||
try:
|
|
||||||
hostname = socket.gethostname()
|
|
||||||
node_id = f"pve-{hostname}"
|
|
||||||
pve_version = None
|
|
||||||
|
|
||||||
# Try to get Proxmox version
|
|
||||||
try:
|
|
||||||
result = subprocess.run(['pveversion'], capture_output=True, text=True, timeout=5)
|
|
||||||
if result.returncode == 0:
|
|
||||||
pve_version = result.stdout.strip().split('\n')[0]
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Try to get node info from Proxmox API
|
|
||||||
try:
|
|
||||||
result = subprocess.run(['pvesh', 'get', '/nodes', '--output-format', 'json'],
|
|
||||||
capture_output=True, text=True, timeout=5)
|
|
||||||
if result.returncode == 0:
|
|
||||||
nodes = json.loads(result.stdout)
|
|
||||||
if nodes and len(nodes) > 0:
|
|
||||||
node_info = nodes[0]
|
|
||||||
node_id = node_info.get('node', node_id)
|
|
||||||
hostname = node_info.get('node', hostname)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
response = {
|
|
||||||
'hostname': hostname,
|
|
||||||
'node_id': node_id,
|
|
||||||
'status': 'online',
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
if pve_version:
|
|
||||||
response['pve_version'] = pve_version
|
|
||||||
else:
|
|
||||||
response['error'] = 'Proxmox version not available - pveversion command not found'
|
|
||||||
|
|
||||||
return jsonify(response)
|
|
||||||
except Exception as e:
|
|
||||||
# print(f"Error getting system info: {e}")
|
|
||||||
pass
|
|
||||||
return jsonify({
|
|
||||||
'error': f'Unable to access system information: {str(e)}',
|
|
||||||
'hostname': socket.gethostname(),
|
|
||||||
'status': 'error',
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.route('/api/info', methods=['GET'])
|
@app.route('/api/info', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_info():
|
def api_info():
|
||||||
"""Root endpoint with API information"""
|
"""Root endpoint with API information"""
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'name': 'ProxMenux Monitor API',
|
'name': 'ProxMenux Monitor API',
|
||||||
'version': '1.0.0',
|
'version': '1.0.1',
|
||||||
'endpoints': [
|
'endpoints': [
|
||||||
'/api/system',
|
'/api/system',
|
||||||
'/api/system-info',
|
'/api/system-info',
|
||||||
@@ -5523,6 +5745,7 @@ def api_info():
|
|||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/api/hardware', methods=['GET'])
|
@app.route('/api/hardware', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_hardware():
|
def api_hardware():
|
||||||
"""Get hardware information"""
|
"""Get hardware information"""
|
||||||
try:
|
try:
|
||||||
@@ -5559,6 +5782,7 @@ def api_hardware():
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/gpu/<slot>/realtime', methods=['GET'])
|
@app.route('/api/gpu/<slot>/realtime', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_gpu_realtime(slot):
|
def api_gpu_realtime(slot):
|
||||||
"""Get real-time GPU monitoring data for a specific GPU"""
|
"""Get real-time GPU monitoring data for a specific GPU"""
|
||||||
try:
|
try:
|
||||||
@@ -5621,6 +5845,7 @@ def api_gpu_realtime(slot):
|
|||||||
|
|
||||||
# CHANGE: Modificar el endpoint para incluir la información completa de IPs
|
# CHANGE: Modificar el endpoint para incluir la información completa de IPs
|
||||||
@app.route('/api/vms/<int:vmid>', methods=['GET'])
|
@app.route('/api/vms/<int:vmid>', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def get_vm_config(vmid):
|
def get_vm_config(vmid):
|
||||||
"""Get detailed configuration for a specific VM/LXC"""
|
"""Get detailed configuration for a specific VM/LXC"""
|
||||||
try:
|
try:
|
||||||
@@ -5717,6 +5942,7 @@ def get_vm_config(vmid):
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/vms/<int:vmid>/logs', methods=['GET'])
|
@app.route('/api/vms/<int:vmid>/logs', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
def api_vm_logs(vmid):
|
def api_vm_logs(vmid):
|
||||||
"""Download real logs for a specific VM/LXC (not task history)"""
|
"""Download real logs for a specific VM/LXC (not task history)"""
|
||||||
try:
|
try:
|
||||||
@@ -5766,6 +5992,7 @@ def api_vm_logs(vmid):
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/vms/<int:vmid>/control', methods=['POST'])
|
@app.route('/api/vms/<int:vmid>/control', methods=['POST'])
|
||||||
|
@require_auth
|
||||||
def api_vm_control(vmid):
|
def api_vm_control(vmid):
|
||||||
"""Control VM/LXC (start, stop, shutdown, reboot)"""
|
"""Control VM/LXC (start, stop, shutdown, reboot)"""
|
||||||
try:
|
try:
|
||||||
@@ -5818,6 +6045,7 @@ def api_vm_control(vmid):
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/vms/<int:vmid>/config', methods=['PUT'])
|
@app.route('/api/vms/<int:vmid>/config', methods=['PUT'])
|
||||||
|
@require_auth
|
||||||
def api_vm_config_update(vmid):
|
def api_vm_config_update(vmid):
|
||||||
"""Update VM/LXC configuration (description/notes)"""
|
"""Update VM/LXC configuration (description/notes)"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,367 @@
|
|||||||
|
"""
|
||||||
|
Health Monitor Persistence Module
|
||||||
|
Manages persistent error tracking across AppImage updates using SQLite.
|
||||||
|
Stores errors in /root/.config/proxmenux-monitor/health_monitor.db
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Persistent error storage (survives AppImage updates)
|
||||||
|
- Smart error resolution (auto-clear when VM starts, or after 48h)
|
||||||
|
- Event system for future Telegram notifications
|
||||||
|
- Manual acknowledgment support
|
||||||
|
|
||||||
|
Author: MacRimi
|
||||||
|
Version: 1.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
class HealthPersistence:
|
||||||
|
"""Manages persistent health error tracking"""
|
||||||
|
|
||||||
|
# Error retention periods (seconds)
|
||||||
|
VM_ERROR_RETENTION = 48 * 3600 # 48 hours
|
||||||
|
LOG_ERROR_RETENTION = 24 * 3600 # 24 hours
|
||||||
|
DISK_ERROR_RETENTION = 48 * 3600 # 48 hours
|
||||||
|
UPDATES_SUPPRESSION = 180 * 24 * 3600 # 180 days (6 months)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize persistence with database in config directory"""
|
||||||
|
self.data_dir = Path('/root/.config/proxmenux-monitor')
|
||||||
|
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
self.db_path = self.data_dir / 'health_monitor.db'
|
||||||
|
self._init_database()
|
||||||
|
|
||||||
|
def _init_database(self):
|
||||||
|
"""Initialize SQLite database with required tables"""
|
||||||
|
conn = sqlite3.connect(str(self.db_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Errors table
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS errors (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
error_key TEXT UNIQUE NOT NULL,
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
severity TEXT NOT NULL,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
details TEXT,
|
||||||
|
first_seen TEXT NOT NULL,
|
||||||
|
last_seen TEXT NOT NULL,
|
||||||
|
resolved_at TEXT,
|
||||||
|
acknowledged INTEGER DEFAULT 0,
|
||||||
|
notification_sent INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Events table (for future Telegram notifications)
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
event_type TEXT NOT NULL,
|
||||||
|
error_key TEXT NOT NULL,
|
||||||
|
timestamp TEXT NOT NULL,
|
||||||
|
data TEXT
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Indexes for performance
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_error_key ON errors(error_key)')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_category ON errors(category)')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_resolved ON errors(resolved_at)')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_events_error ON events(error_key)')
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def record_error(self, error_key: str, category: str, severity: str,
|
||||||
|
reason: str, details: Optional[Dict] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Record or update an error.
|
||||||
|
Returns event info (new_error, updated, etc.)
|
||||||
|
"""
|
||||||
|
conn = sqlite3.connect(str(self.db_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
details_json = json.dumps(details) if details else None
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT acknowledged, resolved_at
|
||||||
|
FROM errors
|
||||||
|
WHERE error_key = ? AND acknowledged = 1
|
||||||
|
''', (error_key,))
|
||||||
|
ack_check = cursor.fetchone()
|
||||||
|
|
||||||
|
if ack_check and ack_check[1]: # Has resolved_at timestamp
|
||||||
|
try:
|
||||||
|
resolved_dt = datetime.fromisoformat(ack_check[1])
|
||||||
|
hours_since_ack = (datetime.now() - resolved_dt).total_seconds() / 3600
|
||||||
|
|
||||||
|
if category == 'updates':
|
||||||
|
# Updates: suppress for 180 days (6 months)
|
||||||
|
suppression_hours = self.UPDATES_SUPPRESSION / 3600
|
||||||
|
else:
|
||||||
|
# Other errors: suppress for 24 hours
|
||||||
|
suppression_hours = 24
|
||||||
|
|
||||||
|
if hours_since_ack < suppression_hours:
|
||||||
|
# Skip re-adding recently acknowledged errors
|
||||||
|
conn.close()
|
||||||
|
return {'type': 'skipped_acknowledged', 'needs_notification': False}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT id, first_seen, notification_sent, acknowledged, resolved_at
|
||||||
|
FROM errors WHERE error_key = ?
|
||||||
|
''', (error_key,))
|
||||||
|
existing = cursor.fetchone()
|
||||||
|
|
||||||
|
event_info = {'type': 'updated', 'needs_notification': False}
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
error_id, first_seen, notif_sent, acknowledged, resolved_at = existing
|
||||||
|
|
||||||
|
if acknowledged == 1:
|
||||||
|
conn.close()
|
||||||
|
return {'type': 'skipped_acknowledged', 'needs_notification': False}
|
||||||
|
|
||||||
|
# Update existing error (only if NOT acknowledged)
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE errors
|
||||||
|
SET last_seen = ?, severity = ?, reason = ?, details = ?
|
||||||
|
WHERE error_key = ? AND acknowledged = 0
|
||||||
|
''', (now, severity, reason, details_json, error_key))
|
||||||
|
|
||||||
|
# Check if severity escalated
|
||||||
|
cursor.execute('SELECT severity FROM errors WHERE error_key = ?', (error_key,))
|
||||||
|
old_severity_row = cursor.fetchone()
|
||||||
|
if old_severity_row:
|
||||||
|
old_severity = old_severity_row[0]
|
||||||
|
if old_severity == 'WARNING' and severity == 'CRITICAL':
|
||||||
|
event_info['type'] = 'escalated'
|
||||||
|
event_info['needs_notification'] = True
|
||||||
|
else:
|
||||||
|
# Insert new error
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO errors
|
||||||
|
(error_key, category, severity, reason, details, first_seen, last_seen)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
''', (error_key, category, severity, reason, details_json, now, now))
|
||||||
|
|
||||||
|
event_info['type'] = 'new'
|
||||||
|
event_info['needs_notification'] = True
|
||||||
|
|
||||||
|
# Record event
|
||||||
|
self._record_event(cursor, event_info['type'], error_key,
|
||||||
|
{'severity': severity, 'reason': reason})
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return event_info
|
||||||
|
|
||||||
|
def resolve_error(self, error_key: str, reason: str = 'auto-resolved'):
|
||||||
|
"""Mark an error as resolved"""
|
||||||
|
conn = sqlite3.connect(str(self.db_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE errors
|
||||||
|
SET resolved_at = ?
|
||||||
|
WHERE error_key = ? AND resolved_at IS NULL
|
||||||
|
''', (now, error_key))
|
||||||
|
|
||||||
|
if cursor.rowcount > 0:
|
||||||
|
self._record_event(cursor, 'resolved', error_key, {'reason': reason})
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def acknowledge_error(self, error_key: str):
|
||||||
|
"""
|
||||||
|
Manually acknowledge an error (won't notify again or re-appear for 24h).
|
||||||
|
Also marks as resolved so it disappears from active errors.
|
||||||
|
"""
|
||||||
|
conn = sqlite3.connect(str(self.db_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE errors
|
||||||
|
SET acknowledged = 1, resolved_at = ?
|
||||||
|
WHERE error_key = ?
|
||||||
|
''', (now, error_key))
|
||||||
|
|
||||||
|
self._record_event(cursor, 'acknowledged', error_key, {})
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_active_errors(self, category: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||||
|
"""Get all active (unresolved) errors, optionally filtered by category"""
|
||||||
|
conn = sqlite3.connect(str(self.db_path))
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
if category:
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT * FROM errors
|
||||||
|
WHERE resolved_at IS NULL AND category = ?
|
||||||
|
ORDER BY severity DESC, last_seen DESC
|
||||||
|
''', (category,))
|
||||||
|
else:
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT * FROM errors
|
||||||
|
WHERE resolved_at IS NULL
|
||||||
|
ORDER BY severity DESC, last_seen DESC
|
||||||
|
''')
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
for row in rows:
|
||||||
|
error_dict = dict(row)
|
||||||
|
if error_dict.get('details'):
|
||||||
|
error_dict['details'] = json.loads(error_dict['details'])
|
||||||
|
errors.append(error_dict)
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
def cleanup_old_errors(self):
|
||||||
|
"""Clean up old resolved errors and auto-resolve stale errors"""
|
||||||
|
conn = sqlite3.connect(str(self.db_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
# Delete resolved errors older than 7 days
|
||||||
|
cutoff_resolved = (now - timedelta(days=7)).isoformat()
|
||||||
|
cursor.execute('DELETE FROM errors WHERE resolved_at < ?', (cutoff_resolved,))
|
||||||
|
|
||||||
|
# Auto-resolve VM/CT errors older than 48h
|
||||||
|
cutoff_vm = (now - timedelta(seconds=self.VM_ERROR_RETENTION)).isoformat()
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE errors
|
||||||
|
SET resolved_at = ?
|
||||||
|
WHERE category = 'vms'
|
||||||
|
AND resolved_at IS NULL
|
||||||
|
AND first_seen < ?
|
||||||
|
AND acknowledged = 0
|
||||||
|
''', (now.isoformat(), cutoff_vm))
|
||||||
|
|
||||||
|
# Auto-resolve log errors older than 24h
|
||||||
|
cutoff_logs = (now - timedelta(seconds=self.LOG_ERROR_RETENTION)).isoformat()
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE errors
|
||||||
|
SET resolved_at = ?
|
||||||
|
WHERE category = 'logs'
|
||||||
|
AND resolved_at IS NULL
|
||||||
|
AND first_seen < ?
|
||||||
|
AND acknowledged = 0
|
||||||
|
''', (now.isoformat(), cutoff_logs))
|
||||||
|
|
||||||
|
# Delete old events (>30 days)
|
||||||
|
cutoff_events = (now - timedelta(days=30)).isoformat()
|
||||||
|
cursor.execute('DELETE FROM events WHERE timestamp < ?', (cutoff_events,))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def check_vm_running(self, vm_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a VM/CT is running and resolve error if so.
|
||||||
|
Returns True if running and error was resolved.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check qm status for VMs
|
||||||
|
result = subprocess.run(
|
||||||
|
['qm', 'status', vm_id],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=2
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0 and 'running' in result.stdout.lower():
|
||||||
|
self.resolve_error(f'vm_{vm_id}', 'VM started')
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check pct status for containers
|
||||||
|
result = subprocess.run(
|
||||||
|
['pct', 'status', vm_id],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=2
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0 and 'running' in result.stdout.lower():
|
||||||
|
self.resolve_error(f'ct_{vm_id}', 'Container started')
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _record_event(self, cursor, event_type: str, error_key: str, data: Dict):
|
||||||
|
"""Internal: Record an event"""
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO events (event_type, error_key, timestamp, data)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
''', (event_type, error_key, datetime.now().isoformat(), json.dumps(data)))
|
||||||
|
|
||||||
|
def get_unnotified_errors(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Get errors that need Telegram notification"""
|
||||||
|
conn = sqlite3.connect(str(self.db_path))
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT * FROM errors
|
||||||
|
WHERE notification_sent = 0
|
||||||
|
AND resolved_at IS NULL
|
||||||
|
AND acknowledged = 0
|
||||||
|
ORDER BY severity DESC, first_seen ASC
|
||||||
|
''')
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
for row in rows:
|
||||||
|
error_dict = dict(row)
|
||||||
|
if error_dict.get('details'):
|
||||||
|
error_dict['details'] = json.loads(error_dict['details'])
|
||||||
|
errors.append(error_dict)
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
def mark_notified(self, error_key: str):
|
||||||
|
"""Mark error as notified"""
|
||||||
|
conn = sqlite3.connect(str(self.db_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE errors
|
||||||
|
SET notification_sent = 1
|
||||||
|
WHERE error_key = ?
|
||||||
|
''', (error_key,))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# Global instance
|
||||||
|
health_persistence = HealthPersistence()
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""
|
||||||
|
JWT Middleware Module
|
||||||
|
Provides decorator to protect Flask routes with JWT authentication
|
||||||
|
Automatically checks auth status and validates tokens
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import request, jsonify
|
||||||
|
from functools import wraps
|
||||||
|
from auth_manager import load_auth_config, verify_token
|
||||||
|
|
||||||
|
|
||||||
|
def require_auth(f):
|
||||||
|
"""
|
||||||
|
Decorator to protect Flask routes with JWT authentication
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- If auth is disabled or declined: Allow access (no token required)
|
||||||
|
- If auth is enabled: Require valid JWT token in Authorization header
|
||||||
|
- Returns 401 if auth required but token missing/invalid
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
@app.route('/api/protected')
|
||||||
|
@require_auth
|
||||||
|
def protected_route():
|
||||||
|
return jsonify({"data": "secret"})
|
||||||
|
"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
# Check if authentication is enabled
|
||||||
|
config = load_auth_config()
|
||||||
|
|
||||||
|
# If auth is disabled or declined, allow access
|
||||||
|
if not config.get("enabled", False) or config.get("declined", False):
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
# Auth is enabled, require token
|
||||||
|
auth_header = request.headers.get('Authorization')
|
||||||
|
|
||||||
|
if not auth_header:
|
||||||
|
return jsonify({
|
||||||
|
"error": "Authentication required",
|
||||||
|
"message": "No authorization header provided"
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
# Extract token from "Bearer <token>" format
|
||||||
|
parts = auth_header.split()
|
||||||
|
if len(parts) != 2 or parts[0].lower() != 'bearer':
|
||||||
|
return jsonify({
|
||||||
|
"error": "Invalid authorization header",
|
||||||
|
"message": "Authorization header must be in format: Bearer <token>"
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
token = parts[1]
|
||||||
|
|
||||||
|
# Verify token
|
||||||
|
username = verify_token(token)
|
||||||
|
if not username:
|
||||||
|
return jsonify({
|
||||||
|
"error": "Invalid or expired token",
|
||||||
|
"message": "Please log in again"
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
# Token is valid, allow access
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def optional_auth(f):
|
||||||
|
"""
|
||||||
|
Decorator for routes that can optionally use auth
|
||||||
|
Passes username if authenticated, None otherwise
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
@app.route('/api/optional')
|
||||||
|
@optional_auth
|
||||||
|
def optional_route(username=None):
|
||||||
|
if username:
|
||||||
|
return jsonify({"message": f"Hello {username}"})
|
||||||
|
return jsonify({"message": "Hello guest"})
|
||||||
|
"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
config = load_auth_config()
|
||||||
|
username = None
|
||||||
|
|
||||||
|
if config.get("enabled", False):
|
||||||
|
auth_header = request.headers.get('Authorization')
|
||||||
|
if auth_header:
|
||||||
|
parts = auth_header.split()
|
||||||
|
if len(parts) == 2 and parts[0].lower() == 'bearer':
|
||||||
|
username = verify_token(parts[1])
|
||||||
|
|
||||||
|
# Inject username into kwargs
|
||||||
|
kwargs['username'] = username
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { fetchApi } from "@/lib/api-config"
|
||||||
|
|
||||||
export interface Temperature {
|
export interface Temperature {
|
||||||
name: string
|
name: string
|
||||||
original_name?: string
|
original_name?: string
|
||||||
@@ -33,6 +35,13 @@ export interface StorageDevice {
|
|||||||
rotation_rate?: number | string
|
rotation_rate?: number | string
|
||||||
form_factor?: string
|
form_factor?: string
|
||||||
sata_version?: string
|
sata_version?: string
|
||||||
|
pcie_gen?: string // e.g., "PCIe 4.0"
|
||||||
|
pcie_width?: string // e.g., "x4"
|
||||||
|
pcie_max_gen?: string // Maximum supported PCIe generation
|
||||||
|
pcie_max_width?: string // Maximum supported PCIe lanes
|
||||||
|
sas_version?: string // e.g., "SAS-3"
|
||||||
|
sas_speed?: string // e.g., "12Gb/s"
|
||||||
|
link_speed?: string // Generic link speed info
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PCIDevice {
|
export interface PCIDevice {
|
||||||
@@ -201,4 +210,8 @@ export interface HardwareData {
|
|||||||
ups?: UPS | UPS[]
|
ups?: UPS | UPS[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
export const fetcher = async (url: string) => {
|
||||||
|
// Extract just the endpoint from the URL if it's a full URL
|
||||||
|
const endpoint = url.startsWith("http") ? new URL(url).pathname : url
|
||||||
|
return fetchApi(endpoint)
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -9,8 +9,9 @@
|
|||||||
|
|
||||||
ProxMenux Monitor is designed to support future updates where **actions can be triggered without using the terminal**, and managed through a **user-friendly interface** accessible across multiple formats and devices.
|
ProxMenux Monitor 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.
|
||||||
|
|
||||||

|
Access it at: **http://your-server-ip:8008**
|
||||||
|
|
||||||
|

|
||||||
- **New Banner Removal Method**
|
- **New Banner Removal Method**
|
||||||
A new function to disable the Proxmox subscription message with improved safety:
|
A new function to disable the Proxmox subscription message with improved safety:
|
||||||
- Creates a full backup before modifying any files
|
- Creates a full backup before modifying any files
|
||||||
|
|||||||
@@ -1,21 +1,34 @@
|
|||||||
MIT License
|
ProxMenux An Interactive Menu for Proxmox VE Management
|
||||||
|
Copyright (c) 2025 MacRimi
|
||||||
|
|
||||||
Copyright (c) 2024 MacRimi
|
This project is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License.
|
||||||
|
See the full license terms below.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
======================================================================
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
This is a human-readable summary of (and not a substitute for) the license.
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
You may obtain a copy of the full license at:
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
https://creativecommons.org/licenses/by-nc/4.0/
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
You are free to:
|
||||||
SOFTWARE.
|
- Share — copy and redistribute the material in any medium or format.
|
||||||
|
- Adapt — remix, transform, and build upon the material.
|
||||||
|
|
||||||
|
Under the following terms:
|
||||||
|
- Attribution — You must give appropriate credit, provide a link to the license,
|
||||||
|
and indicate if changes were made.
|
||||||
|
- NonCommercial — You may not use the material for commercial purposes.
|
||||||
|
|
||||||
|
No additional restrictions — You may not apply legal terms or technological
|
||||||
|
measures that legally restrict others from doing anything the license permits.
|
||||||
|
|
||||||
|
Disclaimer:
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|||||||
@@ -0,0 +1,512 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# **Análisis Completo del proyecto ProxMenux**
|
||||||
|
|
||||||
|
## **1. Estructura General del Proyecto**
|
||||||
|
|
||||||
|
### **Archivos Principales**
|
||||||
|
- **[install_proxmenux.sh](cci:7://file:///home/debian/src/ProxMenuxOffline/install_proxmenux.sh:0:0-0:0)**: Script de instalación principal (723 líneas)
|
||||||
|
- **[menu](cci:7://file:///home/debian/src/ProxMenuxOffline/menu:0:0-0:0)**: Script principal que se instala como comando del sistema (93 líneas)
|
||||||
|
- **[version.txt](cci:7://file:///home/debian/src/ProxMenuxOffline/version.txt:0:0-0:0)**: Control de versiones (actual: 1.1.7)
|
||||||
|
|
||||||
|
### **Directorios Principales**
|
||||||
|
```
|
||||||
|
ProxMenuxOffline/
|
||||||
|
├── scripts/ # 122 archivos de scripts bash
|
||||||
|
│ ├── menus/ # 13 scripts de menús
|
||||||
|
│ ├── lxc/ # 6 scripts para contenedores LXC
|
||||||
|
│ ├── vm/ # 13 scripts para máquinas virtuales
|
||||||
|
│ ├── storage/ # 9 scripts de almacenamiento
|
||||||
|
│ ├── share/ # 12 scripts para compartir recursos
|
||||||
|
│ ├── utilities/ # 6 utilidades del sistema
|
||||||
|
│ ├── global/ # 10 funciones comunes
|
||||||
|
│ ├── backup_restore/ # 6 scripts de respaldo
|
||||||
|
│ ├── post_install/ # 3 scripts post-instalación
|
||||||
|
│ └── gpu_tpu/ # Scripts para hardware gráfico
|
||||||
|
├── web/ # 136 archivos - Dashboard Next.js
|
||||||
|
├── AppImage/ # 54 archivos - ProxMenux Monitor
|
||||||
|
├── json/ # Archivos de caché de traducciones
|
||||||
|
├── lang/ # Archivos de idioma
|
||||||
|
├── guides/ # 5 guías de usuario
|
||||||
|
└── images/ # 7 imágenes del proyecto
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **2. Flujo de Instalación**
|
||||||
|
|
||||||
|
### **Script: [install_proxmenux.sh](cci:7://file:///home/debian/src/ProxMenuxOffline/install_proxmenux.sh:0:0-0:0)**
|
||||||
|
|
||||||
|
**Fase 1: Inicialización**
|
||||||
|
- Verifica permisos root (línea 716-719)
|
||||||
|
- Carga [utils.sh](cci:7://file:///home/debian/src/ProxMenuxOffline/scripts/utils.sh:0:0-0:0) desde GitHub (línea 54-57)
|
||||||
|
- Limpia archivos corruptos de configuración (línea 59-68)
|
||||||
|
|
||||||
|
**Fase 2: Detección de Instalación Existente**
|
||||||
|
- Función [check_existing_installation()](cci:1://file:///home/debian/src/ProxMenuxOffline/install_proxmenux.sh:70:0-106:1) (línea 71-107)
|
||||||
|
- Detecta 4 tipos: `none`, `normal`, `translation`, `unknown`
|
||||||
|
- Verifica entorno virtual Python en `/opt/googletrans-env`
|
||||||
|
- Verifica configuración de idioma en `/usr/local/share/proxmenux/config.json`
|
||||||
|
|
||||||
|
**Fase 3: Selección de Versión**
|
||||||
|
- **Versión Normal** (opción 1):
|
||||||
|
- Dependencias: `dialog`, `curl`, `jq`
|
||||||
|
- Solo inglés
|
||||||
|
- Más ligera y rápida
|
||||||
|
|
||||||
|
- **Versión con Traducción** (opción 2):
|
||||||
|
- Dependencias adicionales: `python3`, `python3-venv`, `python3-pip`
|
||||||
|
- Instala `googletrans==4.0.0-rc1` en entorno virtual
|
||||||
|
- Soporte multiidioma: en, es, fr, de, it, pt
|
||||||
|
- **Nota**: No compatible con Proxmox VE 9+ (línea 639-658)
|
||||||
|
|
||||||
|
**Fase 4: Instalación Normal** ([install_normal_version()](cci:1://file:///home/debian/src/ProxMenuxOffline/install_proxmenux.sh:402:0-484:1))
|
||||||
|
1. Instala dependencias básicas
|
||||||
|
2. Crea directorios:
|
||||||
|
- `/usr/local/bin` (ejecutables)
|
||||||
|
- `/usr/local/share/proxmenux` (archivos del sistema)
|
||||||
|
3. Descarga desde GitHub:
|
||||||
|
- `utils.sh` → `/usr/local/share/proxmenux/utils.sh`
|
||||||
|
- `menu` → `/usr/local/bin/menu`
|
||||||
|
- `version.txt` → `/usr/local/share/proxmenux/version.txt`
|
||||||
|
4. Instala ProxMenux Monitor (AppImage)
|
||||||
|
|
||||||
|
**Fase 5: Instalación con Traducción** (`install_translation_version()`)
|
||||||
|
- Pasos adicionales:
|
||||||
|
- Selector de idioma interactivo (línea 234-273)
|
||||||
|
- Crea entorno virtual Python en `/opt/googletrans-env`
|
||||||
|
- Instala googletrans con pip
|
||||||
|
- Descarga `cache.json` con traducciones precargadas
|
||||||
|
- Sistema de caché para reducir llamadas a la API de traducción
|
||||||
|
|
||||||
|
**Fase 6: ProxMenux Monitor**
|
||||||
|
- Descarga AppImage desde GitHub (línea 317-360)
|
||||||
|
- Verifica checksum SHA256 (línea 333-351)
|
||||||
|
- Crea servicio systemd `/etc/systemd/system/proxmenux-monitor.service`
|
||||||
|
- Puerto por defecto: 8008
|
||||||
|
- Se ejecuta como usuario root
|
||||||
|
- Auto-inicio en boot
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **3. Funcionamiento del Comando `menu`**
|
||||||
|
|
||||||
|
### **Script Principal: `/usr/local/bin/menu`**
|
||||||
|
|
||||||
|
**Flujo de Ejecución:**
|
||||||
|
|
||||||
|
1. **Carga de Configuración** (línea 33-44):
|
||||||
|
```bash
|
||||||
|
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
||||||
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
|
source "$UTILS_FILE"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Sistema de Traducción** (línea 89-92):
|
||||||
|
- Carga idioma desde `config.json`
|
||||||
|
- Inicializa caché de traducciones
|
||||||
|
- Función `translate()` en `utils.sh`
|
||||||
|
|
||||||
|
3. **Verificación de Actualizaciones** (línea 48-80):
|
||||||
|
- Compara versión local vs remota
|
||||||
|
- Prompt interactivo para actualizar
|
||||||
|
- Descarga y ejecuta nuevo `install_proxmenux.sh` si hay actualización
|
||||||
|
|
||||||
|
4. **Ejecución del Menú Principal** (línea 84-86):
|
||||||
|
```bash
|
||||||
|
exec bash <(curl -fsSL "$REPO_URL/scripts/menus/main_menu.sh")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Importante**: El comando `menu` **NO ejecuta scripts locales**, siempre descarga desde GitHub.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **4. Sistema de Menús**
|
||||||
|
|
||||||
|
### **Menú Principal: `scripts/menus/main_menu.sh`**
|
||||||
|
|
||||||
|
**Compatibilidad PVE 9** (línea 26-64):
|
||||||
|
- Detecta versión de Proxmox
|
||||||
|
- Si PVE 9+ y tiene traducciones instaladas → fuerza reinstalación en versión normal
|
||||||
|
- Previene errores de compatibilidad
|
||||||
|
|
||||||
|
**Opciones del Menú** (línea 97-111):
|
||||||
|
```
|
||||||
|
1. Settings post-install Proxmox → menu_post_install.sh
|
||||||
|
2. Hardware: GPUs and Coral-TPU → hw_grafics_menu.sh
|
||||||
|
3. Create VM from template → create_vm_menu.sh
|
||||||
|
4. Disk and Storage Manager → storage_menu.sh
|
||||||
|
5. Mount and Share Manager → share_menu.sh
|
||||||
|
6. Proxmox VE Helper Scripts → menu_Helper_Scripts.sh
|
||||||
|
7. Network Management → network_menu.sh
|
||||||
|
8. Utilities and Tools → utilities_menu.sh
|
||||||
|
h. Help and Info Commands → help_info_menu.sh
|
||||||
|
s. Settings → config_menu.sh
|
||||||
|
0. Exit
|
||||||
|
```
|
||||||
|
|
||||||
|
**Patrón de Ejecución**:
|
||||||
|
```bash
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/submenu.sh")
|
||||||
|
```
|
||||||
|
|
||||||
|
Todos los menús descargan y ejecutan scripts desde GitHub en tiempo real.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **5. Scripts Locales vs Remotos**
|
||||||
|
|
||||||
|
### **Estado Actual**
|
||||||
|
- **Scripts locales**: Están presentes en el repositorio (122 archivos)
|
||||||
|
- **Ejecución**: Siempre desde GitHub mediante `curl`
|
||||||
|
- **Ventaja actual**: Actualizaciones automáticas sin reinstalar
|
||||||
|
- **Desventaja**: Requiere conexión a internet constante
|
||||||
|
|
||||||
|
### **Scripts Principales Disponibles Localmente**
|
||||||
|
|
||||||
|
**Gestión de VMs** (`scripts/vm/`):
|
||||||
|
- `create_vm.sh` - Crear VMs
|
||||||
|
- `synology.sh` (39KB) - Instalación Synology DSM
|
||||||
|
- `zimaos.sh` (40KB) - Instalación ZimaOS
|
||||||
|
- `uupdump_creator.sh` - Creador de ISOs Windows
|
||||||
|
- `select_windows_iso.sh`, `select_linux_iso.sh`, `select_nas_iso.sh`
|
||||||
|
|
||||||
|
**Gestión de LXC** (`scripts/lxc/`):
|
||||||
|
- `lxc-manual-guide.sh` - Guía manual
|
||||||
|
- `lxc-privileged-to-unprivileged.sh`
|
||||||
|
- `lxc-unprivileged-to-privileged.sh`
|
||||||
|
|
||||||
|
**Almacenamiento** (`scripts/storage/`):
|
||||||
|
- `disk-passthrough.sh` - Passthrough disco a VM
|
||||||
|
- `disk-passthrough_ct.sh` - Passthrough disco a LXC (22KB)
|
||||||
|
- `import-disk-image.sh` - Importar imágenes
|
||||||
|
- `format-disk.sh`, `mount-disk-on-host.sh`
|
||||||
|
|
||||||
|
**Compartir Recursos** (`scripts/share/`):
|
||||||
|
- `lxc-mount-manager_minimal.sh` (35KB) - Gestión mount points
|
||||||
|
- `nfs_host.sh` (35KB) - Servidor NFS en host
|
||||||
|
- `samba_host.sh` (52KB) - Servidor Samba en host
|
||||||
|
- `nfs_client.sh`, `samba_client.sh` - Clientes en LXC
|
||||||
|
- `local-shared-manager.sh` - Directorios compartidos locales
|
||||||
|
|
||||||
|
**Post-Instalación** (`scripts/post_install/`):
|
||||||
|
- `auto_post_install.sh` (29KB) - Automatizado sin interacción
|
||||||
|
- `customizable_post_install.sh` (148KB) - Personalizable
|
||||||
|
- `uninstall-tools.sh` (34KB) - Desinstalador
|
||||||
|
|
||||||
|
**Utilidades** (`scripts/utilities/`):
|
||||||
|
- `upgrade_pve8_to_pve9.sh` (35KB) - Upgrade PVE 8→9
|
||||||
|
- `system_utils.sh` (20KB) - Instalador de utilidades
|
||||||
|
- `proxmox_update.sh` - Actualización de Proxmox
|
||||||
|
|
||||||
|
**Red** (`scripts/menus/network_menu.sh`):
|
||||||
|
- 43KB de funcionalidades de red
|
||||||
|
- Optimizaciones para LXC+NFS
|
||||||
|
|
||||||
|
**Global** (`scripts/global/`):
|
||||||
|
- `update-pve.sh`, `update-pve8.sh`, `update-pve9_2.sh`
|
||||||
|
- `remove-banner-pve8.sh`, `remove-banner-pve9.sh`
|
||||||
|
- `share-common.func` (30KB) - Funciones compartidas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **6. Sistema de Utilidades: `utils.sh`**
|
||||||
|
|
||||||
|
### **Funciones Principales**
|
||||||
|
|
||||||
|
**Interfaz Visual** (línea 50-71):
|
||||||
|
- Definición de colores ANSI
|
||||||
|
- Códigos de estilo para terminal
|
||||||
|
- Spinner animado (línea 75-88)
|
||||||
|
|
||||||
|
**Mensajes Estandarizados**:
|
||||||
|
- `msg_info()` - Info con spinner
|
||||||
|
- `msg_ok()` - Éxito (checkmark verde)
|
||||||
|
- `msg_error()` - Error (rojo)
|
||||||
|
- `msg_warn()` - Advertencia (amarillo)
|
||||||
|
- `msg_title()` - Títulos
|
||||||
|
- `type_text()` - Efecto máquina de escribir
|
||||||
|
|
||||||
|
**Sistema de Traducción** (línea 232-305):
|
||||||
|
```bash
|
||||||
|
translate() {
|
||||||
|
# Si idioma es "en" → retorna texto original
|
||||||
|
# Busca en caché local (cache.json)
|
||||||
|
# Si no existe → llama a googletrans vía Python
|
||||||
|
# Guarda en caché para futuras traducciones
|
||||||
|
# Limpia prefijos de contexto
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Contexto de Traducción** (línea 48):
|
||||||
|
```bash
|
||||||
|
TRANSLATION_CONTEXT="Context: Technical message for Proxmox and IT. Translate:"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logo ASCII** (línea 314-400):
|
||||||
|
- Dos versiones: terminal noVNC y SSH
|
||||||
|
- Detección automática del entorno
|
||||||
|
- Diseño en ASCII art con colores
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **7. ProxMenux Monitor**
|
||||||
|
|
||||||
|
### **Componente Web (AppImage)**
|
||||||
|
|
||||||
|
**Tecnología**:
|
||||||
|
- **Frontend**: Next.js 14, React 18, TypeScript
|
||||||
|
- **UI**: Radix UI + shadcn/ui + Tailwind CSS
|
||||||
|
- **Gráficos**: Recharts
|
||||||
|
- **Backend**: Flask (Python) para recolección de datos del sistema
|
||||||
|
- **Empaquetado**: AppImage (10.3 MB)
|
||||||
|
|
||||||
|
**Características**:
|
||||||
|
- Dashboard en tiempo real
|
||||||
|
- Monitoreo de CPU, RAM, temperatura
|
||||||
|
- Estado de VMs y LXC containers
|
||||||
|
- Gestión de almacenamiento visual
|
||||||
|
- Estadísticas de red
|
||||||
|
- Logs del sistema
|
||||||
|
- Tema oscuro/claro
|
||||||
|
- Responsive design
|
||||||
|
- Puerto: 8008
|
||||||
|
|
||||||
|
**Servicio Systemd**:
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=ProxMenux Monitor - Web Dashboard
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/usr/local/share/proxmenux
|
||||||
|
ExecStart=/usr/local/share/proxmenux/ProxMenux-Monitor.AppImage
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
Environment="PORT=8008"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Estado**: Se instala automáticamente en ambas versiones (normal y traducción)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **8. Sistema de Configuración**
|
||||||
|
|
||||||
|
### **Archivos de Configuración**
|
||||||
|
|
||||||
|
**`/usr/local/share/proxmenux/config.json`**:
|
||||||
|
- Estado de instalación de componentes
|
||||||
|
- Idioma seleccionado
|
||||||
|
- Timestamps de instalación
|
||||||
|
- Estados: `installed`, `already_installed`, `failed`
|
||||||
|
|
||||||
|
**Componentes Rastreados** (línea 201):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dialog": {"status": "installed", "timestamp": "..."},
|
||||||
|
"curl": {"status": "already_installed", "timestamp": "..."},
|
||||||
|
"jq": {"status": "installed", "timestamp": "..."},
|
||||||
|
"python3": {"status": "installed", "timestamp": "..."},
|
||||||
|
"virtual_environment": {"status": "created", "timestamp": "..."},
|
||||||
|
"googletrans": {"status": "installed", "timestamp": "..."},
|
||||||
|
"proxmenux_monitor": {"status": "installed", "timestamp": "..."},
|
||||||
|
"language": "es"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`/usr/local/share/proxmenux/cache.json`**:
|
||||||
|
- Traducciones cacheadas (100 KB)
|
||||||
|
- Formato: `{"texto_original": {"es": "traducción", "fr": "traduction"}}`
|
||||||
|
- Reduce llamadas a Google Translate API
|
||||||
|
|
||||||
|
**`/usr/local/share/proxmenux/installed_tools.json`**:
|
||||||
|
- Registro de herramientas post-instalación
|
||||||
|
- Usado por el desinstalador
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **9. Funcionalidades Destacadas**
|
||||||
|
|
||||||
|
### **Post-Instalación Automatizada**
|
||||||
|
- **Optimizaciones de repositorios**: Limpia duplicados, configura repos gratuitos
|
||||||
|
- **Eliminación de banner de suscripción**: Con respaldo y reversión
|
||||||
|
- **Optimización de memoria y kernel**: Ajustes según RAM disponible
|
||||||
|
- **Log2RAM**: Instalación automática en SSD/NVMe
|
||||||
|
- **Network tuning**: Optimización de stack de red
|
||||||
|
- **Límites del sistema**: Aumenta límites de archivos y procesos
|
||||||
|
- **Configuración de journald**: Ajustada para Log2RAM
|
||||||
|
- **Entropía**: Mejora generación de números aleatorios
|
||||||
|
- **Aliases bash**: Personalización del entorno
|
||||||
|
|
||||||
|
### **Gestión de Compartición de Recursos**
|
||||||
|
**Enfoque**: Mount Points LXC (Host ↔ Container)
|
||||||
|
- Detección automática de tipo de filesystem
|
||||||
|
- Mapeo UID/GID para contenedores unprivileged
|
||||||
|
- Visualización de mount points existentes
|
||||||
|
- Eliminación segura con verificación
|
||||||
|
|
||||||
|
**Configuraciones disponibles**:
|
||||||
|
- NFS: Host, Client LXC, Server LXC
|
||||||
|
- Samba: Host, Client LXC, Server LXC
|
||||||
|
- Directorios locales compartidos
|
||||||
|
|
||||||
|
### **Hardware Especializado**
|
||||||
|
- **Coral TPU**: Instalación de drivers compatible con PVE 8 y 9
|
||||||
|
- **GPUs**: Passthrough y configuración para VMs y LXC
|
||||||
|
- **iGPU**: Configuración para contenedores LXC
|
||||||
|
|
||||||
|
### **Upgrade PVE 8 → 9**
|
||||||
|
- Script de 35 KB con verificaciones exhaustivas
|
||||||
|
- Guía manual interactiva
|
||||||
|
- Checker de compatibilidad
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **10. Arquitectura de Ejecución**
|
||||||
|
|
||||||
|
### **Patrón de Descarga Dinámica**
|
||||||
|
|
||||||
|
**Todos los scripts siguen este patrón**:
|
||||||
|
```bash
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/path/to/script.sh")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ventajas**:
|
||||||
|
- ✅ Usuarios siempre tienen la última versión
|
||||||
|
- ✅ No requiere reinstalación para actualizaciones
|
||||||
|
- ✅ Hotfixes inmediatos
|
||||||
|
- ✅ Control centralizado de versiones
|
||||||
|
|
||||||
|
**Consideraciones**:
|
||||||
|
- ⚠️ Requiere internet en cada ejecución
|
||||||
|
- ⚠️ Dependencia de disponibilidad de GitHub
|
||||||
|
- ⚠️ No funciona offline
|
||||||
|
- ⚠️ Los scripts locales del repo no se usan directamente
|
||||||
|
|
||||||
|
### **Sistema de Versionado**
|
||||||
|
- `version.txt` en repo: versión remota
|
||||||
|
- `/usr/local/share/proxmenux/version.txt`: versión local instalada
|
||||||
|
- Check en cada ejecución del comando `menu`
|
||||||
|
- Prompt para actualizar si hay nueva versión
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **11. Flujo de Navegación**
|
||||||
|
|
||||||
|
```
|
||||||
|
Comando: menu
|
||||||
|
↓
|
||||||
|
Verifica actualizaciones
|
||||||
|
↓
|
||||||
|
Carga utils.sh y traducciones
|
||||||
|
↓
|
||||||
|
Descarga main_menu.sh desde GitHub
|
||||||
|
↓
|
||||||
|
Usuario selecciona opción
|
||||||
|
↓
|
||||||
|
Descarga submenu correspondiente desde GitHub
|
||||||
|
↓
|
||||||
|
Usuario selecciona acción
|
||||||
|
↓
|
||||||
|
Descarga y ejecuta script específico desde GitHub
|
||||||
|
↓
|
||||||
|
Retorna al menú anterior
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ejemplo de navegación**:
|
||||||
|
```
|
||||||
|
menu → main_menu.sh
|
||||||
|
→ opción 5: share_menu.sh
|
||||||
|
→ opción 4: lxc-mount-manager_minimal.sh (35KB)
|
||||||
|
→ Ejecuta acciones
|
||||||
|
→ Retorna a share_menu.sh
|
||||||
|
→ opción 0: Retorna a main_menu.sh
|
||||||
|
→ opción 0: Exit
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **12. Integración con Comunidad**
|
||||||
|
|
||||||
|
### **Scripts de la Comunidad Integrados**
|
||||||
|
|
||||||
|
**Proxmox VE Helper-Scripts**:
|
||||||
|
- Post-install script oficial
|
||||||
|
- Ejecutado desde: `https://github.com/community-scripts/ProxmoxVE`
|
||||||
|
|
||||||
|
**Xshok-proxmox** (fork):
|
||||||
|
- Post-install alternativo
|
||||||
|
- Descarga desde fork de MacRimi
|
||||||
|
|
||||||
|
**Elementos compartidos**:
|
||||||
|
- Funciones de `utils.sh` basadas en Helper-Scripts
|
||||||
|
- Misma filosofía de mensajes estandarizados
|
||||||
|
- Licencia MIT compatible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **13. Sistema de Desinstalación**
|
||||||
|
|
||||||
|
### **Función: `uninstall_proxmenux()`** (línea 109-161)
|
||||||
|
|
||||||
|
**Proceso**:
|
||||||
|
1. Confirmación interactiva (whiptail)
|
||||||
|
2. Desinstala googletrans y entorno virtual Python
|
||||||
|
3. Selector de dependencias a eliminar (python3, python3-venv, pip)
|
||||||
|
4. Elimina `/usr/local/bin/menu`
|
||||||
|
5. Elimina `/usr/local/share/proxmenux/`
|
||||||
|
6. Restaura `.bashrc` desde backup
|
||||||
|
7. Restaura `/etc/motd` desde backup
|
||||||
|
|
||||||
|
**Tool-specific uninstaller**: `scripts/post_install/uninstall-tools.sh`
|
||||||
|
- Lee `installed_tools.json`
|
||||||
|
- Permite desinstalar herramientas individualmente
|
||||||
|
- Restaura configuraciones originales
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **14. Estructura de Archivos JSON**
|
||||||
|
|
||||||
|
### **`json/cache.json`** (100 KB)
|
||||||
|
Traducciones precargadas para acelerar el sistema
|
||||||
|
|
||||||
|
### **`json/helpers_cache.json`** (273 KB)
|
||||||
|
Caché extendido, probablemente para Helper Scripts
|
||||||
|
|
||||||
|
### **`lang/cache.json`** (5.5 KB)
|
||||||
|
Caché de idiomas específico
|
||||||
|
|
||||||
|
### **`lang/en.lang`** y **`lang/es.lang`**
|
||||||
|
Archivos de idioma estáticos (4-5 KB cada uno)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **15. Resumen de Componentes**
|
||||||
|
|
||||||
|
| Componente | Ubicación | Función |
|
||||||
|
|------------|-----------|---------|
|
||||||
|
| **Instalador** | `install_proxmenux.sh` | Instalación inicial y actualizaciones |
|
||||||
|
| **Comando principal** | `/usr/local/bin/menu` | Punto de entrada del usuario |
|
||||||
|
| **Utilidades** | `/usr/local/share/proxmenux/utils.sh` | Funciones compartidas |
|
||||||
|
| **Configuración** | `/usr/local/share/proxmenux/config.json` | Estado del sistema |
|
||||||
|
| **Caché traducciones** | `/usr/local/share/proxmenux/cache.json` | Traducciones cacheadas |
|
||||||
|
| **Entorno Python** | `/opt/googletrans-env/` | Traducción (solo versión translation) |
|
||||||
|
| **Monitor** | `/usr/local/share/proxmenux/ProxMenux-Monitor.AppImage` | Dashboard web |
|
||||||
|
| **Servicio Monitor** | `/etc/systemd/system/proxmenux-monitor.service` | Servicio systemd |
|
||||||
|
| **Scripts** | GitHub (descarga dinámica) | Todos los scripts funcionales |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Conclusión**
|
||||||
|
|
||||||
|
ProxMenuxOffline es un **sistema modular de gestión de Proxmox VE** que utiliza una arquitectura híbrida:
|
||||||
|
|
||||||
|
- **Núcleo local**: Comando `menu`, utilidades, sistema de configuración
|
||||||
|
- **Scripts remotos**: Toda la funcionalidad se descarga dinámicamente desde GitHub
|
||||||
|
- **Dashboard web**: AppImage independiente con Next.js + Flask
|
||||||
|
- **Sistema de traducción**: Opcional, basado en Python + googletrans + caché
|
||||||
|
|
||||||
|
El proyecto tiene **122 scripts bash** en el repositorio local que **podrían ejecutarse localmente**, pero actualmente **todos se descargan desde GitHub en tiempo de ejecución**. Esta arquitectura prioriza mantener a los usuarios actualizados sobre la ejecución offline.
|
||||||
@@ -0,0 +1,699 @@
|
|||||||
|
# Scripts a Modificar para Ejecución 100% Local
|
||||||
|
|
||||||
|
**Fecha**: 2025-11-01
|
||||||
|
**Objetivo**: Eliminar dependencias de GitHub y permitir ejecución completamente local
|
||||||
|
**Repositorio**: ProxMenuxDotDeb
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resumen Ejecutivo
|
||||||
|
|
||||||
|
Para que ProxMenux funcione 100% localmente sin depender de GitHub, se deben modificar **47 archivos** en total:
|
||||||
|
|
||||||
|
- **2 archivos principales** (instalador y comando menu)
|
||||||
|
- **13 scripts de menús** (sistema de navegación)
|
||||||
|
- **32 scripts funcionales** (operaciones específicas)
|
||||||
|
|
||||||
|
**Cambios principales**:
|
||||||
|
1. Cambiar `REPO_URL` de GitHub a rutas locales del sistema
|
||||||
|
2. Reemplazar descargas `curl` por ejecución de scripts locales
|
||||||
|
3. Copiar todos los scripts a `/usr/local/share/proxmenux/scripts/` durante instalación
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Archivos Principales (CRÍTICOS) ⚠️
|
||||||
|
|
||||||
|
### 1.1. `install_proxmenux.sh` (Raíz del repositorio)
|
||||||
|
|
||||||
|
**Líneas a modificar**:
|
||||||
|
- **Línea 37**: `REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"`
|
||||||
|
- **Línea 38**: `UTILS_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main/scripts/utils.sh"`
|
||||||
|
- **Línea 54-57**: Carga de `utils.sh` con curl
|
||||||
|
- **Línea 459-476**: Descarga de archivos con wget (versión normal)
|
||||||
|
- **Línea 583-603**: Descarga de archivos con wget (versión traducción)
|
||||||
|
|
||||||
|
**Cambios necesarios**:
|
||||||
|
```bash
|
||||||
|
# Cambiar URLs a rutas locales
|
||||||
|
REPO_URL="/usr/local/share/proxmenux"
|
||||||
|
UTILS_URL="./scripts/utils.sh"
|
||||||
|
|
||||||
|
# Reemplazar wget por cp
|
||||||
|
# En lugar de descargar, copiar archivos locales del repositorio
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impacto**: 🔴 CRÍTICO - Sin esto, la instalación falla completamente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2. `menu` (Raíz del repositorio)
|
||||||
|
|
||||||
|
**Líneas a modificar**:
|
||||||
|
- **Línea 34**: `REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"`
|
||||||
|
- **Línea 52**: Verificación de actualizaciones (curl remoto)
|
||||||
|
- **Línea 72**: Descarga de instalador actualizado
|
||||||
|
- **Línea 85**: `exec bash <(curl -fsSL "$REPO_URL/scripts/menus/main_menu.sh")`
|
||||||
|
|
||||||
|
**Cambios necesarios**:
|
||||||
|
```bash
|
||||||
|
# Cambiar a ruta local
|
||||||
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
|
|
||||||
|
# Ejecutar localmente
|
||||||
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impacto**: 🔴 CRÍTICO - Es el punto de entrada del usuario
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Scripts de Menús (13 archivos)
|
||||||
|
|
||||||
|
### 2.1. `scripts/menus/main_menu.sh` ⭐
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 14**: `REPO_URL`
|
||||||
|
- **Línea 57**: curl para reinstalación PVE9
|
||||||
|
- **Líneas 125-135**: Todas las opciones del menú (12 líneas)
|
||||||
|
|
||||||
|
**Comandos a reemplazar**:
|
||||||
|
```bash
|
||||||
|
# Todas estas líneas:
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/menu_post_install.sh")
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/hw_grafics_menu.sh")
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/create_vm_menu.sh")
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/storage_menu.sh")
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/share_menu.sh")
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/menu_Helper_Scripts.sh")
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh")
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/help_info_menu.sh")
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2. `scripts/menus/menu_post_install.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 12**: `REPO_URL`
|
||||||
|
- **Línea 73**: `bash <(curl -s $REPO_URL/scripts/post_install/auto_post_install.sh)`
|
||||||
|
- **Línea 171**: `exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")`
|
||||||
|
|
||||||
|
**Nota**: Mantener URLs remotas para scripts de comunidad externa (líneas 90-91)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3. `scripts/menus/config_menu.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 13**: `REPO_URL`
|
||||||
|
- No tiene llamadas curl ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4. `scripts/menus/create_vm_menu.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 13**: `REPO_URL`
|
||||||
|
- Múltiples `exec bash <(curl -s ...)` en opciones del menú
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5. `scripts/menus/hw_grafics_menu.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 13**: `REPO_URL`
|
||||||
|
- **Líneas 38, 44, 50, 55, 56**: Llamadas curl
|
||||||
|
|
||||||
|
**Comandos a reemplazar**:
|
||||||
|
```bash
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/configure_igpu_lxc.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/install_coral_lxc.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/gpu_tpu/install_coral_pve9.sh")
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.6. `scripts/menus/lxc_menu.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 13**: `REPO_URL`
|
||||||
|
- Todos los `exec bash <(curl ...)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.7. `scripts/menus/menu_Helper_Scripts.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 13**: `REPO_URL`
|
||||||
|
- **Línea 296**: `exec bash <(curl -s ...)`
|
||||||
|
|
||||||
|
**Nota**: Mantener URLs de Helper-Scripts externos (comunidad)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.8. `scripts/menus/network_menu.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 13**: `REPO_URL`
|
||||||
|
- **Línea 1085**: `exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.9. `scripts/menus/share_menu.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 13**: `REPO_URL`
|
||||||
|
- **Líneas 46, 55-82, 85**: 11 llamadas curl
|
||||||
|
|
||||||
|
**Comandos a reemplazar**:
|
||||||
|
```bash
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/share/nfs_host.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/share/samba_host.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/share/local-shared-manager.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/share/lxc-mount-manager_minimal.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/share/nfs_client.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/share/samba_client.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/share/nfs_lxc_server.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/share/samba_lxc_server.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/share/commands_share.sh")
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") # 2 veces
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.10. `scripts/menus/storage_menu.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 15**: `REPO_URL`
|
||||||
|
- **Líneas 39, 42, 45, 48, 51**: 5 llamadas curl
|
||||||
|
|
||||||
|
**Comandos a reemplazar**:
|
||||||
|
```bash
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough_ct.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/storage/import-disk-image.sh")
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") # 2 veces
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.11. `scripts/menus/utilities_menu.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 15**: `REPO_URL`
|
||||||
|
- **Líneas 39, 45, 67, 74, 79, 80**: 6 llamadas curl
|
||||||
|
|
||||||
|
**Comandos a reemplazar**:
|
||||||
|
```bash
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/utilities/uup_dump_iso_creator.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/utilities/system_utils.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/utilities/proxmox_update.sh")
|
||||||
|
bash <(curl -s "$REPO_URL/scripts/utilities/upgrade_pve8_to_pve9.sh")
|
||||||
|
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") # 2 veces
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.12. `scripts/menus/main_menu_.sh`
|
||||||
|
|
||||||
|
**Modificaciones**: Igual que `main_menu.sh` (archivo alternativo/backup)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.13. `scripts/menus/sm.sh`
|
||||||
|
|
||||||
|
**Modificaciones**: Igual que `share_menu.sh` (archivo alternativo)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Scripts Post-Instalación (3 archivos)
|
||||||
|
|
||||||
|
### 3.1. `scripts/post_install/auto_post_install.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 39**: `REPO_URL`
|
||||||
|
- **Línea 110**: `bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve9_2.sh")`
|
||||||
|
- **Línea 113**: `bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve8.sh")`
|
||||||
|
- **Línea 150**: `bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve-v3.sh")`
|
||||||
|
- **Línea 157**: `bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve8.sh")`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2. `scripts/post_install/customizable_post_install.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 39**: `REPO_URL`
|
||||||
|
- **Línea 197**: `bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve9_2.sh")`
|
||||||
|
- **Línea 200**: `bash <(curl -fsSL "$REPO_URL/scripts/global/update-pve8.sh")`
|
||||||
|
- **Línea 2905**: `bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve-v3.sh")`
|
||||||
|
- **Línea 2908**: `bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve8.sh")`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3. `scripts/post_install/uninstall-tools.sh`
|
||||||
|
|
||||||
|
**Modificaciones**: Solo lectura de configs locales ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Scripts de VMs (8 archivos)
|
||||||
|
|
||||||
|
### 4.1. `scripts/vm/create_vm.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 29**: `REPO_URL`
|
||||||
|
- **Líneas 30-32**: `VM_REPO`, `ISO_REPO`, `MENU_REPO`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2. `scripts/vm/select_linux_iso.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 28**: `REPO_URL`
|
||||||
|
- **Línea 222**: `exec bash <(curl -s "$REPO_URL/scripts/vm/create_vm.sh")`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.3. `scripts/vm/select_windows_iso.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 27**: `REPO_URL`
|
||||||
|
- **Línea 28**: `UUP_REPO`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.4. `scripts/vm/select_nas_iso.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 31**: `REPO_URL`
|
||||||
|
- **Línea 65**: `bash <(curl -s "$REPO_URL/scripts/vm/synology.sh")`
|
||||||
|
- **Línea 106**: `bash <(curl -s "$REPO_URL/scripts/vm/zimaos.sh")`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.5. `scripts/vm/synology.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 32**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.6. `scripts/vm/synology_.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 32**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.7. `scripts/vm/zimaos.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- Verificar si tiene `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.8. `scripts/vm/vm_creator.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 497**: `bash <(curl -fsSL "$REPO_URL/scripts/menus/create_vm_menu.sh")`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Scripts de LXC (4 archivos)
|
||||||
|
|
||||||
|
### 5.1. `scripts/lxc/lxc-manual-guide.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 14**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2. `scripts/lxc/lxc-privileged-to-unprivileged.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 18**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3. `scripts/lxc/lxc-unprivileged-to-privileged.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 19**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.4. `scripts/lxc/lxc-mount-manager_minimal.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- Verificar si tiene `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Scripts de Compartir Recursos (9 archivos)
|
||||||
|
|
||||||
|
### 6.1. `scripts/share/nfs_host.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 16**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6.2. `scripts/share/nfs_client.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 16**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6.3. `scripts/share/nfs_lxc_server.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 16**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6.4. `scripts/share/samba_host.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 16**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6.5. `scripts/share/samba_client.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 18**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6.6. `scripts/share/samba_lxc_server.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 16**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6.7. `scripts/share/local-shared-manager.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 13**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6.8. `scripts/share/lxc-mount-manager_minimal.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- Verificar `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6.9. `scripts/share/commands_share.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 14**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Scripts de Almacenamiento (3 archivos)
|
||||||
|
|
||||||
|
### 7.1. `scripts/storage/disk-passthrough.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- Verificar `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7.2. `scripts/storage/disk-passthrough_ct.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- Verificar `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7.3. `scripts/storage/import-disk-image.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 30**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Scripts de Utilidades (4 archivos)
|
||||||
|
|
||||||
|
### 8.1. `scripts/utilities/upgrade_pve8_to_pve9.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- Verificar `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.2. `scripts/utilities/system_utils.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- Verificar `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.3. `scripts/utilities/proxmox_update.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- Verificar `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.4. `scripts/utilities/uup_dump_iso_creator.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- Verificar `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Scripts Globales (3 archivos)
|
||||||
|
|
||||||
|
### 9.1. `scripts/global/update-pve.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 32**: `source <(curl -s "$REPO_URL/scripts/global/common-functions.sh")`
|
||||||
|
|
||||||
|
**Cambiar a**:
|
||||||
|
```bash
|
||||||
|
source "$LOCAL_SCRIPTS/global/common-functions.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9.2. `scripts/global/update-pve8.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 32**: `source <(curl -s "$REPO_URL/scripts/global/common-functions.sh")`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9.3. `scripts/global/update-pve9_2.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 32**: `source <(curl -s "$REPO_URL/scripts/global/common-functions.sh")`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Scripts de Hardware (2 archivos)
|
||||||
|
|
||||||
|
### 10.1. `scripts/configure_igpu_lxc.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 19**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10.2. `scripts/install_coral_lxc.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 25**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Scripts de Red (2 archivos)
|
||||||
|
|
||||||
|
### 11.1. `scripts/repair_network.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 204**: `exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")`
|
||||||
|
- **Línea 205**: `exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.2. `scripts/telegram-notifier.sh`
|
||||||
|
|
||||||
|
**Modificaciones**:
|
||||||
|
- **Línea 5**: `REPO_URL`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Scripts Duplicados/Alternos (en `scripts/auto_post_install.sh`)
|
||||||
|
|
||||||
|
**Modificaciones**: Igual que `scripts/post_install/auto_post_install.sh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tabla Resumen
|
||||||
|
|
||||||
|
| Categoría | Archivos | Modificaciones Principales |
|
||||||
|
|-----------|----------|---------------------------|
|
||||||
|
| **Principales** | 2 | REPO_URL + curl → rutas locales |
|
||||||
|
| **Menús** | 13 | REPO_URL + exec bash curl |
|
||||||
|
| **Post-Install** | 3 | bash curl a scripts global |
|
||||||
|
| **VMs** | 8 | REPO_URL + llamadas remotas |
|
||||||
|
| **LXC** | 4 | REPO_URL |
|
||||||
|
| **Share** | 9 | REPO_URL |
|
||||||
|
| **Storage** | 3 | REPO_URL |
|
||||||
|
| **Utilities** | 4 | REPO_URL |
|
||||||
|
| **Global** | 3 | source curl |
|
||||||
|
| **Hardware** | 2 | REPO_URL |
|
||||||
|
| **Red** | 2 | exec bash curl |
|
||||||
|
| **TOTAL** | **47** | **~150-200 líneas** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plan de Implementación Recomendado
|
||||||
|
|
||||||
|
### Paso 1: Preparación
|
||||||
|
```bash
|
||||||
|
# Crear backup
|
||||||
|
cp -r . ../ProxMenuxDotDeb_backup
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Documentar información relevante del proyecto en directorio "docs"
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 2: Modificación Automática Global
|
||||||
|
```bash
|
||||||
|
# Script de conversión masiva
|
||||||
|
find . -name "*.sh" -o -name "menu" | xargs sed -i \
|
||||||
|
's|REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"|LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"|g'
|
||||||
|
|
||||||
|
# Cambiar referencias
|
||||||
|
find . -name "*.sh" -o -name "menu" | xargs sed -i \
|
||||||
|
's|\$REPO_URL/scripts|\$LOCAL_SCRIPTS|g'
|
||||||
|
|
||||||
|
# Cambiar bash curl
|
||||||
|
find . -name "*.sh" -o -name "menu" | xargs sed -i -E \
|
||||||
|
's|bash <\(curl -[sfSL]+ "\$REPO_URL/([^"]+)"|bash "\$LOCAL_SCRIPTS/\1"|g'
|
||||||
|
|
||||||
|
# Cambiar exec bash curl
|
||||||
|
find . -name "*.sh" -o -name "menu" | xargs sed -i -E \
|
||||||
|
's|exec bash <\(curl -[sfSL]+ "\$REPO_URL/([^"]+)"|exec bash "\$LOCAL_SCRIPTS/\1"|g'
|
||||||
|
|
||||||
|
# Cambiar source curl
|
||||||
|
find . -name "*.sh" | xargs sed -i -E \
|
||||||
|
's|source <\(curl -[sfSL]+ "\$REPO_URL/([^"]+)"|source "\$LOCAL_SCRIPTS/\1"|g'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 3: Modificar install_proxmenux.sh manualmente
|
||||||
|
|
||||||
|
Cambiar secciones de descarga wget por copias locales:
|
||||||
|
```bash
|
||||||
|
# En lugar de:
|
||||||
|
wget -qO "$dest" "$url"
|
||||||
|
|
||||||
|
# Usar:
|
||||||
|
cp "./scripts/utils.sh" "$UTILS_FILE"
|
||||||
|
cp "./menu" "$INSTALL_DIR/$MENU_SCRIPT"
|
||||||
|
cp "./version.txt" "$LOCAL_VERSION_FILE"
|
||||||
|
```
|
||||||
|
|
||||||
|
Agregar copia de todos los scripts:
|
||||||
|
```bash
|
||||||
|
msg_info "Copying local scripts..."
|
||||||
|
mkdir -p "$BASE_DIR/scripts"
|
||||||
|
cp -r "./scripts/"* "$BASE_DIR/scripts/"
|
||||||
|
chmod -R +x "$BASE_DIR/scripts/"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 4: Modificar comando menu
|
||||||
|
|
||||||
|
Comentar o modificar verificación de actualizaciones remotas.
|
||||||
|
|
||||||
|
### Paso 5: Validación
|
||||||
|
```bash
|
||||||
|
# Verificar que no queden referencias remotas
|
||||||
|
grep -r "githubusercontent.com" . --include="*.sh" --include="menu"
|
||||||
|
|
||||||
|
# Verificar llamadas curl
|
||||||
|
grep -r "curl.*REPO_URL" . --include="*.sh" --include="menu"
|
||||||
|
|
||||||
|
# Contar archivos modificados
|
||||||
|
grep -r "LOCAL_SCRIPTS=" . --include="*.sh" --include="menu" | wc -l
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estructura Post-Modificación
|
||||||
|
|
||||||
|
```
|
||||||
|
/usr/local/share/proxmenux/
|
||||||
|
├── utils.sh
|
||||||
|
├── config.json
|
||||||
|
├── cache.json
|
||||||
|
├── version.txt
|
||||||
|
├── ProxMenux-Monitor.AppImage
|
||||||
|
└── scripts/ # ⭐ NUEVO
|
||||||
|
├── menus/
|
||||||
|
│ ├── main_menu.sh
|
||||||
|
│ ├── menu_post_install.sh
|
||||||
|
│ └── ...
|
||||||
|
├── post_install/
|
||||||
|
├── vm/
|
||||||
|
├── lxc/
|
||||||
|
├── storage/
|
||||||
|
├── share/
|
||||||
|
├── utilities/
|
||||||
|
├── global/
|
||||||
|
└── gpu_tpu/
|
||||||
|
|
||||||
|
/usr/local/bin/
|
||||||
|
└── menu
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consideraciones Especiales
|
||||||
|
|
||||||
|
### Scripts Externos de la Comunidad
|
||||||
|
Mantener URLs remotas para:
|
||||||
|
- Proxmox VE Helper-Scripts (community-scripts)
|
||||||
|
- xshok-proxmox scripts
|
||||||
|
|
||||||
|
### ProxMenux Monitor
|
||||||
|
El AppImage se mantiene descargable desde GitHub durante la instalación inicial (10 MB).
|
||||||
|
|
||||||
|
### Sistema de Actualizaciones
|
||||||
|
Opciones:
|
||||||
|
1. Deshabilitar completamente
|
||||||
|
2. Mostrar mensaje para ejecutar `install_proxmenux.sh` manualmente
|
||||||
|
3. Sistema híbrido (check opcional remoto)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist de Validación
|
||||||
|
|
||||||
|
- [ok] Backup completo del repositorio
|
||||||
|
- [ok] Conversión automática ejecutada
|
||||||
|
- [ok] `install_proxmenux.sh` modificado
|
||||||
|
- [ok] `menu` modificado
|
||||||
|
- [ip] Scripts de menús verificados
|
||||||
|
- [ ] Sin referencias a githubusercontent.com
|
||||||
|
- [ ] Sin llamadas curl a REPO_URL
|
||||||
|
- [ ] Instalación local funcional
|
||||||
|
- [ ] Navegación por todos los menús OK
|
||||||
|
- [ ] Ejecución offline confirmada
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total de archivos a modificar**: 47
|
||||||
|
**Líneas estimadas**: ~150-200
|
||||||
|
**Tiempo estimado**: 2-4 horas
|
||||||
|
**Riesgo**: Medio (requiere testing)
|
||||||
|
**Beneficio**: Sistema completamente offline
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 323 KiB |
Regular → Executable
+476
-150
@@ -1,41 +1,46 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
# ProxMenux - A menu-driven toolkit for Proxmox VE management
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Contributors : cod378
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# Subproject : ProxMenux Monitor (System Health & Web Dashboard)
|
||||||
# Version : 1.3
|
# Copyright : (c) 2024-2025 MacRimi
|
||||||
# Last Updated: 04/07/2025
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
|
# Version : 1.4
|
||||||
|
# Last Updated : 12/11/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Description:
|
# Description:
|
||||||
# This script installs and configures ProxMenux, a menu-driven
|
# This script installs and configures ProxMenux, a menu-driven
|
||||||
# tool for managing Proxmox VE.
|
# toolkit for managing and optimizing Proxmox VE servers.
|
||||||
#
|
#
|
||||||
# - Ensures the script is run with root privileges.
|
# - Ensures the script is run with root privileges.
|
||||||
# - Displays an installation confirmation prompt.
|
# - Displays an installation confirmation prompt.
|
||||||
# - Installs required dependencies:
|
# - Installs required dependencies:
|
||||||
# - whiptail (for interactive terminal menus)
|
# • whiptail (interactive terminal menus)
|
||||||
# - curl (for downloading remote files)
|
# • curl (downloads and connectivity checks)
|
||||||
# - jq (for handling JSON data)
|
# • jq (JSON parsing)
|
||||||
# - Python 3 and virtual environment (for translations)
|
# • Python 3 + venv (for translation support)
|
||||||
# - Configures the Python virtual environment and installs googletrans.
|
# - Creates the ProxMenux base directories and configuration files:
|
||||||
# - Creates necessary directories for storing ProxMenux data.
|
# • $BASE_DIR/config.json
|
||||||
# - Downloads required files from GitHub, including:
|
# • $BASE_DIR/cache.json
|
||||||
# - Cache file (`cache.json`) for translation caching.
|
# - Copies local project files into the target paths (offline mode by default):
|
||||||
# - Utility script (`utils.sh`) for core functions.
|
# • scripts/* → $BASE_DIR/scripts/
|
||||||
# - Main script (`menu.sh`) to launch ProxMenux.
|
# • utils.sh → $BASE_DIR/scripts/utils.sh
|
||||||
# - Sets correct permissions for execution.
|
# • menu → $INSTALL_DIR/menu (main launcher)
|
||||||
# - Displays final instructions on how to start ProxMenux.
|
# • install_proxmenux.sh → $BASE_DIR/install_proxmenux.sh
|
||||||
|
# - Sets correct permissions for all executables.
|
||||||
|
# - Displays the final instruction on how to start ProxMenux ("menu").
|
||||||
#
|
#
|
||||||
# This installer ensures a smooth setup process and prepares
|
# Notes:
|
||||||
# the system for running ProxMenux efficiently.
|
# - This installer supports both offline and online setups.
|
||||||
|
# - ProxMenux Monitor can be installed later as an optional module
|
||||||
|
# to provide real-time system monitoring and a web dashboard.
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
UTILS_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main/scripts/utils.sh"
|
|
||||||
INSTALL_DIR="/usr/local/bin"
|
INSTALL_DIR="/usr/local/bin"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
CONFIG_FILE="$BASE_DIR/config.json"
|
CONFIG_FILE="$BASE_DIR/config.json"
|
||||||
@@ -45,17 +50,222 @@ LOCAL_VERSION_FILE="$BASE_DIR/version.txt"
|
|||||||
MENU_SCRIPT="menu"
|
MENU_SCRIPT="menu"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|
||||||
MONITOR_APPIMAGE_URL="https://github.com/MacRimi/ProxMenux/raw/refs/heads/main/AppImage/ProxMenux-1.0.0.AppImage"
|
MONITOR_INSTALL_DIR="$BASE_DIR"
|
||||||
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_SERVICE_FILE="/etc/systemd/system/proxmenux-monitor.service"
|
||||||
MONITOR_PORT=8008
|
MONITOR_PORT=8008
|
||||||
|
|
||||||
if ! source <(curl -sSf "$UTILS_URL"); then
|
# Offline installer envs
|
||||||
echo "Error: Could not load utils.sh from $UTILS_URL"
|
REPO_URL="https://github.com/MacRimi/ProxMenux.git"
|
||||||
exit 1
|
TEMP_DIR="/tmp/proxmenux-install-$$"
|
||||||
|
|
||||||
|
# Load utility functions
|
||||||
|
NEON_PURPLE_BLUE="\033[38;5;99m"
|
||||||
|
WHITE="\033[38;5;15m"
|
||||||
|
RESET="\033[0m"
|
||||||
|
DARK_GRAY="\033[38;5;244m"
|
||||||
|
ORANGE="\033[38;5;208m"
|
||||||
|
YW="\033[33m"
|
||||||
|
YWB="\033[1;33m"
|
||||||
|
GN="\033[1;92m"
|
||||||
|
RD="\033[01;31m"
|
||||||
|
CL="\033[m"
|
||||||
|
BL="\033[36m"
|
||||||
|
DGN="\e[32m"
|
||||||
|
BGN="\e[1;32m"
|
||||||
|
DEF="\e[1;36m"
|
||||||
|
CUS="\e[38;5;214m"
|
||||||
|
BOLD="\033[1m"
|
||||||
|
BFR="\\r\\033[K"
|
||||||
|
HOLD="-"
|
||||||
|
BOR=" | "
|
||||||
|
CM="${GN}✓ ${CL}"
|
||||||
|
TAB=" "
|
||||||
|
|
||||||
|
|
||||||
|
# Create and display spinner
|
||||||
|
spinner() {
|
||||||
|
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||||||
|
local spin_i=0
|
||||||
|
local interval=0.1
|
||||||
|
printf "\e[?25l"
|
||||||
|
|
||||||
|
local color="${YW}"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
printf "\r ${color}%s${CL}" "${frames[spin_i]}"
|
||||||
|
spin_i=$(( (spin_i + 1) % ${#frames[@]} ))
|
||||||
|
sleep "$interval"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Function to simulate typing effect
|
||||||
|
type_text() {
|
||||||
|
local text="$1"
|
||||||
|
local delay=0.05
|
||||||
|
for ((i=0; i<${#text}; i++)); do
|
||||||
|
echo -n "${text:$i:1}"
|
||||||
|
sleep $delay
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Display info message with spinner
|
||||||
|
msg_info() {
|
||||||
|
local msg="$1"
|
||||||
|
echo -ne "${TAB}${YW}${HOLD}${msg}"
|
||||||
|
spinner &
|
||||||
|
SPINNER_PID=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Display info2 message
|
||||||
|
msg_info2() {
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${TAB}${BOLD}${YW}${HOLD}${msg}${CL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Display title script
|
||||||
|
msg_title() {
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "\n"
|
||||||
|
echo -e "${TAB}${BOLD}${HOLD}${BOR}${msg}${BOR}${HOLD}${CL}"
|
||||||
|
echo -e "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Display warning or highlighted information message
|
||||||
|
msg_warn() {
|
||||||
|
if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then
|
||||||
|
kill $SPINNER_PID > /dev/null
|
||||||
|
fi
|
||||||
|
printf "\e[?25h"
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${BFR}${TAB}${CL} ${YWB}${msg}${CL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Display success message
|
||||||
|
msg_ok() {
|
||||||
|
if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then
|
||||||
|
kill $SPINNER_PID > /dev/null
|
||||||
|
fi
|
||||||
|
printf "\e[?25h"
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${BFR}${TAB}${CM}${GN}${msg}${CL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Display error message
|
||||||
|
msg_error() {
|
||||||
|
if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then
|
||||||
|
kill $SPINNER_PID > /dev/null
|
||||||
|
fi
|
||||||
|
printf "\e[?25h"
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${BFR}${TAB}${RD}[ERROR] ${msg}${CL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
show_proxmenux_logo() {
|
||||||
|
clear
|
||||||
|
|
||||||
|
if [[ -z "$SSH_TTY" && -z "$(who am i | awk '{print $NF}' | grep -E '([0-9]{1,3}\.){3}[0-9]{1,3}')" ]]; then
|
||||||
|
|
||||||
|
# Logo for terminal noVNC
|
||||||
|
|
||||||
|
LOGO=$(cat << "EOF"
|
||||||
|
\e[0m\e[38;2;61;61;61m▆\e[38;2;60;60;60m▄\e[38;2;54;54;54m▂\e[0m \e[38;2;0;0;0m \e[0m \e[38;2;54;54;54m▂\e[38;2;60;60;60m▄\e[38;2;61;61;61m▆\e[0m
|
||||||
|
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[38;2;61;61;61;48;2;37;37;37m▇\e[0m\e[38;2;60;60;60m▅\e[38;2;56;56;56m▃\e[38;2;37;37;37m▁ \e[38;2;36;36;36m▁\e[38;2;56;56;56m▃\e[38;2;60;60;60m▅\e[38;2;61;61;61;48;2;37;37;37m▇\e[48;2;62;62;62m \e[0m\e[7m\e[38;2;60;60;60m▁\e[0m
|
||||||
|
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[7m\e[38;2;61;61;61m▂\e[0m\e[38;2;62;62;62;48;2;61;61;61m┈\e[48;2;62;62;62m \e[48;2;61;61;61m┈\e[0m\e[38;2;60;60;60m▆\e[38;2;57;57;57m▄\e[38;2;48;48;48m▂\e[0m \e[38;2;47;47;47m▂\e[38;2;57;57;57m▄\e[38;2;60;60;60m▆\e[38;2;62;62;62;48;2;61;61;61m┈\e[48;2;62;62;62m \e[48;2;61;61;61m┈\e[0m\e[7m\e[38;2;60;60;60m▂\e[38;2;57;57;57m▄\e[38;2;47;47;47m▆\e[0m \e[0m
|
||||||
|
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[7m\e[38;2;39;39;39m▇\e[38;2;57;57;57m▅\e[38;2;60;60;60m▃\e[0m\e[38;2;40;40;40;48;2;61;61;61m▁\e[48;2;62;62;62m \e[38;2;54;54;54;48;2;61;61;61m┊\e[48;2;62;62;62m \e[38;2;39;39;39;48;2;61;61;61m▁\e[0m\e[7m\e[38;2;60;60;60m▃\e[38;2;57;57;57m▅\e[38;2;38;38;38m▇\e[0m \e[38;2;193;60;2m▃\e[38;2;217;67;2m▅\e[38;2;225;70;2m▇\e[0m
|
||||||
|
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[0m \e[38;2;203;63;2m▄\e[38;2;147;45;1m▂\e[0m \e[7m\e[38;2;55;55;55m▆\e[38;2;60;60;60m▄\e[38;2;61;61;61m▂\e[38;2;60;60;60m▄\e[38;2;55;55;55m▆\e[0m \e[38;2;144;44;1m▂\e[38;2;202;62;2m▄\e[38;2;219;68;2m▆\e[38;2;231;72;3;48;2;226;70;2m┈\e[48;2;231;72;3m \e[48;2;225;70;2m▉\e[0m
|
||||||
|
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[7m\e[38;2;121;37;1m▉\e[0m\e[38;2;0;0;0;48;2;231;72;3m \e[0m\e[38;2;221;68;2m▇\e[38;2;208;64;2m▅\e[38;2;212;66;2m▂\e[38;2;123;37;0m▁\e[38;2;211;65;2m▂\e[38;2;207;64;2m▅\e[38;2;220;68;2m▇\e[48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m┈\e[0m\e[7m\e[38;2;221;68;2m▂\e[0m\e[38;2;44;13;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
|
||||||
|
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[0m \e[7m\e[38;2;190;59;2m▅\e[38;2;216;67;2m▃\e[38;2;225;70;2m▁\e[0m\e[38;2;95;29;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;230;71;2m┈\e[48;2;231;72;3m \e[0m\e[7m\e[38;2;225;70;2m▁\e[38;2;216;67;2m▃\e[38;2;191;59;2m▅\e[0m \e[38;2;0;0;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
|
||||||
|
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏ \e[0m \e[7m\e[38;2;172;53;1m▆\e[38;2;213;66;2m▄\e[38;2;219;68;2m▂\e[38;2;213;66;2m▄\e[38;2;174;54;2m▆\e[0m \e[38;2;0;0;0m \e[0m \e[38;2;0;0;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
|
||||||
|
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏ \e[0m \e[38;2;0;0;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
|
||||||
|
\e[7m\e[38;2;52;52;52m▆\e[38;2;59;59;59m▄\e[38;2;61;61;61m▂\e[0m\e[38;2;31;31;31m▏ \e[0m \e[7m\e[38;2;228;71;2m▂\e[38;2;221;69;2m▄\e[38;2;196;60;2m▆\e[0m
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
TEXT=(
|
||||||
|
""
|
||||||
|
""
|
||||||
|
"${BOLD}ProxMenux${RESET}"
|
||||||
|
""
|
||||||
|
"${BOLD}${NEON_PURPLE_BLUE}An Interactive Menu for${RESET}"
|
||||||
|
"${BOLD}${NEON_PURPLE_BLUE}Proxmox VE management${RESET}"
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
mapfile -t logo_lines <<< "$LOGO"
|
||||||
|
|
||||||
|
for i in {0..9}; do
|
||||||
|
echo -e "${TAB}${logo_lines[i]} ${WHITE}│${RESET} ${TEXT[i]}"
|
||||||
|
done
|
||||||
|
echo -e
|
||||||
|
|
||||||
|
else
|
||||||
|
|
||||||
|
|
||||||
|
# Logo for terminal SSH
|
||||||
|
TEXT=(
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
"${BOLD}ProxMenux${RESET}"
|
||||||
|
""
|
||||||
|
"${BOLD}${NEON_PURPLE_BLUE}An Interactive Menu for${RESET}"
|
||||||
|
"${BOLD}${NEON_PURPLE_BLUE}Proxmox VE management${RESET}"
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
LOGO=(
|
||||||
|
"${DARK_GRAY}░░░░ ░░░░${RESET}"
|
||||||
|
"${DARK_GRAY}░░░░░░░ ░░░░░░ ${RESET}"
|
||||||
|
"${DARK_GRAY}░░░░░░░░░░░ ░░░░░░░ ${RESET}"
|
||||||
|
"${DARK_GRAY}░░░░ ░░░░░░ ░░░░░░ ${ORANGE}░░${RESET}"
|
||||||
|
"${DARK_GRAY}░░░░ ░░░░░░░ ${ORANGE}░░▒▒▒${RESET}"
|
||||||
|
"${DARK_GRAY}░░░░ ░░░ ${ORANGE}░▒▒▒▒▒▒▒${RESET}"
|
||||||
|
"${DARK_GRAY}░░░░ ${ORANGE}▒▒▒░ ░▒▒▒▒▒▒▒▒▒▒${RESET}"
|
||||||
|
"${DARK_GRAY}░░░░ ${ORANGE}░▒▒▒▒▒ ▒▒▒▒▒░░ ▒▒▒▒${RESET}"
|
||||||
|
"${DARK_GRAY}░░░░ ${ORANGE}░░▒▒▒▒▒▒▒░░ ▒▒▒▒${RESET}"
|
||||||
|
"${DARK_GRAY}░░░░ ${ORANGE}░░░ ▒▒▒▒${RESET}"
|
||||||
|
"${DARK_GRAY}░░░░ ${ORANGE}▒▒▒▒${RESET}"
|
||||||
|
"${DARK_GRAY}░░░░ ${ORANGE}▒▒▒░${RESET}"
|
||||||
|
"${DARK_GRAY} ░░ ${ORANGE}░░ ${RESET}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in {0..12}; do
|
||||||
|
echo -e "${TAB}${LOGO[i]} │${RESET} ${TEXT[i]}"
|
||||||
|
done
|
||||||
|
echo -e
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
cleanup_corrupted_files() {
|
cleanup_corrupted_files() {
|
||||||
if [ -f "$CONFIG_FILE" ] && ! jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
|
if [ -f "$CONFIG_FILE" ] && ! jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
|
||||||
echo "Cleaning up corrupted configuration file..."
|
echo "Cleaning up corrupted configuration file..."
|
||||||
@@ -67,6 +277,17 @@ cleanup_corrupted_files() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Cleanup function
|
||||||
|
cleanup() {
|
||||||
|
if [ -d "$TEMP_DIR" ]; then
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set trap to ensure cleanup on exit
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
check_existing_installation() {
|
check_existing_installation() {
|
||||||
local has_venv=false
|
local has_venv=false
|
||||||
@@ -118,6 +339,27 @@ uninstall_proxmenux() {
|
|||||||
|
|
||||||
echo "Uninstalling ProxMenux..."
|
echo "Uninstalling ProxMenux..."
|
||||||
|
|
||||||
|
if systemctl is-active --quiet proxmenux-monitor.service; then
|
||||||
|
echo "Stopping ProxMenux Monitor service..."
|
||||||
|
systemctl stop proxmenux-monitor.service
|
||||||
|
fi
|
||||||
|
|
||||||
|
if systemctl is-enabled --quiet proxmenux-monitor.service 2>/dev/null; then
|
||||||
|
echo "Disabling ProxMenux Monitor service..."
|
||||||
|
systemctl disable proxmenux-monitor.service
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$MONITOR_SERVICE_FILE" ]; then
|
||||||
|
echo "Removing ProxMenux Monitor service file..."
|
||||||
|
rm -f "$MONITOR_SERVICE_FILE"
|
||||||
|
systemctl daemon-reload
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$MONITOR_INSTALL_DIR" ]; then
|
||||||
|
echo "Removing ProxMenux Monitor directory..."
|
||||||
|
rm -rf "$MONITOR_INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -f "$VENV_PATH/bin/activate" ]; then
|
if [ -f "$VENV_PATH/bin/activate" ]; then
|
||||||
echo "Removing googletrans and virtual environment..."
|
echo "Removing googletrans and virtual environment..."
|
||||||
source "$VENV_PATH/bin/activate"
|
source "$VENV_PATH/bin/activate"
|
||||||
@@ -314,55 +556,108 @@ get_server_ip() {
|
|||||||
echo "$ip"
|
echo "$ip"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
detect_latest_appimage() {
|
||||||
|
local appimage_dir="$TEMP_DIR/AppImage"
|
||||||
|
|
||||||
|
if [ ! -d "$appimage_dir" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local latest_appimage=$(find "$appimage_dir" -name "ProxMenux-*.AppImage" -type f | sort -V | tail -1)
|
||||||
|
|
||||||
|
if [ -z "$latest_appimage" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$latest_appimage"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
get_appimage_version() {
|
||||||
|
local appimage_path="$1"
|
||||||
|
local filename=$(basename "$appimage_path")
|
||||||
|
|
||||||
|
local version=$(echo "$filename" | grep -oP 'ProxMenux-\K[0-9]+\.[0-9]+\.[0-9]+')
|
||||||
|
|
||||||
|
echo "$version"
|
||||||
|
}
|
||||||
|
|
||||||
install_proxmenux_monitor() {
|
install_proxmenux_monitor() {
|
||||||
# Check if URL is accessible
|
local appimage_source=$(detect_latest_appimage)
|
||||||
if ! wget --spider -q "$MONITOR_APPIMAGE_URL" 2>/dev/null; then
|
|
||||||
msg_warn "ProxMenux Monitor AppImage not available at: $MONITOR_APPIMAGE_URL"
|
if [ -z "$appimage_source" ] || [ ! -f "$appimage_source" ]; then
|
||||||
msg_info "The monitor will be available in future releases."
|
msg_error "ProxMenux Monitor AppImage not found in $TEMP_DIR/AppImage/"
|
||||||
|
msg_warn "Please ensure the AppImage directory exists with ProxMenux-*.AppImage files."
|
||||||
|
update_config "proxmenux_monitor" "appimage_not_found"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Download AppImage silently
|
local appimage_version=$(get_appimage_version "$appimage_source")
|
||||||
if ! wget -q -O "$MONITOR_INSTALL_PATH" "$MONITOR_APPIMAGE_URL" 2>&1; then
|
|
||||||
msg_warn "Failed to download ProxMenux Monitor from GitHub."
|
if systemctl is-active --quiet proxmenux-monitor.service; then
|
||||||
msg_info "You can install it manually later when available."
|
systemctl stop proxmenux-monitor.service
|
||||||
return 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Download SHA256 checksum silently
|
local service_exists=false
|
||||||
local sha256_file="/tmp/proxmenux-monitor.sha256"
|
if [ -f "$MONITOR_SERVICE_FILE" ]; then
|
||||||
if ! wget -q -O "$sha256_file" "$MONITOR_SHA256_URL" 2>/dev/null; then
|
service_exists=true
|
||||||
msg_warn "SHA256 checksum file not available. Skipping verification."
|
fi
|
||||||
msg_info "AppImage downloaded but integrity cannot be verified."
|
|
||||||
rm -f "$sha256_file"
|
local sha256_file="$TEMP_DIR/AppImage/ProxMenux-Monitor.AppImage.sha256"
|
||||||
else
|
|
||||||
# Verify SHA256 silently
|
if [ -f "$sha256_file" ]; then
|
||||||
local expected_hash=$(cat "$sha256_file" | awk '{print $1}')
|
msg_info "Verifying AppImage integrity..."
|
||||||
local actual_hash=$(sha256sum "$MONITOR_INSTALL_PATH" | awk '{print $1}')
|
local expected_hash=$(cat "$sha256_file" | grep -Eo '^[a-f0-9]+' | tr -d '\n')
|
||||||
|
local actual_hash=$(sha256sum "$appimage_source" | awk '{print $1}')
|
||||||
|
|
||||||
if [ "$expected_hash" != "$actual_hash" ]; then
|
if [ "$expected_hash" != "$actual_hash" ]; then
|
||||||
msg_error "SHA256 verification failed! AppImage may be corrupted."
|
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
|
return 1
|
||||||
fi
|
fi
|
||||||
rm -f "$sha256_file"
|
msg_ok "SHA256 verification passed."
|
||||||
|
else
|
||||||
|
msg_warn "SHA256 checksum not available. Skipping verification."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Make executable
|
msg_info "Installing ProxMenux Monitor..."
|
||||||
chmod +x "$MONITOR_INSTALL_PATH"
|
mkdir -p "$MONITOR_INSTALL_DIR"
|
||||||
|
|
||||||
# Show single success message at the end
|
local target_path="$MONITOR_INSTALL_DIR/ProxMenux-Monitor.AppImage"
|
||||||
msg_ok "ProxMenux Monitor installed and activated successfully."
|
cp "$appimage_source" "$target_path"
|
||||||
|
chmod +x "$target_path"
|
||||||
|
|
||||||
return 0
|
msg_ok "ProxMenux Monitor v$appimage_version installed."
|
||||||
|
|
||||||
|
if [ "$service_exists" = false ]; then
|
||||||
|
return 0 # New installation - service needs to be created
|
||||||
|
else
|
||||||
|
|
||||||
|
systemctl start proxmenux-monitor.service
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if systemctl is-active --quiet proxmenux-monitor.service; then
|
||||||
|
|
||||||
|
update_config "proxmenux_monitor" "updated"
|
||||||
|
return 2 # Update successful
|
||||||
|
else
|
||||||
|
msg_warn "Service failed to restart. Check: journalctl -u proxmenux-monitor"
|
||||||
|
update_config "proxmenux_monitor" "failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
create_monitor_service() {
|
create_monitor_service() {
|
||||||
msg_info "Creating ProxMenux Monitor service..."
|
msg_info "Creating ProxMenux Monitor service..."
|
||||||
|
|
||||||
cat > "$MONITOR_SERVICE_FILE" << EOF
|
local exec_path="$MONITOR_INSTALL_DIR/ProxMenux-Monitor.AppImage"
|
||||||
|
|
||||||
|
if [ -f "$TEMP_DIR/systemd/proxmenux-monitor.service" ]; then
|
||||||
|
sed "s|ExecStart=.*|ExecStart=$exec_path|g" \
|
||||||
|
"$TEMP_DIR/systemd/proxmenux-monitor.service" > "$MONITOR_SERVICE_FILE"
|
||||||
|
msg_ok "Using service file from repository."
|
||||||
|
else
|
||||||
|
cat > "$MONITOR_SERVICE_FILE" << EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=ProxMenux Monitor - Web Dashboard
|
Description=ProxMenux Monitor - Web Dashboard
|
||||||
After=network.target
|
After=network.target
|
||||||
@@ -370,8 +665,8 @@ After=network.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=root
|
User=root
|
||||||
WorkingDirectory=$BASE_DIR
|
WorkingDirectory=$MONITOR_INSTALL_DIR
|
||||||
ExecStart=$MONITOR_INSTALL_PATH
|
ExecStart=$exec_path
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
Environment="PORT=$MONITOR_PORT"
|
Environment="PORT=$MONITOR_PORT"
|
||||||
@@ -379,55 +674,63 @@ Environment="PORT=$MONITOR_PORT"
|
|||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
EOF
|
EOF
|
||||||
|
msg_ok "Created default service file."
|
||||||
|
fi
|
||||||
|
|
||||||
# Reload systemd, enable and start service
|
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable proxmenux-monitor.service > /dev/null 2>&1
|
systemctl enable proxmenux-monitor.service > /dev/null 2>&1
|
||||||
systemctl start proxmenux-monitor.service > /dev/null 2>&1
|
systemctl start proxmenux-monitor.service > /dev/null 2>&1
|
||||||
|
|
||||||
# Wait a moment for service to start
|
sleep 3
|
||||||
sleep 2
|
|
||||||
|
|
||||||
# Check if service is running
|
|
||||||
if systemctl is-active --quiet proxmenux-monitor.service; then
|
if systemctl is-active --quiet proxmenux-monitor.service; then
|
||||||
msg_ok "ProxMenux Monitor service started successfully."
|
msg_ok "ProxMenux Monitor service started successfully."
|
||||||
update_config "proxmenux_monitor" "installed"
|
update_config "proxmenux_monitor" "installed"
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
msg_warn "ProxMenux Monitor service failed to start. Check logs with: journalctl -u proxmenux-monitor"
|
msg_warn "ProxMenux Monitor service failed to start."
|
||||||
|
msg_info "Check logs with: journalctl -u proxmenux-monitor -n 20"
|
||||||
|
msg_info "Check status with: systemctl status proxmenux-monitor"
|
||||||
|
update_config "proxmenux_monitor" "failed"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
####################################################
|
|
||||||
install_normal_version() {
|
install_normal_version() {
|
||||||
local total_steps=4 # Increased from 3 to 4 for monitor installation
|
local total_steps=5
|
||||||
local current_step=1
|
local current_step=1
|
||||||
|
|
||||||
show_progress $current_step $total_steps "Installing basic dependencies"
|
show_progress $current_step $total_steps "Installing basic dependencies."
|
||||||
|
|
||||||
if ! dpkg -l | grep -qw "jq"; then
|
if ! command -v jq > /dev/null 2>&1; then
|
||||||
msg_info "Installing jq..."
|
|
||||||
apt-get update > /dev/null 2>&1
|
apt-get update > /dev/null 2>&1
|
||||||
if apt-get install -y jq > /dev/null 2>&1; then
|
|
||||||
msg_ok "jq installed successfully."
|
if apt-get install -y jq > /dev/null 2>&1 && command -v jq > /dev/null 2>&1; then
|
||||||
update_config "jq" "installed"
|
update_config "jq" "installed"
|
||||||
else
|
else
|
||||||
msg_error "Failed to install jq. Please install it manually."
|
local jq_url="https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64"
|
||||||
update_config "jq" "failed"
|
if wget -q -O /usr/local/bin/jq "$jq_url" 2>/dev/null && chmod +x /usr/local/bin/jq; then
|
||||||
return 1
|
if command -v jq > /dev/null 2>&1; then
|
||||||
|
update_config "jq" "installed_from_github"
|
||||||
|
else
|
||||||
|
msg_error "Failed to install jq. Please install it manually."
|
||||||
|
update_config "jq" "failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
msg_error "Failed to install jq from both APT and GitHub. Please install it manually."
|
||||||
|
update_config "jq" "failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
msg_ok "jq is already installed."
|
|
||||||
update_config "jq" "already_installed"
|
update_config "jq" "already_installed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
BASIC_DEPS=("dialog" "curl")
|
BASIC_DEPS=("dialog" "curl" "git")
|
||||||
for pkg in "${BASIC_DEPS[@]}"; do
|
for pkg in "${BASIC_DEPS[@]}"; do
|
||||||
if ! dpkg -l | grep -qw "$pkg"; then
|
if ! dpkg -l | grep -qw "$pkg"; then
|
||||||
msg_info "Installing $pkg..."
|
|
||||||
if apt-get install -y "$pkg" > /dev/null 2>&1; then
|
if apt-get install -y "$pkg" > /dev/null 2>&1; then
|
||||||
msg_ok "$pkg installed successfully."
|
|
||||||
update_config "$pkg" "installed"
|
update_config "$pkg" "installed"
|
||||||
else
|
else
|
||||||
msg_error "Failed to install $pkg. Please install it manually."
|
msg_error "Failed to install $pkg. Please install it manually."
|
||||||
@@ -435,11 +738,25 @@ install_normal_version() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
msg_ok "$pkg is already installed."
|
|
||||||
update_config "$pkg" "already_installed"
|
update_config "$pkg" "already_installed"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
msg_ok "jq, dialog, curl and git installed successfully."
|
||||||
|
|
||||||
|
((current_step++))
|
||||||
|
|
||||||
|
show_progress $current_step $total_steps "Install ProxMenux repository"
|
||||||
|
msg_info "Cloning ProxMenux repositoryy."
|
||||||
|
if ! git clone --depth 1 "$REPO_URL" "$TEMP_DIR" 2>/dev/null; then
|
||||||
|
msg_error "Failed to clone repository from $REPO_URL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_ok "Repository cloned successfully."
|
||||||
|
|
||||||
|
cd "$TEMP_DIR"
|
||||||
|
|
||||||
((current_step++))
|
((current_step++))
|
||||||
|
|
||||||
show_progress $current_step $total_steps "Creating directories and configuration"
|
show_progress $current_step $total_steps "Creating directories and configuration"
|
||||||
@@ -454,39 +771,36 @@ install_normal_version() {
|
|||||||
msg_ok "Directories and configuration created."
|
msg_ok "Directories and configuration created."
|
||||||
((current_step++))
|
((current_step++))
|
||||||
|
|
||||||
show_progress $current_step $total_steps "Downloading necessary files"
|
show_progress $current_step $total_steps "Copying necessary files"
|
||||||
|
|
||||||
FILES=(
|
|
||||||
"$UTILS_FILE $REPO_URL/scripts/utils.sh"
|
|
||||||
"$INSTALL_DIR/$MENU_SCRIPT $REPO_URL/$MENU_SCRIPT"
|
|
||||||
"$LOCAL_VERSION_FILE $REPO_URL/version.txt"
|
|
||||||
)
|
|
||||||
|
|
||||||
for file in "${FILES[@]}"; do
|
|
||||||
IFS=" " read -r dest url <<< "$file"
|
|
||||||
msg_info "Downloading ${dest##*/}..."
|
|
||||||
sleep 2
|
|
||||||
if wget -qO "$dest" "$url"; then
|
|
||||||
msg_ok "${dest##*/} downloaded successfully."
|
|
||||||
else
|
|
||||||
msg_error "Failed to download ${dest##*/}. Check your Internet connection."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
|
cp "./scripts/utils.sh" "$UTILS_FILE"
|
||||||
|
cp "./menu" "$INSTALL_DIR/$MENU_SCRIPT"
|
||||||
|
cp "./version.txt" "$LOCAL_VERSION_FILE"
|
||||||
|
cp "./install_proxmenux.sh" "$BASE_DIR/install_proxmenux.sh"
|
||||||
|
|
||||||
|
mkdir -p "$BASE_DIR/scripts"
|
||||||
|
cp -r "./scripts/"* "$BASE_DIR/scripts/"
|
||||||
|
chmod -R +x "$BASE_DIR/scripts/"
|
||||||
|
chmod +x "$BASE_DIR/install_proxmenux.sh"
|
||||||
|
msg_ok "Necessary files created."
|
||||||
|
|
||||||
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
|
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
|
||||||
|
|
||||||
((current_step++))
|
((current_step++))
|
||||||
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
|
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
|
||||||
|
|
||||||
if install_proxmenux_monitor; then
|
install_proxmenux_monitor
|
||||||
|
local monitor_status=$?
|
||||||
|
|
||||||
|
if [ $monitor_status -eq 0 ]; then
|
||||||
create_monitor_service
|
create_monitor_service
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
msg_ok "ProxMenux Normal Version installation completed successfully."
|
||||||
}
|
}
|
||||||
|
|
||||||
####################################################
|
|
||||||
install_translation_version() {
|
install_translation_version() {
|
||||||
local total_steps=5 # Increased from 4 to 5 for monitor installation
|
local total_steps=5
|
||||||
local current_step=1
|
local current_step=1
|
||||||
|
|
||||||
show_progress $current_step $total_steps "Language selection"
|
show_progress $current_step $total_steps "Language selection"
|
||||||
@@ -495,28 +809,35 @@ install_translation_version() {
|
|||||||
|
|
||||||
show_progress $current_step $total_steps "Installing system dependencies"
|
show_progress $current_step $total_steps "Installing system dependencies"
|
||||||
|
|
||||||
if ! dpkg -l | grep -qw "jq"; then
|
if ! command -v jq > /dev/null 2>&1; then
|
||||||
msg_info "Installing jq..."
|
|
||||||
apt-get update > /dev/null 2>&1
|
apt-get update > /dev/null 2>&1
|
||||||
if apt-get install -y jq > /dev/null 2>&1; then
|
|
||||||
msg_ok "jq installed successfully."
|
if apt-get install -y jq > /dev/null 2>&1 && command -v jq > /dev/null 2>&1; then
|
||||||
update_config "jq" "installed"
|
update_config "jq" "installed"
|
||||||
else
|
else
|
||||||
msg_error "Failed to install jq. Please install it manually."
|
local jq_url="https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64"
|
||||||
update_config "jq" "failed"
|
if wget -q -O /usr/local/bin/jq "$jq_url" 2>/dev/null && chmod +x /usr/local/bin/jq; then
|
||||||
return 1
|
if command -v jq > /dev/null 2>&1; then
|
||||||
|
update_config "jq" "installed_from_github"
|
||||||
|
else
|
||||||
|
msg_error "Failed to install jq. Please install it manually."
|
||||||
|
update_config "jq" "failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
msg_error "Failed to install jq from both APT and GitHub. Please install it manually."
|
||||||
|
update_config "jq" "failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
msg_ok "jq is already installed."
|
|
||||||
update_config "jq" "already_installed"
|
update_config "jq" "already_installed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
DEPS=("dialog" "curl" "python3" "python3-venv" "python3-pip")
|
DEPS=("dialog" "curl" "git" "python3" "python3-venv" "python3-pip")
|
||||||
for pkg in "${DEPS[@]}"; do
|
for pkg in "${DEPS[@]}"; do
|
||||||
if ! dpkg -l | grep -qw "$pkg"; then
|
if ! dpkg -l | grep -qw "$pkg"; then
|
||||||
msg_info "Installing $pkg..."
|
|
||||||
if apt-get install -y "$pkg" > /dev/null 2>&1; then
|
if apt-get install -y "$pkg" > /dev/null 2>&1; then
|
||||||
msg_ok "$pkg installed successfully."
|
|
||||||
update_config "$pkg" "installed"
|
update_config "$pkg" "installed"
|
||||||
else
|
else
|
||||||
msg_error "Failed to install $pkg. Please install it manually."
|
msg_error "Failed to install $pkg. Please install it manually."
|
||||||
@@ -524,36 +845,32 @@ install_translation_version() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
msg_ok "$pkg is already installed."
|
|
||||||
update_config "$pkg" "already_installed"
|
update_config "$pkg" "already_installed"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
msg_ok "jq, dialog, curl, git, python3, python3-venv and python3-pip installed successfully."
|
||||||
|
|
||||||
((current_step++))
|
((current_step++))
|
||||||
|
|
||||||
show_progress $current_step $total_steps "Setting up translation environment"
|
show_progress $current_step $total_steps "Setting up translation environment"
|
||||||
|
|
||||||
if [ ! -d "$VENV_PATH" ] || [ ! -f "$VENV_PATH/bin/activate" ]; then
|
if [ ! -d "$VENV_PATH" ] || [ ! -f "$VENV_PATH/bin/activate" ]; then
|
||||||
msg_info "Creating the virtual environment..."
|
|
||||||
python3 -m venv --system-site-packages "$VENV_PATH" > /dev/null 2>&1
|
python3 -m venv --system-site-packages "$VENV_PATH" > /dev/null 2>&1
|
||||||
if [ ! -f "$VENV_PATH/bin/activate" ]; then
|
if [ ! -f "$VENV_PATH/bin/activate" ]; then
|
||||||
msg_error "Failed to create virtual environment. Please check your Python installation."
|
msg_error "Failed to create virtual environment. Please check your Python installation."
|
||||||
update_config "virtual_environment" "failed"
|
update_config "virtual_environment" "failed"
|
||||||
return 1
|
return 1
|
||||||
else
|
else
|
||||||
msg_ok "Virtual environment created successfully."
|
|
||||||
update_config "virtual_environment" "created"
|
update_config "virtual_environment" "created"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
msg_ok "Virtual environment already exists."
|
|
||||||
update_config "virtual_environment" "already_exists"
|
update_config "virtual_environment" "already_exists"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
source "$VENV_PATH/bin/activate"
|
source "$VENV_PATH/bin/activate"
|
||||||
|
|
||||||
msg_info "Upgrading pip..."
|
|
||||||
if pip install --upgrade pip > /dev/null 2>&1; then
|
if pip install --upgrade pip > /dev/null 2>&1; then
|
||||||
msg_ok "Pip upgraded successfully."
|
|
||||||
update_config "pip" "upgraded"
|
update_config "pip" "upgraded"
|
||||||
else
|
else
|
||||||
msg_error "Failed to upgrade pip."
|
msg_error "Failed to upgrade pip."
|
||||||
@@ -561,9 +878,7 @@ install_translation_version() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
msg_info "Installing googletrans..."
|
|
||||||
if pip install --break-system-packages --no-cache-dir googletrans==4.0.0-rc1 > /dev/null 2>&1; then
|
if pip install --break-system-packages --no-cache-dir googletrans==4.0.0-rc1 > /dev/null 2>&1; then
|
||||||
msg_ok "Googletrans installed successfully."
|
|
||||||
update_config "googletrans" "installed"
|
update_config "googletrans" "installed"
|
||||||
else
|
else
|
||||||
msg_error "Failed to install googletrans. Please check your internet connection."
|
msg_error "Failed to install googletrans. Please check your internet connection."
|
||||||
@@ -573,46 +888,54 @@ install_translation_version() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
deactivate
|
deactivate
|
||||||
|
|
||||||
|
show_progress $current_step $total_steps "Cloning ProxMenux repository"
|
||||||
|
if ! git clone --depth 1 "$REPO_URL" "$TEMP_DIR" 2>/dev/null; then
|
||||||
|
msg_error "Failed to clone repository from $REPO_URL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
msg_ok "Repository cloned successfully."
|
||||||
|
|
||||||
|
cd "$TEMP_DIR"
|
||||||
|
|
||||||
((current_step++))
|
((current_step++))
|
||||||
|
|
||||||
show_progress $current_step $total_steps "Downloading necessary files"
|
show_progress $current_step $total_steps "Copying necessary files"
|
||||||
|
|
||||||
mkdir -p "$BASE_DIR"
|
mkdir -p "$BASE_DIR"
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
FILES=(
|
cp "./json/cache.json" "$CACHE_FILE"
|
||||||
"$CACHE_FILE $REPO_URL/json/cache.json"
|
msg_ok "Cache file copied with translations."
|
||||||
"$UTILS_FILE $REPO_URL/scripts/utils.sh"
|
|
||||||
"$INSTALL_DIR/$MENU_SCRIPT $REPO_URL/$MENU_SCRIPT"
|
|
||||||
"$LOCAL_VERSION_FILE $REPO_URL/version.txt"
|
|
||||||
)
|
|
||||||
|
|
||||||
for file in "${FILES[@]}"; do
|
cp "./scripts/utils.sh" "$UTILS_FILE"
|
||||||
IFS=" " read -r dest url <<< "$file"
|
cp "./menu" "$INSTALL_DIR/$MENU_SCRIPT"
|
||||||
msg_info "Downloading ${dest##*/}..."
|
cp "./version.txt" "$LOCAL_VERSION_FILE"
|
||||||
sleep 2
|
cp "./install_proxmenux.sh" "$BASE_DIR/install_proxmenux.sh"
|
||||||
if wget -qO "$dest" "$url"; then
|
|
||||||
msg_ok "${dest##*/} downloaded successfully."
|
mkdir -p "$BASE_DIR/scripts"
|
||||||
if [[ "$dest" == "$CACHE_FILE" ]]; then
|
cp -r "./scripts/"* "$BASE_DIR/scripts/"
|
||||||
msg_ok "Cache file updated with latest translations."
|
chmod -R +x "$BASE_DIR/scripts/"
|
||||||
fi
|
chmod +x "$BASE_DIR/install_proxmenux.sh"
|
||||||
else
|
msg_ok "Necessary files created."
|
||||||
msg_error "Failed to download ${dest##*/}. Check your Internet connection."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
|
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
|
||||||
|
|
||||||
((current_step++))
|
((current_step++))
|
||||||
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
|
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
|
||||||
|
|
||||||
if install_proxmenux_monitor; then
|
install_proxmenux_monitor
|
||||||
|
local monitor_status=$?
|
||||||
|
|
||||||
|
if [ $monitor_status -eq 0 ]; then
|
||||||
create_monitor_service
|
create_monitor_service
|
||||||
|
elif [ $monitor_status -eq 2 ]; then
|
||||||
|
msg_ok "ProxMenux Monitor updated successfully."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
msg_ok "ProxMenux Translation Version installation completed successfully."
|
||||||
}
|
}
|
||||||
|
|
||||||
####################################################
|
|
||||||
show_installation_options() {
|
show_installation_options() {
|
||||||
local current_install_type
|
local current_install_type
|
||||||
current_install_type=$(check_existing_installation)
|
current_install_type=$(check_existing_installation)
|
||||||
@@ -663,7 +986,6 @@ show_installation_options() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# For new installations, show confirmation with details
|
|
||||||
if [ "$current_install_type" = "none" ]; then
|
if [ "$current_install_type" = "none" ]; then
|
||||||
if ! show_installation_confirmation "$INSTALL_TYPE"; then
|
if ! show_installation_confirmation "$INSTALL_TYPE"; then
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
@@ -679,7 +1001,7 @@ show_installation_options() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
install_proxmenu() {
|
install_proxmenux() {
|
||||||
show_installation_options
|
show_installation_options
|
||||||
|
|
||||||
case "$INSTALL_TYPE" in
|
case "$INSTALL_TYPE" in
|
||||||
@@ -698,17 +1020,21 @@ install_proxmenu() {
|
|||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
if [[ -f "$UTILS_FILE" ]]; then
|
||||||
|
source "$UTILS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
msg_title "$(translate "ProxMenux has been installed successfully")"
|
msg_title "ProxMenux has been installed successfully"
|
||||||
|
|
||||||
if systemctl is-active --quiet proxmenux-monitor.service; then
|
if systemctl is-active --quiet proxmenux-monitor.service; then
|
||||||
local server_ip=$(get_server_ip)
|
local server_ip=$(get_server_ip)
|
||||||
echo -e "${GN}🌐 $(translate "ProxMenux Monitor activated")${CL}: ${BL}http://${server_ip}:${MONITOR_PORT}${CL}"
|
echo -e "${GN}🌐 ProxMenux Monitor activated${CL}: ${BL}http://${server_ip}:${MONITOR_PORT}${CL}"
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -ne "${GN}"
|
echo -ne "${GN}"
|
||||||
type_text "$(translate "To run ProxMenux, simply execute this command in the console or terminal:")"
|
type_text "To run ProxMenux, simply execute this command in the console or terminal:"
|
||||||
echo -e "${YWB} menu${CL}"
|
echo -e "${YWB} menu${CL}"
|
||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
@@ -719,4 +1045,4 @@ if [ "$(id -u)" -ne 0 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
cleanup_corrupted_files
|
cleanup_corrupted_files
|
||||||
install_proxmenu
|
install_proxmenux
|
||||||
|
|||||||
+1517
-214
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.1
|
# Version : 1.1
|
||||||
# Last Updated: 04/07/2025
|
# Last Updated: 04/07/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
CONFIG_FILE="$BASE_DIR/config.json"
|
CONFIG_FILE="$BASE_DIR/config.json"
|
||||||
CACHE_FILE="$BASE_DIR/cache.json"
|
CACHE_FILE="$BASE_DIR/cache.json"
|
||||||
@@ -44,7 +44,10 @@ if [[ -f "$UTILS_FILE" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
|
# For now, update is not available in the local version.
|
||||||
|
# Take in mind that in future versions, updates must be
|
||||||
|
# a warning to update the .deb package
|
||||||
|
# =========================================================
|
||||||
check_updates() {
|
check_updates() {
|
||||||
local INSTALL_SCRIPT="$BASE_DIR/install_proxmenux.sh"
|
local INSTALL_SCRIPT="$BASE_DIR/install_proxmenux.sh"
|
||||||
|
|
||||||
@@ -80,13 +83,13 @@ check_updates() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
main_menu() {
|
main_menu() {
|
||||||
exec bash <(curl -fsSL "$REPO_URL/scripts/menus/main_menu.sh")
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
load_language
|
load_language
|
||||||
initialize_cache
|
initialize_cache
|
||||||
check_updates
|
# Check updates doesn't make sense in offline mode
|
||||||
|
# check_updates
|
||||||
main_menu
|
main_menu
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.0
|
# Version : 1.0
|
||||||
# Last Updated: 06/07/2025
|
# Last Updated: 06/07/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -31,12 +31,12 @@
|
|||||||
# - Translation support: Multi-language compatible through ProxMenux framework
|
# - Translation support: Multi-language compatible through ProxMenux framework
|
||||||
# - Rollback compatibility: All optimizations can be reversed using the uninstall script
|
# - Rollback compatibility: All optimizations can be reversed using the uninstall script
|
||||||
#
|
#
|
||||||
# This script is based on the post-install script cutotomizable
|
# This script is based on the post-install script customizable
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -99,7 +99,7 @@ lvm_repair_check() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
msg_ok "$(translate "LVM PV headers check completed")"
|
msg_ok "$(translate "LVM PV headers check completed")"
|
||||||
|
register_tool "lvm_repair" true
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -257,7 +257,7 @@ apt_upgrade() {
|
|||||||
if [ "$total_packages" -eq 0 ]; then
|
if [ "$total_packages" -eq 0 ]; then
|
||||||
total_packages=1
|
total_packages=1
|
||||||
fi
|
fi
|
||||||
msg_ok "$(translate "Packages upgrade successfull")"
|
msg_ok "$(translate "Packages upgrade successful")"
|
||||||
tput civis
|
tput civis
|
||||||
tput sc
|
tput sc
|
||||||
|
|
||||||
@@ -748,8 +748,9 @@ install_log2ram_auto() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Detect RAM
|
# Detect RAM (in MB first for better accuracy)
|
||||||
RAM_SIZE_GB=$(free -g | awk '/^Mem:/{print $2}')
|
RAM_SIZE_MB=$(free -m | awk '/^Mem:/{print $2}')
|
||||||
|
RAM_SIZE_GB=$((RAM_SIZE_MB / 1024))
|
||||||
[[ -z "$RAM_SIZE_GB" || "$RAM_SIZE_GB" -eq 0 ]] && RAM_SIZE_GB=4
|
[[ -z "$RAM_SIZE_GB" || "$RAM_SIZE_GB" -eq 0 ]] && RAM_SIZE_GB=4
|
||||||
|
|
||||||
if (( RAM_SIZE_GB <= 8 )); then
|
if (( RAM_SIZE_GB <= 8 )); then
|
||||||
@@ -773,7 +774,13 @@ install_log2ram_auto() {
|
|||||||
cat << 'EOF' > /usr/local/bin/log2ram-check.sh
|
cat << 'EOF' > /usr/local/bin/log2ram-check.sh
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
CONF_FILE="/etc/log2ram.conf"
|
CONF_FILE="/etc/log2ram.conf"
|
||||||
LIMIT_KB=$(grep '^SIZE=' "$CONF_FILE" | cut -d'=' -f2 | tr -d 'M')000
|
SIZE_VALUE=$(grep '^SIZE=' "$CONF_FILE" | cut -d'=' -f2)
|
||||||
|
# Convert to KB: handle M (megabytes) and G (gigabytes)
|
||||||
|
if [[ "$SIZE_VALUE" == *"G" ]]; then
|
||||||
|
LIMIT_KB=$(($(echo "$SIZE_VALUE" | tr -d 'G') * 1024 * 1024))
|
||||||
|
else
|
||||||
|
LIMIT_KB=$(($(echo "$SIZE_VALUE" | tr -d 'M') * 1024))
|
||||||
|
fi
|
||||||
USED_KB=$(df /var/log --output=used | tail -1)
|
USED_KB=$(df /var/log --output=used | tail -1)
|
||||||
THRESHOLD=$(( LIMIT_KB * 90 / 100 ))
|
THRESHOLD=$(( LIMIT_KB * 90 / 100 ))
|
||||||
if (( USED_KB > THRESHOLD )); then
|
if (( USED_KB > THRESHOLD )); then
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -16,7 +16,7 @@ initialize_cache
|
|||||||
|
|
||||||
get_external_backup_mount_point() {
|
get_external_backup_mount_point() {
|
||||||
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
|
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
|
||||||
local STORAGE_REPO="$REPO_URL/scripts/backup_restore"
|
local STORAGE_REPO="$LOCAL_SCRIPTS/backup_restore"
|
||||||
local MOUNT_POINT
|
local MOUNT_POINT
|
||||||
|
|
||||||
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
|
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
|
||||||
@@ -36,7 +36,7 @@ get_external_backup_mount_point() {
|
|||||||
echo "$MOUNT_POINT"
|
echo "$MOUNT_POINT"
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
source <(curl -s "$STORAGE_REPO/mount_disk_host_bk.sh")
|
source "$STORAGE_REPO/mount_disk_host_bk.sh"
|
||||||
MOUNT_POINT=$(mount_disk_host_bk)
|
MOUNT_POINT=$(mount_disk_host_bk)
|
||||||
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
|
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
|
||||||
echo "$MOUNT_POINT"
|
echo "$MOUNT_POINT"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -16,7 +16,7 @@ initialize_cache
|
|||||||
|
|
||||||
get_external_backup_mount_point() {
|
get_external_backup_mount_point() {
|
||||||
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
|
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
|
||||||
local STORAGE_REPO="$REPO_URL/scripts/backup_restore"
|
local STORAGE_REPO="$LOCAL_SCRIPTS/backup_restore"
|
||||||
local MOUNT_POINT
|
local MOUNT_POINT
|
||||||
|
|
||||||
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
|
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
|
||||||
@@ -36,7 +36,7 @@ get_external_backup_mount_point() {
|
|||||||
echo "$MOUNT_POINT"
|
echo "$MOUNT_POINT"
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
source <(curl -s "$STORAGE_REPO/mount_disk_host_bk.sh")
|
source "$STORAGE_REPO/mount_disk_host_bk.sh"
|
||||||
MOUNT_POINT=$(mount_disk_host_bk)
|
MOUNT_POINT=$(mount_disk_host_bk)
|
||||||
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
|
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
|
||||||
echo "$MOUNT_POINT"
|
echo "$MOUNT_POINT"
|
||||||
@@ -1058,4 +1058,4 @@ read -r
|
|||||||
# ===============================
|
# ===============================
|
||||||
|
|
||||||
|
|
||||||
host_backup_menu
|
host_backup_menu
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -16,7 +16,7 @@ initialize_cache
|
|||||||
|
|
||||||
get_external_backup_mount_point() {
|
get_external_backup_mount_point() {
|
||||||
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
|
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
|
||||||
local STORAGE_REPO="$REPO_URL/scripts/backup_restore"
|
local STORAGE_REPO="$LOCAL_SCRIPTS/backup_restore"
|
||||||
local MOUNT_POINT
|
local MOUNT_POINT
|
||||||
|
|
||||||
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
|
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
|
||||||
@@ -36,7 +36,7 @@ get_external_backup_mount_point() {
|
|||||||
echo "$MOUNT_POINT"
|
echo "$MOUNT_POINT"
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
source <(curl -s "$STORAGE_REPO/mount_disk_host_bk.sh")
|
source "$STORAGE_REPO/mount_disk_host_bk.sh"
|
||||||
MOUNT_POINT=$(mount_disk_host_bk)
|
MOUNT_POINT=$(mount_disk_host_bk)
|
||||||
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
|
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
|
||||||
echo "$MOUNT_POINT"
|
echo "$MOUNT_POINT"
|
||||||
@@ -1291,4 +1291,4 @@ read -r
|
|||||||
# ===============================
|
# ===============================
|
||||||
|
|
||||||
|
|
||||||
host_backup_menu
|
host_backup_menu
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -16,7 +16,7 @@ initialize_cache
|
|||||||
|
|
||||||
get_external_backup_mount_point() {
|
get_external_backup_mount_point() {
|
||||||
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
|
local BACKUP_MOUNT_FILE="/usr/local/share/proxmenux/last_backup_mount.txt"
|
||||||
local STORAGE_REPO="$REPO_URL/scripts/backup_restore"
|
local STORAGE_REPO="$LOCAL_SCRIPTS/backup_restore"
|
||||||
local MOUNT_POINT
|
local MOUNT_POINT
|
||||||
|
|
||||||
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
|
if [[ -f "$BACKUP_MOUNT_FILE" ]]; then
|
||||||
@@ -36,7 +36,7 @@ get_external_backup_mount_point() {
|
|||||||
echo "$MOUNT_POINT"
|
echo "$MOUNT_POINT"
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
source <(curl -s "$STORAGE_REPO/mount_disk_host_bk.sh")
|
source "$STORAGE_REPO/mount_disk_host_bk.sh"
|
||||||
MOUNT_POINT=$(mount_disk_host_bk)
|
MOUNT_POINT=$(mount_disk_host_bk)
|
||||||
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
|
[[ -z "$MOUNT_POINT" ]] && msg_error "$(translate "No disk mounted.")" && return 1
|
||||||
echo "$MOUNT_POINT"
|
echo "$MOUNT_POINT"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
# Last Updated: 13/12/2024
|
# Last Updated: 13/12/2024
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.1
|
# Version : 1.1
|
||||||
# Last Updated: 17/08/2025
|
# Last Updated: 17/08/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.0
|
# Version : 1.0
|
||||||
# Last Updated: 28/01/2025
|
# Last Updated: 28/01/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.0
|
# Version : 1.0
|
||||||
# Last Updated: 28/01/2025
|
# Last Updated: 28/01/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 2.0
|
# Version : 2.0
|
||||||
# Last Updated: 07/01/2025
|
# Last Updated: 07/01/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -100,8 +100,16 @@ verify_js_integrity() {
|
|||||||
patch_checked_command() {
|
patch_checked_command() {
|
||||||
[ -f "$JS_FILE" ] || return 0
|
[ -f "$JS_FILE" ] || return 0
|
||||||
|
|
||||||
# Check if already patched
|
# Check if already patched - look for our marker
|
||||||
grep -q "$MARK" "$JS_FILE" && return 0
|
if grep -q "$MARK" "$JS_FILE"; then
|
||||||
|
# Verify the patch is actually applied by checking if function is simplified
|
||||||
|
if grep -A 2 "checked_command: function" "$JS_FILE" | grep -q "orig_cmd();"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
# Marker exists but patch not applied - remove marker and try again
|
||||||
|
sed -i "/$MARK/d" "$JS_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Create backup
|
# Create backup
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
@@ -111,27 +119,105 @@ patch_checked_command() {
|
|||||||
# Set trap to restore on error
|
# Set trap to restore on error
|
||||||
trap "cp -a '$backup' '$JS_FILE' 2>/dev/null || true" ERR
|
trap "cp -a '$backup' '$JS_FILE' 2>/dev/null || true" ERR
|
||||||
|
|
||||||
|
# Use Python to replace the entire checked_command function using brace counting
|
||||||
|
python3 <<'PYTHON_END'
|
||||||
|
import sys
|
||||||
|
|
||||||
|
js_file = "/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(js_file, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# Find the line with checked_command
|
||||||
|
start_line = -1
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'checked_command: function' in line or 'checked_command:function' in line:
|
||||||
|
start_line = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if start_line == -1:
|
||||||
|
print("checked_command function not found", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Count braces to find the end of the function
|
||||||
|
brace_count = 0
|
||||||
|
end_line = -1
|
||||||
|
started_counting = False
|
||||||
|
|
||||||
|
for i in range(start_line, len(lines)):
|
||||||
|
line = lines[i]
|
||||||
|
|
||||||
|
# Count opening and closing braces
|
||||||
|
for char in line:
|
||||||
|
if char == '{':
|
||||||
|
brace_count += 1
|
||||||
|
started_counting = True
|
||||||
|
elif char == '}':
|
||||||
|
brace_count -= 1
|
||||||
|
|
||||||
|
# When we reach 0 and we've started counting, we found the end
|
||||||
|
if started_counting and brace_count == 0:
|
||||||
|
# Check if this line ends with "}," which is the function closure
|
||||||
|
if '},' in line or '},\n' in line:
|
||||||
|
end_line = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if end_line == -1:
|
||||||
|
print("Could not find end of checked_command function", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Get the indentation of the original function
|
||||||
|
indent = len(lines[start_line]) - len(lines[start_line].lstrip())
|
||||||
|
indent_str = ' ' * indent
|
||||||
|
|
||||||
|
# Create the replacement function (simple version that just calls orig_cmd)
|
||||||
|
replacement = [
|
||||||
|
f"{indent_str}checked_command: function (orig_cmd) {{\n",
|
||||||
|
f"{indent_str} orig_cmd();\n",
|
||||||
|
f"{indent_str}}},\n"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Replace the function
|
||||||
|
new_lines = lines[:start_line] + replacement + lines[end_line+1:]
|
||||||
|
|
||||||
|
# Write the modified content
|
||||||
|
with open(js_file, 'w', encoding='utf-8') as f:
|
||||||
|
f.writelines(new_lines)
|
||||||
|
|
||||||
|
#print(f"Successfully replaced lines {start_line+1} to {end_line+1}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Python patch error: {e}", file=sys.stderr)
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
PYTHON_END
|
||||||
|
|
||||||
|
local python_result=$?
|
||||||
|
|
||||||
|
if [ $python_result -ne 0 ]; then
|
||||||
|
# Python failed, restore backup
|
||||||
|
cp -a "$backup" "$JS_FILE"
|
||||||
|
trap - ERR
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify the patch was applied
|
||||||
|
if ! grep -A 2 "checked_command: function" "$JS_FILE" | grep -q "orig_cmd();"; then
|
||||||
|
cp -a "$backup" "$JS_FILE"
|
||||||
|
trap - ERR
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Add patch marker at the beginning
|
# Add patch marker at the beginning
|
||||||
sed -i "1s|^|$MARK\n|" "$JS_FILE"
|
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
|
# Verify integrity after patch
|
||||||
if ! verify_js_integrity "$JS_FILE"; then
|
if ! verify_js_integrity "$JS_FILE"; then
|
||||||
cp -a "$backup" "$JS_FILE"
|
cp -a "$backup" "$JS_FILE"
|
||||||
|
trap - ERR
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -217,6 +303,7 @@ EOFAPT
|
|||||||
|
|
||||||
# Verify APT hook syntax
|
# Verify APT hook syntax
|
||||||
apt-config dump >/dev/null 2>&1 || {
|
apt-config dump >/dev/null 2>&1 || {
|
||||||
|
msg_warn "APT hook syntax issue, removing..."
|
||||||
rm -f "$APT_HOOK"
|
rm -f "$APT_HOOK"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +313,7 @@ remove_subscription_banner_v3() {
|
|||||||
local pve_version
|
local pve_version
|
||||||
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||||
|
|
||||||
msg_info "$(translate "Detected Proxmox VE") ${pve_version} - $(translate "applying banner patch")"
|
msg_info "$(translate "Detected Proxmox VE") ${pve_version} - $(translate "applying minimal banner patch")"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -239,16 +326,14 @@ remove_subscription_banner_v3() {
|
|||||||
local backup_file
|
local backup_file
|
||||||
backup_file=$(create_backup "$JS_FILE")
|
backup_file=$(create_backup "$JS_FILE")
|
||||||
if [ -n "$backup_file" ]; then
|
if [ -n "$backup_file" ]; then
|
||||||
# msg_ok "$(translate "Desktop UI backup created"): $backup_file"
|
msg_ok "$(translate "Desktop UI backup created")"
|
||||||
:
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "$MOBILE_UI_FILE" ]; then
|
if [ -f "$MOBILE_UI_FILE" ]; then
|
||||||
local mobile_backup
|
local mobile_backup
|
||||||
mobile_backup=$(create_backup "$MOBILE_UI_FILE")
|
mobile_backup=$(create_backup "$MOBILE_UI_FILE")
|
||||||
if [ -n "$mobile_backup" ]; then
|
if [ -n "$mobile_backup" ]; then
|
||||||
# msg_ok "$(translate "Mobile UI backup created"): $mobile_backup"
|
msg_ok "$(translate "Mobile UI backup created")"
|
||||||
:
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Remove Subscription Banner - Proxmox VE 8.4.9
|
# Remove Subscription Banner - Proxmox VE 8.4.9
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Remove Subscription Banner - Proxmox VE 9.x
|
# Remove Subscription Banner - Proxmox VE 9.x
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Remove Subscription Banner - Proxmox VE 9.x ONLY
|
# Remove Subscription Banner - Proxmox VE 9.x ONLY
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -29,7 +29,7 @@ register_tool() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
download_common_functions() {
|
download_common_functions() {
|
||||||
if ! source <(curl -s "$REPO_URL/scripts/global/common-functions.sh"); then
|
if ! source "$LOCAL_SCRIPTS/global/common-functions.sh"; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -29,7 +29,7 @@ register_tool() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
download_common_functions() {
|
download_common_functions() {
|
||||||
if ! source <(curl -s "$REPO_URL/scripts/global/common-functions.sh"); then
|
if ! source "$LOCAL_SCRIPTS/global/common-functions.sh"; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -29,7 +29,7 @@ register_tool() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
download_common_functions() {
|
download_common_functions() {
|
||||||
if ! source <(curl -s "$REPO_URL/scripts/global/common-functions.sh"); then
|
if ! source "$LOCAL_SCRIPTS/global/common-functions.sh"; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -152,23 +152,48 @@ EOF
|
|||||||
msg_ok "$(translate "Non-free firmware warnings disabled")"
|
msg_ok "$(translate "Non-free firmware warnings disabled")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
#update_output=$(apt-get update 2>&1)
|
# UPDATE: no progress bar here (dpkg is not involved); capture output to parse errors
|
||||||
update_output=$(apt-get -o Dpkg::Progress-Fancy=1 update 2>&1)
|
update_output=$(apt-get update 2>&1)
|
||||||
update_exit_code=$?
|
update_exit_code=$?
|
||||||
|
|
||||||
if [ $update_exit_code -eq 0 ]; then
|
if [ $update_exit_code -eq 0 ]; then
|
||||||
msg_ok "$(translate "Package lists updated successfully")" | tee -a "$screen_capture"
|
msg_ok "$(translate "Package lists updated successfully")" | tee -a "$screen_capture"
|
||||||
else
|
else
|
||||||
if echo "$update_output" | grep -q "NO_PUBKEY\|GPG error"; then
|
# Handle common apt errors
|
||||||
msg_info "$(translate "Fixing GPG key issues...")"
|
if echo "$update_output" | grep -Eq "NO_PUBKEY|GPG error"; then
|
||||||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $(echo "$update_output" | grep "NO_PUBKEY" | sed 's/.*NO_PUBKEY //' | head -1) 2>/dev/null
|
|
||||||
|
|
||||||
|
# Extract first missing key (NO_PUBKEY ABCDEF... pattern)
|
||||||
|
key=$(echo "$update_output" | sed -n 's/.*NO_PUBKEY \([0-9A-F]\{8,40\}\).*/\1/p' | head -1)
|
||||||
|
|
||||||
|
if [ -n "$key" ]; then
|
||||||
|
mkdir -p /etc/apt/keyrings
|
||||||
|
|
||||||
|
if command -v gpg >/dev/null 2>&1; then
|
||||||
|
# Modern approach: receive -> export -> dearmor into /etc/apt/keyrings/<KEY>.gpg
|
||||||
|
if gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" \
|
||||||
|
&& gpg --batch --export "$key" | gpg --dearmor -o "/etc/apt/keyrings/${key}.gpg"; then
|
||||||
|
msg_ok "$(translate "Imported missing GPG key: $key")"
|
||||||
|
else
|
||||||
|
msg_warn "$(translate "Keyrings method failed; trying apt-key fallback")"
|
||||||
|
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys "$key" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Fallback for minimal systems without gpg installed
|
||||||
|
msg_warn "$(translate "gpg not found; trying apt-key fallback")"
|
||||||
|
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys "$key" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retry update after importing the key
|
||||||
if apt-get update > "$log_file" 2>&1; then
|
if apt-get update > "$log_file" 2>&1; then
|
||||||
msg_ok "$(translate "Package lists updated after GPG fix")" | tee -a "$screen_capture"
|
msg_ok "$(translate "Package lists updated after GPG fix")" | tee -a "$screen_capture"
|
||||||
else
|
else
|
||||||
msg_error "$(translate "Failed to update package lists. Check log: $log_file")"
|
msg_error "$(translate "Failed to update package lists. Check log: $log_file")"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
elif echo "$update_output" | grep -q "404\|Failed to fetch"; then
|
|
||||||
|
elif echo "$update_output" | grep -Eq "404|Failed to fetch"; then
|
||||||
msg_warn "$(translate "Some repositories are not available, continuing with available ones...")"
|
msg_warn "$(translate "Some repositories are not available, continuing with available ones...")"
|
||||||
else
|
else
|
||||||
msg_error "$(translate "Failed to update package lists. Check log: $log_file")"
|
msg_error "$(translate "Failed to update package lists. Check log: $log_file")"
|
||||||
@@ -177,10 +202,11 @@ EOF
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
if apt policy 2>/dev/null | grep -q "${TARGET_CODENAME}.*pve-no-subscription"; then
|
if apt policy 2>/dev/null | grep -q "${TARGET_CODENAME}.*pve-no-subscription"; then
|
||||||
msg_ok "$(translate "Proxmox VE 9.x repositories verified")" | tee -a "$screen_capture"
|
msg_ok "$(translate "Proxmox VE $pve_version repositories verified")" | tee -a "$screen_capture"
|
||||||
else
|
else
|
||||||
msg_warn "$(translate "Proxmox VE 9.x repositories verification inconclusive, continuing...")"
|
msg_warn "$(translate "Proxmox VE $pve_version repositories verification inconclusive, continuing...")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local current_pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
local current_pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||||
@@ -294,7 +320,7 @@ EOF
|
|||||||
echo -e "${TAB}${GN}📦 $(translate "Packages upgraded")${CL}: ${BL}$upgradable${CL}"
|
echo -e "${TAB}${GN}📦 $(translate "Packages upgraded")${CL}: ${BL}$upgradable${CL}"
|
||||||
echo -e "${TAB}${GN}🖥️ $(translate "Proxmox VE")${CL}: ${BL}$available_pve_version (Debian $OS_CODENAME)${CL}"
|
echo -e "${TAB}${GN}🖥️ $(translate "Proxmox VE")${CL}: ${BL}$available_pve_version (Debian $OS_CODENAME)${CL}"
|
||||||
|
|
||||||
msg_ok "$(translate "Proxmox VE 9.x configuration completed.")"
|
msg_ok "$(translate "Proxmox VE configuration completed.")"
|
||||||
|
|
||||||
rm -f "$screen_capture"
|
rm -f "$screen_capture"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
# Last Updated: 25/09/2025
|
# Last Updated: 25/09/2025
|
||||||
# =========================================
|
# =========================================
|
||||||
|
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
LOG_FILE="/tmp/coral_install.log"
|
LOG_FILE="/tmp/coral_install.log"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.0
|
# Version : 1.0
|
||||||
# Last Updated: 28/01/2025
|
# Last Updated: 28/01/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.1
|
# Version : 1.1
|
||||||
# Last Updated: 29/05/2025
|
# Last Updated: 29/05/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Revision : @Blaspt (USB passthrough via udev rule with persistent /dev/coral)
|
# Revision : @Blaspt (USB passthrough via udev rule with persistent /dev/coral)
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.1
|
# Version : 1.1
|
||||||
# Last Updated: 16/05/2025
|
# Last Updated: 16/05/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
# Includes USB passthrough enhancement using persistent udev alias (/dev/coral).
|
# Includes USB passthrough enhancement using persistent udev alias (/dev/coral).
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.0
|
# Version : 1.0
|
||||||
# Last Updated: 28/01/2025
|
# Last Updated: 28/01/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.0
|
# Version : 1.0
|
||||||
# Last Updated: 19/08/2025
|
# Last Updated: 19/08/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.1
|
# Version : 1.1
|
||||||
# Last Updated: 19/08/2025
|
# Last Updated: 19/08/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 2.0
|
# Version : 2.0
|
||||||
# Last Updated: 19/08/2025
|
# Last Updated: 19/08/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|||||||
@@ -4,13 +4,14 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# Contributors : cod378
|
||||||
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.1
|
# Version : 1.1
|
||||||
# Last Updated: 04/07/2025
|
# Last Updated: 04/07/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
CONFIG_FILE="$BASE_DIR/config.json"
|
CONFIG_FILE="$BASE_DIR/config.json"
|
||||||
CACHE_FILE="$BASE_DIR/cache.json"
|
CACHE_FILE="$BASE_DIR/cache.json"
|
||||||
@@ -19,7 +20,10 @@ LOCAL_VERSION_FILE="$BASE_DIR/version.txt"
|
|||||||
INSTALL_DIR="/usr/local/bin"
|
INSTALL_DIR="/usr/local/bin"
|
||||||
MENU_SCRIPT="menu"
|
MENU_SCRIPT="menu"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|
||||||
MONITOR_SERVICE="proxmenux-monitor.service"
|
MONITOR_SERVICE="proxmenux-monitor.service"
|
||||||
|
MONITOR_UNIT_FILE="/etc/systemd/system/${MONITOR_SERVICE}"
|
||||||
|
MONITOR_CONFIG_DIR="/root/.config/proxmenux-monitor"
|
||||||
|
|
||||||
if [[ -f "$UTILS_FILE" ]]; then
|
if [[ -f "$UTILS_FILE" ]]; then
|
||||||
source "$UTILS_FILE"
|
source "$UTILS_FILE"
|
||||||
@@ -30,6 +34,50 @@ initialize_cache
|
|||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
|
uninstall_proxmenux_monitor() {
|
||||||
|
|
||||||
|
# 1. Stop service if it is running
|
||||||
|
if systemctl is-active --quiet "${MONITOR_SERVICE}"; then
|
||||||
|
echo " - Stoping service..."
|
||||||
|
systemctl stop "${MONITOR_SERVICE}" > /dev/null 2>&1
|
||||||
|
else
|
||||||
|
echo " - Service is not running (ok)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Disable service if enabled
|
||||||
|
if systemctl is-enabled --quiet "${MONITOR_SERVICE}"; then
|
||||||
|
echo " - Disabling service..."
|
||||||
|
systemctl disable "${MONITOR_SERVICE}" > /dev/null 2>&1
|
||||||
|
else
|
||||||
|
echo " - Service is not enabled (ok)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Remove unit file
|
||||||
|
if [ -f "${MONITOR_UNIT_FILE}" ]; then
|
||||||
|
echo " - Removing unit file ${MONITOR_UNIT_FILE}..."
|
||||||
|
rm -f "${MONITOR_UNIT_FILE}"
|
||||||
|
else
|
||||||
|
echo " - Unit file ${MONITOR_UNIT_FILE} does not exist (ok)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Remove config directory (~/.config/proxmenux-monitor)
|
||||||
|
if [ -d "${MONITOR_CONFIG_DIR}" ]; then
|
||||||
|
echo " - Removing config dir ${MONITOR_CONFIG_DIR}..."
|
||||||
|
rm -rf "${MONITOR_CONFIG_DIR}"
|
||||||
|
else
|
||||||
|
echo " - Config dir ${MONITOR_CONFIG_DIR} does not exist (ok)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Reload systemd
|
||||||
|
echo " - Recargando systemd..."
|
||||||
|
systemctl daemon-reload > /dev/null 2>&1
|
||||||
|
systemctl reset-failed > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
echo "==> Service ${MONITOR_SERVICE} uninstalled."
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
detect_installation_type() {
|
detect_installation_type() {
|
||||||
local has_venv=false
|
local has_venv=false
|
||||||
local has_language=false
|
local has_language=false
|
||||||
@@ -218,7 +266,7 @@ show_config_menu() {
|
|||||||
uninstall_proxmenu
|
uninstall_proxmenu
|
||||||
;;
|
;;
|
||||||
"return_main"|"")
|
"return_main"|"")
|
||||||
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -257,11 +305,7 @@ change_language() {
|
|||||||
--msgbox "\n\n$(translate "Language changed to") $new_language" 10 50
|
--msgbox "\n\n$(translate "Language changed to") $new_language" 10 50
|
||||||
|
|
||||||
# Reload menu with new language
|
# Reload menu with new language
|
||||||
TMP_FILE=$(mktemp)
|
exec bash "$LOCAL_SCRIPTS/menus/config_menu.sh"
|
||||||
curl -s "$REPO_URL/scripts/menus/config_menu.sh" > "$TMP_FILE"
|
|
||||||
chmod +x "$TMP_FILE"
|
|
||||||
trap 'rm -f "$TMP_FILE"' EXIT
|
|
||||||
exec bash "$TMP_FILE"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -404,6 +448,9 @@ uninstall_proxmenu() {
|
|||||||
done
|
done
|
||||||
apt-get autoremove -y --purge >/dev/null 2>&1
|
apt-get autoremove -y --purge >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "80" ; echo "Removing ProxMenux Monitor..."
|
||||||
|
uninstall_proxmenux_monitor
|
||||||
|
|
||||||
echo "90" ; echo "Restoring system files..."
|
echo "90" ; echo "Restoring system files..."
|
||||||
# Restore .bashrc and motd
|
# Restore .bashrc and motd
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.0
|
# Version : 1.0
|
||||||
# Last Updated: 07/05/2025
|
# Last Updated: 07/05/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -26,30 +26,30 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
VM_REPO="$REPO_URL/scripts/vm"
|
VM_REPO="$LOCAL_SCRIPTS/vm"
|
||||||
ISO_REPO="$REPO_URL/scripts/vm"
|
ISO_REPO="$LOCAL_SCRIPTS/vm"
|
||||||
MENU_REPO="$REPO_URL/scripts/menus"
|
MENU_REPO="$LOCAL_SCRIPTS/menus"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
|
||||||
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
|
# Source utilities and required scripts
|
||||||
|
|
||||||
|
|
||||||
source <(curl -s "$VM_REPO/vm_configurator.sh")
|
|
||||||
source <(curl -s "$VM_REPO/disk_selector.sh")
|
|
||||||
source <(curl -s "$VM_REPO/vm_creator.sh")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if [[ -f "$UTILS_FILE" ]]; then
|
if [[ -f "$UTILS_FILE" ]]; then
|
||||||
source "$UTILS_FILE"
|
source "$UTILS_FILE"
|
||||||
|
else
|
||||||
|
echo "Error: $UTILS_FILE not found"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
load_language
|
load_language
|
||||||
initialize_cache
|
initialize_cache
|
||||||
|
|
||||||
|
# Source VM management scripts
|
||||||
|
[[ -f "$VM_REPO/vm_configurator.sh" ]] && source "$VM_REPO/vm_configurator.sh" || { echo "Error: vm_configurator.sh not found"; exit 1; }
|
||||||
|
[[ -f "$VM_REPO/disk_selector.sh" ]] && source "$VM_REPO/disk_selector.sh" || { echo "Error: disk_selector.sh not found"; exit 1; }
|
||||||
|
[[ -f "$VM_REPO/vm_creator.sh" ]] && source "$VM_REPO/vm_creator.sh" || { echo "Error: vm_creator.sh not found"; exit 1; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function header_info() {
|
function header_info() {
|
||||||
@@ -108,17 +108,17 @@ while true; do
|
|||||||
3>&1 1>&2 2>&3)
|
3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
|
||||||
[[ $? -ne 0 || "$OS_TYPE" == "6" ]] && exec bash <(curl -s "$MENU_REPO/main_menu.sh")
|
[[ $? -ne 0 || "$OS_TYPE" == "6" ]] && exec bash "$MENU_REPO/main_menu.sh"
|
||||||
|
|
||||||
case "$OS_TYPE" in
|
case "$OS_TYPE" in
|
||||||
1)
|
1)
|
||||||
source <(curl -fsSL "$ISO_REPO/select_nas_iso.sh") && select_nas_iso || continue
|
source "$ISO_REPO/select_nas_iso.sh" && select_nas_iso || continue
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
source <(curl -fsSL "$ISO_REPO/select_windows_iso.sh") && select_windows_iso || continue
|
source "$ISO_REPO/select_windows_iso.sh" && select_windows_iso || continue
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
source <(curl -fsSL "$ISO_REPO/select_linux_iso.sh") && select_linux_iso || continue
|
source "$ISO_REPO/select_linux_iso.sh" && select_linux_iso || continue
|
||||||
;;
|
;;
|
||||||
4)
|
4)
|
||||||
whiptail --title "OSX-PROXMOX" --yesno "$(translate "This is an external script that creates a macOS VM in Proxmox VE in just a few steps, whether you are using AMD or Intel hardware.")\n\n$(translate "The script clones the osx-proxmox.com repository and once the setup is complete, the server will automatically reboot.")\n\n$(translate "Make sure there are no critical services running as they will be interrupted. Ensure your server can be safely rebooted.")\n\n$(translate "Visit https://osx-proxmox.com for more information.")\n\n$(translate "Do you want to run the script now?")" 24 70
|
whiptail --title "OSX-PROXMOX" --yesno "$(translate "This is an external script that creates a macOS VM in Proxmox VE in just a few steps, whether you are using AMD or Intel hardware.")\n\n$(translate "The script clones the osx-proxmox.com repository and once the setup is complete, the server will automatically reboot.")\n\n$(translate "Make sure there are no critical services running as they will be interrupted. Ensure your server can be safely rebooted.")\n\n$(translate "Visit https://osx-proxmox.com for more information.")\n\n$(translate "Do you want to run the script now?")" 24 70
|
||||||
@@ -128,7 +128,7 @@ while true; do
|
|||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
5)
|
5)
|
||||||
source <(curl -fsSL "$ISO_REPO/select_linux_iso.sh") && select_linux_other_scripts || continue
|
source "$ISO_REPO/select_linux_iso.sh" && select_linux_other_scripts || continue
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -149,26 +149,3 @@ while true; do
|
|||||||
create_vm
|
create_vm
|
||||||
break
|
break
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function start_vm_configuration() {
|
|
||||||
|
|
||||||
if (whiptail --title "ProxMenux" --yesno "$(translate "Use Default Settings?")" --no-button "$(translate "Advanced")" 10 60); then
|
|
||||||
header_info
|
|
||||||
load_default_vm_config "$OS_TYPE"
|
|
||||||
|
|
||||||
if [[ -z "$HN" ]]; then
|
|
||||||
HN=$(whiptail --inputbox "$(translate "Enter a name for the new virtual machine:")" 10 60 --title "VM Hostname" 3>&1 1>&2 2>&3)
|
|
||||||
[[ -z "$HN" ]] && HN="custom-vm"
|
|
||||||
fi
|
|
||||||
|
|
||||||
apply_default_vm_config
|
|
||||||
else
|
|
||||||
header_info
|
|
||||||
echo -e "${CUS}$(translate "Using advanced configuration")${CL}"
|
|
||||||
configure_vm_advanced "$OS_TYPE"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.0
|
# Version : 1.0
|
||||||
# Last Updated: 28/01/2025
|
# Last Updated: 28/01/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -35,24 +35,24 @@ initialize_cache
|
|||||||
|
|
||||||
case $OPTION in
|
case $OPTION in
|
||||||
1)
|
1)
|
||||||
bash <(curl -s "$REPO_URL/scripts/configure_igpu_lxc.sh")
|
bash "$LOCAL_SCRIPTS/configure_igpu_lxc.sh"
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
bash <(curl -s "$REPO_URL/scripts/install_coral_lxc.sh")
|
bash "$LOCAL_SCRIPTS/install_coral_lxc.sh"
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
bash <(curl -s "$REPO_URL/scripts/gpu_tpu/install_coral_pve9.sh")
|
bash "$LOCAL_SCRIPTS/gpu_tpu/install_coral_pve9.sh"
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
4) exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") ;;
|
4) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
|
||||||
*) exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") ;;
|
*) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.0
|
# Version : 1.0
|
||||||
# Last Updated: 19/08/2025
|
# Last Updated: 19/08/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -40,22 +40,23 @@ show_main_menu() {
|
|||||||
|
|
||||||
case $CHOICE in
|
case $CHOICE in
|
||||||
1)
|
1)
|
||||||
bash <(curl -s "$REPO_URL/scripts/lxc/lxc-privileged-to-unprivileged.sh")
|
bash "$LOCAL_SCRIPTS/lxc/lxc-privileged-to-unprivileged.sh"
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
bash <(curl -s "$REPO_URL/scripts/lxc/lxc-unprivileged-to-privileged.sh")
|
bash "$LOCAL_SCRIPTS/lxc/lxc-unprivileged-to-privileged.sh"
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
show_container_status
|
show_container_status
|
||||||
;;
|
;;
|
||||||
4)
|
4)
|
||||||
bash <(curl -s "$REPO_URL/scripts/lxc/lxc-conversion-manual-guide.sh")
|
bash "$LOCAL_SCRIPTS/lxc/lxc-conversion-manual-guide.sh"
|
||||||
;;
|
;;
|
||||||
5)
|
5)
|
||||||
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +100,4 @@ show_container_status() {
|
|||||||
show_main_menu
|
show_main_menu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
show_main_menu
|
||||||
|
|
||||||
show_main_menu
|
|
||||||
|
|||||||
+14
-17
@@ -5,13 +5,13 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 2.0
|
# Version : 2.0
|
||||||
# Last Updated: 04/04/2025
|
# Last Updated: 04/04/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -54,10 +54,10 @@ check_pve9_translation_compatibility() {
|
|||||||
--title "Translation Environment Incompatible with PVE $pve_version" \
|
--title "Translation Environment Incompatible with PVE $pve_version" \
|
||||||
--msgbox "NOTICE: You are running Proxmox VE $pve_version with translation components installed.\n\nTranslations are NOT supported in PVE 9+. This causes:\n• Menu loading errors\n• Translation failures\n• System instability\n\nREQUIRED ACTION:\nProxMenux will now automatically reinstall the Normal Version.\n\nThis process will:\n• Remove incompatible translation components\n• Install PVE 9+ compatible version\n• Preserve all your settings and preferences\n\nPress OK to continue with automatic reinstallation..." 20 75
|
--msgbox "NOTICE: You are running Proxmox VE $pve_version with translation components installed.\n\nTranslations are NOT supported in PVE 9+. This causes:\n• Menu loading errors\n• Translation failures\n• System instability\n\nREQUIRED ACTION:\nProxMenux will now automatically reinstall the Normal Version.\n\nThis process will:\n• Remove incompatible translation components\n• Install PVE 9+ compatible version\n• Preserve all your settings and preferences\n\nPress OK to continue with automatic reinstallation..." 20 75
|
||||||
|
|
||||||
bash <(curl -sSL "$REPO_URL/install_proxmenux.sh")
|
bash "$BASE_DIR/install_proxmenux.sh"
|
||||||
|
|
||||||
fi
|
fi
|
||||||
exit
|
exit 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +90,6 @@ show_menu() {
|
|||||||
while true; do
|
while true; do
|
||||||
|
|
||||||
local menu_title="Main ProxMenux"
|
local menu_title="Main ProxMenux"
|
||||||
if [[ -n "$PROXMENUX_PVE9_WARNING_SHOWN" ]]; then
|
|
||||||
menu_title="Main ProxMenux"
|
|
||||||
fi
|
|
||||||
|
|
||||||
dialog --clear \
|
dialog --clear \
|
||||||
--backtitle "ProxMenux" \
|
--backtitle "ProxMenux" \
|
||||||
@@ -122,16 +119,16 @@ show_menu() {
|
|||||||
OPTION=$(<"$TEMP_FILE")
|
OPTION=$(<"$TEMP_FILE")
|
||||||
|
|
||||||
case $OPTION in
|
case $OPTION in
|
||||||
1) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_post_install.sh") ;;
|
1) exec bash "$LOCAL_SCRIPTS/menus/menu_post_install.sh" ;;
|
||||||
2) exec bash <(curl -s "$REPO_URL/scripts/menus/hw_grafics_menu.sh") ;;
|
2) exec bash "$LOCAL_SCRIPTS/menus/hw_grafics_menu.sh" ;;
|
||||||
3) exec bash <(curl -s "$REPO_URL/scripts/menus/create_vm_menu.sh") ;;
|
3) exec bash "$LOCAL_SCRIPTS/menus/create_vm_menu.sh" ;;
|
||||||
4) exec bash <(curl -s "$REPO_URL/scripts/menus/storage_menu.sh") ;;
|
4) exec bash "$LOCAL_SCRIPTS/menus/storage_menu.sh" ;;
|
||||||
5) exec bash <(curl -s "$REPO_URL/scripts/menus/share_menu.sh") ;;
|
5) exec bash "$LOCAL_SCRIPTS/menus/share_menu.sh" ;;
|
||||||
6) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_Helper_Scripts.sh") ;;
|
6) exec bash "$LOCAL_SCRIPTS/menus/menu_Helper_Scripts.sh" ;;
|
||||||
7) exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh") ;;
|
7) exec bash "$LOCAL_SCRIPTS/menus/network_menu.sh" ;;
|
||||||
8) exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh") ;;
|
8) exec bash "$LOCAL_SCRIPTS/menus/utilities_menu.sh" ;;
|
||||||
h) bash <(curl -s "$REPO_URL/scripts/help_info_menu.sh") ;;
|
h) bash "$LOCAL_SCRIPTS/help_info_menu.sh" ;;
|
||||||
s) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;;
|
s) exec bash "$LOCAL_SCRIPTS/menus/config_menu.sh" ;;
|
||||||
0) clear; msg_ok "$(translate "Thank you for using ProxMenux. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
|
0) clear; msg_ok "$(translate "Thank you for using ProxMenux. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
|
||||||
*) msg_warn "$(translate "Invalid option")"; sleep 2 ;;
|
*) msg_warn "$(translate "Invalid option")"; sleep 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
+11
-13
@@ -5,13 +5,13 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 2.0
|
# Version : 2.0
|
||||||
# Last Updated: 04/04/2025
|
# Last Updated: 04/04/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -62,21 +62,19 @@ show_menu() {
|
|||||||
OPTION=$(<"$TEMP_FILE")
|
OPTION=$(<"$TEMP_FILE")
|
||||||
|
|
||||||
case $OPTION in
|
case $OPTION in
|
||||||
1) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_post_install.sh") ;;
|
1) exec bash "$LOCAL_SCRIPTS/menus/menu_post_install.sh" ;;
|
||||||
2) bash <(curl -s "$REPO_URL/scripts/help_info_menu.sh") ;;
|
2) bash "$LOCAL_SCRIPTS/help_info_menu.sh" ;;
|
||||||
3) exec bash <(curl -s "$REPO_URL/scripts/menus/hw_grafics_menu.sh") ;;
|
3) exec bash "$LOCAL_SCRIPTS/menus/hw_grafics_menu.sh" ;;
|
||||||
4) exec bash <(curl -s "$REPO_URL/scripts/menus/create_vm_menu.sh") ;;
|
4) exec bash "$LOCAL_SCRIPTS/menus/create_vm_menu.sh" ;;
|
||||||
5) exec bash <(curl -s "$REPO_URL/scripts/menus/storage_menu.sh") ;;
|
5) exec bash "$LOCAL_SCRIPTS/menus/storage_menu.sh" ;;
|
||||||
6) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_Helper_Scripts.sh") ;;
|
6) exec bash "$LOCAL_SCRIPTS/menus/menu_Helper_Scripts.sh" ;;
|
||||||
7) exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh") ;;
|
7) exec bash "$LOCAL_SCRIPTS/menus/network_menu.sh" ;;
|
||||||
8) exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh") ;;
|
8) exec bash "$LOCAL_SCRIPTS/menus/utilities_menu.sh" ;;
|
||||||
9) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;;
|
9) exec bash "$LOCAL_SCRIPTS/menus/config_menu.sh" ;;
|
||||||
0) clear; msg_ok "$(translate "Thank you for using ProxMenux. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
|
0) clear; msg_ok "$(translate "Thank you for using ProxMenux. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
|
||||||
*) msg_warn "$(translate "Invalid option")"; sleep 2 ;;
|
*) msg_warn "$(translate "Invalid option")"; sleep 2 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
show_menu
|
show_menu
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.1
|
# Version : 1.2
|
||||||
# Last Updated: 04/06/2025
|
# Last Updated: 14/11/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Description:
|
# Description:
|
||||||
# This script provides a simple and efficient way to access and execute Proxmox VE scripts
|
# This script provides a simple and efficient way to access and execute Proxmox VE scripts
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -52,7 +52,8 @@ while read -r id name; do
|
|||||||
done < <(echo "$META_JSON" | jq -r '.categories[] | "\(.id)\t\(.name)"')
|
done < <(echo "$META_JSON" | jq -r '.categories[] | "\(.id)\t\(.name)"')
|
||||||
|
|
||||||
declare -A CATEGORY_COUNT
|
declare -A CATEGORY_COUNT
|
||||||
for id in $(echo "$CACHE_JSON" | jq -r '.[].categories[]'); do
|
for id in $(echo "$CACHE_JSON" | jq -r '
|
||||||
|
group_by(.slug) | map(.[0])[] | .categories[]'); do
|
||||||
((CATEGORY_COUNT[$id]++))
|
((CATEGORY_COUNT[$id]++))
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -112,74 +113,121 @@ format_credentials() {
|
|||||||
echo "$credentials_info"
|
echo "$credentials_info"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
run_script_by_slug() {
|
run_script_by_slug() {
|
||||||
local slug="$1"
|
local slug="$1"
|
||||||
local script_info
|
local -a script_infos
|
||||||
script_info=$(echo "$CACHE_JSON" | jq -r --arg slug "$slug" '.[] | select(.slug == $slug) | @base64')
|
mapfile -t script_infos < <(echo "$CACHE_JSON" | jq -r --arg slug "$slug" '.[] | select(.slug == $slug) | @base64')
|
||||||
|
|
||||||
|
if [[ ${#script_infos[@]} -eq 0 ]]; then
|
||||||
|
dialog --title "Helper Scripts" --msgbox "Error: No script data found for slug: $slug" 8 60
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
decode() {
|
decode() {
|
||||||
echo "$1" | base64 --decode | jq -r "$2"
|
echo "$1" | base64 --decode | jq -r "$2"
|
||||||
}
|
}
|
||||||
|
|
||||||
local name desc script_url notes
|
local first="${script_infos[0]}"
|
||||||
name=$(decode "$script_info" ".name")
|
local name desc notes
|
||||||
desc=$(decode "$script_info" ".desc")
|
name=$(decode "$first" ".name")
|
||||||
script_url=$(decode "$script_info" ".script_url")
|
desc=$(decode "$first" ".desc")
|
||||||
notes=$(decode "$script_info" ".notes | join(\"\n\")")
|
notes=$(decode "$first" ".notes | join(\"\n\")")
|
||||||
|
|
||||||
|
|
||||||
local notes_dialog=""
|
local notes_dialog=""
|
||||||
if [[ -n "$notes" ]]; then
|
if [[ -n "$notes" ]]; then
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
notes_dialog+="• $line\n"
|
notes_dialog+="• $line\n"
|
||||||
done <<< "$notes"
|
done <<< "$notes"
|
||||||
notes_dialog="${notes_dialog%\\n}"
|
notes_dialog="${notes_dialog%\\n}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
local credentials
|
local credentials
|
||||||
credentials=$(format_credentials "$script_info")
|
credentials=$(format_credentials "$first")
|
||||||
|
|
||||||
|
|
||||||
local msg="\Zb\Z4Descripción:\Zn\n$desc"
|
local msg="\Zb\Z4Descripción:\Zn\n$desc"
|
||||||
[[ -n "$notes_dialog" ]] && msg+="\n\n\Zb\Z4Notes:\Zn\n$notes_dialog"
|
[[ -n "$notes_dialog" ]] && msg+="\n\n\Zb\Z4Notes:\Zn\n$notes_dialog"
|
||||||
[[ -n "$credentials" ]] && msg+="\n\n\Zb\Z4Default Credentials:\Zn\n$credentials"
|
[[ -n "$credentials" ]] && msg+="\n\n\Zb\Z4Default Credentials:\Zn\n$credentials"
|
||||||
|
|
||||||
|
# Add separator before menu options
|
||||||
|
msg+="\n\n$(translate "Choose how to run the script:"):"
|
||||||
|
|
||||||
dialog --clear --colors --backtitle "ProxMenux" --title "$name" --yesno "$msg\n\nExecute this script?" 22 85
|
declare -a MENU_OPTS=()
|
||||||
if [[ $? -eq 0 ]]; then
|
local idx=0
|
||||||
download_script "$script_url"
|
for s in "${script_infos[@]}"; do
|
||||||
echo
|
local os script_url script_url_mirror script_name
|
||||||
echo
|
os=$(decode "$s" ".os // empty")
|
||||||
|
[[ -z "$os" ]] && os="$(translate "default")"
|
||||||
|
script_name=$(decode "$s" ".name")
|
||||||
|
script_url=$(decode "$s" ".script_url")
|
||||||
|
script_url_mirror=$(decode "$s" ".script_url_mirror // empty")
|
||||||
|
|
||||||
if [[ -n "$desc" || -n "$notes" || -n "$credentials" ]]; then
|
MENU_OPTS+=("${idx}_GH" "$os | $script_name | GitHub")
|
||||||
echo -e "$TAB\e[1;36mScript Information:\e[0m"
|
|
||||||
|
|
||||||
|
if [[ -n "$script_url_mirror" ]]; then
|
||||||
|
MENU_OPTS+=("${idx}_MR" "$os | $script_name | Mirror")
|
||||||
if [[ -n "$notes" ]]; then
|
|
||||||
echo -e "$TAB\e[1;33mNotes:\e[0m"
|
|
||||||
while IFS= read -r line; do
|
|
||||||
[[ -z "$line" ]] && continue
|
|
||||||
echo -e "$TAB• $line"
|
|
||||||
done <<< "$notes"
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if [[ -n "$credentials" ]]; then
|
|
||||||
echo -e "$TAB\e[1;32mDefault Credentials:\e[0m"
|
|
||||||
echo "$TAB$credentials"
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
msg_success "Press Enter to return to the main menu..."
|
((idx++))
|
||||||
read -r
|
done
|
||||||
RETURN_TO_MAIN=true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
local choice
|
||||||
|
choice=$(dialog --clear --colors --backtitle "ProxMenux" \
|
||||||
|
--title "$name" \
|
||||||
|
--menu "$msg" 28 80 6 \
|
||||||
|
"${MENU_OPTS[@]}" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
if [[ $? -ne 0 || -z "$choice" ]]; then
|
||||||
|
RETURN_TO_MAIN=false
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local sel_idx sel_src
|
||||||
|
IFS="_" read -r sel_idx sel_src <<< "$choice"
|
||||||
|
|
||||||
|
local selected="${script_infos[$sel_idx]}"
|
||||||
|
local gh_url mirror_url
|
||||||
|
gh_url=$(decode "$selected" ".script_url")
|
||||||
|
mirror_url=$(decode "$selected" ".script_url_mirror // empty")
|
||||||
|
|
||||||
|
if [[ "$sel_src" == "GH" ]]; then
|
||||||
|
download_script "$gh_url"
|
||||||
|
elif [[ "$sel_src" == "MR" ]]; then
|
||||||
|
if [[ -n "$mirror_url" ]]; then
|
||||||
|
download_script "$mirror_url"
|
||||||
|
else
|
||||||
|
dialog --title "Helper Scripts" --msgbox "$(translate "Mirror URL not available for this script.")" 8 60
|
||||||
|
RETURN_TO_MAIN=false
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ -n "$desc" || -n "$notes" || -n "$credentials" ]]; then
|
||||||
|
echo -e "$TAB\e[1;36mScript Information:\e[0m"
|
||||||
|
|
||||||
|
if [[ -n "$notes" ]]; then
|
||||||
|
echo -e "$TAB\e[1;33mNotes:\e[0m"
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
echo -e "$TAB• $line"
|
||||||
|
done <<< "$notes"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$credentials" ]]; then
|
||||||
|
echo -e "$TAB\e[1;32mDefault Credentials:\e[0m"
|
||||||
|
echo "$TAB$credentials"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_success "Press Enter to return to the main menu..."
|
||||||
|
read -r
|
||||||
|
RETURN_TO_MAIN=true
|
||||||
|
}
|
||||||
|
|
||||||
search_and_filter_scripts() {
|
search_and_filter_scripts() {
|
||||||
local search_term=""
|
local search_term=""
|
||||||
@@ -204,7 +252,7 @@ search_and_filter_scripts() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local count
|
local count
|
||||||
count=$(echo "$filtered_json" | jq length)
|
count=$(echo "$filtered_json" | jq 'group_by(.slug) | length')
|
||||||
|
|
||||||
if [[ $count -eq 0 ]]; then
|
if [[ $count -eq 0 ]]; then
|
||||||
dialog --msgbox "No scripts found for: '$search_term'\n\nTry a different search term." 8 50
|
dialog --msgbox "No scripts found for: '$search_term'\n\nTry a different search term." 8 50
|
||||||
@@ -226,7 +274,7 @@ search_and_filter_scripts() {
|
|||||||
menu_items+=("$i" "$entry")
|
menu_items+=("$i" "$entry")
|
||||||
((i++))
|
((i++))
|
||||||
done < <(echo "$filtered_json" | jq -r '
|
done < <(echo "$filtered_json" | jq -r '
|
||||||
sort_by(.name)[] | [.slug, .name, .type] | @tsv')
|
group_by(.slug) | map(.[0]) | sort_by(.name)[] | [.slug, .name, .type] | @tsv')
|
||||||
|
|
||||||
menu_items+=("" "")
|
menu_items+=("" "")
|
||||||
menu_items+=("new_search" "New Search")
|
menu_items+=("new_search" "New Search")
|
||||||
@@ -256,7 +304,7 @@ search_and_filter_scripts() {
|
|||||||
"show_all")
|
"show_all")
|
||||||
search_term=""
|
search_term=""
|
||||||
filtered_json="$CACHE_JSON"
|
filtered_json="$CACHE_JSON"
|
||||||
count=$(echo "$filtered_json" | jq length)
|
count=$(echo "$filtered_json" | jq 'group_by(.slug) | length')
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
"back"|"")
|
"back"|"")
|
||||||
@@ -290,10 +338,9 @@ while true; do
|
|||||||
SELECTED=$(dialog --backtitle "ProxMenux" --title "Proxmox VE Helper-Scripts" --menu \
|
SELECTED=$(dialog --backtitle "ProxMenux" --title "Proxmox VE Helper-Scripts" --menu \
|
||||||
"Select a category or search for scripts:" 20 70 14 \
|
"Select a category or search for scripts:" 20 70 14 \
|
||||||
"${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
|
"${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
|
||||||
dialog --clear --title "Proxmox VE Helper-Scripts" \
|
dialog --clear --title "ProxMenux" \
|
||||||
--msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70
|
--msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70
|
||||||
#clear
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "$SELECTED" == "search" ]]; then
|
if [[ "$SELECTED" == "search" ]]; then
|
||||||
@@ -312,8 +359,17 @@ while true; do
|
|||||||
entry="$padded_name $label"
|
entry="$padded_name $label"
|
||||||
SCRIPTS+=("$i" "$entry")
|
SCRIPTS+=("$i" "$entry")
|
||||||
((i++))
|
((i++))
|
||||||
done < <(echo "$CACHE_JSON" | jq -r --argjson id "$SELECTED" \
|
done < <(echo "$CACHE_JSON" | jq -r --argjson id "$SELECTED" '
|
||||||
'[.[] | select(.categories | index($id)) | {slug, name, type}] | sort_by(.name)[] | [.slug, .name, .type] | @tsv')
|
[
|
||||||
|
.[]
|
||||||
|
| select(.categories | index($id))
|
||||||
|
| {slug, name, type}
|
||||||
|
]
|
||||||
|
| group_by(.slug)
|
||||||
|
| map(.[0])
|
||||||
|
| sort_by(.name)[]
|
||||||
|
| [.slug, .name, .type]
|
||||||
|
| @tsv')
|
||||||
|
|
||||||
SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" --title "Scripts in ${CATEGORY_NAMES[$SELECTED]}" --menu \
|
SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" --title "Scripts in ${CATEGORY_NAMES[$SELECTED]}" --menu \
|
||||||
"Choose a script to execute:" 20 70 14 \
|
"Choose a script to execute:" 20 70 14 \
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.2
|
# Version : 1.2
|
||||||
# Last Updated: 06/07/2025
|
# Last Updated: 06/07/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -70,7 +70,7 @@ confirm_automated_script() {
|
|||||||
clear
|
clear
|
||||||
|
|
||||||
if [ $response -eq 0 ]; then
|
if [ $response -eq 0 ]; then
|
||||||
bash <(curl -s $REPO_URL/scripts/post_install/auto_post_install.sh)
|
bash "$LOCAL_SCRIPTS/post_install/auto_post_install.sh"
|
||||||
else
|
else
|
||||||
msg_warn "$(translate "Cancelled by user.")"
|
msg_warn "$(translate "Cancelled by user.")"
|
||||||
sleep 1
|
sleep 1
|
||||||
@@ -80,9 +80,9 @@ confirm_automated_script() {
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
declare -a PROXMENUX_SCRIPTS=(
|
declare -a PROXMENUX_SCRIPTS=(
|
||||||
"Customizable post-installation script|ProxMenux|bash <(curl -s $REPO_URL/scripts/post_install/customizable_post_install.sh)"
|
"Customizable post-installation script|ProxMenux|bash \"$LOCAL_SCRIPTS/post_install/customizable_post_install.sh\""
|
||||||
"Automated post-installation script|ProxMenux|confirm_automated_script"
|
"Automated post-installation script|ProxMenux|confirm_automated_script"
|
||||||
"Uninstall optimizations|ProxMenux|bash <(curl -s $REPO_URL/scripts/post_install/uninstall-tools.sh)"
|
"Uninstall optimizations|ProxMenux|bash \"$LOCAL_SCRIPTS/post_install/uninstall-tools.sh\""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ show_menu() {
|
|||||||
|
|
||||||
|
|
||||||
if [ $exit_status -ne 0 ] || [ "$script_selection" = "0" ]; then
|
if [ $exit_status -ne 0 ] || [ "$script_selection" = "0" ]; then
|
||||||
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.1
|
# Version : 1.1
|
||||||
# Last Updated: 08/07/2025
|
# Last Updated: 08/07/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
# Special thanks to @Andres_Eduardo_Rojas_Moya for contributing the persistent
|
# Special thanks to @Andres_Eduardo_Rojas_Moya for contributing the persistent
|
||||||
# network naming function and for the original idea.
|
# network naming function and for the original idea.
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -1082,7 +1082,7 @@ show_menu() {
|
|||||||
|
|
||||||
|
|
||||||
if [ $exit_status -ne 0 ] || [ "$script_selection" = "0" ]; then
|
if [ $exit_status -ne 0 ] || [ "$script_selection" = "0" ]; then
|
||||||
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+13
-13
@@ -10,7 +10,7 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -43,7 +43,7 @@ while true; do
|
|||||||
"h" "$(translate "Help & Info (commands)")" \
|
"h" "$(translate "Help & Info (commands)")" \
|
||||||
"0" "$(translate "Return to Main Menu")" \
|
"0" "$(translate "Return to Main Menu")" \
|
||||||
2>&1 >/dev/tty
|
2>&1 >/dev/tty
|
||||||
) || { exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh"); }
|
) || { exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"; }
|
||||||
|
|
||||||
case "$OPTION" in
|
case "$OPTION" in
|
||||||
|
|
||||||
@@ -52,37 +52,37 @@ while true; do
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
1)
|
1)
|
||||||
bash <(curl -s "$REPO_URL/scripts/share/nfs_host.sh")
|
bash "$LOCAL_SCRIPTS/share/nfs_host.sh"
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
bash <(curl -s "$REPO_URL/scripts/share/samba_host.sh")
|
bash "$LOCAL_SCRIPTS/share/samba_host.sh"
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
bash <(curl -s "$REPO_URL/scripts/share/local-shared-manager.sh")
|
bash "$LOCAL_SCRIPTS/share/local-shared-manager.sh"
|
||||||
;;
|
;;
|
||||||
4)
|
4)
|
||||||
bash <(curl -s "$REPO_URL/scripts/share/lxc-mount-manager_minimal.sh")
|
bash "$LOCAL_SCRIPTS/share/lxc-mount-manager_minimal.sh"
|
||||||
;;
|
;;
|
||||||
5)
|
5)
|
||||||
bash <(curl -s "$REPO_URL/scripts/share/nfs_client.sh")
|
bash "$LOCAL_SCRIPTS/share/nfs_client.sh"
|
||||||
;;
|
;;
|
||||||
6)
|
6)
|
||||||
bash <(curl -s "$REPO_URL/scripts/share/samba_client.sh")
|
bash "$LOCAL_SCRIPTS/share/samba_client.sh"
|
||||||
;;
|
;;
|
||||||
7)
|
7)
|
||||||
bash <(curl -s "$REPO_URL/scripts/share/nfs_lxc_server.sh")
|
bash "$LOCAL_SCRIPTS/share/nfs_lxc_server.sh"
|
||||||
;;
|
;;
|
||||||
8)
|
8)
|
||||||
bash <(curl -s "$REPO_URL/scripts/share/samba_lxc_server.sh")
|
bash "$LOCAL_SCRIPTS/share/samba_lxc_server.sh"
|
||||||
;;
|
;;
|
||||||
h)
|
h)
|
||||||
bash <(curl -s "$REPO_URL/scripts/share/commands_share.sh")
|
bash "$LOCAL_SCRIPTS/share/commands_share.sh"
|
||||||
;;
|
;;
|
||||||
0)
|
0)
|
||||||
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|||||||
+11
-12
@@ -5,14 +5,14 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.0
|
# Version : 1.0
|
||||||
# Last Updated: 28/01/2025
|
# Last Updated: 28/01/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -39,38 +39,37 @@ while true; do
|
|||||||
case $OPTION in
|
case $OPTION in
|
||||||
1)
|
1)
|
||||||
msg_info2 "$(translate "Running script: Add Disk Passthrough to a VM")..."
|
msg_info2 "$(translate "Running script: Add Disk Passthrough to a VM")..."
|
||||||
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough.sh")
|
bash "$LOCAL_SCRIPTS/storage/disk-passthrough.sh"
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
msg_info2 "$(translate "Running script: Add Disk Passthrough to a CT")..."
|
msg_info2 "$(translate "Running script: Add Disk Passthrough to a CT")..."
|
||||||
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough_ct.sh")
|
bash "$LOCAL_SCRIPTS/storage/disk-passthrough_ct.sh"
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
msg_info2 "$(translate "Running script: Import Disk Image to a VM")..."
|
msg_info2 "$(translate "Running script: Import Disk Image to a VM")..."
|
||||||
bash <(curl -s "$REPO_URL/scripts/storage/import-disk-image.sh")
|
bash "$LOCAL_SCRIPTS/storage/import-disk-image.sh"
|
||||||
;;
|
;;
|
||||||
4)
|
4)
|
||||||
msg_info2 "$(translate "Running script: Mount point to CT")..."
|
msg_info2 "$(translate "Running script: Mount point to CT")..."
|
||||||
bash <(curl -s "$REPO_URL/scripts/storage/mount-point-to-ct.sh")
|
bash "$LOCAL_SCRIPTS/storage/mount-point-to-ct.sh"
|
||||||
;;
|
;;
|
||||||
5)
|
5)
|
||||||
msg_info2 "$(translate "Running script: Mount disk on HOST")..."
|
msg_info2 "$(translate "Running script: Mount disk on HOST")..."
|
||||||
bash <(curl -s "$REPO_URL/scripts/storage/mount-disk-on-host.sh")
|
bash "$LOCAL_SCRIPTS/storage/mount-disk-on-host.sh"
|
||||||
;;
|
;;
|
||||||
6)
|
6)
|
||||||
msg_info2 "$(translate "Running script: Unmount disk from HOST")..."
|
msg_info2 "$(translate "Running script: Unmount disk from HOST")..."
|
||||||
bash <(curl -s "$REPO_URL/scripts/storage/unmount-disk-from-host.sh")
|
bash "$LOCAL_SCRIPTS/storage/unmount-disk-from-host.sh"
|
||||||
;;
|
;;
|
||||||
7)
|
7)
|
||||||
msg_info2 "$(translate "Running script: Format disk")..."
|
msg_info2 "$(translate "Running script: Format disk")..."
|
||||||
bash <(curl -s "$REPO_URL/scripts/storage/format-disk.sh")
|
bash "$LOCAL_SCRIPTS/storage/format-disk.sh"
|
||||||
;;
|
;;
|
||||||
8)
|
8)
|
||||||
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.1
|
# Version : 1.1
|
||||||
# Last Updated: 15/04/2025
|
# Last Updated: 15/04/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -36,19 +36,19 @@ while true; do
|
|||||||
|
|
||||||
case $OPTION in
|
case $OPTION in
|
||||||
1)
|
1)
|
||||||
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough.sh")
|
bash "$LOCAL_SCRIPTS/storage/disk-passthrough.sh"
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough_ct.sh")
|
bash "$LOCAL_SCRIPTS/storage/disk-passthrough_ct.sh"
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
bash <(curl -s "$REPO_URL/scripts/storage/import-disk-image.sh")
|
bash "$LOCAL_SCRIPTS/storage/import-disk-image.sh"
|
||||||
;;
|
;;
|
||||||
4)
|
4)
|
||||||
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
|
# License : (CC BY-NC 4.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.0
|
# Version : 1.0
|
||||||
# Last Updated: 02/07/2025
|
# Last Updated: 02/07/2025
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
@@ -36,13 +36,13 @@ initialize_cache
|
|||||||
|
|
||||||
case $OPTION in
|
case $OPTION in
|
||||||
1)
|
1)
|
||||||
bash <(curl -s "$REPO_URL/scripts/utilities/uup_dump_iso_creator.sh")
|
bash "$LOCAL_SCRIPTS/utilities/uup_dump_iso_creator.sh"
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
bash <(curl -s "$REPO_URL/scripts/utilities/system_utils.sh")
|
bash "$LOCAL_SCRIPTS/utilities/system_utils.sh"
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -64,19 +64,20 @@ initialize_cache
|
|||||||
|
|
||||||
dialog_result=$?
|
dialog_result=$?
|
||||||
if [[ $dialog_result -eq 0 ]]; then
|
if [[ $dialog_result -eq 0 ]]; then
|
||||||
bash <(curl -s "$REPO_URL/scripts/utilities/proxmox_update.sh")
|
bash "$LOCAL_SCRIPTS/utilities/proxmox_update.sh"
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
4)
|
4)
|
||||||
bash <(curl -s "$REPO_URL/scripts/utilities/upgrade_pve8_to_pve9.sh")
|
bash "$LOCAL_SCRIPTS/utilities/upgrade_pve8_to_pve9.sh"
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
5) exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") ;;
|
5) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
|
||||||
*) exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") ;;
|
*) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user