Adding server uptime to status pages.

This commit is contained in:
Roland 2025-12-26 10:05:13 -07:00
parent b408d11ea1
commit d4f1cfb859
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
6 changed files with 107 additions and 3 deletions

View file

@ -96,6 +96,27 @@ function IOWaitStatistic({percent_iowait, ...props}) {
return <Statistic label='IO Wait %' value={percent_iowait.toFixed(1)} {...props}/>
}
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 <Statistic label='Uptime' value={value} {...props}/>
}
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() {
<LoadStatistic label='5 Minute Load' value={minute_5} cores={cores}/>
<LoadStatistic label='15 Minute Load' value={minute_15} cores={cores}/>
<IOWaitStatistic percent_iowait={percent_iowait}/>
<UptimeStatistic uptime_seconds={uptime_seconds}/>
</Statistic.Group>
</Segment>
</Media>
@ -296,6 +324,7 @@ export function StatusPage() {
<LoadStatistic label='5 Min.' value={minute_5} cores={cores}/>
<LoadStatistic label='15 Min.' value={minute_15} cores={cores}/>
<IOWaitStatistic percent_iowait={percent_iowait}/>
<UptimeStatistic uptime_seconds={uptime_seconds}/>
</Statistic.Group>
</Segment>
</Media>

View file

@ -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())

View file

@ -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')

View file

@ -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(),
}

View file

@ -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,

View file

@ -633,6 +633,11 @@
<p class="value" id="iowait-value">{% if iostat and iostat.percent_iowait is not none %}{{ iostat.percent_iowait }}{% else %}--{% endif %}%</p>
<p class="sub">disk waiting</p>
</div>
<div class="status-card">
<h3>Uptime</h3>
<p class="value" id="uptime-value">{{ uptime.formatted|default('--') }}</p>
<p class="sub">since boot</p>
</div>
</div>
</section>
@ -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);
}