节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
31.66 KiB
Stardust
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>尘埃视角 · 奇点坍缩舱 — 星尘 Stardust</title>
<link rel="stylesheet" href="cosmic.css" />
<style>
  /* ===== 本页专属:奇点坍缩舱(不依赖任何既有令牌,沿用 cosmic.css 宇宙系统)===== */
  .stage { position: fixed; inset: 0; }
  #singularity { position: absolute; inset: 0; width: 100%; height: 100%; display: block; cursor: crosshair; }

  /* 维度罗盘:四个悬浮锚点环绕奇点 */
  .dim-orb {
    position: absolute; z-index: 30; width: 66px; height: 66px; margin: -33px 0 0 -33px; border-radius: 50%;
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
    cursor: pointer; color: var(--ink-2);
    background: radial-gradient(circle at 50% 30%, color-mix(in srgb, var(--star) 20%, transparent), var(--glass-2) 72%);
    border: 1px solid var(--line); backdrop-filter: blur(14px);
    box-shadow: 0 6px 22px rgba(0,0,0,.4);
    transition: transform .25s ease, color .25s ease, box-shadow .25s ease, border-color .25s;
  }
  .dim-orb .ic { font-size: 20px; line-height: 1; }
  .dim-orb .lb { font-size: 10px; letter-spacing: .04em; }
  .dim-orb:hover { transform: scale(1.12); color: var(--ink); }
  .dim-orb.active { color: var(--bg-abyss); background: linear-gradient(135deg, var(--star), var(--accent));
    box-shadow: var(--glow-star); border-color: transparent; transform: scale(1.06); }
  .dim-orb[data-d="monitor"] { left: 50%; top: 86px; }
  .dim-orb[data-d="config"]  { right: 40px; top: 50%; margin: -33px 0 0 0; }
  .dim-orb[data-d="logs"]    { left: 50%; bottom: 132px; }
  .dim-orb[data-d="topology"]{ left: 40px;  top: 50%; margin: -33px 0 0 0; }

  /* 信息读出条(数据读出器,非卡片堆叠) */
  .readout {
    position: absolute; left: 50%; bottom: 96px; transform: translateX(-50%);
    width: min(680px, 86vw); max-height: 168px; overflow: auto; z-index: 25;
    padding: 12px 16px; font-size: 12.5px; line-height: 1.6;
  }
  .readout .rh { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--ink-2); margin-bottom: 6px; }
  .readout .rh b { color: var(--star); }

  /* 引力透镜编辑环 —— 从星核浮出,区别于普通卡片 */
  #lens { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%) scale(.4);
    width: min(440px, 90vw); z-index: 70; display: none; opacity: 0; padding: 0; }
  #lens.open { display: block; animation: lensIn .42s cubic-bezier(.2,.9,.3,1) forwards; }
  @keyframes lensIn { to { opacity: 1; transform: translate(-50%,-50%) scale(1); } }
  #lens.closing { animation: lensOut .3s ease forwards; }
  @keyframes lensOut { to { opacity: 0; transform: translate(-50%,-50%) scale(.7); } }
  /* 透镜两侧旋转光环 */
  #lens::before, #lens::after { content: ""; position: absolute; border-radius: 50%; pointer-events: none; }
  #lens::before { inset: -26px; border: 1px dashed color-mix(in srgb, var(--star) 50%, transparent);
    animation: spin 14s linear infinite; opacity: .5; }
  #lens::after { inset: -40px; border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
    border-top-color: transparent; border-bottom-color: transparent; animation: spin 9s linear infinite reverse; opacity: .35; }
  @keyframes spin { to { transform: rotate(360deg); } }
  .lens-head { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--line-2); }
  .lens-head .badge { font-size: 10px; letter-spacing: .2em; color: var(--star); border: 1px solid var(--line); padding: 2px 8px; border-radius: 999px; }
  .lens-head .k { font-size: 15px; font-weight: 700; color: var(--ink); flex: 1; }
  .lens-body { padding: 16px; }
  .lens-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
  .lens-row label { font-size: 11px; color: var(--ink-3); letter-spacing: .1em; }
  .lens-row .old { font-size: 11px; color: var(--ink-3); }
  .lens-row .old s { color: var(--danger); }
  .lens-foot { display: flex; gap: 10px; padding: 12px 16px; border-top: 1px solid var(--line-2); }
  .lens-foot .btn { flex: 1; justify-content: center; }

  /* 时间晶体(版本回溯控件) */
  .timecrystal {
    position: fixed; right: 24px; bottom: 96px; z-index: 40; width: 220px; padding: 12px 14px;
  }
  .timecrystal .tc-h { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--ink-2); margin-bottom: 10px; }
  .tc-gem { width: 14px; height: 14px; transform: rotate(45deg); border: 1px solid var(--accent);
    background: linear-gradient(135deg, var(--star), var(--accent)); box-shadow: var(--glow-soft); animation: pulse 3s ease-in-out infinite; }
  .tc-item { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 9px; cursor: pointer;
    font-size: 12px; color: var(--ink-2); transition: .15s; }
  .tc-item:hover { background: color-mix(in srgb, var(--star) 12%, transparent); color: var(--ink); }
  .tc-item.cur { color: var(--star); background: color-mix(in srgb, var(--star) 10%, transparent); }
  .tc-item .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; box-shadow: 0 0 8px currentColor; }
  .tc-item .vn { font-weight: 600; } .tc-item .vt { margin-left: auto; font-size: 10px; color: var(--ink-3); }

  /* 控制台(超新星发布) */
  .console { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 45;
    display: flex; gap: 10px; }
  .pubbtn { padding: 11px 22px; font-size: 13px; font-weight: 700; }
