"""Settings — config read/write, hostname, timezone management.""" import asyncio import platform import socket from typing import Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from backend.auth import get_current_user from backend.config import load_config, save_config router = APIRouter(prefix="/api/settings", tags=["settings"]) class ConfigUpdate(BaseModel): """Partial config update — only send fields to change.""" hostname_display: Optional[str] = None timezone: Optional[str] = None ntp_enabled: Optional[bool] = None dock_apps: Optional[list[str]] = None panel_services: Optional[list[str]] = None gui_apps: Optional[list[dict]] = None session_timeout_minutes: Optional[int] = None stats_interval_seconds: Optional[int] = None update_check_enabled: Optional[bool] = None update_check_interval: Optional[int] = None class HostnameRequest(BaseModel): hostname: str class TimezoneRequest(BaseModel): timezone: str async def _run(cmd: list[str]) -> tuple[int, str, str]: import shutil if not shutil.which(cmd[0]): return 1, "", f"{cmd[0]}: command not found" proc = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await proc.communicate() return proc.returncode, stdout.decode(), stderr.decode() @router.get("") async def get_config(_user: str = Depends(get_current_user)): return load_config() @router.put("") async def update_config(update: ConfigUpdate, _user: str = Depends(get_current_user)): cfg = load_config() for field, value in update.model_dump(exclude_none=True).items(): cfg[field] = value save_config(cfg) return cfg @router.get("/system") async def system_info(_user: str = Depends(get_current_user)): """System info for the About section.""" hostname = socket.gethostname() uname = platform.uname() # OS release info os_name = "Linux" os_version = "" try: with open("/etc/os-release") as f: for line in f: if line.startswith("PRETTY_NAME="): os_name = line.split("=", 1)[1].strip().strip('"') elif line.startswith("VERSION="): os_version = line.split("=", 1)[1].strip().strip('"') except FileNotFoundError: pass return { "hostname": hostname, "os": os_name, "os_version": os_version, "kernel": uname.release, "arch": uname.machine, "python": platform.python_version(), } @router.post("/hostname") async def set_hostname(req: HostnameRequest, _user: str = Depends(get_current_user)): rc, _, err = await _run(["hostnamectl", "set-hostname", req.hostname]) if rc != 0: raise HTTPException(500, f"Failed to set hostname: {err}") return {"hostname": req.hostname} @router.get("/timezone") async def get_timezone(_user: str = Depends(get_current_user)): rc, out, _ = await _run(["timedatectl", "show", "--property=Timezone", "--value"]) return {"timezone": out.strip() if rc == 0 else "unknown"} @router.post("/timezone") async def set_timezone(req: TimezoneRequest, _user: str = Depends(get_current_user)): rc, _, err = await _run(["timedatectl", "set-timezone", req.timezone]) if rc != 0: raise HTTPException(500, f"Failed to set timezone: {err}") return {"timezone": req.timezone} @router.get("/timezones") async def list_timezones(_user: str = Depends(get_current_user)): rc, out, _ = await _run(["timedatectl", "list-timezones"]) if rc != 0: return [] return out.strip().splitlines()