Creating Controller service which will control WROLPi services, admin, SMART, disks, and status. nginx will fallback to Controller UI when React UI is unavailable (such as during upgrades).

This commit is contained in:
Roland 2025-12-11 18:32:55 -07:00 • committed by lrnselfreliance
parent 344fe9035f
commit b9158b60fe
66 changed files with 9920 additions and 327 deletions

View file

@ -0,0 +1 @@
"""Controller API routes."""

135
controller/api/admin.py Normal file
View file

@ -0,0 +1,135 @@
"""
Admin API endpoints for WROLPi Controller.
"""
from fastapi import APIRouter, HTTPException
from controller.api.schemas import (
HotspotActionResponse,
HotspotStatusResponse,
RestartServicesResponse,
ServiceRestartResult,
SystemActionResponse,
ThrottleActionResponse,
ThrottleStatusResponse,
)
from controller.lib.admin import (
disable_hotspot,
disable_throttle,
enable_hotspot,
enable_throttle,
get_hotspot_status_dict,
get_throttle_status_dict,
reboot_system,
restart_all_services,
shutdown_system,
)
from controller.lib.config import is_docker_mode
router = APIRouter(tags=["admin"])
# --- Hotspot ---
@router.get("/api/hotspot/status", response_model=HotspotStatusResponse)
async def hotspot_status() -> HotspotStatusResponse:
"""Get WiFi hotspot status."""
return HotspotStatusResponse(**get_hotspot_status_dict())
@router.post("/api/hotspot/enable", response_model=HotspotActionResponse)
async def hotspot_enable() -> HotspotActionResponse:
"""Enable WiFi hotspot."""
result = enable_hotspot()
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed"))
return HotspotActionResponse(**result)
@router.post("/api/hotspot/disable", response_model=HotspotActionResponse)
async def hotspot_disable() -> HotspotActionResponse:
"""Disable WiFi hotspot."""
result = disable_hotspot()
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed"))
return HotspotActionResponse(**result)
# --- Throttle ---
@router.get("/api/throttle/status", response_model=ThrottleStatusResponse)
async def throttle_status() -> ThrottleStatusResponse:
"""Get CPU throttle status."""
return ThrottleStatusResponse(**get_throttle_status_dict())
@router.post("/api/throttle/enable", response_model=ThrottleActionResponse)
async def throttle_enable() -> ThrottleActionResponse:
"""Enable CPU throttle (powersave mode)."""
result = enable_throttle()
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed"))
return ThrottleActionResponse(**result)
@router.post("/api/throttle/disable", response_model=ThrottleActionResponse)
async def throttle_disable() -> ThrottleActionResponse:
"""Disable CPU throttle (normal performance)."""
result = disable_throttle()
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed"))
return ThrottleActionResponse(**result)
# --- System Control ---
@router.post("/api/shutdown", response_model=SystemActionResponse)
async def system_shutdown() -> SystemActionResponse:
"""Shut down the system."""
if is_docker_mode():
raise HTTPException(
status_code=501,
detail="Cannot shutdown from Docker container"
)
result = shutdown_system()
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed"))
return SystemActionResponse(**result)
@router.post("/api/reboot", response_model=SystemActionResponse)
async def system_reboot() -> SystemActionResponse:
"""Reboot the system."""
if is_docker_mode():
raise HTTPException(
status_code=501,
detail="Cannot reboot from Docker container"
)
result = reboot_system()
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed"))
return SystemActionResponse(**result)
@router.post("/api/restart", response_model=RestartServicesResponse)
async def services_restart() -> RestartServicesResponse:
"""Restart all WROLPi services including Controller."""
if is_docker_mode():
raise HTTPException(
status_code=501,
detail="Service restart via systemctl not available in Docker mode"
)
result = await restart_all_services()
# Convert nested dicts to Pydantic models
services = {
name: ServiceRestartResult(**status)
for name, status in result.get("services", {}).items()
}
return RestartServicesResponse(
success=result.get("success", False),
services=services,
error=result.get("error"),
)

188
controller/api/disks.py Normal file
View file