</style>
</head>
<body class="theme-serene">
  <!-- 宇宙背景 -->
  <div class="cosmos" id="cosmos">
    <div class="starfield" id="starfield"></div>
    <div class="nebula a"></div>
    <div class="nebula b"></div>
    <div class="vignette"></div>
  </div>

  <!-- 面包屑:回到恒星系 -->
  <div class="crumbs">
    <a href="#" id="crumbRealm">星域</a><span class="sep">→</span>
    <a href="prototype-overview.html" id="crumbGalaxy">银河</a><span class="sep">→</span>
    <a href="prototype-monitor.html" id="crumbSys">恒星系</a><span class="sep">→</span>
    <span class="cur" id="crumbsSvc">尘埃 · order-svc</span>
  </div>

  <!-- 主题切换 + 寓意图例 -->
  <div class="theme-switch">
    <div class="ttl">环境状态</div>
    <div class="theme-row" id="themeRow">
      <div class="theme-btn active" data-t="serene"    title="宁静 · 全部健康"></div>
      <div class="theme-btn" data-t="attention" title="微澜 · 存在告警"></div>
      <div class="theme-btn" data-t="crisis"    title="危机 · 严重故障"></div>
      <div class="theme-btn" data-t="supernova" title="超新星 · 发布中"></div>
      <div class="theme-btn" data-t="genesis"   title="扩容 · 弹性伸缩"></div>
    </div>
  </div>
  <div class="legend glass" id="legend"></div>

  <!-- 奇点坍缩舱舞台 -->
  <div class="stage">
    <canvas id="singularity"></canvas>

    <!-- 维度罗盘 -->
    <div class="dim-orb active" data-d="config"><span class="ic">◍</span><span class="lb">配置</span></div>
    <div class="dim-orb" data-d="monitor"><span class="ic">◉</span><span class="lb">监控</span></div>
    <div class="dim-orb" data-d="logs"><span class="ic">❈</span><span class="lb">日志</span></div>
    <div class="dim-orb" data-d="topology"><span class="ic">⊕</span><span class="lb">拓扑</span></div>

    <!-- 信息读出条 -->
    <div class="readout glass" id="readout"></div>

    <!-- 引力透镜编辑环 -->
    <div id="lens" class="holo">
      <div class="lens-head">
        <span class="badge">引力透镜</span>
        <span class="k" id="lensKey">key</span>
        <span class="holo-btn" id="lensClose" title="关闭">✕</span>
      </div>
      <div class="lens-body">
        <div class="lens-row">
          <label>当前值</label>
          <input id="lensVal" />
          <div class="old" id="lensOld"></div>
        </div>
        <div class="lens-row" id="lensDiffRow" style="display:none">
          <label>变更预览</label>
          <div class="diff" id="lensDiff"></div>
        </div>
      </div>
      <div class="lens-foot">
        <span class="btn ghost" id="lensCancel">取消</span>
        <span class="btn primary" id="lensConfirm">注入奇点</span>
      </div>
    </div>
  </div>

  <!-- 时间晶体 -->
  <div class="timecrystal glass">
    <div class="tc-h"><span class="tc-gem"></span><span>时间晶体 · 版本回溯</span></div>
    <div id="tcList"></div>
  </div>

  <!-- 控制台 -->
  <div class="console">
    <span class="btn primary pubbtn" id="pubBtn">✦ 超新星发布</span>
  </div>

  <!-- 星港坞 + 曲速命令 -->
  <div class="dock-hint" id="dockHint"></div>
  <nav class="stardock" id="dock">
    <div class="dock-pt" data-go="overview"><span>✦</span><span class="lbl">银河</span></div>
    <div class="dock-pt" data-go="monitor"><span>◉</span><span class="lbl">恒星系</span></div>
    <div class="dock-pt active" data-go="config"><span>◍</span><span class="lbl">尘埃</span></div>
  </nav>
  <div class="warp-scrim" id="warp">
    <div class="warp">
      <input id="warpInput" placeholder="曲速跃迁 · 搜索星体 / 服务 / 指令…" />
      <div class="warp-list" id="warpList"></div>
      <div class="warp-foot"><span><kbd>↑</kbd><kbd>↓</kbd> 导航</span><span><kbd>↵</kbd> 跃迁</span><span><kbd>Esc</kbd> 关闭</span></div>
    </div>
  </div>
  <div class="warp-flash" id="flash"></div>
  <div class="fx-layer" id="fx"></div>
  <div class="tip" id="tip"></div>

