scm-harmonic-cmts-admin/static/terminal.js

196 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// --- static/terminal.js ---
// 1. 從剛剛建立的 utils.js 引入工具
import { getGlobalConnectionInfo } from './utils.js';
// 2. 宣告終端機專用的變數
let term, fitAddon, ws;
export let isConnected = false;
// 3. 終端機文字上色 (內部使用,不需要 export)
function colorizeTerminalStream(text) {
let res = text;
// 1. IP 地址:改為「柔和冰藍色」 (移除 1; 粗體)
res = res.replace(/\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g, '\x1b[38;5;111m$1\x1b[0m');
// 2. MAC 地址:改為「柔和薰衣草紫」 (移除 1; 粗體)
res = res.replace(/\b([0-9a-fA-F]{4}\.[0-9a-fA-F]{4}\.[0-9a-fA-F]{4}|[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})\b/g, '\x1b[38;5;140m$1\x1b[0m');
// 3. 正常狀態:維持「柔和薄荷綠」
res = res.replace(/\b([a-z]-online|online|init\([^)]+\)|up|active|success|yes)\b/gi, '\x1b[38;5;114m$1\x1b[0m');
// 4. 異常狀態:維持「柔和珊瑚紅」
res = res.replace(/\b(offline|down|admin-down|error|fail|failed|shutdown|reject|invalid|no)\b/gi, '\x1b[38;5;204m$1\x1b[0m');
// 5. 介面名稱 (Cable/RPD等):改為「柔和奶油黃」 (原本是 \x1b[33m)
res = res.replace(/\b(Cable|GigabitEthernet|TenGigabitEthernet|Upstream|Downstream|Mac-domain|bonding-group|rpd)\b/gi, '\x1b[38;5;179m$1\x1b[0m');
return res;
}
// 4. 匯出 (export) 所有需要被外部呼叫的函數
export function initTerminal() {
const container = document.getElementById('terminal-container');
// ==========================================
// 🛡️ DOM 防火牆:攔截殘缺的假鍵盤事件
// ==========================================
container.addEventListener('keydown', (e) => {
// 如果這個事件物件沒有 getModifierState 函數 (代表它是外掛產生的假事件)
if (typeof e.getModifierState !== 'function') {
// 🛑 停止事件傳遞!不讓它流進 Xterm.js 裡
e.stopPropagation();
}
}, true); // 🌟 關鍵:傳入 true 啟用「捕獲階段 (Capture Phase)」,確保我們比 Xterm.js 更早拿到事件
// ==========================================
term = new Terminal({ cursorBlink: true, theme: { background: '#1e1e1e', foreground: '#e0e0e0' }, fontFamily: 'Courier New, monospace', fontSize: 15, scrollback: 100000 });
fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('terminal-container'));
fitAddon.fit();
term.writeln('Welcome to Harmonic cOS Web Terminal.');
term.writeln('Please confirm the target device above and click "連線至 CMTS"... \r\n');
term.onData((data) => {
if (ws && ws.readyState === WebSocket.OPEN) ws.send(data);
});
}
export function connectWebSocket(resetCallback) {
if (ws && ws.readyState === WebSocket.OPEN) return;
const connInfo = getGlobalConnectionInfo();
if (!connInfo) return;
if (resetCallback) resetCallback(); // 呼叫 app.js 傳進來的重置函數
// 🌟 [修復] 切換設備時,徹底清空終端機舊有畫面與緩衝區
if (term) {
term.clear();
}
term.writeln(`\x1b[33mConnecting to ${connInfo.host} via WebSocket...\x1b[0m`);
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/terminal?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
isConnected = true;
const btnLoadTask = document.getElementById('btn-load-task');
if (btnLoadTask) {
btnLoadTask.disabled = false;
btnLoadTask.style.backgroundColor = '#c0392b';
btnLoadTask.style.cursor = 'pointer';
}
// 🌟 UX 升級:剛連上 WS 時,顯示「驗證中...」而不是直接顯示「已連線」
document.getElementById('wsStatus').textContent = `狀態:連線與驗證中...`;
document.getElementById('wsStatus').style.color = '#f39c12'; // 橘色
document.getElementById('btnConnect').style.display = 'none';
document.getElementById('btnDisconnect').style.display = 'inline-block';
document.getElementById('cmtsHost').disabled = true;
document.getElementById('cmtsUser').disabled = true;
document.getElementById('cmtsPass').disabled = true;
term.focus();
// 🌟 原有:連線成功後,自動觸發一次查詢任務的 change 事件,讓背景預載 Datalist
const queryTaskSelect = document.getElementById('queryTask');
if (queryTaskSelect) {
queryTaskSelect.dispatchEvent(new Event('change'));
}
// 🌟 新增:連線成功後,強制觸發配置任務切換,藉此啟動「正確 IP」的 SSE 監聽
const configTaskSelect = document.getElementById('configTask');
if (configTaskSelect) {
configTaskSelect.dispatchEvent(new Event('change'));
}
};
ws.onmessage = (event) => {
// 🌟 UX 升級:只要收到設備傳來的第一個字元,就代表 SSH 驗證成功,正式轉為綠色「已連線」
const statusEl = document.getElementById('wsStatus');
if (statusEl.textContent.includes('驗證中')) {
statusEl.textContent = `狀態:已連線`;
statusEl.style.color = '#27ae60'; // 綠色
}
term.write(colorizeTerminalStream(event.data));
};
ws.onclose = () => {
isConnected = false;
const btnLoadTask = document.getElementById('btn-load-task');
if (btnLoadTask) {
btnLoadTask.disabled = true;
btnLoadTask.style.backgroundColor = '#95a5a6';
btnLoadTask.style.cursor = 'not-allowed';
}
document.getElementById('wsStatus').textContent = '狀態:已斷線';
document.getElementById('wsStatus').style.color = '#c0392b';
document.getElementById('btnConnect').style.display = 'inline-block';
document.getElementById('btnDisconnect').style.display = 'none';
document.getElementById('cmtsHost').disabled = false;
document.getElementById('cmtsUser').disabled = false;
document.getElementById('cmtsPass').disabled = false;
term.writeln('\r\n\x1b[31m--- Connection Closed ---\x1b[0m\r\n');
// 🌟 關鍵修復:攔截後端傳來的 4001 錯誤碼,彈出明確的警告視窗
if (event.code === 4001) {
alert(`❌ 連線失敗!\n請檢查目標設備 IP、帳號與密碼是否正確。\n\n系統訊息: ${event.reason}`);
}
// 🌟 新增斷線時強制觸發配置任務切換藉此「關閉」SSE 監聽
const configTaskSelect = document.getElementById('configTask');
if (configTaskSelect) {
configTaskSelect.dispatchEvent(new Event('change'));
}
if (resetCallback) resetCallback();
};
}
export function disconnectWebSocket() { if (ws) ws.close(); }
export function injectCommand() {
if (!ws || ws.readyState !== WebSocket.OPEN) return alert("請先點擊「連線至 CMTS」");
const cmd = document.getElementById('fixedCmd').value;
ws.send(cmd + '\r');
term.focus();
}
export function getTerminalText() {
if (!term) return '';
try {
if (term.hasSelection()) return term.getSelection();
const buffer = term.buffer.active;
let text = '';
for (let i = 0; i < buffer.length; i++) {
const line = buffer.getLine(i);
if (line) text += line.translateToString(true) + '\n';
}
return text;
} catch (err) {
console.error("讀取終端機文字失敗:", err);
return '';
}
}
export function downloadTerminal() {
const text = getTerminalText();
if (!text.trim()) return alert("沒有內容可下載!");
try {
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
a.download = `cmts_terminal_log_${timestamp}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (err) {
console.error("下載失敗:", err);
}
}