// Server Observations display - shows fastfetch output import { safeJson } from './utils/fetch.mjs'; import STATUS from './status.mjs'; import WeatherDisplay from './weatherdisplay.mjs'; import { registerDisplay } from './navigation.mjs'; import { debugFlag } from './utils/debug.mjs'; import { withBasePath } from './utils/base-path.mjs'; const LINES_PER_PAGE = 4; const PAGE_DURATION_MS = 7000; 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 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 trimmed = line.trim(); if (!trimmed) return null; const separators = [' == ', ': ']; const parsedLine = separators.map((separator) => { const separatorIndex = trimmed.indexOf(separator); if (separatorIndex > 0) { const key = trimmed.slice(0, separatorIndex).trim(); const value = trimmed.slice(separatorIndex + separator.length).trim(); if (key && value && isAllowedObservationKey(key)) { return { key, value: formatValue(key, value) }; } } return null; }).find((entry) => entry); return parsedLine ?? null; }; class ServerObservations extends WeatherDisplay { constructor(navId, elemId) { super(navId, elemId, 'Server Observations', true); this.timing.baseDelay = PAGE_DURATION_MS; } async getData(weatherParameters, refresh) { if (!super.getData(weatherParameters, refresh)) return; try { // Fetch server info from the API const response = await safeJson(withBasePath('api/server-info'), { retryCount: 0, }); // Check if fastfetch is available if (!response || !response.success) { if (debugFlag('serverobservations')) { console.log('Server Observations: fastfetch not available'); } this.setStatus(STATUS.noData); return; } this.data = response.data; this.setStatus(STATUS.loaded); } catch (error) { if (debugFlag('serverobservations')) { console.log('Server Observations: error fetching data:', error.message); } this.setStatus(STATUS.noData); } } async drawCanvas() { super.drawCanvas(); // Get the output container const outputElem = this.elem.querySelector('.server-output'); const container = this.elem.querySelector('.container'); // Split the fastfetch output into lines const infoLines = this.data.split('\n') .map((line) => parseServerObservationLine(line)) .filter((line) => line); const pages = []; for (let i = 0; i < infoLines.length; i += LINES_PER_PAGE) { pages.push(infoLines.slice(i, i + LINES_PER_PAGE)); } outputElem.innerHTML = ''; pages.forEach((pageLines) => { const pageElem = document.createElement('div'); pageElem.className = 'server-page'; pageLines.forEach((line) => { const lineDiv = document.createElement('div'); lineDiv.className = 'server-line'; lineDiv.textContent = truncateLine(`${line.key}: ${line.value}`); pageElem.appendChild(lineDiv); }); outputElem.appendChild(pageElem); }); this.pageHeight = container.offsetHeight; this.timing.totalScreens = Math.max(1, pages.length); this.timing.delay = new Array(this.timing.totalScreens).fill(1); this.calcNavTiming(); const top = -this.screenIndex * this.pageHeight; outputElem.style.top = `${top}px`; this.finishDraw(); } } // Register display with navId 13 (after other displays) registerDisplay(new ServerObservations(13, 'server-observations'));