@ -0,0 +1,188 @@
"""
Disk management API endpoints for WROLPi Controller.
"""
from typing import Optional
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from controller.lib.config import is_docker_mode
from controller.lib.disks import (
get_block_devices,
get_mounts,
mount_drive,
unmount_drive,
)
from controller.lib.fstab import (
add_fstab_entry,
get_wrolpi_fstab_entries,
remove_fstab_entry,
)
from controller.lib.smart import (
get_all_smart_status,
is_smart_available,
)
router = APIRouter(prefix="/api/disks", tags=["disks"])
class MountRequest(BaseModel):
"""Request model for mounting a drive."""
device: str = Field(description="Device path (e.g., /dev/sda1)")
mount_point: str = Field(description="Where to mount (must be under /media)")
fstype: Optional[str] = Field(default=None, description="Filesystem type (auto-detected if not specified)")
options: str = Field(default="defaults", description="Mount options")
persist: bool = Field(default=False, description="Add to fstab for persistent mounting")
class UnmountRequest(BaseModel):
"""Request model for unmounting a drive."""
mount_point: str = Field(description="Mount point to unmount")
lazy: bool = Field(default=False, description="Use lazy unmount if busy")
class FstabRequest(BaseModel):
"""Request model for adding an fstab entry."""
device: str = Field(description="Device path")
mount_point: str = Field(description="Where to mount")
fstype: str = Field(description="Filesystem type")
options: str = Field(default="defaults,nofail,x-systemd.device-timeout=10s", description="Mount options")
def _check_native_mode():
"""Raise 501 if in Docker mode."""
if is_docker_mode():
raise HTTPException(
status_code=501,
detail="Disk management is not available in Docker mode. "
"Configure mounts on the host system."
)
@router.get("")
async def list_disks():
"""List all detected disks/partitions."""
_check_native_mode()
devices = get_block_devices()
return [
{
"name": d.name,
"path": d.path,
"size": d.size,
"fstype": d.fstype,
"mountpoint": d.mountpoint,
"label": d.label,
"uuid": d.uuid,
"model": d.model,
}
for d in devices
]
@router.get("/mounts")
async def list_mounts():
"""List current mounts under /media."""
_check_native_mode()
return get_mounts()
@router.post("/mount")
async def disk_mount(request: MountRequest):
"""Mount a partition."""
_check_native_mode()
result = mount_drive(
device=request.device,
mount_point=request.mount_point,
fstype=request.fstype,
options=request.options,
)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Mount failed"))
# Add to fstab if requested
if request.persist:
fstab_result = add_fstab_entry(
device=request.device,
mount_point=request.mount_point,
fstype=request.fstype or "auto",
)
result["fstab"] = fstab_result
return result
@router.post("/unmount")
async def disk_unmount(request: UnmountRequest):
"""Unmount a partition."""
_check_native_mode()
result = unmount_drive(
mount_point=request.mount_point,
lazy=request.lazy,
)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Unmount failed"))
return result
@router.get("/fstab")
async def list_fstab():
"""Get WROLPi-related fstab entries."""
_check_native_mode()
return get_wrolpi_fstab_entries()
@router.post("/fstab")
async def add_fstab(request: FstabRequest):
"""Add an fstab entry for persistent mounting."""
_check_native_mode()
result = add_fstab_entry(
device=request.device,
mount_point=request.mount_point,
fstype=request.fstype,
options=request.options,
)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed"))
return result
@router.delete("/fstab/{mount_point:path}")
async def delete_fstab(mount_point: str):
"""Remove an fstab entry."""
_check_native_mode()
# URL decode the mount point
mount_point = f"/{mount_point}"
result = remove_fstab_entry(mount_point)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed"))
return result
# --- SMART ---
@router.get("/smart")
async def list_smart():
"""Get SMART status for all drives."""
_check_native_mode()
if not is_smart_available():
return {"available": False, "reason": "pySMART not installed or not supported"}
return {
"available": True,
"drives": get_all_smart_status(),
}

265
controller/api/schemas.py Normal file
View file