<script>
/* ===================== 数据(星尘真实风格示例) ===================== */
const qs=new URLSearchParams(location.search);
const svc=qs.get('svc')||'order-svc';
document.getElementById('crumbsSvc').textContent='尘埃 · '+svc;
// 星域(项目)参数:面包屑串接
const PROJ2=qs.get('project')||'galaxy-core';
const PNAME={'galaxy-core':'核心星域','payment':'支付星域','iot':'物联星域','ml-train':'训练星域'}[PROJ2]||'核心星域';
document.getElementById('crumbRealm').textContent='星域:'+PNAME;
document.getElementById('crumbRealm').href='prototype-console.html?project='+PROJ2;
document.getElementById('crumbGalaxy').href='prototype-overview.html?project='+PROJ2;
document.getElementById('crumbSys').href='prototype-monitor.html?node='+(qs.get('node')||'n3')+'&project='+PROJ2;

// 配置快照(不同版本的配置值用于时间晶体回溯)
const VERSION_CONFIG={
  'v3.4.0':[
    {key:'ConnString',val:'Server=pg-order;Database=order',group:'db'},
    {key:'Redis',val:'order-cache:6379',group:'cache'},
    {key:'LogLevel',val:'Information',group:'log'},
    {key:'MaxPool',val:'128',group:'perf'},
    {key:'Timeout',val:'30s',group:'perf'},
    {key:'Feature.NewUI',val:'true',group:'feature'},
    {key:'RateLimit',val:'5000',group:'perf'},
    {key:'Consul',val:'http://consul:8500',group:'disc'},
  ],
  'v3.3.2':[
    {key:'ConnString',val:'Server=pg-order;Database=order',group:'db'},
    {key:'Redis',val:'order-cache:6379',group:'cache'},
    {key:'LogLevel',val:'Warning',group:'log'},
    {key:'MaxPool',val:'64',group:'perf'},
    {key:'Timeout',val:'15s',group:'perf'},
    {key:'Feature.NewUI',val:'false',group:'feature'},
    {key:'RateLimit',val:'2000',group:'perf'},
    {key:'Consul',val:'http://consul:8500',group:'disc'},
  ],
  'v3.2.0':[
    {key:'ConnString',val:'Server=pg-old;Database=order',group:'db'},
    {key:'Redis',val:'redis-old:6379',group:'cache'},
    {key:'LogLevel',val:'Information',group:'log'},
    {key:'MaxPool',val:'32',group:'perf'},
    {key:'Timeout',val:'10s',group:'perf'},
    {key:'Feature.NewUI',val:'false',group:'feature'},
    {key:'RateLimit',val:'800',group:'perf'},
    {key:'Consul',val:'http://consul-old:8500',group:'disc'},
  ],
  'v3.0.1':[
    {key:'ConnString',val:'Server=pg-old;Database=order',group:'db'},
    {key:'Redis',val:'redis-old:6379',group:'cache'},
    {key:'LogLevel',val:'Error',group:'log'},
    {key:'MaxPool',val:'16',group:'perf'},
    {key:'Timeout',val:'10s',group:'perf'},
    {key:'Feature.NewUI',val:'false',group:'feature'},
    {key:'RateLimit',val:'300',group:'perf'},
    {key:'Consul',val:'http://consul-old:8500',group:'disc'},
  ],
};
const VERSIONS=[
  {v:'v3.4.0',when:'2 分钟前',by:'笑笑',cur:true},
  {v:'v3.3.2',when:'3 天前',by:'构建机'},
  {v:'v3.2.0',when:'2 周前',by:'阿强'},
  {v:'v3.0.1',when:'1 个月前',by:'阿强'},
];
const DEPS=[
  {name:'pg-order',kind:'数据库'},{name:'order-cache',kind:'缓存'},
  {name:'consul',kind:'注册中心'},{name:'payment-svc',kind:'支付'},
  {name:'notify-svc',kind:'通知'},
];
const METRICS=[
  {name:'CPU',val:42,unit:'%',color:'star'},{name:'内存',val:61,unit:'%',color:'accent'},
  {name:'QPS',val:1280,unit:'',color:'ok'},{name:'P99',val:86,unit:'ms',color:'warn'},
];
const LOGS=[
  {lv:'ok',ts:'12:04:21',msg:'配置热更新生效 ConnString'},
  {lv:'w',ts:'12:03:58',msg:'连接池使用率 78%,接近阈值'},
  {lv:'ok',ts:'12:02:10',msg:'心跳上报成功 节点 order-07'},
  {lv:'e',ts:'11:58:33',msg:'payment-svc 调用超时 1 次,已剔除'},
  {lv:'ok',ts:'11:55:02',msg:'版本 v3.4.0 部署完成'},
];
const GROUP_COLOR={db:'star',cache:'ok',log:'accent',perf:'warn',feature:'star2',disc:'beam'};

