From d4f1cfb85983812dd80ab47ff87c304a0dae280d Mon Sep 17 00:00:00 2001 From: Roland Date: Fri, 26 Dec 2025 10:05:13 -0700 Subject: [PATCH] Adding server uptime to status pages. --- app/src/components/admin/Status.js | 31 +++++++++++++++++++++++++++++- controller/api/status.py | 11 +++++++++++ controller/lib/status.py | 19 ++++++++++++++++++ controller/lib/status_worker.py | 5 ++++- controller/main.py | 16 +++++++++++++++ controller/templates/index.html | 28 ++++++++++++++++++++++++++- 6 files changed, 107 insertions(+), 3 deletions(-) diff --git a/app/src/components/admin/Status.js b/app/src/components/admin/Status.js index 2807fb1e..edbffb83 100644 --- a/app/src/components/admin/Status.js +++ b/app/src/components/admin/Status.js @@ -96,6 +96,27 @@ function IOWaitStatistic({percent_iowait, ...props}) { return } +function formatUptime(seconds) { + if (!seconds || seconds < 0) return '?'; + + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + if (days > 0) { + return `${days}d ${hours}h`; + } else if (hours > 0) { + return `${hours}h ${minutes}m`; + } else { + return `${minutes}m`; + } +} + +function UptimeStatistic({uptime_seconds, ...props}) { + const value = formatUptime(uptime_seconds); + return +} + export function BandwidthProgress({label = '', bytes, maxBytes, ...props}) { // Gigabit by default. maxBytes = maxBytes || 125_000_000; @@ -216,9 +237,10 @@ export function StatusPage() { let memoryPercent; let memorySize; let percent_iowait; + let uptime_seconds; if (status && status['cpu_stats']) { - const {cpu_stats, load_stats, memory_stats, processes_stats, iostat_stats} = status; + const {cpu_stats, load_stats, memory_stats, processes_stats, iostat_stats, uptime_stats} = status; percent = cpu_stats['percent']; cores = cpu_stats['cores'] || '?'; temperature = cpu_stats['temperature']; @@ -241,6 +263,11 @@ export function StatusPage() { if (iostat_stats) { percent_iowait = iostat_stats['percent_iowait']; } + + // Extract uptime from uptime stats + if (uptime_stats) { + uptime_seconds = uptime_stats['uptime_seconds']; + } } const SizedHeader = ({children, sizeMobile = 'h1', sizeTablet = 'h2'}) => { @@ -277,6 +304,7 @@ export function StatusPage() { + @@ -296,6 +324,7 @@ export function StatusPage() { + diff --git a/controller/api/status.py b/controller/api/status.py index c7bd68eb..8a7fdde1 100644 --- a/controller/api/status.py +++ b/controller/api/status.py @@ -154,3 +154,14 @@ async def get_iostat(request: Request): # Fallback to direct call from controller.lib.status import get_iostat_status return JSONResponse(content=get_iostat_status()) + + +@router.get("/uptime") +async def get_uptime(request: Request): + """Get system uptime information.""" + cached = get_cached_status(request) + if cached and cached.get("uptime_stats"): + return JSONResponse(content=cached["uptime_stats"]) + # Fallback to direct call + from controller.lib.status import get_uptime_status + return JSONResponse(content=get_uptime_status()) diff --git a/controller/lib/status.py b/controller/lib/status.py index cb1f790c..c30bf400 100644 --- a/controller/lib/status.py +++ b/controller/lib/status.py @@ -341,6 +341,25 @@ def get_processes_status() -> list[dict]: return processes +def get_uptime_status() -> dict: + """ + Get system uptime information. + + Returns format: + dict with keys: boot_time (ISO timestamp), uptime_seconds (int) + """ + from datetime import datetime + + boot_time = psutil.boot_time() + boot_datetime = datetime.fromtimestamp(boot_time) + uptime_seconds = int((datetime.now() - boot_datetime).total_seconds()) + + return { + "boot_time": boot_datetime.isoformat(), + "uptime_seconds": uptime_seconds, + } + + IGNORED_DISK_NAMES = ('loop', 'ram') diff --git a/controller/lib/status_worker.py b/controller/lib/status_worker.py index fadf3419..424359a5 100644 --- a/controller/lib/status_worker.py +++ b/controller/lib/status_worker.py @@ -20,6 +20,7 @@ from controller.lib.status import ( get_network_status, get_power_status, get_processes_status, + get_uptime_status, ) # Type alias for cached status dict @@ -35,7 +36,7 @@ async def collect_all_status() -> CachedStatus: """ # Run all status collections concurrently using asyncio.to_thread # since the status functions are synchronous (use psutil) - cpu, memory, load, drives, network, power, iostat, processes, disk_bandwidth = await asyncio.gather( + cpu, memory, load, drives, network, power, iostat, processes, disk_bandwidth, uptime = await asyncio.gather( asyncio.to_thread(get_cpu_status), asyncio.to_thread(get_memory_status), asyncio.to_thread(get_load_status), @@ -45,6 +46,7 @@ async def collect_all_status() -> CachedStatus: asyncio.to_thread(get_iostat_status), asyncio.to_thread(get_processes_status), asyncio.to_thread(get_disk_bandwidth_status), + asyncio.to_thread(get_uptime_status), return_exceptions=True, ) @@ -59,6 +61,7 @@ async def collect_all_status() -> CachedStatus: "iostat_stats": iostat if not isinstance(iostat, Exception) else None, "processes_stats": processes if not isinstance(processes, Exception) else [], "disk_bandwidth_stats": disk_bandwidth if not isinstance(disk_bandwidth, Exception) else {}, + "uptime_stats": uptime if not isinstance(uptime, Exception) else None, "last_status": datetime.now().isoformat(), } diff --git a/controller/main.py b/controller/main.py index a415f30e..bd7d33d4 100644 --- a/controller/main.py +++ b/controller/main.py @@ -38,6 +38,7 @@ from controller.lib.status import ( get_load_status, get_memory_status, get_primary_drive_status, + get_uptime_status, ) from controller.lib.status_worker import status_worker_loop from controller.lib.systemd import get_all_services_status @@ -118,6 +119,7 @@ async def dashboard(request: Request): memory = cached.get("memory_stats") or {} load = cached.get("load_stats") or {} iostat = cached.get("iostat_stats") or {} + uptime = cached.get("uptime_stats") or {} drives = cached.get("drives_stats") or [] primary_drive = None for drive in drives: @@ -130,6 +132,7 @@ async def dashboard(request: Request): memory = get_memory_status() load = get_load_status() iostat = get_iostat_status() + uptime = get_uptime_status() primary_drive = get_primary_drive_status() # Format storage data @@ -141,6 +144,15 @@ async def dashboard(request: Request): "free_gb": round(free_bytes / (1024 ** 3), 1), } + # Format uptime + uptime_seconds = uptime.get("uptime_seconds", 0) + if uptime_seconds: + days = uptime_seconds // 86400 + hours = (uptime_seconds % 86400) // 3600 + uptime_formatted = f"{days}d {hours}h" if days > 0 else f"{hours}h {(uptime_seconds % 3600) // 60}m" + else: + uptime_formatted = "--" + # Get service status if is_docker_mode(): if can_manage_containers(): @@ -175,6 +187,10 @@ async def dashboard(request: Request): "iostat": { "percent_iowait": iostat.get("percent_iowait"), }, + "uptime": { + "formatted": uptime_formatted, + "seconds": uptime_seconds, + }, # Real service status "services": services, diff --git a/controller/templates/index.html b/controller/templates/index.html index 4553deb3..a29f58d2 100644 --- a/controller/templates/index.html +++ b/controller/templates/index.html @@ -633,6 +633,11 @@

{% if iostat and iostat.percent_iowait is not none %}{{ iostat.percent_iowait }}{% else %}--{% endif %}%

disk waiting

+
+

Uptime

+

{{ uptime.formatted|default('--') }}

+

since boot

+
@@ -1472,15 +1477,30 @@ // ===== PERIODIC UPDATES ===== + function formatUptime(seconds) { + if (!seconds || seconds < 0) return '--'; + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + if (days > 0) { + return `${days}d ${hours}h`; + } else if (hours > 0) { + return `${hours}h ${minutes}m`; + } else { + return `${minutes}m`; + } + } + async function updateSystemStatus() { try { // Fetch all stats endpoints in parallel - const [cpu, memory, load, primaryDrive, iostat] = await Promise.all([ + const [cpu, memory, load, primaryDrive, iostat, uptime] = await Promise.all([ apiCall('api/stats/cpu'), apiCall('api/stats/memory'), apiCall('api/stats/load'), apiCall('api/stats/drives/primary'), apiCall('api/stats/iostat'), + apiCall('api/stats/uptime'), ]); // Update CPU card @@ -1538,6 +1558,12 @@ iowaitValue.style.color = '#4ecca3'; } } + + // Update Uptime card + const uptimeValue = document.getElementById('uptime-value'); + if (uptimeValue) { + uptimeValue.textContent = formatUptime(uptime.uptime_seconds); + } } catch (e) { console.error('Failed to update system status:', e); }