feat(webui): FastAPI REST + WebSocket-консоль + MCP-эндпоинт + браузерный UI
- server.py: /api/power/*, /api/status, /api/history, /ws/console, /mcp (MCPServer streamable-http) - auth: Bearer-токен (ATXPI_WEB_TOKEN) для /api/* и /mcp/*; WS — через ?token= - статика: index.html, app.js, style.css + vendored xterm.js (работает офлайн на Pi) - lightweight: один процесс uvicorn, без node/сборки - install.sh: отдаю сервис-юзеру владение atxpi/ и /etc/atxpi (pyc/tmp/audit)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
/* atxpi — браузерный клиент (без сборки, vanilla JS). */
|
||||
'use strict';
|
||||
|
||||
const TOKEN = 'atxpi_token';
|
||||
let term = null, ws = null, termOpen = false;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem(TOKEN) || sessionStorage.getItem(TOKEN) || '';
|
||||
}
|
||||
function setToken(t) {
|
||||
localStorage.setItem(TOKEN, t);
|
||||
}
|
||||
function hdr() {
|
||||
return { 'Authorization': 'Bearer ' + getToken() };
|
||||
}
|
||||
|
||||
/* ── API ─────────────────────────────────────────────────────────── */
|
||||
async function api(path, opts = {}) {
|
||||
const headers = Object.assign({}, hdr(), opts.headers || {});
|
||||
const r = await fetch(path, Object.assign({}, opts, { headers }));
|
||||
if (r.status === 401) { showLogin(); throw new Error('unauthorized'); }
|
||||
return r.json();
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const s = await api('/api/status');
|
||||
document.getElementById('powerState').textContent =
|
||||
s.power === 'on' ? '🟢 ON' : '🔴 OFF';
|
||||
document.getElementById('status').textContent =
|
||||
`driver=${s.driver} · serial=${s.serial_port}@${s.serial_baud}`;
|
||||
} catch (e) { /* уже обработано в api() */ }
|
||||
}
|
||||
|
||||
async function doPower(action) {
|
||||
try {
|
||||
await api('/api/power/' + action, { method: 'POST' });
|
||||
await refresh();
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
/* ── логин ───────────────────────────────────────────────────────── */
|
||||
async function login() {
|
||||
const t = document.getElementById('tok').value.trim();
|
||||
const r = await fetch('/api/status', { headers: { 'Authorization': 'Bearer ' + t } });
|
||||
if (r.ok) {
|
||||
setToken(t);
|
||||
showApp();
|
||||
} else {
|
||||
document.getElementById('loginMsg').textContent = 'Неверный токен';
|
||||
}
|
||||
}
|
||||
|
||||
function showLogin() {
|
||||
document.getElementById('login').classList.remove('hidden');
|
||||
document.getElementById('app').classList.add('hidden');
|
||||
}
|
||||
function showApp() {
|
||||
document.getElementById('login').classList.add('hidden');
|
||||
document.getElementById('app').classList.remove('hidden');
|
||||
refresh();
|
||||
}
|
||||
|
||||
/* ── консоль (xterm.js + WebSocket) ──────────────────────────────── */
|
||||
function toggleConsole() {
|
||||
if (termOpen) { disconnectConsole(); return; }
|
||||
|
||||
term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 13,
|
||||
convertEol: true,
|
||||
theme: { background: '#0b0f14', foreground: '#d7d7d7', cursor: '#4e8cff' },
|
||||
});
|
||||
term.open(document.getElementById('term'));
|
||||
|
||||
const scheme = (location.protocol === 'https:') ? 'wss' : 'ws';
|
||||
ws = new WebSocket(
|
||||
scheme + '://' + location.host + '/ws/console?token=' + encodeURIComponent(getToken())
|
||||
);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
|
||||
ws.onopen = () => {
|
||||
termOpen = true;
|
||||
document.getElementById('consoleState').textContent = 'подключена';
|
||||
term.writeln('\x1b[32m(консоль подключена)\x1b[0m');
|
||||
};
|
||||
ws.onmessage = (ev) => {
|
||||
if (typeof ev.data === 'string') term.write(ev.data);
|
||||
else term.write(new Uint8Array(ev.data));
|
||||
};
|
||||
ws.onclose = () => { disconnectConsole(true); };
|
||||
term.onData((d) => { if (ws && ws.readyState === 1) ws.send(d); });
|
||||
}
|
||||
|
||||
function disconnectConsole(byRemote = false) {
|
||||
if (ws) { try { ws.close(); } catch (e) {} ws = null; }
|
||||
if (term) { try { term.dispose(); } catch (e) {} term = null; }
|
||||
termOpen = false;
|
||||
document.getElementById('consoleState').textContent = 'не подключена';
|
||||
if (byRemote) { /* уже закрыто сервером */ }
|
||||
}
|
||||
|
||||
/* ── init ────────────────────────────────────────────────────────── */
|
||||
if (getToken()) showApp(); else showLogin();
|
||||
Reference in New Issue
Block a user