/* ===================== 主题 / 寓意 / 公共件 ===================== */
const THEME_LEGEND={
  serene:    {c:'var(--ok)',t:'集群宁静',d:'蓝紫星海如呼吸般缓慢。所有节点健康,引力场稳定,可安心观测。'},
  attention: {c:'var(--warn)',t:'存在告警',d:'暮色琥珀亮起。部分服务微澜,引力场出现扰动涟漪,需留意。'},
  crisis:    {c:'var(--danger)',t:'严重故障',d:'黑洞红吸积盘揭示危机。奇点坍缩加剧,光束断裂,立即处置。'},
  supernova: {c:'var(--star)',t:'发布中',d:'炽金爆发。超新星发布冲击波正在重组星核,配置向全集群扩散。'},
  genesis:   {c:'var(--ok)',t:'扩容中',d:'星海新绿泛起引力波。弹性扩容涟漪展开,新节点正被吸入轨道。'},
};
function setTheme(t){
  document.body.className='theme-'+t;
  document.querySelectorAll('.theme-btn').forEach(b=>b.classList.toggle('active',b.dataset.t===t));
  const L=THEME_LEGEND[t];
  document.getElementById('legend').innerHTML=
    `<div class="lh"><span class="sw" style="background:${L.c};color:${L.c}"></span><span>色彩寓意 · ${L.t}</span></div>
     <div class="desc">${L.d}</div>`;
  C=readColors();
}
document.getElementById('themeRow').querySelectorAll('.theme-btn').forEach(b=>b.onclick=()=>setTheme(b.dataset.t));

/* ===================== 颜色读取(canvas 用) ===================== */
function readColors(){
  const s=getComputedStyle(document.body); const g=n=>s.getPropertyValue(n).trim();
  return {star:g('--star'),star2:g('--star-2'),accent:g('--accent'),beam:g('--beam'),
    ink2:g('--ink-2'),ink3:g('--ink-3'),ok:g('--ok'),warn:g('--warn'),danger:g('--danger'),
    line:g('--line'),line2:g('--line-2'),bg:g('--bg-abyss')};
}
let C=readColors();

/* ===================== 星空 / 星港坞 / 曲速 / 闪屏(复用体系) ===================== */
(function buildStars(){
  const sf=document.getElementById('starfield');
  for(let i=0;i<140;i++){
    const s=document.createElement('div'); s.className='star';
    const sz=Math.random()*2+0.5;
    s.style.cssText=`left:${Math.random()*100}%;top:${Math.random()*100}%;width:${sz}px;height:${sz}px;--tw:${2+Math.random()*3}s;opacity:${.3+Math.random()*.7}`;
    sf.appendChild(s);
  }
})();
const dock=document.getElementById('dock'),dockHint=document.getElementById('dockHint');
dockHint.addEventListener('mouseenter',()=>dock.classList.add('show'));
document.addEventListener('mousemove',e=>{ if(e.clientY>window.innerHeight-60) dock.classList.add('show'); });
dock.querySelectorAll('.dock-pt').forEach(p=>p.onclick=()=>{ const g=p.dataset.go; if(g==='config')return; jumpFlash(g+'.html?project='+PROJ2); });

