Skip to content

静态链接二进制可执行文件 减少WebUI所需的依赖(对比官方构建缩小10倍体积) #5

@uxhzbaz

Description

@uxhzbaz

Statically linked binary executable,Reduce WebUI dependencies.
体积对比图
减少编译步骤图例

这个HTML-WebUI可以控制CFnat 使用它需要修改源码

修改 src/core/service.rs263行

    pub fn stop(&self) -> Result<(), String> {
        if !self.is_running() {
            return Err("服务未运行".to_string());
        }

        // 先取消 ServiceState 的 CancellationToken,让 forward 和 httping 任务收到停止信号
        if let Some(token) = self.cancel_token.read().as_ref() {
            token.cancel();
        }

        // 再取消 LoadBalancer 自己的 CancellationToken,让健康检查和 sticky 维护器退出
        if let Some(lb) = self.loadbalancer.read().as_ref() {
            lb.stop();
        }
        
        self.running.store(false, Ordering::Relaxed);

        // 等待所有已注册的任务退出(带超时)
        let handles: Vec<JoinHandle<()>> = self.task_handles.write().drain(..).collect();
        for handle in handles {
            let rt = tokio::runtime::Handle::try_current();
            if let Ok(rt) = rt {
                let _ = rt.block_on(tokio::time::timeout(Duration::from_secs(3), handle));
            }
        }

        *self.ip_pool.write() = None;
        *self.loadbalancer.write() = None;
        *self.cancel_token.write() = None;
        *self.start_time.write() = None;

        push_log("INFO", "服务已停止");
        
        Ok(())
    }

替换为:

    pub async fn stop(&self) -> Result<(), String> {
        if !self.is_running() {
            return Err("服务未运行".to_string());
        }

        if let Some(token) = self.cancel_token.read().as_ref() {
            token.cancel();
        }

        if let Some(lb) = self.loadbalancer.read().as_ref() {
            lb.stop();
        }
        
        self.running.store(false, Ordering::Relaxed);

        let handles: Vec<JoinHandle<()>> = self.task_handles.write().drain(..).collect();
        for handle in handles {
            let _ = tokio::time::timeout(Duration::from_secs(3), handle).await;
        }

        *self.ip_pool.write() = None;
        *self.loadbalancer.write() = None;
        *self.cancel_token.write() = None;
        *self.start_time.write() = None;

        push_log("INFO", "服务已停止");
        
        Ok(())
    }

修改 src/api/handlers.rs46行:

