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
|
# Драйвер железа: real | mock
|
||||||
ATXPI_DRIVER=real
|
ATXPI_DRIVER=real
|
||||||
|
|
||||||
|
# Режим: full | console. В console питание отключено (только RS232-консоль).
|
||||||
|
ATXPI_MODE=full
|
||||||
|
|
||||||
# RS232-консоль сервера
|
# RS232-консоль сервера
|
||||||
SERIAL_PORT=/dev/ttyUSB0
|
SERIAL_PORT=/dev/ttyUSB0
|
||||||
SERIAL_BAUD=115200
|
SERIAL_BAUD=115200
|
||||||
|
|||||||
+14
-1
@@ -22,7 +22,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
from pathlib import Path
|
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.responses import FileResponse, JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
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")
|
STATIC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
||||||
WEB_TOKEN = os.environ.get("ATXPI_WEB_TOKEN", "")
|
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")
|
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):
|
def audit(action: str, **kw):
|
||||||
"""Логируем действие в jsonl (для /api/history)."""
|
"""Логируем действие в jsonl (для /api/history)."""
|
||||||
entry = {"t": time.time(), "action": action, **kw}
|
entry = {"t": time.time(), "action": action, **kw}
|
||||||
@@ -77,29 +84,34 @@ async def auth_mw(request: Request, call_next):
|
|||||||
# ── REST: питание ──────────────────────────────────────────────────────────
|
# ── REST: питание ──────────────────────────────────────────────────────────
|
||||||
@app.get("/api/power")
|
@app.get("/api/power")
|
||||||
async def api_power_status():
|
async def api_power_status():
|
||||||
|
_power_guard()
|
||||||
return power_mod.status()
|
return power_mod.status()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/power/on")
|
@app.post("/api/power/on")
|
||||||
async def api_power_on():
|
async def api_power_on():
|
||||||
|
_power_guard()
|
||||||
audit("power_on")
|
audit("power_on")
|
||||||
return power_mod.press_power()
|
return power_mod.press_power()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/power/off")
|
@app.post("/api/power/off")
|
||||||
async def api_power_off():
|
async def api_power_off():
|
||||||
|
_power_guard()
|
||||||
audit("power_off")
|
audit("power_off")
|
||||||
return power_mod.force_off()
|
return power_mod.force_off()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/power/reset")
|
@app.post("/api/power/reset")
|
||||||
async def api_power_reset():
|
async def api_power_reset():
|
||||||
|
_power_guard()
|
||||||
audit("power_reset")
|
audit("power_reset")
|
||||||
return power_mod.press_reset()
|
return power_mod.press_reset()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/power/cycle")
|
@app.post("/api/power/cycle")
|
||||||
async def api_power_cycle():
|
async def api_power_cycle():
|
||||||
|
_power_guard()
|
||||||
audit("power_cycle")
|
audit("power_cycle")
|
||||||
return power_mod.cycle()
|
return power_mod.cycle()
|
||||||
|
|
||||||
@@ -113,6 +125,7 @@ async def api_status():
|
|||||||
"driver": DRIVER,
|
"driver": DRIVER,
|
||||||
"serial_port": SERIAL_PORT,
|
"serial_port": SERIAL_PORT,
|
||||||
"serial_baud": SERIAL_BAUD,
|
"serial_baud": SERIAL_BAUD,
|
||||||
|
"mode": MODE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ async function refresh() {
|
|||||||
document.getElementById('powerState').textContent =
|
document.getElementById('powerState').textContent =
|
||||||
s.power === 'on' ? '🟢 ON' : '🔴 OFF';
|
s.power === 'on' ? '🟢 ON' : '🔴 OFF';
|
||||||
document.getElementById('status').textContent =
|
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() */ }
|
} catch (e) { /* уже обработано в api() */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,10 +25,12 @@
|
|||||||
<h1>⚡ atxpi</h1>
|
<h1>⚡ atxpi</h1>
|
||||||
<span id="powerState" class="badge">…</span>
|
<span id="powerState" class="badge">…</span>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
|
<span id="powerControls">
|
||||||
<button class="btn" onclick="doPower('on')" title="Нажать кнопку питания">⏻ ON</button>
|
<button class="btn" onclick="doPower('on')" title="Нажать кнопку питания">⏻ ON</button>
|
||||||
<button class="btn" onclick="doPower('off')" title="Принудительно выключить (долгое нажатие)">⭘ OFF</button>
|
<button class="btn" onclick="doPower('off')" title="Принудительно выключить (долгое нажатие)">⭘ OFF</button>
|
||||||
<button class="btn" onclick="doPower('reset')" title="Аппаратный сброс">↻ Reset</button>
|
<button class="btn" onclick="doPower('reset')" title="Аппаратный сброс">↻ Reset</button>
|
||||||
<button class="btn" onclick="doPower('cycle')" title="Жёсткий перезапуск">⟳ Cycle</button>
|
<button class="btn" onclick="doPower('cycle')" title="Жёсткий перезапуск">⟳ Cycle</button>
|
||||||
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section id="status" class="status-line"></section>
|
<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_SERIAL="${ATXPI_SERIAL:-/dev/ttyUSB0}"
|
||||||
ATXPI_BAUD="${ATXPI_BAUD:-115200}"
|
ATXPI_BAUD="${ATXPI_BAUD:-115200}"
|
||||||
ATXPI_DRIVER="${ATXPI_DRIVER:-real}"
|
ATXPI_DRIVER="${ATXPI_DRIVER:-real}"
|
||||||
|
ATXPI_MODE="${ATXPI_MODE:-full}"
|
||||||
|
|
||||||
log() { echo -e "\e[1;32m[atxpi]\e[0m $*"; }
|
log() { echo -e "\e[1;32m[atxpi]\e[0m $*"; }
|
||||||
err() { echo -e "\e[1;31m[atxpi]\e[0m $*" >&2; }
|
err() { echo -e "\e[1;31m[atxpi]\e[0m $*" >&2; }
|
||||||
@@ -99,6 +100,8 @@ if [ ! -f "$ENV_FILE" ]; then
|
|||||||
# atxpi — локальная конфигурация (создаётся install.sh, НЕ хранится в git)
|
# atxpi — локальная конфигурация (создаётся install.sh, НЕ хранится в git)
|
||||||
# ATXPI_DRIVER: real | mock
|
# ATXPI_DRIVER: real | mock
|
||||||
ATXPI_DRIVER=$ATXPI_DRIVER
|
ATXPI_DRIVER=$ATXPI_DRIVER
|
||||||
|
# Режим: full | console (в console питание отключено)
|
||||||
|
ATXPI_MODE=$ATXPI_MODE
|
||||||
SERIAL_PORT=$ATXPI_SERIAL
|
SERIAL_PORT=$ATXPI_SERIAL
|
||||||
SERIAL_BAUD=$ATXPI_BAUD
|
SERIAL_BAUD=$ATXPI_BAUD
|
||||||
# токен для REST-API и браузера (генерируется при установке)
|
# токен для REST-API и браузера (генерируется при установке)
|
||||||
|
|||||||
Reference in New Issue
Block a user