From 1d47ad0c4b14e553e9c2c683a771d08d5f91d637 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 16:58:45 +0100 Subject: [PATCH 01/23] Update AppImage --- AppImage/scripts/build_appimage.sh | 1 + AppImage/scripts/flask_server.py | 34 +++++++++-- AppImage/scripts/jwt_middleware.py | 98 ++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 AppImage/scripts/jwt_middleware.py diff --git a/AppImage/scripts/build_appimage.sh b/AppImage/scripts/build_appimage.sh index 76d2045..d92686b 100644 --- a/AppImage/scripts/build_appimage.sh +++ b/AppImage/scripts/build_appimage.sh @@ -80,6 +80,7 @@ echo "📋 Copying Flask server..." 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" diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 7742e21..f040ef9 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -35,6 +35,7 @@ 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__) CORS(app) # Enable CORS for Next.js frontend @@ -1740,6 +1741,7 @@ def get_proxmox_storage(): # END OF CHANGES FOR get_proxmox_storage @app.route('/api/storage/summary', methods=['GET']) +@require_auth def api_storage_summary(): """Get storage summary without SMART data (optimized for Overview page)""" try: @@ -3474,7 +3476,7 @@ def get_detailed_gpu_info(gpu): 'shared': 0, '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 # Parse GTT (Graphics Translation Table) usage (está dentro de usage.usage) @@ -3488,7 +3490,7 @@ def get_detailed_gpu_info(gpu): else: # Add GTT to existing VRAM 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 # Parse engine utilization for this process (están dentro de usage.usage) @@ -4519,6 +4521,7 @@ def get_hardware_info(): @app.route('/api/system', methods=['GET']) +@require_auth def api_system(): """Get system information including CPU, memory, and temperature""" try: @@ -4575,21 +4578,25 @@ def api_system(): return jsonify({'error': str(e)}), 500 @app.route('/api/storage', methods=['GET']) +@require_auth def api_storage(): """Get storage information""" return jsonify(get_storage_info()) @app.route('/api/proxmox-storage', methods=['GET']) +@require_auth def api_proxmox_storage(): """Get Proxmox storage information""" return jsonify(get_proxmox_storage()) @app.route('/api/network', methods=['GET']) +@require_auth def api_network(): """Get network information""" return jsonify(get_network_info()) @app.route('/api/network/summary', methods=['GET']) +@require_auth def api_network_summary(): """Optimized network summary endpoint - returns basic network info without detailed analysis""" try: @@ -4668,6 +4675,7 @@ def api_network_summary(): return jsonify({'error': str(e)}), 500 @app.route('/api/network//metrics', methods=['GET']) +@require_auth def api_network_interface_metrics(interface_name): """Get historical metrics (RRD data) for a specific network interface""" try: @@ -4750,12 +4758,13 @@ def api_network_interface_metrics(interface_name): return jsonify({'error': str(e)}), 500 @app.route('/api/vms', methods=['GET']) +@require_auth def api_vms(): """Get virtual machine information""" return jsonify(get_proxmox_vms()) -# Add the new api_vm_metrics endpoint here @app.route('/api/vms//metrics', methods=['GET']) +@require_auth def api_vm_metrics(vmid): """Get historical metrics (RRD data) for a specific VM/LXC""" try: @@ -4822,6 +4831,7 @@ def api_vm_metrics(vmid): return jsonify({'error': str(e)}), 500 @app.route('/api/node/metrics', methods=['GET']) +@require_auth def api_node_metrics(): """Get historical metrics (RRD data) for the node""" try: @@ -4865,6 +4875,7 @@ def api_node_metrics(): return jsonify({'error': str(e)}), 500 @app.route('/api/logs', methods=['GET']) +@require_auth def api_logs(): """Get system logs""" try: @@ -4942,6 +4953,7 @@ def api_logs(): }) @app.route('/api/logs/download', methods=['GET']) +@require_auth def api_logs_download(): """Download system logs as a text file""" try: @@ -5000,6 +5012,7 @@ def api_logs_download(): return jsonify({'error': str(e)}), 500 @app.route('/api/notifications', methods=['GET']) +@require_auth def api_notifications(): """Get Proxmox notification history""" try: @@ -5116,6 +5129,7 @@ def api_notifications(): }) @app.route('/api/notifications/download', methods=['GET']) +@require_auth def api_notifications_download(): """Download complete log for a specific notification""" try: @@ -5171,6 +5185,7 @@ def api_notifications_download(): return jsonify({'error': str(e)}), 500 @app.route('/api/backups', methods=['GET']) +@require_auth def api_backups(): """Get list of all backup files from Proxmox storage""" try: @@ -5259,6 +5274,7 @@ def api_backups(): }) @app.route('/api/events', methods=['GET']) +@require_auth def api_events(): """Get recent Proxmox events and tasks""" try: @@ -5335,6 +5351,7 @@ def api_events(): }) @app.route('/api/task-log/') +@require_auth def get_task_log(upid): """Get complete task log from Proxmox using UPID""" try: @@ -5432,6 +5449,7 @@ def get_task_log(upid): return jsonify({'error': str(e)}), 500 @app.route('/api/health', methods=['GET']) +@require_auth def api_health(): """Health check endpoint""" return jsonify({ @@ -5441,6 +5459,7 @@ def api_health(): }) @app.route('/api/prometheus', methods=['GET']) +@require_auth def api_prometheus(): """Export metrics in Prometheus format""" try: @@ -5697,11 +5716,12 @@ def api_prometheus(): @app.route('/api/info', methods=['GET']) +@require_auth def api_info(): """Root endpoint with API information""" return jsonify({ 'name': 'ProxMenux Monitor API', - 'version': '1.0.0', + 'version': '1.0.1', 'endpoints': [ '/api/system', '/api/system-info', @@ -5725,6 +5745,7 @@ def api_info(): }) @app.route('/api/hardware', methods=['GET']) +@require_auth def api_hardware(): """Get hardware information""" try: @@ -5761,6 +5782,7 @@ def api_hardware(): return jsonify({'error': str(e)}), 500 @app.route('/api/gpu//realtime', methods=['GET']) +@require_auth def api_gpu_realtime(slot): """Get real-time GPU monitoring data for a specific GPU""" try: @@ -5823,6 +5845,7 @@ def api_gpu_realtime(slot): # CHANGE: Modificar el endpoint para incluir la información completa de IPs @app.route('/api/vms/', methods=['GET']) +@require_auth def get_vm_config(vmid): """Get detailed configuration for a specific VM/LXC""" try: @@ -5919,6 +5942,7 @@ def get_vm_config(vmid): return jsonify({'error': str(e)}), 500 @app.route('/api/vms//logs', methods=['GET']) +@require_auth def api_vm_logs(vmid): """Download real logs for a specific VM/LXC (not task history)""" try: @@ -5968,6 +5992,7 @@ def api_vm_logs(vmid): return jsonify({'error': str(e)}), 500 @app.route('/api/vms//control', methods=['POST']) +@require_auth def api_vm_control(vmid): """Control VM/LXC (start, stop, shutdown, reboot)""" try: @@ -6020,6 +6045,7 @@ def api_vm_control(vmid): return jsonify({'error': str(e)}), 500 @app.route('/api/vms//config', methods=['PUT']) +@require_auth def api_vm_config_update(vmid): """Update VM/LXC configuration (description/notes)""" try: diff --git a/AppImage/scripts/jwt_middleware.py b/AppImage/scripts/jwt_middleware.py new file mode 100644 index 0000000..291edcf --- /dev/null +++ b/AppImage/scripts/jwt_middleware.py @@ -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 " 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 " + }), 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 From 364e80826136fd113b85d5179f5c82b26ae3100c Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 17:14:47 +0100 Subject: [PATCH 02/23] Update api-config.ts --- AppImage/lib/api-config.ts | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/AppImage/lib/api-config.ts b/AppImage/lib/api-config.ts index 8ee2ff6..63e8a66 100644 --- a/AppImage/lib/api-config.ts +++ b/AppImage/lib/api-config.ts @@ -60,6 +60,18 @@ export function getApiUrl(endpoint: string): string { 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 + } + return localStorage.getItem("proxmenux-auth-token") +} + /** * Fetches data from an API endpoint with error handling * @@ -70,12 +82,20 @@ export function getApiUrl(endpoint: string): string { export async function fetchApi(endpoint: string, options?: RequestInit): Promise { const url = getApiUrl(endpoint) + const token = getAuthToken() + + const headers: Record = { + "Content-Type": "application/json", + ...(options?.headers as Record), + } + + if (token) { + headers["Authorization"] = `Bearer ${token}` + } + const response = await fetch(url, { ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, + headers, cache: "no-store", }) From ec22c857d54b6c1b08c74546e861f2b6b0ee7d87 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 17:20:31 +0100 Subject: [PATCH 03/23] Update api-config.ts --- AppImage/lib/api-config.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/AppImage/lib/api-config.ts b/AppImage/lib/api-config.ts index 63e8a66..0a2f266 100644 --- a/AppImage/lib/api-config.ts +++ b/AppImage/lib/api-config.ts @@ -69,7 +69,9 @@ export function getAuthToken(): string | null { if (typeof window === "undefined") { return null } - return localStorage.getItem("proxmenux-auth-token") + const token = localStorage.getItem("proxmenux-auth-token") + console.log("[v0] getAuthToken:", token ? `Token found (${token.substring(0, 20)}...)` : "No token found") + return token } /** @@ -91,14 +93,21 @@ export async function fetchApi(endpoint: string, options?: RequestInit): Prom if (token) { headers["Authorization"] = `Bearer ${token}` + console.log("[v0] fetchApi: Adding Authorization header to request:", endpoint) + } else { + console.log("[v0] fetchApi: No token available for request:", endpoint) } + console.log("[v0] fetchApi: Fetching", url, "with headers:", Object.keys(headers)) + const response = await fetch(url, { ...options, headers, cache: "no-store", }) + console.log("[v0] fetchApi: Response status for", endpoint, ":", response.status) + if (!response.ok) { throw new Error(`API request failed: ${response.status} ${response.statusText}`) } From 07603f11db57832d022027e698c082d5984f7073 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 17:25:51 +0100 Subject: [PATCH 04/23] Update api-config.ts --- AppImage/lib/api-config.ts | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/AppImage/lib/api-config.ts b/AppImage/lib/api-config.ts index 0a2f266..34175c9 100644 --- a/AppImage/lib/api-config.ts +++ b/AppImage/lib/api-config.ts @@ -70,7 +70,10 @@ export function getAuthToken(): string | null { return null } const token = localStorage.getItem("proxmenux-auth-token") - console.log("[v0] getAuthToken:", token ? `Token found (${token.substring(0, 20)}...)` : "No token found") + console.log( + "[v0] getAuthToken called:", + token ? `Token found (length: ${token.length})` : "No token found in localStorage", + ) return token } @@ -93,24 +96,31 @@ export async function fetchApi(endpoint: string, options?: RequestInit): Prom if (token) { headers["Authorization"] = `Bearer ${token}` - console.log("[v0] fetchApi: Adding Authorization header to request:", endpoint) + console.log("[v0] fetchApi:", endpoint, "- Authorization header ADDED") } else { - console.log("[v0] fetchApi: No token available for request:", endpoint) + console.log("[v0] fetchApi:", endpoint, "- NO TOKEN - Request will fail if endpoint is protected") } - console.log("[v0] fetchApi: Fetching", url, "with headers:", Object.keys(headers)) + try { + const response = await fetch(url, { + ...options, + headers, + cache: "no-store", + }) - const response = await fetch(url, { - ...options, - headers, - cache: "no-store", - }) + console.log("[v0] fetchApi:", endpoint, "- Response status:", response.status) - console.log("[v0] fetchApi: Response status for", endpoint, ":", 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}`) + } - if (!response.ok) { - 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 } - - return response.json() } From c1d1121ed11445a2a99887933ee36ed5a0115927 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 17:46:07 +0100 Subject: [PATCH 05/23] Update AppImage --- AppImage/components/auth-setup.tsx | 5 +++++ AppImage/scripts/flask_auth_routes.py | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/AppImage/components/auth-setup.tsx b/AppImage/components/auth-setup.tsx index 672cf11..e2915d4 100644 --- a/AppImage/components/auth-setup.tsx +++ b/AppImage/components/auth-setup.tsx @@ -61,8 +61,13 @@ export function AuthSetup({ onComplete }: AuthSetupProps) { 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) { diff --git a/AppImage/scripts/flask_auth_routes.py b/AppImage/scripts/flask_auth_routes.py index 00f4f5f..25677dc 100644 --- a/AppImage/scripts/flask_auth_routes.py +++ b/AppImage/scripts/flask_auth_routes.py @@ -135,7 +135,12 @@ def auth_skip(): success, message = auth_manager.decline_auth() if success: - return jsonify({"success": True, "message": message}) + # 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: From 8064e107f40d99d4bcebc0e2dc6136e50cdb35b4 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 17:56:42 +0100 Subject: [PATCH 06/23] Update AppImage --- AppImage/components/proxmox-dashboard.tsx | 18 +---- AppImage/components/storage-overview.tsx | 11 +-- AppImage/components/system-overview.tsx | 91 +++-------------------- 3 files changed, 15 insertions(+), 105 deletions(-) diff --git a/AppImage/components/proxmox-dashboard.tsx b/AppImage/components/proxmox-dashboard.tsx index 4737717..0ab1c83 100644 --- a/AppImage/components/proxmox-dashboard.tsx +++ b/AppImage/components/proxmox-dashboard.tsx @@ -14,7 +14,7 @@ import { Settings } from "./settings" import { OnboardingCarousel } from "./onboarding-carousel" import { HealthStatusModal } from "./health-status-modal" import { ReleaseNotesModal, useVersionCheck } from "./release-notes-modal" -import { getApiUrl } from "../lib/api-config" +import { getApiUrl, fetchApi } from "../lib/api-config" import { RefreshCw, AlertTriangle, @@ -80,22 +80,8 @@ export function ProxmoxDashboard() { const { showReleaseNotes, setShowReleaseNotes } = useVersionCheck() const fetchSystemData = useCallback(async () => { - const apiUrl = getApiUrl("/api/system-info") - try { - const response = await fetch(apiUrl, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - cache: "no-store", - }) - - if (!response.ok) { - throw new Error(`Server responded with status: ${response.status}`) - } - - const data: FlaskSystemInfo = await response.json() + const data: FlaskSystemInfo = await fetchApi("/api/system-info") const uptimeValue = data.uptime && typeof data.uptime === "string" && data.uptime.trim() !== "" ? data.uptime : "N/A" diff --git a/AppImage/components/storage-overview.tsx b/AppImage/components/storage-overview.tsx index f37192d..8c3724b 100644 --- a/AppImage/components/storage-overview.tsx +++ b/AppImage/components/storage-overview.tsx @@ -6,7 +6,7 @@ import { HardDrive, Database, AlertTriangle, CheckCircle2, XCircle, Square, Ther import { Badge } from "@/components/ui/badge" import { Progress } from "@/components/ui/progress" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { getApiUrl } from "../lib/api-config" +import { fetchApi } from "../lib/api-config" interface DiskInfo { name: string @@ -94,14 +94,11 @@ export function StorageOverview() { const fetchStorageData = async () => { try { - const [storageResponse, proxmoxResponse] = await Promise.all([ - fetch(getApiUrl("/api/storage")), - fetch(getApiUrl("/api/proxmox-storage")), + const [data, proxmoxData] = await Promise.all([ + fetchApi("/api/storage"), + fetchApi("/api/proxmox-storage"), ]) - const data = await storageResponse.json() - const proxmoxData = await proxmoxResponse.json() - setStorageData(data) setProxmoxStorage(proxmoxData) } catch (error) { diff --git a/AppImage/components/system-overview.tsx b/AppImage/components/system-overview.tsx index 43ad0d1..546b3c4 100644 --- a/AppImage/components/system-overview.tsx +++ b/AppImage/components/system-overview.tsx @@ -8,7 +8,7 @@ import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Net import { NodeMetricsCharts } from "./node-metrics-charts" import { NetworkTrafficChart } from "./network-traffic-chart" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" -import { getApiUrl } from "../lib/api-config" +import { fetchApi } from "../lib/api-config" interface SystemData { cpu_usage: number @@ -98,21 +98,7 @@ interface ProxmoxStorageData { const fetchSystemData = async (): Promise => { try { - const apiUrl = getApiUrl("/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() + const data = await fetchApi("/api/system") return data } catch (error) { console.error("[v0] Failed to fetch system data:", error) @@ -122,21 +108,7 @@ const fetchSystemData = async (): Promise => { const fetchVMData = async (): Promise => { try { - const apiUrl = getApiUrl("/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() + const data = await fetchApi("/api/vms") return Array.isArray(data) ? data : data.vms || [] } catch (error) { console.error("[v0] Failed to fetch VM data:", error) @@ -146,75 +118,30 @@ const fetchVMData = async (): Promise => { const fetchStorageData = async (): Promise => { try { - const apiUrl = getApiUrl("/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() + const data = await fetchApi("/api/storage/summary") return data } 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 } } const fetchNetworkData = async (): Promise => { try { - const apiUrl = getApiUrl("/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() + const data = await fetchApi("/api/network/summary") return data } 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 } } const fetchProxmoxStorageData = async (): Promise => { try { - const apiUrl = getApiUrl("/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() + const data = await fetchApi("/api/proxmox-storage") return data } 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 } } From 1d0bb205067c34d60d3b951ff28a5d29774717a8 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 18:21:37 +0100 Subject: [PATCH 07/23] Update AppImage --- AppImage/components/hardware.tsx | 224 +++++++++--------- AppImage/components/network-card.tsx | 20 +- AppImage/components/network-metrics.tsx | 15 +- AppImage/components/network-traffic-chart.tsx | 23 +- AppImage/components/node-metrics-charts.tsx | 23 +- AppImage/components/system-logs.tsx | 103 +++----- AppImage/components/virtual-machines.tsx | 132 ++++------- 7 files changed, 210 insertions(+), 330 deletions(-) diff --git a/AppImage/components/hardware.tsx b/AppImage/components/hardware.tsx index 8bb8e9d..4c92378 100644 --- a/AppImage/components/hardware.tsx +++ b/AppImage/components/hardware.tsx @@ -20,8 +20,14 @@ import { } from "lucide-react" import useSWR from "swr" import { useState, useEffect } from "react" -import { type HardwareData, type GPU, type PCIDevice, type StorageDevice, fetcher } from "../types/hardware" -import { API_PORT } from "@/lib/api-config" +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 => { if (!sizeStr) return 0 @@ -169,7 +175,7 @@ export default function Hardware() { data: staticHardwareData, error: staticError, isLoading: staticLoading, - } = useSWR("/api/hardware", fetcher, { + } = useSWR("/api/hardware", swrFetcher, { revalidateOnFocus: false, revalidateOnReconnect: false, refreshInterval: 0, // No auto-refresh for static data @@ -180,7 +186,7 @@ export default function Hardware() { data: dynamicHardwareData, error: dynamicError, isLoading: dynamicLoading, - } = useSWR("/api/hardware", fetcher, { + } = useSWR("/api/hardware", swrFetcher, { refreshInterval: 7000, }) @@ -231,6 +237,21 @@ export default function Hardware() { const [selectedNetwork, setSelectedNetwork] = useState(null) const [selectedUPS, setSelectedUPS] = useState(null) + const fetcher = async (url: string) => { + const data = await fetchApi(url) + return data + } + + const { + data: hardwareDataSWR, + error: swrError, + isLoading: swrLoading, + mutate, + } = useSWR("/api/hardware", fetcher, { + refreshInterval: 30000, + revalidateOnFocus: false, + }) + useEffect(() => { if (!selectedGPU) return @@ -243,30 +264,10 @@ export default function Hardware() { const fetchRealtimeData = async () => { try { - const { protocol, hostname, port } = window.location - const isStandardPort = port === "" || port === "80" || port === "443" - - const apiUrl = isStandardPort - ? `/api/gpu/${fullSlot}/realtime` - : `${protocol}//${hostname}:${API_PORT}/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() + const data = await fetchApi(`/api/gpu/${fullSlot}/realtime`) setRealtimeGPUData(data) setDetailsLoading(false) } catch (error) { - // Only log non-abort errors if (error instanceof Error && error.name !== "AbortError") { console.error("[v0] Error fetching GPU realtime data:", error) } @@ -275,10 +276,7 @@ export default function Hardware() { } } - // Initial fetch fetchRealtimeData() - - // Poll every 3 seconds const interval = setInterval(fetchRealtimeData, 3000) return () => { @@ -294,14 +292,14 @@ export default function Hardware() { } 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") - 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 (!pciDevice && gpu.slot.length <= 2) { - pciDevice = hardwareData.pci_devices.find( + pciDevice = hardwareDataSWR.pci_devices.find( (d) => d.slot.startsWith(gpu.slot + ":") && (d.type.toLowerCase().includes("vga") || @@ -320,7 +318,7 @@ export default function Hardware() { return realtimeGPUData.has_monitoring_tool === true } - if (isLoading) { + if (swrLoading) { return (
@@ -333,7 +331,7 @@ export default function Hardware() { return (
{/* System Information - CPU & Motherboard */} - {(hardwareData?.cpu || hardwareData?.motherboard) && ( + {(hardwareDataSWR?.cpu || hardwareDataSWR?.motherboard) && (
@@ -342,44 +340,44 @@ export default function Hardware() {
{/* CPU Info */} - {hardwareData?.cpu && Object.keys(hardwareData.cpu).length > 0 && ( + {hardwareDataSWR?.cpu && Object.keys(hardwareDataSWR.cpu).length > 0 && (

CPU

- {hardwareData.cpu.model && ( + {hardwareDataSWR.cpu.model && (
Model - {hardwareData.cpu.model} + {hardwareDataSWR.cpu.model}
)} - {hardwareData.cpu.cores_per_socket && hardwareData.cpu.sockets && ( + {hardwareDataSWR.cpu.cores_per_socket && hardwareDataSWR.cpu.sockets && (
Cores - {hardwareData.cpu.sockets} × {hardwareData.cpu.cores_per_socket} ={" "} - {hardwareData.cpu.sockets * hardwareData.cpu.cores_per_socket} cores + {hardwareDataSWR.cpu.sockets} × {hardwareDataSWR.cpu.cores_per_socket} ={" "} + {hardwareDataSWR.cpu.sockets * hardwareDataSWR.cpu.cores_per_socket} cores
)} - {hardwareData.cpu.total_threads && ( + {hardwareDataSWR.cpu.total_threads && (
Threads - {hardwareData.cpu.total_threads} + {hardwareDataSWR.cpu.total_threads}
)} - {hardwareData.cpu.l3_cache && ( + {hardwareDataSWR.cpu.l3_cache && (
L3 Cache - {hardwareData.cpu.l3_cache} + {hardwareDataSWR.cpu.l3_cache}
)} - {hardwareData.cpu.virtualization && ( + {hardwareDataSWR.cpu.virtualization && (
Virtualization - {hardwareData.cpu.virtualization} + {hardwareDataSWR.cpu.virtualization}
)}
@@ -387,41 +385,41 @@ export default function Hardware() { )} {/* Motherboard Info */} - {hardwareData?.motherboard && Object.keys(hardwareData.motherboard).length > 0 && ( + {hardwareDataSWR?.motherboard && Object.keys(hardwareDataSWR.motherboard).length > 0 && (

Motherboard

- {hardwareData.motherboard.manufacturer && ( + {hardwareDataSWR.motherboard.manufacturer && (
Manufacturer - {hardwareData.motherboard.manufacturer} + {hardwareDataSWR.motherboard.manufacturer}
)} - {hardwareData.motherboard.model && ( + {hardwareDataSWR.motherboard.model && (
Model - {hardwareData.motherboard.model} + {hardwareDataSWR.motherboard.model}
)} - {hardwareData.motherboard.bios?.vendor && ( + {hardwareDataSWR.motherboard.bios?.vendor && (
BIOS - {hardwareData.motherboard.bios.vendor} + {hardwareDataSWR.motherboard.bios.vendor}
)} - {hardwareData.motherboard.bios?.version && ( + {hardwareDataSWR.motherboard.bios?.version && (
Version - {hardwareData.motherboard.bios.version} + {hardwareDataSWR.motherboard.bios.version}
)} - {hardwareData.motherboard.bios?.date && ( + {hardwareDataSWR.motherboard.bios?.date && (
Date - {hardwareData.motherboard.bios.date} + {hardwareDataSWR.motherboard.bios.date}
)}
@@ -432,18 +430,18 @@ export default function Hardware() { )} {/* Memory Modules */} - {hardwareData?.memory_modules && hardwareData.memory_modules.length > 0 && ( + {hardwareDataSWR?.memory_modules && hardwareDataSWR.memory_modules.length > 0 && (

Memory Modules

- {hardwareData.memory_modules.length} installed + {hardwareDataSWR.memory_modules.length} installed
- {hardwareData.memory_modules.map((module, index) => ( + {hardwareDataSWR.memory_modules.map((module, index) => (
{module.slot}
@@ -479,29 +477,29 @@ export default function Hardware() { )} {/* Thermal Monitoring */} - {hardwareData?.temperatures && hardwareData.temperatures.length > 0 && ( + {hardwareDataSWR?.temperatures && hardwareDataSWR.temperatures.length > 0 && (

Thermal Monitoring

- {hardwareData.temperatures.length} sensors + {hardwareDataSWR.temperatures.length} sensors
{/* CPU Sensors */} - {groupAndSortTemperatures(hardwareData.temperatures).CPU.length > 0 && ( + {groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.length > 0 && (

CPU

- {groupAndSortTemperatures(hardwareData.temperatures).CPU.length} + {groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.length}
- {groupAndSortTemperatures(hardwareData.temperatures).CPU.map((temp, index) => { + {groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.map((temp, index) => { const percentage = temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100 const isHot = temp.current > (temp.high || 80) @@ -532,21 +530,21 @@ export default function Hardware() { )} {/* GPU Sensors */} - {groupAndSortTemperatures(hardwareData.temperatures).GPU.length > 0 && ( + {groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 0 && (
1 ? "md:col-span-2" : ""} + className={groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 1 ? "md:col-span-2" : ""} >

GPU

- {groupAndSortTemperatures(hardwareData.temperatures).GPU.length} + {groupAndSortTemperatures(hardwareDataSWR.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 = temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100 const isHot = temp.current > (temp.high || 80) @@ -577,21 +575,23 @@ export default function Hardware() { )} {/* NVME Sensors */} - {groupAndSortTemperatures(hardwareData.temperatures).NVME.length > 0 && ( + {groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 0 && (
1 ? "md:col-span-2" : ""} + className={ + groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 1 ? "md:col-span-2" : "" + } >

NVME

- {groupAndSortTemperatures(hardwareData.temperatures).NVME.length} + {groupAndSortTemperatures(hardwareDataSWR.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 = temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100 const isHot = temp.current > (temp.high || 80) @@ -622,21 +622,21 @@ export default function Hardware() { )} {/* PCI Sensors */} - {groupAndSortTemperatures(hardwareData.temperatures).PCI.length > 0 && ( + {groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 0 && (
1 ? "md:col-span-2" : ""} + className={groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 1 ? "md:col-span-2" : ""} >

PCI

- {groupAndSortTemperatures(hardwareData.temperatures).PCI.length} + {groupAndSortTemperatures(hardwareDataSWR.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 = temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100 const isHot = temp.current > (temp.high || 80) @@ -667,21 +667,23 @@ export default function Hardware() { )} {/* OTHER Sensors */} - {groupAndSortTemperatures(hardwareData.temperatures).OTHER.length > 0 && ( + {groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 0 && (
1 ? "md:col-span-2" : ""} + className={ + groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 1 ? "md:col-span-2" : "" + } >

OTHER

- {groupAndSortTemperatures(hardwareData.temperatures).OTHER.length} + {groupAndSortTemperatures(hardwareDataSWR.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 = temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100 const isHot = temp.current > (temp.high || 80) @@ -715,18 +717,18 @@ export default function Hardware() { )} {/* GPU Information - Enhanced with on-demand data fetching */} - {hardwareData?.gpus && hardwareData.gpus.length > 0 && ( + {hardwareDataSWR?.gpus && hardwareDataSWR.gpus.length > 0 && (

Graphics Cards

- {hardwareData.gpus.length} GPU{hardwareData.gpus.length > 1 ? "s" : ""} + {hardwareDataSWR.gpus.length} GPU{hardwareDataSWR.gpus.length > 1 ? "s" : ""}
- {hardwareData.gpus.map((gpu, index) => { + {hardwareDataSWR.gpus.map((gpu, index) => { const pciDevice = findPCIDeviceForGPU(gpu) const fullSlot = pciDevice?.slot || gpu.slot @@ -1104,18 +1106,18 @@ export default function Hardware() { {/* PCI Devices - Changed to modal */} - {hardwareData?.pci_devices && hardwareData.pci_devices.length > 0 && ( + {hardwareDataSWR?.pci_devices && hardwareDataSWR.pci_devices.length > 0 && (

PCI Devices

- {hardwareData.pci_devices.length} devices + {hardwareDataSWR.pci_devices.length} devices
- {hardwareData.pci_devices.map((device, index) => ( + {hardwareDataSWR.pci_devices.map((device, index) => (
setSelectedPCIDevice(device)} @@ -1190,7 +1192,7 @@ export default function Hardware() { {/* Power Consumption */} - {hardwareData?.power_meter && ( + {hardwareDataSWR?.power_meter && (
@@ -1200,13 +1202,13 @@ export default function Hardware() {
-

{hardwareData.power_meter.name}

- {hardwareData.power_meter.adapter && ( -

{hardwareData.power_meter.adapter}

+

{hardwareDataSWR.power_meter.name}

+ {hardwareDataSWR.power_meter.adapter && ( +

{hardwareDataSWR.power_meter.adapter}

)}
-

{hardwareData.power_meter.watts.toFixed(1)} W

+

{hardwareDataSWR.power_meter.watts.toFixed(1)} W

Current Draw

@@ -1215,18 +1217,18 @@ export default function Hardware() { )} {/* Power Supplies */} - {hardwareData?.power_supplies && hardwareData.power_supplies.length > 0 && ( + {hardwareDataSWR?.power_supplies && hardwareDataSWR.power_supplies.length > 0 && (

Power Supplies

- {hardwareData.power_supplies.length} PSUs + {hardwareDataSWR.power_supplies.length} PSUs
- {hardwareData.power_supplies.map((psu, index) => ( + {hardwareDataSWR.power_supplies.map((psu, index) => (
{psu.name} @@ -1243,18 +1245,18 @@ export default function Hardware() { )} {/* Fans */} - {hardwareData?.fans && hardwareData.fans.length > 0 && ( + {hardwareDataSWR?.fans && hardwareDataSWR.fans.length > 0 && (

System Fans

- {hardwareData.fans.length} fans + {hardwareDataSWR.fans.length} fans
- {hardwareData.fans.map((fan, index) => { + {hardwareDataSWR.fans.map((fan, index) => { const isPercentage = fan.unit === "percent" || fan.unit === "%" const percentage = isPercentage ? fan.speed : Math.min((fan.speed / 5000) * 100, 100) @@ -1278,18 +1280,18 @@ export default function Hardware() { )} {/* UPS */} - {hardwareData?.ups && Array.isArray(hardwareData.ups) && hardwareData.ups.length > 0 && ( + {hardwareDataSWR?.ups && Array.isArray(hardwareDataSWR.ups) && hardwareDataSWR.ups.length > 0 && (

UPS Status

- {hardwareData.ups.length} UPS + {hardwareDataSWR.ups.length} UPS
- {hardwareData.ups.map((ups: any, index: number) => { + {hardwareDataSWR.ups.map((ups: any, index: number) => { const batteryCharge = ups.battery_charge_raw || Number.parseFloat(ups.battery_charge?.replace("%", "") || "0") const loadPercent = ups.load_percent_raw || Number.parseFloat(ups.load_percent?.replace("%", "") || "0") @@ -1560,19 +1562,19 @@ export default function Hardware() { {/* Network Summary - Clickable */} - {hardwareData?.pci_devices && - hardwareData.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length > 0 && ( + {hardwareDataSWR?.pci_devices && + hardwareDataSWR.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length > 0 && (

Network Summary

- {hardwareData.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length} interfaces + {hardwareDataSWR.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length} interfaces
- {hardwareData.pci_devices + {hardwareDataSWR.pci_devices .filter((d) => d.type.toLowerCase().includes("network")) .map((device, index) => (
{/* Storage Summary - Clickable */} - {hardwareData?.storage_devices && hardwareData.storage_devices.length > 0 && ( + {hardwareDataSWR?.storage_devices && hardwareDataSWR.storage_devices.length > 0 && (

Storage Summary

{ - hardwareData.storage_devices.filter( + hardwareDataSWR.storage_devices.filter( (device) => device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"), ).length @@ -1669,7 +1671,7 @@ export default function Hardware() {
- {hardwareData.storage_devices + {hardwareDataSWR.storage_devices .filter( (device) => device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"), ) diff --git a/AppImage/components/network-card.tsx b/AppImage/components/network-card.tsx index 3c00c28..06f8f95 100644 --- a/AppImage/components/network-card.tsx +++ b/AppImage/components/network-card.tsx @@ -4,6 +4,7 @@ import { Card, CardContent } from "./ui/card" import { Badge } from "./ui/badge" import { Wifi, Zap } from "lucide-react" import { useState, useEffect } from "react" +import { fetchApi } from "../lib/api-config" interface NetworkCardProps { interface_: { @@ -94,26 +95,12 @@ export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps useEffect(() => { const fetchTrafficData = async () => { try { - const response = await fetch(`/api/network/${interface_.name}/metrics?timeframe=${timeframe}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - signal: AbortSignal.timeout(5000), - }) + const data = await fetchApi(`/api/network/${interface_.name}/metrics?timeframe=${timeframe}`) - if (!response.ok) { - throw new Error(`Failed to fetch traffic data: ${response.status}`) - } - - const data = await response.json() - - // Calculate totals from the data points if (data.data && data.data.length > 0) { const lastPoint = data.data[data.data.length - 1] const firstPoint = data.data[0] - // Calculate the difference between last and first data points const receivedGB = Math.max(0, (lastPoint.netin || 0) - (firstPoint.netin || 0)) const sentGB = Math.max(0, (lastPoint.netout || 0) - (firstPoint.netout || 0)) @@ -124,16 +111,13 @@ export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps } } catch (error) { console.error("[v0] Failed to fetch traffic data for card:", error) - // Keep showing 0 values on error setTrafficData({ received: 0, sent: 0 }) } } - // Only fetch if interface is up and not a VM if (interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm") { fetchTrafficData() - // Refresh every 60 seconds const interval = setInterval(fetchTrafficData, 60000) return () => clearInterval(interval) } diff --git a/AppImage/components/network-metrics.tsx b/AppImage/components/network-metrics.tsx index 6b6de14..724bd80 100644 --- a/AppImage/components/network-metrics.tsx +++ b/AppImage/components/network-metrics.tsx @@ -8,6 +8,7 @@ import { Wifi, Activity, Network, Router, AlertCircle, Zap } from "lucide-react" import useSWR from "swr" import { NetworkTrafficChart } from "./network-traffic-chart" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" +import { fetchApi } from "../lib/api-config" interface NetworkData { interfaces: NetworkInterface[] @@ -128,19 +129,7 @@ const formatSpeed = (speed: number): string => { } const fetcher = async (url: string): Promise => { - const response = await fetch(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() + return fetchApi(url) } export function NetworkMetrics() { diff --git a/AppImage/components/network-traffic-chart.tsx b/AppImage/components/network-traffic-chart.tsx index a093c41..7d9eac0 100644 --- a/AppImage/components/network-traffic-chart.tsx +++ b/AppImage/components/network-traffic-chart.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react" import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts" import { Loader2 } from "lucide-react" -import { API_PORT } from "@/lib/api-config" +import { fetchApi } from "@/lib/api-config" interface NetworkMetricsData { time: string @@ -76,24 +76,13 @@ export function NetworkTrafficChart({ setError(null) try { - const { protocol, hostname, port } = window.location - const isStandardPort = port === "" || port === "80" || port === "443" + const apiPath = interfaceName + ? `/api/network/${interfaceName}/metrics?timeframe=${timeframe}` + : `/api/node/metrics?timeframe=${timeframe}` - const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:${API_PORT}` + console.log("[v0] Fetching network metrics from:", apiPath) - const apiUrl = interfaceName - ? `${baseUrl}/api/network/${interfaceName}/metrics?timeframe=${timeframe}` - : `${baseUrl}/api/node/metrics?timeframe=${timeframe}` - - console.log("[v0] Fetching network metrics from:", apiUrl) - - const response = await fetch(apiUrl) - - if (!response.ok) { - throw new Error(`Failed to fetch network metrics: ${response.status}`) - } - - const result = await response.json() + const result = await fetchApi(apiPath) if (!result.data || !Array.isArray(result.data)) { throw new Error("Invalid data format received from server") diff --git a/AppImage/components/node-metrics-charts.tsx b/AppImage/components/node-metrics-charts.tsx index 40be442..575b973 100644 --- a/AppImage/components/node-metrics-charts.tsx +++ b/AppImage/components/node-metrics-charts.tsx @@ -6,7 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ". import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts" import { Loader2, TrendingUp, MemoryStick } from "lucide-react" import { useIsMobile } from "../hooks/use-mobile" -import { API_PORT } from "@/lib/api-config" +import { fetchApi } from "@/lib/api-config" const TIMEFRAME_OPTIONS = [ { value: "hour", label: "1 Hour" }, @@ -89,27 +89,8 @@ export function NodeMetricsCharts() { setError(null) try { - const { protocol, hostname, port } = window.location - const isStandardPort = port === "" || port === "80" || port === "443" + const result = await fetchApi(`/api/node/metrics?timeframe=${timeframe}`) - const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:${API_PORT}` - - const apiUrl = `${baseUrl}/api/node/metrics?timeframe=${timeframe}` - - console.log("[v0] Fetching node metrics from:", apiUrl) - - const response = await fetch(apiUrl) - - console.log("[v0] Response status:", response.status) - console.log("[v0] Response ok:", response.ok) - - if (!response.ok) { - const errorText = await response.text() - console.log("[v0] Error response text:", errorText) - throw new Error(`Failed to fetch node metrics: ${response.status}`) - } - - const result = await response.json() console.log("[v0] Node metrics result:", result) console.log("[v0] Result keys:", Object.keys(result)) console.log("[v0] Data array length:", result.data?.length || 0) diff --git a/AppImage/components/system-logs.tsx b/AppImage/components/system-logs.tsx index 0ef6091..ecc4ce7 100644 --- a/AppImage/components/system-logs.tsx +++ b/AppImage/components/system-logs.tsx @@ -28,7 +28,7 @@ import { Terminal, } from "lucide-react" import { useState, useEffect, useMemo } from "react" -import { API_PORT } from "@/lib/api-config" +import { API_PORT, fetchApi } from "@/lib/api-config" interface Log { timestamp: string @@ -135,6 +135,10 @@ export function SystemLogs() { return `${protocol}//${hostname}:${API_PORT}${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}` } @@ -194,27 +198,15 @@ export function SystemLogs() { const [logsRes, backupsRes, eventsRes, notificationsRes] = await Promise.all([ fetchSystemLogs(), - fetch(getApiUrl("/api/backups")), - fetch(getApiUrl("/api/events?limit=50")), - fetch(getApiUrl("/api/notifications")), + fetchApi("/api/backups"), + fetchApi("/api/events?limit=50"), + fetchApi("/api/notifications"), ]) setLogs(logsRes) - - if (backupsRes.ok) { - const backupsData = await backupsRes.json() - 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 || []) - } + setBackups(backupsRes.backups || []) + setEvents(eventsRes.events || []) + setNotifications(notificationsRes.notifications || []) } catch (err) { console.error("[v0] Error fetching system logs data:", err) setError("Failed to connect to server") @@ -225,7 +217,7 @@ export function SystemLogs() { const fetchSystemLogs = async (): Promise => { try { - let apiUrl = getApiUrl("/api/logs") + let apiUrl = "/api/logs" const params = new URLSearchParams() // CHANGE: Always add since_days parameter (no more "now" option) @@ -258,22 +250,7 @@ export function SystemLogs() { } console.log("[v0] Making fetch request to:", apiUrl) - const response = await fetch(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() + const data = await fetchApi(apiUrl) console.log("[v0] Received logs data, count:", data.logs?.length || 0) const logsArray = Array.isArray(data) ? data : data.logs || [] @@ -364,37 +341,33 @@ export function SystemLogs() { if (upid) { // Try to fetch the complete task log from Proxmox try { - const response = await fetch(getApiUrl(`/api/task-log/${encodeURIComponent(upid)}`)) + const taskLog = await fetchApi(`/api/task-log/${encodeURIComponent(upid)}`, {}, "text") - if (response.ok) { - const taskLog = await response.text() + // Download the complete task log + 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 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" }, - ) - - 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 - } + 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) { console.error("[v0] Failed to fetch task log from Proxmox:", error) // Fall through to download notification message diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index 9a0fd49..6687b2a 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -26,6 +26,7 @@ import { import useSWR from "swr" import { MetricsView } from "./metrics-dialog" import { formatStorage } from "@/lib/utils" // Import formatStorage utility +import { fetchApi } from "../lib/api-config" interface VMData { vmid: number @@ -133,20 +134,7 @@ interface VMDetails extends VMData { } const fetcher = async (url: string) => { - const response = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - signal: AbortSignal.timeout(60000), - }) - - if (!response.ok) { - throw new Error(`Flask server responded with status: ${response.status}`) - } - - const data = await response.json() - return data + return fetchApi(url) } const formatBytes = (bytes: number | undefined): string => { @@ -310,19 +298,14 @@ export function VirtualMachines() { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 10000) - const response = await fetch(`/api/vms/${lxc.vmid}`, { - signal: controller.signal, - }) + const details = await fetchApi(`/api/vms/${lxc.vmid}`) clearTimeout(timeoutId) - if (response.ok) { - const details = await response.json() - if (details.lxc_ip_info?.primary_ip) { - configs[lxc.vmid] = details.lxc_ip_info.primary_ip - } else if (details.config) { - configs[lxc.vmid] = extractIPFromConfig(details.config, details.lxc_ip_info) - } + if (details.lxc_ip_info?.primary_ip) { + configs[lxc.vmid] = details.lxc_ip_info.primary_ip + } else if (details.config) { + configs[lxc.vmid] = extractIPFromConfig(details.config, details.lxc_ip_info) } } catch (error) { console.log(`[v0] Could not fetch IP for LXC ${lxc.vmid}`) @@ -350,11 +333,8 @@ export function VirtualMachines() { setEditedNotes("") setDetailsLoading(true) try { - const response = await fetch(`/api/vms/${vm.vmid}`) - if (response.ok) { - const details = await response.json() - setVMDetails(details) - } + const details = await fetchApi(`/api/vms/${vm.vmid}`) + setVMDetails(details) } catch (error) { console.error("Error fetching VM details:", error) } finally { @@ -373,23 +353,16 @@ export function VirtualMachines() { const handleVMControl = async (vmid: number, action: string) => { setControlLoading(true) try { - const response = await fetch(`/api/vms/${vmid}/control`, { + await fetchApi(`/api/vms/${vmid}/control`, { method: "POST", - headers: { - "Content-Type": "application/json", - }, body: JSON.stringify({ action }), }) - if (response.ok) { - mutate() - setSelectedVM(null) - setVMDetails(null) - } else { - console.error("Failed to control VM") - } + mutate() + setSelectedVM(null) + setVMDetails(null) } catch (error) { - console.error("Error controlling VM:", error) + console.error("Failed to control VM") } finally { setControlLoading(false) } @@ -397,36 +370,33 @@ export function VirtualMachines() { const handleDownloadLogs = async (vmid: number, vmName: string) => { try { - const response = await fetch(`/api/vms/${vmid}/logs`) - if (response.ok) { - const data = await response.json() + const data = await fetchApi(`/api/vms/${vmid}/logs`) - // Format logs as plain text - let logText = `=== Logs for ${vmName} (VMID: ${vmid}) ===\n` - logText += `Node: ${data.node}\n` - logText += `Type: ${data.type}\n` - logText += `Total lines: ${data.log_lines}\n` - logText += `Generated: ${new Date().toISOString()}\n` - logText += `\n${"=".repeat(80)}\n\n` + // Format logs as plain text + let logText = `=== Logs for ${vmName} (VMID: ${vmid}) ===\n` + logText += `Node: ${data.node}\n` + logText += `Type: ${data.type}\n` + logText += `Total lines: ${data.log_lines}\n` + logText += `Generated: ${new Date().toISOString()}\n` + logText += `\n${"=".repeat(80)}\n\n` - if (data.logs && Array.isArray(data.logs)) { - data.logs.forEach((log: any) => { - if (typeof log === "object" && log.t) { - logText += `${log.t}\n` - } else if (typeof log === "string") { - 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) + if (data.logs && Array.isArray(data.logs)) { + data.logs.forEach((log: any) => { + if (typeof log === "object" && log.t) { + logText += `${log.t}\n` + } else if (typeof log === "string") { + 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) } catch (error) { console.error("Error downloading logs:", error) } @@ -621,29 +591,21 @@ export function VirtualMachines() { setSavingNotes(true) try { - const response = await fetch(`/api/vms/${selectedVM.vmid}/config`, { + await fetchApi(`/api/vms/${selectedVM.vmid}/config`, { method: "PUT", - headers: { - "Content-Type": "application/json", - }, body: JSON.stringify({ description: editedNotes, // Send as-is, pvesh will handle encoding }), }) - if (response.ok) { - setVMDetails({ - ...vmDetails, - config: { - ...vmDetails.config, - description: editedNotes, // Store unencoded - }, - }) - setIsEditingNotes(false) - } else { - console.error("Failed to save notes") - alert("Failed to save notes. Please try again.") - } + setVMDetails({ + ...vmDetails, + config: { + ...vmDetails.config, + description: editedNotes, // Store unencoded + }, + }) + setIsEditingNotes(false) } catch (error) { console.error("Error saving notes:", error) alert("Error saving notes. Please try again.") From 774cbe4c9d3edf45c86f95e5ea613282d85a08d9 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 18:32:44 +0100 Subject: [PATCH 08/23] Update AppImage --- AppImage/components/metrics-dialog.tsx | 18 ++---------------- AppImage/types/hardware.ts | 8 +++++++- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/AppImage/components/metrics-dialog.tsx b/AppImage/components/metrics-dialog.tsx index c6f521a..ce201a6 100644 --- a/AppImage/components/metrics-dialog.tsx +++ b/AppImage/components/metrics-dialog.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { ArrowLeft, Loader2 } from "lucide-react" import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts" -import { API_PORT } from "@/lib/api-config" +import { fetchApi } from "@/lib/api-config" interface MetricsViewProps { vmid: number @@ -119,21 +119,7 @@ export function MetricsView({ vmid, vmName, vmType, onBack }: MetricsViewProps) setError(null) try { - const { protocol, hostname, port } = window.location - const isStandardPort = port === "" || port === "80" || port === "443" - - const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:${API_PORT}` - - 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 result = await fetchApi(`/api/vms/${vmid}/metrics?timeframe=${timeframe}`) const transformedData = result.data.map((item: any) => { const date = new Date(item.time * 1000) diff --git a/AppImage/types/hardware.ts b/AppImage/types/hardware.ts index c31de3c..90561ac 100644 --- a/AppImage/types/hardware.ts +++ b/AppImage/types/hardware.ts @@ -1,3 +1,5 @@ +import { fetchApi } from "@/lib/api-config" + export interface Temperature { name: string original_name?: string @@ -208,4 +210,8 @@ export interface HardwareData { 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) +} From cd32e11c6dd25c158fe8f6d62eecaa0f8e55ef46 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 19:11:56 +0100 Subject: [PATCH 09/23] Update AppImage --- AppImage/README.md | 630 +++++++++++++++++++++++++- AppImage/components/settings.tsx | 275 ++++++++++- AppImage/scripts/flask_auth_routes.py | 39 ++ 3 files changed, 917 insertions(+), 27 deletions(-) diff --git a/AppImage/README.md b/AppImage/README.md index ecd8f9e..ea8c4b5 100644 --- a/AppImage/README.md +++ b/AppImage/README.md @@ -2,29 +2,600 @@ A modern, responsive dashboard for monitoring Proxmox VE systems built with Next.js and React. +

+ ProxMenux Monitor Logo +

+ +## 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) +- [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) +- [Onboarding Experience](#onboarding-experience) +- [Contributing](#contributing) + +--- + +## 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. + ## Features -- **System Overview**: Real-time monitoring of CPU, memory, temperature, and active VMs/LXC containers -- **Storage Management**: Visual representation of storage distribution and disk performance metrics -- **Network Monitoring**: Network interface statistics and performance graphs -- **Virtual Machines**: Comprehensive view of VMs and LXC containers with resource usage -- **System Logs**: Real-time system log monitoring and filtering +- **System Overview**: Real-time monitoring of CPU, memory, temperature, and system uptime +- **Storage Management**: Visual representation of storage distribution, disk health, and SMART data +- **Network Monitoring**: Network interface statistics, real-time traffic graphs, and bandwidth usage +- **Virtual Machines & LXC**: Comprehensive view of all VMs and containers with resource usage and controls +- **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 -- **Responsive Design**: Works seamlessly on desktop and mobile devices -- **Onboarding Experience**: Interactive welcome carousel for first-time users +- **Responsive Design**: Works seamlessly on desktop, tablet, and mobile devices +- **Release Notes**: Automatic notifications of new features and improvements ## Technology Stack - **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 - **UI Components**: Radix UI primitives with shadcn/ui -- **Backend**: Flask server for system data collection -- **Packaging**: AppImage for easy distribution +- **Backend**: Flask (Python) server for system data collection +- **Packaging**: AppImage for easy distribution and deployment -## Onboarding Images +## Installation -To customize the onboarding experience, place your screenshot images in `public/images/onboarding/`: +1. Download the latest `ProxMenux-Monitor.AppImage` from the releases page +2. Make it executable: + \`\`\`bash + chmod +x ProxMenux-Monitor.AppImage + \`\`\` +3. Run the AppImage: + \`\`\`bash + ./ProxMenux-Monitor.AppImage + \`\`\` +4. Access the dashboard at `http://your-proxmox-ip:8008` + +The application will start automatically and create a systemd service for persistence. + +--- + +## 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) + +![Authentication Setup](public/images/docs/auth-setup.png) + +### 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 + +![2FA Setup](public/images/docs/2fa-setup.png) + +--- + +## 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 + +\`\`\` +http://your-proxmox-ip:8008/api/ +\`\`\` + +### Generating API Tokens + +To use the API with authentication enabled, you need to generate a long-lived API token. + +#### Option 1: Generate via API Call + +\`\`\`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 2: Generate via cURL (with 2FA) + +\`\`\`bash +# With 2FA enabled +curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \ + -H "Content-Type: application/json" \ + -d '{"username":"pedro","password":"your-password","totp_token":"123456","token_name":"Home Assistant"}' +\`\`\` + +#### 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//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/` | GET | Yes | Detailed configuration for specific VM/LXC | +| `/api/vms//metrics` | GET | Yes | Historical metrics (RRD) for specific VM/LXC | +| `/api/vms//logs` | GET | Yes | Download real logs for specific VM/LXC | +| `/api/vms//control` | POST | Yes | Control VM/LXC (start, stop, shutdown, reboot) | +| `/api/vms//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//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/` | 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 + +First, generate an API token: + +\`\`\`bash +curl -X POST http://proxmox.example.tld:8008/api/auth/generate-api-token \ + -H "Content-Type: application/json" \ + -d '{ + "username": "your-username", + "password": "your-password", + "token_name": "Homepage Integration" + }' +\`\`\` + +Then add the token to your Homepage configuration: + +\`\`\`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 YOUR_API_TOKEN_HERE + 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 +- 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 YOUR_API_TOKEN_HERE + 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 YOUR_API_TOKEN_HERE + 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 YOUR_API_TOKEN_HERE + 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 +\`\`\` + +![Homepage Integration Example](public/images/docs/homepage-integration.png) + +### Home Assistant Integration + +[Home Assistant](https://www.home-assistant.io/) is an open-source home automation platform. + +#### Configuration.yaml + +\`\`\`yaml +# ProxMenux Monitor Sensors +sensor: + - platform: rest + name: ProxMenux CPU + resource: http://proxmox.example.tld:8008/api/system + headers: + Authorization: Bearer YOUR_API_TOKEN_HERE + 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: Bearer YOUR_API_TOKEN_HERE + 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: Bearer YOUR_API_TOKEN_HERE + 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: Bearer YOUR_API_TOKEN_HERE + 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 +\`\`\` + +![Home Assistant Integration Example](public/images/docs/homeassistant-integration.png) + +--- + +## Onboarding Experience + +ProxMenux Monitor includes an interactive onboarding carousel that introduces new users to the dashboard features. + +### Customizing Onboarding Images + +To customize the onboarding screenshots, place your images in `public/images/onboarding/`: - `imagen1.png` - Overview section screenshot - `imagen2.png` - Storage section screenshot @@ -35,7 +606,38 @@ To customize the onboarding experience, place your screenshot images in `public/ **Recommended image specifications:** - Format: PNG or JPG -- Size: 1200x800px or similar 3:2 aspect ratio +- 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". +The onboarding carousel appears on first visit and can be dismissed or marked as "Don't show again". + +--- + +## 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 MIT License. + +--- + +## 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 diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index bbc7cb1..be030b2 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -5,9 +5,23 @@ 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 } from "lucide-react" +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 } from "../lib/api-config" +import { getApiUrl, fetchApi } from "../lib/api-config" import { TwoFactorSetup } from "./two-factor-setup" interface ProxMenuxTool { @@ -45,6 +59,15 @@ export function Settings() { [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() @@ -278,6 +301,55 @@ export function Settings() { 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 response = await fetchApi("/api/auth/generate-api-token", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + password: tokenPassword, + totp_code: totpEnabled ? tokenTotpCode : undefined, + }), + }) + + if (!response.ok) { + const data = await response.json() + throw new Error(data.message || "Failed to generate API token") + } + + const data = await response.json() + 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") + } finally { + setGeneratingToken(false) + } + } + + const copyApiToken = () => { + navigator.clipboard.writeText(apiToken) + setTokenCopied(true) + setTimeout(() => setTokenCopied(false), 2000) + } + const toggleVersion = (version: string) => { setExpandedVersions((prev) => ({ ...prev, @@ -501,17 +573,6 @@ export function Settings() {
)} - {!totpEnabled && ( - - )} - {totpEnabled && (
@@ -577,6 +638,194 @@ export function Settings() { + {/* API Access Tokens */} + {authEnabled && ( + + +
+ + API Access Tokens +
+ + Generate long-lived API tokens for external integrations like Homepage and Home Assistant + +
+ + {error && ( +
+ +

{error}

+
+ )} + + {success && ( +
+ +

{success}

+
+ )} + +
+
+ +
+

About API Tokens

+
    +
  • Tokens are valid for 1 year
  • +
  • Use them to access APIs from external services
  • +
  • Include in Authorization header: Bearer YOUR_TOKEN
  • +
  • See README.md for complete integration examples
  • +
+
+
+
+ + {!showApiTokenSection && !apiToken && ( + + )} + + {showApiTokenSection && !apiToken && ( +
+

Generate API Token

+

+ Enter your credentials to generate a new long-lived API token +

+ +
+ +
+ + setTokenPassword(e.target.value)} + className="pl-10" + disabled={generatingToken} + /> +
+
+ + {totpEnabled && ( +
+ +
+ + setTokenTotpCode(e.target.value)} + className="pl-10" + maxLength={6} + disabled={generatingToken} + /> +
+
+ )} + +
+ + +
+
+ )} + + {apiToken && ( +
+
+ +

Your API Token

+
+ +
+ +

+ Save this token now! You won't be able to see it again. +

+
+ +
+ +
+ +
+ + +
+
+ {tokenCopied && ( +

+ + Copied to clipboard! +

+ )} +
+ +
+

How to use this token:

+
+

# Add to request headers:

+

Authorization: Bearer YOUR_TOKEN_HERE

+
+

+ See the README documentation for complete integration examples with Homepage and Home Assistant. +

+
+ + +
+ )} +
+
+ )} + {/* ProxMenux Optimizations */} diff --git a/AppImage/scripts/flask_auth_routes.py b/AppImage/scripts/flask_auth_routes.py index 25677dc..7082c90 100644 --- a/AppImage/scripts/flask_auth_routes.py +++ b/AppImage/scripts/flask_auth_routes.py @@ -5,6 +5,8 @@ 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__) @@ -223,3 +225,40 @@ def totp_disable(): 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: + data = request.json + username = data.get('username') + password = data.get('password') + totp_token = data.get('totp_token') # Optional 2FA token + token_name = data.get('token_name', 'API Token') # Optional token description + + # Authenticate user first + success, token, 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.SECRET_KEY, 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: + return jsonify({"success": False, "message": str(e)}), 500 From 5ca55798b2615d5b5ccb22cee7c563a024812ee1 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 19:20:06 +0100 Subject: [PATCH 10/23] Update README.md --- AppImage/README.md | 80 +++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/AppImage/README.md b/AppImage/README.md index ea8c4b5..9e64e51 100644 --- a/AppImage/README.md +++ b/AppImage/README.md @@ -61,13 +61,13 @@ The application runs as a standalone AppImage on your Proxmox server and serves 1. Download the latest `ProxMenux-Monitor.AppImage` from the releases page 2. Make it executable: - \`\`\`bash + ```bash chmod +x ProxMenux-Monitor.AppImage - \`\`\` + ``` 3. Run the AppImage: - \`\`\`bash + ```bash ./ProxMenux-Monitor.AppImage - \`\`\` + ``` 4. Access the dashboard at `http://your-proxmox-ip:8008` The application will start automatically and create a systemd service for persistence. @@ -112,9 +112,9 @@ When authentication is enabled on ProxMenux Monitor, all API endpoints (except ` #### API Endpoint Base URL -\`\`\` +``` http://your-proxmox-ip:8008/api/ -\`\`\` +``` ### Generating API Tokens @@ -122,7 +122,7 @@ To use the API with authentication enabled, you need to generate a long-lived AP #### Option 1: Generate via API Call -\`\`\`bash +```bash curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \ -H "Content-Type: application/json" \ -d '{ @@ -131,10 +131,10 @@ curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \ "totp_token": "123456", "token_name": "Homepage Integration" }' -\`\`\` +``` **Response:** -\`\`\`json +```json { "success": true, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", @@ -142,7 +142,7 @@ curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \ "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 @@ -152,30 +152,30 @@ curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \ #### Option 2: Generate via cURL (with 2FA) -\`\`\`bash +```bash # With 2FA enabled curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \ -H "Content-Type: application/json" \ -d '{"username":"pedro","password":"your-password","totp_token":"123456","token_name":"Home Assistant"}' -\`\`\` +``` #### Option 3: Generate via cURL (without 2FA) -\`\`\`bash +```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 +```bash curl -H "Authorization: Bearer YOUR_API_TOKEN_HERE" \ http://your-proxmox-ip:8008/api/system -\`\`\` +``` --- @@ -193,7 +193,7 @@ Below is a complete list of all API endpoints with descriptions and example resp | `/api/prometheus` | GET | Yes | Export metrics in Prometheus format | **Example `/api/system` Response:** -\`\`\`json +```json { "hostname": "pve", "cpu_usage": 15.2, @@ -203,7 +203,7 @@ Below is a complete list of all API endpoints with descriptions and example resp "kernel": "6.2.16-3-pve", "pve_version": "8.0.3" } -\`\`\` +``` #### Storage @@ -215,7 +215,7 @@ Below is a complete list of all API endpoints with descriptions and example resp | `/api/backups` | GET | Yes | List of all backup files | **Example `/api/storage/summary` Response:** -\`\`\`json +```json { "total_capacity": 1431894917120, "used_space": 197414092800, @@ -230,7 +230,7 @@ Below is a complete list of all API endpoints with descriptions and example resp } ] } -\`\`\` +``` #### Network @@ -241,7 +241,7 @@ Below is a complete list of all API endpoints with descriptions and example resp | `/api/network//metrics` | GET | Yes | Historical metrics (RRD) for specific interface | **Example `/api/network/summary` Response:** -\`\`\`json +```json { "interfaces": [ { @@ -253,7 +253,7 @@ Below is a complete list of all API endpoints with descriptions and example resp } ] } -\`\`\` +``` #### Virtual Machines & Containers @@ -267,7 +267,7 @@ Below is a complete list of all API endpoints with descriptions and example resp | `/api/vms//config` | PUT | Yes | Update VM/LXC configuration (description/notes) | **Example `/api/vms` Response:** -\`\`\`json +```json { "vms": [ { @@ -283,7 +283,7 @@ Below is a complete list of all API endpoints with descriptions and example resp } ] } -\`\`\` +``` #### Hardware @@ -293,7 +293,7 @@ Below is a complete list of all API endpoints with descriptions and example resp | `/api/gpu//realtime` | GET | Yes | Real-time monitoring for specific GPU | **Example `/api/hardware` Response:** -\`\`\`json +```json { "cpu": { "model": "AMD Ryzen 9 5950X", @@ -310,7 +310,7 @@ Below is a complete list of all API endpoints with descriptions and example resp } ] } -\`\`\` +``` #### Logs, Events & Notifications @@ -324,9 +324,9 @@ Below is a complete list of all API endpoints with descriptions and example resp | `/api/task-log/` | GET | Yes | Full log for specific task using UPID | **Example `/api/logs` Query Parameters:** -\`\`\` +``` /api/logs?severity=error&since=1h&search=failed -\`\`\` +``` #### Health Monitoring @@ -369,7 +369,7 @@ Below is a complete list of all API endpoints with descriptions and example resp #### Basic Configuration (No Authentication) -\`\`\`yaml +```yaml - ProxMenux Monitor: href: http://proxmox.example.tld:8008/ icon: lucide:flask-round @@ -395,13 +395,13 @@ Below is a complete list of all API endpoints with descriptions and example resp icon: lucide:thermometer-sun format: number suffix: °C -\`\`\` +``` #### With Authentication Enabled First, generate an API token: -\`\`\`bash +```bash curl -X POST http://proxmox.example.tld:8008/api/auth/generate-api-token \ -H "Content-Type: application/json" \ -d '{ @@ -409,11 +409,11 @@ curl -X POST http://proxmox.example.tld:8008/api/auth/generate-api-token \ "password": "your-password", "token_name": "Homepage Integration" }' -\`\`\` +``` Then add the token to your Homepage configuration: -\`\`\`yaml +```yaml - ProxMenux Monitor: href: http://proxmox.example.tld:8008/ icon: lucide:flask-round @@ -441,11 +441,11 @@ Then add the token to your Homepage configuration: icon: lucide:thermometer-sun format: number suffix: °C -\`\`\` +``` #### Advanced Multi-Widget Configuration -\`\`\`yaml +```yaml - ProxMenux System: href: http://proxmox.example.tld:8008/ icon: lucide:server @@ -510,7 +510,7 @@ Then add the token to your Homepage configuration: label: Sent icon: lucide:upload format: bytes -\`\`\` +``` ![Homepage Integration Example](public/images/docs/homepage-integration.png) @@ -520,7 +520,7 @@ Then add the token to your Homepage configuration: #### Configuration.yaml -\`\`\`yaml +```yaml # ProxMenux Monitor Sensors sensor: - platform: rest @@ -563,11 +563,11 @@ sensor: {% set minutes = ((uptime_seconds % 3600) / 60) | int %} {{ days }}d {{ hours }}h {{ minutes }}m scan_interval: 60 -\`\`\` +``` #### Lovelace Card Example -\`\`\`yaml +```yaml type: entities title: Proxmox Monitor entities: @@ -583,7 +583,7 @@ entities: - entity: sensor.proxmenux_uptime name: Uptime icon: mdi:clock-outline -\`\`\` +``` ![Home Assistant Integration Example](public/images/docs/homeassistant-integration.png) From 96ffdb65d0f9edaa5263c29a3d0fe4fbcd3b15a6 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 19:36:45 +0100 Subject: [PATCH 11/23] Update README.md --- AppImage/README.md | 168 +++++++++++++++++++++++++++++++++------------ 1 file changed, 124 insertions(+), 44 deletions(-) diff --git a/AppImage/README.md b/AppImage/README.md index 9e64e51..03829d0 100644 --- a/AppImage/README.md +++ b/AppImage/README.md @@ -3,9 +3,51 @@ A modern, responsive dashboard for monitoring Proxmox VE systems built with Next.js and React.

- ProxMenux Monitor Logo + ProxMenux Monitor Logo

+## Screenshots + +Get a quick overview of ProxMenux Monitor's main features: + +

+ Overview Dashboard +
+ System Overview - Monitor CPU, memory, temperature, and uptime in real-time +

+ +

+ Storage Management +
+ Storage Management - Visual representation of disk usage and health +

+ +

+ Network Monitoring +
+ Network Monitoring - Real-time traffic graphs and interface statistics +

+ +

+ Virtual Machines & LXC +
+ VMs & LXC Containers - Comprehensive view with resource usage and controls +

+ +

+ Hardware Information +
+ Hardware Information - Detailed specs for CPU, GPU, and PCIe devices +

+ +

+ System Logs +
+ System Logs - Real-time monitoring with filtering and search +

+ +--- + ## Table of Contents - [Overview](#overview) @@ -22,8 +64,8 @@ A modern, responsive dashboard for monitoring Proxmox VE systems built with Next - [Integration Examples](#integration-examples) - [Homepage Integration](#homepage-integration) - [Home Assistant Integration](#home-assistant-integration) -- [Onboarding Experience](#onboarding-experience) - [Contributing](#contributing) +- [License](#license) --- @@ -59,6 +101,43 @@ The application runs as a standalone AppImage on your Proxmox server and serves ## Installation +**ProxMenux Monitor is integrated into [ProxMenux](https://proxmenux.com) and comes enabled by default.** No manual installation is required if you're using ProxMenux. + +The monitor automatically starts when ProxMenux is installed and runs as a systemd service on your Proxmox server. + +### Accessing the Dashboard + +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; +} +``` + +### Manual Installation (Standalone) + +If you want to run ProxMenux Monitor as a standalone application outside of ProxMenux: + 1. Download the latest `ProxMenux-Monitor.AppImage` from the releases page 2. Make it executable: ```bash @@ -72,8 +151,6 @@ The application runs as a standalone AppImage on your Proxmox server and serves The application will start automatically and create a systemd service for persistence. ---- - ## Authentication & Security ProxMenux Monitor includes an optional authentication system to protect your dashboard with a password and two-factor authentication. @@ -86,7 +163,7 @@ On first launch, you'll be presented with three options: 2. **Enable 2FA** - Add TOTP-based two-factor authentication for enhanced security 3. **Skip** - Continue without authentication (not recommended for production environments) -![Authentication Setup](public/images/docs/auth-setup.png) +![Authentication Setup](AppImage/public/images/docs/auth-setup.png) ### Two-Factor Authentication (2FA) @@ -98,7 +175,7 @@ After setting up your password, you can enable 2FA using any TOTP authenticator 4. Enter the 6-digit code to verify 5. Save your backup codes in a secure location -![2FA Setup](public/images/docs/2fa-setup.png) +![2FA Setup](AppImage/public/images/docs/2fa-setup.png) --- @@ -110,17 +187,43 @@ ProxMenux Monitor provides a comprehensive RESTful API for integrating with exte 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 +### 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 API Call +#### 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 + +![Generate API Token](AppImage/public/images/docs/generate-api-token.png) + +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 \ @@ -150,15 +253,6 @@ curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \ - The token is valid for **365 days** (1 year) - Store the token securely - it cannot be retrieved again -#### Option 2: Generate via cURL (with 2FA) - -```bash -# With 2FA enabled -curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \ - -H "Content-Type: application/json" \ - -d '{"username":"pedro","password":"your-password","totp_token":"123456","token_name":"Home Assistant"}' -``` - #### Option 3: Generate via cURL (without 2FA) ```bash @@ -512,7 +606,7 @@ Then add the token to your Homepage configuration: format: bytes ``` -![Homepage Integration Example](public/images/docs/homepage-integration.png) +![Homepage Integration Example](AppImage/public/images/docs/homepage-integration.png) ### Home Assistant Integration @@ -585,31 +679,7 @@ entities: icon: mdi:clock-outline ``` -![Home Assistant Integration Example](public/images/docs/homeassistant-integration.png) - ---- - -## Onboarding Experience - -ProxMenux Monitor includes an interactive onboarding carousel that introduces new users to the dashboard features. - -### Customizing Onboarding Images - -To customize the onboarding screenshots, place your images in `public/images/onboarding/`: - -- `imagen1.png` - Overview section screenshot -- `imagen2.png` - Storage section screenshot -- `imagen3.png` - Network section screenshot -- `imagen4.png` - VMs & LXCs section screenshot -- `imagen5.png` - Hardware section screenshot -- `imagen6.png` - System Logs section screenshot - -**Recommended image specifications:** -- Format: PNG or JPG -- Size: 1200x800px (or similar 3:2 aspect ratio) -- Quality: High-quality screenshots with representative data - -The onboarding carousel appears on first visit and can be dismissed or marked as "Don't show again". +![Home Assistant Integration Example](AppImage/public/images/docs/homeassistant-integration.png) --- @@ -628,7 +698,17 @@ Contributions are welcome! Please feel free to submit issues, feature requests, ## License -This project is licensed under the MIT 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/). --- From 307ed0c637f2415854babb057dbd2979e5dd1d40 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 19:43:17 +0100 Subject: [PATCH 12/23] Update AppImage --- AppImage/README.md | 103 ++++++++++++-------------- AppImage/scripts/flask_auth_routes.py | 11 ++- 2 files changed, 54 insertions(+), 60 deletions(-) diff --git a/AppImage/README.md b/AppImage/README.md index 03829d0..65c2429 100644 --- a/AppImage/README.md +++ b/AppImage/README.md @@ -2,50 +2,6 @@ A modern, responsive dashboard for monitoring Proxmox VE systems built with Next.js and React. -

- ProxMenux Monitor Logo -

- -## Screenshots - -Get a quick overview of ProxMenux Monitor's main features: - -

- Overview Dashboard -
- System Overview - Monitor CPU, memory, temperature, and uptime in real-time -

- -

- Storage Management -
- Storage Management - Visual representation of disk usage and health -

- -

- Network Monitoring -
- Network Monitoring - Real-time traffic graphs and interface statistics -

- -

- Virtual Machines & LXC -
- VMs & LXC Containers - Comprehensive view with resource usage and controls -

- -

- Hardware Information -
- Hardware Information - Detailed specs for CPU, GPU, and PCIe devices -

- -

- System Logs -
- System Logs - Real-time monitoring with filtering and search -

- --- ## Table of Contents @@ -75,6 +31,52 @@ Get a quick overview of ProxMenux Monitor's main features: The application runs as a standalone AppImage on your Proxmox server and serves a web interface accessible from any device on your network. +

+ ProxMenux Monitor Logo +

+ +## Screenshots + +Get a quick overview of ProxMenux Monitor's main features: + +

+ Overview Dashboard +
+ System Overview - Monitor CPU, memory, temperature, and uptime in real-time +

+ +

+ Storage Management +
+ Storage Management - Visual representation of disk usage and health +

+ +

+ Network Monitoring +
+ Network Monitoring - Real-time traffic graphs and interface statistics +

+ +

+ Virtual Machines & LXC +
+ VMs & LXC Containers - Comprehensive view with resource usage and controls +

+ +

+ Hardware Information +
+ Hardware Information - Detailed specs for CPU, GPU, and PCIe devices +

+ +

+ System Logs +
+ System Logs - Real-time monitoring with filtering and search +

+ +--- + ## Features - **System Overview**: Real-time monitoring of CPU, memory, temperature, and system uptime @@ -683,19 +685,6 @@ entities: --- -## 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)**. diff --git a/AppImage/scripts/flask_auth_routes.py b/AppImage/scripts/flask_auth_routes.py index 7082c90..32f8bae 100644 --- a/AppImage/scripts/flask_auth_routes.py +++ b/AppImage/scripts/flask_auth_routes.py @@ -231,14 +231,19 @@ def totp_disable(): def generate_api_token(): """Generate a long-lived API token for external integrations (Homepage, Home Assistant, etc.)""" try: + token = request.headers.get('Authorization', '').replace('Bearer ', '') + username = auth_manager.verify_token(token) + + if not username: + return jsonify({"success": False, "message": "Unauthorized. Please log in first."}), 401 + data = request.json - username = data.get('username') password = data.get('password') totp_token = data.get('totp_token') # Optional 2FA token token_name = data.get('token_name', 'API Token') # Optional token description - # Authenticate user first - success, token, requires_totp, message = auth_manager.authenticate(username, password, totp_token) + # 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) From 3883039764e43b677834b404fedbcb832d13cb57 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 19:51:42 +0100 Subject: [PATCH 13/23] Update AppImage --- AppImage/README.md | 32 +++++++++++---------------- AppImage/scripts/flask_auth_routes.py | 15 ++++++++++--- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/AppImage/README.md b/AppImage/README.md index 65c2429..36e005f 100644 --- a/AppImage/README.md +++ b/AppImage/README.md @@ -31,9 +31,6 @@ A modern, responsive dashboard for monitoring Proxmox VE systems built with Next The application runs as a standalone AppImage on your Proxmox server and serves a web interface accessible from any device on your network. -

- ProxMenux Monitor Logo -

## Screenshots @@ -136,22 +133,6 @@ location /proxmenux-monitor/ { } ``` -### Manual Installation (Standalone) - -If you want to run ProxMenux Monitor as a standalone application outside of ProxMenux: - -1. Download the latest `ProxMenux-Monitor.AppImage` from the releases page -2. Make it executable: - ```bash - chmod +x ProxMenux-Monitor.AppImage - ``` -3. Run the AppImage: - ```bash - ./ProxMenux-Monitor.AppImage - ``` -4. Access the dashboard at `http://your-proxmox-ip:8008` - -The application will start automatically and create a systemd service for persistence. ## Authentication & Security @@ -685,6 +666,19 @@ entities: --- +## 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)**. diff --git a/AppImage/scripts/flask_auth_routes.py b/AppImage/scripts/flask_auth_routes.py index 32f8bae..bf3ab30 100644 --- a/AppImage/scripts/flask_auth_routes.py +++ b/AppImage/scripts/flask_auth_routes.py @@ -231,17 +231,25 @@ def totp_disable(): def generate_api_token(): """Generate a long-lived API token for external integrations (Homepage, Home Assistant, etc.)""" try: - token = request.headers.get('Authorization', '').replace('Bearer ', '') + 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": "Unauthorized. Please log in first."}), 401 + 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) @@ -266,4 +274,5 @@ def generate_api_token(): else: return jsonify({"success": False, "message": message}), 401 except Exception as e: - return jsonify({"success": False, "message": str(e)}), 500 + print(f"[ERROR] generate_api_token: {str(e)}") # Log error for debugging + return jsonify({"success": False, "message": f"Internal error: {str(e)}"}), 500 From 2faac48adf37702116bcb62543fe027b6e7b730b Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 20:01:08 +0100 Subject: [PATCH 14/23] Update settings.tsx --- AppImage/components/settings.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index be030b2..0ad8955 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -573,6 +573,26 @@ export function Settings() {
)} + {!totpEnabled && ( +
+
+ +
+

Two-Factor Authentication (2FA)

+

+ Add an extra layer of security by requiring a code from your authenticator app in addition to + your password. +

+
+
+ + +
+ )} + {totpEnabled && (
From 59a3b7eac57cda8fc7600899dfe66a43173fc423 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 20:10:00 +0100 Subject: [PATCH 15/23] Update settings.tsx --- AppImage/components/settings.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index 0ad8955..e65180e 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -323,21 +323,22 @@ export function Settings() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password: tokenPassword, - totp_code: totpEnabled ? tokenTotpCode : undefined, + totp_token: totpEnabled ? tokenTotpCode : undefined, }), }) + const data = await response.json() + if (!response.ok) { - const data = await response.json() - throw new Error(data.message || "Failed to generate API token") + throw new Error(data.message || data.error || "Failed to generate API token") } - const data = await response.json() 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) { + console.error("[v0] Token generation error:", err) setError(err instanceof Error ? err.message : "Failed to generate API token") } finally { setGeneratingToken(false) From a030cd7e28b17082b2ee0967d23eb44fd405c316 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 20:16:39 +0100 Subject: [PATCH 16/23] Update flask_auth_routes.py --- AppImage/scripts/flask_auth_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppImage/scripts/flask_auth_routes.py b/AppImage/scripts/flask_auth_routes.py index bf3ab30..d5b5e8d 100644 --- a/AppImage/scripts/flask_auth_routes.py +++ b/AppImage/scripts/flask_auth_routes.py @@ -260,7 +260,7 @@ def generate_api_token(): 'token_name': token_name, 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=365), 'iat': datetime.datetime.utcnow() - }, auth_manager.SECRET_KEY, algorithm='HS256') + }, auth_manager.JWT_SECRET, algorithm='HS256') return jsonify({ "success": True, From c65fef638e9bcae0bb53c65b828c4200384e5d3d Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 20:23:08 +0100 Subject: [PATCH 17/23] Update settings.tsx --- AppImage/components/settings.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index e65180e..fc72944 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -327,8 +327,15 @@ export function Settings() { }), }) - const data = await response.json() + // Parse response first + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error("Invalid server response") + } + // Then check if request was successful if (!response.ok) { throw new Error(data.message || data.error || "Failed to generate API token") } From 8cd1ac6a4b1b71fd00f38e86dfebe6508096dacd Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 20:29:22 +0100 Subject: [PATCH 18/23] Update settings.tsx --- AppImage/components/settings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index fc72944..a5760f6 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -336,8 +336,8 @@ export function Settings() { } // Then check if request was successful - if (!response.ok) { - throw new Error(data.message || data.error || "Failed to generate API token") + if (!response.ok || !data.success) { + throw new Error(data.message || "Failed to generate API token") } setApiToken(data.token) From 4baf60174fa31cf040b27508d257d81448aa1812 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 20:36:35 +0100 Subject: [PATCH 19/23] Update settings.tsx --- AppImage/components/settings.tsx | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index a5760f6..718a496 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -318,6 +318,8 @@ export function Settings() { setGeneratingToken(true) try { + console.log("[v0] Generating API token with password and 2FA:", { totpEnabled, hasTotpCode: !!tokenTotpCode }) + const response = await fetchApi("/api/auth/generate-api-token", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -327,19 +329,24 @@ export function Settings() { }), }) - // Parse response first - let data - try { - data = await response.json() - } catch (parseError) { - throw new Error("Invalid server response") + console.log("[v0] Response status:", response.status, response.statusText) + + const data = await response.json() + console.log("[v0] Parsed response data:", { success: data.success, hasToken: !!data.token }) + + if (!response.ok) { + throw new Error(data.message || data.error || "Failed to generate API token") } - // Then check if request was successful - if (!response.ok || !data.success) { + if (!data.success) { throw new Error(data.message || "Failed to generate API token") } + if (!data.token) { + throw new Error("No token received from server") + } + + console.log("[v0] API token generated successfully") 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("") From 305d37a13bf8db9f42979a439c43c0e6881efdb5 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 20:43:13 +0100 Subject: [PATCH 20/23] Update settings.tsx --- AppImage/components/settings.tsx | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index 718a496..d0f26bf 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -318,8 +318,6 @@ export function Settings() { setGeneratingToken(true) try { - console.log("[v0] Generating API token with password and 2FA:", { totpEnabled, hasTotpCode: !!tokenTotpCode }) - const response = await fetchApi("/api/auth/generate-api-token", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -329,31 +327,24 @@ export function Settings() { }), }) - console.log("[v0] Response status:", response.status, response.statusText) - const data = await response.json() - console.log("[v0] Parsed response data:", { success: data.success, hasToken: !!data.token }) - if (!response.ok) { - throw new Error(data.message || data.error || "Failed to generate API token") - } - - if (!data.success) { - throw new Error(data.message || "Failed to generate API token") + if (!response.ok || !data.success) { + setError(data.message || data.error || "Failed to generate API token") + return } if (!data.token) { - throw new Error("No token received from server") + setError("No token received from server") + return } - console.log("[v0] API token generated successfully") 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) { - console.error("[v0] Token generation error:", err) - setError(err instanceof Error ? err.message : "Failed to generate API token") + setError(err instanceof Error ? err.message : "Failed to generate API token. Please try again.") } finally { setGeneratingToken(false) } From 12442b4bd308ce7c985affc8553d744152ab7ec8 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 20:59:36 +0100 Subject: [PATCH 21/23] Update settings.tsx --- AppImage/components/settings.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index d0f26bf..022b445 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -318,7 +318,7 @@ export function Settings() { setGeneratingToken(true) try { - const response = await fetchApi("/api/auth/generate-api-token", { + const data = await fetchApi("/api/auth/generate-api-token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -327,9 +327,7 @@ export function Settings() { }), }) - const data = await response.json() - - if (!response.ok || !data.success) { + if (!data.success) { setError(data.message || data.error || "Failed to generate API token") return } From 42f2e69e3ae596611896336dc141e27a0c086314 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 13 Nov 2025 21:12:32 +0100 Subject: [PATCH 22/23] Update AppImagen --- AppImage/README.md | 144 +++++++++++++++++++++++++++---- AppImage/components/settings.tsx | 15 ++-- 2 files changed, 135 insertions(+), 24 deletions(-) diff --git a/AppImage/README.md b/AppImage/README.md index 36e005f..988af1c 100644 --- a/AppImage/README.md +++ b/AppImage/README.md @@ -13,6 +13,7 @@ A modern, responsive dashboard for monitoring Proxmox VE systems built with Next - [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) @@ -160,6 +161,100 @@ After setting up your password, you can enable 2FA using any TOTP authenticator ![2FA Setup](AppImage/public/images/docs/2fa-setup.png) +### 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 @@ -474,21 +569,18 @@ Below is a complete list of all API endpoints with descriptions and example resp suffix: °C ``` -#### With Authentication Enabled +#### With Authentication Enabled (Using Secrets) -First, generate an API token: +First, generate an API token via the web interface (Settings > API Access Tokens) or via API. -```bash -curl -X POST http://proxmox.example.tld:8008/api/auth/generate-api-token \ - -H "Content-Type: application/json" \ - -d '{ - "username": "your-username", - "password": "your-password", - "token_name": "Homepage Integration" - }' +Then, store your token securely in Homepage's `secrets.yaml`: + +```yaml +# secrets.yaml (add to .gitignore!) +proxmenux_token: "your_actual_api_token_here" ``` -Then add the token to your Homepage configuration: +Finally, reference the secret in your `services.yaml`: ```yaml - ProxMenux Monitor: @@ -498,7 +590,7 @@ Then add the token to your Homepage configuration: type: customapi url: http://proxmox.example.tld:8008/api/system headers: - Authorization: Bearer YOUR_API_TOKEN_HERE + Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}} refreshInterval: 10000 mappings: - field: uptime @@ -523,6 +615,9 @@ Then add the token to your Homepage configuration: #### 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 @@ -531,7 +626,7 @@ Then add the token to your Homepage configuration: type: customapi url: http://proxmox.example.tld:8008/api/system headers: - Authorization: Bearer YOUR_API_TOKEN_HERE + Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}} refreshInterval: 5000 mappings: - field: cpu_usage @@ -556,7 +651,7 @@ Then add the token to your Homepage configuration: type: customapi url: http://proxmox.example.tld:8008/api/storage/summary headers: - Authorization: Bearer YOUR_API_TOKEN_HERE + Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}} refreshInterval: 30000 mappings: - field: usage_percentage @@ -576,7 +671,7 @@ Then add the token to your Homepage configuration: type: customapi url: http://proxmox.example.tld:8008/api/network/summary headers: - Authorization: Bearer YOUR_API_TOKEN_HERE + Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}} refreshInterval: 5000 mappings: - field: interfaces[0].rx_bytes @@ -595,6 +690,17 @@ Then add the token to your Homepage configuration: [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 @@ -604,7 +710,7 @@ sensor: name: ProxMenux CPU resource: http://proxmox.example.tld:8008/api/system headers: - Authorization: Bearer YOUR_API_TOKEN_HERE + Authorization: !secret proxmenux_api_token value_template: "{{ value_json.cpu_usage }}" unit_of_measurement: "%" scan_interval: 30 @@ -613,7 +719,7 @@ sensor: name: ProxMenux Memory resource: http://proxmox.example.tld:8008/api/system headers: - Authorization: Bearer YOUR_API_TOKEN_HERE + Authorization: !secret proxmenux_api_token value_template: "{{ value_json.memory_usage }}" unit_of_measurement: "%" scan_interval: 30 @@ -622,7 +728,7 @@ sensor: name: ProxMenux Temperature resource: http://proxmox.example.tld:8008/api/system headers: - Authorization: Bearer YOUR_API_TOKEN_HERE + Authorization: !secret proxmenux_api_token value_template: "{{ value_json.temperature }}" unit_of_measurement: "°C" device_class: temperature @@ -632,7 +738,7 @@ sensor: name: ProxMenux Uptime resource: http://proxmox.example.tld:8008/api/system headers: - Authorization: Bearer YOUR_API_TOKEN_HERE + Authorization: !secret proxmenux_api_token value_template: > {% set uptime_seconds = value_json.uptime | int %} {% set days = (uptime_seconds / 86400) | int %} diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index 022b445..81b6fe8 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -785,11 +785,16 @@ export function Settings() {

Your API Token

-
- -

- Save this token now! You won't be able to see it again. -

+
+ +
+

+ ⚠️ Important: Save this token now! +

+

+ You won't be able to see it again. Store it securely. +

+
From 6ba817cd4315bfc3c6191da3e8e2862fc40bdf19 Mon Sep 17 00:00:00 2001 From: ProxMenuxBot Date: Fri, 14 Nov 2025 01:03:21 +0000 Subject: [PATCH 23/23] Update helpers_cache.json --- json/helpers_cache.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/json/helpers_cache.json b/json/helpers_cache.json index 97aa34c..aedfa96 100644 --- a/json/helpers_cache.json +++ b/json/helpers_cache.json @@ -441,20 +441,6 @@ "notes": [], "type": "ct" }, - { - "name": "Barcode Buddy", - "slug": "barcode-buddy", - "desc": "Barcode Buddy for Grocy is an extension for Grocy, allowing to pass barcodes to Grocy. It supports barcodes for products and chores. If you own a physical barcode scanner, it can be integrated, so that all barcodes scanned are automatically pushed to BarcodeBuddy/Grocy.", - "script": "ct/barcode-buddy.sh", - "script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/barcode-buddy.sh", - "categories": [ - 24 - ], - "notes": [ - "After install enable the option \"Use Redis cache\" on the settings page." - ], - "type": "ct" - }, { "name": "Bazarr", "slug": "bazarr", @@ -3071,6 +3057,20 @@ ], "type": "addon" }, + { + "name": "NetVisor", + "slug": "netvisor", + "desc": "Automatically discover and visually document network infrastructure", + "script": "ct/netvisor.sh", + "script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/netvisor.sh", + "categories": [ + 9 + ], + "notes": [ + "The integrated daemon config is located at `/root/.config/daemon/config.json`" + ], + "type": "ct" + }, { "name": "Nextcloud", "slug": "nextcloud-vm",