pub async fn stop_service(State(state): State<AppState>) -> Json<ApiResponse> {
    match state.service.stop() {

替换为:

pub async fn stop_service(State(state): State<AppState>) -> Json<ApiResponse> {
    match state.service.stop().await {

以下是呕心沥血鞭挞AI写的凑合用WebUI

<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=yes"><title>CFnat</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"><style>*{box-sizing:border-box;margin:0}body{font-family:Inter,system-ui,-apple-system,sans-serif;margin:0;min-height:100vh;transition:background .2s,color .2s}body.dark{--bg:#0E1117;--sf:#161B22;--se:#1C2128;--br:#2A2F3A;--bl:#3A3F4A;--t1:#e6edf3;--t2:#8b949e;--ac:#2D7DFF;--sc:#238636;--dg:#da3633;--wn:#d29922;--pp:#a371f7;--gn:#3fb950;--sh:0 4px 12px rgba(0,0,0,0.4)}body.light{--bg:#f6f8fa;--sf:#fff;--se:#f0f2f5;--br:#d0d7de;--bl:#d8dee4;--t1:#1F2328;--t2:#59636e;--ac:#0969da;--sc:#1a7f37;--dg:#cf222e;--wn:#9a6700;--pp:#8250df;--gn:#1a7f37;--sh:0 4px 12px rgba(0,0,0,0.08)}body{background:var(--bg);color:var(--t1)}.app{max-width:1600px;margin:0 auto;padding:16px 20px;display:flex;flex-direction:column;gap:18px}.hd{display:flex;align-items:center;justify-content:space-between}.ttl{font-weight:600;font-size:1.6rem;letter-spacing:-0.5px;background:linear-gradient(135deg,var(--ac),var(--pp));-webkit-background-clip:text;background-clip:text;color:transparent}.thm{background:var(--sf);border:1px solid var(--br);color:var(--t1);padding:8px 14px;border-radius:30px;font-size:14px;cursor:pointer;display:flex;align-items:center;gap:8px}.thm:hover{background:var(--se)}.conn{margin-left:12px;font-size:13px;display:flex;align-items:center;gap:4px}.grid{display:grid;grid-template-columns:1fr 1.4fr;gap:18px}@media (max-width:900px){.grid{grid-template-columns:1fr}}.card{background:var(--sf);border:1px solid var(--br);border-radius:16px;padding:18px 20px;box-shadow:var(--sh)}.chd{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;font-weight:600;font-size:16px;border-bottom:1px solid var(--br);padding-bottom:12px;cursor:pointer}.chd i{margin-right:8px;color:var(--ac)}.chd .toggle-icon{margin-left:auto;transition:transform .2s}.card.collapsed :is(.fg,.lgc,.sg,.ipp,.legend-bar){display:none}.card.collapsed{padding:8px 20px}.card.collapsed .chd{margin:0;border:0;padding:0}
#mBtn{display:flex;align-items:center;gap:8px}#mLg{display:none}.card.collapsed #mLg{display:flex!important}.t-el{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60vw}#connIcon{display:inline-flex;align-items:center;margin-left:8px;font-size:14px}
.fg{display:grid;grid-template-columns:1fr 1fr;gap:12px 16px}.fw{grid-column:1/-1}.fld label{display:block;font-size:12px;color:var(--t2);margin-bottom:4px}.fld input,.fld textarea{width:100%;background:var(--bg);border:1px solid var(--br);color:var(--t1);padding:9px 12px;border-radius:10px;font-size:14px}.fld input:focus,.fld textarea:focus{border-color:var(--ac);outline:none}.fld textarea{resize:vertical;min-height:70px;font-family:monospace}.fr{display:flex;gap:8px;align-items:center}.fr input{flex:1}.bic{background:var(--se);border:1px solid var(--br);color:var(--t1);padding:8px 12px;border-radius:10px;cursor:pointer;white-space:nowrap}.badge{font-size:12px;background:var(--se);padding:2px 10px;border-radius:20px;color:var(--ac)}.acts{display:flex;gap:12px;margin:20px 0 8px}.btn{flex:1;padding:12px;border:none;border-radius:12px;font-weight:600;font-size:15px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:6px;transition:.15s}.btn-p{background:var(--sc);color:#fff}.btn-d{background:var(--dg);color:#fff}.btn-o{background:0 0;border:1px solid var(--br);color:var(--t1)}.btn:disabled{opacity:.5;cursor:not-allowed}.stb{padding:6px 14px;border-radius:40px;font-weight:600;font-size:14px}.st-run{background:rgba(35,134,54,.18);color:var(--gn);border:1px solid rgba(35,134,54,.4)}.st-stop{background:rgba(218,54,51,.15);color:#ff7b72;border:1px solid rgba(218,54,51,.4)}
.stb i{color:inherit}body.light .st-run{color:#1a7f37}body.light .st-stop{color:#cf222e}.sg{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:20px}.si{background:var(--bg);padding:12px;border-radius:14px;border:1px solid var(--br)}.sil{font-size:12px;color:var(--t2)}.siv{font-size:22px;font-weight:600}.ipp{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:8px}@media (max-width:600px){.ipp{grid-template-columns:1fr}}.ipl{background:var(--bg);border-radius:14px;border:1px solid var(--br);overflow:hidden}.iplh{padding:10px 14px;background:var(--se);font-weight:600;font-size:14px;border-bottom:1px solid var(--br);display:flex;justify-content:space-between}.ipt{width:100%;font-size:13px}.ipt th{text-align:left;padding:8px 10px;color:var(--t2);font-weight:500;border-bottom:1px solid var(--br)}.ipt td{padding:8px 10px;border-bottom:1px solid var(--bl)}.sticky{background:rgba(163,113,247,.1);border-left:4px solid var(--pp)}.dg{color:var(--gn)}.dw{color:var(--wn)}.dd{color:var(--dg)}.lgc{margin-top:18px;background:var(--bg);border-radius:14px;border:1px solid var(--br);padding:6px 0}.lgh{padding:8px 16px;display:flex;justify-content:space-between;border-bottom:1px solid var(--br)}.lg{height:200px;overflow-y:auto;padding:8px 12px;font-family:'SF Mono',monospace;font-size:12px}.lge{padding:2px 0;white-space:nowrap}.lgt{color:var(--t2);margin-right:12px}.lgi{color:#58a6ff}.lgw{color:var(--wn)}.lge{color:var(--dg)}.fr2{display:flex;align-items:center;gap:8px}.thg{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.thg input{width:90px;min-width:70px;flex:1}.unit{font-size:13px;color:var(--t2);margin-left:2px}.legend-bar{display:flex;flex-wrap:wrap;justify-content:space-around;align-items:center;gap:12px;margin-top:14px;font-size:13px;color:var(--t2)}.inline-tip{font-size:12px;color:var(--t2);margin-left:8px;white-space:nowrap}.inline-tip i{margin-right:4px}</style></head><body class="dark"><div class="app"><div class="hd"><div class="fr2"><span class="ttl"><i class="fas fa-bolt"></i> CFnat</span><span id="connIcon" style="font-size:14px;white-space:nowrap;margin-left:12px;"></span></div><div style="display:flex;gap:8px;align-items:center;"><i class="fas fa-sliders-h bic" id="togCfg" style="padding:8px;cursor:pointer;"></i><i class="fas fa-terminal bic" id="togLog" style="padding:8px;cursor:pointer;"></i><div class="bic" id="togSta" style="display:flex;align-items:center;gap:6px;cursor:pointer;"><i class="fas fa-circle-pause" style="color:var(--dg)"></i><span style="color:var(--dg);font-size:13px;font-weight:600">已停止</span></div><button class="thm" id="thmBtn"><i class="fas fa-moon"></i><span>暗色</span></button></div></div><div class="grid"><div style="display:flex;flex-direction:column;gap:18px"><div class="card" id="configCard"><div class="chd" id="configToggle"><span><i class="fas fa-sliders-h"></i> 服务配置</span><div id="mBtn"></div></div><div class="fg"><div class="fld fw"><label><i class="fas fa-folder-open"></i> 测速文件 / 手动输入</label><div class="fr"><input id="ipf" placeholder="文件路径或留空 手动IP优先"><button class="bic" id="upBtn"><i class="fas fa-upload"></i> 上传</button></div></div><div class="fld fw"><div style="display:flex;justify-content:space-between"><label><i class="fas fa-list-ul"></i> IP列表 (每行一个)</label><span id="ipc" class="badge">0 个</span></div><textarea id="ipa" placeholder="1.1.1.1&#10;2.2.2.2&#10;..."></textarea></div><div class="fld"><label><i class="fas fa-globe"></i> 测速地址</label><input id="http" placeholder="http://cp.cloudflare.com/cdn-cgi/trace"></div><div class="fld"><label><i class="fas fa-building"></i> 数据中心</label><input id="colo" placeholder="SJC,LAX"></div><div class="fld"><label><i class="fas fa-stopwatch"></i> 延迟</label><input id="dl" type="number" value="300" placeholder="ms"></div><div class="fld"><label><i class="fas fa-box"></i> 丢包率(%)</label><input id="tlr" type="number" min="0" max="100" step="0.1" value="5.0"></div><div class="fld"><label><i class="fas fa-gauge-high"></i> 测速并发</label><input id="thr" type="number" value="8"></div><div class="fld"><label><i class="fas fa-weight-scale"></i> 负载数量</label><input id="ips" type="number" value="10"></div><div class="fld"><label><i class="fas fa-fingerprint"></i> TLS端口</label><input id="tls" type="number" value="443"></div><div class="fld"><label><i class="fas fa-network-wired"></i> HTTP端口</label><input id="hport" type="number" value="80"></div><div class="fld fw"><div style="display:grid;grid-template-columns:1fr 1fr;gap:12px"><div><label><i class="fas fa-server"></i> 本地服务器</label><input id="addr"></div><div><label><i class="fas fa-archive"></i> 最大负载槽</label><input id="mss" type="number"></div></div></div><div class="fld fw"><label><i class="fas fa-stopwatch"></i> 延迟阈值</label><div class="thg"><span>绿 ≤</span><input id="gth" type="number" value="350" placeholder="ms"><span style="margin-left:12px">红 ≥</span><input id="yth" type="number" value="900" placeholder="ms"></div></div><div class="acts" style="grid-column:1/-1;"><button class="btn" id="runBtn"><i class="fas fa-play"></i> 启动</button><button class="btn btn-o" id="cfgBtn"><i class="fas fa-sync-alt"></i> 重启并应用</button></div></div></div><div class="card" id="logCard"><div class="chd" id="logToggle"><span><i class="fas fa-terminal"></i> 运行日志</span><div id="mLg" class="t-el" style="font-size:12px;color:var(--t2);font-weight:400;margin-left:10px;flex:1;text-align:right"></div></div><div class="lgc"><div class="lgh"><span>最新事件</span><button class="bic" id="clrLog" style="padding:4px 12px"><i class="fas fa-trash-alt"></i> 清空</button></div><div class="lg" id="logP"><div class="lge">等待日志...</div></div></div></div></div><div><div class="card" id="staCard"><div class="chd"><span><i class="fas fa-wave-square"></i> 实时状态</span><span id="stb" class="stb st-stop"><i class="fas fa-circle-pause"></i> 已停止</span></div><div class="sg"><div class="si"><div class="sil">运行时间</div><div class="siv" id="upt">00:00:00</div></div><div class="si"><div class="sil">下次检查</div><div class="siv" id="nxt">0s</div></div><div class="si"><div class="sil">主队列</div><div class="siv" id="pq">0/0</div></div><div class="si"><div class="sil">备选队列</div><div class="siv" id="bq">0/0</div></div><div class="si"><div class="sil">Sticky</div><div class="siv" id="stk">0</div></div><div class="si"><div class="sil">检查间隔</div><div class="siv" id="intv">30s</div></div></div><div class="ipp"><div class="ipl"><div class="iplh"><div style="display:flex;align-items:center;gap:8px"><span><i class="fas fa-scale-balanced" style="color:var(--sc)"></i> 负载均衡</span><button class="bic" id="expPBtn" style="padding:3px 10px;font-size:12px"><i class="fas fa-download"></i> 导出</button></div><span id="pt">0</span></div><div style="max-height:280px;overflow-y:auto"><table class="ipt" id="ptb"><thead><tr><th>IP/数据中心</th><th>延迟</th><th>丢包</th><th>采样</th></tr></thead><tbody id="pb"></tbody></table></div></div><div class="ipl"><div class="iplh"><div style="display:flex;align-items:center;gap:8px"><span><i class="fas fa-layer-group" style="color:var(--ac)"></i> 备选列表</span><button class="bic" id="expBBtn" style="padding:3px 10px;font-size:12px"><i class="fas fa-download"></i> 导出</button></div><span id="bt">0</span></div><div style="max-height:280px;overflow-y:auto"><table class="ipt" id="btb"><thead><tr><th>IP/数据中心</th><th>延迟</th><th>丢包</th><th>采样</th></tr></thead><tbody id="bb"></tbody></table></div></div></div><div class="legend-bar"><span><i class="fas fa-thumbtack" style="color:var(--pp)"></i> Sticky</span><span><i class="fas fa-circle-check" style="color:var(--gn)"></i><span id="gv">350</span>ms</span><span><i class="fas fa-circle-exclamation" style="color:var(--wn)"></i><span id="yv">900</span>ms</span><span><i class="fas fa-circle-xmark" style="color:var(--dg)"></i> ><span id="rv">900</span>ms</span></div></div></div></div></div><input type="file" id="hf" accept="text/*" style="display:none"><script>
(function(){
const D={};document.querySelectorAll('[id]').forEach(i=>D[i.id]=i);
let cur={running:!1},es,lt,cfgL=!1;
const H=(e,h)=>e.innerHTML=h,S=(e,c)=>e.className=c;
document.querySelectorAll('.chd').forEach((h,i)=>{h.onclick=()=>h.closest('.card').classList.toggle('collapsed');if(i<3)[D.togCfg,D.togLog,D.togSta][i].onclick=e=>{e.stopPropagation();h.click()}});
function setTheme(t){document.body.classList.remove('dark','light');document.body.classList.add(t);D.thmBtn.innerHTML=t==='light'?'<i class="fas fa-sun"></i><span>亮色</span>':'<i class="fas fa-moon"></i><span>暗色</span>';localStorage.setItem('cfnat-theme',t)}
setTheme(localStorage.getItem('cfnat-theme')||'dark');
D.thmBtn.onclick=()=>setTheme(document.body.classList.contains('dark')?'light':'dark');
function setConn(c){conn=c;const i=D.connIcon;i.innerHTML=c?'<i class="fas fa-link" style="color:var(--sc)"></i> 已连接':'<i class="fas fa-triangle-exclamation" style="color:var(--dg)"></i> 离线';i.title=c?'已连接':'离线';}
function fmtUp(s){let h=Math.floor(s/3600),m=Math.floor((s%3600)/60),sec=s%60;return `${h.toString().padStart(2,'0')}:${m.toString().padStart(2,'0')}:${sec.toString().padStart(2,'0')}`}
if(localStorage.th)[D.gth.value,D.yth.value]=localStorage.th.split(',');
function getTh(){let g=parseInt(D.gth.value)||350,y=parseInt(D.yth.value)||900;if(y<g)y=g;return{g,y};}
function updLeg(){let{g,y}=getTh();localStorage.th=`${g},${y}`;D.gv.textContent=g;D.yv.textContent=y;D.rv.textContent=y;if(cur&&cur.primary_ips)renderIPs(cur);}
D.gth.oninput=updLeg;D.yth.oninput=updLeg;updLeg();
function dc(d){if(d<=0)return'';let{g,y}=getTh();if(d<=g)return'dg';if(d<=y)return'dw';return'dd'}
function renderIPs(s){
  let st=new Set(s.sticky_ips||[]),r=(ips,tb)=>{let h=ips.length?ips.map(i=>`<tr class="${st.has(i.ip)?'sticky':''}"><td>${i.ip}${i.colo?' ['+i.colo+']':''}</td><td class="${dc(i.delay)}">${i.delay>0?i.delay.toFixed(0)+'ms':'-'}</td><td>${((i.loss||0)*100).toFixed(1)}%</td><td>${i.samples}</td></tr>`).join(''):'<tr><td colspan="4" style="text-align:center;padding:16px;color:var(--t2);">暂无数据</td></tr>';if(tb.innerHTML!==h)tb.innerHTML=h};
  r(s.primary_ips||[],D.pb);r(s.backup_ips||[],D.bb);
}
function applyStat(s){
  if(s.running)localStorage.setItem('cfnat-last',JSON.stringify(s));else s=Object.assign(JSON.parse(localStorage.getItem('cfnat-last')||'{}'),{running:!1});
  cur=s;let r=s.running,v=r?'run':'stop',t=r?'运行中':'已停止',g=r?'gn':'dg',is=`<i class="fas fa-circle-${r?'play':'pause'}"></i> `,ia=`<i class="fas fa-${r?'stop':'play'}"></i> `;
  S(D.stb,'stb st-'+v);H(D.stb,is+t);H(D.runBtn,ia+(r?' 停止':' 启动'));S(D.runBtn,'btn btn-'+(r?'d':'p'));
  H(D.togSta,`<span style="color:var(--${g});font-size:13px;font-weight:600">${is}${t}</span>`);
  H(D.mBtn,`<button class="btn btn-${r?'d':'p'}" style="padding:4px 14px;font-size:12px;width:auto">${ia}${r?' 停止':' 启动'}</button>`);
  [['upt',fmtUp(s.uptime_secs||0)],['nxt',(s.next_health_check||0)+'s'],['intv',(s.health_check_interval||30)+'s'],['pq',`${s.primary_count||0}/${s.primary_target||0}`],['bq',`${s.backup_count||0}/${s.backup_target||0}`],['stk',(s.sticky_ips||[]).length],['pt','目标 '+(s.primary_target||0)],['bt','目标 '+(s.backup_target||0)]].map(x=>D[x[0]].innerText=x[1]);
  if(s.primary_ips)renderIPs(s);updCnt();
}
function applyCfg(c,l){
  if(!c||(cfgL&&!l))return;
  'ipf:ip_file,http:http,dl:delay_limit,ips:ips,thr:threads,tls:tls_port,hport:http_port,mss:max_sticky_slots,addr:addr'.split(',').map(k=>{let[d,a]=k.split(':');D[d].value=c[a]??''});
  D.tlr.value=(c.tlr!=null?c.tlr*100:10).toFixed(1);D.colo.value=(c.colo||[]).join(',');
  if(l)cfgL=1;
}
function updCnt(){let n=D.ipa.value.split('\n').filter(l=>l.trim()).length;D.ipc.textContent=`${n} 个`}
D.ipa.oninput=updCnt;
const req=async(u,p,d)=>{try{return await(await fetch(u,{method:p?'POST':'GET',headers:d?{'Content-Type':'application/json'}:{},body:d?JSON.stringify(d):null})).json()}catch(e){return{message:'网络异常'}}}
async function fetchCfg(){let r=await req('/api/config');if(r&&!r.message)applyCfg(r,cfgL=1)}
function startSSE(){if(es)es.close();es=new EventSource('/api/stream');es.onopen=()=>setConn(!0);es.onerror=()=>cur.running&&setConn(!1);es.onmessage=e=>{try{let d=JSON.parse(e.data);if(d.status)applyStat(d.status);if(d.config&&!cur.running)applyCfg(d.config);setConn(!0)}catch(ex){}}}
async function fetchLogs(){let l=await req('/api/logs');if(!l||l.message)return;D.mLg.textContent=l.length?l[l.length-1].message:'';let h=l.length?l.map(i=>`<div class="lge"><span class="lgt">${i.timestamp}</span><span class="lg${i.level[0].toLowerCase()}">[${i.level}]</span> ${i.message.replace(/</g,'&lt;')}</div>`).join(''):'<div class="lge">暂无日志</div>';if(D.logP.innerHTML!==h){D.logP.innerHTML=h;D.logP.scrollTop=D.logP.scrollHeight}}
async function clearLogs(){await req('/api/logs/clear',1);fetchLogs()}
async function start(){
  let ipc=D.ipa.value.split('\n').map(l=>l.trim()).filter(l=>l),c=D.colo.value.trim(),t=parseFloat(D.tlr.value),
  p={ip_content:ipc.length?ipc:null,colo:c?c.split(',').map(s=>s.trim()):null,tlr:isNaN(t)?0.05:Math.min(1,Math.max(0,t/100))};
  'ipf:ip_file:s,http:http:s,dl:delay_limit:i,ips:ips:i,thr:threads:i,tls:tls_port:i,hport:http_port:i,mss:max_sticky_slots:i,addr:addr:s'.split(',').map(k=>{let[d,a,y]=k.split(':'),v=D[d].value;p[a]=y=='i'?parseInt(v)||null:v||null});
  Object.keys(p).forEach(k=>p[k]==null&&delete p[k]);
  D.runBtn.disabled=!0;let j=await req('/api/start',1,p);alert(j.message||'请求完毕');if(j.success){applyStat({running:!0});setTimeout(fetchCfg,500)}D.runBtn.disabled=!1
}
async function stop(){D.runBtn.disabled=!0;let j=await req('/api/stop',1);alert(j.message);if(j.success)applyStat({running:!1});D.runBtn.disabled=!1}
D.upBtn.onclick=()=>D.hf.click();
D.hf.onchange=async e=>{let f=D.hf.files[0];if(!f)return;D.ipa.value=await f.text();updCnt();D.ipf.value=f.name;D.hf.value=''}
function exp(ips,p,t){if(!ips||!ips.length)return alert(t+'列表为空');navigator.clipboard.writeText(ips.map(i=>`${i.ip}:${p}${i.colo?'#'+i.colo:''}`).join('\n')).then(()=>alert(`${ips.length}个IP已复制`),()=>alert('复制失败'))}
async function init(){applyStat({running:!1});setConn(!1);await fetchCfg();startSSE();fetchLogs();lt=setInterval(fetchLogs,3000);updCnt();}
D.mBtn.onclick=e=>{e.stopPropagation();D.runBtn.click()};
D.clrLog.onclick=clearLogs;
D.runBtn.onclick=()=>cur.running?stop():start();
D.cfgBtn.onclick=async()=>{if(cur.running)await stop();await start()};
D.expPBtn.onclick=()=>exp(cur.primary_ips,D.tls.value,'主');
D.expBBtn.onclick=()=>exp(cur.backup_ips,D.tls.value,'备选');
init();
})();
</script></body></html>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions