- ATXPI_MODE=console: /api/power/* → 403, кнопки питания скрыты в UI - /api/status отдаёт mode; env-шаблон и install.sh учитывают ATXPI_MODE - нужно для безопасного запуска панели на серверах-«управленцах» без риска питания
108 lines
4.0 KiB
JavaScript
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();
|