|
|
|
@@ -1,4 +1,6 @@
|
|
|
|
|
// 主题初始化 (必须在渲染前执行, 防止闪白)
|
|
|
|
|
/* ── SenSu WebUI — Theme + Routing + Page Loader ── */
|
|
|
|
|
|
|
|
|
|
// ═══ 主题初始化 (必须在渲染前, 防闪白) ═══
|
|
|
|
|
(function(){
|
|
|
|
|
var s = localStorage.getItem("sensu-theme") || "dark";
|
|
|
|
|
document.documentElement.setAttribute("data-theme", s);
|
|
|
|
@@ -12,97 +14,222 @@ window.toggleTheme = function(){
|
|
|
|
|
if(btn) btn.textContent = t === "light" ? "☀️" : "🌙";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 初始化检查
|
|
|
|
|
window.onload = async () => {
|
|
|
|
|
// 设置按钮初始图标
|
|
|
|
|
|
|
|
|
|
// ═══ 路由系统 ═══
|
|
|
|
|
// 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'];
|
|
|
|
|
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 {
|
|
|
|
|
const res = await fetch('./api/auth/status', { credentials: 'include' });
|
|
|
|
|
if(res.status === 401) { window.location.href = './index.html'; return; }
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
if(!data.authenticated) { window.location.href = './index.html'; return; }
|
|
|
|
|
|
|
|
|
|
document.getElementById('uname').textContent = data.username || 'Admin';
|
|
|
|
|
loadPage('dashboard'); // 默认加载
|
|
|
|
|
} catch(e) { window.location.href = './index.html'; }
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
const content = document.getElementById('page-content');
|
|
|
|
|
const bar = document.getElementById('progress');
|
|
|
|
|
|
|
|
|
|
// 侧边栏高亮
|
|
|
|
|
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
|
|
|
|
document.querySelector(`.nav-item[data-page="${pageName}"]`)?.classList.add('active');
|
|
|
|
|
|
|
|
|
|
// 进度条动画
|
|
|
|
|
bar.classList.add('active'); bar.style.width = '0%';
|
|
|
|
|
await new Promise(r => requestAnimationFrame(() => { bar.style.width = '80%'; setTimeout(r, 100); }));
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
if(!resp.ok) throw new Error('404');
|
|
|
|
|
var html = await resp.text();
|
|
|
|
|
|
|
|
|
|
var scripts = [];
|
|
|
|
|
var cleanHtml = html.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, function(m,code){ scripts.push(code.trim()); return ''; });
|
|
|
|
|
content.innerHTML = cleanHtml;
|
|
|
|
|
|
|
|
|
|
scripts.forEach(function(code){
|
|
|
|
|
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 {
|
|
|
|
|
const resp = await fetch(`./static/pages/${pageName}.html`);
|
|
|
|
|
if(!resp.ok) throw new Error('404');
|
|
|
|
|
const html = await resp.text();
|
|
|
|
|
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); }
|
|
|
|
|
|
|
|
|
|
// Extract inline scripts before innerHTML (browsers skip them)
|
|
|
|
|
const scripts = [];
|
|
|
|
|
const cleanHtml = html.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, (m, code) => {
|
|
|
|
|
scripts.push(code.trim()); return '';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
content.innerHTML = cleanHtml;
|
|
|
|
|
|
|
|
|
|
// Execute inline scripts via dynamic <script> tag so function
|
|
|
|
|
// declarations become globally accessible (onclick handlers need them)
|
|
|
|
|
for(const code of scripts) {
|
|
|
|
|
try {
|
|
|
|
|
const s = document.createElement('script');
|
|
|
|
|
s.textContent = code;
|
|
|
|
|
document.head.appendChild(s);
|
|
|
|
|
document.head.removeChild(s);
|
|
|
|
|
} catch(e) { console.error('Inline script error:', e); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load external JS module the same way (optional — skip if 404)
|
|
|
|
|
try {
|
|
|
|
|
const jsResp = await fetch(`./static/pages/${pageName}.js?t=${Date.now()}`);
|
|
|
|
|
if(jsResp.ok) {
|
|
|
|
|
const jsCode = await jsResp.text();
|
|
|
|
|
const s = document.createElement('script');
|
|
|
|
|
s.textContent = jsCode;
|
|
|
|
|
document.head.appendChild(s);
|
|
|
|
|
document.head.removeChild(s);
|
|
|
|
|
const moduleName = pageName.charAt(0).toUpperCase() + pageName.slice(1) + 'Module';
|
|
|
|
|
if(window[moduleName]?.init) window[moduleName].init();
|
|
|
|
|
}
|
|
|
|
|
} catch(e) { console.error('Page JS error:', pageName, e); }
|
|
|
|
|
|
|
|
|
|
bar.style.width = '100%';
|
|
|
|
|
setTimeout(() => bar.classList.remove('active'), 200);
|
|
|
|
|
} catch(e) {
|
|
|
|
|
content.innerHTML = `<div style="color:var(--error); text-align:center; margin-top:20vh;">页面加载失败: ${e.message}</div>`;
|
|
|
|
|
bar.style.background = 'var(--error)';
|
|
|
|
|
setTimeout(() => { bar.style.width = '100%'; setTimeout(() => { bar.classList.remove('active'); bar.style.background = 'var(--primary)'; }, 200); }, 100);
|
|
|
|
|
}
|
|
|
|
|
bar.style.width = '100%';
|
|
|
|
|
setTimeout(function(){ bar.classList.remove('active'); }, 200);
|
|
|
|
|
} catch(e) {
|
|
|
|
|
content.innerHTML = '<div style="color:var(--error);text-align:center;margin-top:20vh">页面加载失败: '+e.message+'</div>';
|
|
|
|
|
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, {credentials:'include'});
|
|
|
|
|
if(!resp.ok) throw new Error(resp.status);
|
|
|
|
|
var html = await resp.text();
|
|
|
|
|
|
|
|
|
|
var styles = '';
|
|
|
|
|
html.replace(/<style\b[^>]*>([\s\S]*?)<\/style>/gi, function(m,code){ styles+=code; return ''; });
|
|
|
|
|
|
|
|
|
|
var bodyMatch = html.match(/<body\b[^>]*>([\s\S]*)<\/body>/i);
|
|
|
|
|
var clean = bodyMatch ? bodyMatch[1] : html.replace(/<html[^>]*>|<\/html>|<head[^>]*>[\s\S]*?<\/head>|<body[^>]*>|<\/body>/gi, '');
|
|
|
|
|
|
|
|
|
|
var scripts = [];
|
|
|
|
|
clean = clean.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, function(m,code){ scripts.push(code.trim()); return ''; });
|
|
|
|
|
|
|
|
|
|
content.style.padding = '0';
|
|
|
|
|
content.innerHTML = '<style>'+styles+'</style>'+clean;
|
|
|
|
|
|
|
|
|
|
scripts.forEach(function(code){
|
|
|
|
|
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 = '<div style="color:var(--error);text-align:center;margin-top:20vh">插件页面加载失败: '+ex.message+'</div>';
|
|
|
|
|
bar.style.background='var(--error)';
|
|
|
|
|
setTimeout(function(){ bar.style.width='100%'; setTimeout(function(){ bar.classList.remove('active'); bar.style.background='var(--primary)'; },200); },100);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ═══ Sidebar toggle ═══
|
|
|
|
|
function toggleSidebar() {
|
|
|
|
|
document.getElementById('app').classList.toggle('collapsed');
|
|
|
|
|
document.getElementById('app').classList.toggle('collapsed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 退出登录
|
|
|
|
|
// ═══ Logout ═══
|
|
|
|
|
async function doLogout() {
|
|
|
|
|
await fetch('./api/logout', { method: 'POST', credentials: 'include' });
|
|
|
|
|
window.location.href = './index.html';
|
|
|
|
|
await fetch('./api/logout', {method:'POST', credentials:'include'});
|
|
|
|
|
window.location.href = './index.html';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 点击侧边栏事件委托
|
|
|
|
|
document.addEventListener('click', (e) => {
|
|
|
|
|
const nav = e.target.closest('.nav-item');
|
|
|
|
|
if(nav) { loadPage(nav.dataset.page); e.preventDefault(); }
|
|
|
|
|
|
|
|
|
|
// ═══ 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 += '<div class="nav-sub-item" data-plugin="'+p.plugin+'" data-path="'+p.path+'">'+
|
|
|
|
|
'<span class="nav-icon">'+(p.icon||'P')+'</span><span>'+p.title+'</span></div>';
|
|
|
|
|
});
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|