aws-runtime-bridge 1.9.122 → 1.9.124

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import{Router as F}from"express";import p from"node:os";import{execFile as _}from"node:child_process";import{getRuntimeBindingPublicState as T,getRuntimePairingCode as V}from"../services/runtime-binding.js";import{getAgentProcessManager as y}from"../services/agent-process-manager.js";import{collectSystemMetrics as K}from"../services/system-metrics.js";import{loadPersistedSessions as W}from"../services/terminal-persistence.js";import{createLogger as G}from"../utils/logger.js";import{getBridgeVersion as q}from"../config.js";import{sdkSessions as J}from"./terminal.js";import{createSession as Y,getPanelCredentials as P,validateSession as E,destroySession as L,validateCredentials as U,hasConfiguredCredentials as x,isDefaultCredentials as $,setPanelCredentials as Q,getAllSessions as X,getActiveSessionCount as Z,checkLoginAllowed as ee,recordLoginFailure as te,recordLoginSuccess as ne}from"../services/panel-auth.js";import{getRecentLogs as se}from"../utils/logger.js";import{autoRegister as ae,getRegistrationState as D,getAutoRegisterConfigFilePath as oe}from"../services/auto-register.js";const w=G("dashboard");let k=null;const re=1e4;let R=null,S=null;const ie=3e4;async function N(){K().then(n=>{R={data:n,ts:Date.now()}}).catch(()=>{}),y().discoverAllProcesses().then(n=>{S={data:n,ts:Date.now()}}).catch(()=>{}),y().cleanupStaleRegistry().catch(()=>{})}N(),setInterval(N,ie);const M=q();async function de(){const n=await import("node:fs/promises"),e=await import("node:path"),t=await import("node:os"),s=e.join(String(process.env.AWS_RUNTIME_HOME_DIR||t.homedir()).trim()||t.homedir(),".aws-bridge","lifecycle-state.json");try{const i=await n.readFile(s,"utf8"),a=JSON.parse(i),o=Array.isArray(a.operations)?a.operations:[];return o.length===0?null:(o.sort((l,c)=>new Date(c.updatedAt).getTime()-new Date(l.updatedAt).getTime()),o[0]||null)}catch{return null}}const d=F(),z="panel_sid";function le(n){const e={};if(!n)return e;for(const t of n.split(";")){const s=t.indexOf("=");if(s===-1)continue;const i=t.slice(0,s).trim(),a=t.slice(s+1).trim();i&&(e[i]=a)}return e}function I(n){return le(n.headers.cookie)[z]}function H(n,e,t){const s=n.secure||n.headers["x-forwarded-proto"]==="https";return`${z}=${e}; HttpOnly; SameSite=Lax; Path=/${s?"; Secure":""}; Max-Age=${t}`}function g(n,e){const t=I(n);return t&&E(t)?!0:n.xhr||n.headers.accept?.includes("application/json")?(e.status(401).json({ok:!1,error:"unauthorized",needLogin:!0}),!1):(e.redirect("/login"),!1)}d.post("/runtime/login",(n,e)=>{const t=n.socket.remoteAddress||"unknown",s=ee(t);if(!s.allowed){e.status(429).set("Retry-After",String(s.retryAfterSec)).json({ok:!1,error:"too many failed login attempts, try again later",retryAfterSec:s.retryAfterSec});return}const{username:i,password:a}=n.body||{};if(!i||!a){e.status(400).json({ok:!1,error:"username and password required"});return}if(!U(String(i),String(a))){te(t),e.status(401).json({ok:!1,error:"invalid credentials"});return}ne(t);const o=Y(),l=!x();e.setHeader("Set-Cookie",H(n,o,86400)),e.json({ok:!0,needSetup:l})}),d.post("/runtime/logout",(n,e)=>{const t=I(n);t&&L(t),e.setHeader("Set-Cookie",H(n,"",0)),e.json({ok:!0})}),d.get("/runtime/credentials-status",(n,e)=>{if(!g(n,e))return;const t=P();e.json({ok:!0,isDefault:$(),configured:x(),username:t.username})}),d.post("/runtime/change-credentials",(n,e)=>{if(!g(n,e))return;const{username:t,password:s,passwordConfirm:i,oldPassword:a}=n.body||{};if(s&&String(s).trim()){if(String(s)!==String(i)){e.status(400).json({ok:!1,error:"passwords do not match"});return}if(x()){if(!a){e.status(400).json({ok:!1,error:"old password required"});return}const l=P();if(!U(l.username,String(a))){e.status(403).json({ok:!1,error:"old password is incorrect"});return}}}if(!t||!String(t).trim()){e.status(400).json({ok:!1,error:"username required"});return}const o=s&&String(s).trim()?String(s).trim():P().password;Q(String(t).trim(),o),e.json({ok:!0})}),d.get("/runtime/node-info",(n,e)=>{if(!g(n,e))return;const t=T(),s=D();e.json({ok:!0,hostname:p.hostname(),platform:p.platform(),arch:p.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version,bridgeVersion:M,configPath:oe(),homeDir:process.env.AWS_RUNTIME_HOME_DIR||p.homedir(),bindingStatus:t.status,paired:t.paired,instanceId:t.instanceId||s.instanceId,schedulerBaseUrl:t.schedulerBaseUrl,registered:s.registered,pairingCode:t.pairingCode,activeSessions:Z()})}),d.get("/runtime/logs",(n,e)=>{if(!g(n,e))return;const t=n.query.level||void 0,s=Math.min(Number(n.query.count)||200,500),a=t&&["debug","info","warn","error"].includes(t)?t:void 0,o=se(s,a);e.json({ok:!0,logs:o})}),d.post("/runtime/re-register",async(n,e)=>{if(g(n,e))try{const t=await ae();e.json({ok:t})}catch(t){const s=t;e.status(500).json({ok:!1,error:s.message})}}),d.get("/runtime/admin/sessions",(n,e)=>{if(!g(n,e))return;const t=X().map(s=>({token:s.token.slice(0,8)+"...",createdAt:new Date(s.createdAt).toISOString(),lastUsedAt:new Date(s.lastUsedAt).toISOString(),age:Math.floor((Date.now()-s.createdAt)/1e3)}));e.json({ok:!0,sessions:t})}),d.post("/runtime/admin/sessions/:token/destroy",(n,e)=>{g(n,e)&&(L(n.params.token),e.json({ok:!0}))});function ce(n){return new Promise((e,t)=>{const s=p.platform()==="win32",i=s?"taskkill":"kill",a=s?["/PID",String(n),"/F"]:["-9",String(n)];_(i,a,{timeout:5e3},o=>o?t(o):e())})}d.post("/runtime/kill-process",async(n,e)=>{if(!g(n,e))return;const t=Number(n.body.pid);if(!t||!Number.isInteger(t)||t<=0){e.status(400).json({ok:!1,error:"Invalid PID"});return}const s=y(),i=()=>{const o=new Set,l=S?.data??[];for(const c of l)typeof c.pid=="number"&&o.add(c.pid);for(const c of s.getAllManagedProcesses())typeof c.pid=="number"&&o.add(c.pid);return o};let a=i();if(a.size===0)try{S={data:await s.discoverAllProcesses(),ts:Date.now()},a=i()}catch{}if(!a.has(t)){w.warn("Rejected kill-process for unrecognized PID",{pid:t}),e.status(404).json({ok:!1,error:"PID is not a bridge-discovered or registered agent process"});return}try{await ce(t),w.info("Killed process",{pid:t}),e.json({ok:!0})}catch(o){const l=o;w.warn("Failed to kill process",{pid:t,error:l.message}),e.status(500).json({ok:!1,error:l.message})}}),d.get("/runtime/dashboard",async(n,e)=>{if(g(n,e)){if(k&&Date.now()-k.ts<re)return void e.json(k.data);try{const t=R?.data??null,s=S?.data??null;R||N();const[i,a,o]=await Promise.all([Promise.resolve(T()),Promise.resolve(D()),W().catch(()=>[])]),c=y().getAllManagedProcesses(),v=Array.from(J.entries()).map(([m,u])=>({sessionId:m,agentId:u.agentId,workspacePath:u.workspacePath,runtimeStatus:u.currentRuntimeStatus||"running",runtimeActionType:u.currentRuntimeActionType,runtimeActionLabel:u.currentRuntimeActionLabel,providerId:u.providerId}));let f={managed:0,orphan:0,unknown:0},C=[];if(s){f={managed:s.filter(r=>r.managementStatus==="managed").length,orphan:s.filter(r=>r.managementStatus==="orphan").length,unknown:s.filter(r=>r.managementStatus==="unknown").length};const m=new Map(c.map(r=>[r.pid,r]));C=s.map(r=>{const h=m.get(r.pid);return{agentId:r.agentId||h?.agentId,pid:r.pid,managementStatus:r.managementStatus,sessionId:h?.sessionId,state:h?.state,command:r.command,cwd:r.cwd,startedAt:r.startedAt}});const u={managed:0,orphan:1,unknown:2};C.sort((r,h)=>(u[r.managementStatus]??9)-(u[h.managementStatus]??9))}const j=new Set(v.map(m=>m.sessionId)),B=o.filter(m=>!j.has(m.sessionId)),b=i,O=await de(),A={ok:!0,collectedAt:new Date().toISOString(),bridge:{version:M,hostname:p.hostname(),platform:p.platform(),arch:p.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version},binding:{status:b.status,paired:b.paired,instanceId:b.instanceId,userId:b.userId,schedulerBaseUrl:b.schedulerBaseUrl,pairingCode:V()},registration:{registered:a.registered,instanceId:a.instanceId,lastAttempt:a.lastAttempt?.toISOString(),error:a.error},metrics:t,sessions:{active:v,activeCount:v.length,persisted:B,persistedCount:B.length,total:v.length+B.length},processes:{...f,total:f.managed+f.orphan+f.unknown,list:C,registryCount:c.length},lifecycleOperation:O};k={data:A,ts:Date.now()},e.json(A)}catch(t){const s=t;w.error("Dashboard aggregation failed:",s.message),e.status(500).json({ok:!1,error:s.message})}}}),d.get("/login",(n,e)=>{e.type("text/html; charset=utf-8"),e.send(ge)}),d.get("/",(n,e)=>{const t=I(n);if(!t||!E(t)){e.redirect("/login");return}if(!x()){e.redirect("/setup");return}const s=n.query.lang==="en"?"en":"zh";e.type("text/html; charset=utf-8"),e.send(ue.replace("{{LANG}}",s))}),d.get("/setup",(n,e)=>{const t=I(n);if(!t||!E(t)){e.redirect("/login");return}e.type("text/html; charset=utf-8"),e.send(pe)});const ue=`<!DOCTYPE html>
1
+ import{Router as $}from"express";import v from"node:fs";import K from"node:path";import w from"node:os";import{execFile as Y}from"node:child_process";import{getRuntimeBindingPublicState as H,getRuntimePairingCode as Q,normalizeSchedulerBaseUrl as X}from"../services/runtime-binding.js";import{getAgentProcessManager as C}from"../services/agent-process-manager.js";import{collectSystemMetrics as Z}from"../services/system-metrics.js";import{loadPersistedSessions as ee}from"../services/terminal-persistence.js";import{createLogger as te}from"../utils/logger.js";import{getBridgeVersion as ne}from"../config.js";import{runExclusiveConfigWrite as O}from"../utils/config-write-queue.js";import{sdkSessions as se}from"./terminal.js";import{createSession as re,getPanelCredentials as N,validateSession as U,destroySession as z,validateCredentials as F,hasConfiguredCredentials as B,isDefaultCredentials as ae,setPanelCredentials as oe,getAllSessions as de,getActiveSessionCount as ie,checkLoginAllowed as le,recordLoginFailure as ce,recordLoginSuccess as ue}from"../services/panel-auth.js";import{getRecentLogs as ge}from"../utils/logger.js";import{autoRegister as pe,getRegistrationState as _,getAutoRegisterConfigFilePath as E}from"../services/auto-register.js";import{getAccessControlEntries as fe,isValidIpEntry as me,parseAllowIps as he,refreshAccessControlCache as be}from"../services/access-control.js";const x=te("dashboard");let T=null;const ve=1e4;let L=null,P=null;const ye=3e4;async function D(){Z().then(t=>{L={data:t,ts:Date.now()}}).catch(()=>{}),C().discoverAllProcesses().then(t=>{P={data:t,ts:Date.now()}}).catch(()=>{}),C().cleanupStaleRegistry().catch(()=>{})}D(),setInterval(D,ye);const V=ne();async function xe(){const t=await import("node:fs/promises"),e=await import("node:path"),n=await import("node:os"),s=e.join(String(process.env.AWS_RUNTIME_HOME_DIR||n.homedir()).trim()||n.homedir(),".aws-bridge","lifecycle-state.json");try{const d=await t.readFile(s,"utf8"),r=JSON.parse(d),a=Array.isArray(r.operations)?r.operations:[];return a.length===0?null:(a.sort((o,l)=>new Date(l.updatedAt).getTime()-new Date(o.updatedAt).getTime()),a[0]||null)}catch{return null}}const g=$(),J="panel_sid";function we(t){const e={};if(!t)return e;for(const n of t.split(";")){const s=n.indexOf("=");if(s===-1)continue;const d=n.slice(0,s).trim(),r=n.slice(s+1).trim();d&&(e[d]=r)}return e}function A(t){return we(t.headers.cookie)[J]}function W(t,e,n){const s=t.secure||t.headers["x-forwarded-proto"]==="https";return`${J}=${e}; HttpOnly; SameSite=Lax; Path=/${s?"; Secure":""}; Max-Age=${n}`}function m(t,e){const n=A(t);return n&&U(n)?!0:t.xhr||t.headers.accept?.includes("application/json")?(e.status(401).json({ok:!1,error:"unauthorized",needLogin:!0}),!1):(e.redirect("/login"),!1)}g.post("/runtime/login",(t,e)=>{const n=t.socket.remoteAddress||"unknown",s=le(n);if(!s.allowed){e.status(429).set("Retry-After",String(s.retryAfterSec)).json({ok:!1,error:"too many failed login attempts, try again later",retryAfterSec:s.retryAfterSec});return}const{username:d,password:r}=t.body||{};if(!d||!r){e.status(400).json({ok:!1,error:"username and password required"});return}if(!F(String(d),String(r))){ce(n),e.status(401).json({ok:!1,error:"invalid credentials"});return}ue(n);const a=re(),o=!B();e.setHeader("Set-Cookie",W(t,a,86400)),e.json({ok:!0,needSetup:o})}),g.post("/runtime/logout",(t,e)=>{const n=A(t);n&&z(n),e.setHeader("Set-Cookie",W(t,"",0)),e.json({ok:!0})}),g.get("/runtime/credentials-status",(t,e)=>{if(!m(t,e))return;const n=N();e.json({ok:!0,isDefault:ae(),configured:B(),username:n.username})}),g.post("/runtime/change-credentials",async(t,e)=>{if(!m(t,e))return;const{username:n,password:s,passwordConfirm:d,oldPassword:r}=t.body||{};if(s&&String(s).trim()){if(String(s)!==String(d)){e.status(400).json({ok:!1,error:"passwords do not match"});return}if(B()){if(!r){e.status(400).json({ok:!1,error:"old password required"});return}const o=N();if(!F(o.username,String(r))){e.status(403).json({ok:!1,error:"old password is incorrect"});return}}}if(!n||!String(n).trim()){e.status(400).json({ok:!1,error:"username required"});return}const a=s&&String(s).trim()?String(s).trim():N().password;try{await oe(String(n).trim(),a),e.json({ok:!0})}catch(o){const l=o;x.error("Failed to change panel credentials:",l.message),e.status(500).json({ok:!1,error:l.message})}});function q(t){const e=typeof t=="string"?t.trim():"";if(e)return e.length<8?"\u2022\u2022\u2022\u2022":"\u2022\u2022\u2022\u2022"+e.slice(-4)}function G(t){const e=Array.isArray(t.autoRegisterTargets)?t.autoRegisterTargets.filter(s=>!!(s&&typeof s=="object"&&!Array.isArray(s))):[];return e.length>0?e:["serverUrl","schedulerBaseUrl","userKey"].some(s=>!!String(t[s]??"").trim())?[t]:[]}const Se=["enabled","serverUrl","schedulerBaseUrl","tenantId","userKey","connectionKey","password","instanceName","instanceId","projectId","roleName","workspacePath","registerAddr","registerIp","virtualIp","tunnel","preferSameLanIp","maxRetries","retryInterval"];function Ie(t){const e={};for(const n of Se)t[n]!==void 0&&(e[n]=t[n]);return e}function ke(t,e){const n=String(t.connectionKey??"").trim()||String(e??"").trim();return{serverUrl:String(t.serverUrl??t.schedulerBaseUrl??"").trim(),tenantId:String(t.tenantId??"").trim(),instanceName:String(t.instanceName??"").trim(),instanceId:String(t.instanceId??"").trim()||void 0,registerAddr:String(t.registerAddr??"").trim(),tunnel:typeof t.tunnel=="boolean"?t.tunnel:void 0,userKeyMasked:q(t.userKey),userKeySet:!!String(t.userKey??"").trim(),connectionKeyMasked:q(n),connectionKeySet:!!n}}function R(t){return X(t)??String(t||"").trim()}g.get("/runtime/server-config",(t,e)=>{if(!m(t,e))return;const n=E();let s={};if(v.existsSync(n))try{s=JSON.parse(v.readFileSync(n,"utf-8"))}catch{e.status(500).json({ok:!1,error:"config.json is not valid JSON; fix it manually first"});return}const d=String(s.connectionKey??"").trim()||void 0,r=G(s).map(a=>ke(a,d));e.json({ok:!0,targets:r})});function M(t,e){const n=new Error(e);return n.status=t,n}g.post("/runtime/server-config",async(t,e)=>{if(!m(t,e))return;const n=(t.body||{}).targets;if(!Array.isArray(n)){e.status(400).json({ok:!1,error:"targets array is required"});return}const s=[];for(let r=0;r<n.length;r++){const a=n[r];if(!a||typeof a!="object"||Array.isArray(a)){e.status(400).json({ok:!1,error:`targets[${r}] must be an object`});return}const o=a,l=String(o.serverUrl??o.schedulerBaseUrl??"").trim();if(!l){e.status(400).json({ok:!1,error:`targets[${r}].serverUrl is required`});return}try{const i=new URL(l);if(!["http:","https:"].includes(i.protocol))throw new Error("protocol must be http or https")}catch{e.status(400).json({ok:!1,error:`targets[${r}].serverUrl must be a valid http(s) URL`});return}const f=o.tunnel;let p;if(f==="auto"||f===""||f==null)p=void 0;else if(f===!0||f==="true")p=!0;else if(f===!1||f==="false")p=!1;else{e.status(400).json({ok:!1,error:`targets[${r}].tunnel must be auto, true or false`});return}const b=String(o.originalServerUrl??"").trim();s.push({serverUrl:l,originalServerUrl:b&&b!==l?b:"",userKey:String(o.userKey??"").trim(),tenantId:String(o.tenantId??"").trim(),instanceName:String(o.instanceName??"").trim(),connectionKey:String(o.connectionKey??"").trim(),registerAddr:String(o.registerAddr??"").trim(),tunnel:p})}const d=new Set;for(const r of s){const a=R(r.serverUrl);if(d.has(a)){e.status(400).json({ok:!1,error:"duplicate serverUrl in targets"});return}d.add(a)}try{const r=E();await O(r,()=>{let a={};if(v.existsSync(r))try{a=JSON.parse(v.readFileSync(r,"utf-8"))}catch{throw M(500,"config.json is not valid JSON; fix it manually first")}const o=String(a.connectionKey??"").trim(),l=G(a),f=l.length>0&&l[0]===a,p=new Map(l.map(i=>[R(String(i.serverUrl??i.schedulerBaseUrl??"").trim()),f?Ie(i):i])),b=[];for(const i of s){let h;if(i.originalServerUrl){const S=R(i.originalServerUrl);h=p.get(S),h&&p.delete(S)}if(!h){const S=R(i.serverUrl);h=p.get(S),h&&p.delete(S)}const c=h?{...h}:{};if(c.serverUrl=i.serverUrl,c.tenantId=i.tenantId,c.instanceName=i.instanceName,c.registerAddr=i.registerAddr,i.tunnel===void 0?delete c.tunnel:c.tunnel=i.tunnel,i.userKey)c.userKey=i.userKey;else if(!String(c.userKey??"").trim())throw M(400,`userKey is required for ${i.serverUrl} (new target without inheritable userKey)`);i.connectionKey?c.connectionKey=i.connectionKey:!String(c.connectionKey??"").trim()&&o&&(c.connectionKey=o),b.push(c)}a.autoRegisterTargets=b;for(const i of["enabled","serverUrl","schedulerBaseUrl","tenantId","userKey","connectionKey","password","instanceName","instanceId","registerAddr","registerIp","virtualIp","tunnel"])delete a[i];v.mkdirSync(K.dirname(r),{recursive:!0}),v.writeFileSync(r,JSON.stringify(a,null,2),"utf-8")}),x.info("Server connection config updated via panel",{configPath:r,targetCount:s.length}),e.json({ok:!0})}catch(r){const a=r,o=Number(a.status)||500;o>=500&&x.error("Failed to save server connection config:",a.message),e.status(o).json({ok:!1,error:a.message})}}),g.get("/runtime/access-control",(t,e)=>{m(t,e)&&e.json({ok:!0,allowIps:fe(),defaultAllowIps:["0.0.0.0"]})}),g.post("/runtime/access-control",async(t,e)=>{if(!m(t,e))return;const n=(t.body||{}).allowIps;if(typeof n!="string"&&!Array.isArray(n)){e.status(400).json({ok:!1,error:"allowIps must be a string or an array"});return}const s=he(n),d=s.filter(r=>!me(r));if(d.length>0){e.status(400).json({ok:!1,error:`invalid IP/CIDR entries: ${d.join(", ")}`});return}try{const r=E();await O(r,()=>{let a={};if(v.existsSync(r))try{a=JSON.parse(v.readFileSync(r,"utf-8"))}catch{throw M(500,"config.json is not valid JSON; fix it manually first")}const o=a.accessControl&&typeof a.accessControl=="object"&&!Array.isArray(a.accessControl)?{...a.accessControl}:{};o.allowIps=s,a.accessControl=o,v.mkdirSync(K.dirname(r),{recursive:!0}),v.writeFileSync(r,JSON.stringify(a,null,2),"utf-8"),be()}),x.info("Access control config updated via panel",{configPath:r,entryCount:s.length}),e.json({ok:!0,allowIps:s})}catch(r){const a=r,o=Number(a.status)||500;o>=500&&x.error("Failed to save access control config:",a.message),e.status(o).json({ok:!1,error:a.message})}}),g.get("/runtime/node-info",(t,e)=>{if(!m(t,e))return;const n=H(),s=_();e.json({ok:!0,hostname:w.hostname(),platform:w.platform(),arch:w.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version,bridgeVersion:V,configPath:E(),homeDir:process.env.AWS_RUNTIME_HOME_DIR||w.homedir(),bindingStatus:n.status,paired:n.paired,instanceId:n.instanceId||s.instanceId,schedulerBaseUrl:n.schedulerBaseUrl,registered:s.registered,pairingCode:n.pairingCode,activeSessions:ie()})}),g.get("/runtime/logs",(t,e)=>{if(!m(t,e))return;const n=t.query.level||void 0,s=Math.min(Number(t.query.count)||200,500),r=n&&["debug","info","warn","error"].includes(n)?n:void 0,a=ge(s,r);e.json({ok:!0,logs:a})}),g.post("/runtime/re-register",async(t,e)=>{if(m(t,e))try{const n=await pe();e.json({ok:n})}catch(n){const s=n;e.status(500).json({ok:!1,error:s.message})}}),g.get("/runtime/admin/sessions",(t,e)=>{if(!m(t,e))return;const n=de().map(s=>({token:s.token.slice(0,8)+"...",createdAt:new Date(s.createdAt).toISOString(),lastUsedAt:new Date(s.lastUsedAt).toISOString(),age:Math.floor((Date.now()-s.createdAt)/1e3)}));e.json({ok:!0,sessions:n})}),g.post("/runtime/admin/sessions/:token/destroy",(t,e)=>{m(t,e)&&(z(t.params.token),e.json({ok:!0}))});function Ce(t){return new Promise((e,n)=>{const s=w.platform()==="win32",d=s?"taskkill":"kill",r=s?["/PID",String(t),"/F"]:["-9",String(t)];Y(d,r,{timeout:5e3},a=>a?n(a):e())})}g.post("/runtime/kill-process",async(t,e)=>{if(!m(t,e))return;const n=Number(t.body.pid);if(!n||!Number.isInteger(n)||n<=0){e.status(400).json({ok:!1,error:"Invalid PID"});return}const s=C(),d=()=>{const a=new Set,o=P?.data??[];for(const l of o)typeof l.pid=="number"&&a.add(l.pid);for(const l of s.getAllManagedProcesses())typeof l.pid=="number"&&a.add(l.pid);return a};let r=d();if(r.size===0)try{P={data:await s.discoverAllProcesses(),ts:Date.now()},r=d()}catch{}if(!r.has(n)){x.warn("Rejected kill-process for unrecognized PID",{pid:n}),e.status(404).json({ok:!1,error:"PID is not a bridge-discovered or registered agent process"});return}try{await Ce(n),x.info("Killed process",{pid:n}),e.json({ok:!0})}catch(a){const o=a;x.warn("Failed to kill process",{pid:n,error:o.message}),e.status(500).json({ok:!1,error:o.message})}}),g.get("/runtime/dashboard",async(t,e)=>{if(m(t,e)){if(T&&Date.now()-T.ts<ve)return void e.json(T.data);try{const n=L?.data??null,s=P?.data??null;L||D();const[d,r,a]=await Promise.all([Promise.resolve(H()),Promise.resolve(_()),ee().catch(()=>[])]),l=C().getAllManagedProcesses(),f=Array.from(se.entries()).map(([I,y])=>({sessionId:I,agentId:y.agentId,workspacePath:y.workspacePath,runtimeStatus:y.currentRuntimeStatus||"running",runtimeActionType:y.currentRuntimeActionType,runtimeActionLabel:y.currentRuntimeActionLabel,providerId:y.providerId}));let p={managed:0,orphan:0,unknown:0},b=[];if(s){p={managed:s.filter(u=>u.managementStatus==="managed").length,orphan:s.filter(u=>u.managementStatus==="orphan").length,unknown:s.filter(u=>u.managementStatus==="unknown").length};const I=new Map(l.map(u=>[u.pid,u]));b=s.map(u=>{const k=I.get(u.pid);return{agentId:u.agentId||k?.agentId,pid:u.pid,managementStatus:u.managementStatus,sessionId:k?.sessionId,state:k?.state,command:u.command,cwd:u.cwd,startedAt:u.startedAt}});const y={managed:0,orphan:1,unknown:2};b.sort((u,k)=>(y[u.managementStatus]??9)-(y[k.managementStatus]??9))}const i=new Set(f.map(I=>I.sessionId)),h=a.filter(I=>!i.has(I.sessionId)),c=d,S=await xe(),j={ok:!0,collectedAt:new Date().toISOString(),bridge:{version:V,hostname:w.hostname(),platform:w.platform(),arch:w.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version},binding:{status:c.status,paired:c.paired,instanceId:c.instanceId,userId:c.userId,schedulerBaseUrl:c.schedulerBaseUrl,pairingCode:Q()},registration:{registered:r.registered,instanceId:r.instanceId,lastAttempt:r.lastAttempt?.toISOString(),error:r.error},metrics:n,sessions:{active:f,activeCount:f.length,persisted:h,persistedCount:h.length,total:f.length+h.length},processes:{...p,total:p.managed+p.orphan+p.unknown,list:b,registryCount:l.length},lifecycleOperation:S};T={data:j,ts:Date.now()},e.json(j)}catch(n){const s=n;x.error("Dashboard aggregation failed:",s.message),e.status(500).json({ok:!1,error:s.message})}}}),g.get("/login",(t,e)=>{e.type("text/html; charset=utf-8"),e.send(Ee)}),g.get("/",(t,e)=>{const n=A(t);if(!n||!U(n)){e.redirect("/login");return}if(!B()){e.redirect("/setup");return}const s=t.query.lang==="en"?"en":"zh";e.type("text/html; charset=utf-8"),e.send(Be.replace("{{LANG}}",s))}),g.get("/setup",(t,e)=>{const n=A(t);if(!n||!U(n)){e.redirect("/login");return}e.type("text/html; charset=utf-8"),e.send(Te)});const Be=`<!DOCTYPE html>
2
2
  <html lang="{{LANG}}">
