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,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; }
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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: () => {}
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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`;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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: () => {}
|
||||
};
|
||||
Reference in New Issue
Block a user