@ -0,0 +1,265 @@
"""
Pydantic models for Controller API requests and responses.
"""
from typing import Optional
from pydantic import BaseModel, Field
# ============================================================================
# Health
# ============================================================================
class HealthResponse(BaseModel):
"""Response model for /api/health endpoint."""
status: str = Field(description="Health status of the controller")
version: str = Field(description="Controller version")
docker_mode: bool = Field(description="Whether running in Docker mode")
drive_mounted: bool = Field(description="Whether the primary drive is mounted")
# ============================================================================
# Status - CPU
# ============================================================================
class CpuStatusResponse(BaseModel):
"""Response model for /api/stats/cpu endpoint."""
percent: float = Field(description="CPU usage percentage (0-100)")
frequency_mhz: Optional[int] = Field(default=None, description="Current CPU frequency in MHz")
frequency_max_mhz: Optional[int] = Field(default=None, description="Maximum CPU frequency in MHz")
temperature_c: Optional[float] = Field(default=None, description="CPU temperature in Celsius")
cores: int = Field(description="Number of CPU cores")
# ============================================================================
# Status - Memory
# ============================================================================
class MemoryStatusResponse(BaseModel):
"""Response model for /api/stats/memory endpoint."""
total_bytes: int = Field(description="Total memory in bytes")
available_bytes: int = Field(description="Available memory in bytes")
used_bytes: int = Field(description="Used memory in bytes")
percent: float = Field(description="Memory usage percentage (0-100)")
total_gb: float = Field(description="Total memory in GB")
used_gb: float = Field(description="Used memory in GB")
available_gb: float = Field(description="Available memory in GB")
# ============================================================================
# Status - Load
# ============================================================================
class LoadStatusResponse(BaseModel):
"""Response model for /api/stats/load endpoint."""
load_1min: float = Field(description="1-minute load average")
load_5min: float = Field(description="5-minute load average")
load_15min: float = Field(description="15-minute load average")
# ============================================================================
# Status - Drives
# ============================================================================
class DriveStatusResponse(BaseModel):
"""Response model for individual drive status."""
device: str = Field(description="Device path (e.g., /dev/sda1)")
mount_point: str = Field(description="Mount point path")
fstype: str = Field(description="Filesystem type")
total_bytes: int = Field(description="Total space in bytes")
used_bytes: int = Field(description="Used space in bytes")
free_bytes: int = Field(description="Free space in bytes")
percent: float = Field(description="Usage percentage (0-100)")
total_gb: float = Field(description="Total space in GB")
used_gb: float = Field(description="Used space in GB")
free_gb: float = Field(description="Free space in GB")
class PrimaryDriveStatusResponse(BaseModel):
"""Response model for /api/stats/drives/primary endpoint."""
mounted: bool = Field(description="Whether the primary drive is mounted")
device: Optional[str] = Field(default=None, description="Device path")
mount_point: Optional[str] = Field(default=None, description="Mount point path")
fstype: Optional[str] = Field(default=None, description="Filesystem type")
total_bytes: Optional[int] = Field(default=None, description="Total space in bytes")
used_bytes: Optional[int] = Field(default=None, description="Used space in bytes")
free_bytes: Optional[int] = Field(default=None, description="Free space in bytes")
percent: Optional[float] = Field(default=None, description="Usage percentage")
total_gb: Optional[float] = Field(default=None, description="Total space in GB")
used_gb: Optional[float] = Field(default=None, description="Used space in GB")
free_gb: Optional[float] = Field(default=None, description="Free space in GB")
# ============================================================================
# Status - Network
# ============================================================================
class NetworkInterfaceResponse(BaseModel):
"""Response model for individual network interface."""
name: str = Field(description="Interface name (e.g., eth0, wlan0)")
ipv4: Optional[str] = Field(default=None, description="IPv4 address")
ipv6: Optional[str] = Field(default=None, description="IPv6 address")
mac: Optional[str] = Field(default=None, description="MAC address")
bytes_sent: int = Field(description="Total bytes sent")
bytes_recv: int = Field(description="Total bytes received")
bytes_sent_mb: float = Field(description="Total MB sent")
bytes_recv_mb: float = Field(description="Total MB received")
# ============================================================================
# Status - Power
# ============================================================================
class PowerStatusResponse(BaseModel):
"""Response model for /api/stats/power endpoint."""
undervoltage_detected: bool = Field(description="Currently experiencing undervoltage")
currently_throttled: bool = Field(description="Currently throttled due to voltage/temp")
undervoltage_occurred: bool = Field(description="Undervoltage has occurred since boot")
throttling_occurred: bool = Field(description="Throttling has occurred since boot")
# ============================================================================
# Status - Full
# ============================================================================
class FullStatusResponse(BaseModel):
"""Response model for /api/stats endpoint."""
cpu: CpuStatusResponse = Field(description="CPU status")
memory: MemoryStatusResponse = Field(description="Memory status")
load: LoadStatusResponse = Field(description="System load")
drives: list[DriveStatusResponse] = Field(description="All mounted drives")
primary_drive: Optional[DriveStatusResponse] = Field(default=None, description="Primary WROLPi drive")
network: list[NetworkInterfaceResponse] = Field(description="Network interfaces")
power: PowerStatusResponse = Field(description="Power status")
# ============================================================================
# Admin - Hotspot
# ============================================================================
class HotspotStatusResponse(BaseModel):
"""Response model for /api/hotspot/status endpoint."""
enabled: bool = Field(description="Whether hotspot is currently enabled")
available: bool = Field(description="Whether hotspot functionality is available")
reason: Optional[str] = Field(default=None, description="Reason if unavailable")
ssid: Optional[str] = Field(default=None, description="Hotspot SSID when enabled")
device: Optional[str] = Field(default=None, description="WiFi device name")
class HotspotActionResponse(BaseModel):
"""Response model for hotspot enable/disable actions."""
success: bool = Field(description="Whether the action succeeded")
ssid: Optional[str] = Field(default=None, description="Hotspot SSID")
device: Optional[str] = Field(default=None, description="WiFi device name")
error: Optional[str] = Field(default=None, description="Error message if failed")
# ============================================================================
# Admin - Throttle
# ============================================================================
class ThrottleStatusResponse(BaseModel):
"""Response model for /api/throttle/status endpoint."""
enabled: bool = Field(description="Whether CPU throttle (powersave) is enabled")
available: bool = Field(description="Whether throttle control is available")
reason: Optional[str] = Field(default=None, description="Reason if unavailable")
governor: Optional[str] = Field(default=None, description="Current CPU governor")
available_governors: Optional[list[str]] = Field(default=None, description="Available governors")
class ThrottleActionResponse(BaseModel):
"""Response model for throttle enable/disable actions."""
success: bool = Field(description="Whether the action succeeded")
governor: Optional[str] = Field(default=None, description="New CPU governor")
error: Optional[str] = Field(default=None, description="Error message if failed")
# ============================================================================
# Admin - System Control
# ============================================================================
class SystemActionResponse(BaseModel):
"""Response model for system shutdown/reboot actions."""
success: bool = Field(description="Whether the action was initiated")
message: Optional[str] = Field(default=None, description="Status message")
error: Optional[str] = Field(default=None, description="Error message if failed")
class ServiceRestartResult(BaseModel):
"""Result of restarting a single service."""
success: bool = Field(description="Whether the service restart succeeded")
error: Optional[str] = Field(default=None, description="Error message if failed")
pending: Optional[bool] = Field(default=None, description="Whether restart is pending")
class RestartServicesResponse(BaseModel):
"""Response model for /api/restart endpoint."""
success: bool = Field(description="Whether the restart was initiated")
services: dict[str, ServiceRestartResult] = Field(description="Results per service")
error: Optional[str] = Field(default=None, description="Error message if failed")
# ============================================================================
# Services - Status
# ============================================================================
class ServiceStatusResponse(BaseModel):
"""Response model for individual service status."""
name: str = Field(description="Service name")
status: str = Field(description="Service status (running, stopped, failed, unknown)")
systemd_name: Optional[str] = Field(default=None, description="Systemd unit name")
container_name: Optional[str] = Field(default=None, description="Docker container name")
active: Optional[str] = Field(default=None, description="Systemd active state")
docker_status: Optional[str] = Field(default=None, description="Docker container status")
enabled: Optional[bool] = Field(default=None, description="Whether service starts at boot")
port: Optional[int] = Field(default=None, description="Service port")
viewable: Optional[bool] = Field(default=False, description="Whether service has a web UI")
view_path: Optional[str] = Field(default="", description="Path to web UI")
use_https: Optional[bool] = Field(default=False, description="Whether service uses HTTPS")
description: Optional[str] = Field(default="", description="Service description")
available: Optional[bool] = Field(default=None, description="Whether management is available")
reason: Optional[str] = Field(default=None, description="Reason if unavailable")
error: Optional[str] = Field(default=None, description="Error message if any")
class ServiceActionResponse(BaseModel):
"""Response model for service start/stop/restart/enable/disable actions."""
success: bool = Field(description="Whether the action succeeded")
service: str = Field(description="Service name")
action: str = Field(description="Action performed (start, stop, restart, enable, disable)")
error: Optional[str] = Field(default=None, description="Error message if failed")
class ServiceLogsResponse(BaseModel):
"""Response model for service logs."""
service: str = Field(description="Service name")
lines: int = Field(description="Number of lines requested")
since: Optional[str] = Field(default=None, description="Time filter if specified")
logs: str = Field(description="Log content")
class ServicesListErrorResponse(BaseModel):
"""Response model when services list fails."""
error: str = Field(description="Error message")
reason: Optional[str] = Field(default=None, description="Detailed reason")

