refactor(admin): focus System tab temperatures on the CPU die
Some checks failed
deploy / test-frontend (pull_request) Waiting to run
deploy / build-and-push (pull_request) Blocked by required conditions
deploy / deploy (pull_request) Blocked by required conditions
deploy / test-backend (pull_request) Has been cancelled

The Temperatures box listed every component sysinfo exposes (per-core,
chipset, NVMe…), dominating the System tab. Surface only the CPU
die/package reading and collapse the rest behind a disclosure.

Add a tested pure helper, pickCpuTemp, that selects the best CPU sensor
(Intel package → AMD Tctl/Tdie → hottest core → any cpu/coretemp → null),
and render it via a shared Temp snippet with the remaining sensors in a
default-closed "N other sensors" details element.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-16 20:19:14 +02:00
parent 937b6a9588
commit d64ab038ef
3 changed files with 129 additions and 28 deletions

View File

@@ -0,0 +1,43 @@
import { describe, it, expect } from 'vitest';
import { pickCpuTemp } from './temps';
import type { TempStat } from '$lib/api/admin';
function t(label: string, celsius: number): TempStat {
return { label, celsius, max_celsius: celsius, critical_celsius: 100 };
}
describe('pickCpuTemp', () => {
it('returns null when there are no sensors', () => {
expect(pickCpuTemp([])).toBeNull();
});
it('prefers the Intel package sensor over individual cores', () => {
const temps = [
t('coretemp Core 0', 50),
t('coretemp Core 1', 49),
t('coretemp Package id 0', 51),
t('acpitz temp1', 28)
];
expect(pickCpuTemp(temps)?.label).toBe('coretemp Package id 0');
});
it('falls back to the AMD Tctl/Tdie sensor when there is no package', () => {
const temps = [t('k10temp Tctl', 55), t('nvme Composite', 41)];
expect(pickCpuTemp(temps)?.label).toBe('k10temp Tctl');
});
it('falls back to the hottest core when only per-core sensors exist', () => {
const temps = [t('coretemp Core 0', 50), t('coretemp Core 3', 58), t('coretemp Core 1', 49)];
expect(pickCpuTemp(temps)?.label).toBe('coretemp Core 3');
});
it('falls back to any cpu/coretemp-labelled sensor as a last resort', () => {
const temps = [t('nvme Composite', 41), t('cpu_thermal', 47)];
expect(pickCpuTemp(temps)?.label).toBe('cpu_thermal');
});
it('returns null when nothing looks like a CPU sensor', () => {
const temps = [t('nvme Composite', 41), t('acpitz temp1', 28)];
expect(pickCpuTemp(temps)).toBeNull();
});
});

View File

@@ -0,0 +1,25 @@
import type { TempStat } from '$lib/api/admin';
/** Pick the sensor that best represents the CPU die/package temperature
* from a heterogeneous component list. Intel exposes
* `coretemp Package id 0`; AMD exposes `k10temp Tctl`/`Tdie`. When neither
* is present we fall back to the hottest individual core, then to any
* CPU-ish sensor, and finally `null` (nothing CPU-related — e.g. only NVMe
* / chipset sensors are exposed). Used to keep the System tab focused on
* the one reading operators care about, with the rest tucked away. */
export function pickCpuTemp(temps: TempStat[]): TempStat | null {
const byLabel = (re: RegExp) => temps.find((t) => re.test(t.label)) ?? null;
return (
byLabel(/package/i) ??
byLabel(/tctl|tdie|k10temp/i) ??
hottestCore(temps) ??
byLabel(/cpu|coretemp/i) ??
null
);
}
function hottestCore(temps: TempStat[]): TempStat | null {
const cores = temps.filter((t) => /core\s*\d+/i.test(t.label));
if (cores.length === 0) return null;
return cores.reduce((a, b) => (b.celsius > a.celsius ? b : a));
}

View File

