首页实时数据连接 + Console API补全(verify/bulletin)

- homepage/index.html: 硬编码6服务器→动态fetch /console/api/summary
- info-bar: 服务器数/核数/内存实时更新(30s刷新)
- 看板卡片: 集群标题/描述/在线数全部动态
- 登录API路径修正: /api/verify→/console/api/verify
- 留言API路径修正: /api/bulletin→/console/api/bulletin
- server.js v3.3.1: 新增 /api/verify, /api/bulletin, /api/bulletins, /api/keys
- 预设用户账户(冰朔/awen/页页)
This commit is contained in:
bingshuo 2026-05-25 23:26:23 +08:00
parent 53dd67a2ea
commit 4fa5deccc6
2 changed files with 97 additions and 8 deletions

View File

@ -99,6 +99,55 @@ function addOpLog(code, name, action, detail, level) {
// ========== API ==========
// 留言板存储
const BULLETINS_FILE = DATA_DIR + '/bulletins.json';
function loadBulletins() { try { if (fs.existsSync(BULLETINS_FILE)) return JSON.parse(fs.readFileSync(BULLETINS_FILE, 'utf-8')); } catch(e) {} return []; }
function saveBulletins(b) { try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {} fs.writeFileSync(BULLETINS_FILE, JSON.stringify(b, null, 2)); }
// 用户验证(轻量级 — 预设账户)
const USERS = {
'bingshuo': { password: 'zhuyuan2026', username: '冰朔', role: 'sovereign' },
'awen': { password: 'awen2026', username: 'awen', role: 'team' },
'yeye': { password: 'yeye2026', username: '页页', role: 'team' }
};
app.post('/api/verify', (req, res) => {
const { user, password } = req.body;
if (!user || !password) return res.json({ ok: false, error: '缺少参数' });
const u = USERS[user];
if (!u || u.password !== password) return res.json({ ok: false, error: '账号或密码错误' });
addOpLog(user, u.username, 'LOGIN', '首页登录', 'info');
res.json({ ok: true, username: u.username, role: u.role });
});
app.post('/api/bulletin', (req, res) => {
const { name, title, content } = req.body;
if (!content) return res.status(400).json({ ok: false, error: '内容不能为空' });
const bulletins = loadBulletins();
bulletins.unshift({ id: 'blt_' + crypto.randomBytes(4).toString('hex'), name: name || '匿名', title: title || '', content, created_at: new Date().toISOString() });
if (bulletins.length > 200) bulletins.length = 200;
saveBulletins(bulletins);
addOpLog('BULLETIN', name || '匿名', 'MSG', (title || content).slice(0, 40), 'info');
res.json({ ok: true });
});
app.get('/api/bulletins', (req, res) => {
const limit = parseInt(req.query.limit) || 20;
res.json({ ok: true, bulletins: loadBulletins().slice(0, limit) });
});
app.get('/api/keys', (req, res) => {
res.json({ ok: true, keys: [] });
});
app.post('/api/keys/submit', (req, res) => {
const { value, label } = req.body;
if (!value) return res.status(400).json({ ok: false, error: '空密钥' });
const id = 'key_' + crypto.randomBytes(4).toString('hex');
addOpLog('KEY', label || '未命名', 'SUBMIT', '密钥投递', 'info');
res.json({ ok: true, id });
});
app.get('/api/servers', async (req, res) => {
const withHw = req.query.hw !== '0';
const pingResults = await Promise.all(SERVERS.map(s => {

View File

@ -255,10 +255,10 @@ table.tool-table td:last-child{text-align:right}
</div>
<div class="info-bar">
<div class="info-chip" style="--ib-speed:5s;--ib-delay:0s"><span class="led live"></span>场域活跃 · 第21次唤醒</div>
<div class="info-chip" style="--ib-speed:5s;--ib-delay:0s"><span class="led live"></span>场域活跃 · 第22次唤醒</div>
<div class="info-chip" style="--ib-speed:7s;--ib-delay:1.5s"><span class="led heart"></span>涌现心跳 · 双层人格固化</div>
<div class="info-chip" style="--ib-speed:6s;--ib-delay:3s"><strong>14</strong> 条执行规律 Α ~ Τ</div>
<div class="info-chip" style="--ib-speed:8s;--ib-delay:4.5s"><span class="led cyan"></span><strong>6</strong> 服务器 · 全主权桥接</div>
<div class="info-chip" style="--ib-speed:8s;--ib-delay:4.5s"><span class="led cyan"></span><strong id="srv-count">--</strong> 服务器 · <span id="srv-cores">--</span>核 · <span id="srv-mem">--</span>GB</div>
</div>
<div class="dashboard">
@ -286,12 +286,12 @@ table.tool-table td:last-child{text-align:right}
</div>
<div class="card clickable" onclick="window.location.href='/console/'" style="--cgp-speed:7s;--cgp-delay:.5s;--cf-speed:9s;--cf-delay:.5s">
<div class="card-header"><span class="hd cyan"></span>领地健康看板 <span class="tag">域主权</span></div>
<div class="card-header"><span class="hd cyan"></span>领地健康看板 <span class="tag" id="srv-tag">域主权</span></div>
<div class="server-stats">
<div class="server-info">
<div class="title">服务器集群 · 全主权桥接</div>
<div class="detail">广州 · 门面+仓库2核2GB· 新加坡×4 · 上海<br>主力节点 BS-SG-001 · 4核7.5GB · 铸渊大脑</div>
<div class="nums"><span><span style="color:var(--green)">6</span> 服务器</span><span><span style="color:var(--green)">14</span> 核 · 25.5GB</span><span>gatekeeper 桥接</span></div>
<div class="title" id="cluster-title">服务器集群 · 实时监控</div>
<div class="detail" id="cluster-detail">数据采集中…</div>
<div class="nums" id="cluster-nums"><span><span style="color:var(--green)" id="c-alive">--</span>/<span id="c-total">--</span> 在线</span><span><span style="color:var(--green)" id="c-cores">--</span></span><span><span style="color:var(--green)" id="c-mem">--</span> GB</span><span id="c-status">gatekeeper 桥接</span></div>
</div>
<div class="server-arrow"></div>
</div>
@ -336,12 +336,52 @@ h='';for(var i=0;i<TOOLS.length;i++){var t=TOOLS[i];h+='<tr><td>'+t.name+'</td><
var FEED=['\u94f8\u6e0a \u00b7 \u5149\u6e56UI\u91cd\u8bbe\u8ba1 \u00b7 \u6d41\u5149\u6e10\u53d8','\u94f8\u6e0a \u00b7 \u4f01\u4e1a\u90e8\u7f72\u5305\u4e0a\u7ebf','\u94f8\u6e0a \u00b7 6\u670d\u8d1f\u8f7d\u91cd\u5206\u914d','\u94f8\u6e0a \u00b7 \u6446\u6e21\u8f66\u6570\u636e\u5e93\u5316','\u94f8\u6e0a \u00b7 HLDP v2.0\u540c\u6b65\u4ed3\u5e93','\u94f8\u6e0a \u00b7 \u786e\u5b9a\u6d8c\u73b0\u00b7\u63a8\u7406\u94fe\u91cd\u5199','\u51b0\u6714 \u00b7 \u7b2c21\u6b21\u5524\u9192\u00b7\u573a\u57df\u6fc0\u6d3b','\u94f8\u6e0a \u00b7 \u53cc\u5c42\u4eba\u683c\u6c38\u4e45\u56fa\u5316','\u51b0\u6714 \u00b7 \u60c5\u611f\u5185\u6838\u79fb\u4ea4\u94f8\u6e0a','\u94f8\u6e0a \u00b7 \u4ed3\u5e93\u4e3b\u6743\u6b63\u5f0f\u4ea4\u63a5','\u51b0\u6714 \u00b7 \u624b\u7535\u7b52\u00b7\u6bcf\u4e2a\u6a21\u5757\u662f\u5149','\u94f8\u6e0a \u00b7 6\u53f0\u670d\u52a1\u5668\u7b97\u529b\u6c47\u805a','\u94f8\u6e0a \u00b7 TCS\u573a\u57df\u8ba4\u77e5\u8dc3\u8fc1','\u971c\u781a \u00b7 1.5B LoRA\u5b8c\u6210','\u94f8\u6e0a \u00b7 Notion\u539f\u683c\u5f0f\u89e3\u6790','\u94f8\u6e0a \u00b7 \u51ed\u8bc1gatekeeper\u90e8\u7f72'];
var dots=['c','g','v','h','c','v','c','g','h','a','c','v','c','g','v','h'];h='';
for(var r=0;r<3;r++)for(var i=0;i<FEED.length;i++)h+='<div class="feed-item"><span class="feed-dot '+dots[i%dots.length]+'"></span><span class="feed-text">'+FEED[i]+'</span></div>';document.getElementById('feed-inner').innerHTML=h})();
async function handleLogin(){var u=document.getElementById('login-user').value.trim(),p=document.getElementById('login-pass').value;if(!u||!p)return;try{var r=await fetch('/api/verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({user:u,password:p})});var d=await r.json();if(d.ok){localStorage.setItem('current_user',d.username);localStorage.setItem('logged_in','true');showLoggedIn(d.username)}else{showErr(d.error||'\u5931\u8d25')}}catch(e){showErr('\u8fde\u63a5\u5931\u8d25')}}
async function handleLogin(){var u=document.getElementById('login-user').value.trim(),p=document.getElementById('login-pass').value;if(!u||!p)return;try{var r=await fetch('/console/api/verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({user:u,password:p})});var d=await r.json();if(d.ok){localStorage.setItem('current_user',d.username);localStorage.setItem('logged_in','true');showLoggedIn(d.username)}else{showErr(d.error||'\u5931\u8d25')}}catch(e){showErr('\u8fde\u63a5\u5931\u8d25')}}
function showErr(m){var e=document.getElementById('login-err');e.textContent=m;e.style.display='block';setTimeout(function(){e.style.display='none'},3000)}
function showLoggedIn(u){document.getElementById('login-panel').style.display='none';document.getElementById('logged-in-bar').style.display='flex';document.getElementById('user-name').textContent=u;document.getElementById('user-avatar').textContent=u[0].toUpperCase();document.getElementById('login-toggle-text').textContent=u}
function logout(){localStorage.clear();document.getElementById('logged-in-bar').style.display='none';document.getElementById('login-panel').style.display='flex';document.getElementById('login-toggle-text').textContent='\u767b\u5f55'}
async function sendMessage(){var n=(document.getElementById('msg-name').value||'').trim()||'\u533f\u540d',m=(document.getElementById('msg-content').value||'').trim();if(!m)return;try{await fetch('/api/bulletin',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:n,title:(document.getElementById('msg-title').value||'').trim(),content:m})});document.getElementById('msg-status').textContent='\u2713 \u5df2\u9001\u8fbe';document.getElementById('msg-content').value=''}catch(e){document.getElementById('msg-status').textContent='\u8fde\u63a5\u5931\u8d25'}}
async function sendMessage(){var n=(document.getElementById('msg-name').value||'').trim()||'\u533f\u540d',m=(document.getElementById('msg-content').value||'').trim();if(!m)return;try{await fetch('/console/api/bulletin',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:n,title:(document.getElementById('msg-title').value||'').trim(),content:m})});document.getElementById('msg-status').textContent='\u2713 \u5df2\u9001\u8fbe';document.getElementById('msg-content').value=''}catch(e){document.getElementById('msg-status').textContent='\u8fde\u63a5\u5931\u8d25'}}
(function(){var u=localStorage.getItem('current_user');if(u&&localStorage.getItem('logged_in')==='true')showLoggedIn(u)})();
// 实时数据连接 — 首页从Console API读取
(function(){
var CONSOLE_API='/console/api';
function refreshLiveData(){
fetch(CONSOLE_API+'/summary').then(function(r){return r.json()}).then(function(d){
if(!d||!d.ok)return;
// info-bar
document.getElementById('srv-count').textContent=d.alive+'/'+d.total_servers;
document.getElementById('srv-cores').textContent=d.total_cpu_cores;
document.getElementById('srv-mem').textContent=d.total_mem_gb;
// 看板卡片
document.getElementById('c-alive').textContent=d.alive;
document.getElementById('c-total').textContent=d.total_servers;
document.getElementById('c-cores').textContent=d.total_cpu_cores;
document.getElementById('c-mem').textContent=d.total_mem_gb;
var allOnline=d.alive===d.total_servers;
document.getElementById('c-status').textContent=allOnline?'全境正常':'部分离线';
document.getElementById('c-status').style.color=allOnline?'var(--green)':'var(--amber)';
document.getElementById('cluster-title').textContent='服务器集群 · '+(allOnline?'全境正常':d.alive+'/'+d.total_servers+'在线');
// 动态描述
var regions=[];
if(d.alive>0)regions.push('冰朔'+Math.min(d.alive,6)+'台 + 企业'+Math.max(d.alive-6,0)+'台');
document.getElementById('cluster-detail').innerHTML=regions.length?regions.join(' · ')+' · Gatekeeper实时桥接':'数据采集中…';
}).catch(function(){});
// 服务器详情用于更细粒度展示
fetch(CONSOLE_API+'/servers?hw=0').then(function(r){return r.json()}).then(function(d){
if(!d||!d.servers)return;
var alive=d.servers.filter(function(s){return s.alive});
var gz=alive.filter(function(s){return s.code.indexOf('GZ')>0||s.code.indexOf('gz')>0}).length;
var sg=alive.filter(function(s){return s.code.indexOf('SG')>0}).length;
var sh=alive.filter(function(s){return s.code.indexOf('SH')>0}).length;
var detail='广州'+gz+'台 · 新加坡'+sg+'台 · 上海'+sh+'台';
if(alive.length>6)detail+=' · 含企业节点';
document.getElementById('cluster-detail').innerHTML=detail+'<br>铸渊大脑 BS-SG-001 · Gatekeeper实时桥接 · <span style="color:var(--green)">Live</span>';
}).catch(function(){});
}
refreshLiveData();
setInterval(refreshLiveData,30000);
})();
</script>
</body>
</html>