feat(webui): режим console (только RS232-консоль, питание отключено)
- ATXPI_MODE=console: /api/power/* → 403, кнопки питания скрыты в UI - /api/status отдаёт mode; env-шаблон и install.sh учитывают ATXPI_MODE - нужно для безопасного запуска панели на серверах-«управленцах» без риска питания
This commit is contained in:
@@ -5,6 +5,9 @@
|
||||
# Драйвер железа: real | mock
|
||||
ATXPI_DRIVER=real
|
||||
|
||||
# Режим: full | console. В console питание отключено (только RS232-консоль).
|
||||
ATXPI_MODE=full
|
||||
|
||||
# RS232-консоль сервера
|
||||
SERIAL_PORT=/dev/ttyUSB0
|
||||
SERIAL_BAUD=115200
|
||||
|
||||
+14
-1
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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() */ }
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,12 @@
|
||||
<h1>⚡ atxpi</h1>
|
||||
<span id="powerState" class="badge">…</span>
|
||||
<span class="spacer"></span>
|
||||
<span id="powerControls">
|
||||
<button class="btn" onclick="doPower('on')" title="Нажать кнопку питания">⏻ ON</button>
|
||||
<button class="btn" onclick="doPower('off')" title="Принудительно выключить (долгое нажатие)">⭘ OFF</button>
|
||||
<button class="btn" onclick="doPower('reset')" title="Аппаратный сброс">↻ Reset</button>
|
||||
<button class="btn" onclick="doPower('cycle')" title="Жёсткий перезапуск">⟳ Cycle</button>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<section id="status" class="status-line"></section>
|
||||
|
||||
@@ -29,6 +29,7 @@ ATXPI_BIND_PORT="${ATXPI_BIND_PORT:-8443}"
|
||||
ATXPI_SERIAL="${ATXPI_SERIAL:-/dev/ttyUSB0}"
|
||||
ATXPI_BAUD="${ATXPI_BAUD:-115200}"
|
||||
ATXPI_DRIVER="${ATXPI_DRIVER:-real}"
|
||||
ATXPI_MODE="${ATXPI_MODE:-full}"
|
||||
|
||||
log() { echo -e "\e[1;32m[atxpi]\e[0m $*"; }
|
||||
err() { echo -e "\e[1;31m[atxpi]\e[0m $*" >&2; }
|
||||
@@ -99,6 +100,8 @@ if [ ! -f "$ENV_FILE" ]; then
|
||||
# atxpi — локальная конфигурация (создаётся install.sh, НЕ хранится в git)
|
||||
# ATXPI_DRIVER: real | mock
|
||||
ATXPI_DRIVER=$ATXPI_DRIVER
|
||||
# Режим: full | console (в console питание отключено)
|
||||
ATXPI_MODE=$ATXPI_MODE
|
||||
SERIAL_PORT=$ATXPI_SERIAL
|
||||
SERIAL_BAUD=$ATXPI_BAUD
|
||||
# токен для REST-API и браузера (генерируется при установке)
|
||||
|
||||
Reference in New Issue
Block a user