// --- 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 傳進來的重置函數 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'; } document.getElementById('wsStatus').textContent = `狀態:已連線`; document.getElementById('wsStatus').style.color = '#27ae60'; 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) => { 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'); // 🌟 新增:斷線時,強制觸發配置任務切換,藉此「關閉」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); } }