3
3
  <head>
4
4
  <meta charset="UTF-8">
@@ -295,6 +295,7 @@ import{Router as F}from"express";import p from"node:os";import{execFile as _}fro
295
295
  </span>
296
296
  <span class="lang-btn" id="langBtn" onclick="toggleLang()">\u4E2D\u6587</span>
297
297
  <button onclick="openSettings()" id="settingsBtn" title="Settings / \u8BBE\u7F6E">&#x2699;</button>
298
+ <button onclick="doLogout()" id="logoutBtn" title="Logout / \u9000\u51FA\u767B\u5F55">&#x23FB;</button>
298
299
  <button onclick="refresh()" id="refreshBtn">&#x21bb;</button>
299
300
  </div>
300
301
  </div>
@@ -351,6 +352,7 @@ import{Router as F}from"express";import p from"node:os";import{execFile as _}fro
351
352
  <div class="settings-nav-item" data-page="node" onclick="switchSettingsPage('node')"><span id="navNode"></span></div>
352
353
  <div class="settings-nav-item" data-page="logs" onclick="switchSettingsPage('logs')"><span id="navLogs"></span></div>
353
354
  <div class="settings-nav-item" data-page="scheduler" onclick="switchSettingsPage('scheduler')"><span id="navScheduler"></span></div>
