mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 10:51:36 -04:00
Add a VNC toggle to the Controller (both UIs) that starts/stops whichever VNC backend is installed: wayvnc for Wayland (Raspberry Pi OS), RealVNC for X11. Runtime-only, like the Desktop toggle: the unit is never enabled, so VNC does not return after a reboot. Users who want it permanently on can enable it with raspi-config. VNC serves the desktop session, so starting it without a desktop gives clients a blank screen. The gate lives in the backend, not just the UI, so curl callers get it too (409, distinct from a systemctl failure). Stopping is never gated so a running VNC server can't be stranded on with no way to turn it off. Backend selection deliberately avoids raspi-config's live-compositor probe (pgrep labwc), which reports X11 — and would pick the wrong backend — whenever the desktop is stopped. wayvnc also requires its config to exist; that is checked directly rather than via the unit's ConditionResult, which reports 'no' for any unit that has never been started. Extracts _unit_properties()/_systemctl_unit() so Desktop and VNC share one systemd probe and start/stop path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
170 lines
5.9 KiB
Python
170 lines
5.9 KiB
Python
"""
|
|
Stats API endpoints for WROLPi Controller.
|
|
|
|
Endpoints read from cached status data collected by the background worker.
|
|
Individual endpoints remain for backwards compatibility.
|
|
New aggregated /api/stats endpoint returns all status at once.
|
|
|
|
This endpoint provides system-level stats. App-level status (flags, downloads,
|
|
wrol_mode) remain in the main WROLPi API at /api/status.
|
|
"""
|
|
|
|
from fastapi import APIRouter, Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from controller.lib.admin import get_bluetooth_status, get_desktop_status, get_hotspot_ssid, get_hotspot_status, \
|
|
get_throttle_status, get_vnc_status, BluetoothStatus, HotspotStatus
|
|
from controller.lib.config import is_docker_mode, is_rpi, is_rpi4, is_rpi5
|
|
from controller.lib.status import (
|
|
get_cpu_status,
|
|
get_load_status,
|
|
get_memory_status,
|
|
get_network_status,
|
|
get_power_status,
|
|
get_primary_drive_status,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/stats", tags=["stats"])
|
|
|
|
|
|
def get_cached_status(request: Request) -> dict:
|
|
"""Get cached status from app.state, or empty dict if not yet available."""
|
|
return getattr(request.app.state, "cached_status", {})
|
|
|
|
|
|
async def get_system_info() -> dict:
|
|
"""
|
|
Get static system info that doesn't need frequent updates.
|
|
These are collected on each request as they rarely change.
|
|
"""
|
|
hotspot_status = get_hotspot_status()
|
|
bluetooth_status = await get_bluetooth_status()
|
|
desktop_status = await get_desktop_status()
|
|
vnc_status = await get_vnc_status()
|
|
throttle_status = get_throttle_status()
|
|
|
|
# Get hotspot SSID from config if hotspot is connected
|
|
hotspot_ssid = get_hotspot_ssid() if hotspot_status == HotspotStatus.connected else None
|
|
|
|
return {
|
|
"dockerized": is_docker_mode(),
|
|
"is_rpi": is_rpi(),
|
|
"is_rpi4": is_rpi4(),
|
|
"is_rpi5": is_rpi5(),
|
|
"bluetooth_status": bluetooth_status.name,
|
|
"desktop_status": desktop_status.name,
|
|
"vnc_status": vnc_status.name,
|
|
"hotspot_status": hotspot_status.name,
|
|
"hotspot_ssid": hotspot_ssid,
|
|
"throttle_status": throttle_status.name,
|
|
}
|
|
|
|
|
|
@router.get("")
|
|
@router.get("/")
|
|
async def get_all_stats(request: Request):
|
|
"""
|
|
Get all system status at once (cached data + system info).
|
|
|
|
Returns aggregated CPU, memory, load, drives, network, power, iostat,
|
|
processes, disk_bandwidth stats along with system info (dockerized,
|
|
is_rpi, hotspot, throttle).
|
|
|
|
This endpoint returns O(1) cached data updated every ~5 seconds,
|
|
plus system info collected on each request.
|
|
"""
|
|
cached = get_cached_status(request)
|
|
|
|
if not cached:
|
|
# Fallback to on-demand collection if cache not yet populated
|
|
from controller.lib.status_worker import collect_all_status
|
|
cached = await collect_all_status()
|
|
|
|
# Add system info to response
|
|
response = {**cached, **await get_system_info()}
|
|
return JSONResponse(content=response)
|
|
|
|
|
|
@router.get("/cpu")
|
|
async def get_cpu(request: Request):
|
|
"""Get CPU status (usage, frequency, temperature)."""
|
|
cached = get_cached_status(request)
|
|
if cached and cached.get("cpu_stats"):
|
|
return JSONResponse(content=cached["cpu_stats"])
|
|
return JSONResponse(content=get_cpu_status())
|
|
|
|
|
|
@router.get("/memory")
|
|
async def get_memory(request: Request):
|
|
"""Get memory usage statistics."""
|
|
cached = get_cached_status(request)
|
|
if cached and cached.get("memory_stats"):
|
|
return JSONResponse(content=cached["memory_stats"])
|
|
return JSONResponse(content=get_memory_status())
|
|
|
|
|
|
@router.get("/load")
|
|
async def get_load(request: Request):
|
|
"""Get system load averages."""
|
|
cached = get_cached_status(request)
|
|
if cached and cached.get("load_stats"):
|
|
return JSONResponse(content=cached["load_stats"])
|
|
return JSONResponse(content=get_load_status())
|
|
|
|
|
|
@router.get("/drives/primary")
|
|
async def get_primary_drive(request: Request):
|
|
"""Get status of the primary WROLPi drive."""
|
|
cached = get_cached_status(request)
|
|
if cached and cached.get("drives_stats"):
|
|
# Find primary drive from cached drives
|
|
for drive in cached["drives_stats"]:
|
|
if drive.get("mount") == "/media/wrolpi":
|
|
return JSONResponse(content={"mounted": True, **drive})
|
|
return JSONResponse(content={"mounted": False})
|
|
|
|
# Fallback to direct call
|
|
status = get_primary_drive_status()
|
|
if status is None:
|
|
return JSONResponse(content={"mounted": False})
|
|
return JSONResponse(content={"mounted": True, **status})
|
|
|
|
|
|
@router.get("/network")
|
|
async def get_network(request: Request):
|
|
"""Get network interface statistics."""
|
|
cached = get_cached_status(request)
|
|
if cached and cached.get("nic_bandwidth_stats"):
|
|
return JSONResponse(content=cached["nic_bandwidth_stats"])
|
|
return JSONResponse(content=get_network_status())
|
|
|
|
|
|
@router.get("/power")
|
|
async def get_power(request: Request):
|
|
"""Get power status (undervoltage, throttling)."""
|
|
cached = get_cached_status(request)
|
|
if cached and cached.get("power_stats"):
|
|
return JSONResponse(content=cached["power_stats"])
|
|
return JSONResponse(content=get_power_status())
|
|
|
|
|
|
@router.get("/iostat")
|
|
async def get_iostat(request: Request):
|
|
"""Get IO statistics (iowait, user, system, idle percentages)."""
|
|
cached = get_cached_status(request)
|
|
if cached and cached.get("iostat_stats"):
|
|
return JSONResponse(content=cached["iostat_stats"])
|
|
# 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())
|