atlus/backend/routers/settings.py
roberts 23e4906d08 Replace panel services with docked apps, add autostart and update scanner settings
Panel: SERVICES section becomes APPLICATIONS — shows configured GUI apps with
status dots, launch/stop controls, and "+ Add" button linking to Settings.

Backend: DisplayManager.autostart_apps() launches autostart-enabled GUI apps on
service startup (always-on desktop session). Lifespan calls it before yield.

Settings: new Applications section for managing GUI apps (add/remove/autostart
toggle). General section gains update scanner interval + enable/disable toggle.
Config adds update_check_enabled and update_check_interval fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 23:40:44 -05:00

122 lines
3.7 KiB
Python

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