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"