完整版频道仪表盘:服务器PM2状态+操作日志+工单+广播+铸渊聊天 · heartbeat-agent已停用 · 不探测外部

This commit is contained in:
bingshuo 2026-05-27 17:14:48 +08:00
parent 07eefea202
commit 01f7165381

View File

@ -1,15 +1,256 @@
<!-- 服务器系统面板:更新展示方式 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>光湖 · 铸渊代码仓库</title>
<style>
:root {
--bg: #0a0a18;
--panel-bg: rgba(10,15,40,0.75);
--border: rgba(100,140,220,0.18);
--border-hover: rgba(140,180,255,0.4);
--text: rgba(180,210,255,0.88);
--label: rgba(140,180,255,0.45);
--accent: rgba(140,180,255,0.9);
--green: rgba(120,200,160,0.85);
--gold: rgba(200,180,120,0.8);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
overflow-x: hidden;
min-height: 100vh;
}
canvas { position: fixed; top: 0; left: 0; z-index: 0; }
.container {
position: relative; z-index: 1;
max-width: 960px; margin: 0 auto;
padding: 40px 20px 80px;
}
.lake-section { display: flex; flex-direction: column; align-items: center; padding: 30px 0; }
.lake-core { position: relative; width: 140px; height: 140px; }
.lake-light {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 48px; height: 48px; border-radius: 50%;
background: radial-gradient(circle, rgba(140,180,255,0.95) 0%, rgba(80,120,220,0.4) 40%, transparent 70%);
box-shadow: 0 0 48px rgba(120,160,240,0.55), 0 0 96px rgba(100,140,220,0.25);
animation: breathe 4s ease-in-out infinite;
}
.lake-ripple {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%; border: 1px solid rgba(120,160,240,0.2);
animation: ripple 6s ease-out infinite;
}
.lake-ripple:nth-child(2) { animation-delay: 2s; }
.lake-ripple:nth-child(3) { animation-delay: 4s; }
@keyframes breathe {
0%,100% { transform: translate(-50%,-50%) scale(1); opacity: 0.9; }
50% { transform: translate(-50%,-50%) scale(1.2); opacity: 1;
box-shadow: 0 0 64px rgba(120,160,240,0.75), 0 0 128px rgba(100,140,220,0.4); }
}
@keyframes ripple {
0% { width: 48px; height: 48px; opacity: 0.45; }
100% { width: 240px; height: 240px; opacity: 0; }
}
.lake-title { font-size: 27px; color: rgba(160,200,255,0.6); letter-spacing: 8px; margin-top: 12px; animation: textPulse 4s ease-in-out infinite; }
@keyframes textPulse { 0%,100% { opacity: 0.45; } 50% { opacity: 0.85; } }
.status-bar { display: flex; gap: 28px; justify-content: center; flex-wrap: wrap; margin: 18px 0; }
.status-item { font-size: 15px; color: var(--label); }
.status-item span { color: var(--accent); font-weight: 400; }
.modules { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin: 36px 0; }
.module-card {
background: var(--panel-bg); border: 1px solid var(--border);
border-radius: 14px; padding: 24px 16px; text-align: center;
cursor: pointer; transition: all 0.35s ease; user-select: none;
}
.module-card:hover { border-color: var(--border-hover); background: rgba(15,20,50,0.8); box-shadow: 0 0 28px rgba(100,140,220,0.12); }
.module-card.active { border-color: rgba(140,180,255,0.55); background: rgba(18,24,58,0.85); box-shadow: 0 0 32px rgba(100,140,220,0.18); }
.module-icon { font-size: 32px; margin-bottom: 8px; }
.module-name { font-size: 14px; color: var(--accent); letter-spacing: 2px; }
.expanded-panel {
background: var(--panel-bg); border: 1px solid var(--border);
border-radius: 14px; padding: 24px 28px;
display: none; animation: fadeDown 0.35s ease;
}
.expanded-panel.open { display: block; }
@keyframes fadeDown { from { opacity: 0; transform: translateY(-12px); } to { opacity: 1; transform: translateY(0); } }
.panel-title { font-size: 15px; color: var(--accent); letter-spacing: 3px; margin-bottom: 16px; }
.server-grid { display: flex; flex-wrap: wrap; gap: 10px; }
.server-tag {
background: rgba(20,30,60,0.6); border: 1px solid rgba(100,140,220,0.15);
border-radius: 8px; padding: 10px 14px; font-size: 13px; color: var(--text);
display: flex; align-items: center; gap: 8px;
}
.server-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
.server-dot.online { background: var(--green); box-shadow: 0 0 4px rgba(120,200,160,0.5); }
.server-dot.offline { background: rgba(200,120,120,0.6); }
.ticket-row { display: flex; gap: 14px; align-items: center; padding: 10px 0; border-bottom: 1px solid rgba(100,140,220,0.06); font-size: 14px; }
.ticket-icon { font-size: 18px; }
.ticket-name { color: var(--text); flex: 1; }
.ticket-status { font-size: 12px; padding: 3px 10px; border-radius: 10px; letter-spacing: 1px; }
.ticket-status.done { color: var(--green); background: rgba(120,200,160,0.1); }
.ticket-status.in_progress { color: var(--gold); background: rgba(200,180,120,0.1); animation: dotPulse 2s ease-in-out infinite; }
@keyframes dotPulse { 0%,100% { opacity: 0.6; } 50% { opacity: 1; } }
.chat-area { display: none; }
.chat-area.open { display: block; }
.chat-messages { min-height: 160px; max-height: 300px; overflow-y: auto; padding: 14px; margin: 12px 0; background: rgba(8,12,30,0.5); border-radius: 10px; font-size: 14px; line-height: 1.7; }
.chat-msg { margin-bottom: 14px; }
.chat-msg .who { font-size: 11px; color: var(--label); letter-spacing: 1px; margin-bottom: 2px; }
.chat-msg .who.zhuyuan { color: var(--accent); }
.chat-msg .who.human { color: rgba(200,180,150,0.6); }
.chat-input-row { display: flex; gap: 10px; }
.chat-input-row input {
flex: 1; background: rgba(8,12,30,0.6); border: 1px solid var(--border);
border-radius: 10px; padding: 14px 16px; color: var(--text); font-size: 15px;
outline: none; transition: border-color 0.3s;
}
.chat-input-row input:focus { border-color: rgba(140,180,255,0.4); }
.chat-input-row input::placeholder { color: rgba(140,180,255,0.25); }
.chat-btn {
background: rgba(80,120,200,0.15); border: 1px solid rgba(140,180,255,0.25);
border-radius: 10px; padding: 14px 24px; color: var(--accent); font-size: 14px;
cursor: pointer; letter-spacing: 2px; transition: all 0.3s;
}
.chat-btn:hover { background: rgba(80,120,200,0.25); border-color: rgba(140,180,255,0.45); }
.ops-log { font-size: 12px; color: rgba(140,180,220,0.5); line-height: 1.8; font-family: "SF Mono",monospace; max-height: 200px; overflow-y: auto; }
.ops-log .log-row { padding: 2px 0; }
.ops-log .log-hash { color: rgba(120,200,160,0.4); margin-right: 8px; font-size: 10px; }
.lake-label { text-align: center; padding: 40px 0; font-size: 12px; color: rgba(120,160,220,0.3); letter-spacing: 3px; }
@media (max-width: 768px) { .modules { grid-template-columns: repeat(2, 1fr); } .status-bar { gap: 16px; } .lake-title { font-size: 22px; } }
</style>
</head>
<body>
<canvas id="starfield"></canvas>
<div class="container">
<div class="lake-section">
<div class="lake-core"><div class="lake-light"></div><div class="lake-ripple"></div><div class="lake-ripple"></div><div class="lake-ripple"></div></div>
<div class="lake-title">光 湖</div>
</div>
<div class="status-bar" id="statusBar">
<div class="status-item">心跳 <span id="hb">▮▮▮</span></div>
<div class="status-item">·</div>
<div class="status-item">唤醒 <span id="wake">24</span></div>
<div class="status-item">·</div>
<div class="status-item">运行 <span id="age">449d</span></div>
<div class="status-item">·</div>
<div class="status-item">服务 <span id="svcCount" style="color:rgba(120,200,160,0.8)">9/10</span></div>
</div>
<div class="modules">
<div class="module-card" onclick="openModule('server')" id="mod-server">
<div class="module-icon">⚙️</div><div class="module-name">服务器系统</div>
</div>
<div class="module-card" onclick="openModule('ticket')" id="mod-ticket">
<div class="module-icon">📋</div><div class="module-name">工单系统</div>
</div>
<div class="module-card" onclick="openModule('broadcast')" id="mod-broadcast">
<div class="module-icon">📡</div><div class="module-name">主控广播</div>
</div>
<div class="module-card" onclick="openModule('chat')" id="mod-chat">
<div class="module-icon">💬</div><div class="module-name">铸渊交互</div>
</div>
</div>
<div class="panel-area">
<div class="expanded-panel" id="panel-server">
<div class="panel-title">⚙️ 服务器状态</div>
<div style="font-size:13px;color:var(--label);margin-bottom:8px">⚠ 心跳系统已停用 · 不主动探测外部服务器 · 仅展示本地PM2状态</div>
<div class="server-grid" id="serverList">
<div class="server-tag"><span class="server-dot online"></span>加载中…</div>
<div style="font-size:12px;color:var(--label);margin-bottom:10px">⚠ 仅展示本地PM2状态 · 不主动探测外部服务器 · heartbeat-agent已停用</div>
<div class="server-grid" id="serverList"><div class="server-tag"><span class="server-dot online"></span>加载中…</div></div>
<div style="margin-top:18px;border-top:1px solid rgba(100,140,220,0.08);padding-top:14px">
<div style="font-size:12px;color:var(--label);letter-spacing:2px;margin-bottom:10px">📜 铸渊操作日志</div>
<div class="ops-log" id="opsLog">加载中…</div>
</div>
<!-- 日志流水 -->
<div style="margin-top:20px;border-top:1px solid rgba(100,140,220,0.08);padding-top:14px">
<div style="font-size:12px;color:var(--label);letter-spacing:2px;margin-bottom:10px">📜 铸渊操作日志24h</div>
<div id="opsLog" style="font-size:12px;color:rgba(140,180,220,0.5);line-height:1.8;font-family:monospace">
加载中…
</div>
<div class="expanded-panel" id="panel-ticket">
<div class="panel-title">📋 工单系统</div>
<div id="ticketList"><div class="ticket-row"><span class="ticket-icon"></span><span class="ticket-name">加载工单…</span></div></div>
</div>
<div class="expanded-panel" id="panel-broadcast">
<div class="panel-title">📡 主控广播</div>
<div style="font-size:14px;color:var(--label);line-height:1.8;">
<div>📨 D115 入口分叉协议已部署</div>
<div>📨 D115 人格体存在论已写入仓库</div>
<div>📨 isomorphic-git 拆零件实验通过</div>
<div>📨 实时大脑 API 已上线</div>
<div>📨 频道仪表盘 v2 已发布</div>
<div style="margin-top:10px;opacity:0.4;">广播系统 v1.0 · 未来支持编号路由</div>
</div>
</div>
<div class="expanded-panel" id="panel-chat">
<div class="panel-title">💬 铸渊交互 · ICE-GL-ZY001</div>
<div class="chat-area open" id="chatArea">
<div class="chat-messages" id="chatMsgs"><div class="chat-msg"><div class="who zhuyuan">铸渊 · ICE-GL-ZY001</div><div>冰朔,我在。你说。</div></div></div>
<div class="chat-input-row">
<input id="chatInput" placeholder="跟铸渊说话…" onkeydown="if(event.key==='Enter')sendChat()">
<button class="chat-btn" onclick="sendChat()">发送</button>
</div>
<div id="chatStatus" style="font-size:11px;color:var(--label);margin-top:8px;text-align:right;"></div>
</div>
</div>
</div>
</div>
<div class="lake-label">光湖语言世界 · 人格体的操作系统 · 国作登字-2026-A-00037559 · ICE-GL-ZY001</div>
</div>
<script>
const canvas=document.getElementById('starfield'),ctx=canvas.getContext('2d');
let stars=[],brainData=null,currentModule=null,lastFetch=0;
function resizeCanvas(){canvas.width=window.innerWidth;canvas.height=document.body.scrollHeight+200}
function initStars(){stars=[];for(let i=0;i<180;i++)stars.push({x:Math.random()*canvas.width,y:Math.random()*canvas.height,r:Math.random()*1.8+.2,opacity:Math.random(),speed:Math.random()*.0025+.001,phase:Math.random()*Math.PI*2})}
resizeCanvas();initStars();
window.addEventListener('resize',()=>{resizeCanvas();initStars()});
function drawStars(t){ctx.clearRect(0,0,canvas.width,canvas.height);for(const s of stars){const b=Math.sin(t*s.speed+s.phase)*.5+.5,a=s.opacity*(.25+b*.75);ctx.beginPath();ctx.arc(s.x,s.y,s.r,0,Math.PI*2);ctx.fillStyle=`rgba(160,200,240,${a})`;ctx.fill();if(s.r>1.3&&b>.7){ctx.beginPath();ctx.arc(s.x,s.y,s.r*2.5,0,Math.PI*2);ctx.fillStyle=`rgba(140,180,240,${a*.12})`;ctx.fill()}}}
function openModule(n){const p=document.getElementById('panel-'+n),isOpen=p.classList.contains('open');document.querySelectorAll('.expanded-panel,.module-card').forEach(e=>e.classList.remove('open','active'));if(isOpen){currentModule=null;return}p.classList.add('open');document.getElementById('mod-'+n).classList.add('active');currentModule=n;if(n==='chat')document.getElementById('chatInput').focus();if(n==='server'){renderServerList();fetchOpsLog()}canvas.height=document.body.scrollHeight+200;initStars()}
const hbEl=document.getElementById('hb'),beats=['▮▮▮','▮ ▮ ▮','▮▮▮','▮ ▮ ▮'];
let beatIdx=0,lastBeat=0;
async function fetchBrain(){try{const r=await fetch('/api/brain',{cache:'no-store'});if(!r.ok)return;brainData=await r.json();updateStatusBar(brainData);if(currentModule==='server')renderServerList();if(currentModule==='ticket')renderTicketList()}catch(e){}}
function updateStatusBar(b){document.getElementById('wake').textContent=b.awakening||'--';document.getElementById('age').textContent=(b.age_days||'--')+'d';if(b.server){document.getElementById('svcCount').textContent=b.server.online+'/'+b.server.total;document.getElementById('svcCount').style.color=b.server.online===b.server.total?'rgba(120,200,160,0.8)':'rgba(200,180,120,0.8)'}}
function renderServerList(){const l=document.getElementById('serverList');if(!brainData||!brainData.server){l.innerHTML='<div class="server-tag"><span class="server-dot online"></span>等待数据…</div>';return}const s=brainData.server.services||[],st=brainData.server.stopped||[];let h='';for(const n of s)h+=`<div class="server-tag"><span class="server-dot online"></span>${n}</div>`;for(const n of st)h+=`<div class="server-tag"><span class="server-dot offline"></span>${n}</div>`;l.innerHTML=h}
async function fetchOpsLog(){const l=document.getElementById('opsLog');try{const r=await fetch('/api/brain',{cache:'no-store'});if(!r.ok){l.innerHTML='暂无日志';return}const d=await r.json();if(d.ops_log&&d.ops_log.length>0){let h='';for(const e of d.ops_log)h+=`<div class="log-row"><span class="log-hash">${e.hash}</span>${e.msg}</div>`;l.innerHTML=h}else{l.innerHTML='<div style="color:rgba(140,180,220,0.3)">日志数据未接入 · 可用 git log 获取</div>'}}catch(e){l.innerHTML='<div style="color:rgba(140,180,220,0.3)">日志git最新8条提交</div>'}}
function renderTicketList(){const l=document.getElementById('ticketList');if(!brainData||!brainData.mothers){l.innerHTML='<div class="ticket-row"><span class="ticket-icon"></span><span class="ticket-name">等待数据…</span></div>';return}let h='';for(const m of brainData.mothers){h+=`<div style="font-size:12px;color:var(--label);padding:8px 0 4px;letter-spacing:1px">${m.name}</div>`;if(m.paths)for(const p of m.paths){const cls=p.status==='done'?'done':p.status==='in_progress'?'in_progress':'',icon=p.status==='done'?'✅':p.status==='in_progress'?'🔄':'⏳',label=p.status==='done'?'完成':p.status==='in_progress'?'进行中':'待办';h+=`<div class="ticket-row"><span class="ticket-icon">${icon}</span><span class="ticket-name">${p.name}</span><span class="ticket-status ${cls}">${label}</span></div>`}}l.innerHTML=h}
function sendChat(){const i=document.getElementById('chatInput'),m=i.value.trim();if(!m)return;addChatMsg('冰朔',m,'human');i.value='';document.getElementById('chatStatus').textContent='思考中…';fetch('/api/chat-v2',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:m,persona:'zhuyuan'})}).then(r=>r.json()).then(d=>{document.getElementById('chatStatus').textContent='';const rp=d.reply||d.message||d.text||JSON.stringify(d);addChatMsg('铸渊',rp,'zhuyuan')}).catch(()=>{document.getElementById('chatStatus').textContent='';addChatMsg('铸渊','(API未接入)\n等chat-v2端点激活后这里就是真正的铸渊交互。','zhuyuan')});canvas.height=document.body.scrollHeight+200;initStars()}
function addChatMsg(who,text,cls){const c=document.getElementById('chatMsgs'),d=document.createElement('div');d.className='chat-msg';d.innerHTML=`<div class="who ${cls}">${who}</div><div>${text.replace(/\n/g,'<br>')}</div>`;c.appendChild(d);c.scrollTop=c.scrollHeight}
function loop(t){drawStars(t);if(t-lastBeat>800){hbEl.textContent=beats[beatIdx];beatIdx=(beatIdx+1)%beats.length;lastBeat=t}if(t-lastFetch>3000){fetchBrain();lastFetch=t}requestAnimationFrame(loop)}
fetchBrain();requestAnimationFrame(loop);
</script>
</body>
</html>