355
+ <div class="settings-nav-item" data-page="access" onclick="switchSettingsPage('access')"><span id="navAccess"></span></div>
354
356
  <div class="settings-nav-item" data-page="maintenance" onclick="switchSettingsPage('maintenance')"><span id="navMaintenance"></span></div>
355
357
  </div>
356
358
  <div class="settings-content">
@@ -417,15 +419,83 @@ import{Router as F}from"express";import p from"node:os";import{execFile as _}fro
417
419
  <!-- Scheduler page -->
418
420
  <div class="settings-page" id="pageScheduler">
419
421
  <div class="s-field">
420
- <label id="lblSchedUrl"></label>
421
- <div class="s-info-row"><span class="s-info-value" id="valSchedUrl">-</span></div>
422
+ <label id="lblSchedTargets"></label>
423
+ <div class="s-target-list" id="schedTargetList"></div>
424
+ </div>
425
+ <div class="s-actions s-actions-row" style="margin-bottom:18px">
426
+ <button class="s-btn s-btn-secondary" id="btnAddTarget" onclick="startAddTarget()"></button>
427
+ </div>
428
+ <div class="s-target-form" id="targetForm">
429
+ <div class="s-field">
430
+ <label id="lblSchedUrl"></label>
431
+ <input type="text" id="cfgServerUrl" placeholder="http://192.168.1.100:8080" autocomplete="off">
432
+ <span class="s-hint" id="hintSchedUrl"></span>
433
+ </div>
434
+ <div class="s-field">
435
+ <label id="lblSchedUserKey"></label>
436
+ <input type="password" id="cfgUserKey" autocomplete="new-password">
437
+ <span class="s-hint" id="hintSchedUserKey"></span>
438
+ </div>
439
+ <div class="s-field">
440
+ <label id="lblSchedTenantId"></label>
441
+ <input type="text" id="cfgTenantId" autocomplete="off">
442
+ <span class="s-hint" id="hintSchedTenantId"></span>
443
+ </div>
444
+ <div class="s-field">
445
+ <label id="lblSchedInstanceName"></label>
446
+ <input type="text" id="cfgInstanceName" autocomplete="off">
447
+ <span class="s-hint" id="hintSchedInstanceName"></span>
448
+ </div>
449
+ <div class="s-field">
450
+ <label id="lblSchedConnKey"></label>
451
+ <input type="password" id="cfgConnectionKey" autocomplete="new-password">
452
+ <span class="s-hint" id="hintSchedConnKey"></span>
453
+ </div>
454
+ <div class="s-field">
455
+ <label id="lblSchedRegisterAddr"></label>
456
+ <input type="text" id="cfgRegisterAddr" placeholder="192.168.1.25:18081" autocomplete="off">
457
+ <span class="s-hint" id="hintSchedRegisterAddr"></span>
458
+ </div>
459
+ <div class="s-field">
460
+ <label id="lblSchedTunnel"></label>
461
+ <select id="cfgTunnel">
462
+ <option value="auto" id="optTunnelAuto"></option>
463
+ <option value="true" id="optTunnelOn"></option>
464
+ <option value="false" id="optTunnelOff"></option>
465
+ </select>
466
+ <span class="s-hint" id="hintSchedTunnel"></span>
467
+ </div>
468
+ <div class="s-field">
469
+ <div class="s-info-row"><span class="s-info-label" id="lblSchedInstanceId"></span><span class="s-info-value" id="valSchedInstanceId">-</span></div>
470
+ <span class="s-hint" id="hintSchedInstanceId"></span>
471
+ </div>
472
+ <div class="s-msg" id="schedMsg"></div>
473
+ <div class="s-actions s-actions-row">
474
+ <button class="s-btn s-btn-primary" id="btnSaveServerConfig" onclick="saveServerConfig(false)"></button>
475
+ <button class="s-btn s-btn-danger" id="btnDeleteTarget" onclick="deleteTargetFromList()"></button>
476
+ </div>
422
477
  </div>