@@ -8,8 +8,16 @@
type StorageStats
} from '$lib/api/admin';
import { formatBytes as fmtBytes } from '$lib/upload-validation';
import { pickCpuTemp } from '$lib/admin/temps';
let stats: SystemStats | null = $state(null);
// The component list is noisy (every core, chipset, NVMe…). Surface the
// CPU die/package reading and tuck the rest behind a disclosure.
const cpuTemp = $derived.by(() => (stats ? pickCpuTemp(stats.temperatures) : null));
const otherTemps = $derived.by(() =>
stats ? stats.temperatures.filter((t) => t !== cpuTemp) : []
);
let error: string | null = $state(null);
let timer: ReturnType<typeof setInterval> | null = null;
let storageTimer: ReturnType<typeof setInterval> | null = null;
@@ -168,36 +176,19 @@
<h2>Temperatures</h2>
{#if stats.temperatures.length === 0}
<p class="muted">Sensor data unavailable (not exposed in this environment).</p>
{:else if cpuTemp}
{@render Temp(cpuTemp)}
{#if otherTemps.length > 0}
<details class="more-temps">
<summary>{otherTemps.length} other sensor{otherTemps.length === 1 ? '' : 's'}</summary>
{#each otherTemps as t (t.label)}
{@render Temp(t)}
{/each}
</details>
{/if}
{:else}
{#each stats.temperatures as t (t.label)}
{@const limit = t.critical_celsius ?? 100}
{@const pct = Math.min(100, Math.max(0, (t.celsius / limit) * 100))}
<div class="temp">
<div class="temp-head">
<span class="temp-label">{t.label}</span>
<span class="temp-val">{t.celsius.toFixed(0)}°C</span>
</div>
<div
class="bar"
role="progressbar"
aria-valuenow={t.celsius}
aria-valuemin="0"
aria-valuemax={limit}
aria-label="{t.label} {t.celsius.toFixed(0)}°C"
>
<div
class="fill"
class:high={pct >= 90}
class:mid={pct >= 70 && pct < 90}
style:width="{pct}%"
></div>
</div>
<p class="sub">
{#if t.max_celsius !== null}max {t.max_celsius.toFixed(0)}°C{/if}
{#if t.critical_celsius !== null}
· crit {t.critical_celsius.toFixed(0)}°C{/if}
</p>
</div>
{@render Temp(t)}
{/each}
{/if}
</article>
@@ -299,6 +290,37 @@
{/if}
</section>
{#snippet Temp(t: { label: string; celsius: number; max_celsius: number | null; critical_celsius: number | null })}
{@const limit = t.critical_celsius ?? 100}
{@const pct = Math.min(100, Math.max(0, (t.celsius / limit) * 100))}
<div class="temp">
<div class="temp-head">
<span class="temp-label">{t.label}</span>
<span class="temp-val">{t.celsius.toFixed(0)}°C</span>
</div>
<div
class="bar"
role="progressbar"
aria-valuenow={t.celsius}
aria-valuemin="0"
aria-valuemax={limit}
aria-label="{t.label} {t.celsius.toFixed(0)}°C"
>
<div
class="fill"
class:high={pct >= 90}
class:mid={pct >= 70 && pct < 90}
style:width="{pct}%"
></div>
</div>
<p class="sub">
{#if t.max_celsius !== null}max {t.max_celsius.toFixed(0)}°C{/if}
{#if t.critical_celsius !== null}
· crit {t.critical_celsius.toFixed(0)}°C{/if}
</p>
</div>
{/snippet}
{#snippet Bar({ percent }: { percent: number })}
<div
class="bar"
@@ -400,6 +422,17 @@
.temp + .temp {
margin-top: var(--space-3);
}
.more-temps {
margin-top: var(--space-3);
}
.more-temps summary {
cursor: pointer;
font-size: var(--font-sm);
color: var(--text-muted);
}
.more-temps .temp:first-of-type {
margin-top: var(--space-3);
}
.temp-head {
display: flex;
justify-content: space-between;