Skip to content

Commit e9aac32

Browse files
committed
add glances support
1 parent 39468b2 commit e9aac32

11 files changed

Lines changed: 486 additions & 1 deletion

File tree

js/apps/appIndex.js

100644100755
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ import './imageApp.js';
44
import './weatherApp.js';
55
import './calendarApp.js';
66
import './clockApp.js';
7-
import './helloWorld.js';
7+
import './helloWorld.js';
8+
import './glancesApp.js';

js/apps/calendarApp.js

100644100755
File mode changed.

js/apps/clockApp.js

100644100755
File mode changed.

js/apps/glances/gCore.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { resolveToHex } from "../../utils.js";
2+
3+
// Shared Config
4+
export const HISTORY_SIZE = 40;
5+
6+
// Helper: Format Bytes
7+
export function formatBytes(bytes) {
8+
if (!bytes || bytes === 0) return '0 B';
9+
const k = 1024;
10+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
11+
const i = Math.floor(Math.log(bytes) / Math.log(k));
12+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
13+
}
14+
15+
// Helper: Generic API Fetcher
16+
export async function fetchGlances(url, apiVer, endpoint) {
17+
const target = `${url}/api/${apiVer}/${endpoint}`;
18+
const res = await fetch(target);
19+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
20+
return await res.json();
21+
}
22+
23+
// Helper: Shared Canvas Graph Drawer
24+
export function drawGraph(canvas, ctx, dataPoints, colorVar, maxValOverride = null) {
25+
if (!ctx) return;
26+
const w = canvas.width;
27+
const h = canvas.height;
28+
ctx.clearRect(0, 0, w, h);
29+
30+
// Resolve Theme Color
31+
const brandColor = getComputedStyle(document.documentElement).getPropertyValue(colorVar).trim();
32+
const hexColor = resolveToHex(brandColor) || '#ffffff';
33+
34+
// Scaling Logic
35+
let maxVal = 100; // Default ceiling (Percentage)
36+
if (maxValOverride !== null) {
37+
maxVal = maxValOverride;
38+
}
39+
40+
// Draw Line
41+
ctx.beginPath();
42+
ctx.strokeStyle = hexColor;
43+
ctx.lineWidth = 2;
44+
const stepX = w / (HISTORY_SIZE - 1);
45+
46+
dataPoints.forEach((val, i) => {
47+
const x = i * stepX;
48+
// Map value to canvas height (Inverted Y)
49+
const y = h - ((val / maxVal) * h);
50+
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
51+
});
52+
ctx.stroke();
53+
54+
// Draw Gradient Fill
55+
ctx.lineTo(w, h); ctx.lineTo(0, h); ctx.closePath();
56+
const grad = ctx.createLinearGradient(0, 0, 0, h);
57+
grad.addColorStop(0, hexColor + '40');
58+
grad.addColorStop(1, hexColor + '00');
59+
ctx.fillStyle = grad;
60+
ctx.fill();
61+
}

js/apps/glances/gCpu.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { fetchGlances, drawGraph, HISTORY_SIZE } from "./gCore.js";
2+
3+
export function initCpu(el, config) {
4+
const { url, apiVer, dataPoints } = config;
5+
const bodyEl = el.querySelector('.glances-body');
6+
7+
// 1. Setup DOM
8+
bodyEl.innerHTML = `
9+
<div class="canvas-wrapper"><canvas class="glances-graph"></canvas></div>
10+
<div class="graph-meta">Cores: <span id="core-count">--</span></div>`;
11+
12+
const canvas = el.querySelector('canvas');
13+
const ctx = canvas.getContext('2d');
14+
const titleEl = el.querySelector('.metric-title');
15+
const valEl = el.querySelector('.metric-value');
16+
17+
// 2. Setup Resize Observer
18+
const wrapper = el.querySelector('.canvas-wrapper');
19+
if (wrapper) {
20+
new ResizeObserver(() => {
21+
canvas.width = wrapper.clientWidth;
22+
canvas.height = wrapper.clientHeight;
23+
drawGraph(canvas, ctx, dataPoints, '--red');
24+
}).observe(wrapper);
25+
}
26+
27+
// 3. Return Update Function
28+
return async () => {
29+
const [cpu, core] = await Promise.all([
30+
fetchGlances(url, apiVer, 'cpu'),
31+
fetchGlances(url, apiVer, 'core').catch(() => null)
32+
]);
33+
34+
titleEl.innerText = "CPU LOAD";
35+
valEl.innerText = cpu.total.toFixed(1) + '%';
36+
el.querySelector('#core-count').innerText = core || '?';
37+
38+
dataPoints.push(cpu.total);
39+
if (dataPoints.length > HISTORY_SIZE) dataPoints.shift();
40+
drawGraph(canvas, ctx, dataPoints, '--red');
41+
};
42+
}

