thermal: van-thermal daemon (CPU+NVMe monitor, alert, log)
Single sysfs sample loop drives three jobs off one daemon (modeled on van-failover): - live state at /run/van-thermal/state.json; new Cockpit "Temperatures" card reads it in the existing 5s loop (no per-refresh `sensors` shell-out) - journal alerts on ok<->warn<->crit crossings, with clear_margin hysteresis - throttled, self-rotating CSV history at /var/log/van-thermal.csv Sensors resolved by hwmon name+label (coretemp/Package id 0, nvme/Composite), never by hwmonN index (not stable across boots). Defaults: CPU warn 80 / crit 95, NVMe warn 65 / crit 70. deploy.sh installs + enables the service. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
35466f0141
commit
ef6c1ce182
@@ -23,6 +23,11 @@
|
||||
<div id="ap"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Temperatures</h3>
|
||||
<div id="thermal"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>WAN Failover</h3>
|
||||
<div id="failover"></div>
|
||||
|
||||
@@ -12,6 +12,7 @@ th { color: #6a6e73; font-weight: 600; }
|
||||
.pill { padding: 2px 8px; border-radius: 10px; font-size: 12px; white-space: nowrap; }
|
||||
.ok { background: #bde5b8; color: #1e4f18; }
|
||||
.bad { background: #f0b8b8; color: #5f1414; }
|
||||
.warn { background: #f5d9a8; color: #5f4414; }
|
||||
.muted { color: #6a6e73; font-size: 12px; }
|
||||
.active-wan { font-weight: 700; color: #0066cc; }
|
||||
.btn { margin-left: 6px; padding: 3px 10px; cursor: pointer; }
|
||||
|
||||
@@ -66,6 +66,37 @@ function renderAP(ap) {
|
||||
document.getElementById("ap").innerHTML = html;
|
||||
}
|
||||
|
||||
/* ---------- Temperatures (van-thermal daemon state) ---------- */
|
||||
|
||||
async function readThermal() {
|
||||
try {
|
||||
return JSON.parse(await run(["cat", "/run/van-thermal/state.json"]));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderThermal(th) {
|
||||
const el = document.getElementById("thermal");
|
||||
if (!th || !th.sensors) {
|
||||
el.innerHTML = `<p class="muted">van-thermal daemon not running (no state file).</p>`;
|
||||
return;
|
||||
}
|
||||
const pillClass = { ok: "ok", warn: "warn", crit: "bad" };
|
||||
let html = `<table><thead><tr><th>Sensor</th><th>Temp</th><th>Status</th><th>Warn / Crit</th></tr></thead><tbody>`;
|
||||
Object.keys(th.sensors).forEach(name => {
|
||||
const s = th.sensors[name];
|
||||
const temp = s.temp == null ? "—" : `${esc(s.temp)} °C`;
|
||||
const lvl = s.level || "ok";
|
||||
html += `<tr><td>${esc(name.toUpperCase())}</td><td>${temp}</td>` +
|
||||
`<td><span class="pill ${pillClass[lvl] || ""}">${esc(lvl)}</span></td>` +
|
||||
`<td class="muted">${esc(s.warn)} / ${esc(s.crit)} °C</td></tr>`;
|
||||
});
|
||||
html += `</tbody></table>`;
|
||||
html += `<p class="muted">History: <code>/var/log/van-thermal.csv</code> · alerts: <code>journalctl -u van-thermal</code></p>`;
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
/* ---------- WAN failover (van-failover daemon state) ---------- */
|
||||
|
||||
async function readFailover() {
|
||||
@@ -194,8 +225,9 @@ async function restartAP() {
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const [ap, fo, wan] = await Promise.all([readAP(), readFailover(), readWAN()]);
|
||||
const [ap, th, fo, wan] = await Promise.all([readAP(), readThermal(), readFailover(), readWAN()]);
|
||||
renderAP(ap);
|
||||
renderThermal(th);
|
||||
renderFailover(fo);
|
||||
renderWAN(wan);
|
||||
document.getElementById("updated").textContent = "updated " + new Date().toLocaleTimeString();
|
||||
|
||||
Reference in New Issue
Block a user