217 lines
9.6 KiB
HTML
217 lines
9.6 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>ATestCServer — MSYNC 连接状态</title>
|
||
<style>
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{font-family:'Segoe UI',system-ui,-apple-system,sans-serif;background:#0f1923;color:#c0d0e0;min-height:100vh}
|
||
.header{background:#1a2a3a;border-bottom:2px solid #2a4a6a;padding:20px 30px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px}
|
||
.header h1{font-size:22px;color:#e0f0ff;font-weight:600}
|
||
.header .badge{padding:6px 14px;border-radius:20px;font-size:13px;font-weight:600;letter-spacing:.5px}
|
||
.badge-online{background:#0a3a0a;color:#5ae05a;border:1px solid #1a6a1a}
|
||
.badge-offline{background:#3a0a0a;color:#e05a5a;border:1px solid #6a1a1a}
|
||
.main{max-width:980px;margin:30px auto;padding:0 20px;display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px}
|
||
.card{background:#152232;border:1px solid #1e3a50;border-radius:12px;padding:24px;transition:border-color .2s}
|
||
.card:hover{border-color:#2a5a80}
|
||
.card h3{font-size:14px;color:#6888a8;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:14px}
|
||
.card .value{font-size:28px;font-weight:700;color:#e8f8ff;margin-bottom:6px}
|
||
.card .sub{font-size:12px;color:#5a7a9a}
|
||
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||
.kv{display:flex;flex-direction:column;gap:3px}
|
||
.kv .key{font-size:11px;color:#5a7a9a;text-transform:uppercase;letter-spacing:.8px}
|
||
.kv .val{font-size:14px;color:#c8e0f8;word-break:break-all}
|
||
.status-row{display:flex;align-items:center;gap:8px;margin:6px 0}
|
||
.dot{width:10px;height:10px;border-radius:50%;display:inline-block}
|
||
.dot-green{background:#4ade80;box-shadow:0 0 8px #4ade80}
|
||
.dot-red{background:#ef4444;box-shadow:0 0 8px #ef4444}
|
||
.dot-yellow{background:#f59e0b;box-shadow:0 0 8px #f59e0b}
|
||
.dot-gray{background:#555;box-shadow:none}
|
||
.divider{border:0;border-top:1px solid #1e3a50;margin:16px 0}
|
||
.refresh{font-size:12px;color:#5a7a9a;margin-top:8px;text-align:right}
|
||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
||
.pulse{animation:pulse 2s infinite}
|
||
.footer{text-align:center;padding:20px;font-size:12px;color:#3a5a7a}
|
||
pre{background:#0a1620;border-radius:8px;padding:12px;overflow-x:auto;font-size:12px;color:#80a0c0;max-height:260px;overflow-y:auto}
|
||
.services-list{list-style:none}
|
||
.services-list li{padding:8px 10px;border-bottom:1px solid #1a2a3a;font-size:13px;display:flex;justify-content:space-between;color:#90b0d0}
|
||
.services-list li:last-child{border-bottom:none}
|
||
.section-title{font-size:15px;color:#a0c0e0;font-weight:600;margin-bottom:10px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<div>
|
||
<h1>ATestCServer</h1>
|
||
<span style="font-size:12px;color:#5a7a9a">SMANAGE 子服务器 · HTTP :25080 · HTTPS :25443</span>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:12px">
|
||
<span id="clock" style="font-size:14px;color:#6888a8"></span>
|
||
<span id="topBadge" class="badge badge-offline">断开</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main">
|
||
<!-- 连接状态卡片 -->
|
||
<div class="card">
|
||
<h3>连接状态</h3>
|
||
<div class="status-row"><span id="dotConn" class="dot dot-gray"></span> <span id="txtConn">加载中...</span></div>
|
||
<div class="status-row"><span id="dotAuth" class="dot dot-gray"></span> <span id="txtAuth">加载中...</span></div>
|
||
<div class="status-row"><span id="dotReg" class="dot dot-gray"></span> <span id="txtReg">加载中...</span></div>
|
||
<div class="status-row"><span id="dotHear" class="dot dot-gray"></span> <span id="txtHear">加载中...</span></div>
|
||
<div class="refresh pulse" id="refresher">自动刷新中...</div>
|
||
</div>
|
||
|
||
<!-- 服务信息卡片 -->
|
||
<div class="card">
|
||
<h3>服务标识</h3>
|
||
<div class="grid-2">
|
||
<div class="kv"><span class="key">服务名</span><span class="val" id="svcName">—</span></div>
|
||
<div class="kv"><span class="key">域名</span><span class="val" id="svcDomain">—</span></div>
|
||
<div class="kv"><span class="key">主服务器</span><span class="val" id="svcMaster">—</span></div>
|
||
<div class="kv"><span class="key">Token</span><span class="val" id="svcToken">—</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 对等服务卡片 -->
|
||
<div class="card" style="grid-column:span 2">
|
||
<h3>对等服务</h3>
|
||
<ul class="services-list" id="peerList"><li style="color:#5a7a9a">加载中...</li></ul>
|
||
</div>
|
||
|
||
<!-- 最近事件卡片 -->
|
||
<div class="card" style="grid-column:span 2">
|
||
<h3>最近同步事件</h3>
|
||
<ul class="services-list" id="eventLog"><li style="color:#5a7a9a">暂无事件</li></ul>
|
||
</div>
|
||
|
||
<!-- 原始数据卡片 -->
|
||
<div class="card" style="grid-column:span 2">
|
||
<h3>原始响应</h3>
|
||
<pre id="rawData">{}</pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer">
|
||
MSYNC Bridge · Heartbeat/30s · ATestCServer © SMANAGE
|
||
</div>
|
||
|
||
<script>
|
||
const HEALTH_URL = '/health/';
|
||
const STATUS_URL = '/api/sub/msync/status/';
|
||
|
||
function fmtTime(){const n=new Date();return n.toLocaleTimeString('zh-CN',{hour12:false})}
|
||
|
||
function updClock(){document.getElementById('clock').textContent=fmtTime()}
|
||
|
||
async function fetchStatus(){
|
||
try{
|
||
const[h,s]=await Promise.all([
|
||
fetch(HEALTH_URL).then(r=>r.json()),
|
||
fetch(STATUS_URL).then(r=>r.json())
|
||
]);
|
||
render(h?.data,h?.code===200,s?.data,s?.code===200);
|
||
}catch(e){
|
||
showError('请求失败: '+e.message);
|
||
}
|
||
}
|
||
|
||
function dot(cls){return '<span class="dot '+cls+'"></span>'}
|
||
|
||
function showError(msg){
|
||
document.getElementById('topBadge').className='badge badge-offline';
|
||
document.getElementById('topBadge').textContent='错误';
|
||
document.getElementById('dotConn').className='dot dot-red';
|
||
document.getElementById('txtConn').innerHTML=msg;
|
||
document.getElementById('refresher').textContent='刷新失败,10s后重试';
|
||
}
|
||
|
||
function render(hdata,hok,sdata,sok){
|
||
const connected = hdata?.MSync?.Connected || false;
|
||
const authenticated = hdata?.MSync?.Authenticated || false;
|
||
const registered = hdata?.MSync?.Registered || false;
|
||
const running = sdata?.running || connected;
|
||
const tokenRaw = hdata?.MSync?.AccessToken || sdata?.AccessToken || null;
|
||
|
||
// 顶部徽章
|
||
const badge = document.getElementById('topBadge');
|
||
if(connected && authenticated){
|
||
badge.className='badge badge-online'; badge.textContent='已连接';
|
||
}else if(connected){
|
||
badge.className='badge badge-online'; badge.textContent='已注册';
|
||
}else{
|
||
badge.className='badge badge-offline'; badge.textContent='断开';
|
||
}
|
||
|
||
// 连接状态
|
||
document.getElementById('dotConn').className='dot '+(connected?'dot-green pulse':'dot-red');
|
||
document.getElementById('txtConn').textContent='MSYNC 运行: '+(running?'是':'否');
|
||
|
||
// 认证状态——只有连接成功后才看 token,否则一律视为未认证
|
||
const authEffective = connected && authenticated;
|
||
const tokenEffective = connected && tokenRaw;
|
||
document.getElementById('dotAuth').className='dot '+(authEffective?'dot-green':tokenEffective?'dot-yellow':'dot-red');
|
||
document.getElementById('txtAuth').textContent='服务认证: '+(authEffective?'已认证':tokenEffective?'令牌存在(可能过期)':connected?'认证中':'未连接');
|
||
|
||
document.getElementById('dotReg').className='dot '+(registered?'dot-green':'dot-red');
|
||
document.getElementById('txtReg').textContent='服务注册: '+(registered?'已注册':'未注册');
|
||
|
||
document.getElementById('dotHear').className='dot '+(connected&®istered?'dot-green pulse':'dot-gray');
|
||
document.getElementById('txtHear').textContent='心跳线程: '+(connected&®istered?'运行中(间隔30s)':'未启动');
|
||
|
||
// 服务信息
|
||
document.getElementById('svcName').textContent=hdata?.MSync?.ServiceName||sdata?.ServiceName||'—';
|
||
document.getElementById('svcDomain').textContent=hdata?.MSync?.ServiceDomain||sdata?.ServiceDomain||'—';
|
||
document.getElementById('svcMaster').textContent=hdata?.MSync?.MasterURL||sdata?.MasterURL||'—';
|
||
|
||
// Token 截断展示(防完整泄露)
|
||
const tokenEl = document.getElementById('svcToken');
|
||
if(tokenRaw && tokenRaw.length>40){
|
||
tokenEl.textContent = tokenRaw.slice(0,30)+'…'+tokenRaw.slice(-15);
|
||
tokenEl.title = tokenRaw;
|
||
tokenEl.style.cursor = 'help';
|
||
}else{
|
||
tokenEl.textContent = tokenRaw||'—';
|
||
}
|
||
|
||
// 对等服务(PascalCase 字段:Name/Domain)
|
||
const peers=sdata?.PeerServices||[];
|
||
const pl=document.getElementById('peerList');
|
||
if(peers.length===0){
|
||
pl.innerHTML='<li style="color:#5a7a9a">无对等服务(主服务器可能未上线,或尚无其他子服务器)</li>';
|
||
}else{
|
||
pl.innerHTML=peers.map(p=>
|
||
'<li><span>'+(p.Name||p.name||'?')+'</span><span style="color:#6888a8;font-size:11px">'+(p.Domain||p.domain||'')+'</span></li>'
|
||
).join('');
|
||
}
|
||
|
||
// 事件日志
|
||
fetch('/api/sub/sync/logs/').then(r=>r.json()).then(d=>{
|
||
const logs=d?.data||[];
|
||
const el=document.getElementById('eventLog');
|
||
if(logs.length===0){
|
||
el.innerHTML='<li style="color:#5a7a9a">暂无同步事件(需要与主服务器通信后产生)</li>';
|
||
}else{
|
||
el.innerHTML=logs.slice(0,10).map(l=>
|
||
'<li><span>'+l.EventType+'</span><span style="color:#5a7a9a;font-size:11px">'+l.CreateTime+'</span></li>'
|
||
).join('');
|
||
}
|
||
}).catch(()=>{});
|
||
|
||
// 原始数据
|
||
document.getElementById('rawData').textContent=JSON.stringify({
|
||
health: hdata?.msync||{},
|
||
status: {running:running,authenticated:authenticated,registered:registered,peers:peers.length}
|
||
},null,2);
|
||
|
||
document.getElementById('refresher').textContent='最后刷新: '+fmtTime();
|
||
}
|
||
|
||
updClock();
|
||
setInterval(updClock,1000);
|
||
fetchStatus();
|
||
setInterval(fetchStatus,10000);
|
||
</script>
|
||
</body>
|
||
</html> |