scm-harmonic-cmts-admin/static/edit-mode.js

1141 lines
51 KiB
JavaScript
Raw Permalink Normal View History

// --- static/edit-mode.js ---
import {
apiAcquireLock, apiReleaseLock, apiSendHeartbeat,
2026-05-14 08:30:10 +00:00
apiGetLeafOptions, apiSyncLeafOptions, apiGenerateCli, apiExecuteConfig,
2026-05-28 02:24:17 +00:00
apiSyncLeafOptionsStream
} from './api.js';
import { getGlobalConnectionInfo } from './utils.js';
// --- 安全跳脫 HTML 特殊字元,防止破版 ---
function escapeHTML(str) {
if (str === null || str === undefined) return "";
return String(str)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
// ==========================================
// 1. 全域狀態與鎖定管理 (Lock Management)
// ==========================================
export const SESSION_USER_ID = crypto.randomUUID ? crypto.randomUUID() : 'user-' + Math.random().toString(36).substr(2, 9);
const ACTIVE_HEARTBEATS = {};
// 🌟 新增:用來防禦「秒按取消」導致的非同步渲染競爭危害
const activeEditSessions = new Set();
export let currentEditPath = null;
export let currentEditElementId = null;
export let currentEditIsSuccess = false;
export let currentEditType = null;
2026-05-28 02:24:17 +00:00
export let currentEditDiffs = [];
export async function releaseAllLocks() {
const keysToRelease = Object.keys(ACTIVE_HEARTBEATS);
2026-06-08 09:01:55 +00:00
// 🌟 1. 檢查當前是否處於 God Mode 狀態
const wasGodModeUnlocked = sessionStorage.getItem('godModeUnlocked') === 'true';
// 🌟 2. 只要觸發閒置,立刻無條件清除 God Mode 權限
sessionStorage.removeItem('godModeUnlocked');
if (keysToRelease.length === 0) {
// 如果沒有正在編輯的節點,但剛剛是 God Mode依然要登出並重整
if (wasGodModeUnlocked) {
alert("您已閒置超過 5 分鐘,系統已自動為您登出進階維護者模式。");
location.reload();
}
return;
}
const releasePromises = keysToRelease.map(key => {
const [host, path] = key.split('@@');
clearInterval(ACTIVE_HEARTBEATS[key]);
delete ACTIVE_HEARTBEATS[key];
return apiReleaseLock(path, SESSION_USER_ID, host)
.catch(err => console.error(`釋放鎖定 ${path} 失敗:`, err));
});
await Promise.all(releasePromises);
2026-06-08 09:01:55 +00:00
alert("您已閒置超過 5 分鐘,系統已自動釋放編輯鎖定並登出進階模式。");
location.reload(); // 重整網頁,確保所有 UI 恢復未解鎖狀態
}
async function sendHeartbeat(path, host, intervalId, lockKey) {
try {
const response = await apiSendHeartbeat(path, SESSION_USER_ID, host);
if (!response.ok) {
console.warn(`心跳發送失敗 (${path}): 伺服器回傳 ${response.status}`);
if (response.status === 404) {
clearInterval(intervalId);
if (ACTIVE_HEARTBEATS[lockKey] === intervalId) {
delete ACTIVE_HEARTBEATS[lockKey];
}
}
}
} catch (error) {
console.error(`心跳請求發生錯誤 (${path}):`, error);
}
}
// ==========================================
// 2. 編輯模式啟動與取消 (Folder & Leaf)
// ==========================================
export async function startEditFolder(path, elementId) {
if (activeEditSessions.has(elementId)) return;
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
activeEditSessions.add(elementId);
const connInfo = getGlobalConnectionInfo();
const username = connInfo ? connInfo.user : 'admin';
2026-05-28 02:24:17 +00:00
const host = connInfo ? connInfo.host : '';
const lockKey = `${host}@@${path}`;
const editBtn = document.getElementById(`edit-btn-${elementId}`);
if (editBtn) {
editBtn.style.pointerEvents = 'none';
editBtn.innerText = "⏳";
}
try {
const result = await apiAcquireLock(path, SESSION_USER_ID, username, host);
if (!activeEditSessions.has(elementId)) {
if (result.status === 200 || (result.data && result.data.status === 'success')) {
if (!window.recentlyReleasedLocks) window.recentlyReleasedLocks = {};
window.recentlyReleasedLocks[`${host}@@${path}`] = Date.now();
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
apiReleaseLock(path, SESSION_USER_ID, host);
}
if (editBtn) {
editBtn.style.pointerEvents = 'auto';
editBtn.style.opacity = '0.3';
editBtn.title = "鎖定並編輯此項目";
editBtn.innerText = "✏️";
}
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
return;
}
if (result.status === 409) {
activeEditSessions.delete(elementId);
if (editBtn) {
editBtn.style.pointerEvents = 'auto';
editBtn.innerText = "✏️";
}
return alert(`⚠️ ${result.data.detail}`);
}
if (result.data.status === 'success') {
if (ACTIVE_HEARTBEATS[lockKey]) clearInterval(ACTIVE_HEARTBEATS[lockKey]);
let intervalId;
intervalId = setInterval(() => sendHeartbeat(path, host, intervalId, lockKey), 15000);
ACTIVE_HEARTBEATS[lockKey] = intervalId;
if (!window.globalActiveLocks) window.globalActiveLocks = {};
if (!window.globalActiveLocks[host]) window.globalActiveLocks[host] = {};
window.globalActiveLocks[host][path] = { user_id: SESSION_USER_ID, username: username };
if (window.recentlyReleasedLocks) {
delete window.recentlyReleasedLocks[`${host}@@${path}`];
}
const safePath = path.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
document.querySelectorAll(`.edit-btn[data-path="${safePath}"], .edit-btn[data-path^="${safePath}::"]`).forEach(btn => {
if (btn.id !== `edit-btn-${elementId}`) {
btn.style.pointerEvents = 'none';
btn.style.opacity = '0.2';
const btnPath = btn.getAttribute('data-path');
if (btnPath === path) {
btn.title = `🔒 您正在另一個視圖編輯此項目`;
} else {
btn.title = `🔒 父層級已被鎖定,無法編輯此項目`;
}
btn.innerText = "🔒";
}
});
if (editBtn) editBtn.style.display = 'none';
const actionBtn = document.getElementById(`actions-${elementId}`);
if (actionBtn) actionBtn.style.display = 'inline-block';
const detailsEl = document.getElementById(`details-${elementId}`);
if (detailsEl) detailsEl.open = true;
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
// ==========================================
// 🌟 完美修復:在抓取葉子節點前,強制將該資料夾底下的所有 HTML 預先渲染出來
// 這樣 querySelectorAll 就能一次抓到所有深層的欄位!
// ==========================================
if (typeof window.forceRenderFolderHTML === 'function') {
window.forceRenderFolderHTML(elementId);
}
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
const contentDiv = document.getElementById(`content-${elementId}`);
if (!contentDiv) return;
const leafContainers = contentDiv.querySelectorAll('.leaf-container');
let optData = {};
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
try { optData = await apiGetLeafOptions(host); } catch (e) {}
if (!activeEditSessions.has(elementId)) return;
const pathsToSync = [];
const containersArray = Array.from(leafContainers);
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
// 🌟 第一階段:盤點缺少的快取
containersArray.forEach(container => {
const rawPath = container.getAttribute('data-path');
const origVal = container.getAttribute('data-original');
let cacheKey = rawPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
if (rawPath.endsWith('::no')) cacheKey = cacheKey.replace(/ no$/, '') + ' ' + origVal;
if (!optData[cacheKey]) pathsToSync.push(cacheKey);
});
// 🌟 第二階段:如果有缺,觸發同步並「等待」
if (pathsToSync.length > 0) {
// 先讓所有欄位顯示載入中
containersArray.forEach(c => c.innerHTML = `<span style="font-size: 12px; color: #f39c12; font-weight: bold;">⏳ 載入選項中...</span>`);
apiSyncLeafOptions(host, pathsToSync);
for (let i = 0; i < 10; i++) {
if (!activeEditSessions.has(elementId)) return;
await new Promise(resolve => setTimeout(resolve, 1000));
optData = await apiGetLeafOptions(host);
// 檢查是否至少抓到一部分了 (只要有 hint 或 options 就當作抓到了)
const isMakingProgress = pathsToSync.some(p => optData[p] && (optData[p].hint || (optData[p].options && optData[p].options.length > 0)));
if (isMakingProgress && i > 2) break; // 給它至少 3 秒,有進度就放行,避免死等
}
}
if (!activeEditSessions.has(elementId)) return;
// 🌟 第三階段:正式渲染輸入框與 ❓
const CHUNK_SIZE = 50;
for (let i = 0; i < containersArray.length; i += CHUNK_SIZE) {
if (!activeEditSessions.has(elementId)) return;
const chunk = containersArray.slice(i, i + CHUNK_SIZE);
chunk.forEach(container => {
const origVal = container.getAttribute('data-original');
const rawPath = container.getAttribute('data-path');
const safeOrigVal = escapeHTML(origVal);
let cacheKey = rawPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
if (rawPath.endsWith('::no')) cacheKey = cacheKey.replace(/ no$/, '') + ' ' + origVal;
let leafCache = optData[cacheKey];
let inputHtml = "";
let hintAttr = "";
let hintIcon = "";
if (leafCache && leafCache.hint) {
const safeHint = leafCache.hint.replace(/"/g, '&quot;');
hintAttr = `title="${safeHint}"`;
hintIcon = `<span style="cursor: help; margin-left: 6px; color: #e67e22; font-size: 14px;" title="${safeHint}">❓</span>`;
}
if (leafCache && leafCache.options && leafCache.options.length > 0) {
let options = leafCache.options;
if (origVal && !options.includes(origVal)) options = [origVal, ...options];
inputHtml = `<select class="edit-input" data-path="${rawPath}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; height: 26px; outline: none; margin-left: 5px; background-color: white; cursor: pointer;">`;
options.forEach(opt => {
const safeOpt = escapeHTML(opt);
const isSelected = (opt === origVal) ? 'selected' : '';
inputHtml += `<option value="${safeOpt}" ${isSelected}>${safeOpt}</option>`;
});
inputHtml += `</select>${hintIcon}`;
} else {
inputHtml = `<input type="text" class="edit-input" data-path="${rawPath}" value="${safeOrigVal}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; outline: none; margin-left: 5px;">${hintIcon}`;
}
container.innerHTML = inputHtml;
});
await new Promise(resolve => setTimeout(resolve, 0));
}
}
} catch (error) {
activeEditSessions.delete(elementId);
if (editBtn) {
editBtn.style.pointerEvents = 'auto';
editBtn.innerText = "✏️";
}
alert("❌ 無法連線到鎖定伺服器:" + error.message);
}
}
export async function startEditLeaf(path, elementId) {
if (activeEditSessions.has(elementId)) return;
activeEditSessions.add(elementId);
const connInfo = getGlobalConnectionInfo();
const username = connInfo ? connInfo.user : 'admin';
const host = connInfo ? connInfo.host : '';
const lockKey = `${host}@@${path}`;
const editBtn = document.getElementById(`edit-btn-${elementId}`);
if (editBtn) {
editBtn.style.pointerEvents = 'none';
editBtn.innerText = "⏳";
}
try {
const result = await apiAcquireLock(path, SESSION_USER_ID, username, host);
if (!activeEditSessions.has(elementId)) {
if (result.status === 200 || (result.data && result.data.status === 'success')) {
if (!window.recentlyReleasedLocks) window.recentlyReleasedLocks = {};
window.recentlyReleasedLocks[`${host}@@${path}`] = Date.now();
apiReleaseLock(path, SESSION_USER_ID, host);
}
if (editBtn) {
editBtn.style.pointerEvents = 'auto';
editBtn.style.opacity = '0.3';
editBtn.title = "鎖定並編輯此項目";
editBtn.innerText = "✏️";
}
return;
}
if (result.status === 409) {
activeEditSessions.delete(elementId);
if (editBtn) {
editBtn.style.pointerEvents = 'auto';
editBtn.innerText = "✏️";
}
return alert(`⚠️ ${result.data.detail}`);
}
if (result.data.status === 'success') {
if (ACTIVE_HEARTBEATS[lockKey]) clearInterval(ACTIVE_HEARTBEATS[lockKey]);
let intervalId;
intervalId = setInterval(() => sendHeartbeat(path, host, intervalId, lockKey), 15000);
ACTIVE_HEARTBEATS[lockKey] = intervalId;
if (!window.globalActiveLocks) window.globalActiveLocks = {};
if (!window.globalActiveLocks[host]) window.globalActiveLocks[host] = {};
window.globalActiveLocks[host][path] = { user_id: SESSION_USER_ID, username: username };
if (window.recentlyReleasedLocks) {
delete window.recentlyReleasedLocks[`${host}@@${path}`];
}
const safePath = path.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
document.querySelectorAll(`.edit-btn[data-path="${safePath}"]`).forEach(btn => {
if (btn.id !== `edit-btn-${elementId}`) {
btn.style.pointerEvents = 'none';
btn.style.opacity = '0.2';
btn.title = `🔒 您正在另一個視圖編輯此項目`;
btn.innerText = "🔒";
}
});
if (editBtn) editBtn.style.display = 'none';
document.getElementById(`actions-${elementId}`).style.display = 'inline-block';
const container = document.getElementById(`container-${elementId}`);
const origVal = container.getAttribute('data-original');
const safeOrigVal = escapeHTML(origVal);
container.innerHTML = `<span style="font-size: 12px; color: #f39c12; margin-left: 5px; font-weight: bold;">⏳ 設備連線與載入選項中...</span>`;
try {
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
let optData = await apiGetLeafOptions(host);
let cacheKey = path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
if (path.endsWith('::no')) cacheKey = cacheKey.replace(/ no$/, '') + ' ' + origVal;
let leafCache = optData[cacheKey];
if (!leafCache) {
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
apiSyncLeafOptions(host, [cacheKey]);
let found = false;
for (let i = 0; i < 10; i++) {
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
if (!activeEditSessions.has(elementId)) return;
await new Promise(resolve => setTimeout(resolve, 1000));
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
optData = await apiGetLeafOptions(host);
leafCache = optData[cacheKey];
2026-06-08 09:01:55 +00:00
if (leafCache && (leafCache.hint || (leafCache.options && leafCache.options.length > 0))) {
found = true; break;
}
}
}
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
if (!activeEditSessions.has(elementId)) return;
let inputHtml = "";
let hintAttr = "";
let hintIcon = "";
if (leafCache && leafCache.hint) {
const safeHint = leafCache.hint.replace(/"/g, '&quot;');
hintAttr = `title="${safeHint}"`;
hintIcon = `<span style="cursor: help; margin-left: 6px; color: #e67e22; font-size: 14px;" title="${safeHint}">❓</span>`;
}
if (leafCache && leafCache.options && leafCache.options.length > 0) {
let options = leafCache.options;
if (origVal && !options.includes(origVal)) options = [origVal, ...options];
inputHtml = `<select class="edit-input" data-path="${path}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; height: 26px; outline: none; margin-left: 5px; background-color: white; cursor: pointer;">`;
options.forEach(opt => {
const safeOpt = escapeHTML(opt);
const isSelected = (opt === origVal) ? 'selected' : '';
inputHtml += `<option value="${safeOpt}" ${isSelected}>${safeOpt}</option>`;
});
inputHtml += `</select>${hintIcon}`;
} else {
inputHtml = `<input type="text" class="edit-input" data-path="${path}" value="${safeOrigVal}" ${hintAttr} style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; outline: none; margin-left: 5px;">${hintIcon}`;
}
container.innerHTML = inputHtml;
} catch (e) {
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
if (!activeEditSessions.has(elementId)) return;
container.innerHTML = `<input type="text" class="edit-input" data-path="${path}" value="${safeOrigVal}" style="padding: 2px 6px; border: 1px solid #3498db; border-radius: 3px; font-family: Courier New, monospace; font-size: 13px; width: 200px; outline: none; margin-left: 5px;">`;
}
}
} catch (error) {
activeEditSessions.delete(elementId);
if (editBtn) {
editBtn.style.pointerEvents = 'auto';
editBtn.innerText = "✏️";
}
alert("❌ 無法連線到鎖定伺服器:" + error.message);
}
}
export async function cancelEditFolder(path, elementId) {
activeEditSessions.delete(elementId); // 🚨 防護:註銷編輯狀態,強制中斷背景渲染迴圈
const connInfo = getGlobalConnectionInfo();
const host = connInfo ? connInfo.host : '';
const lockKey = `${host}@@${path}`;
if (ACTIVE_HEARTBEATS[lockKey]) {
clearInterval(ACTIVE_HEARTBEATS[lockKey]);
delete ACTIVE_HEARTBEATS[lockKey];
}
// 🌟 關鍵修復:寫入防閃爍冷卻表必須在 await 之前!防止 API 延遲期間被輪詢重新上鎖
if (!window.recentlyReleasedLocks) window.recentlyReleasedLocks = {};
window.recentlyReleasedLocks[`${host}@@${path}`] = Date.now();
try { await apiReleaseLock(path, SESSION_USER_ID, host); } catch (error) {}
// 🌟 立即解除全域鎖定狀態 (加入 Host 隔離)
if (window.globalActiveLocks && window.globalActiveLocks[host] && window.globalActiveLocks[host][path]) {
delete window.globalActiveLocks[host][path];
}
const safePath = path.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
// 🌟 擴大選擇器:連同所有子節點一起秒級解除鎖定
document.querySelectorAll(`.edit-btn[data-path="${safePath}"], .edit-btn[data-path^="${safePath}::"]`).forEach(btn => {
if (btn.id !== `edit-btn-${elementId}`) {
const btnPath = btn.getAttribute('data-path');
let stillLockedByOther = false;
// 確保解除鎖定時,不會誤解開被其他父節點鎖定的子節點
if (window.globalActiveLocks && window.globalActiveLocks[host]) {
const hostLocks = window.globalActiveLocks[host];
if (hostLocks[btnPath]) {
stillLockedByOther = true;
} else {
for (const lockedPath of Object.keys(hostLocks)) {
if (btnPath.startsWith(lockedPath + "::")) {
stillLockedByOther = true;
break;
}
}
}
}
if (!stillLockedByOther) {
btn.style.pointerEvents = 'auto';
btn.style.opacity = '0.3';
btn.title = "鎖定並編輯此項目";
btn.innerText = "✏️";
}
}
});
const editBtn = document.getElementById(`edit-btn-${elementId}`);
if (editBtn) {
editBtn.style.display = 'inline-block';
editBtn.style.pointerEvents = 'auto';
editBtn.style.opacity = '0.3';
editBtn.title = "鎖定並編輯此項目";
editBtn.innerText = "✏️"; // 🌟 確保沙漏被清掉
}
const actionBtns = document.getElementById(`actions-${elementId}`);
if (actionBtns) actionBtns.style.display = 'none';
const contentDiv = document.getElementById(`content-${elementId}`);
const leafContainers = contentDiv.querySelectorAll('.leaf-container');
leafContainers.forEach(container => {
const origVal = container.getAttribute('data-original');
const safeOrigVal = escapeHTML(origVal);
container.innerHTML = `<span class="leaf-value" style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace;">${safeOrigVal}</span>`;
});
// 🌟 聯動防呆機制:如果右側預覽視窗正在顯示這個節點的指令,連帶關閉它,防止無鎖寫入
if (currentEditElementId === elementId) {
document.querySelectorAll('.tree-view-instance').forEach(pane => {
pane.style.flex = '1';
pane.style.width = '100%';
pane.style.maxWidth = 'none';
});
const rightPane = document.getElementById('side-cli-preview');
if (rightPane) {
rightPane.style.flex = '1';
rightPane.style.maxWidth = '50%';
rightPane.style.width = '';
rightPane.style.display = 'none';
}
document.getElementById('drag-resizer').style.display = 'none';
currentEditPath = null;
currentEditElementId = null;
currentEditIsSuccess = false;
currentEditType = null;
}
}
export async function cancelEditLeaf(path, elementId) {
activeEditSessions.delete(elementId); // 🚨 防護:註銷編輯狀態,強制中斷背景渲染迴圈
const connInfo = getGlobalConnectionInfo();
const host = connInfo ? connInfo.host : '';
const lockKey = `${host}@@${path}`;
if (ACTIVE_HEARTBEATS[lockKey]) {
clearInterval(ACTIVE_HEARTBEATS[lockKey]);
delete ACTIVE_HEARTBEATS[lockKey];
}
// 🌟 關鍵修復:寫入防閃爍冷卻表必須在 await 之前!防止 API 延遲期間被輪詢重新上鎖
if (!window.recentlyReleasedLocks) window.recentlyReleasedLocks = {};
window.recentlyReleasedLocks[`${host}@@${path}`] = Date.now();
try { await apiReleaseLock(path, SESSION_USER_ID, host); } catch (error) {}
// 🌟 立即解除全域鎖定狀態 (加入 Host 隔離)
if (window.globalActiveLocks && window.globalActiveLocks[host] && window.globalActiveLocks[host][path]) {
delete window.globalActiveLocks[host][path];
}
const safePath = path.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
document.querySelectorAll(`.edit-btn[data-path="${safePath}"]`).forEach(btn => {
if (btn.id !== `edit-btn-${elementId}`) {
btn.style.pointerEvents = 'auto';
btn.style.opacity = '0.3';
btn.title = "鎖定並編輯此項目";
btn.innerText = "✏️";
}
});
const editBtn = document.getElementById(`edit-btn-${elementId}`);
if (editBtn) {
editBtn.style.display = 'inline-block';
editBtn.style.pointerEvents = 'auto';
editBtn.style.opacity = '0.3';
editBtn.title = "鎖定並編輯此項目";
editBtn.innerText = "✏️"; // 🌟 確保沙漏被清掉
}
const actionBtns = document.getElementById(`actions-${elementId}`);
if (actionBtns) actionBtns.style.display = 'none';
const container = document.getElementById(`container-${elementId}`);
const origVal = container.getAttribute('data-original');
const safeOrigVal = escapeHTML(origVal);
container.innerHTML = `<span class="leaf-value" style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace;">${safeOrigVal}</span>`;
// 🌟 聯動防呆機制:如果右側預覽視窗正在顯示這個節點的指令,連帶關閉它,防止無鎖寫入
if (currentEditElementId === elementId) {
document.querySelectorAll('.tree-view-instance').forEach(pane => {
pane.style.flex = '1';
pane.style.width = '100%';
pane.style.maxWidth = 'none';
});
const rightPane = document.getElementById('side-cli-preview');
if (rightPane) {
rightPane.style.flex = '1';
rightPane.style.maxWidth = '50%';
rightPane.style.width = '';
rightPane.style.display = 'none';
}
document.getElementById('drag-resizer').style.display = 'none';
currentEditPath = null;
currentEditElementId = null;
currentEditIsSuccess = false;
currentEditType = null;
}
}
// ==========================================
// 3. CLI 生成與預覽 (CLI Generation)
// ==========================================
function getInterfacesWithAdminState() {
const currentMode = document.getElementById('configTask').value === 'form-full-config' ? 'full' : 'running';
const activeContainer = document.getElementById(`tree-container-${currentMode}`);
if (!activeContainer) return [];
const nodes = activeContainer.querySelectorAll('.leaf-container');
const interfaces = new Set();
nodes.forEach(node => {
const path = node.getAttribute('data-path');
if (path && path.endsWith('::admin-state')) {
const interfacePath = path.replace('::admin-state', '').replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
interfaces.add(interfacePath);
}
});
return Array.from(interfaces);
}
export async function previewFolderCLI(path, elementId) {
currentEditPath = path;
currentEditElementId = elementId;
currentEditIsSuccess = false;
currentEditType = 'folder';
const contentDiv = document.getElementById(`content-${elementId}`);
if (!contentDiv) return;
const inputs = contentDiv.querySelectorAll('.edit-input');
const diffs = [];
inputs.forEach(input => {
const container = input.closest('.leaf-container');
if (!container) return;
const originalVal = container.getAttribute('data-original') || "";
const newVal = input.value.trim();
const nodePath = container.getAttribute('data-path');
if (originalVal !== newVal && nodePath) {
diffs.push({ path: nodePath, old_val: originalVal, new_val: newVal });
}
});
if (diffs.length === 0) return alert("沒有偵測到任何修改,無需生成指令!");
2026-05-28 02:24:17 +00:00
currentEditDiffs = diffs;
try {
const result = await apiGenerateCli(diffs, getInterfacesWithAdminState());
if (result.status === 'success') {
showCliPreviewModal(result.data);
} else {
alert("CLI 生成失敗: " + result.message);
}
} catch (error) {
console.error("Error generating CLI:", error);
alert("網路連線錯誤,無法生成 CLI");
}
}
export async function previewLeafCLI(path, elementId) {
currentEditPath = path;
currentEditElementId = elementId;
currentEditIsSuccess = false;
currentEditType = 'leaf';
const container = document.getElementById(`container-${elementId}`);
if (!container) return;
const input = container.querySelector('.edit-input');
if (!input) return;
const originalVal = container.getAttribute('data-original') || "";
const newVal = input.value.trim();
if (originalVal === newVal) return alert("沒有偵測到任何修改,無需生成指令!");
const diffs = [{ path: path, old_val: originalVal, new_val: newVal }];
2026-05-28 02:24:17 +00:00
currentEditDiffs = diffs;
try {
const result = await apiGenerateCli(diffs, getInterfacesWithAdminState());
if (result.status === 'success') {
showCliPreviewModal(result.data);
} else {
alert("CLI 生成失敗: " + result.message);
}
} catch (error) {
alert("網路連線錯誤,無法生成 CLI");
}
}
function showCliPreviewModal(cliScript) {
const leftPanes = document.querySelectorAll('.tree-view-instance');
const resizer = document.getElementById('drag-resizer');
const rightPane = document.getElementById('side-cli-preview');
document.getElementById('side-pane-title').innerHTML = '⚠️ 即將寫入的指令';
document.getElementById('side-pane-title').style.color = '#f1c40f';
document.getElementById('btn-side-cancel').style.display = 'inline-block';
document.getElementById('btn-side-confirm').style.display = 'inline-block';
document.getElementById('btn-side-close').style.display = 'none';
document.getElementById('btn-side-close').textContent = '關閉';
document.getElementById('side-cli-textarea').style.display = 'block';
document.getElementById('side-execution-result').style.display = 'none';
const textarea = document.getElementById('side-cli-textarea');
textarea.value = cliScript;
textarea.readOnly = false;
2026-05-28 02:24:17 +00:00
textarea.style.backgroundColor = '#1e1e1e';
textarea.style.color = '#ecf0f1';
// 🛡️ 安全修改:解除 Flexbox 均分限制,讓 JS 拖拉的 width 生效
leftPanes.forEach(pane => {
pane.style.flex = 'none';
pane.style.maxWidth = 'none';
pane.style.width = 'calc(50% - 11px)';
});
rightPane.style.flex = 'none';
rightPane.style.maxWidth = 'none';
rightPane.style.width = 'calc(50% - 11px)';
resizer.style.display = 'flex';
rightPane.style.display = 'block';
if (!window.isResizerInitialized) {
initResizer();
window.isResizerInitialized = true;
}
}
// ==========================================
// 4. 側邊欄與執行邏輯
// ==========================================
export async function hideSideCLI() {
// 🌟 完美修復:明確恢復 Flexbox 伸展能力與 100% 寬度
document.querySelectorAll('.tree-view-instance').forEach(pane => {
pane.style.flex = '1'; // 恢復 HTML 原本賦予的彈性伸展能力
pane.style.width = '100%'; // 強制撐滿整個父容器
pane.style.maxWidth = 'none';
});
const rightPane = document.getElementById('side-cli-preview');
if (rightPane) {
rightPane.style.flex = '1';
rightPane.style.maxWidth = '50%'; // 恢復 HTML 原本的 50% 限制
rightPane.style.width = '';
rightPane.style.display = 'none';
}
document.getElementById('drag-resizer').style.display = 'none';
// 以下為原本的取消編輯與還原邏輯,完全保持不變,確保功能安全
if (currentEditIsSuccess && currentEditElementId && currentEditPath) {
const wrapper = document.getElementById(`display-wrapper-${currentEditElementId}`);
if (wrapper) {
const container = document.getElementById(`container-${currentEditElementId}`);
const input = container ? container.querySelector('.edit-input') : null;
const newCommand = input ? input.value.trim() : (container ? container.getAttribute('data-original') : '');
if (currentEditType === 'folder') await cancelEditFolder(currentEditPath, currentEditElementId);
else await cancelEditLeaf(currentEditPath, currentEditElementId);
transformNoToActive(currentEditElementId, newCommand);
} else {
if (currentEditType === 'folder') await applyEditFolder(currentEditPath, currentEditElementId);
else if (currentEditType === 'leaf') await applyEditLeaf(currentEditPath, currentEditElementId);
}
}
currentEditPath = null;
currentEditElementId = null;
currentEditIsSuccess = false;
currentEditType = null;
}
async function applyEditFolder(path, elementId) {
const contentDiv = document.getElementById(`content-${elementId}`);
if (contentDiv) {
const inputs = contentDiv.querySelectorAll('.edit-input');
inputs.forEach(input => {
const container = input.closest('.leaf-container');
if (container) container.setAttribute('data-original', input.value.trim());
});
}
await cancelEditFolder(path, elementId);
}
async function applyEditLeaf(path, elementId) {
const container = document.getElementById(`container-${elementId}`);
if (container) {
const input = container.querySelector('.edit-input');
if (input) container.setAttribute('data-original', input.value.trim());
}
await cancelEditLeaf(path, elementId);
}
export function executeSideCLI() {
const finalScript = document.getElementById('side-cli-textarea').value;
document.getElementById('side-pane-title').innerHTML = '⏳ 正在寫入設備...';
document.getElementById('side-pane-title').style.color = '#f39c12'; // 🌟 統一載入中狀態為橘色 (原為 #3498db)
document.getElementById('btn-side-cancel').style.display = 'none';
document.getElementById('btn-side-confirm').style.display = 'none';
document.getElementById('btn-side-close').style.display = 'none';
document.getElementById('side-cli-textarea').style.display = 'none';
const resultBox = document.getElementById('side-execution-result');
resultBox.style.display = 'block';
resultBox.innerHTML = `<span style="color: #ecf0f1;">${finalScript}</span>`;
executeGeneratedCLI(finalScript);
}
async function executeGeneratedCLI(script) {
const connInfo = getGlobalConnectionInfo();
if (!connInfo) return;
const outputEl = document.getElementById('side-execution-result');
// 1. 建立即時 Log 視窗 UI (無邊界滿版融合風格)
outputEl.innerHTML = `
<div id="edit-log-container" style="width: 100%; height: 100%; background: transparent; border: none; padding: 0; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; color: #ecf0f1; font-size: 14px; line-height: 1.5; text-align: left; margin: 0; box-sizing: border-box;">
<div style="color: #f39c12; font-weight: bold; margin-bottom: 8px;">[系統] 正在透過 SSH 寫入指令請勿關閉視窗...</div>
<div style="color: #7f8c8d;">[系統] 準備連線至設備 ${connInfo.host}...</div>
</div>
`;
const logContainer = document.getElementById('edit-log-container');
// 🌟 修正 1: 捲動目標必須是真正有捲軸的容器
const scrollTarget = document.getElementById('side-execution-result');
try {
const response = await fetch('/api/v1/cmts-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
script: script,
host: connInfo.host,
username: connInfo.user,
password: connInfo.pass
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
let isSuccess = false;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop();
for (let line of lines) {
if (!line.trim()) continue;
try {
const data = JSON.parse(line);
const timeStr = new Date().toLocaleTimeString('en-US', { hour12: false });
if (data.status === 'progress') {
logContainer.innerHTML += `<div style="margin-top: 4px;"><span style="color: #3498db;">[${timeStr}]</span> ${data.message}</div>`;
} else if (data.status === 'success') {
isSuccess = true;
logContainer.innerHTML += `
<div style="margin-top: 15px; color: #2ecc71; font-weight: bold; font-size: 14px;">[${timeStr}] ${data.message}</div>
`;
document.getElementById('side-pane-title').innerHTML = '✅ 寫入成功!正在同步快取...';
document.getElementById('side-pane-title').style.color = '#f39c12';
} else if (data.status === 'error') {
logContainer.innerHTML += `<div style="margin-top: 10px; color: #e74c3c; font-weight: bold;">[${timeStr}] ${data.message}</div>`;
if (data.output) {
logContainer.innerHTML += `<div style="color: #c0392b; margin-left: 10px; border-left: 2px solid #c0392b; padding-left: 8px;">設備回傳: ${data.output}</div>`;
}
document.getElementById('btn-side-close').style.display = 'inline-block';
document.getElementById('side-pane-title').innerHTML = '❌ 執行失敗';
document.getElementById('side-pane-title').style.color = '#ff6b6b';
}
// 自動捲動到最底部
if (scrollTarget) scrollTarget.scrollTop = scrollTarget.scrollHeight;
} catch (e) {
console.warn("解析串流 JSON 失敗:", line);
}
}
}
// 4. 串流結束後,執行快取同步
if (isSuccess) {
currentEditIsSuccess = true;
2026-05-14 08:30:10 +00:00
const pathsToSync = currentEditDiffs.map(d => {
let cacheKey = d.path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
if (d.path.endsWith('::no')) cacheKey = cacheKey.replace(/ no$/, '') + ' ' + d.old_val;
return cacheKey;
});
const uniquePaths = [...new Set(pathsToSync)];
try {
const currentMode = document.getElementById('configTask').value === 'form-full-config' ? 'full' : 'running';
let isSyncDone = false;
2026-05-28 02:24:17 +00:00
// 🌟 修正 2: 先啟動 SSE 監聽 (避免錯過 done 事件)
const streamPromise = apiSyncLeafOptionsStream(connInfo.host, uniquePaths, (data) => {
const titleEl = document.getElementById('side-pane-title');
2026-05-28 02:24:17 +00:00
if (data.event === 'done' || data.status === 'done') {
isSyncDone = true;
if (titleEl) {
titleEl.innerHTML = '✅ 寫入成功!快取同步完成';
titleEl.style.color = '#2ecc71';
}
document.getElementById('btn-side-close').style.display = 'inline-block';
logContainer.innerHTML += `<br><span style="color: #2ecc71; font-weight: bold;">[系統] 快取同步完成!您可以關閉此面板。</span>`;
if (scrollTarget) scrollTarget.scrollTop = scrollTarget.scrollHeight;
} else if (data.event === 'error' || data.status === 'error') {
isSyncDone = true;
if (titleEl) {
titleEl.innerHTML = '⚠️ 寫入成功 (但同步快取失敗)';
titleEl.style.color = '#e74c3c';
}
document.getElementById('btn-side-close').style.display = 'inline-block';
logContainer.innerHTML += `<br><span style="color: #e74c3c; font-weight: bold;">[系統] 同步失敗: ${data.message || '未知錯誤'}</span>`;
if (scrollTarget) scrollTarget.scrollTop = scrollTarget.scrollHeight;
2026-05-14 08:30:10 +00:00
}
feat(core): 系統底層架構重構、資安強化與極致效能優化 本次提交涵蓋了 Phase 3.5 至 Phase 4 的核心架構翻修,徹底解決了 SSH 互動陷阱、資料庫冗餘、機密外洩以及巨量 DOM 渲染的效能瓶頸。 🏗️ 1. 資料庫與快取架構整併 (DB Consolidation) - 重構 `cmts_options` 資料表,主鍵簡化為 `(host, path)`,徹底解耦 `config_type`。 - 實現 `running-tree` 與 `full-tree` 共用全域選項字典,達成極致的「增量掃描 (Incremental Scanning)」。 - 強化 `init_db.py` 防呆機制,保留 `config_backups` 避免誤刪珍貴快照。 🔒 2. 企業級資安與權限管理 (Security Hardening) - 導入 `python-dotenv`,全面落實 12-Factor App 規範。 - 徹底淨化 `index.html`, `shared.py`, `database.py` 中的硬編碼 (Hardcoded) 密碼。 - 建立 `.env.example` 範例檔,確保正式機密碼「零外洩 (Zero Secrets Leakage)」。 - 強化 God Mode 驗證邏輯,未設定環境變數時預設拒絕所有進階授權 (Fail-Fast)。 📡 3. SSH 引擎與 SSE 廣播機制 (SSH & SSE Engine) - 修復 SSH Echo 與 MOTD 陷阱,導入精準的 `prompt_pattern` 絕對靜音偵測。 - 廢除危險的 `Ctrl+C`,改用底層 `Ctrl+U (\x15)` 瞬間清空輸入緩衝區,確保 100% 停留在 config 模式。 - 完善 SSE (Server-Sent Events) 廣播機制,前端精準呈現「百分比進度條」與「動態狀態文字」。 ⚡ 4. 前端效能與使用者體驗 (Performance & UX) - 升級 Regex 解析器,完美支援水平列表與無標題說明,成功找回消失的 `❓` 提示。 - 導入 HTML5 `<datalist>`,將 Enum 欄位無痛升級為「半自由下拉選單 (Combobox)」。 - 解決「延遲載入 (Lazy Load)」與「編輯模式」的衝突:實作「預先渲染但不展開」機制,結合 `content-visibility: auto` 達成零效能損耗。 - 實作 Xterm.js DOM 事件防火牆 (Capture Phase Interception),完美阻擋第三方外掛產生的殘缺鍵盤事件,消除 Console 紅字報錯。 ⚠️ Breaking Changes (破壞性變更) - 系統啟動前必須建立 `.env` 檔案並填入必要的資料庫與設備連線資訊。 - 資料庫結構已變更,舊有環境需執行 `python init_db.py --reset` 重新建表。
2026-06-10 06:37:16 +00:00
});
2026-05-28 02:24:17 +00:00
// 🌟 修正 3: 再觸發後端同步任務
apiSyncLeafOptions(connInfo.host, uniquePaths, currentMode, connInfo.user, connInfo.pass);
// 🌟 修正 4: 加入 8 秒防呆機制,如果後端默默做完沒通知,自動放行
setTimeout(() => {
if (!isSyncDone) {
const titleEl = document.getElementById('side-pane-title');
if (titleEl) {
titleEl.innerHTML = '✅ 寫入成功!(快取同步已於背景完成)';
titleEl.style.color = '#2ecc71';
}
document.getElementById('btn-side-close').style.display = 'inline-block';
logContainer.innerHTML += `<br><span style="color: #f39c12; font-weight: bold;">[系統] 快取同步已於背景執行,您可以關閉此面板。</span>`;
if (scrollTarget) scrollTarget.scrollTop = scrollTarget.scrollHeight;
}
}, 8000);
await streamPromise;
2026-05-14 08:30:10 +00:00
} catch (syncErr) {
2026-05-28 02:24:17 +00:00
console.error("同步設定失敗:", syncErr);
2026-05-14 08:30:10 +00:00
document.getElementById('btn-side-close').style.display = 'inline-block';
}
} else {
currentEditIsSuccess = false;
}
} catch (error) {
document.getElementById('btn-side-close').style.display = 'inline-block';
document.getElementById('side-pane-title').innerHTML = '❌ 連線錯誤';
document.getElementById('side-pane-title').style.color = '#ff6b6b';
logContainer.innerHTML += `<div style="color: #ff6b6b; font-weight: bold; margin-top: 10px;">連線或伺服器錯誤: ${error}</div>`;
}
}
// ==========================================
// 5. 輔助 UI 函數
// ==========================================
function initResizer() {
const resizer = document.getElementById('drag-resizer');
const leftPanes = document.querySelectorAll('.tree-view-instance');
const rightPane = document.getElementById('side-cli-preview');
const container = document.getElementById('split-container');
let isResizing = false;
resizer.addEventListener('mousedown', (e) => {
isResizing = true;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const containerRect = container.getBoundingClientRect();
let newLeftWidth = e.clientX - containerRect.left;
const minWidth = containerRect.width * 0.2;
const maxWidth = containerRect.width * 0.8;
if (newLeftWidth < minWidth) newLeftWidth = minWidth;
if (newLeftWidth > maxWidth) newLeftWidth = maxWidth;
const leftPercentage = (newLeftWidth / containerRect.width) * 100;
const rightPercentage = 100 - leftPercentage;
leftPanes.forEach(pane => pane.style.width = `calc(${leftPercentage}% - 11px)`);
rightPane.style.width = `calc(${rightPercentage}% - 11px)`;
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = 'default';
document.body.style.userSelect = 'auto';
}
});
}
function highlightTerminalOutput(text) {
if (!text) return '';
const lines = text.split('\n');
const formattedLines = lines.map(line => {
if (line.match(/(Aborted|Error|Warning|Failed|% Invalid|% Unknown|Bad command)/i)) {
return `<span style="color: #ff6b6b; font-weight: bold;">${line}</span>`;
}
return line;
});
return formattedLines.join('\n');
}
function transformNoToActive(elementId, newCommand) {
const wrapper = document.getElementById(`display-wrapper-${elementId}`);
if (!wrapper) return;
const parts = newCommand.trim().split(' ');
const firstWord = parts[0];
const restCommand = parts.slice(1).join(' ');
const safeRestCommand = escapeHTML(restCommand);
const svgEnabled = `<svg width="16" height="16" viewBox="0 0 24 24" fill="#27ae60"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zM8.29 11.59L7 12.88l4 4 8-8-1.29-1.29L11 14.18z"/></svg>`;
const iconHtml = `<span style="margin-right: 6px; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px;">${svgEnabled}</span>`;
wrapper.innerHTML = `
${iconHtml}
<b style="color: #2c3e50;">${firstWord}</b>
<span class="leaf-container" style="display: inline-flex; align-items: center; min-height: 24px;">
<span class="leaf-value" style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace; display: inline-block; transform: translateY(1.5px);">${safeRestCommand}</span>
<span style="font-size: 0.8em; color: #f39c12; margin-left: 10px; border: 1px solid #f39c12; padding: 2px 6px; border-radius: 4px; background-color: #fffdf7;">(已啟用重整後歸檔)</span>
</span>
`;
const actionBtns = document.getElementById(`actions-${elementId}`);
const editBtn = document.getElementById(`edit-btn-${elementId}`);
if (actionBtns) actionBtns.style.display = 'none';
if (editBtn) editBtn.style.display = 'none';
}
2026-05-15 09:25:09 +00:00
// ============================================================================
// 💻 開發者工具:預覽節點完整指令 (唯讀 Debug 模式)
// ============================================================================
export async function previewNodeCLI(btnElement, rootPath) {
2026-05-15 09:25:09 +00:00
let container = btnElement.closest('details');
if (container) {
const elementId = container.id.replace('details-', '');
if (container.dataset.loaded !== 'true') {
await window.lazyLoadFolder(elementId, false);
}
2026-05-15 09:25:09 +00:00
}
const targetNode = container || btnElement.closest('.tree-leaf-node');
if (!targetNode) return;
2026-05-15 09:25:09 +00:00
const leafNodes = targetNode.querySelectorAll('.leaf-container');
const interfaceGroups = {};
2026-05-15 09:25:09 +00:00
leafNodes.forEach(node => {
const path = node.getAttribute('data-path') || "";
const val = node.getAttribute('data-original');
if (!path) return;
const parts = path.split('::');
const paramName = parts.pop();
let interfacePath = parts.join(' ').replace(/\s*\[\d+\]/g, '');
if (!interfaceGroups[interfacePath]) {
interfaceGroups[interfacePath] = [];
}
interfaceGroups[interfacePath].push({ param: paramName, val: val });
});
if (Object.keys(interfaceGroups).length === 0 && targetNode.classList.contains('tree-leaf-node')) {
const singleNode = targetNode.querySelector('.leaf-container');
if (singleNode) {
const path = singleNode.getAttribute('data-path');
const val = singleNode.getAttribute('data-original');
const parts = path.split('::');
const paramName = parts.pop();
let interfacePath = parts.join(' ').replace(/\s*\[\d+\]/g, '');
interfaceGroups[interfacePath] = [{ param: paramName, val: val }];
}
}
if (Object.keys(interfaceGroups).length === 0) {
return alert("此節點下沒有任何數值可供預覽!");
}
let cliLines = [];
cliLines.push("! --- Read-Only Configuration Preview ---");
for (const [interfacePath, params] of Object.entries(interfaceGroups)) {
cliLines.push(`! Node: ${interfacePath || 'Global'}`);
params.forEach(({param, val}) => {
if (/^\[\d+\]$/.test(param)) {
if (val) {
cliLines.push(interfacePath ? `${interfacePath} ${val}` : val);
}
} else if (param === 'no') {
cliLines.push(interfacePath ? `${interfacePath} no ${val}` : `no ${val}`);
} else {
let line = interfacePath ? `${interfacePath} ${param}` : param;
if (val) line += ` ${val}`;
cliLines.push(line);
2026-05-15 09:25:09 +00:00
}
});
cliLines.push("!");
2026-05-15 09:25:09 +00:00
}
const finalScript = cliLines.join('\n');
showReadOnlyCliPreviewModal(finalScript, rootPath);
}
2026-05-15 09:25:09 +00:00
function showReadOnlyCliPreviewModal(cliScript, rootPath) {
const leftPanes = document.querySelectorAll('.tree-view-instance');
2026-05-15 09:25:09 +00:00
const resizer = document.getElementById('drag-resizer');
const rightPane = document.getElementById('side-cli-preview');
document.getElementById('side-pane-title').innerHTML = `🔍 [唯讀預覽] ${rootPath}`;
2026-05-15 09:25:09 +00:00
document.getElementById('side-pane-title').style.color = '#3498db';
2026-05-15 09:25:09 +00:00
document.getElementById('btn-side-confirm').style.display = 'none';
document.getElementById('btn-side-cancel').style.display = 'none';
const closeBtn = document.getElementById('btn-side-close');
closeBtn.style.display = 'inline-block';
closeBtn.textContent = '關閉預覽';
2026-05-15 09:25:09 +00:00
const textarea = document.getElementById('side-cli-textarea');
textarea.style.display = 'block';
textarea.value = cliScript;
2026-05-28 02:24:17 +00:00
textarea.readOnly = true;
textarea.style.backgroundColor = '#2c3e50';
2026-05-15 09:25:09 +00:00
document.getElementById('side-execution-result').style.display = 'none';
// 🛡️ 安全修改:解除 Flexbox 均分限制,讓 JS 拖拉的 width 生效
leftPanes.forEach(pane => {
pane.style.flex = 'none';
pane.style.maxWidth = 'none';
pane.style.width = 'calc(50% - 11px)';
});
if (rightPane) {
rightPane.style.flex = 'none';
rightPane.style.maxWidth = 'none';
rightPane.style.width = 'calc(50% - 11px)';
rightPane.style.display = 'block';
}
2026-05-15 09:25:09 +00:00
if (resizer) resizer.style.display = 'flex';
if (!window.isResizerInitialized && typeof initResizer === 'function') {
initResizer();
window.isResizerInitialized = true;
}
}
// --- EOF (檔案結束) ---