js/apps/glances/gMem.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { fetchGlances, drawGraph, HISTORY_SIZE, formatBytes } from "./gCore.js";
2+
3+
export function initMem(el, config) {
4+
const { url, apiVer, dataPoints } = config;
5+
const bodyEl = el.querySelector('.glances-body');
6+
7+
// 1. Setup DOM
8+
bodyEl.innerHTML = `
9+
<div class="canvas-wrapper"><canvas class="glances-graph"></canvas></div>
10+
<div class="graph-meta"><span id="mem-meta">-- / --</span></div>`;
11+
12+
const canvas = el.querySelector('canvas');
13+
const ctx = canvas.getContext('2d');
14+
const titleEl = el.querySelector('.metric-title');
15+
const valEl = el.querySelector('.metric-value');
16+
17+
// 2. Setup Resize Observer
18+
const wrapper = el.querySelector('.canvas-wrapper');
19+
if (wrapper) {
20+
new ResizeObserver(() => {
21+
canvas.width = wrapper.clientWidth;
22+
canvas.height = wrapper.clientHeight;
23+
drawGraph(canvas, ctx, dataPoints, '--green');
24+
}).observe(wrapper);
25+
}
26+
27+
// 3. Return Update Function
28+
return async () => {
29+
const mem = await fetchGlances(url, apiVer, 'mem');
30+
31+
titleEl.innerText = "RAM USAGE";
32+
valEl.innerText = mem.percent.toFixed(1) + '%';
33+
34+
const usedStr = formatBytes(mem.used);
35+
const totalStr = formatBytes(mem.total);
36+
el.querySelector('#mem-meta').innerText = `${usedStr} / ${totalStr}`;
37+
38+
dataPoints.push(mem.percent);
39+
if (dataPoints.length > HISTORY_SIZE) dataPoints.shift();
40+
drawGraph(canvas, ctx, dataPoints, '--green');
41+
};
42+
}

js/apps/glances/gNetwork.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { fetchGlances, drawGraph, HISTORY_SIZE, formatBytes } from "./gCore.js";
2+
3+
export function initNetwork(el, config) {
4+
const { url, apiVer, dataPoints } = config;
5+
const bodyEl = el.querySelector('.glances-body');
6+
let lastNet = null;
7+
8+
// 1. Setup DOM
9+
bodyEl.innerHTML = `
10+
<div class="canvas-wrapper">
11+
<canvas class="glances-graph"></canvas>
12+
<div class="net-overlay">
13+
<div class="net-row"><i class="fa-solid fa-arrow-down"></i> <span id="net-rx">--</span></div>
14+
<div class="net-row"><i class="fa-solid fa-arrow-up"></i> <span id="net-tx">--</span></div>
15+
</div>
16+
</div>
17+
<div class="graph-meta"><span id="net-meta">RX: -- | TX: --</span></div>`;
18+
19+
const canvas = el.querySelector('canvas');
20+
const ctx = canvas.getContext('2d');
21+
const titleEl = el.querySelector('.metric-title');
22+
const valEl = el.querySelector('.metric-value');
23+
24+
// Redraw Helper (Calculates dynamic scale for network)
25+
const redraw = () => {
26+
const peak = Math.max(...dataPoints);
27+
// Add 20% headroom, default 1KB minimum
28+
const maxVal = peak > 0 ? peak * 1.2 : 1024;
29+
drawGraph(canvas, ctx, dataPoints, '--yellow', maxVal);
30+
};
31+
32+
// 2. Setup Resize Observer
33+
const wrapper = el.querySelector('.canvas-wrapper');
34+
if (wrapper) {
35+
new ResizeObserver(() => {
36+
canvas.width = wrapper.clientWidth;
37+
canvas.height = wrapper.clientHeight;
38+
redraw();
39+
}).observe(wrapper);
40+
}
41+
42+
// 3. Return Update Function
43+
return async () => {
44+
const rawData = await fetchGlances(url, apiVer, 'network');
45+
const interfaces = Array.isArray(rawData) ? rawData : Object.values(rawData);
46+
47+
let totalRx = 0;
48+
let totalTx = 0;
49+
50+
interfaces.forEach(iface => {
51+
const rx = iface.rx !== undefined ? iface.rx : (iface.bytes_recv || 0);
52+
const tx = iface.tx !== undefined ? iface.tx : (iface.bytes_sent || 0);
53+
totalRx += rx;
54+
totalTx += tx;
55+
});
56+
57+
const now = Date.now();
58+
59+
if (lastNet) {
60+
const timeDiff = (now - lastNet.time) / 1000;
61+
if (timeDiff > 0) {
62+
const rxDiff = totalRx - lastNet.rx;
63+
const txDiff = totalTx - lastNet.tx;
64+
65+
const rxSpeed = Math.max(0, rxDiff / timeDiff);
66+
const txSpeed = Math.max(0, txDiff / timeDiff);
67+
const totalSpeed = rxSpeed + txSpeed;
68+
69+
titleEl.innerText = "NETWORK";
70+
valEl.innerText = formatBytes(totalSpeed) + '/s';
71+
72+
// Update Overlay & Footer
73+
const rxEl = el.querySelector('#net-rx');
74+
const txEl = el.querySelector('#net-tx');
75+
if (rxEl) rxEl.innerText = formatBytes(rxSpeed) + '/s';
76+
if (txEl) txEl.innerText = formatBytes(txSpeed) + '/s';
77+
el.querySelector('#net-meta').innerText = `⬇ ${formatBytes(rxSpeed)} | ⬆ ${formatBytes(txSpeed)}`;
78+
79+
// Update Graph
80+
dataPoints.push(totalSpeed);
81+
if (dataPoints.length > HISTORY_SIZE) dataPoints.shift();
82+
redraw();
83+
}
84+
} else {
85+
titleEl.innerText = "NETWORK";
86+
valEl.innerText = "Calc...";
87+
}
88+
89+
lastNet = { rx: totalRx, tx: totalTx, time: now };
90+
};
91+
}