162
controller/api/services.py Normal file
View file

@ -0,0 +1,162 @@
"""
Service management API endpoints for WROLPi Controller.
"""
from typing import Optional, Union
from fastapi import APIRouter, HTTPException, Query
from controller.api.schemas import (
ServiceActionResponse,
ServiceLogsResponse,
ServiceStatusResponse,
ServicesListErrorResponse,
)
from controller.lib.config import is_docker_mode
from controller.lib.docker_services import (
can_manage_containers,
get_all_containers_status,
get_container_logs,
get_container_status,
restart_container,
start_container,
stop_container,
)
from controller.lib.systemd import (
disable_service,
enable_service,
get_all_services_status,
get_service_logs,
get_service_status,
restart_service,
start_service,
stop_service,
)
router = APIRouter(prefix="/api/services", tags=["services"])
@router.get(
"",
response_model=Union[list[ServiceStatusResponse], ServicesListErrorResponse],
)
async def list_services() -> Union[list[ServiceStatusResponse], ServicesListErrorResponse]:
"""Get status of all managed services."""
if is_docker_mode():
if can_manage_containers():
statuses = get_all_containers_status()
return [ServiceStatusResponse(**s) for s in statuses if "error" not in s]
else:
return ServicesListErrorResponse(
error="Docker management not available",
reason="Docker socket not mounted or docker library not installed",
)
else:
statuses = get_all_services_status()
return [ServiceStatusResponse(**s) for s in statuses]
@router.get("/{name}", response_model=ServiceStatusResponse)
async def get_service(name: str) -> ServiceStatusResponse:
"""Get status of a specific service."""
if is_docker_mode():
status = get_container_status(name)
else:
status = get_service_status(name)
if "error" in status and status.get("status") is None:
raise HTTPException(status_code=404, detail=status["error"])
return ServiceStatusResponse(**status)
@router.post("/{name}/start", response_model=ServiceActionResponse)
async def service_start(name: str) -> ServiceActionResponse:
"""Start a service."""
if is_docker_mode():
result = start_container(name)
else:
result = start_service(name)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed to start"))
return ServiceActionResponse(**result)
@router.post("/{name}/stop", response_model=ServiceActionResponse)
async def service_stop(name: str) -> ServiceActionResponse:
"""Stop a service."""
if is_docker_mode():
result = stop_container(name)
else:
result = stop_service(name)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed to stop"))
return ServiceActionResponse(**result)
@router.post("/{name}/restart", response_model=ServiceActionResponse)
async def service_restart(name: str) -> ServiceActionResponse:
"""Restart a service."""
if is_docker_mode():
result = restart_container(name)
else:
result = restart_service(name)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed to restart"))
return ServiceActionResponse(**result)
@router.post("/{name}/enable", response_model=ServiceActionResponse)
async def service_enable(name: str) -> ServiceActionResponse:
"""Enable a service to start at boot (systemd only)."""
if is_docker_mode():
raise HTTPException(
status_code=501,
detail="Enable/disable not available in Docker mode"
)
result = enable_service(name)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed"))
return ServiceActionResponse(**result)
@router.post("/{name}/disable", response_model=ServiceActionResponse)
async def service_disable(name: str) -> ServiceActionResponse:
"""Disable a service from starting at boot (systemd only)."""
if is_docker_mode():
raise HTTPException(
status_code=501,
detail="Enable/disable not available in Docker mode"
)
result = disable_service(name)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed"))
return ServiceActionResponse(**result)
@router.get("/{name}/logs", response_model=ServiceLogsResponse)
async def service_logs(
name: str,
lines: int = Query(default=100, ge=1, le=10000),
since: Optional[str] = Query(default=None),
) -> ServiceLogsResponse:
"""Get service logs."""
if is_docker_mode():
result = get_container_logs(name, lines)
else:
result = get_service_logs(name, lines, since)
if "error" in result:
raise HTTPException(status_code=500, detail=result["error"])
return ServiceLogsResponse(**result)

145
controller/api/status.py Normal file
View file

@ -0,0 +1,145 @@
"""
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_hotspot_status, get_throttle_status, 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", {})
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.
"""
from controller.lib.config import get_config
hotspot_status = get_hotspot_status()
throttle_status = get_throttle_status()
# Get hotspot SSID from config if hotspot is connected
config = get_config()
hotspot_config = config.get("hotspot", {})
hotspot_ssid = hotspot_config.get("ssid", "WROLPi") if hotspot_status == HotspotStatus.connected else None
return {
"dockerized": is_docker_mode(),
"is_rpi": is_rpi(),
"is_rpi4": is_rpi4(),
"is_rpi5": is_rpi5(),
"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, **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())