const warp=document.getElementById('warp'),warpInput=document.getElementById('warpInput'),warpList=document.getElementById('warpList');
const COMMANDS=[
  {ic:'✦',k:'视图',t:'返回银河视角',go:'prototype-overview.html?project='+PROJ2},
  {ic:'◉',k:'视图',t:'返回恒星系',go:'prototype-monitor.html?project='+PROJ2},
  {ic:'⌘',k:'指令',t:'切换主题 · 宁静',go:'#theme:serene'},
  {ic:'⌘',k:'指令',t:'切换主题 · 微澜',go:'#theme:attention'},
  {ic:'⌘',k:'指令',t:'切换主题 · 危机',go:'#theme:crisis'},
  {ic:'⌘',k:'指令',t:'切换主题 · 超新星',go:'#theme:supernova'},
  {ic:'⌘',k:'指令',t:'切换主题 · 扩容',go:'#theme:genesis'},
  {ic:'✦',k:'指令',t:'超新星发布 '+svc,go:'#publish'},
];
let warpSel=0;
function renderWarp(q=''){
  const list=COMMANDS.filter(c=>c.t.toLowerCase().includes(q.toLowerCase()));
  warpList.innerHTML=list.map((c,i)=>`<div class="warp-item ${i===warpSel?'sel':''}" data-go="${c.go}">
    <span class="ic">${c.ic}</span><span style="flex:1">${c.t}</span><span class="k">${c.k}</span></div>`).join('')||'<div class="warp-item">无匹配</div>';
  warpList.querySelectorAll('.warp-item').forEach(it=>it.onclick=()=>execGo(it.dataset.go));
}
function execGo(go){
  if(go && go.startsWith('#theme:')){ setTheme(go.split(':')[1]); closeWarp(); return; }
  if(go==='#publish'){ closeWarp(); publish(); return; }
  jumpFlash(go);
}
function openWarp(){ warp.classList.add('show'); document.getElementById('cosmos').classList.add('warp-fold'); renderWarp(); warpInput.value=''; warpInput.focus(); warpSel=0; }
function closeWarp(){ warp.classList.remove('show'); document.getElementById('cosmos').classList.remove('warp-fold'); }
warpInput.addEventListener('keydown',e=>{
  const items=COMMANDS.filter(c=>c.t.toLowerCase().includes(warpInput.value.toLowerCase()));
  if(e.key==='ArrowDown'){ warpSel=Math.min(warpSel+1,items.length-1); renderWarp(warpInput.value); }
  else if(e.key==='ArrowUp'){ warpSel=Math.max(warpSel-1,0); renderWarp(warpInput.value); }
  else if(e.key==='Enter'){ const it=warpList.querySelector('.sel'); if(it) execGo(it.dataset.go); }
  else if(e.key==='Escape'){ closeWarp(); }
});
warpInput.addEventListener('input',()=>{ warpSel=0; renderWarp(warpInput.value); });
warp.addEventListener('click',e=>{ if(e.target===warp) closeWarp(); });
document.addEventListener('keydown',e=>{
  if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='k'){ e.preventDefault(); warp.classList.contains('show')?closeWarp():openWarp(); }
});
function jumpFlash(go){ const f=document.getElementById('flash'); f.classList.add('go');
  document.getElementById('cosmos').classList.add('warp-fold');
  setTimeout(()=>location.href=go,520); }

/* ===================== 奇点坍缩舱 Canvas ===================== */
const cv=document.getElementById('singularity'),ctx=cv.getContext('2d');
let W,H,DPR,cx,cy,R;
function resize(){
  DPR=Math.min(window.devicePixelRatio||1,2);
  const r=cv.getBoundingClientRect(); W=r.width; H=r.height;
  cv.width=W*DPR; cv.height=H*DPR; ctx.setTransform(DPR,0,0,DPR,0,0);
  cx=W/2; cy=H/2; R=Math.min(W,H)*0.26;
}
window.addEventListener('resize',resize); resize();

let paused=false;
const mouse={x:-9999,y:-9999,on:false};
cv.addEventListener('mousemove',e=>{ const r=cv.getBoundingClientRect(); mouse.x=e.clientX-r.left; mouse.y=e.clientY-r.top; mouse.on=true; });
cv.addEventListener('mouseleave',()=>{ mouse.on=false; mouse.x=-9999; mouse.y=-9999; });

// 配置节点(沿环公转)
let cfgNodes=[];
function buildNodes(ver){
  const src=VERSION_CONFIG[ver]||VERSION_CONFIG['v3.4.0'];
  cfgNodes=src.map((c,i)=>({...c,
    ang:i/src.length*Math.PI*2,
    rad:R*0.62+(i%3)*20,
    spd:0.05+(i%4)*0.01,
    x:0,y:0,tx:0,ty:0,hov:false,changed:false}));
}
buildNodes('v3.4.0');

let dim='config';
document.querySelectorAll('.dim-orb').forEach(o=>o.onclick=()=>{
  dim=o.dataset.d;
  document.querySelectorAll('.dim-orb').forEach(x=>x.classList.toggle('active',x===o));
  updateReadout();
});

// 时间晶体
function buildCrystal(){
  const el=document.getElementById('tcList');
  el.innerHTML=VERSIONS.map(v=>`<div class="tc-item ${v.cur?'cur':''}" data-v="${v.v}">
    <span class="dot"></span><span class="vn">${v.v}</span><span class="vt">${v.when}</span></div>`).join('');
  el.querySelectorAll('.tc-item').forEach(it=>it.onclick=()=>rollback(it.dataset.v));
}
function rollback(ver){
  VERSIONS.forEach(v=>v.cur=(v.v===ver));
  buildCrystal(); buildNodes(ver); paused=false; closeLens();
  setTheme(ver==='v3.4.0'?'serene':'genesis');
  shock(cx,cy,180); updateReadout();
}
buildCrystal();

