diff --git a/atxpi/deploy/atxpi.env.example b/atxpi/deploy/atxpi.env.example index 9851c44..5eb1158 100644 --- a/atxpi/deploy/atxpi.env.example +++ b/atxpi/deploy/atxpi.env.example @@ -5,6 +5,9 @@ # Драйвер железа: real | mock ATXPI_DRIVER=real +# Режим: full | console. В console питание отключено (только RS232-консоль). +ATXPI_MODE=full + # RS232-консоль сервера SERIAL_PORT=/dev/ttyUSB0 SERIAL_BAUD=115200 diff --git a/atxpi/webui/server.py b/atxpi/webui/server.py index 49f710c..ba5dc36 100644 --- a/atxpi/webui/server.py +++ b/atxpi/webui/server.py @@ -22,7 +22,7 @@ import os import time from pathlib import Path -from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, HTTPException from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles @@ -34,9 +34,16 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(mess STATIC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static") WEB_TOKEN = os.environ.get("ATXPI_WEB_TOKEN", "") +MODE = os.environ.get("ATXPI_MODE", "full") # full | console (только консоль, питание отключено) AUDIT = os.environ.get("ATXPI_AUDIT", "/etc/atxpi/audit.jsonl") +def _power_guard(): + """В режиме console питание выключено — деструктивные действия запрещены.""" + if MODE == "console": + raise HTTPException(status_code=403, detail="console-only mode: power disabled") + + def audit(action: str, **kw): """Логируем действие в jsonl (для /api/history).""" entry = {"t": time.time(), "action": action, **kw} @@ -77,29 +84,34 @@ async def auth_mw(request: Request, call_next): # ── REST: питание ────────────────────────────────────────────────────────── @app.get("/api/power") async def api_power_status(): + _power_guard() return power_mod.status() @app.post("/api/power/on") async def api_power_on(): + _power_guard() audit("power_on") return power_mod.press_power() @app.post("/api/power/off") async def api_power_off(): + _power_guard() audit("power_off") return power_mod.force_off() @app.post("/api/power/reset") async def api_power_reset(): + _power_guard() audit("power_reset") return power_mod.press_reset() @app.post("/api/power/cycle") async def api_power_cycle(): + _power_guard() audit("power_cycle") return power_mod.cycle() @@ -113,6 +125,7 @@ async def api_status(): "driver": DRIVER, "serial_port": SERIAL_PORT, "serial_baud": SERIAL_BAUD, + "mode": MODE, } diff --git a/atxpi/webui/static/app.js b/atxpi/webui/static/app.js index 0376bef..9eac2e7 100644 --- a/atxpi/webui/static/app.js +++ b/atxpi/webui/static/app.js @@ -28,7 +28,10 @@ async function refresh() { document.getElementById('powerState').textContent = s.power === 'on' ? '🟢 ON' : '🔴 OFF'; document.getElementById('status').textContent = - `driver=${s.driver} · serial=${s.serial_port}@${s.serial_baud}`; + `driver=${s.driver} · serial=${s.serial_port}@${s.serial_baud}` + + (s.mode === 'console' ? ' · режим: только консоль' : ''); + const pc = document.getElementById('powerControls'); + if (pc) pc.style.display = (s.mode === 'console') ? 'none' : ''; } catch (e) { /* уже обработано в api() */ } } diff --git a/atxpi/webui/static/index.html b/atxpi/webui/static/index.html index 41169bd..bdd8111 100644 --- a/atxpi/webui/static/index.html +++ b/atxpi/webui/static/index.html @@ -25,10 +25,12 @@