feat: 1.工具維護者 (System Admin) 機制設計, 新增 URL 隱藏密鑰與彩蛋解鎖法。 2.使用者介面微調整。
This commit is contained in:
parent
3fea4a972a
commit
de48fb1790
15
index.html
15
index.html
|
|
@ -13,7 +13,8 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h1>🎙️ Harmonic CMTS Admin</h1>
|
<!-- 1. 標題加上 id="app-title" -->
|
||||||
|
<h1 id="app-title" style="cursor: default;">🎙️ Harmonic CMTS Admin</h1>
|
||||||
|
|
||||||
<div class="global-settings">
|
<div class="global-settings">
|
||||||
<label><strong>🌐 目標設備:</strong></label>
|
<label><strong>🌐 目標設備:</strong></label>
|
||||||
|
|
@ -157,7 +158,7 @@
|
||||||
<div id="config-tab" class="tab-content" style="overflow-y: auto; background-color: #fdfefe;">
|
<div id="config-tab" class="tab-content" style="overflow-y: auto; background-color: #fdfefe;">
|
||||||
<div class="manager-container">
|
<div class="manager-container">
|
||||||
|
|
||||||
<div class="control-group" style="background: #ffffff; padding: 15px 25px; border-radius: 8px; margin-bottom: 0; box-shadow: 0 2px 4px rgba(0,0,0,0.02); border: 1px solid #e2e8f0;">
|
<div class="control-group" style="background: #ffffff; padding: 10px 15px; border-radius: 8px; margin-bottom: 0; box-shadow: 0 2px 4px rgba(0,0,0,0.02); border: 1px solid #e2e8f0;">
|
||||||
<label style="font-weight: bold; color: #c0392b; font-size: 16px; margin-right: 10px;">🛠️ 選擇配置任務:</label>
|
<label style="font-weight: bold; color: #c0392b; font-size: 16px; margin-right: 10px;">🛠️ 選擇配置任務:</label>
|
||||||
<select id="configTask" onchange="switchConfigTask()" style="width: 400px; max-width: 100%; font-weight: bold; font-size: 15px; padding: 8px; background-color: #fcf3f2; border-color: #fadbd8;">
|
<select id="configTask" onchange="switchConfigTask()" style="width: 400px; max-width: 100%; font-weight: bold; font-size: 15px; padding: 8px; background-color: #fcf3f2; border-color: #fadbd8;">
|
||||||
<option value="form-bonding-config">⚠️ MAC Domain 狀態感知配置精靈</option>
|
<option value="form-bonding-config">⚠️ MAC Domain 狀態感知配置精靈</option>
|
||||||
|
|
@ -250,18 +251,18 @@
|
||||||
|
|
||||||
<!-- 表單 4:完整設備配置樹狀圖 -->
|
<!-- 表單 4:完整設備配置樹狀圖 -->
|
||||||
<div id="form-full-config" class="task-form" style="display: none;">
|
<div id="form-full-config" class="task-form" style="display: none;">
|
||||||
<h3 style="display: flex; align-items: center; margin-bottom: 15px;">
|
<h3 style="display: flex; align-items: center; margin-top: 0; margin-bottom: 10px; font-size: 16px; color: #2c3e50;">
|
||||||
🌳 完整設備配置樹狀圖
|
🌳 完整設備配置樹狀圖
|
||||||
|
|
||||||
<!-- 🌟 一鍵掃描按鈕 -->
|
<!-- 🌟 掃描按鈕 -->
|
||||||
<button id="global-scan-btn" onclick="scanAllMissingOptions()" disabled
|
<button id="global-scan-btn" onclick="scanAllMissingOptions()" disabled
|
||||||
style="padding: 6px 12px; background-color: #8e44ad; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; margin-left: 20px; transition: all 0.3s; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
style="display: none; padding: 6px 12px; background-color: #8e44ad; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; margin-left: 20px; transition: all 0.3s; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||||
🔍 一鍵掃描缺失選項
|
🔍 掃描畫面缺失選項
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 🌟 請在這裡加入這段全新的按鈕 -->
|
<!-- 🌟 請在這裡加入這段全新的按鈕 -->
|
||||||
<button id="clearCacheBtn" onclick="clearVisibleCache()"
|
<button id="clearCacheBtn" onclick="clearVisibleCache()"
|
||||||
style="padding: 6px 12px; background-color: #f39c12; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; margin-left: 10px; transition: all 0.3s; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
style="display: none; padding: 6px 12px; background-color: #f39c12; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; margin-left: 10px; transition: all 0.3s; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||||
🔄 清除畫面選項快取
|
🔄 清除畫面選項快取
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -354,7 +354,7 @@ async function scanAllMissingOptions() {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.style.backgroundColor = "#8e44ad";
|
btn.style.backgroundColor = "#8e44ad";
|
||||||
btn.style.cursor = "pointer";
|
btn.style.cursor = "pointer";
|
||||||
btn.innerText = "🔍 一鍵掃描缺失選項";
|
btn.innerText = "🔍 掃描畫面缺失選項";
|
||||||
}
|
}
|
||||||
localStorage.removeItem('scanLockUntil');
|
localStorage.removeItem('scanLockUntil');
|
||||||
setTimeout(() => statusEl.textContent = "", 5000);
|
setTimeout(() => statusEl.textContent = "", 5000);
|
||||||
|
|
@ -368,7 +368,7 @@ async function scanAllMissingOptions() {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.style.backgroundColor = "#8e44ad";
|
btn.style.backgroundColor = "#8e44ad";
|
||||||
btn.style.cursor = "pointer";
|
btn.style.cursor = "pointer";
|
||||||
btn.innerText = "🔍 一鍵掃描缺失選項";
|
btn.innerText = "🔍 掃描畫面缺失選項";
|
||||||
}
|
}
|
||||||
localStorage.removeItem('scanLockUntil');
|
localStorage.removeItem('scanLockUntil');
|
||||||
}
|
}
|
||||||
|
|
@ -398,11 +398,11 @@ async function clearVisibleCache() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pathsToClear.length === 0) return alert("畫面上沒有展開的選項可以清除!\n請先展開您想重抓的資料夾。");
|
if (pathsToClear.length === 0) return alert("畫面上沒有展開的選項可以清除!\n請先展開您想重抓的資料夾。");
|
||||||
if (!confirm(`確定要清除畫面上 ${pathsToClear.length} 個選項的快取嗎?\n(清除後請重新點擊「一鍵掃描」)`)) return;
|
if (!confirm(`確定要清除畫面上 ${pathsToClear.length} 個選項的快取嗎?\n(清除後請重新點擊「掃描畫面缺失選項」)`)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await apiClearCache(pathsToClear);
|
const result = await apiClearCache(pathsToClear);
|
||||||
alert(`✅ 成功清除 ${result.cleared_count} 筆快取!\n現在請點擊旁邊的「一鍵掃描缺失選項」來重新擷取。`);
|
alert(`✅ 成功清除 ${result.cleared_count} 筆快取!\n現在請點擊旁邊的「掃描畫面缺失選項」來重新擷取。`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("清除快取失敗:", error);
|
console.error("清除快取失敗:", error);
|
||||||
alert("❌ 清除快取失敗,請檢查網路連線或後端 API 是否正確啟動。");
|
alert("❌ 清除快取失敗,請檢查網路連線或後端 API 是否正確啟動。");
|
||||||
|
|
@ -523,6 +523,48 @@ function closeModal(event) {
|
||||||
// 網頁載入時,檢查是否還有背景任務在跑 (維持掃描按鈕狀態)
|
// 網頁載入時,檢查是否還有背景任務在跑 (維持掃描按鈕狀態)
|
||||||
document.addEventListener("DOMContentLoaded", checkScanButtonState);
|
document.addEventListener("DOMContentLoaded", checkScanButtonState);
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 🛡️ 系統管理員雙重解鎖邏輯 (URL + 彩蛋)
|
||||||
|
// ==========================================
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const scanBtn = document.getElementById('global-scan-btn');
|
||||||
|
const clearBtn = document.getElementById('clearCacheBtn');
|
||||||
|
const appTitle = document.getElementById('app-title');
|
||||||
|
|
||||||
|
// 定義解鎖動作:把按鈕顯示出來
|
||||||
|
function unlockAdminFeatures() {
|
||||||
|
if (scanBtn) scanBtn.style.display = 'inline-block';
|
||||||
|
if (clearBtn) clearBtn.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方案一:URL 隱藏密鑰檢查 (?mode=godmode)
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if (urlParams.get('mode') === 'godmode') {
|
||||||
|
unlockAdminFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方案二:彩蛋解鎖 (對標題連點 5 次)
|
||||||
|
let clickCount = 0;
|
||||||
|
let clickTimer = null;
|
||||||
|
|
||||||
|
if (appTitle) {
|
||||||
|
appTitle.addEventListener('click', () => {
|
||||||
|
clickCount++;
|
||||||
|
|
||||||
|
if (clickCount === 5) {
|
||||||
|
unlockAdminFeatures();
|
||||||
|
alert('🔓 已解鎖系統維護者模式!');
|
||||||
|
clickCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(clickTimer);
|
||||||
|
clickTimer = setTimeout(() => {
|
||||||
|
clickCount = 0;
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function checkScanButtonState() {
|
function checkScanButtonState() {
|
||||||
const btn = document.getElementById('global-scan-btn');
|
const btn = document.getElementById('global-scan-btn');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,47 @@
|
||||||
/* --- static/style.css --- */
|
/* --- static/style.css --- */
|
||||||
|
|
||||||
/* 1. 基礎滿版佈局 */
|
/* 1. 基礎滿版佈局 (全域 80% 縮放感) */
|
||||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 20px; box-sizing: border-box; background-color: #f5f7fa; height: 100vh; display: flex; flex-direction: column; }
|
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 15px 20px; box-sizing: border-box; background-color: #f5f7fa; height: 100vh; display: flex; flex-direction: column; font-size: 14px; }
|
||||||
h1 { color: #2c3e50; margin-top: 0; flex-shrink: 0; margin-bottom: 15px; }
|
h1 { color: #2c3e50; margin-top: 0; flex-shrink: 0; margin-bottom: 12px; font-size: 24px; }
|
||||||
|
|
||||||
/* 2. 全域設備設定區塊 */
|
/* 2. 全域設備設定區塊 */
|
||||||
.global-settings { background-color: #e8ecef; padding: 12px 20px; border-radius: 6px; border: 1px solid #d1d8dd; margin-bottom: 15px; display: flex; gap: 10px; align-items: center; flex-shrink: 0; flex-wrap: wrap; }
|
.global-settings { background-color: #e8ecef; padding: 8px 15px; border-radius: 6px; border: 1px solid #d1d8dd; margin-bottom: 12px; display: flex; gap: 10px; align-items: center; flex-shrink: 0; flex-wrap: wrap; font-size: 13px; }
|
||||||
|
|
||||||
/* 3. 頁籤樣式 */
|
/* 3. 頁籤樣式 (🌟 修復 Hover 狀態,確保字體清晰) */
|
||||||
.tabs { display: flex; margin-bottom: 15px; border-bottom: 2px solid #ddd; flex-shrink: 0; }
|
.tabs { display: flex; margin-bottom: 12px; border-bottom: 2px solid #ddd; flex-shrink: 0; }
|
||||||
.tab-btn { padding: 10px 20px; cursor: pointer; background: none; border: none; font-size: 16px; font-weight: bold; color: #7f8c8d; }
|
.tab-btn { padding: 8px 18px; cursor: pointer; background: transparent; border: none; font-size: 15px; font-weight: bold; color: #7f8c8d; transition: all 0.2s ease; border-radius: 6px 6px 0 0; }
|
||||||
.tab-btn.active { color: #2980b9; border-bottom: 3px solid #2980b9; margin-bottom: -2px; }
|
.tab-btn:hover { color: #2c3e50; background-color: #e2e8f0; } /* 滑鼠移入時:深色字+淺灰底 */
|
||||||
|
.tab-btn.active { color: #2980b9; border-bottom: 3px solid #2980b9; margin-bottom: -2px; background-color: transparent; }
|
||||||
|
|
||||||
/* 4. 內容區塊 */
|
/* 4. 內容區塊 */
|
||||||
.tab-content { display: none; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); flex-grow: 1; min-height: 0; }
|
.tab-content { display: none; background: white; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); flex-grow: 1; min-height: 0; }
|
||||||
.tab-content.active { display: flex; flex-direction: column; }
|
.tab-content.active { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
/* 5. 控制列樣式 */
|
/* 5. 控制列樣式 (微縮元件) */
|
||||||
.control-group { margin-bottom: 15px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; flex-shrink: 0; }
|
.control-group { margin-bottom: 12px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; flex-shrink: 0; }
|
||||||
select, input[type="text"], input[type="password"] { padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
|
select, input[type="text"], input[type="password"] { padding: 6px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; }
|
||||||
button { padding: 8px 16px; background-color: #2980b9; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; transition: background-color 0.2s; }
|
button { padding: 6px 14px; background-color: #2980b9; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 13px; transition: background-color 0.2s; }
|
||||||
button:hover { background-color: #3498db; }
|
button:hover { background-color: #3498db; }
|
||||||
|
|
||||||
/* 6. 終端機與輸出區塊 */
|
/* 6. 終端機與輸出區塊 */
|
||||||
#terminal-container { flex-grow: 1; width: 100%; border-radius: 6px; background-color: #1e1e1e; box-shadow: inset 0 0 10px rgba(0,0,0,0.8); box-sizing: border-box; }
|
#terminal-container { flex-grow: 1; width: 100%; border-radius: 6px; background-color: #1e1e1e; box-shadow: inset 0 0 10px rgba(0,0,0,0.8); box-sizing: border-box; }
|
||||||
.xterm { padding: 10px; }
|
.xterm { padding: 10px; }
|
||||||
|
|
||||||
/* 7. 專業級表單排版 (全版面 + 靠左對齊) */
|
/* 7. 專業級表單排版 (🌟 縮小間距,解決空白過多) */
|
||||||
.manager-container { width: 100%; display: flex; flex-direction: column; gap: 20px; }
|
.manager-container { width: 100%; display: flex; flex-direction: column; gap: 12px; }
|
||||||
.task-form { display: none; background: #ffffff; padding: 25px; border-radius: 8px; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px rgba(0,0,0,0.02); }
|
.task-form { display: none; background: #ffffff; padding: 18px 20px; border-radius: 8px; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px rgba(0,0,0,0.02); }
|
||||||
.task-form.active { display: block; animation: fadeIn 0.3s ease-in-out; }
|
.task-form.active { display: block; animation: fadeIn 0.3s ease-in-out; }
|
||||||
|
|
||||||
/* 網格系統:調整為更緊湊的高空間利用率排版 */
|
/* 網格系統:高空間利用率 */
|
||||||
.form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 15px; margin-bottom: 20px; justify-content: start; }
|
.form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-bottom: 15px; justify-content: start; }
|
||||||
.form-row { display: flex; flex-direction: column; gap: 6px; }
|
.form-row { display: flex; flex-direction: column; gap: 4px; }
|
||||||
.form-row label { font-size: 13px; font-weight: bold; color: #34495e; }
|
.form-row label { font-size: 12px; font-weight: bold; color: #34495e; }
|
||||||
.form-row input[type="text"], .form-row select { width: 100%; box-sizing: border-box; padding: 8px; background-color: #f8f9fa; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 14px; }
|
.form-row input[type="text"], .form-row select { width: 100%; box-sizing: border-box; padding: 6px 8px; background-color: #f8f9fa; border: 1px solid #cbd5e1; border-radius: 4px; font-size: 13px; }
|
||||||
.form-row input[type="text"]:focus, .form-row select:focus { outline: none; border-color: #2980b9; background-color: #fff; }
|
.form-row input[type="text"]:focus, .form-row select:focus { outline: none; border-color: #2980b9; background-color: #fff; }
|
||||||
|
|
||||||
/* 表單底部操作區 */
|
/* 表單底部操作區 */
|
||||||
.form-actions { display: flex; justify-content: flex-start; align-items: center; gap: 20px; padding-top: 15px; border-top: 1px solid #ecf0f1; }
|
.form-actions { display: flex; justify-content: flex-start; align-items: center; gap: 15px; padding-top: 12px; border-top: 1px solid #ecf0f1; }
|
||||||
.form-actions button { padding: 10px 25px; font-size: 15px; min-width: 150px; }
|
.form-actions button { padding: 8px 20px; font-size: 14px; min-width: 120px; }
|
||||||
|
|
||||||
/* 8. 彈出式輸出視窗 (Modal) 樣式 */
|
/* 8. 彈出式輸出視窗 (Modal) 樣式 */
|
||||||
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000; align-items: center; justify-content: center; backdrop-filter: blur(3px); }
|
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000; align-items: center; justify-content: center; backdrop-filter: blur(3px); }
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,23 @@ export let isConnected = false;
|
||||||
|
|
||||||
// 3. 終端機文字上色 (內部使用,不需要 export)
|
// 3. 終端機文字上色 (內部使用,不需要 export)
|
||||||
function colorizeTerminalStream(text) {
|
function colorizeTerminalStream(text) {
|
||||||
if (text.includes('\x1b[')) return text;
|
|
||||||
let res = text;
|
let res = text;
|
||||||
res = res.replace(/\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g, '\x1b[1;34m$1\x1b[0m');
|
|
||||||
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[1;35m$1\x1b[0m');
|
// 1. IP 地址:改為「柔和冰藍色」 (移除 1; 粗體)
|
||||||
res = res.replace(/\b(online|w-online|p-online|init\([^)]+\)|up|active|success|yes)\b/gi, '\x1b[1;32m$1\x1b[0m');
|
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');
|
||||||
res = res.replace(/\b(offline|down|admin-down|error|fail|failed|shutdown|reject|invalid|no)\b/gi, '\x1b[1;31m$1\x1b[0m');
|
|
||||||
res = res.replace(/\b(Cable|GigabitEthernet|TenGigabitEthernet|Upstream|Downstream|Mac-domain|bonding-group|rpd)\b/gi, '\x1b[33m$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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"hidden_keys": [
|
"hidden_keys": [
|
||||||
"ssh"
|
"ssh",
|
||||||
|
"hostname",
|
||||||
|
"clock"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue