diff --git a/AppImage/README.md b/AppImage/README.md
index ecd8f9e..988af1c 100644
--- a/AppImage/README.md
+++ b/AppImage/README.md
@@ -2,40 +2,811 @@
A modern, responsive dashboard for monitoring Proxmox VE systems built with Next.js and React.
+---
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Features](#features)
+- [Technology Stack](#technology-stack)
+- [Installation](#installation)
+- [Authentication & Security](#authentication--security)
+ - [Setup Authentication](#setup-authentication)
+ - [Two-Factor Authentication (2FA)](#two-factor-authentication-2fa)
+ - [Security Best Practices for API Tokens](#security-best-practices-for-api-tokens)
+- [API Documentation](#api-documentation)
+ - [API Authentication](#api-authentication)
+ - [Generating API Tokens](#generating-api-tokens)
+ - [Available Endpoints](#available-endpoints)
+- [Integration Examples](#integration-examples)
+ - [Homepage Integration](#homepage-integration)
+ - [Home Assistant Integration](#home-assistant-integration)
+- [Contributing](#contributing)
+- [License](#license)
+
+---
+
+## Overview
+
+**ProxMenux Monitor** is a comprehensive, real-time monitoring dashboard for Proxmox VE environments. Built with modern web technologies, it provides an intuitive interface to monitor system resources, virtual machines, containers, storage, network traffic, and system logs.
+
+The application runs as a standalone AppImage on your Proxmox server and serves a web interface accessible from any device on your network.
+
+
+## Screenshots
+
+Get a quick overview of ProxMenux Monitor's main features:
+
+
+
+
+ System Overview - Monitor CPU, memory, temperature, and uptime in real-time
+
+
+
+
+
+ Storage Management - Visual representation of disk usage and health
+
+
+
+
+
+ Network Monitoring - Real-time traffic graphs and interface statistics
+
+
+
+
+
+ VMs & LXC Containers - Comprehensive view with resource usage and controls
+
+
+
+
+
+ Hardware Information - Detailed specs for CPU, GPU, and PCIe devices
+
+
+
+
+
+ System Logs - Real-time monitoring with filtering and search
+
+
+---
+
## 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/`:
+**ProxMenux Monitor is integrated into [ProxMenux](https://proxmenux.com) and comes enabled by default.** No manual installation is required if you're using ProxMenux.
-- `imagen1.png` - Overview section screenshot
-- `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
+The monitor automatically starts when ProxMenux is installed and runs as a systemd service on your Proxmox server.
-**Recommended image specifications:**
-- Format: PNG or JPG
-- Size: 1200x800px or similar 3:2 aspect ratio
-- Quality: High-quality screenshots with representative data
+### Accessing the Dashboard
-The onboarding carousel will automatically show on first visit and can be dismissed or marked as "Don't show again".
+You can access ProxMenux Monitor in two ways:
+
+1. **Direct Access**: `http://your-proxmox-ip:8008`
+2. **Via Proxy** (Recommended): `https://your-domain.com/proxmenux-monitor/`
+
+**Note**: All API endpoints work seamlessly with both direct access and proxy configurations. When using a reverse proxy, the application automatically detects and adapts to the proxied environment.
+
+### Proxy Configuration
+
+ProxMenux Monitor includes built-in support for reverse proxy configurations. If you're using Nginx, Caddy, or Traefik, the application will automatically:
+
+- Detect the proxy headers (`X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`)
+- Adjust API endpoints to work correctly through the proxy
+- Maintain full functionality for all features including authentication and API access
+
+**Example Nginx configuration:**
+```nginx
+location /proxmenux-monitor/ {
+ proxy_pass http://localhost:8008/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+}
+```
+
+
+## Authentication & Security
+
+ProxMenux Monitor includes an optional authentication system to protect your dashboard with a password and two-factor authentication.
+
+### Setup Authentication
+
+On first launch, you'll be presented with three options:
+
+1. **Set up authentication** - Create a username and password to protect your dashboard
+2. **Enable 2FA** - Add TOTP-based two-factor authentication for enhanced security
+3. **Skip** - Continue without authentication (not recommended for production environments)
+
+
+
+### Two-Factor Authentication (2FA)
+
+After setting up your password, you can enable 2FA using any TOTP authenticator app (Google Authenticator, Authy, 1Password, etc.):
+
+1. Navigate to **Settings > Authentication**
+2. Click **Enable 2FA**
+3. Scan the QR code with your authenticator app
+4. Enter the 6-digit code to verify
+5. Save your backup codes in a secure location
+
+
+
+### Security Best Practices for API Tokens
+
+**IMPORTANT**: Never hardcode your API tokens directly in configuration files or scripts. Instead, use environment variables or secrets management.
+
+**Option 1: Environment Variables**
+
+Store your token in an environment variable:
+
+```bash
+# Linux/macOS - Add to ~/.bashrc or ~/.zshrc
+export PROXMENUX_API_TOKEN="your_actual_token_here"
+
+# Windows PowerShell - Add to profile
+$env:PROXMENUX_API_TOKEN = "your_actual_token_here"
+```
+
+Then reference it in your scripts:
+
+```bash
+# Linux/macOS
+curl -H "Authorization: Bearer $PROXMENUX_API_TOKEN" \
+ http://your-proxmox-ip:8008/api/system
+
+# Windows PowerShell
+curl -H "Authorization: Bearer $env:PROXMENUX_API_TOKEN" `
+ http://your-proxmox-ip:8008/api/system
+```
+
+**Option 2: Secrets File**
+
+Create a dedicated secrets file (make sure to add it to `.gitignore`):
+
+```bash
+# Create secrets file
+echo "PROXMENUX_API_TOKEN=your_actual_token_here" > ~/.proxmenux_secrets
+
+# Secure the file (Linux/macOS only)
+chmod 600 ~/.proxmenux_secrets
+
+# Load in your script
+source ~/.proxmenux_secrets
+```
+
+**Option 3: Homepage Secrets (Recommended)**
+
+Homepage supports secrets management. Create a `secrets.yaml` file:
+
+```yaml
+# secrets.yaml (add to .gitignore!)
+proxmenux_token: "your_actual_token_here"
+```
+
+Then reference it in your `services.yaml`:
+
+```yaml
+- ProxMenux Monitor:
+ widget:
+ type: customapi
+ url: http://proxmox.example.tld:8008/api/system
+ headers:
+ Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
+```
+
+**Option 4: Home Assistant Secrets**
+
+Home Assistant has built-in secrets support. Edit `secrets.yaml`:
+
+```yaml
+# secrets.yaml
+proxmenux_api_token: "your_actual_token_here"
+```
+
+Then reference it in `configuration.yaml`:
+
+```yaml
+sensor:
+ - platform: rest
+ name: ProxMenux CPU
+ resource: http://proxmox.example.tld:8008/api/system
+ headers:
+ Authorization: !secret proxmenux_api_token
+```
+
+**Token Security Checklist:**
+- ✅ Store tokens in environment variables or secrets files
+- ✅ Add secrets files to `.gitignore`
+- ✅ Set proper file permissions (chmod 600 on Linux/macOS)
+- ✅ Rotate tokens periodically (every 3-6 months)
+- ✅ Use different tokens for different integrations
+- ✅ Delete tokens you no longer use
+- ❌ Never commit tokens to version control
+- ❌ Never share tokens in screenshots or logs
+- ❌ Never hardcode tokens in configuration files
+
+---
+
+## API Documentation
+
+ProxMenux Monitor provides a comprehensive RESTful API for integrating with external services like Homepage, Home Assistant, or custom dashboards.
+
+### API Authentication
+
+When authentication is enabled on ProxMenux Monitor, all API endpoints (except `/api/health` and `/api/auth/*`) require a valid JWT token in the `Authorization` header.
+
+### API Endpoint Base URL
+
+**Direct Access:**
+```
+http://your-proxmox-ip:8008/api/
+```
+
+**Via Proxy:**
+```
+https://your-domain.com/proxmenux-monitor/api/
+```
+
+**Note**: All API examples in this documentation work with both direct and proxied URLs. Simply replace the base URL with your preferred access method.
+
+### Generating API Tokens
+
+To use the API with authentication enabled, you need to generate a long-lived API token.
+
+#### Option 1: Generate via Web Panel (Recommended)
+
+The easiest way to generate an API token is through the ProxMenux Monitor web interface:
+
+1. Navigate to **Settings** tab in the dashboard
+2. Scroll to the **API Access Tokens** section
+3. Enter your password
+4. If 2FA is enabled, enter your 6-digit code
+5. Provide a name for the token (e.g., "Homepage Integration")
+6. Click **Generate Token**
+7. Copy the token immediately - it will not be shown again
+
+
+
+The token will be valid for **365 days** (1 year) and can be used for integrations with Homepage, Home Assistant, or any custom application.
+
+#### Option 2: Generate via API Call
+
+For advanced users or automation, you can generate tokens programmatically:
+
+```bash
+curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \
+ -H "Content-Type: application/json" \
+ -d '{
+ "username": "your-username",
+ "password": "your-password",
+ "totp_token": "123456",
+ "token_name": "Homepage Integration"
+ }'
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "token_name": "Homepage Integration",
+ "expires_in": "365 days",
+ "message": "API token generated successfully. Store this token securely, it will not be shown again."
+}
+```
+
+**Notes:**
+- If 2FA is enabled, include the `totp_token` field with your 6-digit code
+- If 2FA is not enabled, omit the `totp_token` field
+- The token is valid for **365 days** (1 year)
+- Store the token securely - it cannot be retrieved again
+
+#### Option 3: Generate via cURL (without 2FA)
+
+```bash
+# Without 2FA
+curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \
+ -H "Content-Type: application/json" \
+ -d '{"username":"pedro","password":"your-password","token_name":"Homepage"}'
+```
+
+### Using API Tokens
+
+Once you have your API token, include it in the `Authorization` header of all API requests:
+
+```bash
+curl -H "Authorization: Bearer YOUR_API_TOKEN_HERE" \
+ http://your-proxmox-ip:8008/api/system
+```
+
+---
+
+### Available Endpoints
+
+Below is a complete list of all API endpoints with descriptions and example responses.
+
+#### System & Metrics
+
+| Endpoint | Method | Auth Required | Description |
+|----------|--------|---------------|-------------|
+| `/api/system` | GET | Yes | Complete system information (CPU, memory, temperature, uptime) |
+| `/api/system-info` | GET | No | Lightweight system info for header (hostname, uptime, health) |
+| `/api/node/metrics` | GET | Yes | Historical metrics data (RRD) for CPU, memory, disk I/O |
+| `/api/prometheus` | GET | Yes | Export metrics in Prometheus format |
+
+**Example `/api/system` Response:**
+```json
+{
+ "hostname": "pve",
+ "cpu_usage": 15.2,
+ "memory_usage": 45.8,
+ "temperature": 42.5,
+ "uptime": 345600,
+ "kernel": "6.2.16-3-pve",
+ "pve_version": "8.0.3"
+}
+```
+
+#### Storage
+
+| Endpoint | Method | Auth Required | Description |
+|----------|--------|---------------|-------------|
+| `/api/storage` | GET | Yes | Complete storage information with SMART data |
+| `/api/storage/summary` | GET | Yes | Optimized storage summary (without SMART) |
+| `/api/proxmox-storage` | GET | Yes | Proxmox storage pools information |
+| `/api/backups` | GET | Yes | List of all backup files |
+
+**Example `/api/storage/summary` Response:**
+```json
+{
+ "total_capacity": 1431894917120,
+ "used_space": 197414092800,
+ "free_space": 1234480824320,
+ "usage_percentage": 13.8,
+ "disks": [
+ {
+ "device": "/dev/sda",
+ "model": "Samsung SSD 970",
+ "size": "476.94 GB",
+ "type": "SSD"
+ }
+ ]
+}
+```
+
+#### Network
+
+| Endpoint | Method | Auth Required | Description |
+|----------|--------|---------------|-------------|
+| `/api/network` | GET | Yes | Complete network information for all interfaces |
+| `/api/network/summary` | GET | Yes | Optimized network summary |
+| `/api/network//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 (Using Secrets)
+
+First, generate an API token via the web interface (Settings > API Access Tokens) or via API.
+
+Then, store your token securely in Homepage's `secrets.yaml`:
+
+```yaml
+# secrets.yaml (add to .gitignore!)
+proxmenux_token: "your_actual_api_token_here"
+```
+
+Finally, reference the secret in your `services.yaml`:
+
+```yaml
+- ProxMenux Monitor:
+ href: http://proxmox.example.tld:8008/
+ icon: lucide:flask-round
+ widget:
+ type: customapi
+ url: http://proxmox.example.tld:8008/api/system
+ headers:
+ Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
+ refreshInterval: 10000
+ mappings:
+ - field: uptime
+ label: Uptime
+ icon: lucide:clock-4
+ format: text
+ - field: cpu_usage
+ label: CPU
+ icon: lucide:cpu
+ format: percent
+ - field: memory_usage
+ label: RAM
+ icon: lucide:memory-stick
+ format: percent
+ - field: temperature
+ label: Temp
+ icon: lucide:thermometer-sun
+ format: number
+ suffix: °C
+```
+
+#### Advanced Multi-Widget Configuration
+
+```yaml
+# Store token in secrets.yaml
+# proxmenux_token: "your_actual_api_token_here"
+
+- ProxMenux System:
+ href: http://proxmox.example.tld:8008/
+ icon: lucide:server
+ description: Proxmox VE Host
+ widget:
+ type: customapi
+ url: http://proxmox.example.tld:8008/api/system
+ headers:
+ Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
+ refreshInterval: 5000
+ mappings:
+ - field: cpu_usage
+ label: CPU
+ icon: lucide:cpu
+ format: percent
+ - field: memory_usage
+ label: RAM
+ icon: lucide:memory-stick
+ format: percent
+ - field: temperature
+ label: Temp
+ icon: lucide:thermometer-sun
+ format: number
+ suffix: °C
+
+- ProxMenux Storage:
+ href: http://proxmox.example.tld:8008/#/storage
+ icon: lucide:hard-drive
+ description: Storage Overview
+ widget:
+ type: customapi
+ url: http://proxmox.example.tld:8008/api/storage/summary
+ headers:
+ Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
+ refreshInterval: 30000
+ mappings:
+ - field: usage_percentage
+ label: Used
+ icon: lucide:database
+ format: percent
+ - field: used_space
+ label: Space
+ icon: lucide:folder
+ format: bytes
+
+- ProxMenux Network:
+ href: http://proxmox.example.tld:8008/#/network
+ icon: lucide:network
+ description: Network Stats
+ widget:
+ type: customapi
+ url: http://proxmox.example.tld:8008/api/network/summary
+ headers:
+ Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
+ refreshInterval: 5000
+ mappings:
+ - field: interfaces[0].rx_bytes
+ label: Received
+ icon: lucide:download
+ format: bytes
+ - field: interfaces[0].tx_bytes
+ label: Sent
+ icon: lucide:upload
+ format: bytes
+```
+
+
+
+### Home Assistant Integration
+
+[Home Assistant](https://www.home-assistant.io/) is an open-source home automation platform.
+
+#### Store Token Securely
+
+First, add your API token to Home Assistant's `secrets.yaml`:
+
+```yaml
+# secrets.yaml
+proxmenux_api_token: "Bearer your_actual_api_token_here"
+```
+
+**Note**: Include "Bearer " prefix in the secrets file for Home Assistant.
+
+#### Configuration.yaml
+
+```yaml
+# ProxMenux Monitor Sensors
+sensor:
+ - platform: rest
+ name: ProxMenux CPU
+ resource: http://proxmox.example.tld:8008/api/system
+ headers:
+ Authorization: !secret proxmenux_api_token
+ value_template: "{{ value_json.cpu_usage }}"
+ unit_of_measurement: "%"
+ scan_interval: 30
+
+ - platform: rest
+ name: ProxMenux Memory
+ resource: http://proxmox.example.tld:8008/api/system
+ headers:
+ Authorization: !secret proxmenux_api_token
+ value_template: "{{ value_json.memory_usage }}"
+ unit_of_measurement: "%"
+ scan_interval: 30
+
+ - platform: rest
+ name: ProxMenux Temperature
+ resource: http://proxmox.example.tld:8008/api/system
+ headers:
+ Authorization: !secret proxmenux_api_token
+ value_template: "{{ value_json.temperature }}"
+ unit_of_measurement: "°C"
+ device_class: temperature
+ scan_interval: 30
+
+ - platform: rest
+ name: ProxMenux Uptime
+ resource: http://proxmox.example.tld:8008/api/system
+ headers:
+ Authorization: !secret proxmenux_api_token
+ value_template: >
+ {% set uptime_seconds = value_json.uptime | int %}
+ {% set days = (uptime_seconds / 86400) | int %}
+ {% set hours = ((uptime_seconds % 86400) / 3600) | int %}
+ {% set minutes = ((uptime_seconds % 3600) / 60) | int %}
+ {{ days }}d {{ hours }}h {{ minutes }}m
+ scan_interval: 60
+```
+
+#### Lovelace Card Example
+
+```yaml
+type: entities
+title: Proxmox Monitor
+entities:
+ - entity: sensor.proxmenux_cpu
+ name: CPU Usage
+ icon: mdi:cpu-64-bit
+ - entity: sensor.proxmenux_memory
+ name: Memory Usage
+ icon: mdi:memory
+ - entity: sensor.proxmenux_temperature
+ name: Temperature
+ icon: mdi:thermometer
+ - entity: sensor.proxmenux_uptime
+ name: Uptime
+ icon: mdi:clock-outline
+```
+
+
+
+---
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.
+
+### Development Setup
+
+1. Clone the repository
+2. Install dependencies: `npm install`
+3. Run development server: `npm run dev`
+4. Build AppImage: `./build_appimage.sh`
+
+---
+
+## License
+
+This project is licensed under the **Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0)**.
+
+You are free to:
+- Share — copy and redistribute the material in any medium or format
+- Adapt — remix, transform, and build upon the material
+
+Under the following terms:
+- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made
+- NonCommercial — You may not use the material for commercial purposes
+
+For more details, see the [full license](https://creativecommons.org/licenses/by-nc/4.0/).
+
+---
+
+## Support
+
+For support, feature requests, or bug reports, please visit:
+- GitHub Issues: [github.com/your-repo/issues](https://github.com/your-repo/issues)
+- Documentation: [github.com/your-repo/wiki](https://github.com/your-repo/wiki)
+
+---
+
+**ProxMenux Monitor** - Made with ❤️ for the Proxmox community
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/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/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/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/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/settings.tsx b/AppImage/components/settings.tsx
index bbc7cb1..81b6fe8 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,59 @@ 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 data = await fetchApi("/api/auth/generate-api-token", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ password: tokenPassword,
+ totp_token: totpEnabled ? tokenTotpCode : undefined,
+ }),
+ })
+
+ if (!data.success) {
+ setError(data.message || data.error || "Failed to generate API token")
+ return
+ }
+
+ if (!data.token) {
+ setError("No token received from server")
+ return
+ }
+
+ setApiToken(data.token)
+ setSuccess("API token generated successfully! Make sure to copy it now as you won't be able to see it again.")
+ setTokenPassword("")
+ setTokenTotpCode("")
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to generate API token. Please try again.")
+ } finally {
+ setGeneratingToken(false)
+ }
+ }
+
+ const copyApiToken = () => {
+ navigator.clipboard.writeText(apiToken)
+ setTokenCopied(true)
+ setTimeout(() => setTokenCopied(false), 2000)
+ }
+
const toggleVersion = (version: string) => {
setExpandedVersions((prev) => ({
...prev,
@@ -502,14 +578,23 @@ export function Settings() {
)}
{!totpEnabled && (
- setShow2FASetup(true)}
- variant="outline"
- className="w-full bg-blue-500/10 hover:bg-blue-500/20 border-blue-500/20"
- >
-
- Enable Two-Factor Authentication
-
+
+
+
+
+
Two-Factor Authentication (2FA)
+
+ Add an extra layer of security by requiring a code from your authenticator app in addition to
+ your password.
+
+
+
+
+
setShow2FASetup(true)} variant="outline" className="w-full">
+
+ Enable Two-Factor Authentication
+
+
)}
{totpEnabled && (
@@ -577,6 +662,199 @@ export function Settings() {
+ {/* API Access Tokens */}
+ {authEnabled && (
+
+
+
+
+ API Access Tokens
+
+
+ Generate long-lived API tokens for external integrations like Homepage and Home Assistant
+
+
+
+ {error && (
+
+ )}
+
+ {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 && (
+ setShowApiTokenSection(true)} className="w-full bg-purple-500 hover:bg-purple-600">
+
+ Generate New API Token
+
+ )}
+
+ {showApiTokenSection && !apiToken && (
+
+
Generate API Token
+
+ Enter your credentials to generate a new long-lived API token
+
+
+
+
Password
+
+
+ setTokenPassword(e.target.value)}
+ className="pl-10"
+ disabled={generatingToken}
+ />
+
+
+
+ {totpEnabled && (
+
+
2FA Code
+
+
+ setTokenTotpCode(e.target.value)}
+ className="pl-10"
+ maxLength={6}
+ disabled={generatingToken}
+ />
+
+
+ )}
+
+
+
+ {generatingToken ? "Generating..." : "Generate Token"}
+
+ {
+ setShowApiTokenSection(false)
+ setTokenPassword("")
+ setTokenTotpCode("")
+ setError("")
+ }}
+ variant="outline"
+ className="flex-1"
+ disabled={generatingToken}
+ >
+ Cancel
+
+
+
+ )}
+
+ {apiToken && (
+
+
+
+
Your API Token
+
+
+
+
+
+
+ ⚠️ Important: Save this token now!
+
+
+ You won't be able to see it again. Store it securely.
+
+
+
+
+
+
Token
+
+
+
+ setApiTokenVisible(!apiTokenVisible)}
+ className="h-7 w-7 p-0"
+ >
+ {apiTokenVisible ? : }
+
+
+
+
+
+
+ {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.
+
+
+
+
{
+ setApiToken("")
+ setShowApiTokenSection(false)
+ }}
+ variant="outline"
+ className="w-full"
+ >
+ Done
+
+
+ )}
+
+
+ )}
+
{/* ProxMenux Optimizations */}
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-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/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
}
}
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.")
diff --git a/AppImage/lib/api-config.ts b/AppImage/lib/api-config.ts
index 8ee2ff6..34175c9 100644
--- a/AppImage/lib/api-config.ts
+++ b/AppImage/lib/api-config.ts
@@ -60,6 +60,23 @@ 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
+ }
+ const token = localStorage.getItem("proxmenux-auth-token")
+ console.log(
+ "[v0] getAuthToken called:",
+ token ? `Token found (length: ${token.length})` : "No token found in localStorage",
+ )
+ return token
+}
+
/**
* Fetches data from an API endpoint with error handling
*
@@ -70,18 +87,40 @@ export function getApiUrl(endpoint: string): string {
export async function fetchApi(endpoint: string, options?: RequestInit): Promise {
const url = getApiUrl(endpoint)
- const response = await fetch(url, {
- ...options,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- },
- cache: "no-store",
- })
+ const token = getAuthToken()
- if (!response.ok) {
- throw new Error(`API request failed: ${response.status} ${response.statusText}`)
+ const headers: Record = {
+ "Content-Type": "application/json",
+ ...(options?.headers as Record),
}
- return response.json()
+ if (token) {
+ headers["Authorization"] = `Bearer ${token}`
+ console.log("[v0] fetchApi:", endpoint, "- Authorization header ADDED")
+ } else {
+ console.log("[v0] fetchApi:", endpoint, "- NO TOKEN - Request will fail if endpoint is protected")
+ }
+
+ try {
+ const response = await fetch(url, {
+ ...options,
+ headers,
+ cache: "no-store",
+ })
+
+ console.log("[v0] fetchApi:", endpoint, "- Response status:", response.status)
+
+ if (!response.ok) {
+ if (response.status === 401) {
+ console.error("[v0] fetchApi: 401 UNAUTHORIZED -", endpoint, "- Token present:", !!token)
+ throw new Error(`Unauthorized: ${endpoint}`)
+ }
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`)
+ }
+
+ return response.json()
+ } catch (error) {
+ console.error("[v0] fetchApi error for", endpoint, ":", error)
+ throw error
+ }
}
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_auth_routes.py b/AppImage/scripts/flask_auth_routes.py
index 00f4f5f..d5b5e8d 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__)
@@ -135,7 +137,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:
@@ -218,3 +225,54 @@ 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:
+ auth_header = request.headers.get('Authorization', '')
+ token = auth_header.replace('Bearer ', '')
+
+ if not token:
+ return jsonify({"success": False, "message": "Unauthorized. Please log in first."}), 401
+
+ username = auth_manager.verify_token(token)
+
+ if not username:
+ return jsonify({"success": False, "message": "Invalid or expired session. Please log in again."}), 401
+
+ data = request.json
+ password = data.get('password')
+ totp_token = data.get('totp_token') # Optional 2FA token
+ token_name = data.get('token_name', 'API Token') # Optional token description
+
+ if not password:
+ return jsonify({"success": False, "message": "Password is required"}), 400
+
+ # Authenticate user with password and optional 2FA
+ success, _, requires_totp, message = auth_manager.authenticate(username, password, totp_token)
+
+ if success:
+ # Generate a long-lived token (1 year expiration)
+ api_token = jwt.encode({
+ 'username': username,
+ 'token_name': token_name,
+ 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=365),
+ 'iat': datetime.datetime.utcnow()
+ }, auth_manager.JWT_SECRET, algorithm='HS256')
+
+ return jsonify({
+ "success": True,
+ "token": api_token,
+ "token_name": token_name,
+ "expires_in": "365 days",
+ "message": "API token generated successfully. Store this token securely, it will not be shown again."
+ })
+ elif requires_totp:
+ return jsonify({"success": False, "requires_totp": True, "message": message}), 200
+ else:
+ return jsonify({"success": False, "message": message}), 401
+ except Exception as e:
+ print(f"[ERROR] generate_api_token: {str(e)}") # Log error for debugging
+ return jsonify({"success": False, "message": f"Internal error: {str(e)}"}), 500
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
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)
+}