/* ---------- 绘制:吸积盘(透视椭圆 + 旋转尘埃) ---------- */
let dust=Array.from({length:160},()=>({a:Math.random()*Math.PI*2,r:Math.random(),s:Math.random()*0.4+0.1,z:Math.random()*0.5+0.5}));
function drawAccretion(now){
  const rx=R*1.9, ry=R*0.62;
  // 三道椭圆环
  for(let i=0;i<3;i++){
    ctx.save(); ctx.translate(cx,cy); ctx.scale(1, ry/rx);
    ctx.beginPath(); ctx.arc(0,0,rx*(0.7+i*0.18),0,Math.PI*2);
    ctx.strokeStyle=C.line2; ctx.lineWidth=1; ctx.globalAlpha=0.5-i*0.12; ctx.stroke();
    ctx.restore();
  }
  // 旋转尘埃
  const t=now/1000;
  dust.forEach(d=>{
    const a=d.a+t*d.s*0.3; const rr=rx*(0.55+d.r*0.5);
    const x=cx+Math.cos(a)*rr, y=cy+Math.sin(a)*rr*(ry/rx);
    ctx.globalAlpha=0.08+d.z*0.22;
    ctx.fillStyle=C.star2;
    ctx.beginPath(); ctx.arc(x,y,d.z*1.4,0,Math.PI*2); ctx.fill();
  });
  ctx.globalAlpha=1;
}

/* ---------- 绘制:奇点(核心) ---------- */
function drawSingularity(now){
  const t=now/600;
  const pulse=1+Math.sin(t)*0.04;
  const r=R*0.20*pulse;
  // 外辉光
  const g=ctx.createRadialGradient(cx,cy,0,cx,cy,r*4);
  g.addColorStop(0,C.star); g.addColorStop(0.2,C.star); g.addColorStop(1,'transparent');
  ctx.globalAlpha=0.5; ctx.fillStyle=g; ctx.beginPath(); ctx.arc(cx,cy,r*4,0,Math.PI*2); ctx.fill();
  ctx.globalAlpha=1;
  // 事件视界(暗核)
  ctx.fillStyle='rgba(2,4,10,0.96)'; ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.fill();
  ctx.strokeStyle=C.star; ctx.lineWidth=2; ctx.shadowBlur=24; ctx.shadowColor=C.star;
  ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.stroke(); ctx.shadowBlur=0;
  // 自转光纹
  ctx.save(); ctx.translate(cx,cy); ctx.rotate(t*0.6);
  ctx.strokeStyle=C.accent; ctx.globalAlpha=0.6; ctx.lineWidth=1.5;
  ctx.beginPath(); ctx.arc(0,0,r*1.25,0.4,2.2); ctx.stroke();
  ctx.beginPath(); ctx.arc(0,0,r*1.25,Math.PI+0.4,Math.PI+2.2); ctx.stroke();
  ctx.restore(); ctx.globalAlpha=1;
}

/* ---------- 绘制:配置节点(公转 + 引力扰动) ---------- */
function drawConfigNodes(dt,now){
  cfgNodes.forEach(n=>{
    if(!paused) n.ang+=n.spd*dt;
    const bx=cx+Math.cos(n.ang)*n.rad, by=cy+Math.sin(n.ang)*n.rad;
    // 引力扰动:鼠标吸引节点产生涟漪(透镜开启时冻结聚焦)
    let tx=bx,ty=by;
    if(mouse.on && !paused){
      const dx=mouse.x-bx, dy=mouse.y-by, d=Math.hypot(dx,dy);
      if(d<140){ const pull=(1-d/140)*34; tx=bx+dx/d*pull; ty=by+dy/d*pull; }
    }
    n.x+=(tx-n.x)*0.18; n.y+=(ty-n.y)*0.18;
    n.tx=tx; n.ty=ty;
    const col=C[GROUP_COLOR[n.group]||'star'];
    const hov=Math.hypot(mouse.x-n.x,mouse.y-n.y)<16; n.hov=hov;
    const sz=hov?7:5;
    ctx.shadowBlur=hov?22:14; ctx.shadowColor=col;
    ctx.fillStyle=col; ctx.beginPath(); ctx.arc(n.x,n.y,sz,0,Math.PI*2); ctx.fill();
    ctx.shadowBlur=0;
    if(n.changed){ ctx.strokeStyle=C.warn; ctx.lineWidth=1.5; ctx.beginPath(); ctx.arc(n.x,n.y,sz+5,0,Math.PI*2); ctx.stroke(); }
    if(hov||paused){ ctx.fillStyle=C.ink2; ctx.font='11px Inter, sans-serif'; ctx.textAlign='center';
      ctx.fillText(n.key,n.x,n.y-sz-8); }
  });
  // 引力光束:鼠标到最近节点的牵引线
  if(mouse.on){
    let best=null,bd=1e9;
    cfgNodes.forEach(n=>{ const d=Math.hypot(mouse.x-n.x,mouse.y-n.y); if(d<bd){bd=d;best=n;} });
    if(best && bd<140){
      ctx.strokeStyle=C.beam; ctx.globalAlpha=0.4; ctx.lineWidth=1;
      ctx.beginPath(); ctx.moveTo(mouse.x,mouse.y); ctx.lineTo(best.x,best.y); ctx.stroke(); ctx.globalAlpha=1;
    }
  }
}

