feat: v.1.2.0 добавлин web интерфейс.

This commit is contained in:
Fedorov Dmitriy
2026-09-20 02:00:28 +03:00
parent 3cd71ba5ff
commit 8f749da14c
4 changed files with 155 additions and 15 deletions
+113 -2
View File
@@ -1,9 +1,12 @@
import logging
import shutil
from contextlib import asynccontextmanager
from typing import Literal, Annotated
import yaml
from fastapi import FastAPI, Request, HTTPException, Path
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from pathlib import Path as FilePath
from app.qingping.service import QingpingService
@@ -184,6 +187,34 @@ app = FastAPI(
lifespan=lifespan,
)
WEB_ROOT = FilePath(__file__).resolve().parents[1] / "web"
app.mount(
"/ui/static",
StaticFiles(directory=WEB_ROOT),
name="ui-static",
)
@app.get("/", include_in_schema=False)
async def root():
return RedirectResponse(url="/ui/panel")
@app.get("/ui", include_in_schema=False)
async def ui_root():
return RedirectResponse(url="/ui/panel")
@app.get("/ui/widget", include_in_schema=False)
async def ui_widget():
return FileResponse(WEB_ROOT / "widget.html")
@app.get("/ui/panel", include_in_schema=False)
async def ui_panel():
return FileResponse(WEB_ROOT / "panel.html")
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
@@ -724,6 +755,86 @@ async def set_light(
# ----------------------------------------------------------------------
# Переход в auto
# ----------------------------------------------------------------------
@app.get("/api/schedule/config")
async def get_schedule_config():
try:
raw = yaml.safe_load(
FilePath(SCHEDULE_FILE).read_text(encoding="utf-8")
)
except (OSError, yaml.YAMLError) as exc:
raise HTTPException(
status_code=500,
detail={
"message": "Could not read schedule config",
"error": f"{type(exc).__name__}: {exc}",
},
) from exc
if not isinstance(raw, dict):
raise HTTPException(
status_code=500,
detail="Schedule config root must be an object",
)
return raw
@app.put("/api/schedule/config")
async def update_schedule_config(payload: dict):
global schedule_load_error
schedule_path = FilePath(SCHEDULE_FILE)
temp_path = schedule_path.with_name(schedule_path.name + ".tmp")
backup_path = schedule_path.with_name(schedule_path.name + ".bak")
try:
serialized = yaml.safe_dump(
payload,
allow_unicode=True,
sort_keys=False,
default_flow_style=False,
)
temp_path.write_text(serialized, encoding="utf-8")
config = load_schedule(temp_path)
except (OSError, ValueError, yaml.YAMLError) as exc:
temp_path.unlink(missing_ok=True)
raise HTTPException(
status_code=400,
detail={
"message": "Invalid schedule config",
"error": str(exc),
},
) from exc
try:
if schedule_path.exists():
shutil.copy2(schedule_path, backup_path)
temp_path.replace(schedule_path)
except OSError as exc:
temp_path.unlink(missing_ok=True)
raise HTTPException(
status_code=500,
detail={
"message": "Could not save schedule config",
"error": f"{type(exc).__name__}: {exc}",
},
) from exc
schedule_load_error = None
restart_required = schedule_service is None
if schedule_service is not None:
await schedule_service.replace_config(config)
return {
"ok": True,
"restart_required": restart_required,
"config": payload,
"schedule": get_schedule_status(),
}
@app.post("/api/schedule/override/clear")
async def clear_schedule_override():
if schedule_service is None:
@@ -776,4 +887,4 @@ async def resume_schedule():
return {
"ok": True,
"schedule": get_schedule_status(),
}
}