diff --git a/.github/workflows/build-appimage-manual.yml b/.github/workflows/build-appimage-manual.yml new file mode 100644 index 0000000..9fdffcb --- /dev/null +++ b/.github/workflows/build-appimage-manual.yml @@ -0,0 +1,81 @@ +name: Build ProxMenux Monitor AppImage + +on: + + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + working-directory: AppImage + run: npm install --legacy-peer-deps + + - name: Build Next.js app + working-directory: AppImage + run: npm run build + + - name: Install Python dependencies + run: | + sudo apt-get update + sudo apt-get install -y python3 python3-pip python3-venv + + - name: Make build script executable + working-directory: AppImage + run: chmod +x scripts/build_appimage.sh + + - name: Build AppImage + working-directory: AppImage + run: ./scripts/build_appimage.sh + + - name: Get version from package.json + id: version + working-directory: AppImage + run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + + - name: Upload AppImage artifact + uses: actions/upload-artifact@v4 + with: + name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage + path: AppImage/dist/*.AppImage + retention-days: 30 + + - name: Generate SHA256 checksum + run: | + cd AppImage/dist + sha256sum *.AppImage > ProxMenux-Monitor.AppImage.sha256 + echo "Generated SHA256:" + cat ProxMenux-Monitor.AppImage.sha256 + + - name: Upload AppImage and checksum to /AppImage folder in main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + git fetch origin main + git checkout main + + rm -f AppImage/*.AppImage AppImage/*.sha256 || true + + # Copy new files + cp AppImage/dist/*.AppImage AppImage/ + cp AppImage/dist/ProxMenux-Monitor.AppImage.sha256 AppImage/ + + git add AppImage/*.AppImage AppImage/*.sha256 + git commit -m "Update AppImage build ($(date +'%Y-%m-%d %H:%M:%S'))" || echo "No changes to commit" + git push origin main diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml index 37bac0e..0d8e146 100644 --- a/.github/workflows/build-appimage.yml +++ b/.github/workflows/build-appimage.yml @@ -8,10 +8,7 @@ on: branches: [ main ] paths: [ 'AppImage/**' ] workflow_dispatch: - -permissions: - contents: write - + jobs: build: runs-on: ubuntu-22.04 @@ -57,30 +54,3 @@ jobs: name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage path: AppImage/dist/*.AppImage retention-days: 30 - - - name: Generate SHA256 checksum - run: | - cd AppImage/dist - sha256sum *.AppImage > ProxMenux-Monitor.AppImage.sha256 - echo "Generated SHA256:" - cat ProxMenux-Monitor.AppImage.sha256 - - - name: Upload AppImage and checksum to /AppImage folder in main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - - git fetch origin main - git checkout main - - rm -f AppImage/*.AppImage AppImage/*.sha256 || true - - # Copy new files - cp AppImage/dist/*.AppImage AppImage/ - cp AppImage/dist/ProxMenux-Monitor.AppImage.sha256 AppImage/ - - git add AppImage/*.AppImage AppImage/*.sha256 - git commit -m "Update AppImage build ($(date +'%Y-%m-%d %H:%M:%S'))" || echo "No changes to commit" - git push origin main diff --git a/AppImage/components/hardware.tsx b/AppImage/components/hardware.tsx index 6fe121c..2ff78e8 100644 --- a/AppImage/components/hardware.tsx +++ b/AppImage/components/hardware.tsx @@ -188,7 +188,12 @@ export default function Hardware() { const fetchRealtimeData = async () => { try { - const apiUrl = `http://${window.location.hostname}:8008/api/gpu/${fullSlot}/realtime` + const { protocol, hostname, port } = window.location + const isStandardPort = port === "" || port === "80" || port === "443" + + const apiUrl = isStandardPort + ? `/api/gpu/${fullSlot}/realtime` + : `${protocol}//${hostname}:8008/api/gpu/${fullSlot}/realtime` const response = await fetch(apiUrl, { method: "GET", @@ -1783,13 +1788,15 @@ export default function Hardware() { {selectedDisk.rotation_rate !== undefined && selectedDisk.rotation_rate !== null && (
Rotation Rate - - {typeof selectedDisk.rotation_rate === "number" && selectedDisk.rotation_rate > 0 - ? `${selectedDisk.rotation_rate} rpm` - : typeof selectedDisk.rotation_rate === "string" - ? selectedDisk.rotation_rate - : "Solid State Device"} - +
+ {typeof selectedDisk.rotation_rate === "number" && selectedDisk.rotation_rate === -1 + ? "N/A" + : typeof selectedDisk.rotation_rate === "number" && selectedDisk.rotation_rate > 0 + ? `${selectedDisk.rotation_rate} rpm` + : typeof selectedDisk.rotation_rate === "string" + ? selectedDisk.rotation_rate + : "Solid State Device"} +
)} diff --git a/AppImage/components/metrics-dialog.tsx b/AppImage/components/metrics-dialog.tsx index 4cf49ae..b8c1c31 100644 --- a/AppImage/components/metrics-dialog.tsx +++ b/AppImage/components/metrics-dialog.tsx @@ -118,8 +118,11 @@ export function MetricsView({ vmid, vmName, vmType, onBack }: MetricsViewProps) setError(null) try { - const baseUrl = - typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : "" + const { protocol, hostname, port } = window.location + const isStandardPort = port === "" || port === "80" || port === "443" + + const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:8008` + const apiUrl = `${baseUrl}/api/vms/${vmid}/metrics?timeframe=${timeframe}` const response = await fetch(apiUrl) diff --git a/AppImage/components/network-traffic-chart.tsx b/AppImage/components/network-traffic-chart.tsx index 34d9a34..a1576f3 100644 --- a/AppImage/components/network-traffic-chart.tsx +++ b/AppImage/components/network-traffic-chart.tsx @@ -75,8 +75,10 @@ export function NetworkTrafficChart({ setError(null) try { - const baseUrl = - typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : "" + const { protocol, hostname, port } = window.location + const isStandardPort = port === "" || port === "80" || port === "443" + + const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:8008` const apiUrl = interfaceName ? `${baseUrl}/api/network/${interfaceName}/metrics?timeframe=${timeframe}` diff --git a/AppImage/components/node-metrics-charts.tsx b/AppImage/components/node-metrics-charts.tsx index 9da7cc5..a8d86e4 100644 --- a/AppImage/components/node-metrics-charts.tsx +++ b/AppImage/components/node-metrics-charts.tsx @@ -86,8 +86,11 @@ export function NodeMetricsCharts() { setError(null) try { - const baseUrl = - typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : "" + const { protocol, hostname, port } = window.location + const isStandardPort = port === "" || port === "80" || port === "443" + + const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:8008` + const apiUrl = `${baseUrl}/api/node/metrics?timeframe=${timeframe}` console.log("[v0] Fetching node metrics from:", apiUrl) diff --git a/AppImage/components/proxmox-dashboard.tsx b/AppImage/components/proxmox-dashboard.tsx index 028e49e..da744ba 100644 --- a/AppImage/components/proxmox-dashboard.tsx +++ b/AppImage/components/proxmox-dashboard.tsx @@ -11,6 +11,7 @@ import { VirtualMachines } from "./virtual-machines" import Hardware from "./hardware" import { SystemLogs } from "./system-logs" import { OnboardingCarousel } from "./onboarding-carousel" +import { getApiUrl } from "../lib/api-config" import { RefreshCw, AlertTriangle, @@ -67,8 +68,7 @@ export function ProxmoxDashboard() { console.log("[v0] Fetching system data from Flask server...") console.log("[v0] Current window location:", window.location.href) - const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : "" - const apiUrl = `${baseUrl}/api/system` + const apiUrl = getApiUrl("/api/system") console.log("[v0] API URL:", apiUrl) @@ -235,13 +235,8 @@ export function ProxmoxDashboard() {

• The ProxMenux server should start automatically on port 8008

• Try accessing:{" "} - - http://{typeof window !== "undefined" ? window.location.host : "localhost:8008"}/api/health + + {getApiUrl("/api/health")}

diff --git a/AppImage/components/storage-overview.tsx b/AppImage/components/storage-overview.tsx index e7263cc..7debfe6 100644 --- a/AppImage/components/storage-overview.tsx +++ b/AppImage/components/storage-overview.tsx @@ -211,6 +211,12 @@ export function StorageOverview() { if (diskName.startsWith("nvme")) { return "NVMe" } + // rotation_rate = -1 means HDD but RPM is unknown (detected via kernel rotational flag) + // rotation_rate = 0 or undefined means SSD + // rotation_rate > 0 means HDD with known RPM + if (rotationRate === -1) { + return "HDD" + } if (!rotationRate || rotationRate === 0) { return "SSD" } diff --git a/AppImage/components/system-logs.tsx b/AppImage/components/system-logs.tsx index 144e862..853a9eb 100644 --- a/AppImage/components/system-logs.tsx +++ b/AppImage/components/system-logs.tsx @@ -125,7 +125,14 @@ export function SystemLogs() { const getApiUrl = (endpoint: string) => { if (typeof window !== "undefined") { - return `${window.location.protocol}//${window.location.hostname}:8008${endpoint}` + const { protocol, hostname, port } = window.location + const isStandardPort = port === "" || port === "80" || port === "443" + + if (isStandardPort) { + return endpoint + } else { + return `${protocol}//${hostname}:8008${endpoint}` + } } return `http://localhost:8008${endpoint}` } diff --git a/AppImage/components/system-overview.tsx b/AppImage/components/system-overview.tsx index 468799f..850e1ac 100644 --- a/AppImage/components/system-overview.tsx +++ b/AppImage/components/system-overview.tsx @@ -8,6 +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" interface SystemData { cpu_usage: number @@ -97,8 +98,7 @@ interface ProxmoxStorageData { const fetchSystemData = async (): Promise => { try { - const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : "" - const apiUrl = `${baseUrl}/api/system` + const apiUrl = getApiUrl("/api/system") const response = await fetch(apiUrl, { method: "GET", @@ -122,8 +122,7 @@ const fetchSystemData = async (): Promise => { const fetchVMData = async (): Promise => { try { - const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : "" - const apiUrl = `${baseUrl}/api/vms` + const apiUrl = getApiUrl("/api/vms") const response = await fetch(apiUrl, { method: "GET", @@ -147,8 +146,7 @@ const fetchVMData = async (): Promise => { const fetchStorageData = async (): Promise => { try { - const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : "" - const apiUrl = `${baseUrl}/api/storage/summary` + const apiUrl = getApiUrl("/api/storage/summary") const response = await fetch(apiUrl, { method: "GET", @@ -173,8 +171,7 @@ const fetchStorageData = async (): Promise => { const fetchNetworkData = async (): Promise => { try { - const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : "" - const apiUrl = `${baseUrl}/api/network/summary` + const apiUrl = getApiUrl("/api/network/summary") const response = await fetch(apiUrl, { method: "GET", @@ -199,8 +196,7 @@ const fetchNetworkData = async (): Promise => { const fetchProxmoxStorageData = async (): Promise => { try { - const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : "" - const apiUrl = `${baseUrl}/api/proxmox-storage` + const apiUrl = getApiUrl("/api/proxmox-storage") const response = await fetch(apiUrl, { method: "GET", diff --git a/AppImage/lib/api-config.ts b/AppImage/lib/api-config.ts new file mode 100644 index 0000000..de2bcc0 --- /dev/null +++ b/AppImage/lib/api-config.ts @@ -0,0 +1,71 @@ +/** + * API Configuration for ProxMenux Monitor + * Handles API URL generation with automatic proxy detection + */ + +/** + * Gets the base URL for API calls + * Automatically detects if running behind a proxy by checking if we're on a standard port + * + * @returns Base URL for API endpoints + */ +export function getApiBaseUrl(): string { + if (typeof window === "undefined") { + return "" + } + + const { protocol, hostname, port } = window.location + + // If accessing via standard ports (80/443) or no port, assume we're behind a proxy + // In this case, use relative URLs so the proxy handles routing + const isStandardPort = port === "" || port === "80" || port === "443" + + if (isStandardPort) { + // Behind a proxy - use relative URL + return "" + } else { + // Direct access - use explicit port 8008 + return `${protocol}//${hostname}:8008` + } +} + +/** + * Constructs a full API URL + * + * @param endpoint - API endpoint path (e.g., '/api/system') + * @returns Full API URL + */ +export function getApiUrl(endpoint: string): string { + const baseUrl = getApiBaseUrl() + + // Ensure endpoint starts with / + const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}` + + return `${baseUrl}${normalizedEndpoint}` +} + +/** + * Fetches data from an API endpoint with error handling + * + * @param endpoint - API endpoint path + * @param options - Fetch options + * @returns Promise with the response data + */ +export async function fetchApi(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", + }) + + if (!response.ok) { + throw new Error(`API request failed: ${response.status} ${response.statusText}`) + } + + return response.json() +} diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 7533f0b..783c611 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -1387,6 +1387,23 @@ def get_smart_data(disk_name): smart_data['health'] = 'warning' # print(f"[v0] Health: WARNING (temperature {smart_data['temperature']}°C)") pass + + # CHANGE: Use -1 to indicate HDD with unknown RPM instead of inventing 7200 RPM + # Fallback: Check kernel's rotational flag if smartctl didn't provide rotation_rate + # This fixes detection for older disks that don't report RPM via smartctl + if smart_data['rotation_rate'] == 0: + try: + rotational_path = f"/sys/block/{disk_name}/queue/rotational" + if os.path.exists(rotational_path): + with open(rotational_path, 'r') as f: + rotational = int(f.read().strip()) + if rotational == 1: + # Disk is rotational (HDD), use -1 to indicate "HDD but RPM unknown" + smart_data['rotation_rate'] = -1 + # If rotational == 0, it's an SSD, keep rotation_rate as 0 + except Exception as e: + pass # If we can't read the file, leave rotation_rate as is + except FileNotFoundError: # print(f"[v0] ERROR: smartctl not found - install smartmontools for disk monitoring.") diff --git a/scripts/post_install/customizable_post_install.sh b/scripts/post_install/customizable_post_install.sh index 63cebc9..dfde6ee 100644 --- a/scripts/post_install/customizable_post_install.sh +++ b/scripts/post_install/customizable_post_install.sh @@ -2671,7 +2671,7 @@ enable_vfio_iommu() { msg_info "$(translate "Checking conflicting drivers blacklist...")" touch "$blacklist_file" - local blacklist_drivers=("nouveau" "lbm-nouveau" "amdgpu" "radeon" "nvidia" "nvidiafb") + local blacklist_drivers=("nouveau" "lbm-nouveau" "radeon" "nvidia" "nvidiafb") for driver in "${blacklist_drivers[@]}"; do if ! grep -q "^blacklist $driver" "$blacklist_file"; then echo "blacklist $driver" >> "$blacklist_file"