Initial commit: SenSu Alpha 0.2.0
- 13-service async plugin framework - Textual TUI with CLI fallback - Plugin hot-reload + permission system - Web management panel (aiohttp) - Bridge-based inter-module communication - 10 regression tests Fixes applied: - PBKDF2-SHA256 auth (was plain SHA256) - Auth bypass removed (was allow-all on fail) - Bare excepts replaced with logged errors - CatFramework/DreamSu -> SenSu naming unified - ServiceManager: health checks + startup_order - Env var credentials (SENSU_ADMIN_PASSWORD etc)
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
const BASE = '/panel';
|
||||
|
||||
export const api = {
|
||||
get: async (url) => {
|
||||
const res = await fetch(`${BASE}${url}`, { credentials: 'include' });
|
||||
return res.json();
|
||||
},
|
||||
post: async (url, data) => {
|
||||
const res = await fetch(`${BASE}${url}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
|
||||
export async function initAuth() {
|
||||
// 登录逻辑绑定到 DOM
|
||||
const loginForm = document.querySelector('#login-form');
|
||||
if(loginForm) {
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const u = document.getElementById('username').value;
|
||||
const p = document.getElementById('password').value;
|
||||
const res = await api.post('/api/login', { username: u, password: p });
|
||||
if (res.success) location.reload(); // 登录成功刷新
|
||||
});
|
||||
}
|
||||
return await api.get('/api/auth/status');
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// 初始化检查
|
||||
window.onload = async () => {
|
||||
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'; }
|
||||
};
|
||||
|
||||
// 路由加载器
|
||||
|
||||
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); }));
|
||||
|
||||
try {
|
||||
// 🟢 关键修改:fetch 路径必须包含 /static/
|
||||
const html = await fetch(`./static/pages/${pageName}.html`).then(r => {
|
||||
if(!r.ok) throw new Error('404');
|
||||
return r.text();
|
||||
});
|
||||
content.innerHTML = html;
|
||||
|
||||
// 动态加载对应 JS 模块
|
||||
// 🟢 关键修改:script src 路径必须包含 /static/
|
||||
const script = document.createElement('script');
|
||||
script.src = `./static/pages/${pageName}.js?t=${Date.now()}`;
|
||||
|
||||
script.onload = () => {
|
||||
// 触发模块初始化
|
||||
const moduleName = pageName.charAt(0).toUpperCase() + pageName.slice(1) + 'Module';
|
||||
if(window[moduleName]?.init) {
|
||||
window[moduleName].init();
|
||||
}
|
||||
bar.style.width = '100%';
|
||||
setTimeout(() => bar.classList.remove('active'), 200);
|
||||
};
|
||||
script.onerror = () => {
|
||||
throw new Error('JS Load Failed');
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
} 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(--accent)'; }, 200); }, 100);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 侧边栏切换
|
||||
function toggleSidebar() {
|
||||
document.getElementById('app').classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
async function doLogout() {
|
||||
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(); }
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
class MiniChart {
|
||||
constructor(canvasId, color = '#7aa2f7') {
|
||||
this.canvas = document.getElementById(canvasId);
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.color = color;
|
||||
this.data = new Array(60).fill(0); // 60秒历史
|
||||
this.maxVal = 100;
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
}
|
||||
|
||||
resize() {
|
||||
const rect = this.canvas.parentElement.getBoundingClientRect();
|
||||
this.canvas.width = rect.width - 24;
|
||||
this.canvas.height = 60;
|
||||
this.draw();
|
||||
}
|
||||
|
||||
update(val) {
|
||||
this.data.push(val);
|
||||
if(this.data.length > 60) this.data.shift();
|
||||
this.maxVal = Math.max(...this.data, 100);
|
||||
this.draw();
|
||||
}
|
||||
|
||||
draw() {
|
||||
if(!this.ctx) return;
|
||||
const { width, height } = this.canvas;
|
||||
this.ctx.clearRect(0, 0, width, height);
|
||||
|
||||
this.ctx.strokeStyle = this.color;
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.beginPath();
|
||||
|
||||
this.data.forEach((v, i) => {
|
||||
const x = (i / 59) * width;
|
||||
const y = height - (v / this.maxVal) * (height - 10);
|
||||
if(i === 0) this.ctx.moveTo(x, y);
|
||||
else this.ctx.lineTo(x, y);
|
||||
});
|
||||
this.ctx.stroke();
|
||||
|
||||
// 填充渐变
|
||||
this.ctx.lineTo(width, height);
|
||||
this.ctx.lineTo(0, height);
|
||||
this.ctx.fillStyle = this.color + '20';
|
||||
this.ctx.fill();
|
||||
}
|
||||
}
|
||||
window.MiniChart = MiniChart;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { initAuth, api } from './api.js';
|
||||
import { initDashboard } from './modules/dashboard.js';
|
||||
import { initPlugins } from './modules/plugins.js';
|
||||
import { initLogs } from './modules/logs.js';
|
||||
import { initCommands } from './modules/commands.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// 1. 检查登录状态
|
||||
const authState = await initAuth();
|
||||
if (!authState.authenticated) return; // 停留在登录页
|
||||
|
||||
// 2. 初始化各模块
|
||||
initDashboard();
|
||||
initPlugins();
|
||||
initLogs();
|
||||
initCommands();
|
||||
|
||||
// 3. Tab 切换逻辑
|
||||
document.querySelectorAll('.tabs button').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tabs button').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
const tabId = `tab-${btn.dataset.tab}`;
|
||||
document.querySelectorAll('.content section').forEach(s => s.classList.add('hidden'));
|
||||
document.getElementById(tabId).classList.remove('hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user