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:
2026-06-10 12:27:14 +08:00
commit e6875f0b4b
78 changed files with 14843 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
:root {
--bg-dark: #121218; --bg-panel: #1e1e24; --bg-card: #252530;
--bg-hover: #2a2a35; --text-main: #e0e0e0; --text-dim: #888890;
--accent: #7aa2f7; --accent-glow: rgba(122, 162, 247, 0.3);
--success: #9ece6a; --warn: #e0af68; --error: #f7768e;
--border: #363642; --sidebar-w: 240px; --sidebar-collapsed: 64px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: var(--bg-dark); color: var(--text-main); font-family: system-ui, -apple-system, sans-serif; height: 100vh; overflow: hidden; }
/* 登录页 */
.login-wrapper { display: flex; align-items: center; justify-content: center; height: 100vh; background: radial-gradient(circle at center, var(--bg-panel) 0%, var(--bg-dark) 100%); }
.login-card { background: var(--bg-card); padding: 2.5rem; border-radius: 16px; width: 340px; text-align: center; box-shadow: 0 10px 40px rgba(0,0,0,0.5); border: 1px solid var(--border); }
.login-card h2 { color: var(--accent); margin-bottom: 1.5rem; font-size: 1.8rem; }
.input-group { margin-bottom: 1rem; text-align: left; }
.input-group label { display: block; font-size: 0.8rem; color: var(--text-dim); margin-bottom: 4px; }
.input-group input { width: 100%; padding: 12px; background: var(--bg-dark); border: 1px solid var(--border); color: white; border-radius: 8px; font-size: 1rem; }
.input-group input:focus { border-color: var(--accent); outline: none; }
.btn-primary { width: 100%; padding: 12px; background: var(--accent); color: #000; font-weight: bold; border: none; border-radius: 8px; cursor: pointer; transition: 0.2s; margin-top: 10px; }
.btn-primary:hover { background: var(--accent-glow); color: white; }
.err-msg { color: var(--error); font-size: 0.8rem; margin-top: 10px; min-height: 1rem; }
/* 主框架布局 */
.app-frame { display: grid; grid-template-columns: var(--sidebar-w) 1fr; grid-template-rows: 60px 1fr; height: 100vh; transition: 0.3s ease; }
.app-frame.collapsed { grid-template-columns: var(--sidebar-collapsed) 1fr; }
header.top-bar { grid-column: 1 / -1; background: var(--bg-panel); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 1.5rem; }
.top-bar .title { font-weight: bold; font-size: 1.1rem; color: var(--accent); }
.top-bar .user-info { display: flex; align-items: center; gap: 1rem; font-size: 0.9rem; color: var(--text-dim); }
.logout-btn { background: none; border: 1px solid var(--border); color: var(--text-dim); padding: 4px 10px; border-radius: 4px; cursor: pointer; }
.logout-btn:hover { border-color: var(--error); color: var(--error); }
aside.sidebar { background: var(--bg-panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; padding: 1rem 0; overflow: hidden; transition: 0.3s; }
.nav-item { display: flex; align-items: center; padding: 12px 16px; color: var(--text-dim); text-decoration: none; cursor: pointer; transition: 0.2s; white-space: nowrap; gap: 12px; margin: 2px 8px; border-radius: 8px; }
.nav-item:hover, .nav-item.active { background: var(--bg-hover); color: var(--accent); }
.nav-item svg { width: 20px; height: 20px; fill: currentColor; flex-shrink: 0; }
.toggle-sidebar { margin-top: auto; padding: 12px; text-align: center; cursor: pointer; color: var(--text-dim); border-top: 1px solid var(--border); }
.toggle-sidebar:hover { color: var(--accent); }
main.content-area { position: relative; overflow: hidden; background: var(--bg-dark); display: flex; flex-direction: column; }
.progress-bar { position: absolute; top: 0; left: 0; height: 3px; background: var(--accent); width: 0; transition: width 0.3s, opacity 0.3s; opacity: 0; z-index: 100; }
.progress-bar.active { opacity: 1; }
.page-container { flex: 1; padding: 1.5rem; overflow-y: auto; }
/* 仪表盘网格 */
.dash-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; }
.stat-card { background: var(--bg-card); border-radius: 12px; padding: 1.2rem; border: 1px solid var(--border); display: flex; flex-direction: column; gap: 0.5rem; }
.stat-card h3 { font-size: 0.85rem; color: var(--text-dim); display: flex; justify-content: space-between; }
.stat-value { font-size: 2rem; font-weight: bold; color: var(--text-main); }
.stat-sub { font-size: 0.8rem; color: var(--text-dim); }
.mini-chart { width: 100%; height: 60px; background: rgba(0,0,0,0.2); border-radius: 6px; margin-top: 8px; }
/* 终端/日志样式 */
.terminal { background: #0a0a0f; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; height: 75vh; display: flex; flex-direction: column; }
.term-header { background: var(--bg-card); padding: 8px 12px; font-family: monospace; font-size: 0.8rem; color: var(--text-dim); border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; }
.term-body { flex: 1; padding: 10px; overflow-y: auto; font-family: monospace; font-size: 0.85rem; color: #ccc; line-height: 1.5; }
.term-input-area { display: flex; border-top: 1px solid var(--border); }
.term-input { flex: 1; background: var(--bg-panel); border: none; padding: 12px; color: white; font-family: monospace; outline: none; }
.log-entry { margin-bottom: 2px; border-bottom: 1px solid #1a1a24; padding: 2px 0; }
.log-INFO { color: var(--accent); } .log-WARNING { color: var(--warn); } .log-ERROR { color: var(--error); }
/* 插件列表 */
.plugin-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; }
.plugin-card { background: var(--bg-card); padding: 1rem; border-radius: 8px; border: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
.plugin-info h4 { color: var(--accent); margin-bottom: 4px; }
.plugin-info p { font-size: 0.8rem; color: var(--text-dim); }
.badge { padding: 2px 8px; border-radius: 10px; font-size: 0.7rem; font-weight: bold; }
.badge-run { background: rgba(158, 206, 106, 0.2); color: var(--success); }
.badge-stop { background: rgba(247, 118, 142, 0.2); color: var(--error); }
.plugin-act { display: flex; gap: 8px; }
.btn-sm { padding: 4px 10px; background: var(--bg-hover); border: 1px solid var(--border); color: var(--text-dim); border-radius: 4px; cursor: pointer; font-size: 0.75rem; }
.btn-sm:hover { color: var(--accent); border-color: var(--accent); }
/* 响应式 */
@media (max-width: 768px) {
.app-frame { grid-template-columns: var(--sidebar-collapsed) 1fr; }
.nav-item span { display: none; }
.toggle-sidebar { display: none; }
}
/* =========================================
仪表盘右侧栏布局扩展
========================================= */
.dash-layout {
display: flex;
gap: 1.5rem;
height: calc(100vh - 140px); /* 减去 Header 和 Padding */
overflow: hidden;
}
.dash-main {
flex: 1;
overflow-y: auto;
padding-right: 5px;
/* 自定义滚动条 */
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
/* 右侧固定侧边栏 */
.dash-sidebar {
width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 1rem;
overflow-y: auto;
padding-right: 5px;
}
/* 侧边卡片样式 */
.side-card {
background: var(--bg-card);
border-radius: 12px;
padding: 1.2rem;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 0.8rem;
transition: 0.3s;
}
.side-card:hover { border-color: var(--accent); }
.side-card h3 {
font-size: 0.9rem;
color: var(--accent);
margin: 0;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 6px;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
padding: 4px 0;
color: var(--text-dim);
}
.info-value {
color: var(--text-main);
font-family: 'Consolas', monospace;
font-weight: 600;
text-align: right;
max-width: 60%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 响应式适配:平板/手机自动切换为上下布局 */
@media (max-width: 1100px) {
.dash-layout { flex-direction: column; height: auto; overflow: visible; }
.dash-sidebar { width: 100%; flex-direction: row; flex-wrap: wrap; overflow: visible; }
.side-card { flex: 1; min-width: 280px; }
}
+51
View File
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SenSu 面板</title>
<!-- 🟢 加上 static 前缀 -->
<link rel="stylesheet" href="./static/css/style.css">
</head>
<body>
<div id="app" class="app-frame">
<header class="top-bar">
<div class="title">🐱 SenSu Alpha <span id="ver-badge" style="font-size:0.7em; opacity:0.5; margin-left:4px;"></span></div>
<div class="user-info">
<span id="uname">Loading...</span>
<button class="logout-btn" onclick="doLogout()">退出</button>
</div>
</header>
<aside class="sidebar">
<a class="nav-item active" data-page="dashboard">
<svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
<span>仪表盘</span>
</a>
<a class="nav-item" data-page="logs">
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
<span>实时日志</span>
</a>
<a class="nav-item" data-page="console">
<svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 14H4v-4h8v4zm0-6H4V8h8v4zm8 6h-8v-4h8v4zm0-6h-8V8h8v4z"/></svg>
<span>控制台</span>
</a>
<a class="nav-item" data-page="plugins">
<svg viewBox="0 0 24 24"><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>
<span>插件管理</span>
</a>
<div class="toggle-sidebar" onclick="toggleSidebar()"></div>
</aside>
<main class="content-area">
<div id="progress" class="progress-bar"></div>
<div id="page-content" class="page-container"></div>
</main>
</div>
<!-- 🟢 1. 先加载图表库 -->
<script src="./static/js/chart.js"></script>
<!-- 🟢 2. 再加载主逻辑 -->
<script src="./static/js/app.js"></script>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SenSu 登录</title>
<link rel="stylesheet" href="./static/css/style.css">
</head>
<body>
<div class="login-wrapper">
<div class="login-card">
<h2>🐱 SenSu Login</h2>
<div class="input-group"><label>用户名</label><input type="text" id="u" value="admin"></div>
<div class="input-group"><label>密码</label><input type="password" id="p" value="admin"></div>
<button class="btn-primary" onclick="doLogin()">进入系统</button>
<div id="err" class="err-msg"></div>
</div>
</div>
<script>
async function doLogin() {
const u = document.getElementById('u').value;
const p = document.getElementById('p').value;
const err = document.getElementById('err');
err.textContent = "验证中...";
try {
const res = await fetch('./api/login', {
method: 'POST', headers: {'Content-Type': 'application/json'},
credentials: 'include', body: JSON.stringify({username: u, password: p})
});
const data = await res.json();
if(res.ok && data.success) window.location.href = './home.html';
else err.textContent = data.msg || "凭证错误";
} catch(e) { err.textContent = "网络异常"; }
}
</script>
</body>
</html>
+32
View File
@@ -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');
}
+77
View File
@@ -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(); }
});
+51
View File
@@ -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;
+29
View File
@@ -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');
});
});
});
+10
View File
@@ -0,0 +1,10 @@
<div class="terminal">
<div class="term-header">💻 FRAMEWORK CONSOLE</div>
<div class="term-body" id="cmd-box" style="font-size:13px; color:#ccc;">
<div style="color:var(--success)">SenSu Console Ready. Type 'help' or 'status'.</div>
</div>
<div class="term-input-area">
<span style="padding:10px; color:var(--accent)">$</span>
<input type="text" id="cmd-input" class="term-input" placeholder="输入命令并回车..." autocomplete="off">
</div>
</div>
+29
View File
@@ -0,0 +1,29 @@
window.ConsoleModule = {
init: () => {
const box = document.getElementById('cmd-box');
const input = document.getElementById('cmd-input');
input.focus();
const append = (text, cls='') => {
box.innerHTML += `<div style="margin-top:2px;${cls?'color:'+cls:''}">${text}</div>`;
box.scrollTop = box.scrollHeight;
};
input.onkeydown = async (e) => {
if(e.key === 'Enter' && input.value.trim()) {
const cmd = input.value.trim();
append(`$ ${cmd}`, 'var(--accent)');
input.value = '';
try {
const res = await fetch('./api/command', {
method:'POST', headers:{'Content-Type':'application/json'}, credentials:'include',
body: JSON.stringify({command: cmd})
});
const d = await res.json();
append(d.success ? d.output : `Error: ${d.error}`, d.success ? '#ccc' : 'var(--error)');
} catch(err) { append(`Network Error: ${err.message}`, 'var(--error)'); }
}
};
},
destroy: () => {}
};
+70
View File
@@ -0,0 +1,70 @@
<div class="dash-layout">
<!-- 🟢 左侧:动态监控区 -->
<div class="dash-main">
<div class="dash-grid">
<div class="stat-card">
<h3>⏳ 运行时间</h3>
<div class="stat-value" id="d-uptime">--</div>
<div class="stat-sub" id="d-ver-badge">v?.?.?</div>
</div>
<div class="stat-card">
<h3>📦 插件状态</h3>
<div class="stat-value" id="d-plugins">--</div>
<div class="stat-sub">已加载 / 活跃</div>
</div>
<div class="stat-card">
<h3>🧠 内存使用</h3>
<div class="stat-value" id="d-mem">--%</div>
<canvas id="chart-mem" class="mini-chart"></canvas>
</div>
<div class="stat-card">
<h3>⚡ CPU 负载</h3>
<div class="stat-value" id="d-cpu">--%</div>
<canvas id="chart-cpu" class="mini-chart"></canvas>
</div>
<div class="stat-card">
<h3>🌐 网络接收</h3>
<div class="stat-value" id="d-net">--</div>
<canvas id="chart-net" class="mini-chart"></canvas>
</div>
<div class="stat-card">
<h3>💾 进程内存</h3>
<div class="stat-value" id="d-proc-mem">--</div>
<canvas id="chart-proc-mem" class="mini-chart"></canvas>
</div>
</div>
</div>
<!-- 🟢 右侧:高优先级信息栏 -->
<aside class="dash-sidebar">
<!-- 1. 硬件平台信息 -->
<div class="side-card">
<h3>💻 硬件平台</h3>
<div class="info-row"><span>操作系统</span><span class="info-value" id="info-os">--</span></div>
<div class="info-row"><span>硬件架构</span><span class="info-value" id="info-arch">--</span></div>
<div class="info-row"><span>核心数 (L/P)</span><span class="info-value" id="info-cores">--</span></div>
<div class="info-row"><span>物理内存</span><span class="info-value" id="info-mem-total">--</span></div>
<div class="info-row"><span>运行环境</span><span class="info-value" id="info-env">--</span></div>
</div>
<!-- 2. 框架简易信息 -->
<div class="side-card">
<h3>🐱 框架状态</h3>
<div class="info-row"><span>版本号</span><span class="info-value" id="info-fw-ver">--</span></div>
<div class="info-row"><span>访问地址</span><span class="info-value" id="info-addr">--</span></div>
<div class="info-row"><span>进程 PID</span><span class="info-value" id="info-pid">--</span></div>
<div class="info-row"><span>系统负载 (15m)</span><span class="info-value" id="info-load">--</span></div>
<div class="info-row" style="border:none; padding-top:8px;">
<span>运行状态</span>
<span class="info-value" style="color:var(--success)">🟢 Online</span>
</div>
</div>
<!-- 3. 快捷操作 (占位) -->
<div class="side-card">
<h3>🛠️ 快捷操作</h3>
<button class="btn-sm" style="width:100%; margin-bottom:6px" onclick="window.PluginsModule?.refresh()">🔄 刷新插件列表</button>
<button class="btn-sm" style="width:100%; color:var(--error); border-color:var(--error)" onclick="doLogout()">🚪 退出登录</button>
</div>
</aside>
</div>
+105
View File
@@ -0,0 +1,105 @@
window.DashboardModule = {
charts: {},
init: () => {
// 1. 初始化图表实例
window.DashboardModule.charts = {
mem: new MiniChart('chart-mem', '#9ece6a'),
cpu: new MiniChart('chart-cpu', '#7aa2f7'),
net: new MiniChart('chart-net', '#e0af68'),
proc_mem: new MiniChart('chart-proc-mem', '#f7768e')
};
// 2. 获取并填充右侧固定信息 (只获取一次即可,除非重启)
fetchSystemStaticInfo();
// 3. 启动实时数据轮询
fetchDash();
window._dashInterval = setInterval(fetchDash, 2000); // 2秒刷新
},
destroy: () => {
clearInterval(window._dashInterval);
window.DashboardModule.charts = {};
}
};
async function fetchSystemStaticInfo() {
try {
const sys = await fetch('./api/system', {credentials:'include'}).then(r => r.json());
// 硬件信息
if(sys.platform) {
document.getElementById('info-os').textContent = sys.platform.system || '--';
document.getElementById('info-arch').textContent = sys.platform.machine || '--';
document.getElementById('info-env').textContent = sys.platform.env || 'Standard';
}
if(sys.cpu) {
const c = sys.cpu.cores || 0;
document.getElementById('info-cores').textContent = `${c} / ${c}`; // Android下通常逻辑核=物理核
}
if(sys.memory) {
document.getElementById('info-mem-total').textContent = sys.memory.total_gb + ' GB';
}
// 框架信息 (部分需结合 API)
const host = window.location.hostname + (window.location.port ? ':'+window.location.port : '');
document.getElementById('info-addr').textContent = host;
} catch(e) {}
}
async function fetchDash() {
try {
const fw = await fetch('./api/framework', {credentials:'include'}).then(r => r.json());
const sys = await fetch('./api/system', {credentials:'include'}).then(r => r.json());
// --- 左侧动态数据更新 ---
if(fw) {
document.getElementById('d-uptime').textContent = formatUptime(fw.uptime || 0);
document.getElementById('d-plugins').textContent = fw.plugins || 0;
if(document.getElementById('d-ver-badge')) document.getElementById('d-ver-badge').textContent = 'v' + (fw.version||'?');
}
if(sys) {
// 内存
const m = sys.memory?.percent || 0;
document.getElementById('d-mem').textContent = m + '%';
window.DashboardModule.charts.mem.update(m);
// CPU (兼容 Android null 情况)
let cpuVal = sys.cpu?.percent;
if (cpuVal === null || cpuVal === undefined) {
const load = sys.cpu?.load_avg?.[0] || 0;
const cores = sys.cpu?.cores || 1;
cpuVal = Math.min(100, (load / cores) * 100);
}
document.getElementById('d-cpu').textContent = Math.round(cpuVal) + '%';
window.DashboardModule.charts.cpu.update(cpuVal);
// 网络 (RX 总量)
const netRx = sys.network?.rx || 0;
document.getElementById('d-net').textContent = netRx + ' MB';
window.DashboardModule.charts.net.update(netRx); // 图表显示总流量趋势
// 进程内存
const pm = sys.process?.memory_mb || 0;
document.getElementById('d-proc-mem').textContent = pm + ' MB';
window.DashboardModule.charts.proc_mem.update(pm);
// --- 右侧动态数据更新 ---
if(sys.process) {
document.getElementById('info-pid').textContent = sys.process.pid || '--';
}
if(sys.cpu?.load_avg) {
document.getElementById('info-load').textContent = sys.cpu.load_avg[2].toFixed(2);
}
}
} catch(e) { console.warn("Dashboard fetch error", e); }
}
// 辅助:秒数转时间格式
function formatUptime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
}
+4
View File
@@ -0,0 +1,4 @@
<div class="terminal">
<div class="term-header">📡 LIVE LOG STREAM (WebSocket)</div>
<div class="term-body" id="log-box" style="font-size:13px;"></div>
</div>
+24
View File
@@ -0,0 +1,24 @@
window.LogsModule = {
ws: null,
init: () => {
const box = document.getElementById('log-box');
const connect = () => {
window.LogsModule.ws = new WebSocket(`ws://${location.host}${window.location.pathname.replace(/\/$/,'')}/api/logs/ws`);
window.LogsModule.ws.onopen = () => box.innerHTML += `<div style="color:var(--success)">🟢 Connected</div>`;
window.LogsModule.ws.onmessage = e => {
try {
const d = JSON.parse(e.data);
if(d.type === 'log') {
const cls = d.level === 'ERROR' ? 'log-ERROR' : d.level === 'WARNING' ? 'log-WARNING' : 'log-INFO';
const t = d.timestamp ? new Date(d.timestamp*1000).toLocaleTimeString() : '--';
box.innerHTML += `<div class="log-entry"><span style="color:#555;margin-right:5px">${t}</span><span class="${cls}">[${d.level}]</span> ${d.message}</div>`;
box.scrollTop = box.scrollHeight;
}
} catch(e){}
};
window.LogsModule.ws.onclose = setTimeout(connect, 3000);
};
connect();
},
destroy: () => window.LogsModule.ws?.close()
};
+5
View File
@@ -0,0 +1,5 @@
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
<h2 style="color:var(--accent);">📦 插件管理</h2>
<button class="btn-sm" onclick="window.PluginsModule.refresh()">🔄 刷新</button>
</div>
<div id="plugin-list" class="plugin-list">加载中...</div>
+36
View File
@@ -0,0 +1,36 @@
window.PluginsModule = {
init: () => window.PluginsModule.refresh(),
refresh: async () => {
const list = document.getElementById('plugin-list');
if(!list) return;
list.innerHTML = '加载中...';
try {
const res = await fetch('./api/plugins', {credentials:'include'});
const data = await res.json();
if(!data.plugins?.length) { list.innerHTML = '<div style="color:var(--text-dim)">暂无已加载插件</div>'; return; }
list.innerHTML = data.plugins.map(p => `
<div class="plugin-card">
<div class="plugin-info">
<h4>${p.name} <span class="badge ${p.running?'badge-run':'badge-stop'}">${p.running?'RUNNING':'STOPPED'}</span></h4>
<p>v${p.version||'1.0.0'} | ${p.enabled?'已启用':'已禁用'}</p>
</div>
<div class="plugin-act">
${p.running
? `<button class="btn-sm" onclick="window.PluginsModule.act('${p.name}','disable')">停用</button>`
: `<button class="btn-sm" onclick="window.PluginsModule.act('${p.name}','enable')">启用</button>`
}
<button class="btn-sm" onclick="window.PluginsModule.act('${p.name}','reload')">重载</button>
</div>
</div>
`).join('');
} catch(e) { list.innerHTML = '加载失败'; }
},
act: async (name, action) => {
try {
await fetch(`./api/plugins/${name}/${action}`, {method:'POST', credentials:'include'});
setTimeout(window.PluginsModule.refresh, 500);
} catch(e) { alert('操作失败'); }
},
destroy: () => {}
};