Stabilize fastfetch/server observations behavior
Some checks are pending
build-docker / Build Image (push) Waiting to run
Some checks are pending
build-docker / Build Image (push) Waiting to run
This commit is contained in:
parent
3fcf2e43cd
commit
c82d7648ab
2 changed files with 69 additions and 5 deletions
12
index.mjs
12
index.mjs
|
|
@ -221,16 +221,22 @@ if (!process.env?.STATIC) {
|
||||||
app.get('/api/server-info', async (req, res) => {
|
app.get('/api/server-info', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Use --structure to show only essential info: OS, Kernel, Uptime, CPU, GPU, Memory, Disk
|
// Use --structure to show only essential info: OS, Kernel, Uptime, CPU, GPU, Memory, Disk
|
||||||
const { stdout } = await execAsync('fastfetch --structure "Title:OS:Kernel:Uptime:CPU:GPU:Memory:Disk" --pipe false');
|
// --logo none prevents image/ascii logo rendering issues on headless systems
|
||||||
// Strip all ANSI escape sequences (color codes, cursor positioning, etc.)
|
// --pipe disables colors for clean text output
|
||||||
|
const { stdout } = await execAsync(
|
||||||
|
'fastfetch --structure "Title:OS:Kernel:Uptime:CPU:GPU:Memory:Disk" --logo none --pipe',
|
||||||
|
{ timeout: 10000 },
|
||||||
|
);
|
||||||
|
// Strip all ANSI escape sequences (color codes, cursor positioning, OSC sequences, etc.)
|
||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
const cleanOutput = stdout.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
const cleanOutput = stdout.replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g, '');
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: cleanOutput,
|
data: cleanOutput,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// fastfetch not available or other error
|
// fastfetch not available or other error
|
||||||
|
console.error('fastfetch failed:', error.message);
|
||||||
res.json({
|
res.json({
|
||||||
success: false,
|
success: false,
|
||||||
data: null,
|
data: null,
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,67 @@ import { withBasePath } from './utils/base-path.mjs';
|
||||||
const LINES_PER_PAGE = 4;
|
const LINES_PER_PAGE = 4;
|
||||||
const PAGE_DURATION_MS = 7000;
|
const PAGE_DURATION_MS = 7000;
|
||||||
const ALLOWED_KEYS = new Set(['OS', 'Kernel', 'Uptime', 'CPU', 'GPU', 'Memory', 'Disk']);
|
const ALLOWED_KEYS = new Set(['OS', 'Kernel', 'Uptime', 'CPU', 'GPU', 'Memory', 'Disk']);
|
||||||
|
const MAX_LINE_LENGTH = 34;
|
||||||
|
|
||||||
const isAllowedObservationKey = (key) => ALLOWED_KEYS.has(key) || key.startsWith('Disk');
|
const isAllowedObservationKey = (key) => ALLOWED_KEYS.has(key) || key.startsWith('Disk');
|
||||||
|
|
||||||
|
const truncateLine = (text, maxLength = MAX_LINE_LENGTH) => {
|
||||||
|
if (text.length <= maxLength) return text;
|
||||||
|
return `${text.slice(0, maxLength - 3)}...`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const roundOneDecimal = (value) => {
|
||||||
|
const rounded = Number(value).toFixed(1);
|
||||||
|
return rounded.endsWith('.0') ? rounded.slice(0, -2) : rounded;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatMemoryOrDisk = (value) => {
|
||||||
|
const cleaned = value.replace(/\s*\(\d+%\)/, '').replace(/\s*- .*/, '').trim();
|
||||||
|
const unitMatch = cleaned.match(/([\d.]+)\s*(TiB|GiB|MiB|KiB|TB|GB|MB|KB|B)\s*\/\s*([\d.]+)\s*(TiB|GiB|MiB|KiB|TB|GB|MB|KB|B)/);
|
||||||
|
if (!unitMatch) return cleaned;
|
||||||
|
return `${roundOneDecimal(unitMatch[1])} / ${roundOneDecimal(unitMatch[3])} ${unitMatch[2]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatValue = (key, value) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'OS':
|
||||||
|
return value
|
||||||
|
.replace(/\s*\([^)]*\)\s*(x86_64|aarch64|arm64|i386|i686|amd64)/gi, '')
|
||||||
|
.replace(/\s*(x86_64|aarch64|arm64|i386|i686|amd64)/gi, '')
|
||||||
|
.trim();
|
||||||
|
case 'Kernel':
|
||||||
|
return value.replace(/(-[a-zA-Z0-9._-]+)+$/, '').trim();
|
||||||
|
case 'Uptime':
|
||||||
|
return value
|
||||||
|
.replace(/ days?/g, 'd')
|
||||||
|
.replace(/ hours?/g, 'h')
|
||||||
|
.replace(/ mins?/g, 'm')
|
||||||
|
.replace(/ secs?/g, 's');
|
||||||
|
case 'CPU': {
|
||||||
|
const match = value.match(/(Intel Core i\d+(?:-\d+\w*)?|Intel Xeon|Intel Pentium|Intel Celeron|AMD Ryzen \d+(?: \d+\w*)?|AMD FX|AMD Athlon|Apple M\d+|ARM Cortex|Qualcomm Snapdragon)/i);
|
||||||
|
if (match) return match[1];
|
||||||
|
return value
|
||||||
|
.replace(/\(R\)/g, '')
|
||||||
|
.replace(/\(TM\)/g, '')
|
||||||
|
.replace(/\s*\(\d+\)\s*@\s*[\d.]+ GHz.*/, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
case 'GPU':
|
||||||
|
return value
|
||||||
|
.replace(/\(R\)/g, '')
|
||||||
|
.replace(/\(TM\)/g, '')
|
||||||
|
.replace(/ Integrated Graphics Controller.*/i, '')
|
||||||
|
.replace(/ @ [\d.]+ GHz.*/, '')
|
||||||
|
.replace(/ \[.*\]$/, '')
|
||||||
|
.trim();
|
||||||
|
case 'Memory':
|
||||||
|
return formatMemoryOrDisk(value);
|
||||||
|
default:
|
||||||
|
if (key.startsWith('Disk')) return formatMemoryOrDisk(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const parseServerObservationLine = (line) => {
|
const parseServerObservationLine = (line) => {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) return null;
|
||||||
|
|
@ -23,7 +81,7 @@ const parseServerObservationLine = (line) => {
|
||||||
const key = trimmed.slice(0, separatorIndex).trim();
|
const key = trimmed.slice(0, separatorIndex).trim();
|
||||||
const value = trimmed.slice(separatorIndex + separator.length).trim();
|
const value = trimmed.slice(separatorIndex + separator.length).trim();
|
||||||
if (key && value && isAllowedObservationKey(key)) {
|
if (key && value && isAllowedObservationKey(key)) {
|
||||||
return { key, value };
|
return { key, value: formatValue(key, value) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -91,7 +149,7 @@ class ServerObservations extends WeatherDisplay {
|
||||||
pageLines.forEach((line) => {
|
pageLines.forEach((line) => {
|
||||||
const lineDiv = document.createElement('div');
|
const lineDiv = document.createElement('div');
|
||||||
lineDiv.className = 'server-line';
|
lineDiv.className = 'server-line';
|
||||||
lineDiv.textContent = `${line.key}: ${line.value}`;
|
lineDiv.textContent = truncateLine(`${line.key}: ${line.value}`);
|
||||||
pageElem.appendChild(lineDiv);
|
pageElem.appendChild(lineDiv);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue