Add GPU fan control, live telemetry, and per-profile fan curves in HyperSwap dashboard
This commit is contained in:
323
static/app.js
323
static/app.js
@@ -36,6 +36,7 @@ function updateDashboard(data) {
|
||||
// 1. GPU VRAM Stats
|
||||
const gpu = data.gpu || {};
|
||||
if (gpu.available) {
|
||||
pushOverclockSample(gpu);
|
||||
document.getElementById('gpu-chip-name').textContent = gpu.device_name || 'NVIDIA GPU';
|
||||
document.getElementById('vram-total-used').textContent = gpu.vram_used_gb || '0.0';
|
||||
document.getElementById('vram-used-pct').textContent = `${gpu.vram_used_pct || 0}% USED`;
|
||||
@@ -68,6 +69,45 @@ function updateDashboard(data) {
|
||||
document.getElementById('gpu-power-val').textContent = `${gpu.power_w || 0} W`;
|
||||
document.getElementById('gpu-fan-val').textContent = `${gpu.fan_pct || 0}%`;
|
||||
|
||||
// Per-fan and animations
|
||||
const fan0 = (gpu.fans && gpu.fans.length > 0) ? gpu.fans[0] : (gpu.fan_pct || 0);
|
||||
const fan1 = (gpu.fans && gpu.fans.length > 1) ? gpu.fans[1] : (gpu.fan_pct || 0);
|
||||
const fanSub = document.getElementById('gpu-fan-sub');
|
||||
if (fanSub) fanSub.textContent = `Fan 0: ${fan0}% | Fan 1: ${fan1}%`;
|
||||
|
||||
const fan0Val = document.getElementById('oc-fan0-val');
|
||||
if (fan0Val) fan0Val.textContent = `${fan0}%`;
|
||||
const fan0Bar = document.getElementById('oc-fan0-bar');
|
||||
if (fan0Bar) fan0Bar.style.width = `${fan0}%`;
|
||||
|
||||
const fan1Val = document.getElementById('oc-fan1-val');
|
||||
if (fan1Val) fan1Val.textContent = `${fan1}%`;
|
||||
const fan1Bar = document.getElementById('oc-fan1-bar');
|
||||
if (fan1Bar) fan1Bar.style.width = `${fan1}%`;
|
||||
|
||||
const fanCurrent = document.getElementById('oc-fan-current');
|
||||
if (fanCurrent) fanCurrent.textContent = `${gpu.fan_pct || 0}%`;
|
||||
|
||||
const spinSpeed = Math.max(0.2, (100 - (gpu.fan_pct || 0)) / 100 * 1.6 + 0.3);
|
||||
const fanIcon = document.getElementById('gpu-fan-icon');
|
||||
if (fanIcon) {
|
||||
if ((gpu.fan_pct || 0) > 0) {
|
||||
fanIcon.classList.add('fan-spinning');
|
||||
fanIcon.style.animationDuration = `${spinSpeed.toFixed(2)}s`;
|
||||
} else {
|
||||
fanIcon.classList.remove('fan-spinning');
|
||||
}
|
||||
}
|
||||
const ocFanCardIcon = document.getElementById('oc-fan-card-icon');
|
||||
if (ocFanCardIcon) {
|
||||
if ((gpu.fan_pct || 0) > 0) {
|
||||
ocFanCardIcon.classList.add('fan-spinning');
|
||||
ocFanCardIcon.style.animationDuration = `${spinSpeed.toFixed(2)}s`;
|
||||
} else {
|
||||
ocFanCardIcon.classList.remove('fan-spinning');
|
||||
}
|
||||
}
|
||||
|
||||
// Processes table
|
||||
const tbody = document.getElementById('gpu-proc-table');
|
||||
if (bd.processes && bd.processes.length > 0) {
|
||||
@@ -285,4 +325,287 @@ async function warmAllModels() {
|
||||
// Startup
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initSSE();
|
||||
initOverclockChart();
|
||||
fetchOverclockStatus();
|
||||
setInterval(fetchOverclockStatus, 3000);
|
||||
});
|
||||
|
||||
// ============ OVERCLOCK CONTROL ============
|
||||
let ocProfiles = {};
|
||||
let ocChart = null;
|
||||
const OC_MAX_SAMPLES = 120;
|
||||
|
||||
function initOverclockChart() {
|
||||
const canvas = document.getElementById('oc-chart');
|
||||
if (!canvas || typeof Chart === 'undefined') return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ocChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{ label: 'Core MHz', data: [], borderColor: '#22d3ee', backgroundColor: 'rgba(34,211,238,0.08)', borderWidth: 1.5, pointRadius: 0, tension: 0.35, yAxisID: 'y', fill: true },
|
||||
{ label: 'Mem MHz', data: [], borderColor: '#c084fc', backgroundColor: 'rgba(192,132,252,0.08)', borderWidth: 1.5, pointRadius: 0, tension: 0.35, yAxisID: 'y', fill: false },
|
||||
{ label: 'Temp °C', data: [], borderColor: '#fb7185', backgroundColor: 'rgba(251,113,133,0.08)', borderWidth: 1.5, pointRadius: 0, tension: 0.35, yAxisID: 'y1', fill: false },
|
||||
{ label: 'Power W', data: [], borderColor: '#fbbf24', backgroundColor: 'rgba(251,191,36,0.08)', borderWidth: 1.5, pointRadius: 0, tension: 0.35, yAxisID: 'y2', fill: false },
|
||||
{ label: 'Fan %', data: [], borderColor: '#34d399', backgroundColor: 'rgba(52,211,153,0.08)', borderWidth: 1.5, pointRadius: 0, tension: 0.35, yAxisID: 'y3', fill: false },
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
scales: {
|
||||
x: { ticks: { color: '#64748b', maxTicksLimit: 8, font: { size: 9 } }, grid: { color: 'rgba(51,65,85,0.35)' } },
|
||||
y: { position: 'left', title: { display: true, text: 'MHz', color: '#22d3ee', font: { size: 9 } }, ticks: { color: '#64748b', font: { size: 9 } }, grid: { color: 'rgba(51,65,85,0.35)' } },
|
||||
y1: { position: 'right', title: { display: true, text: '°C', color: '#fb7185', font: { size: 9 } }, ticks: { color: '#64748b', font: { size: 9 } }, grid: { drawOnChartArea: false }, suggestedMin: 0, suggestedMax: 100 },
|
||||
y2: { position: 'right', offset: true, title: { display: true, text: 'W', color: '#fbbf24', font: { size: 9 } }, ticks: { color: '#64748b', font: { size: 9 } }, grid: { drawOnChartArea: false }, suggestedMin: 0, suggestedMax: 400 },
|
||||
y3: { position: 'right', offset: true, title: { display: false }, ticks: { display: false }, grid: { drawOnChartArea: false }, suggestedMin: 0, suggestedMax: 100 },
|
||||
},
|
||||
plugins: { legend: { display: false } },
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function pushOverclockSample(gpu) {
|
||||
if (!ocChart || !gpu || !gpu.available) return;
|
||||
const label = new Date().toLocaleTimeString([], { hour12: false });
|
||||
ocChart.data.labels.push(label);
|
||||
ocChart.data.datasets[0].data.push(gpu.clock_graphics_mhz || 0);
|
||||
ocChart.data.datasets[1].data.push(gpu.clock_mem_mhz || 0);
|
||||
ocChart.data.datasets[2].data.push(gpu.temperature_c || 0);
|
||||
ocChart.data.datasets[3].data.push(gpu.power_w || 0);
|
||||
ocChart.data.datasets[4].data.push(gpu.fan_pct || 0);
|
||||
if (ocChart.data.labels.length > OC_MAX_SAMPLES) {
|
||||
ocChart.data.labels.shift();
|
||||
ocChart.data.datasets.forEach(d => d.data.shift());
|
||||
}
|
||||
ocChart.update('none');
|
||||
}
|
||||
|
||||
async function fetchOverclockStatus() {
|
||||
try {
|
||||
const resp = await fetch('/api/overclock');
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
ocProfiles = data.profiles || {};
|
||||
renderOverclockStatus(data);
|
||||
} catch (err) {
|
||||
console.warn('Overclock fetch error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function renderOverclockStatus(data) {
|
||||
const active = data.active_profile || 'balanced';
|
||||
|
||||
const badge = document.getElementById('oc-active-badge');
|
||||
badge.textContent = `Active: ${active}`;
|
||||
if (active === 'ollama') {
|
||||
badge.className = 'px-2.5 py-1 text-xs font-mono rounded-lg bg-purple-950 border border-purple-700 text-purple-300';
|
||||
} else if (active === 'comfy') {
|
||||
badge.className = 'px-2.5 py-1 text-xs font-mono rounded-lg bg-cyan-950 border border-cyan-700 text-cyan-300';
|
||||
} else {
|
||||
badge.className = 'px-2.5 py-1 text-xs font-mono rounded-lg bg-slate-800 border border-slate-700 text-slate-300';
|
||||
}
|
||||
|
||||
const gpu = data.gpu || {};
|
||||
document.getElementById('oc-power-limit').textContent = `${gpu.power_limit_w ?? '--'} W`;
|
||||
document.getElementById('oc-core').textContent = `${gpu.clock_sm_mhz ?? '--'} MHz`;
|
||||
document.getElementById('oc-mem').textContent = `${gpu.clock_mem_mhz ?? '--'} MHz`;
|
||||
document.getElementById('oc-temp-draw').textContent = `${gpu.temp_c ?? '--'}°C / ${gpu.power_draw_w ?? '--'}W`;
|
||||
|
||||
// Specs strip
|
||||
document.getElementById('oc-spec-gpu').textContent = gpu.name || '--';
|
||||
document.getElementById('oc-spec-driver').textContent = gpu.driver_version || '--';
|
||||
const vramGb = (gpu.vram_total_mb || 0) / 1024;
|
||||
document.getElementById('oc-spec-vram').textContent = vramGb > 0 ? vramGb.toFixed(0) + ' GB' : '--';
|
||||
document.getElementById('oc-spec-maxcore').textContent = (gpu.clock_sm_max_mhz ?? '--') + ' MHz';
|
||||
document.getElementById('oc-spec-maxmem').textContent = (gpu.clock_mem_max_mhz ?? '--') + ' MHz';
|
||||
document.getElementById('oc-spec-power').textContent = `${gpu.power_limit_w ?? '--'} / ${gpu.power_max_w ?? '--'} W`;
|
||||
|
||||
// Fan status
|
||||
const fan = data.fan || {};
|
||||
const fanBadge = document.getElementById('oc-fan-badge');
|
||||
if (fanBadge) {
|
||||
if (fan.manual) {
|
||||
fanBadge.textContent = `MANUAL (${fan.target_speed_pct ?? '--'}%)`;
|
||||
fanBadge.className = 'px-2.5 py-1 text-xs font-mono rounded-lg bg-emerald-950 border border-emerald-500 text-emerald-300 font-bold';
|
||||
} else {
|
||||
fanBadge.textContent = 'AUTO (VBIOS)';
|
||||
fanBadge.className = 'px-2.5 py-1 text-xs font-mono rounded-lg bg-slate-800 border border-slate-700 text-slate-400 font-bold';
|
||||
}
|
||||
}
|
||||
|
||||
const xBadge = document.getElementById('oc-x-badge');
|
||||
if (data.headless_x_running) {
|
||||
xBadge.textContent = 'X: ON';
|
||||
xBadge.className = 'px-2.5 py-1 text-xs font-mono rounded-lg bg-emerald-950 border border-emerald-700 text-emerald-300';
|
||||
} else {
|
||||
xBadge.textContent = 'X: OFF';
|
||||
xBadge.className = 'px-2.5 py-1 text-xs font-mono rounded-lg bg-rose-950 border border-rose-700 text-rose-300';
|
||||
}
|
||||
|
||||
const offsets = (data.last_result && data.last_result.offsets) || {};
|
||||
const note = document.getElementById('oc-offset-note');
|
||||
if (offsets.supported) {
|
||||
note.textContent = '✅ Clock offsets active (proprietary driver)';
|
||||
note.className = 'text-[11px] font-mono text-emerald-400';
|
||||
} else {
|
||||
note.textContent = '⚠️ Clock offsets off — nvidia-open lacks offset support (power + clock locks active)';
|
||||
note.className = 'text-[11px] font-mono text-amber-400';
|
||||
}
|
||||
|
||||
// highlight active profile button
|
||||
['ollama', 'comfy', 'balanced'].forEach(p => {
|
||||
const btn = document.getElementById('oc-btn-' + p);
|
||||
if (p === active) {
|
||||
btn.classList.add('ring-2', 'ring-fuchsia-400');
|
||||
} else {
|
||||
btn.classList.remove('ring-2', 'ring-fuchsia-400');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadOverclockForProfile(name) {
|
||||
const p = ocProfiles[name];
|
||||
if (!p) return;
|
||||
document.getElementById('oc-slider-power').value = p.power_limit_w || 370;
|
||||
document.getElementById('oc-val-power').textContent = (p.power_limit_w || 370) + ' W';
|
||||
document.getElementById('oc-slider-core').value = p.core_offset_mhz || 0;
|
||||
document.getElementById('oc-val-core').textContent = '+' + (p.core_offset_mhz || 0) + ' MHz';
|
||||
document.getElementById('oc-slider-mem').value = p.mem_offset_mhz || 0;
|
||||
document.getElementById('oc-val-mem').textContent = '+' + (p.mem_offset_mhz || 0) + ' MHz';
|
||||
document.getElementById('oc-lockcore-toggle').checked = (p.lock_core_max || 0) > 0;
|
||||
document.getElementById('oc-val-lockcore').textContent = (p.lock_core_max || 0) > 0 ? 'On' : 'Off';
|
||||
document.getElementById('oc-lockmem-toggle').checked = (p.lock_mem_mhz || 0) > 0;
|
||||
document.getElementById('oc-val-lockmem').textContent = (p.lock_mem_mhz || 0) > 0 ? 'On' : 'Off';
|
||||
|
||||
const fanMode = p.fan_mode || 'auto';
|
||||
const profFanMode = document.getElementById('oc-prof-fanmode');
|
||||
if (profFanMode) profFanMode.value = fanMode;
|
||||
const profFanSpeed = document.getElementById('oc-slider-prof-fanspeed');
|
||||
if (profFanSpeed) profFanSpeed.value = p.fan_speed_pct || 70;
|
||||
const profFanVal = document.getElementById('oc-val-prof-fanspeed');
|
||||
if (profFanVal) profFanVal.textContent = (p.fan_speed_pct || 70) + '%';
|
||||
toggleProfileFanMode();
|
||||
}
|
||||
|
||||
function toggleProfileFanMode() {
|
||||
const mode = document.getElementById('oc-prof-fanmode').value;
|
||||
const container = document.getElementById('oc-prof-fanspeed-container');
|
||||
const valBadge = document.getElementById('oc-val-prof-fanmode');
|
||||
if (valBadge) valBadge.textContent = (mode === 'manual') ? 'Manual Target' : 'Auto (VBIOS)';
|
||||
if (container) {
|
||||
if (mode === 'manual') {
|
||||
container.classList.remove('opacity-50', 'pointer-events-none');
|
||||
} else {
|
||||
container.classList.add('opacity-50', 'pointer-events-none');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCoreLock() {
|
||||
document.getElementById('oc-val-lockcore').textContent = document.getElementById('oc-lockcore-toggle').checked ? 'On' : 'Off';
|
||||
}
|
||||
|
||||
function toggleMemLock() {
|
||||
document.getElementById('oc-val-lockmem').textContent = document.getElementById('oc-lockmem-toggle').checked ? 'On' : 'Off';
|
||||
}
|
||||
|
||||
async function setFanManual(speed) {
|
||||
let pct = speed;
|
||||
if (pct === undefined || pct === null) {
|
||||
pct = parseInt(document.getElementById('oc-fan-slider').value);
|
||||
} else {
|
||||
pct = parseInt(pct);
|
||||
const slider = document.getElementById('oc-fan-slider');
|
||||
if (slider) slider.value = pct;
|
||||
const label = document.getElementById('oc-fan-slider-label');
|
||||
if (label) label.textContent = pct + '%';
|
||||
}
|
||||
try {
|
||||
const resp = await fetch('/api/overclock/fan', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mode: 'manual', percent: pct })
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (!resp.ok) alert(`Fan set failed: ${result.detail || 'error'}`);
|
||||
await fetchOverclockStatus();
|
||||
} catch (err) {
|
||||
alert(`Error setting fan speed: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function setFanAuto() {
|
||||
try {
|
||||
const resp = await fetch('/api/overclock/fan', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mode: 'auto' })
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (!resp.ok) alert(`Fan auto failed: ${result.detail || 'error'}`);
|
||||
await fetchOverclockStatus();
|
||||
} catch (err) {
|
||||
alert(`Error setting fan to auto: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function applyOverclock(profile) {
|
||||
try {
|
||||
const resp = await fetch('/api/overclock/apply', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ profile })
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (!resp.ok) {
|
||||
alert(`Apply failed: ${result.detail || 'error'}`);
|
||||
}
|
||||
await fetchOverclockStatus();
|
||||
} catch (err) {
|
||||
alert(`Error: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOverclockProfile() {
|
||||
const name = document.getElementById('oc-edit-profile').value;
|
||||
const power = parseInt(document.getElementById('oc-slider-power').value);
|
||||
const core = parseInt(document.getElementById('oc-slider-core').value);
|
||||
const mem = parseInt(document.getElementById('oc-slider-mem').value);
|
||||
const lockCore = document.getElementById('oc-lockcore-toggle').checked;
|
||||
const lockMem = document.getElementById('oc-lockmem-toggle').checked;
|
||||
const fanMode = document.getElementById('oc-prof-fanmode') ? document.getElementById('oc-prof-fanmode').value : 'auto';
|
||||
const fanSpeed = document.getElementById('oc-slider-prof-fanspeed') ? parseInt(document.getElementById('oc-slider-prof-fanspeed').value) : 70;
|
||||
|
||||
const config = {
|
||||
power_limit_w: power,
|
||||
core_offset_mhz: core,
|
||||
mem_offset_mhz: mem,
|
||||
lock_core_min: lockCore ? 2900 : 0,
|
||||
lock_core_max: lockCore ? 3105 : 0,
|
||||
lock_mem_mhz: lockMem ? 11501 : 0,
|
||||
fan_mode: fanMode,
|
||||
fan_speed_pct: (fanMode === 'manual') ? fanSpeed : 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/overclock/profiles/${name}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ config })
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (resp.ok) {
|
||||
await fetchOverclockStatus();
|
||||
await applyOverclock(name);
|
||||
} else {
|
||||
alert(`Save failed: ${result.detail || 'error'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Error: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HYPERSWAP // Dual-Engine Model Orchestrator & Live Telemetry</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
@@ -351,9 +352,13 @@
|
||||
<span class="text-[10px] text-slate-400 uppercase block">Power Draw</span>
|
||||
<span id="gpu-power-val" class="text-xl font-bold text-amber-400">0 W</span>
|
||||
</div>
|
||||
<div class="p-3 rounded-xl bg-slate-950 border border-slate-800 text-center">
|
||||
<div class="p-3 rounded-xl bg-slate-950 border border-slate-800 text-center relative overflow-hidden">
|
||||
<span class="text-[10px] text-slate-400 uppercase block">Fan Speed</span>
|
||||
<span id="gpu-fan-val" class="text-xl font-bold text-slate-300">0%</span>
|
||||
<div class="flex items-center justify-center space-x-1.5">
|
||||
<i id="gpu-fan-icon" class="fa-solid fa-fan text-emerald-400 text-sm"></i>
|
||||
<span id="gpu-fan-val" class="text-xl font-bold text-emerald-400">0%</span>
|
||||
</div>
|
||||
<span id="gpu-fan-sub" class="text-[9px] font-mono text-slate-500 block truncate">Fan 0: --% | Fan 1: --%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -419,6 +424,245 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- OVERCLOCK CONTROL PANEL -->
|
||||
<div class="bg-slate-900/80 border border-fuchsia-900/50 rounded-2xl p-5 space-y-4 shadow-lg shadow-fuchsia-950/30">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 pb-3 border-b border-slate-800">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="p-2 rounded-lg bg-fuchsia-950/80 border border-fuchsia-800 text-fuchsia-400">
|
||||
<i class="fa-solid fa-rocket text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-slate-100 text-sm">GPU Overclock Control</h3>
|
||||
<p class="text-xs text-slate-400">Per-app profiles auto-switch with the VRAM arbitrator</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span id="oc-active-badge" class="px-2.5 py-1 text-xs font-mono rounded-lg bg-slate-800 border border-slate-700 text-slate-300">Active: --</span>
|
||||
<span id="oc-x-badge" class="px-2.5 py-1 text-xs font-mono rounded-lg bg-slate-800 border border-slate-700 text-slate-400">X: --</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile quick-select -->
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<button id="oc-btn-ollama" onclick="applyOverclock('ollama')" class="py-2.5 rounded-xl border text-xs font-bold transition flex items-center justify-center space-x-2 bg-purple-950/60 border-purple-800 text-purple-300 hover:bg-purple-900/60">
|
||||
<i class="fa-solid fa-brain"></i><span>Ollama</span>
|
||||
</button>
|
||||
<button id="oc-btn-comfy" onclick="applyOverclock('comfy')" class="py-2.5 rounded-xl border text-xs font-bold transition flex items-center justify-center space-x-2 bg-cyan-950/60 border-cyan-800 text-cyan-300 hover:bg-cyan-900/60">
|
||||
<i class="fa-solid fa-palette"></i><span>ComfyUI</span>
|
||||
</button>
|
||||
<button id="oc-btn-balanced" onclick="applyOverclock('balanced')" class="py-2.5 rounded-xl border text-xs font-bold transition flex items-center justify-center space-x-2 bg-slate-800/60 border-slate-700 text-slate-300 hover:bg-slate-700/60">
|
||||
<i class="fa-solid fa-scale-balanced"></i><span>Balanced</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Live status readback -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 font-mono text-center">
|
||||
<div class="p-3 rounded-xl bg-slate-950 border border-slate-800">
|
||||
<span class="text-[10px] text-slate-400 uppercase block">Power Limit</span>
|
||||
<span id="oc-power-limit" class="text-lg font-bold text-amber-400">-- W</span>
|
||||
</div>
|
||||
<div class="p-3 rounded-xl bg-slate-950 border border-slate-800">
|
||||
<span class="text-[10px] text-slate-400 uppercase block">Core Clock</span>
|
||||
<span id="oc-core" class="text-lg font-bold text-cyan-400">--</span>
|
||||
</div>
|
||||
<div class="p-3 rounded-xl bg-slate-950 border border-slate-800">
|
||||
<span class="text-[10px] text-slate-400 uppercase block">Mem Clock</span>
|
||||
<span id="oc-mem" class="text-lg font-bold text-purple-400">--</span>
|
||||
</div>
|
||||
<div class="p-3 rounded-xl bg-slate-950 border border-slate-800">
|
||||
<span class="text-[10px] text-slate-400 uppercase block">Temp / Draw</span>
|
||||
<span id="oc-temp-draw" class="text-lg font-bold text-emerald-400">--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GPU FAN COOLING CONTROL -->
|
||||
<div class="p-4 rounded-xl bg-slate-950 border border-slate-800 space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i id="oc-fan-card-icon" class="fa-solid fa-fan text-emerald-400 text-sm fan-spinning"></i>
|
||||
<div>
|
||||
<span class="text-xs font-semibold text-slate-200 uppercase font-mono block">GPU Fan Cooling Control</span>
|
||||
<span class="text-[10px] text-slate-400 font-mono">Dual-fan PWM active speed regulation</span>
|
||||
</div>
|
||||
</div>
|
||||
<span id="oc-fan-badge" class="px-2.5 py-1 text-xs font-mono rounded-lg bg-emerald-950/80 border border-emerald-700 text-emerald-300 font-bold">
|
||||
AUTO (VBIOS)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Fan Presets -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-5 gap-2">
|
||||
<button onclick="setFanAuto()" class="py-1.5 px-2 rounded-lg bg-slate-900 hover:bg-slate-800 border border-slate-700 text-slate-300 text-xs font-mono font-semibold transition text-center flex items-center justify-center space-x-1">
|
||||
<i class="fa-solid fa-wand-magic-sparkles text-cyan-400 text-[10px]"></i>
|
||||
<span>Auto (VBIOS)</span>
|
||||
</button>
|
||||
<button onclick="setFanManual(50)" class="py-1.5 px-2 rounded-lg bg-slate-900 hover:bg-slate-800 border border-slate-700 text-slate-300 text-xs font-mono font-semibold transition text-center">
|
||||
50% Quiet
|
||||
</button>
|
||||
<button onclick="setFanManual(65)" class="py-1.5 px-2 rounded-lg bg-slate-900 hover:bg-slate-800 border border-slate-700 text-slate-300 text-xs font-mono font-semibold transition text-center">
|
||||
65% Balanced
|
||||
</button>
|
||||
<button onclick="setFanManual(80)" class="py-1.5 px-2 rounded-lg bg-slate-900 hover:bg-slate-800 border border-slate-700 text-slate-300 text-xs font-mono font-semibold transition text-center">
|
||||
80% Heavy
|
||||
</button>
|
||||
<button onclick="setFanManual(100)" class="py-1.5 px-2 rounded-lg bg-rose-950/60 hover:bg-rose-900/80 border border-rose-800 text-rose-300 text-xs font-mono font-bold transition text-center flex items-center justify-center space-x-1">
|
||||
<i class="fa-solid fa-gauge-max text-rose-400 text-[10px]"></i>
|
||||
<span>100% Turbo</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Live Dual-Fan Meters -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 p-3 rounded-lg bg-slate-900/60 border border-slate-800/80 text-xs font-mono">
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between text-slate-400">
|
||||
<span class="flex items-center space-x-1"><i class="fa-solid fa-fan text-[10px] text-cyan-400"></i><span>Fan 0 (Intake/Core):</span></span>
|
||||
<span id="oc-fan0-val" class="font-bold text-slate-100">--%</span>
|
||||
</div>
|
||||
<div class="w-full bg-slate-950 rounded-full h-2 overflow-hidden border border-slate-800">
|
||||
<div id="oc-fan0-bar" class="bg-gradient-to-r from-cyan-500 to-emerald-400 h-full rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between text-slate-400">
|
||||
<span class="flex items-center space-x-1"><i class="fa-solid fa-fan text-[10px] text-purple-400"></i><span>Fan 1 (Exhaust/VRM):</span></span>
|
||||
<span id="oc-fan1-val" class="font-bold text-slate-100">--%</span>
|
||||
</div>
|
||||
<div class="w-full bg-slate-950 rounded-full h-2 overflow-hidden border border-slate-800">
|
||||
<div id="oc-fan1-bar" class="bg-gradient-to-r from-purple-500 to-emerald-400 h-full rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Manual Slider -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<span class="text-slate-400">Custom Manual Target:</span>
|
||||
<span id="oc-fan-slider-label" class="font-mono text-emerald-400 font-bold">65%</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input id="oc-fan-slider" type="range" min="30" max="100" step="1" value="65" oninput="document.getElementById('oc-fan-slider-label').textContent=this.value+'%'" class="flex-1 accent-emerald-500">
|
||||
<button onclick="setFanManual(parseInt(document.getElementById('oc-fan-slider').value))" class="px-3.5 py-1.5 rounded-lg bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 text-white text-xs font-bold font-mono shadow transition flex items-center space-x-1">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
<span>Apply</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GPU SPECS STRIP -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-2 font-mono text-center">
|
||||
<div class="p-2 rounded-lg bg-slate-950 border border-slate-800">
|
||||
<span class="text-[9px] text-slate-500 uppercase block">GPU</span>
|
||||
<span id="oc-spec-gpu" class="text-[11px] font-bold text-slate-200 block truncate">--</span>
|
||||
</div>
|
||||
<div class="p-2 rounded-lg bg-slate-950 border border-slate-800">
|
||||
<span class="text-[9px] text-slate-500 uppercase block">Driver</span>
|
||||
<span id="oc-spec-driver" class="text-[11px] font-bold text-cyan-400 block">--</span>
|
||||
</div>
|
||||
<div class="p-2 rounded-lg bg-slate-950 border border-slate-800">
|
||||
<span class="text-[9px] text-slate-500 uppercase block">VRAM</span>
|
||||
<span id="oc-spec-vram" class="text-[11px] font-bold text-purple-400 block">--</span>
|
||||
</div>
|
||||
<div class="p-2 rounded-lg bg-slate-950 border border-slate-800">
|
||||
<span class="text-[9px] text-slate-500 uppercase block">Max Core</span>
|
||||
<span id="oc-spec-maxcore" class="text-[11px] font-bold text-cyan-400 block">--</span>
|
||||
</div>
|
||||
<div class="p-2 rounded-lg bg-slate-950 border border-slate-800">
|
||||
<span class="text-[9px] text-slate-500 uppercase block">Max Mem</span>
|
||||
<span id="oc-spec-maxmem" class="text-[11px] font-bold text-purple-400 block">--</span>
|
||||
</div>
|
||||
<div class="p-2 rounded-lg bg-slate-950 border border-slate-800">
|
||||
<span class="text-[9px] text-slate-500 uppercase block">Power</span>
|
||||
<span id="oc-spec-power" class="text-[11px] font-bold text-amber-400 block">--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LIVE TIME-SERIES GRAPH (clocks + temp + fan) -->
|
||||
<div class="rounded-xl bg-slate-950 border border-slate-800 p-3">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs font-semibold text-slate-300 uppercase font-mono">
|
||||
<i class="fa-solid fa-chart-line text-fuchsia-400 mr-1"></i>Live Tuning Graph
|
||||
</span>
|
||||
<div class="flex flex-wrap items-center space-x-3 text-[10px] font-mono">
|
||||
<span class="flex items-center space-x-1"><span class="w-2 h-2 rounded-full bg-cyan-400 inline-block"></span>Core MHz</span>
|
||||
<span class="flex items-center space-x-1"><span class="w-2 h-2 rounded-full bg-purple-400 inline-block"></span>Mem MHz</span>
|
||||
<span class="flex items-center space-x-1"><span class="w-2 h-2 rounded-full bg-rose-400 inline-block"></span>Temp °C</span>
|
||||
<span class="flex items-center space-x-1"><span class="w-2 h-2 rounded-full bg-amber-400 inline-block"></span>Power W</span>
|
||||
<span class="flex items-center space-x-1"><span class="w-2 h-2 rounded-full bg-emerald-400 inline-block"></span>Fan %</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-64">
|
||||
<canvas id="oc-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fine-tune sliders for the selected profile -->
|
||||
<div class="p-4 rounded-xl bg-slate-950 border border-slate-800 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-semibold text-slate-300 uppercase font-mono">Fine-tune profile</span>
|
||||
<select id="oc-edit-profile" onchange="loadOverclockForProfile(this.value)" class="bg-slate-900 border border-slate-700 rounded-lg px-2 py-1 text-xs font-mono text-slate-200 focus:outline-none focus:border-fuchsia-500">
|
||||
<option value="ollama">ollama</option>
|
||||
<option value="comfy">comfy</option>
|
||||
<option value="balanced">balanced</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
<div>
|
||||
<label class="text-xs text-slate-400 flex justify-between"><span>Power Limit</span><span id="oc-val-power" class="font-mono text-amber-400">370 W</span></label>
|
||||
<input id="oc-slider-power" type="range" min="150" max="370" step="5" value="370" oninput="document.getElementById('oc-val-power').textContent=this.value+' W'" class="w-full accent-amber-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-slate-400 flex justify-between"><span>Core Offset</span><span id="oc-val-core" class="font-mono text-cyan-400">+100 MHz</span></label>
|
||||
<input id="oc-slider-core" type="range" min="0" max="200" step="5" value="100" oninput="document.getElementById('oc-val-core').textContent='+'+this.value+' MHz'" class="w-full accent-cyan-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-slate-400 flex justify-between"><span>Memory Offset</span><span id="oc-val-mem" class="font-mono text-purple-400">+500 MHz</span></label>
|
||||
<input id="oc-slider-mem" type="range" min="0" max="1000" step="25" value="500" oninput="document.getElementById('oc-val-mem').textContent='+'+this.value+' MHz'" class="w-full accent-purple-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div>
|
||||
<label class="text-xs text-slate-400 flex justify-between"><span>Core Clock Lock</span><span id="oc-val-lockcore" class="font-mono text-slate-400">Off</span></label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input id="oc-lockcore-toggle" type="checkbox" onchange="toggleCoreLock()" class="w-4 h-4 accent-cyan-500">
|
||||
<span class="text-xs text-slate-500">Lock core to max boost (2900–3105 MHz)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-slate-400 flex justify-between"><span>Memory Clock Lock</span><span id="oc-val-lockmem" class="font-mono text-slate-400">Off</span></label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input id="oc-lockmem-toggle" type="checkbox" onchange="toggleMemLock()" class="w-4 h-4 accent-purple-500">
|
||||
<span class="text-xs text-slate-500">Lock mem to max (11501 MHz)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Per-Profile Fan Settings -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 pt-3 border-t border-slate-900">
|
||||
<div>
|
||||
<label class="text-xs text-slate-400 flex justify-between"><span>Profile Fan Mode</span><span id="oc-val-prof-fanmode" class="font-mono text-emerald-400">Auto</span></label>
|
||||
<select id="oc-prof-fanmode" onchange="toggleProfileFanMode()" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 mt-1 focus:outline-none focus:border-emerald-500">
|
||||
<option value="auto">Auto (VBIOS Dynamic Curve)</option>
|
||||
<option value="manual">Manual (Locked Target %)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="oc-prof-fanspeed-container" class="opacity-50 pointer-events-none transition">
|
||||
<label class="text-xs text-slate-400 flex justify-between"><span>Profile Fan Target</span><span id="oc-val-prof-fanspeed" class="font-mono text-emerald-400">70%</span></label>
|
||||
<input id="oc-slider-prof-fanspeed" type="range" min="30" max="100" step="5" value="70" oninput="document.getElementById('oc-val-prof-fanspeed').textContent=this.value+'%'" class="w-full accent-emerald-500 mt-2">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<span id="oc-offset-note" class="text-[11px] font-mono text-slate-500"></span>
|
||||
<button onclick="saveOverclockProfile()" class="px-4 py-2 bg-gradient-to-r from-fuchsia-600 to-purple-600 hover:from-fuchsia-500 hover:to-purple-500 text-white text-xs font-bold rounded-lg shadow-md transition flex items-center space-x-1.5">
|
||||
<i class="fa-solid fa-floppy-disk"></i><span>Save Profile</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
|
||||
@@ -26,3 +26,13 @@
|
||||
.glow-card {
|
||||
animation: pulseGlow 4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes spinFan {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.fan-spinning {
|
||||
display: inline-block;
|
||||
animation: spinFan 1s linear infinite;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user