feat: add fan curve control via temperature-based fan speed curves

- Backend HAL (hal/fans.py): NVML v2 fan read/set/reset with min/max queries
- Server endpoints: GET/POST /api/fans, POST /api/fans/reset, POST /api/fans/speed
- Background fan poller: reads GPU temp every 2s, interpolates fan speed from curve
- Profile integration: fan_curve field saved/applied, auto-restore on shutdown
- Frontend: FanCurveEditor (SVG chart with drag/add/delete points), FanMonitor sidebar
- App.tsx: three-tab layout (Curve, Performance, Fans)
- GaugeCard: optional history sparkline, Fan Mode card without sparkline
- fan_mode field populated as 'curve' or 'auto' in GET /api/fans
This commit is contained in:
ARIA committed 2026-08-01 15:59:39 +02:00
1 parent 7605d21e27
commit 477c4becae
11 files changed
+1288 -10

No files matched your search

+38 -4
View File
@@ -7,12 +7,15 @@ import { CurveEditor } from './components/CurveEditor/CurveEditor';
import { PointTable } from './components/PointTable/PointTable';
import { PerformancePanel } from './components/Limits/PerformancePanel';
import { PerformanceMonitor } from './components/Monitor/PerformanceMonitor';
import { FanMonitor } from './components/Monitor/FanMonitor';
import { FanCurveEditor } from './components/Fans/FanCurveEditor';
import { ProfilePanel } from './components/Profiles/ProfilePanel';
import { api } from './api/client';
import { useCurveStore } from './store/curveStore';
import { Toaster } from 'sonner';
import { Loader, ChevronDown } from 'lucide-react';
import { useState, useRef, useEffect } from 'react';
import type { FanState } from './types';
export default function App() {
const gpuInfo = useGpu();
@@ -20,7 +23,8 @@ export default function App() {
const { monitor, monitorHistory, wsStatus: monitorWsStatus } = useMonitor();
const { setCurve, activeProfile, setActiveProfile, selectedGpuIndex } = useCurveStore();
const [activeTab, setActiveTab] = useState<'curve' | 'performance'>('curve');
const [activeTab, setActiveTab] = useState<'curve' | 'performance' | 'fans'>('curve');
const [fanState, setFanState] = useState<FanState | null>(null);
const [activeDomain, setActiveDomain] = useState<'gpu' | 'memory'>('gpu');
const [isProfileOpen, setIsProfileOpen] = useState(false);
const profileRef = useRef<HTMLDivElement>(null);
@@ -45,6 +49,16 @@ export default function App() {
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [selectedGpuIndex, setActiveProfile]);
function refreshFans() {
api.fans(selectedGpuIndex).then(setFanState).catch(console.error);
}
useEffect(() => {
if (activeTab === 'fans') {
refreshFans();
}
}, [activeTab, selectedGpuIndex]);
// Worst connection status wins
const wsStatus =
monitorWsStatus === 'disconnected' || curveWsStatus === 'disconnected'
@@ -81,6 +95,12 @@ export default function App() {
>
Performance
</button>
<button
onClick={() => setActiveTab('fans')}
className={`text-lg font-medium pb-2 -mb-[9px] border-b-2 transition-colors ${activeTab === 'fans' ? 'border-pink-500 text-zinc-100' : 'border-transparent text-zinc-500 hover:text-zinc-300'}`}
>
Fans
</button>
</div>
<div className="relative" ref={profileRef}>
<button
@@ -126,12 +146,12 @@ export default function App() {
</div>
)}
</div>
<div className="w-80 shrink-0 flex flex-col">
<LiveMonitor monitor={monitor} history={monitorHistory} />
</div>
</div>
{curve && (
<div className="w-full">
<PointTable
@@ -142,7 +162,7 @@ export default function App() {
</div>
)}
</>
) : (
) : activeTab === 'performance' ? (
<div className="flex gap-4 items-start w-full">
<div className="flex-1 min-w-0">
<PerformancePanel />
@@ -151,6 +171,20 @@ export default function App() {
<PerformanceMonitor monitor={monitor} history={monitorHistory} />
</div>
</div>
) : (
<div className="flex gap-4 items-start w-full">
<div className="flex-1 min-w-0">
<FanCurveEditor onChanged={refreshFans} />
</div>
<div className="w-80 shrink-0 flex flex-col">
<FanMonitor
monitor={monitor}
history={monitorHistory}
fanCurve={fanState?.curve ?? null}
fanCurveActive={fanState?.curve_active ?? false}
/>
</div>
</div>
)}
</div>
</main>