/* ---------- 绘制:监控(环形仪表 + 流动粒子) ---------- */
function drawMonitor(now){
  const t=now/1000;
  METRICS.forEach((m,i)=>{
    const a=i/METRICS.length*Math.PI*2 + Math.PI/4;
    const mx=cx+Math.cos(a)*R*0.95, my=cy+Math.sin(a)*R*0.95;
    const rad=34, col=C[m.color];
    const frac=m.val/ (m.unit==='ms'?200:(m.unit===''?2000:100));
    ctx.strokeStyle=C.line2; ctx.lineWidth=5; ctx.beginPath(); ctx.arc(mx,my,rad,0,Math.PI*2); ctx.stroke();
    ctx.strokeStyle=col; ctx.lineWidth=5; ctx.lineCap='round'; ctx.shadowBlur=12; ctx.shadowColor=col;
    ctx.beginPath(); ctx.arc(mx,my,rad,-Math.PI/2,-Math.PI/2+Math.PI*2*Math.min(frac,1)); ctx.stroke();
    ctx.shadowBlur=0;
    // 流动粒子
    const pa=-Math.PI/2+Math.PI*2*((t*0.3)%1)*Math.min(frac,1);
    ctx.fillStyle=col; ctx.beginPath(); ctx.arc(mx+Math.cos(pa)*rad,my+Math.sin(pa)*rad,3,0,Math.PI*2); ctx.fill();
    ctx.fillStyle=C.ink2; ctx.font='10px Inter'; ctx.textAlign='center';
    ctx.fillText(m.name,mx,my-rad-8);
    ctx.fillStyle=C.ink; ctx.font='bold 13px Inter'; ctx.fillText(m.val+m.unit,mx,my+4);
  });
}

/* ---------- 绘制:日志(螺旋粒子喷流) ---------- */
let logParts=Array.from({length:60},()=>({a:Math.random()*Math.PI*2,r:Math.random(),s:Math.random()*0.5+0.3}));
function drawLogs(dt){
  logParts.forEach(p=>{
    p.r+=p.s*dt*0.4; if(p.r>1){p.r=0;p.a=Math.random()*Math.PI*2;}
    const rr=R*0.3+p.r*R*1.7; const x=cx+Math.cos(p.a+p.r*6)*rr, y=cy+Math.sin(p.a+p.r*6)*rr*0.6;
    ctx.globalAlpha=(1-p.r)*0.7; ctx.fillStyle=C.ok; ctx.shadowBlur=8; ctx.shadowColor=C.ok;
    ctx.beginPath(); ctx.arc(x,y,1.6,0,Math.PI*2); ctx.fill();
  });
  ctx.globalAlpha=1; ctx.shadowBlur=0;
}