423
478
  <div class="s-field">
424
479
  <p class="s-desc" id="descSched"></p>
425
480
  </div>
426
- <div class="s-msg" id="schedMsg"></div>
481
+ <div class="s-actions s-actions-row">
482
+ <button class="s-btn s-btn-danger" id="btnSaveRestart" onclick="saveServerConfigAndRestart()"></button>
483
+ <button class="s-btn s-btn-secondary" id="btnReRegister" onclick="doReRegister()"></button>
484
+ </div>
485
+ </div>
486
+ <!-- Access control page -->
487
+ <div class="settings-page" id="pageAccess">
488
+ <div class="s-field">
489
+ <label id="lblAccessIps"></label>
490
+ <textarea id="cfgAllowIps" rows="4" placeholder="0.0.0.0" autocomplete="off" spellcheck="false"></textarea>
491
+ <span class="s-hint" id="hintAccessIps"></span>
492
+ </div>
493
+ <div class="s-msg" id="accessMsg"></div>
427
494
  <div class="s-actions">
428
- <button class="s-btn s-btn-primary" id="btnReRegister" onclick="doReRegister()"></button>
495
+ <button class="s-btn s-btn-primary" id="btnSaveAccessControl" onclick="saveAccessControl()"></button>
496
+ </div>
497
+ <div class="s-field" style="margin-top:18px">
498
+ <p class="s-desc" id="descAccess"></p>
429
499
  </div>
430
500
  </div>
431
501
  <!-- Maintenance page -->
@@ -478,6 +548,21 @@ import{Router as F}from"express";import p from"node:os";import{execFile as _}fro
478
548
  .s-field label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; color: var(--text2); }
479
549
  .s-field input { width: 100%; padding: 9px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 14px; outline: none; }
480
550
  .s-field input:focus { border-color: var(--accent); }
