Files
toros 49b6cf89f4 feat(webui): режим console (только RS232-консоль, питание отключено)
- ATXPI_MODE=console: /api/power/* → 403, кнопки питания скрыты в UI
- /api/status отдаёт mode; env-шаблон и install.sh учитывают ATXPI_MODE
- нужно для безопасного запуска панели на серверах-«управленцах» без риска питания
2026-09-08 23:34:23 +05:00

108 lines
4.0 KiB
JavaScript

/* 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}` +
(s.mode === 'console' ? ' · режим: только консоль' : '');
const pc = document.getElementById('powerControls');
if (pc) pc.style.display = (s.mode === 'console') ? 'none' : '';
} 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();