/* ── SenSu WebUI — Theme + Routing + Page Loader ── */ // ═══ 主题初始化 (必须在渲染前, 防闪白) ═══ (function(){ var s = localStorage.getItem("sensu-theme") || "dark"; document.documentElement.setAttribute("data-theme", s); })(); // ═══ 工具函数 ═══ function getCookie(name) { var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); return match ? match[2] : ''; } // ── 顶部版本检查 ── async function checkTopUpdate() { var btn = document.getElementById("top-ver-btn"); try { var r = await fetch("./api/updates"), d = await r.json(); var ver = d.current_version || d.latest_version || ""; btn.textContent = ver ? ver : "v?"; if (d.update_available) { btn.textContent = "↻ " + d.latest_version + " 可用"; btn.style.color = "var(--success)"; btn.title = "新版本可用! " + d.latest_version; } else if (ver) { btn.style.color = ""; btn.title = "已是最新 " + ver; } } catch(e) {} } setTimeout(checkTopUpdate, 3000); setInterval(checkTopUpdate, 600000); // 10min window.toggleTheme = function(){ var t = document.documentElement.getAttribute("data-theme") === "light" ? "dark" : "light"; document.documentElement.setAttribute("data-theme", t); localStorage.setItem("sensu-theme", t); var btn = document.querySelector(".theme-toggle"); if(btn) btn.textContent = t === "light" ? "☀️" : "🌙"; }; // ═══ 路由系统 ═══ // Hash format: #/dashboard | #/files | #/plugins/example_plugin // Built-in pages map to ./static/pages/{name}.html // Plugin pages load from /plugin/{name} var BUILTIN_PAGES = ['dashboard','logs','console','plugins','projects','proxy','files','settings']; var _pluginPagesCache = null; // {plugin_name: {path, title, icon}} function currentRoute() { var h = location.hash.replace('#', '') || '/dashboard'; return h.replace(/^\/+/, ''); } function navigateTo(route) { if (!route) route = 'dashboard'; location.hash = '#' + route; // triggers hashchange → routePage() } function routePage() { var r = currentRoute(); if (r.startsWith('plugins/')) { loadPluginPage(r.replace('plugins/', '')); } else if (BUILTIN_PAGES.indexOf(r) >= 0) { loadPage(r); } else { loadPage('dashboard'); location.hash = '#/dashboard'; } } // ═══ onload ═══ window.onload = async function() { var btn = document.querySelector(".theme-toggle"); if(btn) btn.textContent = document.documentElement.getAttribute("data-theme") === "light" ? "☀️" : "🌙"; try { var res = await fetch('./api/auth/status', {credentials:'include'}); if(res.status === 401 || !res.ok) { sessionStorage.setItem('sensu_redirect_hash', location.hash); window.location.href = './index.html'; return; } var data = await res.json(); if(!data.authenticated) { sessionStorage.setItem('sensu_redirect_hash', location.hash); window.location.href = './index.html'; return; } document.getElementById('uname').textContent = data.username || 'Admin'; } catch(e) { window.location.href = './index.html'; } window.addEventListener('hashchange', routePage); routePage(); // load from current hash (or default to dashboard) }; // ═══ Page loader (built-in pages) ═══ async function loadPage(pageName) { var content = document.getElementById('page-content'); var bar = document.getElementById('progress'); // Sidebar highlight document.querySelectorAll('.nav-item').forEach(function(el){ el.classList.remove('active'); }); document.querySelectorAll('.nav-sub-item').forEach(function(el){ el.classList.remove('active'); }); var navItem = document.querySelector('.nav-item[data-page="'+pageName+'"]'); if(navItem) navItem.classList.add('active'); content.style.padding = ''; // Progress bar.classList.add('active'); bar.style.width = '0%'; await new Promise(function(r){ requestAnimationFrame(function(){ bar.style.width='80%'; setTimeout(r,100); }); }); // Destroy previous module var modName = pageName.charAt(0).toUpperCase() + pageName.slice(1) + 'Module'; if(window[modName] && window[modName].destroy) { try { window[modName].destroy(); } catch(e){} } try { var resp = await fetch('./static/pages/'+pageName+'.html?t='+Date.now()); if(!resp.ok) throw new Error('404'); var html = await resp.text(); var scripts = []; var cleanHtml = html.replace(/]*>([\s\S]*?)<\/script>/gi, function(m,code){ scripts.push(code.trim()); return ''; }); content.innerHTML = cleanHtml; try { var s=document.createElement('script'); s.textContent=code; document.head.appendChild(s); document.head.removeChild(s); } catch(ex){ console.error('Inline script error:', ex); } }); // External JS module try { var jsResp = await fetch('./static/pages/'+pageName+'.js?t='+Date.now()); if(jsResp.ok) { var jsCode = await jsResp.text(); var s=document.createElement('script'); s.textContent=jsCode; document.head.appendChild(s); document.head.removeChild(s); if(window[modName] && window[modName].init) window[modName].init(); } } catch(e){ console.error('Page JS error:', pageName, e); } bar.style.width = '100%'; setTimeout(function(){ bar.classList.remove('active'); }, 200); } catch(e) { content.innerHTML = '
页面加载失败: '+e.message+'
'; bar.style.background='var(--error)'; setTimeout(function(){ bar.style.width='100%'; setTimeout(function(){ bar.classList.remove('active'); bar.style.background='var(--primary)'; },200); },100); } } // ═══ Plugin page loader ═══ async function loadPluginPage(pluginName) { var content = document.getElementById('page-content'); var bar = document.getElementById('progress'); // Highlight sub-item document.querySelectorAll('.nav-item').forEach(function(el){ el.classList.remove('active'); }); document.querySelectorAll('.nav-sub-item').forEach(function(el){ el.classList.remove('active'); }); var sub = document.querySelector('.nav-sub-item[data-plugin="'+pluginName+'"]'); if(sub) sub.classList.add('active'); bar.classList.add('active'); bar.style.width='0%'; await new Promise(function(r){ requestAnimationFrame(function(){ bar.style.width='80%'; setTimeout(r,100); }); }); // Also load plugin pages list if not yet if(!_pluginPagesCache) await fetchPluginPages(); try { var resp = await fetch('/plugin/'+pluginName+'?t='+Date.now(), {credentials:'include'}); if(!resp.ok) throw new Error(resp.status); var html = await resp.text(); var styles = ''; html.replace(/]*>([\s\S]*?)<\/style>/gi, function(m,code){ styles+=code; return ''; }); var bodyMatch = html.match(/]*>([\s\S]*)<\/body>/i); var clean = bodyMatch ? bodyMatch[1] : html.replace(/]*>|<\/html>|]*>[\s\S]*?<\/head>|]*>|<\/body>/gi, ''); var scripts = []; clean = clean.replace(/]*>([\s\S]*?)<\/script>/gi, function(m,code){ scripts.push(code.trim()); return ''; }); // Scope plugin styles: replace body selector with .plugin-page-root to avoid // styling the main page's element styles = styles.replace(/body\s*\{/gi, '.plugin-page-root{').replace(/body\b/gi, '.plugin-page-root'); content.style.padding = '0'; content.innerHTML = '
'+clean+'
'; try { var s=document.createElement('script'); s.textContent=code; document.head.appendChild(s); document.head.removeChild(s); } catch(ex){ console.error('Plugin script error:', ex); } }); bar.style.width='100%'; setTimeout(function(){ bar.classList.remove('active'); }, 200); } catch(ex) { content.innerHTML = '
插件页面加载失败: '+ex.message+'
'; bar.style.background='var(--error)'; setTimeout(function(){ bar.style.width='100%'; setTimeout(function(){ bar.classList.remove('active'); bar.style.background='var(--primary)'; },200); },100); } } // ═══ Global file/dir picker (in-page overlay, no iframe) ═══ window.pickPath = function(mode, callback) { var id = 'picker_' + Date.now(); // Create modal overlay var overlay = document.createElement('div'); overlay.id = 'fm-picker-overlay'; overlay.style.cssText = 'position:fixed;inset:0;z-index:500;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center'; var box = document.createElement('div'); box.style.cssText = 'background:var(--md-sys-color-surface-container-high);border-radius:var(--shape-lg);width:720px;height:520px;max-width:95vw;max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:var(--md-sys-elevation-5)'; // Header with close button var header = document.createElement('div'); header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid var(--outline);flex-shrink:0'; var headerTitle = document.createElement('span'); headerTitle.style.cssText = 'font-weight:500;color:var(--primary)'; headerTitle.textContent = '📂 选择' + (mode==='file'?'文件':'目录'); header.appendChild(headerTitle); var closeBtn = document.createElement('span'); closeBtn.style.cssText = 'cursor:pointer;font-size:20px;line-height:1;padding:4px 8px;border-radius:4px;color:var(--text-dim)'; closeBtn.textContent = '✕'; closeBtn.onclick = function(){ closePicker(); }; header.appendChild(closeBtn); box.appendChild(header); // Content area — load file manager via fetch var contentArea = document.createElement('div'); contentArea.style.cssText = 'flex:1;overflow:hidden;display:flex;flex-direction:column'; box.appendChild(contentArea); overlay.appendChild(box); document.body.appendChild(overlay); // Close on backdrop click overlay.addEventListener('click', function(e){ if(e.target===overlay) closePicker(); }); function closePicker() { if(overlay.parentNode) overlay.parentNode.removeChild(overlay); } // Load file manager HTML into content area fetch('./static/pages/files.html?t=' + Date.now()) .then(function(r){ return r.text(); }) .then(function(html){ // Strip meta and CSS link (already available in main page) html = html.replace(/]*>/gi, '').replace(/]*>/gi, ''); // Extract inline scripts var scripts = []; var clean = html.replace(/]*>([\s\S]*?)<\/script>/gi, function(m,code){ scripts.push(code.trim()); return ''; }); contentArea.innerHTML = clean; // Execute scripts try { var s=document.createElement('script'); s.textContent=code; document.head.appendChild(s); document.head.removeChild(s); } catch(ex){} }); // Load files.js if not already loaded // Fix container height for overlay var fmContainer = contentArea.querySelector('.fm-container'); if(fmContainer) fmContainer.style.height = '100%'; function doInit(){ window.FilesModule._apiBase = ''; window.FilesModule.currentPath = ''; window.FilesModule.pickerMode = mode || 'dir'; window.FilesModule.pickerCallback = id; window.FilesModule._scope = contentArea; window.FilesModule._getEl = function(sel){ return contentArea.querySelector(sel); }; // Override _pickResult to close overlay instead of alert window.FilesModule._pickResult = function(path){ closePicker(); if(callback) callback(path || window.FilesModule.currentPath || '/'); }; if(window.FilesModule.destroy) window.FilesModule.destroy(); if(window.FilesModule.init) window.FilesModule.init(); } // Override picker actions to close overlay and call callback window._fmPick = function(){ var p = window.FilesModule && window.FilesModule.currentPath || '/'; closePicker(); if(callback) callback(p); }; window._fmCancel = function(){ closePicker(); }; if(window.FilesModule){ doInit(); } else { var s=document.createElement('script'); s.src='./static/pages/files.js?v=0603'; s.onload=doInit; document.head.appendChild(s); } }) .catch(function(e){ contentArea.innerHTML = '
加载失败: '+e.message+'
'; }); }; // ═══ Sidebar toggle ═══ function toggleSidebar() { document.getElementById('app').classList.toggle('collapsed'); } // ═══ Logout ═══ async function doLogout() { await fetch('./api/logout', {method:'POST', credentials:'include'}); window.location.href = './index.html'; } // ═══ Plugin pages expand/collapse ═══ async function fetchPluginPages() { try { var res = await fetch('./api/plugin-pages', {credentials:'include'}); var data = await res.json(); _pluginPagesCache = {}; (data.pages||[]).forEach(function(p){ _pluginPagesCache[p.plugin] = p; }); return _pluginPagesCache; } catch(e) { return {}; } } async function togglePluginPages() { var body = document.getElementById('nav-plugins-body'); var arrow = document.getElementById('nav-plugins-arrow'); var isOpen = body.style.display !== 'none'; if(isOpen){ body.style.display='none'; arrow.style.transform=''; return; } body.style.display='block'; arrow.style.transform='rotate(180deg)'; if(!_pluginPagesCache) await fetchPluginPages(); var pages = _pluginPagesCache || {}; var names = Object.keys(pages); if(names.length) { var html = ''; names.forEach(function(name){ var p = pages[name]; html += ''; }); body.innerHTML = html; } } // ═══ Click delegation ═══ document.addEventListener('click', function(e) { // Built-in nav var nav = e.target.closest('.nav-item'); if(nav && nav.dataset.page) { navigateTo(nav.dataset.page); e.preventDefault(); return; } // Plugin sub-item var sub = e.target.closest('.nav-sub-item'); if(sub && sub.dataset.plugin) { navigateTo('plugins/' + sub.dataset.plugin); e.preventDefault(); } });