From 4e150f07a996d91c6876d10d9761520e1d12450f Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 11 Apr 2026 10:40:16 -0700 Subject: [PATCH 1/6] fix: add CORS headers and OPTIONS handler for /api/metrics The browser was sending a CORS preflight OPTIONS request before the GET request. In standalone mode, Next.js wasn't handling the preflight, causing 'NetworkError when attempting to fetch resource'. - Add OPTIONS handler that returns proper CORS headers - Add Access-Control headers to GET response - Add Access-Control headers to error response --- app/api/metrics/route.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/api/metrics/route.ts b/app/api/metrics/route.ts index d4b5be4..f3cf6d1 100644 --- a/app/api/metrics/route.ts +++ b/app/api/metrics/route.ts @@ -6,6 +6,16 @@ import type { SystemMetrics } from "@/types/metrics"; export const dynamic = "force-dynamic"; export const revalidate = 0; +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", +}; + +export async function OPTIONS(): Promise { + return NextResponse.json({}, { headers: corsHeaders }); +} + async function getCpuMetrics() { const [cpu, cpuLoad, cpuSpeed, cpuTemp, cpuCache] = await Promise.all([ si.cpu(), @@ -317,9 +327,9 @@ export async function GET(): Promise { rocmRuntimeVersion: gpuData.rocmRuntimeVersion, }; - return NextResponse.json(response); + return NextResponse.json(response, { headers: corsHeaders }); } catch (error) { console.error("Failed to collect metrics:", error); - return NextResponse.json({ error: "Failed to collect metrics" }, { status: 500 }); + return NextResponse.json({ error: "Failed to collect metrics" }, { status: 500, headers: corsHeaders }); } } From db116038da1bba787624434516d54ccf2237f068 Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 11 Apr 2026 10:46:33 -0700 Subject: [PATCH 2/6] fix: resolve Next.js Image aspect ratio warning for AMD ROCm logo The Image component had width=80 and height=32 but CSS set h-8 (32px), creating a mismatch. Added inline style 'width: auto' to maintain aspect ratio and suppress the browser warning. --- lib/components/Dashboard.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/components/Dashboard.tsx b/lib/components/Dashboard.tsx index f8dd0ef..9480a31 100644 --- a/lib/components/Dashboard.tsx +++ b/lib/components/Dashboard.tsx @@ -590,9 +590,10 @@ function GpuColumn({ AMD ROCm Powered by ROCm {rocmRuntimeVersion} From fcbf8d4a4f14a96b1d8d1d7909d8d9f83eac9332 Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 11 Apr 2026 10:48:14 -0700 Subject: [PATCH 3/6] fix: add loading=eager for LCP logo and fix ROCm image sizing - Add loading='eager' to logo.svg as it's above the fold (LCP element) - Change ROCm image from w-auto to w-8 to keep width/height symmetric --- lib/components/Dashboard.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/components/Dashboard.tsx b/lib/components/Dashboard.tsx index 9480a31..7b02908 100644 --- a/lib/components/Dashboard.tsx +++ b/lib/components/Dashboard.tsx @@ -592,8 +592,7 @@ function GpuColumn({ alt="AMD ROCm" width={32} height={32} - className="h-8 w-auto object-contain" - style={{ width: "auto", height: "auto" }} + className="h-8 w-8 object-contain" /> Powered by ROCm {rocmRuntimeVersion} @@ -918,7 +917,7 @@ export function DashboardContent() { >
- Logo + Logo

System Metrics Plus

From 0eb1010fec0966eb3c409ef39a449489d6faca77 Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 11 Apr 2026 10:53:32 -0700 Subject: [PATCH 4/6] fix: add AbortController to prevent React Strict Mode race conditions React Strict Mode double-invokes effects in development, causing concurrent fetches that can result in NetworkError. This change: - Uses useRef to track AbortController - Cancels any pending request before starting a new one - Adds 5s timeout for fetch to prevent hanging - Cleans up controller on unmount --- lib/components/Dashboard.tsx | 45 +++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/lib/components/Dashboard.tsx b/lib/components/Dashboard.tsx index 7b02908..b15b979 100644 --- a/lib/components/Dashboard.tsx +++ b/lib/components/Dashboard.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; import { ThemeProvider, useTheme } from "./ThemeContext"; import type { SystemMetrics } from "@/types/metrics"; import { @@ -863,6 +863,7 @@ export function DashboardContent() { const [activeTab, setActiveTab] = useState("cpu"); const [showPerCore, setShowPerCore] = useState(true); const [showGpuHardware, setShowGpuHardware] = useState(true); + const fetchControllerRef = useRef(null); const toggleTab = (id: TabId) => { setVisibleTabs((prev) => { @@ -880,24 +881,42 @@ export function DashboardContent() { }); }; - const fetchMetrics = useCallback(async () => { - try { - const res = await fetch("/api/metrics"); - if (!res.ok) throw new Error("Failed to fetch"); - const data: SystemMetrics = await res.json(); - setMetrics(data); - setLastUpdate(new Date(data.timestamp)); - setIsLoading(false); - } catch (error) { - console.error("Failed to fetch metrics:", error); - setIsLoading(false); + const fetchMetrics = useCallback(() => { + // Abort any pending request (handles React Strict Mode double-invoke) + if (fetchControllerRef.current) { + fetchControllerRef.current.abort(); } + const controller = new AbortController(); + fetchControllerRef.current = controller; + const timeout = setTimeout(() => controller.abort(), 5000); + + fetch("/api/metrics", { signal: controller.signal }) + .then((res) => { + if (!res.ok) throw new Error("Failed to fetch"); + return res.json(); + }) + .then((data: SystemMetrics) => { + setMetrics(data); + setLastUpdate(new Date(data.timestamp)); + setIsLoading(false); + }) + .catch((error) => { + if (error.name === "AbortError") return; + console.error("Failed to fetch metrics:", error); + setIsLoading(false); + }) + .finally(() => clearTimeout(timeout)); }, []); useEffect(() => { fetchMetrics(); const interval = setInterval(fetchMetrics, UPDATE_INTERVAL); - return () => clearInterval(interval); + return () => { + clearInterval(interval); + if (fetchControllerRef.current) { + fetchControllerRef.current.abort(); + } + }; }, [fetchMetrics]); const getVisibleColumns = () => { From 90138594752cfa31fe61989e76cbcb431429e16b Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 11 Apr 2026 10:57:44 -0700 Subject: [PATCH 5/6] fix: use absolute URL with window.location.origin for API fetch Using relative URL '/api/metrics' may cause issues in some deployment configurations. Switch to absolute URL to ensure the fetch always goes to the correct origin. Also improved error logging to capture error name and message for better debugging. --- lib/components/Dashboard.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/components/Dashboard.tsx b/lib/components/Dashboard.tsx index b15b979..56e0021 100644 --- a/lib/components/Dashboard.tsx +++ b/lib/components/Dashboard.tsx @@ -889,10 +889,11 @@ export function DashboardContent() { const controller = new AbortController(); fetchControllerRef.current = controller; const timeout = setTimeout(() => controller.abort(), 5000); + const url = `${window.location.origin}/api/metrics`; - fetch("/api/metrics", { signal: controller.signal }) + fetch(url, { signal: controller.signal }) .then((res) => { - if (!res.ok) throw new Error("Failed to fetch"); + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); return res.json(); }) .then((data: SystemMetrics) => { @@ -902,7 +903,7 @@ export function DashboardContent() { }) .catch((error) => { if (error.name === "AbortError") return; - console.error("Failed to fetch metrics:", error); + console.error("Failed to fetch metrics:", error.name, error.message); setIsLoading(false); }) .finally(() => clearTimeout(timeout)); From ef420703356e99b8755bf12115c9d18c9e0d930d Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 11 Apr 2026 11:11:44 -0700 Subject: [PATCH 6/6] fix: add mode: same-origin and cache: no-store to fetch options Explicitly set fetch mode to same-origin to prevent any potential CORS preflight issues. Added cache: no-store to prevent response caching issues with streaming. --- lib/components/Dashboard.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/components/Dashboard.tsx b/lib/components/Dashboard.tsx index 56e0021..22da968 100644 --- a/lib/components/Dashboard.tsx +++ b/lib/components/Dashboard.tsx @@ -891,7 +891,12 @@ export function DashboardContent() { const timeout = setTimeout(() => controller.abort(), 5000); const url = `${window.location.origin}/api/metrics`; - fetch(url, { signal: controller.signal }) + fetch(url, { + signal: controller.signal, + credentials: "same-origin", + cache: "no-store", + mode: "same-origin", + }) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); return res.json();