/* ---------- 绘制:拓扑(依赖连线 + 流动) ---------- */
function drawTopology(dt,now){
  const t=now/1000;
  DEPS.forEach((d,i)=>{
    const a=i/DEPS.length*Math.PI*2 - Math.PI/2;
    const dx=cx+Math.cos(a)*R*1.5, dy=cy+Math.sin(a)*R*1.5*0.7;
    const col=C.beam;
    ctx.strokeStyle=C.line; ctx.lineWidth=1; ctx.globalAlpha=0.5;
    ctx.beginPath(); ctx.moveTo(cx,cy); ctx.lineTo(dx,dy); ctx.stroke(); ctx.globalAlpha=1;
    // 流动调用粒子
    const f=((t*0.4+i*0.2)%1);
    const px=cx+(dx-cx)*f, py=cy+(dy-cy)*f;
    ctx.fillStyle=C.accent; ctx.shadowBlur=8; ctx.shadowColor=C.accent;
    ctx.beginPath(); ctx.arc(px,py,3,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
    const hov=Math.hypot(mouse.x-dx,mouse.y-dy)<18;
    ctx.fillStyle=hov?C.star:C.ink2; ctx.font=(hov?'bold ':'')+'11px Inter'; ctx.textAlign='center';
    ctx.fillText(d.name,dx,dy+ (dy>cy?22:-14));
    if(hov){ ctx.fillStyle=C.ink3; ctx.font='10px Inter'; ctx.fillText(d.kind,dx,dy+(dy>cy?34:-26)); }
    d._x=dx; d._y=dy;
  });
  // 中心本服务标记
  ctx.fillStyle=C.star; ctx.font='bold 12px Inter'; ctx.textAlign='center';
  ctx.fillText(svc,cx,cy+R*0.2+18);
}

/* ---------- 主循环 ---------- */
let last=performance.now();
function frame(now){
  const dt=Math.min((now-last)/1000,0.05); last=now;
  ctx.clearRect(0,0,W,H);
  drawAccretion(now);
  if(dim==='config') drawConfigNodes(dt,now);
  else if(dim==='monitor') drawMonitor(now);
  else if(dim==='logs') drawLogs(dt);
  else if(dim==='topology') drawTopology(dt,now);
  drawSingularity(now);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

/* ---------- 点击:配置节点 → 引力透镜 ---------- */
cv.addEventListener('click',e=>{
  const r=cv.getBoundingClientRect(); const mx=e.clientX-r.left, my=e.clientY-r.top;
  if(dim==='config'){
    let hit=null,bd=18;
    cfgNodes.forEach(n=>{ const d=Math.hypot(mx-n.x,my-n.y); if(d<bd){bd=d;hit=n;} });
    if(hit) openLens(hit);
  } else if(dim==='topology'){
    let hit=null,bd=18;
    DEPS.forEach(d=>{ if(d._x==null)return; const dd=Math.hypot(mx-d._x,my-d._y); if(dd<bd){bd=dd;hit=d;} });
    if(hit) showReadoutDep(hit);
  }
});

/* ---------- 引力透镜编辑 ---------- */
const lens=document.getElementById('lens');
let lensNode=null, lensOriginal='';
function openLens(n){
  lensNode=n; paused=true; lensOriginal=n.val;
  document.getElementById('lensKey').textContent=n.key;
  const lv=document.getElementById('lensVal'); lv.value=n.val;
  document.getElementById('lensOld').innerHTML='原始值:<s>'+n.val+'</s>';
  document.getElementById('lensDiffRow').style.display='none';
  lens.classList.remove('closing'); lens.classList.add('open');
}
function closeLens(){
  lens.classList.add('closing'); lens.classList.remove('open');
  setTimeout(()=>{ lens.classList.remove('closing'); },300);
  paused=false;
}
document.getElementById('lensClose').onclick=closeLens;
document.getElementById('lensCancel').onclick=closeLens;
document.getElementById('lensVal').addEventListener('input',e=>{
  const nv=e.target.value;
  if(nv!==lensOriginal){
    document.getElementById('lensDiffRow').style.display='block';
    document.getElementById('lensDiff').textContent=lensOriginal+'  →  '+nv;
  } else document.getElementById('lensDiffRow').style.display='none';
});
document.getElementById('lensConfirm').onclick=()=>{
  if(lensNode){ lensNode.val=document.getElementById('lensVal').value; lensNode.changed=true; }
  shock(lensNode?lensNode.x:cx, lensNode?lensNode.y:cy, 90);
  // 奇点闪光
  shock(cx,cy,120);
  closeLens();
};

/* ---------- 信息读出条 ---------- */
const readout=document.getElementById('readout');
function updateReadout(){
  if(dim==='config'){
    const ch=cfgNodes.filter(n=>n.changed).length;
    readout.innerHTML=`<div class="rh">◍ <b>配置星盘</b> · ${svc} · 共 ${cfgNodes.length} 项${ch?` · <span style="color:var(--warn)">${ch} 项待发布</span>`:''}</div>
      拖拽鼠标牵引配置质点产生引力涟漪;点击发光质点唤起<b>引力透镜</b>就地编辑并注入奇点。`;
  } else if(dim==='monitor'){
    readout.innerHTML=`<div class="rh">◉ <b>实时监控</b> · ${svc}</div>`+
      METRICS.map(m=>`${m.name} <b style="color:var(--${m.color})">${m.val}${m.unit}</b>`).join(' · ');
  } else if(dim==='logs'){
    readout.innerHTML=`<div class="rh">❈ <b>日志喷流</b> · ${svc}</div>`+
      LOGS.slice().reverse().map(l=>`<div class="logline"><span class="ts">${l.ts}</span> <span class="lv${l.lv==='w'?' lv-w':l.lv==='e'?' lv-e':''}">${l.lv.toUpperCase()}</span> ${l.msg}</div>`).join('');
  } else if(dim==='topology'){
    readout.innerHTML=`<div class="rh">⊕ <b>依赖拓扑</b> · ${svc}</div>中心为本服务,外环为上下游依赖,光束沿调用方向流动;点击外环节点查看类型。`;
  }
}
function showReadoutDep(d){
  readout.innerHTML=`<div class="rh">⊕ <b>${d.name}</b> · ${d.kind}</div>该依赖当前链路健康,调用粒子正沿引力光束流入本服务奇点。`;
}
updateReadout();

/* ---------- 超新星发布 ---------- */
const fx=document.getElementById('fx');
function shock(x,y,max){
  const s=document.createElement('div'); s.className='shock';
  s.style.left=x+'px'; s.style.top=y+'px'; s.style.width=s.style.height=max+'px';
  fx.appendChild(s); setTimeout(()=>s.remove(),1200);
}
document.getElementById('pubBtn').onclick=publish;
function publish(){
  setTheme('supernova');
  // 多次冲击波 + 节点抛射
  for(let i=0;i<4;i++) setTimeout(()=>shock(cx,cy,120+i*120),i*140);
  cfgNodes.forEach(n=>{ n.changed=false; n.rad+=60; n.spd*=2.4; });
  setTimeout(()=>cfgNodes.forEach(n=>{ n.rad-=60; n.spd/=2.4; }),900);
  updateReadout();
}

/* 初始 */
setTheme('serene');
</script>
</body>
</html>