/* * SysDeck - Sensors Panel (v0.0.11) * Author: Jeremy Anderson (https://dcos.net) * * Hardware sensor readings from lm_sensors. Attributes the * cockpit-sensors plugin (MIT, ocristopfer) whose standalone plugin * lives at https://github.com/ocristopfer/cockpit-sensors. * * This panel invokes `sensors -j` via cockpit.spawn as a separate * process — no cockpit-sensors code is bundled. * * Shows temperature, fan speed, and voltage readings grouped by * hardware adapter. * * v0.1.4 SECURITY: adapter names and sensor keys come from `sensors -j` * output (driver-controlled chip labels — a malicious/things driver or * SMBus device controls them) and error messages from spawn — all now * escaped before innerHTML (0.3.0 audit). */ function escapeHtml(s) { return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } export async function mount(panel, { bridge, EventBus }) { panel.innerHTML = renderSkeleton(); let data = {}; try { data = await bridge.sensors.summary(); } catch (err) { panel.innerHTML = renderError(err); return; } const temps = filterSensors(data, (key) => /temp|core|tctl/i.test(key)); const fans = filterSensors(data, (key) => /fan/i.test(key)); const voltages = filterSensors(data, (key) => /^in|vcore|vbat/i.test(key)); panel.innerHTML = `

Hardware Sensors

lm_sensors + cockpit-sensors integration

MIT · ocristopfer

Temperatures

${Object.entries(temps).flatMap(([adapter, sensors]) => Object.entries(sensors).map(([key, val]) => ``) ).join('') || ''}
AdapterSensorValueCritical
${escapeHtml(adapter)} ${escapeHtml(key)} ${escapeHtml(sensorVal(val, 'temp'))}°C ${escapeHtml(sensorCrit(val))}
No temperature sensors.

Fan Speeds

${Object.entries(fans).flatMap(([adapter, sensors]) => Object.entries(sensors).map(([key, val]) => ``) ).join('') || ''}
AdapterSensorRPM
${escapeHtml(adapter)} ${escapeHtml(key)} ${escapeHtml(sensorVal(val, 'fan'))} RPM
No fan sensors.

Voltages

${Object.entries(voltages).flatMap(([adapter, sensors]) => Object.entries(sensors).map(([key, val]) => ``) ).join('') || ''}
AdapterSensorVolts
${escapeHtml(adapter)} ${escapeHtml(key)} ${escapeHtml(sensorVal(val, 'in'))} V
No voltage sensors.
`; panel.querySelector('#btn-sensors-refresh')?.addEventListener('click', () => { mount(panel, { bridge, EventBus }); }); } function filterSensors(data, predicate) { const result = {}; Object.entries(data).forEach(([adapter, sensors]) => { const filtered = Object.fromEntries( Object.entries(sensors).filter(([key]) => predicate(key)) ); if (Object.keys(filtered).length > 0) result[adapter] = filtered; }); return result; } function sensorVal(val, prefix) { if (typeof val === 'number') return val.toFixed(1); if (typeof val === 'object' && val !== null) { // lm_sensors JSON: { input: 45.0, crit: 100.0, max: 80.0 } const inputKey = Object.keys(val).find((k) => k.startsWith(prefix) && k.endsWith('_input')) || 'input'; return (val[inputKey] ?? val.input ?? 0).toFixed(1); } return String(val); } function sensorCrit(val) { if (typeof val === 'object' && val !== null) { const critKey = Object.keys(val).find((k) => k.includes('crit')); if (critKey && val[critKey] != null) return val[critKey].toFixed(1) + '°C'; } return '—'; } function renderSkeleton() { return `
`; } function renderError(err) { return `

Sensors unavailable

${escapeHtml(err.message || err)}. Install lm_sensors (pacman -S lm_sensors) and ensure sensors-detect has been run.

cockpit-sensors (MIT) by ocristopfer — https://github.com/ocristopfer/cockpit-sensors

`; }