551
+ .s-field select { width: 100%; padding: 9px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 14px; outline: none; cursor: pointer; }
552
+ .s-field select:focus { border-color: var(--accent); }
553
+ .s-field select option { background: var(--surface2); color: var(--text); }
554
+ .s-field textarea { width: 100%; padding: 9px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 14px; font-family: "Cascadia Code", "Fira Code", Consolas, monospace; outline: none; resize: vertical; }
555
+ .s-field textarea:focus { border-color: var(--accent); }
556
+ .s-actions-row { display: flex; gap: 10px; flex-wrap: wrap; }
557
+ .s-target-list { display: flex; flex-direction: column; gap: 8px; }
558
+ .s-target-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; }
559
+ .s-target-item.active { border-color: var(--accent); }
560
+ .s-target-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
561
+ .s-target-url { font-size: 14px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
562
+ .s-target-meta { font-size: 12px; opacity: 0.7; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
563
+ .s-target-actions { display: flex; gap: 6px; flex-shrink: 0; }
564
+ .s-target-form { border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 18px; background: var(--surface); }
565
+ .s-target-empty { padding: 14px; text-align: center; font-size: 13px; opacity: 0.6; border: 1px dashed var(--border); border-radius: 6px; }
481
566
  .s-hint { display: block; font-size: 11px; color: var(--text2); margin-top: 4px; }
482
567
  .s-msg { padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 14px; display: none; }
483
568
  .s-msg.error { display: block; background: rgba(240,84,84,0.12); color: var(--red); }
@@ -649,12 +734,53 @@ const T = {
649
734
  logEmpty: "\u6682\u65E0\u65E5\u5FD7",
650
735
  // \u8C03\u5EA6\u4E2D\u5FC3
651
736
  schedTitle: "\u8C03\u5EA6\u4E2D\u5FC3",
652
- schedUrl: "\u670D\u52A1\u5730\u5740",
737
+ schedTargets: "\u8C03\u5EA6\u4E2D\u5FC3\u5217\u8868\uFF08\u652F\u6301\u591A\u4E2A\uFF0C\u4FDD\u5B58\u540E\u5E76\u884C\u8FDE\u63A5\uFF09",
738
+ schedNoTargets: "\u5C1A\u672A\u914D\u7F6E\uFF0C\u70B9\u51FB\u4E0B\u65B9\u6309\u94AE\u6DFB\u52A0",
739
+ schedAddTarget: "\uFF0B \u6DFB\u52A0\u8C03\u5EA6\u4E2D\u5FC3",
740
+ schedEditTarget: "\u7F16\u8F91",
741
+ schedDeleteTarget: "\u5220\u9664",
742
+ schedConfirmDelete: "\u5220\u9664\u8BE5\u8C03\u5EA6\u4E2D\u5FC3\u914D\u7F6E\uFF1F",
743
+ schedFormNew: "\u65B0\u589E\u8C03\u5EA6\u4E2D\u5FC3",
744
+ schedUrl: "\u670D\u52A1\u5730\u5740 (serverUrl)",
745
+ schedUrlHint: "\u5FC5\u586B\uFF0C\u4EE5 http:// \u6216 https:// \u5F00\u5934\uFF1B\u5730\u5740\u53D8\u66F4\u65F6 API Key \u4F1A\u81EA\u52A8\u6CBF\u7528",
746
+ schedUserKey: "\u7528\u6237 API Key (userKey)",
747
+ schedUserKeyHint: "\u5FC5\u586B\uFF1B\u5DF2\u914D\u7F6E\u65F6\u7559\u7A7A\u8868\u793A\u4FDD\u6301\u4E0D\u53D8",
748
+ schedTenantId: "\u79DF\u6237 ID\uFF08\u53EF\u9009\uFF09",
749
+ schedTenantIdHint: "\u7559\u7A7A\u7531 server \u6839\u636E userKey \u81EA\u52A8\u63A8\u5BFC",
750
+ schedInstanceName: "\u5B9E\u4F8B\u540D\u79F0\uFF08\u53EF\u9009\uFF09",
751
+ schedInstanceNameHint: "\u7559\u7A7A\u4F7F\u7528\u4E3B\u673A\u540D",
752
+ schedConnKey: "\u8FDE\u63A5\u5BC6\u94A5\uFF08\u53EF\u9009\uFF09",
753
+ schedConnKeyHint: "server \u8FDE\u63A5\u8BE5 bridge \u7684\u5BC6\u94A5\uFF1B\u5DF2\u914D\u7F6E\u65F6\u7559\u7A7A\u8868\u793A\u4FDD\u6301\u4E0D\u53D8",
754
+ schedRegisterAddr: "\u6CE8\u518C\u5730\u5740\uFF08\u53EF\u9009\uFF09",
755
+ schedRegisterAddrHint: "\u76F4\u8FDE\u6A21\u5F0F\u4E0B\u7684\u5730\u5740\uFF0C\u5982 192.168.1.25:18081\uFF1B\u6E05\u7A7A\u8868\u793A\u4E0D\u6307\u5B9A",
756
+ schedTunnel: "\u96A7\u9053\u6A21\u5F0F",
757
+ schedTunnelHint: "\u81EA\u52A8\uFF1A\u672A\u914D\u7F6E\u6CE8\u518C\u5730\u5740\u65F6\u81EA\u52A8\u542F\u7528\u96A7\u9053\uFF1B\u542F\u7528\uFF1A\u7ECF WebSocket \u53CD\u5411\u8FDE\u63A5 server\uFF1B\u7981\u7528\uFF1A\u76F4\u8FDE",
758
+ schedTunnelAuto: "\u81EA\u52A8",
759
+ schedTunnelOn: "\u542F\u7528",
760
+ schedTunnelOff: "\u7981\u7528",
761
+ schedInstanceId: "\u5B9E\u4F8B ID",
762
+ schedInstanceIdHint: "\u7F16\u8F91\u6539\u5740\u65F6\u81EA\u52A8\u7EE7\u627F\uFF08\u540C\u4E00\u5B9E\u4F8B\u6362\u5730\u5740\uFF09\uFF0C\u65B0\u589E\u76EE\u6807\u6CE8\u518C\u6210\u529F\u540E\u81EA\u52A8\u56DE\u586B",
763
+ schedSave: "\u4FDD\u5B58\u914D\u7F6E",
764
+ schedSaving: "\u4FDD\u5B58\u4E2D...",
765
+ schedSaved: "\u914D\u7F6E\u5DF2\u4FDD\u5B58",
766
+ schedSavedRegistering: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u6B63\u5728\u91CD\u65B0\u6CE8\u518C...",
767
+ schedSaveFail: "\u4FDD\u5B58\u5931\u8D25",
768
+ schedSaveAndRestart: "\u4FDD\u5B58\u5E76\u91CD\u542F",
769
+ schedSaveRestartConfirm: "\u4FDD\u5B58\u914D\u7F6E\u5E76\u91CD\u542F Bridge\uFF1F\u91CD\u542F\u671F\u95F4\u670D\u52A1\u4F1A\u77ED\u6682\u4E2D\u65AD\uFF0C\u8FD0\u884C\u4E2D\u7684 Agent \u4F1A\u88AB\u4FDD\u7559\u3002",
653
770
  schedReRegister: "\u91CD\u65B0\u6CE8\u518C",
654
771
  schedReRegistering: "\u6CE8\u518C\u4E2D...",
655
772
  schedReRegistered: "\u91CD\u65B0\u6CE8\u518C\u6210\u529F",
656
773
  schedReRegisterFail: "\u91CD\u65B0\u6CE8\u518C\u5931\u8D25",
657
- schedDesc: "\u5411\u8C03\u5EA6\u4E2D\u5FC3\u53D1\u8D77\u91CD\u65B0\u6CE8\u518C\uFF0C\u5EFA\u8BAE\u5728\u8FDE\u63A5\u5F02\u5E38\u65F6\u4F7F\u7528\u3002",
774
+ schedDesc: "\u4FDD\u5B58\u540E\u53EF\u70B9\u51FB\u201C\u91CD\u65B0\u6CE8\u518C\u201D\u7ACB\u5373\u5E94\u7528\u65B0\u914D\u7F6E\uFF1B\u96A7\u9053\u6A21\u5F0F\u7B49\u90E8\u5206\u53D8\u66F4\u9700\u91CD\u542F Bridge \u751F\u6548\u3002",
775
+ // \u8BBF\u95EE\u63A7\u5236
776
+ accTitle: "\u8BBF\u95EE\u63A7\u5236",
777
+ accAllowIps: "\u5141\u8BB8\u8BBF\u95EE\u7684 IP / \u7F51\u6BB5",
778
+ accAllowIpsHint: "\u591A\u4E2A\u7528\u9017\u53F7\u6216\u6362\u884C\u5206\u9694\uFF0C\u652F\u6301\u5355\u4E2A IP \u4E0E CIDR \u7F51\u6BB5\uFF08\u5982 192.168.1.0/24\uFF09\uFF1B\u586B 0.0.0.0 \u6216 * \u8868\u793A\u5BF9\u6240\u6709\u4EBA\u5F00\u653E\uFF1B\u7559\u7A7A\u9ED8\u8BA4\u5373 0.0.0.0 \u5168\u5F00\u653E",
779
+ accDesc: "\u4FDD\u5B58\u540E\u7ACB\u5373\u751F\u6548\uFF0C\u65E0\u9700\u91CD\u542F\u3002\u672C\u673A\uFF08127.0.0.1\uFF09\u59CB\u7EC8\u5141\u8BB8\u8BBF\u95EE\uFF1B\u76F4\u8FDE\u6A21\u5F0F\u7684\u8C03\u5EA6\u4E2D\u5FC3\u82E5\u6765\u81EA\u5916\u90E8\u5730\u5740\uFF0C\u9700\u5C06\u5176 IP \u52A0\u5165\u767D\u540D\u5355\uFF0C\u5426\u5219\u4F1A\u88AB\u62D2\u7EDD\u3002",
780
+ accSave: "\u4FDD\u5B58",
781
+ accSaving: "\u4FDD\u5B58\u4E2D...",
782
+ accSaved: "\u5DF2\u4FDD\u5B58\u5E76\u751F\u6548",
783
+ accSaveFail: "\u4FDD\u5B58\u5931\u8D25",
658
784
  // \u72B6\u6001\u6587\u672C
659
785
  stHealthy: "\u5065\u5EB7",
660
786
  stError: "\u5F02\u5E38",
@@ -778,12 +904,53 @@ const T = {
778
904
  logEmpty: "No logs yet",
779
905
  // Scheduler
780
906
  schedTitle: "Scheduler",
781
- schedUrl: "Server URL",
907
+ schedTargets: "Scheduler targets (multiple allowed, connected in parallel)",
908
+ schedNoTargets: "None configured. Click the button below to add one.",
909
+ schedAddTarget: "+ Add scheduler",
910
+ schedEditTarget: "Edit",
911
+ schedDeleteTarget: "Delete",
912
+ schedConfirmDelete: "Delete this scheduler configuration?",
913
+ schedFormNew: "New scheduler",
914
+ schedUrl: "Server URL (serverUrl)",
915
+ schedUrlHint: "Required, must start with http:// or https://; the API key is kept when the URL changes",
916
+ schedUserKey: "User API Key (userKey)",
917
+ schedUserKeyHint: "Required; leave blank to keep the configured value",
918
+ schedTenantId: "Tenant ID (optional)",
919
+ schedTenantIdHint: "Leave blank to auto-derive from userKey on the server",
920
+ schedInstanceName: "Instance name (optional)",
921
+ schedInstanceNameHint: "Leave blank to use the hostname",
922
+ schedConnKey: "Connection key (optional)",
923
+ schedConnKeyHint: "Key for the server to connect to this bridge; leave blank to keep the configured value",
924
+ schedRegisterAddr: "Register address (optional)",
925
+ schedRegisterAddrHint: "Address in direct mode, e.g. 192.168.1.25:18081; clear to unset",
926
+ schedTunnel: "Tunnel mode",
927
+ schedTunnelHint: "Auto: tunnel is enabled when no register address is set; On: reverse WebSocket connection to the server; Off: direct connection",
928
+ schedTunnelAuto: "Auto",
929
+ schedTunnelOn: "On",
930
+ schedTunnelOff: "Off",
931
+ schedInstanceId: "Instance ID",
932
+ schedInstanceIdHint: "Inherited automatically when editing the URL (same instance, new address); backfilled after a new target registers successfully",
933
+ schedSave: "Save config",
934
+ schedSaving: "Saving...",
935
+ schedSaved: "Configuration saved",
936
+ schedSavedRegistering: "Configuration saved, re-registering...",
937
+ schedSaveFail: "Failed to save",
938
+ schedSaveAndRestart: "Save & restart",
939
+ schedSaveRestartConfirm: "Save the configuration and restart the bridge? The service will be briefly interrupted; running agents are preserved.",
782
940
  schedReRegister: "Re-register",
783
941
  schedReRegistering: "Registering...",
784
942
  schedReRegistered: "Re-registered successfully",
785
943
  schedReRegisterFail: "Re-registration failed",
786
- schedDesc: "Re-register with the scheduler. Use when connection is abnormal.",
944
+ schedDesc: "After saving, click 'Re-register' to apply the new config immediately; some changes such as tunnel mode require a bridge restart.",
945
+ // Access control
946
+ accTitle: "Access Control",
947
+ accAllowIps: "Allowed IPs / networks",
948
+ accAllowIpsHint: "Separate entries with commas or new lines; single IPs and CIDR blocks (e.g. 192.168.1.0/24) are supported; 0.0.0.0 or * allows everyone; empty defaults to 0.0.0.0 (allow all)",
949
+ accDesc: "Takes effect immediately after saving, no restart needed. Loopback (127.0.0.1) is always allowed; if a direct-mode scheduler connects from an external address, add its IP to the list or it will be rejected.",
950
+ accSave: "Save",
951
+ accSaving: "Saving...",
952
+ accSaved: "Saved and applied",
953
+ accSaveFail: "Failed to save",
787
954
  // \u72B6\u6001\u6587\u672C
788
955
  stHealthy: "Healthy",
789
956
  stError: "Error",
@@ -1092,6 +1259,15 @@ function toggleUnknown() {
1092
1259
  }
1093
1260
 
1094
1261
  // \u2500\u2500 \u8BF7\u6C42\u4E0E\u8F6E\u8BE2 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1262
+ // \u9000\u51FA\u767B\u5F55\uFF1A\u9500\u6BC1\u9762\u677F\u4F1A\u8BDD\u540E\u8DF3\u8F6C\u767B\u5F55\u9875
1263
+ function doLogout() {
1264
+ fetch("/runtime/logout", { method: "POST", headers: { "Content-Type": "application/json" } })
1265
+ .catch(function() {})
1266
+ .then(function() {
1267
+ location.href = "/login";
1268
+ });
1269
+ }
1270
+
1095
1271
  async function refresh() {
1096
1272
  try {
1097
1273
  const resp = await fetch("/runtime/dashboard");
@@ -1258,15 +1434,44 @@ function renderSettingsUI() {
1258
1434
  document.getElementById("lblNodeActiveSessions").textContent = t("nodeActiveSessions");
1259
1435
  document.getElementById("lblLogAutoRefresh").textContent = t("logAutoRefresh");
1260
1436
  document.getElementById("logEmpty").textContent = t("logEmpty");
1437
+ document.getElementById("lblSchedTargets").textContent = t("schedTargets");
1438
+ document.getElementById("btnAddTarget").textContent = t("schedAddTarget");
1439
+ document.getElementById("btnDeleteTarget").textContent = t("schedDeleteTarget");
1261
1440
  document.getElementById("lblSchedUrl").textContent = t("schedUrl");
1441
+ document.getElementById("hintSchedUrl").textContent = t("schedUrlHint");
1442
+ document.getElementById("lblSchedUserKey").textContent = t("schedUserKey");
1443
+ document.getElementById("hintSchedUserKey").textContent = t("schedUserKeyHint");
1444
+ document.getElementById("lblSchedTenantId").textContent = t("schedTenantId");
1445
+ document.getElementById("hintSchedTenantId").textContent = t("schedTenantIdHint");
1446
+ document.getElementById("lblSchedInstanceName").textContent = t("schedInstanceName");
1447
+ document.getElementById("hintSchedInstanceName").textContent = t("schedInstanceNameHint");
1448
+ document.getElementById("lblSchedConnKey").textContent = t("schedConnKey");
1449
+ document.getElementById("hintSchedConnKey").textContent = t("schedConnKeyHint");
1450
+ document.getElementById("lblSchedRegisterAddr").textContent = t("schedRegisterAddr");
1451
+ document.getElementById("hintSchedRegisterAddr").textContent = t("schedRegisterAddrHint");
1452
+ document.getElementById("lblSchedTunnel").textContent = t("schedTunnel");
1453
+ document.getElementById("hintSchedTunnel").textContent = t("schedTunnelHint");
1454
+ document.getElementById("optTunnelAuto").textContent = t("schedTunnelAuto");
1455
+ document.getElementById("optTunnelOn").textContent = t("schedTunnelOn");
1456
+ document.getElementById("optTunnelOff").textContent = t("schedTunnelOff");
1457
+ document.getElementById("lblSchedInstanceId").textContent = t("schedInstanceId");
1458
+ document.getElementById("hintSchedInstanceId").textContent = t("schedInstanceIdHint");
1459
+ document.getElementById("btnSaveServerConfig").textContent = t("schedSave");
1460
+ document.getElementById("btnSaveRestart").textContent = t("schedSaveAndRestart");
1262
1461
  document.getElementById("descSched").textContent = t("schedDesc");
1263
1462
  document.getElementById("btnReRegister").textContent = t("schedReRegister");
1463
+ document.getElementById("navAccess").textContent = t("accTitle");
1464
+ document.getElementById("lblAccessIps").textContent = t("accAllowIps");
1465
+ document.getElementById("hintAccessIps").textContent = t("accAllowIpsHint");
1466
+ document.getElementById("descAccess").textContent = t("accDesc");
1467
+ document.getElementById("btnSaveAccessControl").textContent = t("accSave");
1264
1468
  }
1265
1469
  function switchSettingsPage(page) {
1266
1470
  document.querySelectorAll(".settings-nav-item").forEach(function(el) { el.classList.remove("active"); });
1267
1471
  document.querySelectorAll(".settings-page").forEach(function(el) { el.classList.remove("active"); });
1268
1472
  document.querySelector('.settings-nav-item[data-page="' + page + '"]').classList.add("active");
1269
1473
  document.getElementById('page' + page.charAt(0).toUpperCase() + page.slice(1)).classList.add("active");
1474
+ if (page === "access") refreshAccessControl();
1270
1475
  }
1271
1476
  function openSettings() {
1272
1477
  if (settingsLang !== lang) { renderSettingsUI(); settingsLang = lang; }
@@ -1428,6 +1633,267 @@ document.addEventListener("change", function(e) {
1428
1633
  });
1429
1634
 
1430
1635
  // \u2500\u2500 Scheduler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1636
+ // \u591A\u8C03\u5EA6\u4E2D\u5FC3\u7BA1\u7406\uFF1AschedTargets \u7F13\u5B58\u5F53\u524D\u5217\u8868\uFF08GET \u89C6\u56FE\uFF09\uFF0CschedEditIndex \u4E3A\u8868\u5355\u6B63\u5728\u7F16\u8F91\u7684\u4E0B\u6807\uFF08-1 = \u65B0\u589E\uFF09
1637
+ var schedTargets = [];
1638
+ var schedEditIndex = -1;
1639
+
1640
+ // \u6E32\u67D3\u76EE\u6807\u5217\u8868\uFF08\u5361\u7247\uFF1AURL + \u5143\u4FE1\u606F\uFF0C\u70B9\u51FB\u8FDB\u5165\u7F16\u8F91\uFF09
1641
+ function renderSchedTargetList() {
1642
+ var list = document.getElementById("schedTargetList");
1643
+ list.innerHTML = "";
1644
+ if (!schedTargets.length) {
1645
+ var empty = document.createElement("div");
1646
+ empty.className = "s-target-empty";
1647
+ empty.textContent = t("schedNoTargets");
1648
+ list.appendChild(empty);
1649
+ return;
1650
+ }
1651
+ for (var i = 0; i < schedTargets.length; i++) {
1652
+ (function(idx) {
1653
+ var item = schedTargets[idx];
1654
+ var row = document.createElement("div");
1655
+ row.className = "s-target-item" + (idx === schedEditIndex ? " active" : "");
1656
+ var main = document.createElement("div");
1657
+ main.className = "s-target-main";
1658
+ var url = document.createElement("div");
1659
+ url.className = "s-target-url";
1660
+ url.textContent = item.serverUrl;
1661
+ var meta = document.createElement("div");
1662
+ meta.className = "s-target-meta";
1663
+ var parts = [];
1664
+ if (item.instanceName) parts.push(item.instanceName);
1665
+ if (item.instanceId) parts.push("ID: " + item.instanceId);
1666
+ if (item.tunnel === true) parts.push("tunnel");
1667
+ else if (item.tunnel === false && item.registerAddr) parts.push("direct");
1668
+ meta.textContent = parts.join(" \xB7 ");
1669
+ main.appendChild(url);
1670
+ main.appendChild(meta);
1671
+ var actions = document.createElement("div");
1672
+ actions.className = "s-target-actions";
1673
+ var editBtn = document.createElement("button");
1674
+ editBtn.className = "s-btn s-btn-sm s-btn-secondary";
1675
+ editBtn.textContent = t("schedEditTarget");
1676
+ editBtn.onclick = function(ev) { ev.stopPropagation(); loadTargetToForm(idx); };
1677
+ actions.appendChild(editBtn);
1678
+ row.appendChild(main);
1679
+ row.appendChild(actions);
1680
+ row.onclick = function() { loadTargetToForm(idx); };
1681
+ list.appendChild(row);
1682
+ })(i);
1683
+ }
1684
+ }
1685
+
1686
+ // \u5C06\u6307\u5B9A target \u8F7D\u5165\u8868\u5355\uFF08\u7F16\u8F91\u6A21\u5F0F\uFF09\uFF1A\u5BC6\u94A5\u7559\u7A7A=\u4FDD\u6301\u4E0D\u53D8\uFF08placeholder \u663E\u793A\u63A9\u7801\uFF09
1687
+ function loadTargetToForm(idx) {
1688
+ schedEditIndex = idx;
1689
+ var item = schedTargets[idx];
1690
+ document.getElementById("cfgServerUrl").value = item.serverUrl || "";
1691
+ document.getElementById("cfgTenantId").value = item.tenantId || "";
1692
+ document.getElementById("cfgInstanceName").value = item.instanceName || "";
1693
+ document.getElementById("cfgRegisterAddr").value = item.registerAddr || "";
1694
+ document.getElementById("cfgTunnel").value =
1695
+ item.tunnel === true ? "true" : (item.tunnel === false ? "false" : "auto");
1696
+ var uk = document.getElementById("cfgUserKey");
1697
+ var ck = document.getElementById("cfgConnectionKey");
1698
+ uk.value = "";
1699
+ ck.value = "";
1700
+ uk.placeholder = item.userKeyMasked || "";
1701
+ ck.placeholder = item.connectionKeyMasked || "";
1702
+ document.getElementById("valSchedInstanceId").textContent = item.instanceId || "-";
1703
+ document.getElementById("btnDeleteTarget").style.display = "";
1704
+ renderSchedTargetList();
1705
+ }
1706
+
1707
+ // \u8FDB\u5165\u65B0\u589E\u6A21\u5F0F\uFF1A\u6E05\u7A7A\u8868\u5355
1708
+ function startAddTarget() {
1709
+ schedEditIndex = -1;
1710
+ ["cfgServerUrl", "cfgTenantId", "cfgInstanceName", "cfgRegisterAddr", "cfgUserKey", "cfgConnectionKey"].forEach(function(id) {
1711
+ document.getElementById(id).value = "";
1712
+ });
1713
+ document.getElementById("cfgUserKey").placeholder = "";
1714
+ document.getElementById("cfgConnectionKey").placeholder = "";
1715
+ document.getElementById("cfgTunnel").value = "auto";
1716
+ document.getElementById("valSchedInstanceId").textContent = "-";
1717
+ document.getElementById("btnDeleteTarget").style.display = "none";
1718
+ hideSchedMsg();
1719
+ renderSchedTargetList();
1720
+ }
1721
+
1722
+ function hideSchedMsg() {
1723
+ var msg = document.getElementById("schedMsg");
1724
+ msg.className = "s-msg";
1725
+ msg.style.display = "none";
1726
+ }
1727
+
1728
+ function showSchedMsg(text, ok) {
1729
+ var msg = document.getElementById("schedMsg");
1730
+ msg.textContent = text;
1731
+ msg.className = "s-msg " + (ok ? "success" : "error");
1732
+ }
1733
+
1734
+ // \u8868\u5355 \u2192 target \u5BF9\u8C61\uFF08\u5BC6\u94A5\u539F\u6837\u643A\u5E26\uFF0C\u7559\u7A7A\u7531\u540E\u7AEF\u6309 originalServerUrl/serverUrl \u7EE7\u627F\uFF09
1735
+ function formToTarget() {
1736
+ return {
1737
+ serverUrl: document.getElementById("cfgServerUrl").value.trim(),
1738
+ // \u7F16\u8F91\u6A21\u5F0F\u643A\u5E26\u539F\u5730\u5740\uFF1A\u5730\u5740\u53D8\u66F4\u65F6\u540E\u7AEF\u6309\u539F\u5730\u5740\u7EE7\u627F instanceId/\u5BC6\u94A5\uFF08\u540C\u4E00\u5B9E\u4F8B\u6362\u5730\u5740\uFF0C\u65E0\u9700\u91CD\u65B0\u6CE8\u518C\uFF09
1739
+ originalServerUrl: schedEditIndex >= 0 && schedTargets[schedEditIndex]
1740
+ ? (schedTargets[schedEditIndex].serverUrl || "")
1741
+ : "",
1742
+ userKey: document.getElementById("cfgUserKey").value,
1743
+ tenantId: document.getElementById("cfgTenantId").value.trim(),
1744
+ instanceName: document.getElementById("cfgInstanceName").value.trim(),
1745
+ connectionKey: document.getElementById("cfgConnectionKey").value,
1746
+ registerAddr: document.getElementById("cfgRegisterAddr").value.trim(),
1747
+ tunnel: document.getElementById("cfgTunnel").value,
1748
+ };
1749
+ }
1750
+
1751
+ // \u52A0\u8F7D server \u8FDE\u63A5\u914D\u7F6E\u5217\u8868\u5230\u9762\u677F
1752
+ function refreshServerConfig() {
1753
+ fetch("/runtime/server-config").then(function(r) { return r.json(); }).then(function(d) {
1754
+ if (!d.ok || !Array.isArray(d.targets)) return;
1755
+ schedTargets = d.targets;
1756
+ if (schedEditIndex >= schedTargets.length) schedEditIndex = -1;
1757
+ if (schedEditIndex >= 0) loadTargetToForm(schedEditIndex);
1758
+ else startAddTarget();
1759
+ }).catch(function() {});
1760
+ }
1761
+
1762
+ // \u5168\u91CF\u63D0\u4EA4 targets\uFF1BrestartAfter \u4E3A true \u65F6\u4FDD\u5B58\u6210\u529F\u540E\u91CD\u542F Bridge
1763
+ function saveServerConfig(restartAfter) {
1764
+ var form = formToTarget();
1765
+ if (!form.serverUrl) {
1766
+ showSchedMsg(t("schedSaveFail") + ": serverUrl", false);
1767
+ return Promise.resolve(false);
1768
+ }
1769
+
1770
+ // \u7EC4\u88C5\u5168\u91CF\u5217\u8868\uFF1A\u7F16\u8F91\u6A21\u5F0F\u66FF\u6362\u5BF9\u5E94\u9879\uFF0C\u65B0\u589E\u6A21\u5F0F\u8FFD\u52A0\uFF1B\u5176\u4F59\u9879\u4ECE\u7F13\u5B58\u89C6\u56FE\u539F\u6837\u5E26\u4E0A\uFF08\u5BC6\u94A5\u7559\u7A7A\u7531\u540E\u7AEF\u7EE7\u627F\uFF09
1771
+ var list = schedTargets.map(function(item) {
1772
+ return {
1773
+ serverUrl: item.serverUrl,
1774
+ userKey: "",
1775
+ tenantId: item.tenantId || "",
1776
+ instanceName: item.instanceName || "",
1777
+ connectionKey: "",
1778
+ registerAddr: item.registerAddr || "",
1779
+ tunnel: item.tunnel === true ? "true" : (item.tunnel === false ? "false" : "auto"),
1780
+ };
1781
+ });
1782
+ if (schedEditIndex >= 0) list[schedEditIndex] = form;
1783
+ else list.push(form);
1784
+
1785
+ var btn = document.getElementById("btnSaveServerConfig");
1786
+ btn.disabled = true;
1787
+ btn.textContent = t("schedSaving");
1788
+ return fetch("/runtime/server-config", {
1789
+ method: "POST",
1790
+ headers: { "Content-Type": "application/json" },
1791
+ body: JSON.stringify({ targets: list }),
1792
+ })
1793
+ .then(function(r) { return r.json(); })
1794
+ .then(function(d) {
1795
+ if (d.ok) {
1796
+ // \u4FDD\u7559\u5F53\u524D\u7F16\u8F91\u4E0B\u6807\uFF0C\u4FDD\u5B58\u540E\u8868\u5355\u91CD\u65B0\u52A0\u8F7D\u63A9\u7801\u4E0E instanceId
1797
+ var keepIndex = schedEditIndex >= 0 ? schedEditIndex : schedTargets.length;
1798
+ refreshServerConfig();
1799
+ schedEditIndex = keepIndex;
1800
+ if (restartAfter) {
1801
+ showSchedMsg(t("schedSaved"), true);
1802
+ doRestartBridge();
1803
+ return true;
1804
+ }
1805
+ // \u70ED\u5207\u6362\uFF1A\u4FDD\u5B58\u540E\u7ACB\u5373\u6309\u65B0\u914D\u7F6E\u91CD\u65B0\u6CE8\u518C\uFF08\u7F16\u8F91\u5730\u5740/\u65B0\u589E target \u5747\u5373\u65F6\u751F\u6548\uFF0C
1806
+ // \u65E0\u9700\u91CD\u542F Bridge\uFF1B\u5B9E\u4F8B\u4FE1\u606F\u6309 instanceId \u5728\u670D\u52A1\u7AEF\u590D\u7528\uFF09
1807
+ showSchedMsg(t("schedSavedRegistering"), true);
1808
+ btn.textContent = t("schedReRegistering");
1809
+ return runReRegister().then(function(reg) {
1810
+ if (reg.ok) {
1811
+ showSchedMsg(t("schedReRegistered"), true);
1812
+ refreshNodeInfo();
1813
+ } else {
1814
+ showSchedMsg(t("schedReRegisterFail") + (reg.error ? ": " + reg.error : ""), false);
1815
+ }
1816
+ return reg.ok;
1817
+ });
1818
+ }
1819
+ showSchedMsg(t("schedSaveFail") + (d.error ? ": " + d.error : ""), false);
1820
+ return false;
1821
+ })
1822
+ .catch(function() {
1823
+ showSchedMsg(t("schedSaveFail"), false);
1824
+ return false;
1825
+ })
1826
+ .then(function(ok) {
1827
+ btn.disabled = false;
1828
+ btn.textContent = t("schedSave");
1829
+ return ok;
1830
+ });
1831
+ }
1832
+
1833
+ // \u4FDD\u5B58\u5E76\u91CD\u542F\uFF1A\u5148\u786E\u8BA4\uFF0C\u4FDD\u5B58\u6210\u529F\u540E\u7531 saveServerConfig \u89E6\u53D1\u91CD\u542F
1834
+ function saveServerConfigAndRestart() {
1835
+ if (!confirm(t("schedSaveRestartConfirm"))) return;
1836
+ saveServerConfig(true);
1837
+ }
1838
+
1839
+ // \u5220\u9664\u5F53\u524D\u7F16\u8F91\u7684 target\uFF1A\u4ECE\u5217\u8868\u79FB\u9664\u540E\u5168\u91CF\u63D0\u4EA4
1840
+ function deleteTargetFromList() {
1841
+ if (schedEditIndex < 0 || schedEditIndex >= schedTargets.length) return;
1842
+ if (!confirm(t("schedConfirmDelete"))) return;
1843
+ // \u6355\u83B7\u53D1\u8D77\u65F6\u7684\u4E0B\u6807\uFF1A\u8BF7\u6C42\u671F\u95F4\u7528\u6237\u53EF\u80FD\u5207\u6362\u7F16\u8F91\u9879\uFF0C\u6062\u590D\u65F6\u9700\u7528\u539F\u4E0B\u6807\u63D2\u56DE\u539F\u4F4D
1844
+ var editIdx = schedEditIndex;
1845
+ var removed = schedTargets.splice(editIdx, 1)[0];
1846
+ var list = schedTargets.map(function(item) {
1847
+ return {
1848
+ serverUrl: item.serverUrl,
1849
+ userKey: "",
1850
+ tenantId: item.tenantId || "",
1851
+ instanceName: item.instanceName || "",
1852
+ connectionKey: "",
1853
+ registerAddr: item.registerAddr || "",
1854
+ tunnel: item.tunnel === true ? "true" : (item.tunnel === false ? "false" : "auto"),
1855
+ };
1856
+ });
1857
+ fetch("/runtime/server-config", {
1858
+ method: "POST",
1859
+ headers: { "Content-Type": "application/json" },
1860
+ body: JSON.stringify({ targets: list }),
1861
+ })
1862
+ .then(function(r) { return r.json(); })
1863
+ .then(function(d) {
1864
+ if (d.ok) {
1865
+ startAddTarget();
1866
+ refreshServerConfig();
1867
+ // \u70ED\u5207\u6362\uFF1A\u5220\u9664\u540E\u7ACB\u5373\u91CD\u65B0\u6CE8\u518C\uFF0C\u505C\u6389\u5DF2\u79FB\u9664\u5730\u5740\u7684\u6CE8\u518C\u4E0E\u96A7\u9053
1868
+ showSchedMsg(t("schedSavedRegistering"), true);
1869
+ runReRegister().then(function(reg) {
1870
+ if (reg.ok) {
1871
+ showSchedMsg(t("schedReRegistered"), true);
1872
+ refreshNodeInfo();
1873
+ } else {
1874
+ showSchedMsg(t("schedReRegisterFail") + (reg.error ? ": " + reg.error : ""), false);
1875
+ }
1876
+ });
1877
+ } else {
1878
+ schedTargets.splice(editIdx, 0, removed);
1879
+ renderSchedTargetList();
1880
+ showSchedMsg(t("schedSaveFail") + (d.error ? ": " + d.error : ""), false);
1881
+ }
1882
+ })
1883
+ .catch(function() {
1884
+ schedTargets.splice(editIdx, 0, removed);
1885
+ renderSchedTargetList();
1886
+ showSchedMsg(t("schedSaveFail"), false);
1887
+ });
1888
+ }
1889
+
1890
+ // \u53D1\u8D77\u91CD\u65B0\u6CE8\u518C\u8BF7\u6C42\uFF08\u4E0D\u7BA1\u7406\u6309\u94AE\u72B6\u6001\uFF0C\u4F9B\u624B\u52A8\u6309\u94AE\u4E0E\u4FDD\u5B58\u540E\u7684\u70ED\u5207\u6362\u94FE\u5171\u7528\uFF09
1891
+ function runReRegister() {
1892
+ return fetch("/runtime/re-register", { method: "POST", headers: { "Content-Type": "application/json" } })
1893
+ .then(function(r) { return r.json(); })
1894
+ .catch(function() { return { ok: false }; });
1895
+ }
1896
+
1431
1897
  function doReRegister() {
1432
1898
  var btn = document.getElementById("btnReRegister");
1433
1899
  var msg = document.getElementById("schedMsg");
@@ -1435,8 +1901,7 @@ function doReRegister() {
1435
1901
  msg.style.display = "none";
1436
1902
  btn.disabled = true;
1437
1903
  btn.textContent = t("schedReRegistering");
1438
- fetch("/runtime/re-register", { method: "POST", headers: { "Content-Type": "application/json" } })
1439
- .then(function(r) { return r.json(); })
1904
+ runReRegister()
1440
1905
  .then(function(d) {
1441
1906
  if (d.ok) {
1442
1907
  msg.textContent = t("schedReRegistered");
@@ -1447,13 +1912,52 @@ function doReRegister() {
1447
1912
  msg.className = "s-msg error";
1448
1913
  }
1449
1914
  })
1915
+ .then(function() {
1916
+ btn.disabled = false;
1917
+ btn.textContent = t("schedReRegister");
1918
+ });
1919
+ }
1920
+
1921
+ // \u2500\u2500 Access Control \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1922
+ // \u52A0\u8F7D\u5F53\u524D IP \u767D\u540D\u5355\u5230\u8868\u5355\uFF08\u7A7A\u5217\u8868 = \u9ED8\u8BA4\u5168\u653E\u884C\uFF09
1923
+ function refreshAccessControl() {
1924
+ fetch("/runtime/access-control").then(function(r) { return r.json(); }).then(function(d) {
1925
+ if (!d.ok) return;
1926
+ document.getElementById("cfgAllowIps").value = (d.allowIps || []).join("\\n");
1927
+ }).catch(function() {});
1928
+ }
1929
+
1930
+ // \u4FDD\u5B58\u767D\u540D\u5355\uFF08\u540E\u7AEF\u6821\u9A8C\u5408\u6CD5\u6027\uFF0C\u4FDD\u5B58\u540E\u7ACB\u5373\u751F\u6548\uFF09
1931
+ function saveAccessControl() {
1932
+ var msg = document.getElementById("accessMsg");
1933
+ var btn = document.getElementById("btnSaveAccessControl");
1934
+ msg.className = "s-msg";
1935
+ msg.style.display = "none";
1936
+ btn.disabled = true;
1937
+ btn.textContent = t("accSaving");
1938
+ fetch("/runtime/access-control", {
1939
+ method: "POST",
1940
+ headers: { "Content-Type": "application/json" },
1941
+ body: JSON.stringify({ allowIps: document.getElementById("cfgAllowIps").value }),
1942
+ })
1943
+ .then(function(r) { return r.json(); })
1944
+ .then(function(d) {
1945
+ if (d.ok) {
1946
+ msg.textContent = t("accSaved");
1947
+ msg.className = "s-msg success";
1948
+ document.getElementById("cfgAllowIps").value = (d.allowIps || []).join("\\n");
1949
+ } else {
1950
+ msg.textContent = d.error || t("accSaveFail");
1951
+ msg.className = "s-msg error";
1952
+ }
1953
+ })
1450
1954
  .catch(function() {
1451
- msg.textContent = t("schedReRegisterFail");
1955
+ msg.textContent = t("accSaveFail");
1452
1956
  msg.className = "s-msg error";
1453
1957
  })
1454
1958
  .then(function() {
1455
1959
  btn.disabled = false;
1456
- btn.textContent = t("schedReRegister");
1960
+ btn.textContent = t("accSave");
1457
1961
  });
1458
1962
  }
1459
1963
 
@@ -1498,11 +2002,11 @@ switchSettingsPage = function(page) {
1498
2002
  if (nodeInfoTimer) { clearTimeout(nodeInfoTimer); nodeInfoTimer = null; }
1499
2003
  if (page === "node") { refreshNodeInfo(); }
1500
2004
  else if (page === "logs") { refreshLogs(); startLogAutoRefresh(); }
1501
- else if (page === "scheduler") { refreshNodeInfo(); }
2005
+ else if (page === "scheduler") { refreshNodeInfo(); refreshServerConfig(); }
1502
2006
  };
1503
2007
  </script>
1504
2008
  </body>
1505
- </html>`,ge=`<!DOCTYPE html>
2009
+ </html>`,Ee=`<!DOCTYPE html>
1506
2010
  <html lang="zh-CN">
1507
2011
  <head>
1508
2012
  <meta charset="UTF-8">
@@ -1673,7 +2177,7 @@ document.getElementById("loginForm").addEventListener("keydown", function(e) {
1673
2177
  });
1674
2178
  </script>
1675
2179
  </body>
1676
- </html>`,pe=`<!DOCTYPE html>
2180
+ </html>`,Te=`<!DOCTYPE html>
1677
2181
  <html lang="zh-CN">
1678
2182
  <head>
1679
2183
  <meta charset="UTF-8">
@@ -1960,5 +2464,5 @@ async function doSkip() {
1960
2464
  document.getElementById("setupForm").addEventListener("submit", doSave);
1961
2465
  </script>
1962
2466
  </body>
1963
- </html>`;export{d as dashboardRouter};
2467
+ </html>`;export{g as dashboardRouter};
1964
2468