js/apps/glances/gSensors.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { fetchGlances } from "./gCore.js";
2+
3+
export function initSensors(el, config) {
4+
const { url, apiVer } = config;
5+
const bodyEl = el.querySelector('.glances-body');
6+
7+
// 1. Setup DOM
8+
bodyEl.innerHTML = `<div class="sensor-list" id="sensor-list">Scanning...</div>`;
9+
const titleEl = el.querySelector('.metric-title');
10+
const valEl = el.querySelector('.metric-value');
11+
12+
// 2. Return Update Function
13+
return async () => {
14+
const rawSensors = await fetchGlances(url, apiVer, 'sensors');
15+
let sensors = Array.isArray(rawSensors) ? rawSensors : Object.values(rawSensors || {});
16+
17+
// Filter weird units if needed
18+
if (sensors.length > 0 && sensors[0].unit !== 'C' && sensors[0].unit !== 'F') {
19+
sensors = sensors.filter(s => s.unit === 'C' || s.unit === 'F');
20+
}
21+
22+
titleEl.innerText = "TEMPS";
23+
valEl.innerText = sensors.length > 0 ? sensors.length + " Active" : "--";
24+
25+
const list = el.querySelector('#sensor-list');
26+
list.innerHTML = '';
27+
28+
if (sensors.length > 0) {
29+
sensors.forEach(s => {
30+
let label = s.label || s.adapter || 'Unknown';
31+
if (label.startsWith('Package id')) label = 'Package';
32+
else if (label.startsWith('Core')) label = label.replace('Core ', 'Core');
33+
else if (label === 'Composite') label = 'CPU';
34+
else if (label.startsWith('acpitz')) label = 'Mobo ' + (label.split(' ')[1] || '');
35+
else if (label.startsWith('nvme')) label = 'SSD';
36+
37+
const max = s.critical || 100;
38+
const warn = s.warning || 80;
39+
let percent = (s.value / max) * 100;
40+
if (percent > 100) percent = 100;
41+
42+
let colorClass = 'default';
43+
if (s.value >= max) colorClass = 'critical';
44+
else if (s.value >= warn) colorClass = 'warning';
45+
46+
const row = document.createElement('div');
47+
row.className = 'sensor-row';
48+
row.innerHTML = `
49+
<div class="s-info">
50+
<span class="s-name">${label}</span>
51+
<span class="s-val ${colorClass}">${s.value.toFixed(0)}°</span>
52+
</div>
53+
<div class="s-bar-bg">
54+
<div class="s-bar ${colorClass}" style="width: ${percent}%"></div>
55+
</div>
56+
`;
57+
list.appendChild(row);
58+
});
59+
} else {
60+
list.innerHTML = '<div>No sensors found</div>';
61+
}
62+
};
63+
}

0 commit comments

Comments
 (0)