├── .dockerignore ├── .gitignore ├── Dockerfile ├── bot └── index.js ├── build-docker.sh ├── core.js ├── database ├── index.js ├── load.js ├── servers.js ├── setting.js ├── ssh_scripts.js └── traffic.js ├── getStat.js ├── install.sh ├── modules ├── servers │ ├── func.js │ └── index.js ├── setting │ └── index.js ├── ssh_scripts │ └── index.js └── stats │ └── index.js ├── neko-status ├── .gitignore ├── build.sh ├── config.go ├── config.yaml ├── go.mod ├── iperf3.go ├── iperf3 │ └── iperf3.go ├── main.go ├── mtr.go ├── mtr │ └── mtr.go ├── stat │ └── stat.go └── walled │ └── walled.go ├── nekonekostatus.js ├── nekonekostatus.service ├── package.json ├── readme.md ├── restart.sh ├── run.sh ├── ssh.js ├── static ├── css │ ├── style.css │ └── style.min.css ├── img │ └── logo.svg └── js │ ├── core.js │ ├── load.js │ ├── login.js │ ├── md5.min.js │ ├── stat.js │ ├── stats.js │ ├── traffic.js │ └── webssh.js ├── stop.sh └── views ├── admin.html ├── admin ├── servers.html ├── servers │ ├── add.html │ └── edit.html ├── setting.html └── ssh_scripts.html ├── appbar.html ├── base.html ├── footer.html ├── login.html ├── stat.html ├── statistics.html ├── stats.html ├── stats ├── card.html └── list.html └── webssh.html /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.db 3 | package-lock.json 4 | token.json 5 | .git 6 | neko-status -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.js 2 | database/db.db 3 | node_modules 4 | work.js 5 | neko-status/build 6 | modules/servers/neko-status 7 | tokens.json 8 | log/*.log 9 | package-lock.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 as build 2 | 3 | WORKDIR /app 4 | COPY . ./ 5 | RUN npm install --registry=https://registry.npm.taobao.org 6 | FROM node:16-buster-slim 7 | COPY --from=build /app / 8 | CMD [ "node", "nekonekostatus.js" ] -------------------------------------------------------------------------------- /bot/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // const config=require("../config"); 3 | // const db=require("../database")(); 4 | const TelegramBot = require('node-telegram-bot-api'); 5 | module.exports=(token,chatIds)=>{ 6 | const bot=new TelegramBot(token,{}); 7 | function isPm(msg){return msg.from.id==msg.chat.id;} 8 | var Masters=new Set(); 9 | // console.log(masters); 10 | // for(var chatId of masters)Masters.add(chatId); 11 | async function notice(str){ 12 | for(var chatId of chatIds){ 13 | try{ 14 | await bot.sendMessage(chatId,str); 15 | } catch(e){} 16 | } 17 | } 18 | var funcs={ 19 | isPm, 20 | notice, 21 | // Masters, 22 | },cmds=[]; 23 | for(var mod of [])cmds.push.apply(cmds,mod(bot,funcs,db)); 24 | return { 25 | bot, 26 | funcs, 27 | cmds 28 | } 29 | } -------------------------------------------------------------------------------- /build-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker login 3 | docker build -t nekonekostatus . 4 | docker tag nekonekostatus:latest nkeonkeo/nekonekostatus 5 | docker push nkeonkeo/nekonekostatus -------------------------------------------------------------------------------- /core.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function pr(status,data){return {status,data}}; 3 | function strB(b){ 4 | var base=1024; 5 | if(b{ 4 | var {path=__dirname+'/db.db'}=conf; 5 | var DB=new Database(path); 6 | 7 | const {servers}=require("./servers")(DB), 8 | {traffic,lt}=require("./traffic")(DB), 9 | {load_m,load_h}=require("./load")(DB), 10 | {ssh_scripts}=require("./ssh_scripts")(DB), 11 | {setting}=require("./setting")(DB); 12 | function getServers(){return servers.all();} 13 | return { 14 | DB, 15 | servers,getServers, 16 | traffic,lt, 17 | load_m,load_h, 18 | ssh_scripts, 19 | setting, 20 | }; 21 | } -------------------------------------------------------------------------------- /database/load.js: -------------------------------------------------------------------------------- 1 | `use strict` 2 | function pad(arr,len){ 3 | for(var i=arr.length;i{ 8 | function gen(table,len){ 9 | // DB.prepare(`DROP TABLE ${table}`).run(); 10 | DB.prepare(`CREATE TABLE IF NOT EXISTS ${table} (sid,cpu,mem,swap,ibw,obw)`).run(); 11 | return { 12 | len, 13 | _ins: DB.prepare(`INSERT INTO ${table} (sid,cpu,mem,swap,ibw,obw) VALUES (@sid,@cpu,@mem,@swap,@ibw,@obw)`), 14 | ins(sid){this._ins.run({sid,cpu:0,mem:0,swap:0,ibw:0,obw:0})}, 15 | select(sid){return pad(this._select.all(sid),this.len)},_select: DB.prepare(`SELECT * FROM ${table} WHERE sid=?`), 16 | count(sid){return DB.prepare(`SELECT COUNT(*) FROM ${table} WHERE sid=?`).get(sid)[`COUNT(*)`];}, 17 | shift(sid,{cpu,mem,swap,ibw,obw}){ 18 | if(this.count(sid)>=this.len)this._del.run(sid); 19 | this._ins.run({sid,cpu,mem,swap,ibw,obw}); 20 | },_del:DB.prepare(`DELETE FROM ${table} WHERE sid=? LIMIT 1`), 21 | del_sid(sid){DB.prepare(`DELETE FROM ${table} WHERE sid=?`).run(sid)} 22 | } 23 | } 24 | return { 25 | load_m:gen('load_m',60), 26 | load_h:gen('load_h',24), 27 | }; 28 | } -------------------------------------------------------------------------------- /database/servers.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | module.exports=(DB)=>{ 3 | // // // DB.prepare("DROP TABLE servers").run(); 4 | DB.prepare("CREATE TABLE IF NOT EXISTS servers (sid,name,data,top,status,PRIMARY KEY(sid))").run(); 5 | const servers={ 6 | _ins:DB.prepare("INSERT INTO servers (sid,name,data,top,status) VALUES (?,?,?,?,?)"), 7 | ins(sid,name,data,top,status=1){this._ins.run(sid,name,JSON.stringify(data),top,status)}, 8 | _upd:DB.prepare("UPDATE servers SET name=?,data=?,top=? WHERE sid=?"), 9 | upd(sid,name,data,top){ 10 | this._upd.run(name,JSON.stringify(data),top,sid); 11 | }, 12 | upd_status(sid,status){DB.prepare("UPDATE servers SET status=? WHERE sid=?").run(status,sid);}, 13 | upd_data(sid,data){DB.prepare("UPDATE servers SET data=? WHERE sid=?").run(JSON.stringify(data),sid);}, 14 | upd_top(sid,top){ 15 | this._upd_top.run(top,sid); 16 | },_upd_top:DB.prepare("UPDATE servers set top=? WHERE sid=?"), 17 | _get:DB.prepare("SELECT * FROM servers WHERE sid=?"), 18 | get(sid){ 19 | var server=this._get.get(sid); 20 | if(server)server.data=JSON.parse(server.data); 21 | return server; 22 | }, 23 | del(sid){DB.prepare("DELETE FROM servers WHERE sid=?").run(sid);}, 24 | _all:DB.prepare("SELECT * FROM servers ORDER BY top DESC"), 25 | all(){ 26 | var svrs=this._all.all(); 27 | svrs.forEach(svr=>{svr.data=JSON.parse(svr.data);}); 28 | return svrs; 29 | }, 30 | }; 31 | return {servers}; 32 | } -------------------------------------------------------------------------------- /database/setting.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | module.exports=(DB)=>{ 4 | // DB.prepare("DROP TABLE setting").run(); 5 | DB.prepare("CREATE TABLE IF NOT EXISTS setting (key,val,PRIMARY KEY(key))").run(); 6 | function S(val){return JSON.stringify(val);} 7 | function P(pair){ 8 | return pair?JSON.parse(pair.val):null; 9 | } 10 | const setting={ 11 | ins(key,val){this._ins.run(key,S(val))},_ins:DB.prepare("INSERT INTO setting (key,val) VALUES (?,?)"), 12 | set(key,val){this._set.run(key,S(val));},_set:DB.prepare("REPLACE INTO setting (key,val) VALUES (?,?)"), 13 | get(key){return P(this._get.get(key));},_get:DB.prepare("SELECT * FROM setting WHERE key=?"), 14 | del(key){DB.prepare("DELETE FROM setting WHERE key=?").run(key);}, 15 | all(){ 16 | var s={}; 17 | for(var {key,val} of this._all.all())s[key]=JSON.parse(val); 18 | return s; 19 | },_all:DB.prepare("SELECT * FROM setting"), 20 | }; 21 | function init(key,val){if(setting.get(key)==undefined)setting.ins(key,val);} 22 | init("listen",5555); 23 | init("password","nekonekostatus"); 24 | init("site",{ 25 | name:"Neko Neko Status", 26 | url:"https://status.nekoneko.cloud", 27 | }); 28 | init("neko_status_url","https://github.com/nkeonkeo/nekonekostatus/releases/download/v0.1/neko-status"); 29 | init("debug",0); 30 | return {setting}; 31 | } -------------------------------------------------------------------------------- /database/ssh_scripts.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | module.exports=(DB)=>{ 3 | // DB.prepare("DROP TABLE ssh_scripts").run(); 4 | DB.prepare("CREATE TABLE IF NOT EXISTS ssh_scripts (id,name,content,PRIMARY KEY(id))").run(); 5 | const ssh_scripts={ 6 | ins(id,name,content){this._ins.run({id,name,content})},_ins: DB.prepare("INSERT INTO ssh_scripts (id,name,content) VALUES (@id,@name,@content)"), 7 | get(id){return this._get.get(id);},_get:DB.prepare("SELECT * FROM ssh_scripts WHERE id=? LIMIT 1"), 8 | upd(id,name,content){this._upd.run(name,content,id);},_upd:DB.prepare("UPDATE ssh_scripts set name=?,content=? WHERE id=?"), 9 | del(id){this._del.run(id)},_del:DB.prepare("DELETE FROM ssh_scripts WHERE id=?"), 10 | all(all=1){return all?this._all.all():this._all.get()},_all:DB.prepare("SELECT * FROM ssh_scripts"), 11 | }; 12 | return { 13 | ssh_scripts 14 | }; 15 | } -------------------------------------------------------------------------------- /database/traffic.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | module.exports=(DB)=>{ 3 | function shift(a){a.shift();a.push([0,0]);return a;} 4 | // DB.prepare("DROP TABLE traffic").run(); 5 | DB.prepare("CREATE TABLE IF NOT EXISTS traffic (sid,hs,ds,ms,PRIMARY KEY(sid))").run(); 6 | const traffic={ 7 | _ins: DB.prepare("INSERT INTO traffic (sid,hs,ds,ms) VALUES (@sid,@hs,@ds,@ms)"), 8 | ins(sid){ 9 | this._ins.run({ 10 | sid, 11 | hs:JSON.stringify(new Array(24).fill([0,0])), 12 | ds:JSON.stringify(new Array(31).fill([0,0])), 13 | ms:JSON.stringify(new Array(12).fill([0,0])) 14 | }) 15 | }, 16 | qry(sid){return this._qry.get(sid)},_qry: DB.prepare("SELECT * FROM traffic WHERE sid=?"), 17 | get(sid){ 18 | var t=this._get.get(sid); 19 | if(t)return {hs:JSON.parse(t.hs),ds:JSON.parse(t.ds),ms:JSON.parse(t.ms),} 20 | this.ins(sid); 21 | return { 22 | hs:new Array(24).fill([0,0]), 23 | ds:new Array(31).fill([0,0]), 24 | ms:new Array(12).fill([0,0]) 25 | }; 26 | },_get:DB.prepare("SELECT hs,ds,ms FROM traffic WHERE sid=?"), 27 | UPD(sid,hs,ds,ms){this._UPD.run(JSON.stringify(hs),JSON.stringify(ds),JSON.stringify(ms),sid)},_UPD:DB.prepare("UPDATE traffic SET hs=?,ds=?,ms=? WHERE sid=?"), 28 | upd_sid(sid,newsid){DB.prepare("UPDATE traffic SET sid=? WHERE sid=?").run(newsid,sid)}, 29 | 30 | get_hs(sid){return JSON.parse(this._hs.get(sid).hs)},_hs:DB.prepare("SELECT hs FROM traffic WHERE sid=?"), 31 | upd_hs(sid,hs){this._UpdHs.run(JSON.stringify(hs),sid)},_UpdHs:DB.prepare("UPDATE traffic SET hs=? WHERE sid=?"), 32 | 33 | get_ds(sid){return JSON.parse(this._hs.get(sid).ds)},_ds:DB.prepare("SELECT ds FROM traffic WHERE sid=?"), 34 | upd_ds(sid,ds){this._UpdDs.run(JSON.stringify(ds),sid)},_UpdDs:DB.prepare("UPDATE traffic SET ds=? WHERE sid=?"), 35 | 36 | get_ms(sid){return JSON.parse(this._ms.get(sid).ms)},_ms:DB.prepare("SELECT ms FROM traffic WHERE sid=?"), 37 | upd_ms(sid,ms){this._UpdMs.run(JSON.stringify(ms),sid)},_UpdMs:DB.prepare("UPDATE traffic SET ms=? WHERE sid=?"), 38 | 39 | del(sid){DB.prepare("DELETE FROM traffic WHERE sid=?").run(sid)}, 40 | all(){return this._all.all()},itr(){return this._all.iterate()},_all:DB.prepare("SELECT * FROM traffic"), 41 | 42 | add(sid,tf){ 43 | var {hs,ds,ms}=this.get(sid); 44 | hs[23][0]+=tf[0],ds[30][0]+=tf[0],ms[11][0]+=tf[0]; 45 | hs[23][1]+=tf[1],ds[30][1]+=tf[1],ms[11][1]+=tf[1]; 46 | this.UPD(sid,hs,ds,ms); 47 | }, 48 | shift_hs(){ 49 | for(var {sid,hs} of this.all()) 50 | this.upd_hs(sid,shift(JSON.parse(hs))); 51 | }, 52 | shift_ds(){ 53 | for(var {sid,ds} of this.all()) 54 | this.upd_ds(sid,shift(JSON.parse(ds))); 55 | }, 56 | shift_ms(){ 57 | for(var {sid,ms} of this.all()) 58 | this.upd_ms(sid,shift(JSON.parse(ms))); 59 | } 60 | } 61 | // DB.prepare("DROP TABLE lt").run(); 62 | DB.prepare(`CREATE TABLE IF NOT EXISTS lt (sid,traffic,PRIMARY KEY(sid))`).run(); 63 | const lt={ 64 | ins(sid,traffic=[0,0]){ 65 | this._ins.run(sid,JSON.stringify(traffic)); 66 | return {sid,traffic}; 67 | },_ins:DB.prepare(`INSERT INTO lt (sid,traffic) VALUES (?,?)`), 68 | get(sid){ 69 | var x=this._get.get(sid); 70 | if(x)x.traffic=JSON.parse(x.traffic); 71 | return x; 72 | },_get:DB.prepare(`SELECT * FROM lt WHERE sid=?`), 73 | set(sid,traffic){return this._set.run(JSON.stringify(traffic),sid);},_set:DB.prepare(`UPDATE lt SET traffic=? WHERE sid=?`), 74 | del(sid){this._del.run(sid);},_del:DB.prepare(`DELETE FROM lt WHERE sid=?`), 75 | }; 76 | return {traffic,lt}; 77 | } -------------------------------------------------------------------------------- /getStat.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const ssh=require("./ssh"); 3 | function sleep(ms){return new Promise(resolve=>setTimeout(()=>resolve(),ms));}; 4 | function analyze(data,interval=0.2){ 5 | try{ 6 | var [s1,s2,mem_res]=data.split('\n----\n'), 7 | [cpu1,net1]=s1.split('\n---\n'), 8 | [cpu2,net2]=s2.split('\n---\n'); 9 | mem_res=mem_res.split('\n'); 10 | } 11 | catch(e){return false} 12 | 13 | var sys1=[],cput1=[],sys2=[],cput2=[],per=[]; 14 | for(var l of cpu1.split('\n')){ 15 | var t=l.split(' '),tot=0; 16 | sys1.push(Number(t[3])); 17 | for(var x of t)tot+=Number(x); 18 | cput1.push(tot); 19 | } 20 | for(var l of cpu2.split('\n')){ 21 | var t=l.split(' '),tot=0; 22 | sys2.push(Number(t[3])); 23 | for(var x of t)tot+=Number(x); 24 | cput2.push(tot); 25 | } 26 | for(var i in sys1) 27 | per.push(1-(sys2[i]-sys1[i])/(cput2[i]-cput1[i])); 28 | var cpu={ 29 | multi:per.shift(), 30 | single:per, 31 | }; 32 | 33 | var i1=0,o1=0,i2=0,o2=0; 34 | for(var l of net1.split('\n')){ 35 | var t=l.trim().split(/\s+/); 36 | i1+=Number(t[1]),o1+=Number(t[9]); 37 | } 38 | for(var l of net2.split('\n')){ 39 | var t=l.trim().split(/\s+/); 40 | i2+=Number(t[1]),o2+=Number(t[9]); 41 | } 42 | var net={ 43 | delta:{in:(i2-i1)/interval,out:(o2-o1)/interval}, 44 | total:{in:i2,out:o2} 45 | }; 46 | 47 | var Mem=mem_res[0].split(/\s+/),Swap=mem_res[1].split(/\s+/),MEM=[],SWAP=[]; 48 | for(var x of Mem)MEM.push(Number(x)*1000); 49 | for(var x of Swap)SWAP.push(Number(x)*1000); 50 | var mem={ 51 | virtual:{total:MEM[1],used:MEM[2],free:MEM[3],shared:MEM[4],cache:MEM[5],available:MEM[6]}, 52 | swap:{total:SWAP[1],used:SWAP[2],free:SWAP[3]} 53 | }; 54 | return {cpu,mem,net}; 55 | } 56 | async function get(key,interval=0.1){ 57 | if(key.privateKey=='')delete key.privateKey; 58 | var con=await ssh.ssh_con(key); 59 | if(!con||!con.isConnected())return false; 60 | 61 | var sh=` 62 | cat /proc/stat | grep cpu | awk '{print $2,$3,$4,$5,$6,$7,$8}' 63 | echo '---' 64 | cat /proc/net/dev | tail -n +3 | grep -v lo 65 | echo '----' 66 | sleep ${interval} 67 | cat /proc/stat | grep cpu | awk '{print $2,$3,$4,$5,$6,$7,$8}' 68 | echo '---' 69 | cat /proc/net/dev | tail -n +3 | grep -v lo 70 | echo '----' 71 | free | tail -n +2 72 | `; 73 | var data=await ssh.ssh_exec(con,sh); 74 | if(!data)return false; 75 | con.dispose(); 76 | return analyze(data,interval); 77 | } 78 | module.exports={ 79 | analyze,get 80 | } 81 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | clear && echo "\ 4 | ############################################################ 5 | 6 | Neko Neko Status 一键安装脚本 7 | 8 | 上次更新: 2021-11-07 9 | 10 | Powered by Neko Neko Cloud 11 | 12 | ############################################################ 13 | " 14 | 15 | echo "安装即将开始 16 | 17 | 如果您想取消安装, 请在 5 秒钟内按 Ctrl+C 终止此脚本" 18 | sleep 5 19 | 20 | 21 | clear && echo "正在安装npm,git,gcc" 22 | 23 | yum install epel-release -y && yum install centos-release-scl git -y && yum install nodejs devtoolset-8-gcc* -y 24 | apt update -y && apt-get install nodejs npm git build-essential -y 25 | 26 | clear && echo "正在更新npm" 27 | bash -c "npm install n -g" 28 | source /root/.bashrc 29 | bash -c "n latest" 30 | source /root/.bashrc 31 | bash -c "npm install npm@latest -g" 32 | source /root/.bashrc 33 | bash -c "npm install forever -g" 34 | source /root/.bashrc 35 | cd /root/ 36 | clear && echo "正在克隆仓库" 37 | git clone https://github.com/nkeonkeo/nekonekostatus.git 38 | cd nekonekostatus 39 | git pull 40 | clear && echo "正在安装依赖模块" 41 | source /opt/rh/devtoolset-8/enable 42 | npm install 43 | 44 | echo "安装完成, 正在启动面板" 45 | 46 | echo "[Unit] 47 | Description=nekonekostatus 48 | After=network.target 49 | 50 | [Service] 51 | Type=simple 52 | Restart=always 53 | RestartSec=5 54 | ExecStart=/root/nekonekostatus/nekonekostatus.js 55 | 56 | [Install] 57 | WantedBy=multi-user.target" > /etc/systemd/system/nekonekostatus-dashboard.service 58 | systemctl daemon-reload 59 | systemctl enable nekonekostatus-dashboard.service 60 | systemctl start nekonekostatus-dashboard.service 61 | sleep 3 62 | if systemctl status nekonekostatus-dashboard.service | grep "active (running)" > /dev/null 63 | then 64 | echo "面板启动成功" 65 | echo "" 66 | echo "默认访问端口: 5555" 67 | echo "默认密码: nekonekostatus" 68 | echo "" 69 | echo "请及时修改密码!" 70 | echo "" 71 | echo "------------" 72 | echo "" 73 | echo "TIPS: " 74 | echo "若无法访问, 请先尝试卸载防火墙, 并检查iptables规则" 75 | echo "CentOS: yum remove firewalld -y" 76 | echo "Debian: apt remove ufw -y" 77 | else 78 | echo "面板启动失败" 79 | systemctl status nekonekostatus-dashboard.service 80 | fi -------------------------------------------------------------------------------- /modules/servers/func.js: -------------------------------------------------------------------------------- 1 | const ssh=require("../../ssh"); 2 | async function initServer(server,neko_status_url){ 3 | var sh= 4 | `wget --version||yum install wget -y||apt-get install wget -y 5 | /usr/bin/neko-status -v||(wget ${neko_status_url} -O /usr/bin/neko-status && chmod +x /usr/bin/neko-status) 6 | systemctl stop nekonekostatus 7 | mkdir /etc/neko-status/ 8 | echo "key: ${server.data.api.key} 9 | port: ${server.data.api.port} 10 | debug: false" > /etc/neko-status/config.yaml 11 | systemctl stop nekonekostatus 12 | echo "[Unit] 13 | Description=nekonekostatus 14 | 15 | [Service] 16 | Restart=always 17 | RestartSec=5 18 | ExecStart=/usr/bin/neko-status -c /etc/neko-status/config.yaml 19 | 20 | [Install] 21 | WantedBy=multi-user.target" > /etc/systemd/system/nekonekostatus.service 22 | systemctl daemon-reload 23 | systemctl start nekonekostatus 24 | systemctl enable nekonekostatus` 25 | var res=await ssh.Exec(server.data.ssh,sh); 26 | if(res.success)return {status:1,data:"安装成功"}; 27 | else return {status:0,data:"安装失败/SSH连接失败"}; 28 | } 29 | async function updateServer(server,neko_status_url){ 30 | var sh= 31 | `rm -f /usr/bin/neko-status 32 | wget ${neko_status_url} -O /usr/bin/neko-status 33 | chmod +x /usr/bin/neko-status` 34 | await ssh.Exec(server.data.ssh,sh); 35 | return {status:1,data:"更新成功"}; 36 | } 37 | module.exports={ 38 | initServer,updateServer, 39 | } 40 | -------------------------------------------------------------------------------- /modules/servers/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const {initServer,updateServer}=require("./func"), 3 | ssh=require("../../ssh"); 4 | module.exports=svr=>{ 5 | const {db,setting,pr,parseNumber}=svr.locals; 6 | svr.post("/admin/servers/add",async(req,res)=>{ 7 | var {sid,name,data,top,status}=req.body; 8 | if(!sid)sid=uuid.v1(); 9 | db.servers.ins(sid,name,data,top,status); 10 | res.json(pr(1,sid)); 11 | }); 12 | svr.get("/admin/servers/add",(req,res)=>{ 13 | res.render(`admin/servers/add`,{}); 14 | }); 15 | svr.post("/admin/servers/:sid/edit",async(req,res)=>{ 16 | var {sid}=req.params,{name,data,top,status}=req.body; 17 | db.servers.upd(sid,name,data,top); 18 | if(status!=null)db.servers.upd_status(sid,status); 19 | res.json(pr(1,'修改成功')); 20 | }); 21 | svr.post("/admin/servers/:sid/del",async(req,res)=>{ 22 | var {sid}=req.params; 23 | db.servers.del(sid); 24 | res.json(pr(1,'删除成功')); 25 | }); 26 | svr.post("/admin/servers/:sid/init",async(req,res)=>{ 27 | var {sid}=req.params, 28 | server=db.servers.get(sid); 29 | res.json(await initServer(server,db.setting.get("neko_status_url"))); 30 | }); 31 | svr.post("/admin/servers/:sid/update",async(req,res)=>{ 32 | var {sid}=req.params, 33 | server=db.servers.get(sid); 34 | res.json(await updateServer(server,db.setting.get("neko_status_url"))); 35 | }); 36 | svr.get("/admin/servers",(req,res)=>{ 37 | res.render("admin/servers",{ 38 | servers:db.servers.all() 39 | }) 40 | }); 41 | svr.post("/admin/servers/ord",(req,res)=>{ 42 | var {servers}=req.body,ord=0; 43 | servers.reverse(); 44 | for(var sid of servers)db.servers.upd_top(sid,++ord); 45 | res.json(pr(true,'更新成功')); 46 | }); 47 | svr.get("/admin/servers/:sid",(req,res)=>{ 48 | var {sid}=req.params,server=db.servers.get(sid); 49 | res.render(`admin/servers/edit`,{ 50 | server, 51 | }); 52 | }); 53 | svr.ws("/admin/servers/:sid/ws-ssh/:data",(ws,req)=>{ 54 | var {sid,data}=req.params,server=db.servers.get(sid); 55 | if(data)data=JSON.parse(data); 56 | ssh.createSocket(server.data.ssh,ws,data); 57 | }) 58 | 59 | svr.get("/get-neko-status",async(req,res)=>{ 60 | var path=__dirname+'/neko-status'; 61 | // if(!fs.existsSync(path)){ 62 | // await fetch("文件url", { 63 | // method: 'GET', 64 | // headers: { 'Content-Type': 'application/octet-stream' }, 65 | // }).then(res=>res.buffer()).then(_=>{ 66 | // fs.writeFileSync(path,_,"binary"); 67 | // }); 68 | // } 69 | res.sendFile(path); 70 | }) 71 | } -------------------------------------------------------------------------------- /modules/setting/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const express=require("express"); 3 | module.exports=async(svr)=>{ 4 | const {db,pr}=svr.locals; 5 | var rt=express.Router(); 6 | rt.get("/admin/setting",(req,res)=>{ 7 | res.render("admin/setting",{ 8 | setting:db.setting.all() 9 | }); 10 | }) 11 | rt.post("/admin/setting",(req,res)=>{ 12 | for(var [key,val] of Object.entries(req.body)){ 13 | db.setting.set(key,val); 14 | svr.locals.setting[key]=val; 15 | } 16 | res.json(pr(1,"修改成功")); 17 | if(req.body.listen)svr.server.close(()=>{ 18 | svr.server=svr.listen(req.body.listen,'',()=>{console.log(`server restart @ http://localhost:${req.body.listen}`);}) 19 | }); 20 | }); 21 | svr.use(rt); 22 | } -------------------------------------------------------------------------------- /modules/ssh_scripts/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const uuid=require("uuid"); 3 | module.exports=svr=>{ 4 | const {db,pr}=svr.locals; 5 | var rt=require("express").Router(); 6 | rt.get("/admin/ssh_scripts",(req,res)=>{ 7 | res.render("admin/ssh_scripts",{ 8 | ssh_scripts:db.ssh_scripts.all(), 9 | }); 10 | }); 11 | rt.post("/admin/ssh_scripts/add",(req,res)=>{ 12 | var {id=uuid.v1(),name,content}=req.body; 13 | db.ssh_scripts.ins(id,name,content); 14 | res.json(pr(true,id)); 15 | }); 16 | rt.post("/admin/ssh_scripts/get",(req,res)=>{ 17 | var {id}=req.body; 18 | res.json(pr(true,db.ssh_scripts.get(id))); 19 | }); 20 | rt.post("/admin/ssh_scripts/upd",(req,res)=>{ 21 | var {id,name,content}=req.body; 22 | db.ssh_scripts.upd(id,name,content); 23 | res.json(pr(true,'修改成功')); 24 | }); 25 | rt.post("/admin/ssh_scripts/del",(req,res)=>{ 26 | var {id}=req.body; 27 | db.ssh_scripts.del(id); 28 | res.json(pr(true,'删除成功')); 29 | }); 30 | svr.use(rt); 31 | } -------------------------------------------------------------------------------- /modules/stats/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const fetch=require("node-fetch"), 3 | schedule=require("node-schedule"); 4 | function sleep(ms){return new Promise(resolve=>setTimeout(()=>resolve(),ms));}; 5 | module.exports=async(svr)=>{ 6 | const {db,pr,bot}=svr.locals; 7 | var stats={},fails={},highcpu={},highDown={},updating=new Set(),noticed={}; 8 | function getStats(isAdmin=false){ 9 | let Stats={}; 10 | for(let {sid,status} of db.servers.all())if(status==1||(status==2&&isAdmin)){ 11 | if(stats[sid])Stats[sid]=stats[sid]; 12 | } 13 | return Stats; 14 | } 15 | svr.get("/",(req,res)=>{ 16 | let {theme=db.setting.get("theme")||"card"}=req.query; 17 | res.render(`stats/${theme}`,{ 18 | stats:getStats(req.admin), 19 | admin:req.admin 20 | }); 21 | }); 22 | svr.get("/stats/data",(req,res)=>{res.json(getStats(req.admin));}); 23 | svr.get("/stats/:sid",(req,res)=>{ 24 | let {sid}=req.params,node=stats[sid]; 25 | res.render('stat',{ 26 | sid,node, 27 | traffic:db.traffic.get(sid), 28 | load_m:db.load_m.select(sid), 29 | load_h:db.load_h.select(sid), 30 | admin:req.admin 31 | }); 32 | }); 33 | svr.get("/stats/:sid/data",(req,res)=>{ 34 | let {sid}=req.params; 35 | res.json({sid,...stats[sid]}); 36 | }); 37 | svr.post("/stats/update",(req,res)=>{ 38 | let {sid,data}=req.body; 39 | stats[sid]=data; 40 | res.json(pr(1,'update success')); 41 | }); 42 | async function getStat(server){ 43 | let res; 44 | try{ 45 | res=await fetch(`http://${server.data.ssh.host}:${server.data.api.port}/stat`,{ 46 | method:"GET", 47 | headers:{key:server.data.api.key}, 48 | timeout:15000, 49 | }).then(res=>res.json()); 50 | }catch(e){ 51 | // console.log(e); 52 | res={success:false,msg:'timeout'}; 53 | } 54 | if(res.success)return res.data; 55 | else return false; 56 | } 57 | async function update(server){ 58 | let {sid}=server; 59 | if(server.status<=0){ 60 | delete stats[sid]; 61 | return; 62 | } 63 | let stat=await getStat(server); 64 | if(stat){ 65 | let notice=false; 66 | if(stats[sid]&&stats[sid].stat==false)notice=true; 67 | if(server.data.device){ 68 | let device=stat.net.devices[server.data.device]; 69 | if(device){ 70 | stat.net.total=device.total; 71 | stat.net.delta=device.delta; 72 | } 73 | } 74 | stats[sid]={name:server.name,stat},fails[sid]=0; 75 | if(notice){ 76 | // console.log(`#恢复 ${server.name} ${new Date().toLocaleString()}`); 77 | bot.funcs.notice(`#恢复 ${server.name} ${new Date().toLocaleString()}`); 78 | } 79 | // if(stat.net.delta&&stat.net.delta.in>stat.net.delta.out*5&&stat.net.delta.in>15*1024*1024){ 80 | // if(highDown[sid]){ 81 | // bot.funcs.notice(`#下行异常 ${server.name} ↓:${strB(stat.net.delta.in)}/s ↑:${strB(stat.net.delta.out)}/s ${new Date().toLocaleString()}`); 82 | // } 83 | // else highDown[sid]=true; 84 | // } else highDown[sid]=false; 85 | // if(stat.cpu.multi>0.8){ 86 | // if((highcpu[sid]=(highcpu[sid]||0)+1)>=5){ 87 | // if(!noticed[sid]||new Date()-noticed[sid]>30*60*1000){ 88 | // bot.funcs.notice(`#过载 ${server.name} 持续5次探测CPU超过80% ${new Date().toLocaleString()}`); 89 | // noticed[sid]=new Date(); 90 | // } 91 | // } 92 | // } else if(stat.cpu.multi<0.5)highcpu[sid]=0; 93 | } else { 94 | let notice=false; 95 | if((fails[sid]=(fails[sid]||0)+1)>10){ 96 | if(stats[sid]&&stats[sid].stat)notice=true; 97 | stats[sid]={name:server.name,stat:false}; 98 | } 99 | if(notice){ 100 | // console.log(`#掉线 ${server.name} ${new Date().toLocaleString()}`); 101 | bot.funcs.notice(`#掉线 ${server.name} ${new Date().toLocaleString()}`); 102 | } 103 | } 104 | } 105 | async function get(){ 106 | let s=new Set(),wl=[]; 107 | for(let server of db.servers.all())if(server.status>0){ 108 | s.add(server.sid); 109 | if(updating.has(server.sid))continue; 110 | wl.push((async(server)=>{ 111 | updating.add(server.sid); 112 | await update(server); 113 | updating.delete(server.sid); 114 | })(server)); 115 | } 116 | for(let sid in stats)if(!s.has(sid))delete stats[sid]; 117 | return Promise.all(wl); 118 | } 119 | function calc(){ 120 | for(let server of db.servers.all()){ 121 | let {sid}=server,stat=stats[sid]; 122 | if(!stat||!stat.stat||stat.stat==-1)continue; 123 | let ni=stat.stat.net.total.in, 124 | no=stat.stat.net.total.out, 125 | t=db.lt.get(sid)||db.lt.ins(sid); 126 | let ti=ni{ 138 | for(let {sid} of db.servers.all()){ 139 | let cpu=-1,mem=-1,swap=-1,ibw=-1,obw=-1; 140 | let stat=stats[sid]; 141 | if(stat&&stat.stat&&stat.stat!=-1){ 142 | cpu=stat.stat.cpu.multi*100; 143 | mem=stat.stat.mem.virtual.usedPercent; 144 | swap=stat.stat.mem.swap.usedPercent; 145 | ibw=stat.stat.net.delta.in; 146 | obw=stat.stat.net.delta.out; 147 | } 148 | db.load_m.shift(sid,{cpu,mem,swap,ibw,obw}); 149 | } 150 | }); 151 | schedule.scheduleJob({minute:0,second:1},()=>{ 152 | db.traffic.shift_hs(); 153 | for(let {sid} of db.servers.all()){ 154 | let Cpu=0,Mem=0,Swap=0,Ibw=0,Obw=0,tot=0; 155 | for(let {cpu,mem,swap,ibw,obw} of db.load_m.select(sid))if(cpu!=-1){ 156 | ++tot; 157 | Cpu+=cpu,Mem+=mem,Swap+=swap,Ibw+=ibw,Obw+=obw; 158 | } 159 | if(tot==0)db.load_h.shift(sid,{cpu:-1,mem:-1,swap:-1,ibw:-1,obw:-1}); 160 | else db.load_h.shift(sid,{cpu:Cpu/tot,mem:Mem/tot,swap:Swap/tot,ibw:Ibw/tot,obw:Obw/tot}); 161 | } 162 | }); 163 | schedule.scheduleJob({hour:4,minute:0,second:2},()=>{db.traffic.shift_ds();}); 164 | schedule.scheduleJob({date:1,hour:4,minute:0,second:3},()=>{db.traffic.shift_ms();}); 165 | } 166 | -------------------------------------------------------------------------------- /neko-status/.gitignore: -------------------------------------------------------------------------------- 1 | neko-status 2 | go.sum -------------------------------------------------------------------------------- /neko-status/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mkdir build -p 3 | 4 | echo "build neko-status..." 5 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o build/neko-status 6 | echo "build linux amd64..." 7 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o build/neko-status_linux_amd64 8 | echo "build darwin amd64..." 9 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-w -s" -o build/neko-status_darwin_amd64 10 | # echo "build windows amd64..." 11 | # CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-w -s" -o build/neko-status_windows_amd64.exe 12 | echo "build freebsd amd64..." 13 | CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags="-w -s" -o build/neko-status_freebsd_amd64 14 | echo "build openbsd amd64..." 15 | CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build -ldflags="-w -s" -o build/neko-status_openbsd_amd64 16 | echo "build netbsd amd64..." 17 | CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 go build -ldflags="-w -s" -o build/neko-status_netbsd_amd64 18 | 19 | echo "build linux arm64..." 20 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o build/neko-status_linux_arm64 21 | echo "build darwin arm64..." 22 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-w -s" -o build/neko-status_darwin_arm64 23 | 24 | echo "build linux 386..." 25 | CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags="-w -s" -o build/neko-status_linux_386 26 | # echo "build windows 386..." 27 | # CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags="-w -s" -o build/neko-status_windows_386.exe 28 | echo "build freebsd 386..." 29 | CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags="-w -s" -o build/neko-status_freebsd_386 30 | echo "build openbsd 386..." 31 | CGO_ENABLED=0 GOOS=openbsd GOARCH=386 go build -ldflags="-w -s" -o build/neko-status_openbsd_386 32 | echo "build netbsd 386..." 33 | CGO_ENABLED=0 GOOS=netbsd GOARCH=386 go build -ldflags="-w -s" -o build/neko-status_netbsd_386 34 | 35 | echo "build linux arm7..." 36 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-w -s" -o build/neko-status_linux_arm7 37 | echo "build linux arm6..." 38 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-w -s" -o build/neko-status_linux_arm6 39 | echo "build linux arm5..." 40 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -ldflags="-w -s" -o build/neko-status_linux_arm5 41 | 42 | echo "build linux mips..." 43 | CGO_ENABLED=0 GOOS=linux GOARCH=mips go build -ldflags="-w -s" -o build/neko-status_linux_mips 44 | echo "build linux mipsle..." 45 | CGO_ENABLED=0 GOOS=linux GOARCH=mipsle go build -ldflags="-w -s" -o build/neko-status_linux_mipsle 46 | echo "build linux mips_softfloat..." 47 | CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -ldflags="-w -s" -o build/neko-status_linux_mips_softfloat 48 | echo "build linux mipsle_softfloat..." 49 | CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -ldflags="-w -s" -o build/neko-status_linux_mipsle_softfloat 50 | echo "build linux mips64 ..." 51 | CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -ldflags="-w -s" -o build/neko-status_linux_mips64 52 | echo "build linux mips64le ..." 53 | CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -ldflags="-w -s" -o build/neko-status_linux_mips64le 54 | echo "build linux mips64_softfloat ..." 55 | CGO_ENABLED=0 GOOS=linux GOARCH=mips64 GOMIPS=softfloat go build -ldflags="-w -s" -o build/neko-status_linux_mips64_softfloat 56 | echo "build linux mips64le_softfloat ..." 57 | CGO_ENABLED=0 GOOS=linux GOARCH=mips64le GOMIPS=softfloat go build -ldflags="-w -s" -o build/neko-status_linux_mips64le_softfloat 58 | 59 | echo "build linux ppc64 ..." 60 | CGO_ENABLED=0 GOOS=linux GOARCH=ppc64 go build -ldflags="-w -s" -o build/neko-status_linux_ppc64 61 | echo "build linux ppc64le ..." 62 | CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le go build -ldflags="-w -s" -o build/neko-status_linux_ppc64le -------------------------------------------------------------------------------- /neko-status/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type CONF struct { 4 | Mode int 5 | Key string 6 | Port int 7 | Url string 8 | } 9 | -------------------------------------------------------------------------------- /neko-status/config.yaml: -------------------------------------------------------------------------------- 1 | mode: 0 2 | key: e1c5d2ee-c395-4758-8cf6-13140a03a87e 3 | port: 9999 4 | url: https://status.nekoneko.cloud/ -------------------------------------------------------------------------------- /neko-status/go.mod: -------------------------------------------------------------------------------- 1 | module neko-status 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.7.7 7 | github.com/gorilla/websocket v1.5.0 8 | github.com/shirou/gopsutil v3.21.11+incompatible 9 | github.com/tonobo/mtr v0.1.0 10 | gopkg.in/yaml.v2 v2.4.0 11 | ) 12 | 13 | require ( 14 | github.com/buger/goterm v1.0.4 // indirect 15 | github.com/gin-contrib/sse v0.1.0 // indirect 16 | github.com/go-ole/go-ole v1.2.6 // indirect 17 | github.com/go-playground/locales v0.14.0 // indirect 18 | github.com/go-playground/universal-translator v0.18.0 // indirect 19 | github.com/go-playground/validator/v10 v10.10.1 // indirect 20 | github.com/golang/protobuf v1.5.2 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/leodido/go-urn v1.2.1 // indirect 23 | github.com/mattn/go-isatty v0.0.14 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.2 // indirect 26 | github.com/tklauser/go-sysconf v0.3.10 // indirect 27 | github.com/tklauser/numcpus v0.4.0 // indirect 28 | github.com/ugorji/go/codec v1.2.7 // indirect 29 | github.com/yusufpapurcu/wmi v1.2.2 // indirect 30 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect 31 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect 32 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect 33 | golang.org/x/text v0.3.7 // indirect 34 | google.golang.org/protobuf v1.27.1 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /neko-status/iperf3.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "neko-status/iperf3" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func Iperf3(c *gin.Context) { 11 | host := c.PostForm("host") 12 | port, _ := strconv.Atoi(c.PostForm("count")) 13 | if port == 0 { 14 | port = 5201 15 | } 16 | reverse := c.PostForm("reverse") != "" 17 | time, _ := strconv.Atoi(c.PostForm("time")) 18 | if time == 0 { 19 | time = 10 20 | } 21 | parallel, _ := strconv.Atoi(c.PostForm("parallel")) 22 | if parallel == 0 { 23 | parallel = 1 24 | } 25 | protocol := c.PostForm("protocol") 26 | if protocol == "" { 27 | protocol = "tcp" 28 | } 29 | res, err := iperf3.Iperf3(host, port, reverse, time, parallel, protocol, nil) 30 | if err == nil { 31 | resp(c, true, res, 200) 32 | } else { 33 | resp(c, false, err, 500) 34 | } 35 | } 36 | 37 | func Iperf3Ws(c *gin.Context) { 38 | host := c.Query("host") 39 | port, _ := strconv.Atoi(c.Query("count")) 40 | if port == 0 { 41 | port = 5201 42 | } 43 | reverse := c.Query("reverse") != "" 44 | time, _ := strconv.Atoi(c.Query("time")) 45 | if time == 0 { 46 | time = 10 47 | } 48 | parallel, _ := strconv.Atoi(c.Query("parallel")) 49 | if parallel == 0 { 50 | parallel = 1 51 | } 52 | protocol := c.Query("protocol") 53 | if protocol == "" { 54 | protocol = "tcp" 55 | } 56 | ws, err := upGrader.Upgrade(c.Writer, c.Request, nil) 57 | if err != nil { 58 | return 59 | } 60 | iperf3.Iperf3(host, port, reverse, time, parallel, protocol, ws) 61 | } 62 | -------------------------------------------------------------------------------- /neko-status/iperf3/iperf3.go: -------------------------------------------------------------------------------- 1 | package iperf3 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | type Stat struct { 15 | Type string 16 | Interval string 17 | Transfer uint64 18 | Bitrate uint64 19 | Retr uint64 20 | } 21 | 22 | type Result struct { 23 | Success bool 24 | Stats []Stat 25 | Total Stat 26 | } 27 | 28 | const iperf3path = "/usr/bin/iperf3" 29 | const timeout = 5000 30 | 31 | func toStat(str string) Stat { 32 | t := strings.Fields(str[5:]) 33 | log.Println(str, t) 34 | Transfer, _ := strconv.ParseFloat(t[2], 10) 35 | var transfer uint64 36 | switch t[3] { 37 | case "TBytes": 38 | transfer = uint64(Transfer) * 1024 * 1024 * 1024 * 1024 39 | case "GBytes": 40 | transfer = uint64(Transfer) * 1024 * 1024 * 1024 41 | case "MBytes": 42 | transfer = uint64(Transfer) * 1024 * 1024 43 | case "KBytes": 44 | transfer = uint64(Transfer) * 1024 45 | } 46 | bitrate, _ := strconv.Atoi(t[4]) 47 | stat := Stat{ 48 | Interval: t[0], 49 | Transfer: transfer, 50 | Bitrate: uint64(bitrate), 51 | } 52 | if len(t) > 7 { 53 | retr, _ := strconv.Atoi(t[6]) 54 | stat.Retr = uint64(retr) 55 | } 56 | // log.Println(str,stat) 57 | return stat 58 | } 59 | 60 | func AnalStdout(stdout io.Reader, multi bool, ws *websocket.Conn) (res Result) { 61 | waitID := true 62 | buf := make([]byte, 2048) 63 | for { 64 | n, err := stdout.Read(buf) 65 | if err != nil { 66 | break 67 | } 68 | str := string(buf[:n]) 69 | for _, L := range strings.Split(str, "\n") { 70 | l := strings.TrimSpace(L) 71 | if l == "" { 72 | continue 73 | } 74 | if waitID { 75 | if strings.HasPrefix(l, "[ ID]") { 76 | waitID = false 77 | } 78 | continue 79 | } 80 | if l[0] != '[' || (multi && l[1] != 'S') { 81 | continue 82 | } 83 | if strings.HasSuffix(l, "Mbits/sec") { 84 | stat := toStat(l) 85 | stat.Type = "interval" 86 | res.Stats = append(res.Stats, stat) 87 | if ws != nil { 88 | ws.WriteJSON(stat) 89 | } 90 | } 91 | if strings.HasSuffix(l, "sender") { 92 | stat := toStat(l) 93 | stat.Type = "total" 94 | res.Total = stat 95 | } 96 | } 97 | } 98 | res.Success = true 99 | if ws != nil { 100 | ws.WriteJSON(res) 101 | ws.Close() 102 | } 103 | // log.Println(res) 104 | return 105 | } 106 | 107 | func Iperf3(host string, port int, reverse bool, ti int, parallel int, protocol string, ws *websocket.Conn) (res Result, err error) { 108 | Args := []string{ 109 | iperf3path, 110 | "-c", host, 111 | "-p", strconv.Itoa(port), 112 | "-P", strconv.Itoa(parallel), 113 | "-t", strconv.Itoa(ti), 114 | "--connect-timeout", strconv.Itoa(timeout), 115 | "--rcv-timeout", strconv.Itoa(timeout), 116 | "--forceflush", 117 | "-f", "mbps", 118 | } 119 | if reverse { 120 | Args = append(Args, "-R") 121 | } 122 | if protocol == "udp" { 123 | Args = append(Args, "-u") 124 | } 125 | cmd := exec.Cmd{ 126 | Path: iperf3path, 127 | Args: Args, 128 | Stderr: os.Stderr, 129 | } 130 | stdout, _ := cmd.StdoutPipe() 131 | if err = cmd.Start(); err != nil { 132 | res.Success = false 133 | return 134 | } 135 | res = AnalStdout(stdout, parallel > 1, ws) 136 | cmd.Wait() 137 | return 138 | } 139 | -------------------------------------------------------------------------------- /neko-status/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "strconv" 9 | 10 | "neko-status/stat" 11 | "neko-status/walled" 12 | 13 | "github.com/gin-gonic/gin" 14 | "gopkg.in/yaml.v2" 15 | ) 16 | 17 | var ( 18 | Config CONF 19 | ) 20 | 21 | func resp(c *gin.Context, success bool, data interface{}, code int) { 22 | c.JSON(code, gin.H{ 23 | "success": success, 24 | "data": data, 25 | }) 26 | } 27 | func main() { 28 | var confpath string 29 | var show_version bool 30 | flag.StringVar(&confpath, "c", "", "config path") 31 | flag.IntVar(&Config.Mode, "mode", 0, "access mode") 32 | flag.StringVar(&Config.Key, "key", "", "access key") 33 | flag.IntVar(&Config.Port, "port", 8080, "port") 34 | flag.BoolVar(&show_version, "v", false, "show version") 35 | flag.Parse() 36 | 37 | if confpath != "" { 38 | data, err := ioutil.ReadFile(confpath) 39 | if err != nil { 40 | log.Panic(err) 41 | } 42 | err = yaml.Unmarshal([]byte(data), &Config) 43 | if err != nil { 44 | panic(err) 45 | } 46 | // fmt.Println(Config) 47 | } 48 | if show_version { 49 | fmt.Println("neko-status v1.0") 50 | return 51 | } 52 | go walled.MonitorWalled() 53 | API() 54 | } 55 | func API() { 56 | gin.SetMode(gin.ReleaseMode) 57 | r := gin.New() 58 | r.Use(checkKey) 59 | r.GET("/stat", Stat) 60 | r.GET("/iperf3", Iperf3) 61 | r.GET("/iperf3ws", Iperf3Ws) 62 | r.GET("/walled", Stat) 63 | fmt.Println("Api port:", Config.Port) 64 | fmt.Println("Api key:", Config.Key) 65 | r.Run(":" + strconv.Itoa(Config.Port)) 66 | } 67 | func checkKey(c *gin.Context) { 68 | if c.Request.Header.Get("key") == Config.Key || c.Query("key") == Config.Key { 69 | c.Next() 70 | } else { 71 | resp(c, false, "Api key Incorrect", 500) 72 | c.Abort() 73 | } 74 | } 75 | 76 | func Stat(c *gin.Context) { 77 | res, err := stat.GetStat() 78 | if err == nil { 79 | resp(c, true, res, 200) 80 | } else { 81 | resp(c, false, err, 500) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /neko-status/mtr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "neko-status/mtr" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | var upGrader = websocket.Upgrader{ 13 | CheckOrigin: func(r *http.Request) bool { 14 | return true 15 | }, 16 | } 17 | 18 | func Mtr(c *gin.Context) { 19 | host := c.PostForm("host") 20 | count, _ := strconv.Atoi(c.PostForm("count")) 21 | if count == 0 { 22 | count = 10 23 | } 24 | res, err := mtr.Mtr(host, count, true, nil) 25 | if err == nil { 26 | resp(c, true, res, 200) 27 | } else { 28 | resp(c, false, err, 500) 29 | } 30 | } 31 | 32 | func MtrWs(c *gin.Context) { 33 | host := c.Query("host") 34 | count, _ := strconv.Atoi(c.Query("count")) 35 | if count == 0 { 36 | count = 10 37 | } 38 | ws, err := upGrader.Upgrade(c.Writer, c.Request, nil) 39 | if err != nil { 40 | return 41 | } 42 | mtr.Mtr(host, count, true, ws) 43 | } 44 | -------------------------------------------------------------------------------- /neko-status/mtr/mtr.go: -------------------------------------------------------------------------------- 1 | package mtr 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/gorilla/websocket" 8 | "github.com/tonobo/mtr/pkg/hop" 9 | "github.com/tonobo/mtr/pkg/icmp" 10 | "github.com/tonobo/mtr/pkg/mtr" 11 | ) 12 | 13 | type packet struct { 14 | Success bool 15 | Respond float64 16 | } 17 | 18 | type Node struct { 19 | Host string 20 | Sent int 21 | TTL int 22 | LossPercent float64 23 | Last float64 24 | Avg float64 25 | Best float64 26 | Worst float64 27 | Packets []packet 28 | } 29 | 30 | type Result struct { 31 | Host string 32 | Statistic []Node 33 | } 34 | 35 | var ( 36 | COUNT = 10 37 | TIMEOUT = 1000 * time.Millisecond 38 | INTERVAL = 100 * time.Millisecond 39 | HOP_SLEEP = time.Nanosecond 40 | MAX_HOPS = 64 41 | MAX_UNKNOWN_HOPS = 10 42 | RING_BUFFER_SIZE = 50 43 | PTR_LOOKUP = false 44 | srcAddr = "" 45 | ) 46 | 47 | func packets(h *hop.HopStatistic) []packet { 48 | v := make([]packet, 0, h.RingBufferSize) 49 | h.Packets.Do(func(f interface{}) { 50 | if f == nil { 51 | return 52 | } 53 | x := f.(icmp.ICMPReturn) 54 | if x.Success { 55 | v = append(v, packet{ 56 | Success: true, 57 | Respond: x.Elapsed.Seconds() * 1000, 58 | }) 59 | } else { 60 | v = append(v, packet{ 61 | Success: false, 62 | Respond: 0.0, 63 | }) 64 | } 65 | }) 66 | return v 67 | } 68 | 69 | func toNode(h *hop.HopStatistic, hide bool) Node { 70 | node := Node{ 71 | Host: h.Target, 72 | Sent: h.Sent, 73 | TTL: h.TTL, 74 | LossPercent: h.Loss(), 75 | Last: h.Last.Elapsed.Seconds() * 1000, 76 | Best: h.Best.Elapsed.Seconds() * 1000, 77 | Worst: h.Worst.Elapsed.Seconds() * 1000, 78 | Avg: h.Avg(), 79 | Packets: packets(h), 80 | } 81 | if hide { 82 | t := strings.Split(node.Host, ".") 83 | t[len(t)-1] = "x" 84 | t[len(t)-2] = "x" 85 | node.Host = strings.Join(t, ".") 86 | } 87 | return node 88 | } 89 | 90 | func toRes(m *mtr.MTR) Result { 91 | res := Result{ 92 | Host: m.Address, 93 | Statistic: make([]Node, 0), 94 | } 95 | for i := 1; i <= len(m.Statistic); i++ { 96 | res.Statistic = append(res.Statistic, toNode(m.Statistic[i], i <= 5)) 97 | } 98 | return res 99 | } 100 | 101 | func Mtr(host string, count int, hide bool, ws *websocket.Conn) (res Result, err error) { 102 | m, ch, er := mtr.NewMTR(host, srcAddr, TIMEOUT, INTERVAL, HOP_SLEEP, MAX_HOPS, MAX_UNKNOWN_HOPS, RING_BUFFER_SIZE, PTR_LOOKUP) 103 | if er != nil { 104 | err = er 105 | return 106 | } 107 | go func(ch chan struct{}) { 108 | m.Run(ch, count) 109 | close(ch) 110 | }(ch) 111 | for range ch { 112 | if ws != nil { 113 | ws.WriteJSON(toRes(m)) 114 | } 115 | } 116 | if ws != nil { 117 | ws.Close() 118 | } 119 | res = toRes(m) 120 | return 121 | } 122 | -------------------------------------------------------------------------------- /neko-status/stat/stat.go: -------------------------------------------------------------------------------- 1 | package stat 2 | 3 | import ( 4 | "neko-status/walled" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/shirou/gopsutil/cpu" 9 | "github.com/shirou/gopsutil/host" 10 | "github.com/shirou/gopsutil/mem" 11 | "github.com/shirou/gopsutil/net" 12 | ) 13 | 14 | func GetStat() (map[string]interface{}, error) { 15 | timer := time.NewTimer(500 * time.Millisecond) 16 | res := gin.H{ 17 | "walled": walled.Walled, 18 | } 19 | CPU1, err := cpu.Times(true) 20 | if err != nil { 21 | return nil, err 22 | } 23 | NET1, err := net.IOCounters(true) 24 | if err != nil { 25 | return nil, err 26 | } 27 | <-timer.C 28 | CPU2, err := cpu.Times(true) 29 | if err != nil { 30 | return nil, err 31 | } 32 | NET2, err := net.IOCounters(true) 33 | if err != nil { 34 | return nil, err 35 | } 36 | MEM, err := mem.VirtualMemory() 37 | if err != nil { 38 | return nil, err 39 | } 40 | SWAP, err := mem.SwapMemory() 41 | if err != nil { 42 | return nil, err 43 | } 44 | res["mem"] = gin.H{ 45 | "virtual": MEM, 46 | "swap": SWAP, 47 | } 48 | 49 | single := make([]float64, len(CPU1)) 50 | var idle, total, multi float64 51 | idle, total = 0, 0 52 | for i, c1 := range CPU1 { 53 | c2 := CPU2[i] 54 | single[i] = 1 - (c2.Idle-c1.Idle)/(c2.Total()-c1.Total()) 55 | idle += c2.Idle - c1.Idle 56 | total += c2.Total() - c1.Total() 57 | } 58 | multi = 1 - idle/total 59 | // info, err := cpu.Info() 60 | // if err != nil { 61 | // return nil, err 62 | // } 63 | res["cpu"] = gin.H{ 64 | // "info": info, 65 | "multi": multi, 66 | "single": single, 67 | } 68 | 69 | var in, out, in_total, out_total uint64 70 | in, out, in_total, out_total = 0, 0, 0, 0 71 | res["net"] = gin.H{ 72 | "devices": gin.H{}, 73 | } 74 | for i, x := range NET2 { 75 | _in := x.BytesRecv - NET1[i].BytesRecv 76 | _out := x.BytesSent - NET1[i].BytesSent 77 | res["net"].(gin.H)["devices"].(gin.H)[x.Name] = gin.H{ 78 | "delta": gin.H{ 79 | "in": float64(_in) / 0.5, 80 | "out": float64(_out) / 0.5, 81 | }, 82 | "total": gin.H{ 83 | "in": x.BytesRecv, 84 | "out": x.BytesSent, 85 | }, 86 | } 87 | if x.Name == "lo" { 88 | continue 89 | } 90 | in += _in 91 | out += _out 92 | in_total += x.BytesRecv 93 | out_total += x.BytesSent 94 | } 95 | res["net"].(gin.H)["delta"] = gin.H{ 96 | "in": float64(in) / 0.5, 97 | "out": float64(out) / 0.5, 98 | } 99 | res["net"].(gin.H)["total"] = gin.H{ 100 | "in": in_total, 101 | "out": out_total, 102 | } 103 | host, err := host.Info() 104 | if err != nil { 105 | return nil, err 106 | } 107 | res["host"] = host 108 | 109 | return res, nil 110 | } 111 | -------------------------------------------------------------------------------- /neko-status/walled/walled.go: -------------------------------------------------------------------------------- 1 | package walled 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | var Walled = false 9 | 10 | func MonitorWalled() { 11 | for { 12 | Walled = TestWalled() 13 | time.Sleep(60 * time.Second) 14 | } 15 | } 16 | 17 | func TestWalled() bool { 18 | for i := 0; i < 3; i++ { 19 | if !walled() { 20 | return false 21 | } 22 | } 23 | return true 24 | } 25 | 26 | func walled() bool { 27 | d := net.Dialer{Timeout: 10 * time.Second} 28 | c, err := d.Dial("tcp", "www.baidu.com:80") 29 | if err == nil { 30 | c.Close() 31 | return false 32 | } else { 33 | return true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /nekonekostatus.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict" 3 | const express=require('express'), 4 | bp=require('body-parser'), 5 | ckp=require("cookie-parser"), 6 | nunjucks=require("nunjucks"), 7 | fs=require("fs"), 8 | schedule=require("node-schedule"); 9 | const core=require("./core"), 10 | db=require("./database")(), 11 | {pr,md5,uuid}=core; 12 | var setting=db.setting.all(); 13 | var svr=express(); 14 | 15 | svr.use(bp.urlencoded({extended: false})); 16 | svr.use(bp.json({limit:'100mb'})); 17 | svr.use(ckp()); 18 | svr.use(express.json()); 19 | svr.use(express.static(__dirname+"/static")); 20 | 21 | svr.engine('html', nunjucks.render); 22 | svr.set('view engine', 'html'); 23 | require('express-ws')(svr); 24 | 25 | var env=nunjucks.configure(__dirname+'/views', { 26 | autoescape: true, 27 | express: svr, 28 | watch:setting.debug, 29 | }); 30 | var admin_tokens=new Set(); 31 | try{for(var token of require("./tokens.json"))admin_tokens.add(token);}catch{} 32 | setInterval(()=>{ 33 | var tokens=[]; 34 | for(var token of admin_tokens.keys())tokens.push(token); 35 | fs.writeFileSync(__dirname+"/tokens.json",JSON.stringify(tokens)); 36 | },1000); 37 | svr.all('*',(req,res,nxt)=>{ 38 | if(admin_tokens.has(req.cookies.token))req.admin=true; 39 | nxt(); 40 | }); 41 | svr.get('/login',(req,res)=>{ 42 | if(req.admin)res.redirect('/'); 43 | else res.render('login',{}); 44 | }); 45 | svr.post('/login',(req,res)=>{ 46 | var {password}=req.body; 47 | if(password==md5(db.setting.get("password"))){ 48 | var token=uuid.v4(); 49 | admin_tokens.add(token); 50 | res.cookie("token",token); 51 | res.json(pr(1,token)); 52 | } 53 | else res.json(pr(0,"密码错误")); 54 | }); 55 | svr.get('/logout',(req,res)=>{ 56 | admin_tokens.delete(req.cookies.token); 57 | res.clearCookie("token"); 58 | res.redirect("/login"); 59 | }); 60 | svr.all('/admin*',(req,res,nxt)=>{ 61 | if(req.admin)nxt(); 62 | else res.redirect('/login'); 63 | }); 64 | svr.get('/admin/db',(req,res)=>{ 65 | var path=__dirname+"/database/backup.db"; 66 | db.DB.backup(path).then(()=>{res.sendFile(path)}); 67 | }); 68 | 69 | var bot=null; 70 | if(setting.bot&&setting.bot.token){ 71 | bot=require("./bot")(setting.bot.token,setting.bot.chatIds); 72 | if(setting.bot.webhook){ 73 | bot.bot.setWebHook(setting.site.url+"/bot"+setting.bot.token).then(()=>{ 74 | bot.bot.setMyCommands(bot.cmds); 75 | }); 76 | svr.all('/bot'+setting.bot.token, (req,res)=>{ 77 | bot.bot.processUpdate(req.body); 78 | res.sendStatus(200); 79 | }); 80 | } 81 | else bot.bot.startPolling(); 82 | } 83 | svr.locals={ 84 | setting, 85 | db, 86 | bot, 87 | ...core, 88 | }; 89 | 90 | fs.readdirSync(__dirname+'/modules',{withFileTypes:1}).forEach(file=>{ 91 | if(!file.isDirectory())return; 92 | try{require(`./modules/${file.name}/index.js`)(svr);}catch(e){console.log(e)} 93 | }); 94 | const port=process.env.PORT||db.setting.get("listen"),host=process.env.HOST||''; 95 | svr.server=svr.listen(port,host,()=>{console.log(`server running @ http://${host ? host : 'localhost'}:${port}`);}) -------------------------------------------------------------------------------- /nekonekostatus.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=nekonekostatus 3 | 4 | [Service] 5 | Type=forking 6 | ExecStart=/root/nekonekostatus/run.sh 7 | ExecReload=/root/nekonekostatus/restart.sh 8 | EexcStop=/root/nekonekostatus/stop.sh 9 | 10 | [Install] 11 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nekonekostatus", 3 | "version": "1.0", 4 | "description": "nekonekostatus", 5 | "scripts": { 6 | "start": "node nekonekostatus.js" 7 | }, 8 | "keywords": [], 9 | "engines": { 10 | "node": ">=12" 11 | }, 12 | "author": "nekoneko", 13 | "license": "MIT", 14 | "dependencies": { 15 | "better-sqlite3": "^7.4.3", 16 | "body-parser": "^1.19.0", 17 | "cookie-parser": "^1.4.5", 18 | "express": "^4.17.1", 19 | "express-ws": "^5.0.2", 20 | "fs": "0.0.1-security", 21 | "js-yaml": "^4.1.0", 22 | "md5": "^2.3.0", 23 | "node-fetch": "^2.6.1", 24 | "node-schedule": "^2.0.0", 25 | "node-ssh": "^12.0.0", 26 | "node-telegram-bot-api": "^0.54.0", 27 | "npm": "^7.20.6", 28 | "nunjucks": "^3.2.3", 29 | "socket.io": "^4.1.3", 30 | "uuid": "^8.3.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## NekoNekoStatus 2 | 3 | 一个Material Design风格的服务器探针 4 | 5 | - 默认访问端口: 5555 6 | - 默认密码: `nekonekostatus` 7 | - 默认被控下载地址: https://github.com/nkeonkeo/nekonekostatus/releases/download/v0.1/neko-status 8 | 9 | 安装后务必修改密码! 10 | 11 | 注意: 正处于快速开发迭代期,可能不保证无缝更新 12 | 13 | Feature: 14 | 15 | - 面板一键安装被控 16 | - 负载监控、带宽监控、流量统计图表 17 | - Telegram 掉线/恢复 通知 18 | - 好看的主题 (卡片/列表、夜间模式) 19 | - WEBSSH、脚本片段 20 | 21 | TODOLIST: 22 | 23 | - 主动通知模式 24 | - 硬盘监控 25 | - WEBSSH的一些小问题 26 | 27 | ## 一键脚本安装 28 | 29 | 在centos7/debian 10下测试成功,其他系统请自行尝试,参照[手动安装](#手动安装) 30 | 31 | wget: 32 | 33 | ```bash 34 | wget https://raw.githubusercontent.com/nkeonkeo/nekonekostatus/main/install.sh -O install.sh && bash install.sh 35 | ``` 36 | 37 | curl: 38 | 39 | ```bash 40 | curl https://raw.githubusercontent.com/nkeonkeo/nekonekostatus/main/install.sh -o install.sh && bash install.sh 41 | ``` 42 | 43 | ## 更新 44 | 45 | 记得备份数据库 (`database/db.db`) 46 | 47 | ```bash 48 | cd /root/nekonekostatus 49 | git pull 50 | systemctl restart nekonekostatus-dashboard 51 | ``` 52 | 53 | ## Docker 54 | 55 | ```bash 56 | docker run --restart=on-failure --name nekonekostatus -p 5555:5555 -d nkeonkeo/nekonekostatus:latest 57 | ``` 58 | 59 | 访问目标ip 5555端口即可,`5555:5555`可改成任意其他端口,如`2333:5555` 60 | 61 | 备份数据库: `/root/nekonekostatus/database/db.db` 62 | 63 | ## 手动安装 64 | 65 | 依赖: `nodejs`, `gcc/g++ version 8.x `, `git` 66 | 67 | centos: 68 | 69 | ```bash 70 | yum install epel-release -y && yum install centos-release-scl git -y && yum install nodejs devtoolset-8-gcc* -y 71 | bash -c "npm install n -g" 72 | source /root/.bashrc 73 | bash -c "n latest" 74 | source /root/.bashrc 75 | bash -c "npm install npm@latest -g" 76 | source /root/.bashrc 77 | ``` 78 | 79 | debian/ubuntu: 80 | 81 | ```bash 82 | apt update -y && apt-get install nodejs npm git build-essential -y 83 | bash -c "npm install n -g" 84 | source /root/.bashrc 85 | bash -c "n latest" 86 | source /root/.bashrc 87 | bash -c "npm install npm@latest -g" 88 | source /root/.bashrc 89 | ``` 90 | 91 | --- 92 | 93 | 克隆仓库并安装所需第三方包 94 | 95 | ```bash 96 | git clone https://github.com/nkeonkeo/nekonekostatus.git 97 | cd nekonekostatus 98 | source /opt/rh/devtoolset-8/enable 99 | npm install 100 | ``` 101 | 102 | ## 配置 & 运行 103 | 104 | `node nekonekostatus.js` 即可运行 105 | 106 | 后台常驻: 107 | 108 | 1. 安装`forever`(`npm install forever -g`),然后: `forever start nekonekostatus.js` 109 | 110 | 2. 使用systemd 111 | 112 | ```bash 113 | echo "[Unit] 114 | Description=nekonekostatus 115 | After=network.target 116 | 117 | [Service] 118 | Type=simple 119 | Restart=always 120 | RestartSec=5 121 | ExecStart=/root/nekonekostatus/nekonekostatus.js 122 | 123 | [Install] 124 | WantedBy=multi-user.target" > /etc/systemd/system/nekonekostatus-dashboard.service 125 | systemctl daemon-reload 126 | systemctl enable nekonekostatus-dashboard.service 127 | systemctl start nekonekostatus-dashboard.service 128 | ``` 129 | 130 | https请使用nginx等反代 131 | 132 | ## 新增/配置 服务器 133 | 134 | |变量名|含义|示例| 135 | |-|-|-| 136 | |`sid`|服务器id|`b82cbe8b-1769-4dc2-b909-5d746df392fb`| 137 | |`name`|服务器名称|`localhost`| 138 | |`TOP`|置顶优先级|`1`| 139 | |域名/IP|域名/IP|`127.0.0.1`| 140 | |端口(可选)|ssh端口|`22`| 141 | |密码(可选)|ssh密码|`114514`| 142 | |私钥(可选)|ssh私钥|``| 143 | |被动/主动 同步|同步数据模式|被动(关闭)即可| 144 | |被动通讯端口|被动通讯端口|`10086`| 145 | 146 | 填写ssh保存后即可一键安装/更新后端 (更新后要重新点一下安装) 147 | 148 | ## 手动安装被控 149 | 150 | ```bash 151 | wget --version||yum install wget -y||apt-get install wget -y 152 | /usr/bin/neko-status -v||(wget 被控下载地址 -O /usr/bin/neko-status && chmod +x /usr/bin/neko-status) 153 | systemctl stop nekonekostatus 154 | mkdir /etc/neko-status/ 155 | echo "key: 通讯秘钥 156 | port: 通讯端口 157 | debug: false" > /etc/neko-status/config.yaml 158 | systemctl stop nekonekostatus 159 | echo "[Unit] 160 | Description=nekonekostatus 161 | 162 | [Service] 163 | Restart=always 164 | RestartSec=5 165 | ExecStart=/usr/bin/neko-status -c /etc/neko-status/config.yaml 166 | 167 | [Install] 168 | WantedBy=multi-user.target" > /etc/systemd/system/nekonekostatus.service 169 | systemctl daemon-reload 170 | systemctl start nekonekostatus 171 | systemctl enable nekonekostatus 172 | ``` -------------------------------------------------------------------------------- /restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd /root/nekonekostatus/ 3 | forever restart nekonekostatus.js -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd /root/nekonekostatus/ 3 | forever start -o log/out.log -e log/err.log nekonekostatus.js 4 | -------------------------------------------------------------------------------- /ssh.js: -------------------------------------------------------------------------------- 1 | const {NodeSSH}=require('node-ssh'); 2 | const SSHClient = require("ssh2").Client; 3 | async function ssh_con(key){ 4 | if(!key.privateKey||key.privateKey=='')delete key.privateKey; 5 | key.readyTimeout=10000; 6 | try{ 7 | var ssh=new NodeSSH(); 8 | await ssh.connect(key); 9 | ssh.connection.on("error",(err)=>{}); 10 | } catch(e){return null;} 11 | return ssh; 12 | } 13 | async function ssh_exec(ssh,sh){ 14 | try{ 15 | var res=await ssh.execCommand(sh,{onStdout:null,}); 16 | return {success:true,data:res.stdout};; 17 | } 18 | catch(e){ 19 | // console.log(e); 20 | return {success:false,data:e}; 21 | } 22 | } 23 | async function spwan(key,sh,onData=(chunk)=>{process.stdout.write(chunk)}){ 24 | if(key.privateKey=='')delete key.privateKey; 25 | key.readyTimeout=10000; 26 | try{ 27 | var ssh=new NodeSSH(); 28 | await ssh.connect(key); 29 | ssh.connection.on("error",(err)=>{}); 30 | var res=await ssh.execCommand(sh,{ 31 | onStdout:onData, 32 | }); 33 | await ssh.dispose(); 34 | return {success:true,data:res.stdout}; 35 | }catch(e){return {success:false,data:e};} 36 | } 37 | async function exec(key,sh){ 38 | if(key.privateKey=='')delete key.privateKey; 39 | key.readyTimeout=60000; 40 | try{ 41 | var ssh=new NodeSSH(); 42 | await ssh.connect(key); 43 | ssh.connection.on("error",(err)=>{}); 44 | var res=await ssh.execCommand(sh,{}); 45 | await ssh.dispose(); 46 | return {success:true,data:res.stdout}; 47 | }catch(e){return {success:false,data:e};} 48 | } 49 | async function createSocket(key,ws,conf={}){ 50 | const ssh = new SSHClient(); 51 | ssh.on("ready",()=>{ 52 | if(ws)ws.send("\r\n*** SSH CONNECTION ESTABLISHED ***\r\n".toString('utf-8')); 53 | else return; 54 | ssh.shell((err, stream)=>{ 55 | if(err){ 56 | try{ws.send("\n*** SSH SHELL ERROR: " + err.message + " ***\n".toString('utf-8'));}catch{} 57 | return; 58 | } 59 | if(conf.cols||conf.rows)stream.setWindow(conf.rows,conf.cols); 60 | if(conf.sh)stream.write(conf.sh); 61 | ws.on("message", (data)=>{stream.write(data);}); 62 | ws.on("resize",(data)=>{stream.setWindow(data.rows,data.cols)}) 63 | ws.on("close",()=>{ssh.end()}); 64 | stream.on("data", (data)=>{try{ws.send(data.toString('utf-8'));}catch{}}) 65 | .on("close",()=>{ssh.end()}); 66 | }); 67 | }).on("close",()=>{ 68 | try{ws.close()}catch{}}) 69 | .on("error",(err)=>{ 70 | try{ 71 | ws.send("\r\n*** SSH CONNECTION ERROR: " + err.message + " ***\r\n"); 72 | ws.close(); 73 | } catch {} 74 | }).connect(key); 75 | } 76 | function toJSON(x){return JSON.stringify(x);} 77 | var sshCons={},sshConTime={}; 78 | async function Exec(key,cmd,verbose=0){ 79 | if(!key.privateKey)delete key.privateKey; 80 | if(!key.password)delete key.password; 81 | var k=toJSON(key); 82 | var con=sshCons[k]; 83 | if(con&&con.isConnected()){ 84 | if((new Date())-sshConTime[k]>120000){ 85 | await con.dispose(); 86 | con=await ssh_con(key); 87 | sshConTime[k]=new Date(); 88 | } 89 | } 90 | else{ 91 | con=await ssh_con(key); 92 | sshConTime[k]=new Date(); 93 | } 94 | sshCons[k]=con; 95 | var res=await ssh_exec(con,cmd); 96 | if(verbose)console.log(key.host,cmd,res); 97 | return res; 98 | } 99 | async function pidS(key,keyword){ 100 | var x=await Exec(key,`ps -aux|grep ${keyword}|awk '{print $2}'`); 101 | if(!x.success)return false; 102 | var pids=x.data.trim().split('\n'),pS=new Set(); 103 | for(var pid of pids)pS.add(pid); 104 | return pS; 105 | } 106 | async function netStat(key,keyword){ 107 | var x=await Exec(key,`netstat -lp | grep ${keyword}`); 108 | if(!x.success)return {}; 109 | var lines=x.data.trim().split('\n'),res={}; 110 | try{ 111 | for(var line of lines)if(line){ 112 | var rows=line.trim().split(/\s+/); 113 | var port=rows[3].split(':').pop(),pid=rows.pop().split('/')[0]; 114 | if(Number(port))res[Number(port)]=pid; 115 | } 116 | } 117 | catch{} 118 | return res; 119 | } 120 | module.exports={ 121 | exec,spwan, 122 | ssh_con,ssh_exec, 123 | Exec, 124 | createSocket, 125 | netStat,pidS, 126 | } 127 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | .mdui-appbar{box-shadow:0 -1px 10px 0 rgb(32 33 36 / 28%)} 2 | .mdui-card,.mdui-btn-raised,.mdui-panel-item{box-shadow:0 1px 2px 0 rgba(60,64,67,.3), 0 1px 3px 1px rgba(60,64,67,.15);} 3 | .mdui-drawer{box-shadow:-1px 10px 10px 0 rgb(60 64 67 / 30%), 0 1px 3px 1px rgb(60 64 67 / 15%);} 4 | footer{box-shadow:5px 0px 5px 0 rgb(60 64 67 / 30%), 0 1px 3px 1px rgb(60 64 67 / 15%);} 5 | st{font-weight:600;color:#475bca;} 6 | at{font-weight:600;color:#FF4081;} 7 | gt{font-weight:600;color:#00C853;} 8 | yt{font-weight:600;color:#ffbb00;} 9 | 10 | .mdui-theme-layout-dark st{color:#9aa9ff;} 11 | .mdui-theme-layout-dark at{color:#f7a4b9;} 12 | .mt{margin-top:15px;} 13 | .mt-s{margin-top:3px;} 14 | .mdui-card ul.mdui-list{padding-left: 0;} 15 | .text{font-size:20px;} 16 | .text_s{font-size:17px;} 17 | .mdui-switch{height:20px;line-height:20px;} 18 | .stop{background:#b6b6b6;}.stop td{color:#fff;font-weight:bold;} 19 | .ccp:hover,.poh:hover{cursor: pointer;} 20 | 21 | .mdui-drawer, footer{ 22 | /* background-color: #fdfdfdda; */ 23 | backdrop-filter: blur(15px) brightness(110%); 24 | } 25 | .mdui-card{ 26 | border-radius: 6px; 27 | backdrop-filter:blur(7px); 28 | /* background-color: #ffffff8a; */ 29 | } 30 | .btn{ 31 | border-radius: 5px; 32 | font-weight: bold;font-size: 15px; 33 | } 34 | 35 | @media (max-width: 1023.9px){ 36 | .mdui-drawer{background-color: #fff;} 37 | .mdui-overlay{backdrop-filter:blur(7px);} 38 | } 39 | html{ 40 | /* background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2000 1500'%3E%3Cdefs%3E%3Crect stroke='%23ffffff' stroke-width='0.49' width='1' height='1' id='s'/%3E%3Cpattern id='a' width='3' height='3' patternUnits='userSpaceOnUse' patternTransform='rotate(82 1000 750) scale(13.25) translate(-924.53 -693.4)'%3E%3Cuse fill='%23fafafa' href='%23s' y='2'/%3E%3Cuse fill='%23fafafa' href='%23s' x='1' y='2'/%3E%3Cuse fill='%23f5f5f5' href='%23s' x='2' y='2'/%3E%3Cuse fill='%23f5f5f5' href='%23s'/%3E%3Cuse fill='%23f0f0f0' href='%23s' x='2'/%3E%3Cuse fill='%23f0f0f0' href='%23s' x='1' y='1'/%3E%3C/pattern%3E%3Cpattern id='b' width='7' height='11' patternUnits='userSpaceOnUse' patternTransform='rotate(82 1000 750) scale(13.25) translate(-924.53 -693.4)'%3E%3Cg fill='%23ebebeb'%3E%3Cuse href='%23s'/%3E%3Cuse href='%23s' y='5' /%3E%3Cuse href='%23s' x='1' y='10'/%3E%3Cuse href='%23s' x='2' y='1'/%3E%3Cuse href='%23s' x='2' y='4'/%3E%3Cuse href='%23s' x='3' y='8'/%3E%3Cuse href='%23s' x='4' y='3'/%3E%3Cuse href='%23s' x='4' y='7'/%3E%3Cuse href='%23s' x='5' y='2'/%3E%3Cuse href='%23s' x='5' y='6'/%3E%3Cuse href='%23s' x='6' y='9'/%3E%3C/g%3E%3C/pattern%3E%3Cpattern id='h' width='5' height='13' patternUnits='userSpaceOnUse' patternTransform='rotate(82 1000 750) scale(13.25) translate(-924.53 -693.4)'%3E%3Cg fill='%23ebebeb'%3E%3Cuse href='%23s' y='5'/%3E%3Cuse href='%23s' y='8'/%3E%3Cuse href='%23s' x='1' y='1'/%3E%3Cuse href='%23s' x='1' y='9'/%3E%3Cuse href='%23s' x='1' y='12'/%3E%3Cuse href='%23s' x='2'/%3E%3Cuse href='%23s' x='2' y='4'/%3E%3Cuse href='%23s' x='3' y='2'/%3E%3Cuse href='%23s' x='3' y='6'/%3E%3Cuse href='%23s' x='3' y='11'/%3E%3Cuse href='%23s' x='4' y='3'/%3E%3Cuse href='%23s' x='4' y='7'/%3E%3Cuse href='%23s' x='4' y='10'/%3E%3C/g%3E%3C/pattern%3E%3Cpattern id='c' width='17' height='13' patternUnits='userSpaceOnUse' patternTransform='rotate(82 1000 750) scale(13.25) translate(-924.53 -693.4)'%3E%3Cg fill='%23e5e5e5'%3E%3Cuse href='%23s' y='11'/%3E%3Cuse href='%23s' x='2' y='9'/%3E%3Cuse href='%23s' x='5' y='12'/%3E%3Cuse href='%23s' x='9' y='4'/%3E%3Cuse href='%23s' x='12' y='1'/%3E%3Cuse href='%23s' x='16' y='6'/%3E%3C/g%3E%3C/pattern%3E%3Cpattern id='d' width='19' height='17' patternUnits='userSpaceOnUse' patternTransform='rotate(82 1000 750) scale(13.25) translate(-924.53 -693.4)'%3E%3Cg fill='%23ffffff'%3E%3Cuse href='%23s' y='9'/%3E%3Cuse href='%23s' x='16' y='5'/%3E%3Cuse href='%23s' x='14' y='2'/%3E%3Cuse href='%23s' x='11' y='11'/%3E%3Cuse href='%23s' x='6' y='14'/%3E%3C/g%3E%3Cg fill='%23e0e0e0'%3E%3Cuse href='%23s' x='3' y='13'/%3E%3Cuse href='%23s' x='9' y='7'/%3E%3Cuse href='%23s' x='13' y='10'/%3E%3Cuse href='%23s' x='15' y='4'/%3E%3Cuse href='%23s' x='18' y='1'/%3E%3C/g%3E%3C/pattern%3E%3Cpattern id='e' width='47' height='53' patternUnits='userSpaceOnUse' patternTransform='rotate(82 1000 750) scale(13.25) translate(-924.53 -693.4)'%3E%3Cg fill='%23413fb5'%3E%3Cuse href='%23s' x='2' y='5'/%3E%3Cuse href='%23s' x='16' y='38'/%3E%3Cuse href='%23s' x='46' y='42'/%3E%3Cuse href='%23s' x='29' y='20'/%3E%3C/g%3E%3C/pattern%3E%3Cpattern id='f' width='59' height='71' patternUnits='userSpaceOnUse' patternTransform='rotate(82 1000 750) scale(13.25) translate(-924.53 -693.4)'%3E%3Cg fill='%23413fb5'%3E%3Cuse href='%23s' x='33' y='13'/%3E%3Cuse href='%23s' x='27' y='54'/%3E%3Cuse href='%23s' x='55' y='55'/%3E%3C/g%3E%3C/pattern%3E%3Cpattern id='g' width='139' height='97' patternUnits='userSpaceOnUse' patternTransform='rotate(82 1000 750) scale(13.25) translate(-924.53 -693.4)'%3E%3Cg fill='%23413fb5'%3E%3Cuse href='%23s' x='11' y='8'/%3E%3Cuse href='%23s' x='51' y='13'/%3E%3Cuse href='%23s' x='17' y='73'/%3E%3Cuse href='%23s' x='99' y='57'/%3E%3C/g%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23a)' width='100%25' height='100%25'/%3E%3Crect fill='url(%23b)' width='100%25' height='100%25'/%3E%3Crect fill='url(%23h)' width='100%25' height='100%25'/%3E%3Crect fill='url(%23c)' width='100%25' height='100%25'/%3E%3Crect fill='url(%23d)' width='100%25' height='100%25'/%3E%3Crect fill='url(%23e)' width='100%25' height='100%25'/%3E%3Crect fill='url(%23f)' width='100%25' height='100%25'/%3E%3Crect fill='url(%23g)' width='100%25' height='100%25'/%3E%3C/svg%3E"); */ 41 | /* background-image: url("https://ftp.bmp.ovh/imgs/2021/06/dec02b1482778512.jpg"); */ 42 | /* background-image: url("https://api.ixiaowai.cn/api/api.php"); */ 43 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1600 900'%3E%3Cpolygon fill='%23f6c5d3' points='957 450 539 900 1396 900'/%3E%3Cpolygon fill='%237b8aca' points='957 450 872.9 900 1396 900'/%3E%3Cpolygon fill='%23ecc7e1' points='-60 900 398 662 816 900'/%3E%3Cpolygon fill='%237491d0' points='337 900 398 662 816 900'/%3E%3Cpolygon fill='%23d9c6eb' points='1203 546 1552 900 876 900'/%3E%3Cpolygon fill='%236792cc' points='1203 546 1552 900 1162 900'/%3E%3Cpolygon fill='%23c4caf3' points='641 695 886 900 367 900'/%3E%3Cpolygon fill='%236caae6' points='587 900 641 695 886 900'/%3E%3Cpolygon fill='%23afd3f5' points='1710 900 1401 632 1096 900'/%3E%3Cpolygon fill='%2374bef4' points='1710 900 1401 632 1365 900'/%3E%3Cpolygon fill='%239ddcfa' points='1210 900 971 687 725 900'/%3E%3Cpolygon fill='%2369c3f8' points='943 900 1210 900 971 687'/%3E%3C/svg%3E"); 44 | background-attachment: fixed; 45 | background-size: cover; 46 | } 47 | body{ 48 | background-color: rgba(255, 255, 255, 0.6);; 49 | } -------------------------------------------------------------------------------- /static/css/style.min.css: -------------------------------------------------------------------------------- 1 | .mdui-appbar{box-shadow:0 -1px 10px 0 rgb(32 33 36 / 28%)}.mdui-btn-raised,.mdui-card,.mdui-panel-item{box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}.mdui-drawer{box-shadow:-1px 10px 10px 0 rgb(60 64 67 / 30%),0 1px 3px 1px rgb(60 64 67 / 15%)}footer{box-shadow:5px 0 5px 0 rgb(60 64 67 / 30%),0 1px 3px 1px rgb(60 64 67 / 15%)}st{font-weight:600;color:#475bca}at{font-weight:600;color:#ff4081}gt{font-weight:600;color:#00c853}yt{font-weight:600;color:#fb0}.mdui-theme-layout-dark st{color:#9aa9ff}.mdui-theme-layout-dark at{color:#f7a4b9}.mt{margin-top:15px}.mt-s{margin-top:3px}.mdui-card ul.mdui-list{padding-left:0}.text{font-size:20px}.text_s{font-size:17px}.mdui-switch{height:20px;line-height:20px}.stop{background:#b6b6b6}.stop td{color:#fff;font-weight:700}.ccp:hover,.poh:hover{cursor:pointer}.mdui-drawer,footer{backdrop-filter:blur(15px) brightness(110%)}.mdui-card{border-radius:6px;backdrop-filter:blur(7px)}.btn{border-radius:5px;font-weight:700;font-size:15px}@media (max-width:1023.9px){.mdui-drawer{background-color:#fff}.mdui-overlay{backdrop-filter:blur(7px)}}html{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1600 900'%3E%3Cpolygon fill='%23f6c5d3' points='957 450 539 900 1396 900'/%3E%3Cpolygon fill='%237b8aca' points='957 450 872.9 900 1396 900'/%3E%3Cpolygon fill='%23ecc7e1' points='-60 900 398 662 816 900'/%3E%3Cpolygon fill='%237491d0' points='337 900 398 662 816 900'/%3E%3Cpolygon fill='%23d9c6eb' points='1203 546 1552 900 876 900'/%3E%3Cpolygon fill='%236792cc' points='1203 546 1552 900 1162 900'/%3E%3Cpolygon fill='%23c4caf3' points='641 695 886 900 367 900'/%3E%3Cpolygon fill='%236caae6' points='587 900 641 695 886 900'/%3E%3Cpolygon fill='%23afd3f5' points='1710 900 1401 632 1096 900'/%3E%3Cpolygon fill='%2374bef4' points='1710 900 1401 632 1365 900'/%3E%3Cpolygon fill='%239ddcfa' points='1210 900 971 687 725 900'/%3E%3Cpolygon fill='%2369c3f8' points='943 900 1210 900 971 687'/%3E%3C/svg%3E");background-attachment:fixed;background-size:cover}body{background-color:rgba(255,255,255,.6)} -------------------------------------------------------------------------------- /static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/js/core.js: -------------------------------------------------------------------------------- 1 | function copy(text){ 2 | var x=document.createElement("textarea"); 3 | x.textContent=text;document.body.appendChild(x); 4 | x.select();document.execCommand('copy'); 5 | x.remove(); 6 | mdui.snackbar({message: "复制成功",position: "top"}); 7 | } 8 | function E(id){return document.getElementById(id);} 9 | function V(id){return E(id).value;} 10 | 11 | var Loading=E("loading"); 12 | function startloading(){Loading.hidden=0;} 13 | function endloading(){Loading.hidden=1;} 14 | async function postjson(url,data){ 15 | var res=await fetch(url,{ 16 | method: "POST", 17 | body:JSON.stringify(data), 18 | headers: {'content-type': 'application/json'}, 19 | }).then(res=>res.json()); 20 | return res; 21 | } 22 | function notice(message,timeout=2000,position="top"){ 23 | mdui.snackbar({message,timeout,position}); 24 | } 25 | function open(url){ 26 | var x=document.createElement('a'); 27 | x.href=url; 28 | x.click();x.remove(); 29 | } 30 | function sleep(ti){return new Promise((resolve)=>setTimeout(resolve,ti));} 31 | function refreshPage(ti=600){sleep(ti).then(()=>{window.location.reload()});} 32 | function redirect(url,ti=600){sleep(ti).then(()=>{window.location=url});} 33 | 34 | function setQuery(key,val){ 35 | var x=new URLSearchParams(window.location.search); 36 | x.set(key,val); 37 | window.location.search=x.toString(); 38 | } 39 | function delQuery(key){ 40 | var x=new URLSearchParams(window.location.search); 41 | x.delete(key); 42 | window.location.search=x.toString(); 43 | } 44 | window.onload=()=>{ 45 | document.querySelectorAll("[href]").forEach(x=>{ 46 | if(x.tagName!='A'&&x.tagName!='LINK') 47 | x.onclick=()=>{open(x.getAttribute("href"));}; 48 | }); 49 | document.querySelectorAll(".ccp").forEach(x=>{ 50 | x.onclick=(x)=>{copy(x.target.innerText);}; 51 | x.setAttribute("mdui-tooltip","{content:'点击复制'}"); 52 | }); 53 | }; -------------------------------------------------------------------------------- /static/js/load.js: -------------------------------------------------------------------------------- 1 | Date.prototype.Format=function(fmt){var o={'M+':this.getMonth()+1,'d+':this.getDate(),'H+':this.getHours(),'m+':this.getMinutes(),'s+':this.getSeconds(),'S+':this.getMilliseconds()};if(/(y+)/.test(fmt))fmt=fmt.replace(RegExp.$1,(this.getFullYear()+'').substr(4-RegExp.$1.length));for(var k in o)if(new RegExp('('+k+')').test(fmt))fmt=fmt.replace(RegExp.$1,(RegExp.$1.length==1)?(o[k]):(('00'+o[k]).substr(String(o[k]).length)));return fmt;}; 2 | var load_m=JSON.parse(document.getElementById('load_m_data').value); 3 | var cpus=[],mems=[],swaps=[],ibws=[],obws=[]; 4 | for(var {cpu,mem,swap,ibw,obw} of load_m){ 5 | cpus.push(cpu); 6 | mems.push(mem); 7 | swaps.push(swap); 8 | ibws.push(ibw/128/1024); 9 | obws.push(obw/128/1024); 10 | } 11 | var labels=[]; 12 | for(var i=0,time=new Date();i<60;time.setMinutes(time.getMinutes()-1),++i) 13 | labels.push(time.Format('HH:mm')); 14 | labels.reverse(); 15 | new Chart(document.getElementById('load-m').getContext('2d'),{ 16 | type: 'line', 17 | data: { 18 | labels: labels, 19 | datasets: [{ 20 | label: 'CPU (%)', 21 | backgroundColor: '#66ccff4d', 22 | borderColor: '#0099ffbf', 23 | data: cpus 24 | },{ 25 | label: 'Memory (%)', 26 | backgroundColor: '#f7a4b94d', 27 | borderColor: '#ff789abf', 28 | data: mems 29 | }, 30 | { 31 | label: 'Swap (%)', 32 | backgroundColor: '#767ffd4d', 33 | borderColor: '#6670ffbf', 34 | data: swaps 35 | }] 36 | }, 37 | options: { 38 | scales:{ 39 | y:{min:0,max:100} 40 | } 41 | } 42 | }); 43 | new Chart(document.getElementById('load-m-bw').getContext('2d'),{ 44 | type: 'line', 45 | data: { 46 | labels, 47 | datasets: [{ 48 | label: 'in (Mbps)', 49 | backgroundColor: '#f7a4b94d', 50 | borderColor: '#ff789abf', 51 | data: ibws 52 | },{ 53 | label: 'ou (Mbps)', 54 | backgroundColor: '#66ccff4d', 55 | borderColor: '#0099ffbf', 56 | data: obws 57 | },] 58 | }, 59 | options: { 60 | scales:{ 61 | y:{min:0} 62 | } 63 | } 64 | }); 65 | 66 | var load_h=JSON.parse(document.getElementById('load_h_data').value); 67 | cpus=[],mems=[],swaps=[],ibws=[],obws=[]; 68 | for(var {cpu,mem,swap,ibw,obw} of load_h){ 69 | cpus.push(cpu); 70 | mems.push(mem); 71 | swaps.push(swap); 72 | ibws.push(ibw/128/1024); 73 | obws.push(obw/128/1024); 74 | } 75 | labels=[]; 76 | for(var i=0,time=new Date();i<24;time.setHours(time.getHours()-1),++i) 77 | labels.push(time.Format('HH:00')); 78 | labels.reverse(); 79 | new Chart(document.getElementById('load-h').getContext('2d'),{ 80 | type: 'line', 81 | data: { 82 | labels, 83 | datasets: [{ 84 | label: 'CPU (%)', 85 | backgroundColor: '#66ccff4d', 86 | borderColor: '#0099ffbf', 87 | data: cpus 88 | },{ 89 | label: 'Memory (%)', 90 | backgroundColor: '#f7a4b94d', 91 | borderColor: '#ff789abf', 92 | data: mems 93 | }, 94 | { 95 | label: 'Swap (%)', 96 | backgroundColor: '#767ffd4d', 97 | borderColor: '#6670ffbf', 98 | data: swaps 99 | }] 100 | }, 101 | options: { 102 | scales:{ 103 | y:{min:0,max:100} 104 | } 105 | } 106 | }); 107 | new Chart(document.getElementById('load-h-bw').getContext('2d'),{ 108 | type: 'line', 109 | data: { 110 | labels, 111 | datasets: [{ 112 | label: 'in (Mbps)', 113 | backgroundColor: '#f7a4b94d', 114 | borderColor: '#ff789abf', 115 | data: ibws 116 | },{ 117 | label: 'ou (Mbps)', 118 | backgroundColor: '#66ccff4d', 119 | borderColor: '#0099ffbf', 120 | data: obws 121 | },] 122 | }, 123 | options: { 124 | scales:{ 125 | y:{min:0} 126 | } 127 | } 128 | }); -------------------------------------------------------------------------------- /static/js/login.js: -------------------------------------------------------------------------------- 1 | async function login(){ 2 | startloading(); 3 | var res=await postjson("/login",{ 4 | password: md5(V('password')), 5 | }); 6 | endloading(); 7 | if(res.status)redirect('/'); 8 | else notice(res.data); 9 | } 10 | E('login').onclick=login; 11 | document.onkeyup=function(e){ 12 | var event=e||window.event; 13 | var key=event.which||event.keyCode||event.charCode; 14 | if(key == 13)login(); 15 | }; -------------------------------------------------------------------------------- /static/js/md5.min.js: -------------------------------------------------------------------------------- 1 | !function(n){"use strict";function d(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function f(n,t,r,e,o,u){return d((c=d(d(t,n),d(e,u)))<<(f=o)|c>>>32-f,r);var c,f}function l(n,t,r,e,o,u,c){return f(t&r|~t&e,n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f(t&e|r&~e,n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function i(n,t){var r,e,o,u;n[t>>5]|=128<>>9<<4)]=t;for(var c=1732584193,f=-271733879,i=-1732584194,a=271733878,h=0;h>5]>>>e%32&255);return t}function h(n){var t=[];for(t[(n.length>>2)-1]=void 0,e=0;e>5]|=(255&n.charCodeAt(e/8))<>>4&15)+r.charAt(15&t);return e}function r(n){return unescape(encodeURIComponent(n))}function o(n){return a(i(h(t=r(n)),8*t.length));var t}function u(n,t){return function(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,16res.json()); 21 | if(!node||node.stat==-1)return; 22 | var {cpu,mem,net,host}=node.stat; 23 | E(`CPU`).innerText=(cpu.multi*100).toFixed(2)+'%'; 24 | // E(`CPU_progress`).style.width=`${cpu.multi*100}%`; 25 | var i=0; 26 | for(var usage of cpu.single){ 27 | E(`CPU${++i}_progress`).style.width=`${usage*100}%`; 28 | } 29 | 30 | var {used,total}=mem.virtual,usage=used/total,content; 31 | E(`MEM`).innerText=(usage*100).toFixed(2)+'%'; 32 | E(`MEM_progress`).style.width=`${usage*100}%`; 33 | content=`virtual: ${strB(used)}/${strB(total)}`; 34 | var {used,total}=mem.swap,usage=used/total; 35 | E(`SWAP_progress`).style.width=`${usage*100}%`; 36 | content+=`\nswap: ${strB(used)}/${strB(total)}`; 37 | mem_tooltip.$element[0].innerText=content; 38 | E(`NET_IN`).innerText=strbps(net.delta.in); 39 | E(`NET_OUT`).innerText=strbps(net.delta.out); 40 | E(`NET_IN_TOTAL`).innerText=strB(net.total.in); 41 | E(`NET_OUT_TOTAL`).innerText=strB(net.total.out); 42 | 43 | for(var [device,Net] of Object.entries(net.devices)){ 44 | E(`net_${device}_delta_in`).innerText=strbps(Net.delta.in); 45 | E(`net_${device}_delta_out`).innerText=strbps(Net.delta.out); 46 | E(`net_${device}_total_in`).innerText=strB(Net.total.in); 47 | E(`net_${device}_total_out`).innerText=strB(Net.total.out); 48 | } 49 | 50 | var content= 51 | `系统: ${host.os} 52 | 平台: ${host.platform} 53 | 内核版本: ${host.kernelVersion} 54 | 内核架构: ${host.kernelArch} 55 | 启动: ${new Date(host.bootTime*1000).toLocaleString()} 56 | 在线: ${(host.uptime/86400).toFixed(2)}天`; 57 | host_tooltip.$element[0].innerText=content; 58 | } 59 | get() 60 | setInterval(get,1000); 61 | -------------------------------------------------------------------------------- /static/js/stats.js: -------------------------------------------------------------------------------- 1 | var KB=1024,MB=KB*1024,GB=MB*1024,TB=GB*1024; 2 | function strB(b){ 3 | if(b{ 19 | var stats=await fetch("/stats/data").then(res=>res.json()); 20 | for(var [sid,node] of Object.entries(stats))if(node.stat&&node.stat!=-1){ 21 | var {cpu,mem,net,host}=node.stat; 22 | E(`${sid}_CPU`).innerText=(cpu.multi*100).toFixed(2)+'%'; 23 | E(`${sid}_CPU_progress`).style.width=`${cpu.multi*100}%`; 24 | 25 | var {used,total}=mem.virtual,usage=used/total; 26 | E(`${sid}_MEM`).innerText=(usage*100).toFixed(2)+'%'; 27 | E(`${sid}_MEM_progress`).style.width=`${usage*100}%`; 28 | var content=`${strB(used)}/${strB(total)}`; 29 | if(mem_tooltips[sid])mem_tooltips[sid].$element[0].innerText=content; 30 | else mem_tooltips[sid]=new mdui.Tooltip(`#${sid}_MEM_item`,{content}); 31 | 32 | E(`${sid}_NET_IN`).innerText=strbps(net.delta.in); 33 | E(`${sid}_NET_OUT`).innerText=strbps(net.delta.out); 34 | E(`${sid}_NET_IN_TOTAL`).innerText=strB(net.total.in); 35 | E(`${sid}_NET_OUT_TOTAL`).innerText=strB(net.total.out); 36 | 37 | var content= 38 | `系统: ${host.os} 39 | 平台: ${host.platform} 40 | 内核版本: ${host.kernelVersion} 41 | 内核架构: ${host.kernelArch} 42 | 启动: ${new Date(host.bootTime*1000).toLocaleString()} 43 | 在线: ${(host.uptime/86400).toFixed(2)}天`; 44 | if(!host_tooltips[sid])host_tooltips[sid]=new mdui.Tooltip(`#${sid}_host`,{}); 45 | host_tooltips[sid].$element[0].innerText=content; 46 | } 47 | mdui.mutation(); 48 | },1000); 49 | -------------------------------------------------------------------------------- /static/js/traffic.js: -------------------------------------------------------------------------------- 1 | var G=1000000000; 2 | var traffic=JSON.parse(document.getElementById('traffic_data').value),hs_tot=0,ds_tot=0,ms_tot=0; 3 | var idata=[],odata=[]; 4 | for(var [i,o] of traffic.hs){ 5 | hs_tot+=i+o; 6 | idata.push((i/G).toFixed(3)); 7 | odata.push((o/G).toFixed(3)); 8 | } 9 | Date.prototype.Format=function(fmt){var o={'M+':this.getMonth()+1,'d+':this.getDate(),'H+':this.getHours(),'m+':this.getMinutes(),'s+':this.getSeconds(),'S+':this.getMilliseconds()};if(/(y+)/.test(fmt))fmt=fmt.replace(RegExp.$1,(this.getFullYear()+'').substr(4-RegExp.$1.length));for(var k in o)if(new RegExp('('+k+')').test(fmt))fmt=fmt.replace(RegExp.$1,(RegExp.$1.length==1)?(o[k]):(('00'+o[k]).substr(String(o[k]).length)));return fmt;}; 10 | var labels=[]; 11 | for(var i=0,time=new Date();i<24;time.setHours(time.getHours()-1),time.setMinutes(59),++i) 12 | labels.push(time.Format('HH:mm')); 13 | var hsChart=new Chart(document.getElementById('hs').getContext('2d'),{ 14 | type: 'line',// The type of chart we want to create 15 | data: {// The data for our dataset 16 | // labels: ['23h','22h','21h','20h','19h','18h','17h','16h','15h','14h','13h','12h','11h','10h','9h','8h','7h','6h','5h','4h','3h','2h','1h','现在'], 17 | labels: labels.reverse(), 18 | datasets: [{ 19 | label: 'in (GB)', 20 | backgroundColor: '#f7a4b980', 21 | borderColor: '#f15079bf', 22 | data:idata 23 | },{ 24 | label: 'out (GB)', 25 | backgroundColor: '#66ccff80', 26 | borderColor: '#0099ffbf', 27 | data:odata 28 | }] 29 | }, 30 | options: { 31 | scales:{ 32 | y:{min:0} 33 | } 34 | } 35 | }); 36 | idata=[],odata=[]; 37 | for(var [i,o] of traffic.ds){ 38 | ds_tot+=i+o; 39 | idata.push((i/G).toFixed(3)); 40 | odata.push((o/G).toFixed(3)); 41 | } 42 | labels=[]; 43 | for(var i=0,time=new Date();i<31;time.setDate(time.getDate()-1),++i) 44 | labels.push(time.getDate()); 45 | var dsChart=new Chart(document.getElementById('ds').getContext('2d'),{ 46 | type: 'line',// The type of chart we want to create 47 | data: {// The data for our dataset 48 | // labels: ['30d','29d','28d','27d','26d','25d','24d','23d','22d','21d','20d','19d','18d','17d','16d','15d','14d','13d','12d','11d','10d','9d','8d','7d','6d','5d','4d','3d','2d','1d','今天'], 49 | labels:labels.reverse(), 50 | datasets: [{ 51 | label: 'in (GB)', 52 | backgroundColor: '#f7a4b980', 53 | borderColor: '#f15079bf', 54 | data:idata 55 | },{ 56 | label: 'out (GB)', 57 | backgroundColor: '#66ccff80', 58 | borderColor: '#0099ffbf', 59 | data:odata 60 | }] 61 | }, 62 | options: { 63 | scales:{ 64 | y:{min:0} 65 | } 66 | } 67 | }); 68 | idata=[],odata=[]; 69 | for(var [i,o] of traffic.ms){ 70 | ms_tot+=i+o; 71 | idata.push((i/G).toFixed(3)); 72 | odata.push((o/G).toFixed(3)); 73 | } 74 | labels=[]; 75 | for(var i=0,time=new Date();i<12;time.setMonth(time.getMonth()-1),++i) 76 | labels.push(time.getUTCMonth()+1); 77 | var msChart=new Chart(document.getElementById('ms').getContext('2d'),{ 78 | type: 'line',// The type of chart we want to create 79 | data: {// The data for our dataset 80 | // labels: ['11','10','9','8','7','6','5','4','3','2','1','本月'], 81 | labels:labels.reverse(), 82 | datasets: [{ 83 | label: 'in (GB)', 84 | backgroundColor: '#f7a4b980', 85 | borderColor: '#f15079bf', 86 | data:idata 87 | },{ 88 | label: 'out (GB)', 89 | backgroundColor: '#66ccff80', 90 | borderColor: '#0099ffbf', 91 | data:odata 92 | }] 93 | }, 94 | options: { 95 | scales:{ 96 | y:{min:0} 97 | } 98 | } 99 | }); 100 | document.getElementById('hs_tot').innerText=`${(hs_tot/G).toFixed(2)}G(24小时)`; 101 | document.getElementById('ds_tot').innerText=`${(ds_tot/G).toFixed(2)}G(31天)`; 102 | document.getElementById('ms_tot').innerText=`${(ms_tot/G).toFixed(2)}G(12个月)`; -------------------------------------------------------------------------------- /static/js/webssh.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkeonkeo/nekonekostatus/7eff6bd6082131b9dbc3024eeeabcd9f6935538d/static/js/webssh.js -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd /root/nekonekostatus/ 3 | forever stop nekonekostatus.js -------------------------------------------------------------------------------- /views/admin.html: -------------------------------------------------------------------------------- 1 | {%set title = "管理"%} 2 | {%set admin=true%} 3 | {%extends "./base.html"%} 4 | 5 | {%block content%} 6 | 9 | {%endblock%} -------------------------------------------------------------------------------- /views/admin/servers.html: -------------------------------------------------------------------------------- 1 | {% set title = "管理服务器" %} 2 | {%set admin = true%} 3 | {% extends "../base.html" %} 4 | 5 | {% block content %} 6 |
7 | 11 | 22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {%set stas={'1':'正常','2':'对外隐藏','0':'不可用'}%} 32 | {%for server in servers%} 33 | 34 | 35 | 36 | 37 | 38 | 49 | 50 | {%endfor%} 51 | 52 |
排序名称域名/IP状态操作
drag_handle{{server.name}}{{server.data.ssh.host}}{{stas[server.status]|safe}} 39 | 40 | redo 41 | 42 | 43 | edit 44 | 45 | 46 | delete 47 | 48 |
53 |
54 | {%endblock%} 55 | {%block js%} 56 | 57 | 63 | 92 | {% endblock %} -------------------------------------------------------------------------------- /views/admin/servers/add.html: -------------------------------------------------------------------------------- 1 | {% set title = "新增服务器" %} 2 | {%set admin = true%} 3 | {% extends "../../base.html" %} 4 | 5 | {% block content %} 6 |
7 |
8 |
新增服务器
9 |
10 |
11 |
12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 |
23 | 24 |

SSH

25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 |
46 |
47 | 48 |
49 | 50 |

API

51 |
52 |
53 | 54 | 58 |
59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 |
72 |
73 |
74 | 77 |
78 |
79 | {%endblock%} 80 | {%block js%} 81 | {%block addscript%} 82 | 108 | {%endblock%} 109 | {%endblock%} -------------------------------------------------------------------------------- /views/admin/servers/edit.html: -------------------------------------------------------------------------------- 1 | {% set title = "编辑服务器" %} 2 | {%set admin = true%} 3 | {% extends "../../base.html" %} 4 | 5 | {% block content %} 6 |
7 |
8 |
{{server.name}}
9 | 10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 | {%set stas={'1':'正常','2':'对外隐藏','0':'不可用'}%} 18 |
19 | 20 | 25 |
26 | 27 |
28 | 29 |
30 |

SSH

31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 | 39 |
40 |
41 | 42 | 43 |
44 |
45 | 46 | 47 |
48 |
49 | 50 | 51 |
52 |
53 |

API

54 |
55 |
56 | 57 | 61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 | 73 |
74 |
75 |
76 |
77 | 80 | 83 |
84 |
85 | {%block actions%} 86 | 87 | 88 | {%endblock%} 89 |
90 |
91 | {%endblock%} 92 | 93 | {%block js%} 94 | {%block editscript%} 95 | 122 | {%endblock%} 123 | 151 | {%endblock%} -------------------------------------------------------------------------------- /views/admin/setting.html: -------------------------------------------------------------------------------- 1 | {% set title = "管理设置" %} 2 | {%set admin = true%} 3 | {% extends "../base.html" %} 4 | {%block content%} 5 |
6 | 7 |
8 |
设置
9 |
setting
10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 | 37 |
38 |
39 |
40 |
41 | 42 | 43 |
44 |
45 |
46 |
47 | 48 | 49 |
50 |
51 | 52 | 53 |
54 |
55 | 56 | 60 |
61 |
62 | 66 |
67 |
68 |
69 | 72 |
73 | {%endblock%} 74 | 75 | {%block js%} 76 | 111 | {%endblock%} -------------------------------------------------------------------------------- /views/admin/ssh_scripts.html: -------------------------------------------------------------------------------- 1 | {%set title = "脚本片段"%} 2 | {%set admin = true%} 3 | {%extends "../base.html"%} 4 | 5 | {%block content%} 6 |
7 | 8 | 9 | 10 | 11 | 12 | {%for script in ssh_scripts%} 13 | 14 | 15 | 23 | 24 | {%endfor%} 25 | 26 |
名称操作
{{script.name}} 16 | 19 | 22 |
27 |
28 |
29 | 32 |
33 |
34 |
添加脚本片段
35 |
36 |
37 | 38 | 39 |
40 |
41 | 42 | 43 |
44 |
45 |
46 | 49 |
50 |
51 |
52 |
编辑脚本片段
53 |
54 | 55 |
56 |
57 | 58 | 59 |
60 |
61 | 62 | 63 |
64 |
65 |
66 |
67 | 70 |
71 |
72 | {%endblock%} 73 | {%block js%} 74 | 112 | {%endblock%} -------------------------------------------------------------------------------- /views/appbar.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | {{setting.site.name}} 6 |
7 |
8 | 9 |
10 | {%if admin%} 11 | storage 12 | settings 13 | exit_to_app 14 | {%else%} 15 | build 16 | {%endif%} 17 | 20 |
21 |
22 | {%block js%} 23 | 33 | {%endblock%} -------------------------------------------------------------------------------- /views/base.html: -------------------------------------------------------------------------------- 1 | {%set Title=setting.site.name%} 2 | 3 | 4 | 5 | {{Title}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {%include 'appbar.html'%} 16 | 17 |
{%block content%}{%endblock%}
18 | 19 | {%include "./footer.html"%} 20 | 21 | 22 | 23 | 24 | 29 | {%block js%} 30 | {%endblock%} 31 | -------------------------------------------------------------------------------- /views/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/login.html: -------------------------------------------------------------------------------- 1 | {%set title = "登录"%} 2 | {%extends "./base.html"%} 3 | 4 | {%block content%} 5 |
6 |
7 |
登录
8 |
Login
9 |
10 |
11 |
12 | lock 13 | 14 | 15 |
16 |
17 |
18 |
19 | 23 |
24 |
25 | {%endblock%} 26 | {%block js%} 27 | 28 | 29 | {%endblock%} -------------------------------------------------------------------------------- /views/stat.html: -------------------------------------------------------------------------------- 1 | {%set title = "节点状态"%} 2 | {%extends "./base.html"%} 3 | 4 | {%block content%} 5 | 6 |
7 |
8 |
{{node.name}}
9 | 10 |
11 |
12 | info_outline 13 | {%if admin%} 14 | 15 | edit 16 | 17 | 20 | {%endif%} 21 |
22 |
23 |
    24 |
  • 25 | memory 26 |
    27 | CPU {{(100*node.stat.cpu.multi).toFixed(2)}}% 28 |
    29 | 32 | {%for usage in node.stat.cpu.single%} 33 |
    34 |
    35 |
    36 | {%endfor%} 37 |
    38 |
    39 |
  • 40 |
  • 41 | straighten 42 |
    43 | MEM {{(100*node.stat.mem.mem).toFixed(2)}}% 44 |
    45 |
    46 |
    47 |
    48 |
    49 |
    50 |
    51 |
    52 |
    53 |
  • 54 |
  • 55 | swap_vert 56 |
    57 |
    下行
    58 |
    59 | 60 |
    61 |
    62 |
    63 |
    上行
    64 |
    65 | 66 |
    67 |
    68 |
  • 69 |
  • 70 | swap_horiz 71 |
    72 |
    总下行
    73 |
    74 | {{strB(node.stat.net.total.in)}} 75 |
    76 |
    77 |
    78 |
    总上行
    79 |
    80 | {{strB(node.stat.net.total.out)}} 81 |
    82 |
    83 |
  • 84 |
    85 | {%include "./statistics.html"%} 86 |
    87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | {%for device,net in node.stat.net.devices%} 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | {%endfor%} 104 | 105 |
    device下行上行总下行总上行
    {{device}}{{strB(net.total.in)}}{{strB(net.total.out)}}
    106 | 107 |
108 |
109 |
110 | {%endblock%} 111 | 112 | {%block js%} 113 | 114 | {%include "./webssh.html"%} 115 | {%endblock%} -------------------------------------------------------------------------------- /views/statistics.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 |
8 |
负载监控
9 | keyboard_arrow_down 10 |
11 |
12 | 16 |
17 |
18 |
19 |
20 |
21 |
22 |
带宽监控
23 | keyboard_arrow_down 24 |
25 |
26 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |
流量统计
37 |
38 |
39 |
40 | keyboard_arrow_down 41 |
42 |
43 | 48 |
49 |
50 |
51 |
52 |
53 | {%block js%} 54 | 55 | 56 | 57 | {%endblock%} 58 |
-------------------------------------------------------------------------------- /views/stats.html: -------------------------------------------------------------------------------- 1 | {%set title = "节点状态"%} 2 | {%extends "./base.html"%} 3 | 4 | {%block content%} 5 |
6 | 7 | {%for sid,node in stats%} 8 | {%if node.stat!=-1%} 9 |
10 |
11 |
12 |
{{node.name}}
13 | 14 |
15 |
16 | info_outline 17 | {%if admin%} 18 | edit 19 | {%endif%} 20 |
21 |
22 |
    23 |
  • 24 | memory 25 |
    26 | CPU NaN 27 |
    28 |
    29 |
    30 |
    31 |
    32 |
    33 |
  • 34 |
  • 35 | straighten 36 |
    37 | MEM NaN 38 |
    39 |
    40 |
    41 |
    42 |
    43 |
    44 |
  • 45 |
  • 46 | swap_vert 47 |
    48 |
    下行
    49 |
    50 | NaN 51 |
    52 |
    53 |
    54 |
    上行
    55 |
    56 | NaN 57 |
    58 |
    59 |
  • 60 |
  • 61 | swap_horiz 62 |
    63 |
    总下行
    64 |
    65 | NaN 66 |
    67 |
    68 |
    69 |
    总上行
    70 |
    71 | NaN 72 |
    73 |
    74 |
  • 75 |
76 |
77 |
78 |
79 | {%endif%} 80 | {%endfor%} 81 |
82 | 83 | {%if admin%} 84 |
85 | 89 | 97 |
98 | {%endif%} 99 | {%endblock%} 100 | 101 | {%block js%} 102 | 103 | {%endblock%} -------------------------------------------------------------------------------- /views/stats/card.html: -------------------------------------------------------------------------------- 1 | {%set title = "节点状态"%} 2 | {%extends "../base.html"%} 3 | {%block content%} 4 | 9 |
10 | 11 | {%for sid,node in stats%} 12 | {%if node.stat!=-1%} 13 |
14 |
15 |
16 |
{{node.name}}
17 |
18 |
19 | info_outline 20 | {%if admin%} 21 | 22 | edit 23 | 24 | {%endif%} 25 |
26 |
27 |
    28 |
  • 29 | memory 30 |
    31 | CPU NaN 32 |
    33 |
    34 |
    35 |
    36 |
    37 |
    38 |
  • 39 |
  • 40 | straighten 41 |
    42 | MEM NaN 43 |
    44 |
    45 |
    46 |
    47 |
    48 |
    49 |
  • 50 |
  • 51 | swap_vert 52 |
    53 |
    下行
    54 |
    55 | NaN 56 |
    57 |
    58 |
    59 |
    上行
    60 |
    61 | NaN 62 |
    63 |
    64 |
  • 65 |
  • 66 | swap_horiz 67 |
    68 |
    总下行
    69 |
    70 | NaN 71 |
    72 |
    73 |
    74 |
    总上行
    75 |
    76 | NaN 77 |
    78 |
    79 |
  • 80 |
81 |
82 |
83 |
84 | {%endif%} 85 | {%endfor%} 86 |
87 | {%endblock%} 88 | 89 | {%block js%} 90 | 91 | {%endblock%} -------------------------------------------------------------------------------- /views/stats/list.html: -------------------------------------------------------------------------------- 1 | {%set title = "节点状态"%} 2 | {%extends "../base.html"%} 3 | 4 | {%block content%} 5 | 10 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {%for sid,node in stats%} 25 | {%if node.stat!=-1%} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 39 | 46 | 54 | 55 | {%endif%} 56 | {%endfor%} 57 | 58 |
名称下行上行总下行总上行CPU内存more
{{node.name}}NaNNaNNaNNaN 33 |
34 |
35 | NaN 36 |
37 |
38 |
40 |
41 |
42 | NaN 43 |
44 |
45 |
47 | info_outline 48 | {%if admin%} 49 | 50 | edit 51 | 52 | {%endif%} 53 |
59 | {%endblock%} 60 | 61 | {%block js%} 62 | 63 | {%endblock%} -------------------------------------------------------------------------------- /views/webssh.html: -------------------------------------------------------------------------------- 1 |
2 |
{{node.name}}
3 |
4 |
5 |
6 |
7 | 8 |
    9 | {%for script in db.ssh_scripts.all()%} 10 |
  • 11 | {{script.name}} 12 |
  • 13 | {%endfor%} 14 |
15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 28 |
29 | --------------------------------------------------------------------------------