Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9d9bd248 | ||
|
|
5e528c42c7 | ||
|
|
daa7e03240 | ||
|
|
ab39782c78 | ||
|
|
e24925b049 | ||
|
|
1d4245ee09 | ||
|
|
8df863a3e0 | ||
|
|
07e0382598 | ||
|
|
8dd509aa08 | ||
|
|
99ac12c9fd | ||
|
|
ebc45f9b8f | ||
|
|
3b5ee464a7 | ||
|
|
c4b2c47beb | ||
|
|
0835f13f28 | ||
|
|
ee3cda8aed | ||
|
|
604fc7d4c0 | ||
|
|
5c549261dd | ||
|
|
00041c0ebd | ||
|
|
dbfea9a00b | ||
|
|
e8b208edfa | ||
|
|
7d38634933 | ||
|
|
5bd4b57cbf | ||
|
|
fdb2fd8839 | ||
|
|
f3fac9325c | ||
|
|
626613441d | ||
|
|
b86b73a433 | ||
|
|
e8b8e59bd7 | ||
|
|
c9bcba6f28 | ||
|
|
ad9dc5470a | ||
|
|
8539fb6559 | ||
|
|
11bcc47d49 | ||
|
|
bb789f7652 | ||
|
|
51c1e41123 | ||
|
|
83efdc6258 | ||
|
|
bf58ad24b5 | ||
|
|
53581d48fc | ||
|
|
b00a00e791 | ||
|
|
b91f29077c |
@@ -1,3 +1,4 @@
|
|||||||
|
# latest 镜像还没有 riscv64 https://hub.docker.com/_/alpine/tags
|
||||||
FROM alpine:edge
|
FROM alpine:edge
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<br>
|
<br>
|
||||||
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
|
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
|
||||||
<br><br>
|
<br><br>
|
||||||
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard"> <img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.15.3-brightgreen?style=for-the-badge&logo=linux">
|
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard"> <img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.15.8-brightgreen?style=for-the-badge&logo=linux">
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
|
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
|
||||||
@@ -65,86 +65,107 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
|||||||
<a href="https://github.com/cantoblanco" title="Kris">
|
<a href="https://github.com/cantoblanco" title="Kris">
|
||||||
<img src="https://avatars.githubusercontent.com/u/116849421?v=4" width="50;" alt="Kris"/>
|
<img src="https://avatars.githubusercontent.com/u/116849421?v=4" width="50;" alt="Kris"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/CosmosZ-code" title="CosmosZ-code">
|
<a href="https://github.com/spiritLHLS" title="spiritlhl">
|
||||||
<img src="https://avatars.githubusercontent.com/u/81398224?v=4" width="50;" alt="CosmosZ-code"/>
|
<img src="https://avatars.githubusercontent.com/u/103393591?v=4" width="50;" alt="spiritlhl"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/liuyanxi975" title="刘颜溪">
|
<a href="https://github.com/liuyanxi975" title="刘颜溪">
|
||||||
<img src="https://avatars.githubusercontent.com/u/24417037?v=4" width="50;" alt="刘颜溪"/>
|
<img src="https://avatars.githubusercontent.com/u/24417037?v=4" width="50;" alt="刘颜溪"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/applexad" title="Applexad">
|
<a href="https://github.com/CosmosZ-code" title="CosmosZ-code">
|
||||||
<img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="Applexad"/>
|
<img src="https://avatars.githubusercontent.com/u/81398224?v=4" width="50;" alt="CosmosZ-code"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/uubulb" title="UUBulb">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/lvgj-stack" title="Ko no dio">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/38449861?v=4" width="50;" alt="Ko no dio"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/hhhkkk520" title="Kris">
|
<a href="https://github.com/hhhkkk520" title="Kris">
|
||||||
<img src="https://avatars.githubusercontent.com/u/52115472?v=4" width="50;" alt="Kris"/>
|
<img src="https://avatars.githubusercontent.com/u/52115472?v=4" width="50;" alt="Kris"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/spiritLHLS" title="spiritlhl">
|
<a href="https://github.com/1ridic" title="1ridic">
|
||||||
<img src="https://avatars.githubusercontent.com/u/103393591?v=4" width="50;" alt="spiritlhl"/>
|
<img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Mmx233" title="Mmx">
|
<a href="https://github.com/Mmx233" title="Mmx">
|
||||||
<img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/>
|
<img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/nap0o" title="nap0o">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/>
|
||||||
|
</a>
|
||||||
<a href="https://github.com/rootmelo92118" title="rootmelo92118">
|
<a href="https://github.com/rootmelo92118" title="rootmelo92118">
|
||||||
<img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/>
|
<img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/1ridic" title="1ridic">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/coreff" title="Core F">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/38347122?v=4" width="50;" alt="Core F"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Creling" title="Creling">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/43109504?v=4" width="50;" alt="Creling"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/ch8o" title="no-name-now">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/fscarmen" title="fscarmen">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/iilemon" title="Sean">
|
<a href="https://github.com/iilemon" title="Sean">
|
||||||
<img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/>
|
<img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Es-dese" title="Esdese">
|
<a href="https://github.com/fscarmen" title="fscarmen">
|
||||||
<img src="https://avatars.githubusercontent.com/u/71542548?v=4" width="50;" alt="Esdese"/>
|
<img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/GreenTeodoro839" title="GreenTeodoro839">
|
<a href="https://github.com/ch8o" title="no-name-now">
|
||||||
<img src="https://avatars.githubusercontent.com/u/77104800?v=4" width="50;" alt="GreenTeodoro839"/>
|
<img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/techotaku" title="Ian Li">
|
<a href="https://github.com/Creling" title="Creling">
|
||||||
<img src="https://avatars.githubusercontent.com/u/1948179?v=4" width="50;" alt="Ian Li"/>
|
<img src="https://avatars.githubusercontent.com/u/43109504?v=4" width="50;" alt="Creling"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/KorenKrita" title="KorenKrita">
|
<a href="https://github.com/coreff" title="Core F">
|
||||||
<img src="https://avatars.githubusercontent.com/u/22239339?v=4" width="50;" alt="KorenKrita"/>
|
<img src="https://avatars.githubusercontent.com/u/38347122?v=4" width="50;" alt="Core F"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/MartijnLindeman" title="Martijn Lindeman">
|
<a href="https://github.com/colour93" title="玖叁">
|
||||||
<img src="https://avatars.githubusercontent.com/u/78365708?v=4" width="50;" alt="Martijn Lindeman"/>
|
<img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/nickfox-taterli" title="Tater Li">
|
<a href="https://github.com/arkylin" title="凌">
|
||||||
<img src="https://avatars.githubusercontent.com/u/19658596?v=4" width="50;" alt="Tater Li"/>
|
<img src="https://avatars.githubusercontent.com/u/35104502?v=4" width="50;" alt="凌"/>
|
||||||
</a>
|
|
||||||
<a href="https://github.com/hmsjy2017" title="Tony">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/42692274?v=4" width="50;" alt="Tony"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/adminsama" title="adminsama">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/60880076?v=4" width="50;" alt="adminsama"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/acgpiano" title="Acgpiano">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/15900800?v=4" width="50;" alt="Acgpiano"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/guoyongchang" title="guoyongchang">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/10484506?v=4" width="50;" alt="guoyongchang"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/yuanweize" title="I">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/unclezs" title="unclezs">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/>
|
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/ysicing" title="缘生">
|
<a href="https://github.com/ysicing" title="缘生">
|
||||||
<img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/>
|
<img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/colour93" title="玖叁">
|
<a href="https://github.com/xykt" title="xykt">
|
||||||
<img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/>
|
<img src="https://avatars.githubusercontent.com/u/152045469?v=4" width="50;" alt="xykt"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/unclezs" title="unclezs">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/yuanweize" title="I">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/guoyongchang" title="guoyongchang">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/10484506?v=4" width="50;" alt="guoyongchang"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/acgpiano" title="Acgpiano">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/15900800?v=4" width="50;" alt="Acgpiano"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/adminsama" title="adminsama">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/60880076?v=4" width="50;" alt="adminsama"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/hmsjy2017" title="Tony">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/42692274?v=4" width="50;" alt="Tony"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/nickfox-taterli" title="Tater Li">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/19658596?v=4" width="50;" alt="Tater Li"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/MartijnLindeman" title="Martijn Lindeman">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/78365708?v=4" width="50;" alt="Martijn Lindeman"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/KorenKrita" title="KorenKrita">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/22239339?v=4" width="50;" alt="KorenKrita"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/techotaku" title="Ian Li">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/1948179?v=4" width="50;" alt="Ian Li"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/HsukqiLee" title="HsukqiLee">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/79034142?v=4" width="50;" alt="HsukqiLee"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/GreenTeodoro839" title="GreenTeodoro839">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/77104800?v=4" width="50;" alt="GreenTeodoro839"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Es-dese" title="Esdese">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/71542548?v=4" width="50;" alt="Esdese"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/DarcJC" title="Darc Z.">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/53445798?v=4" width="50;" alt="Darc Z."/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/wwng2333" title="Crazy">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/17147265?v=4" width="50;" alt="Crazy"/>
|
||||||
</a><!--GAMFC_DELIMITER_END-->
|
</a><!--GAMFC_DELIMITER_END-->
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/naiba/nezha/pkg/mygin"
|
|
||||||
"github.com/naiba/nezha/service/singleton"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/naiba/nezha/model"
|
||||||
|
"github.com/naiba/nezha/pkg/mygin"
|
||||||
|
"github.com/naiba/nezha/service/singleton"
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiV1 struct {
|
type apiV1 struct {
|
||||||
@@ -14,18 +17,32 @@ type apiV1 struct {
|
|||||||
|
|
||||||
func (v *apiV1) serve() {
|
func (v *apiV1) serve() {
|
||||||
r := v.r.Group("")
|
r := v.r.Group("")
|
||||||
// API
|
// 强制认证的 API
|
||||||
r.Use(mygin.Authorize(mygin.AuthorizeOption{
|
r.Use(mygin.Authorize(mygin.AuthorizeOption{
|
||||||
Member: true,
|
MemberOnly: true,
|
||||||
IsPage: false,
|
|
||||||
AllowAPI: true,
|
AllowAPI: true,
|
||||||
|
IsPage: false,
|
||||||
Msg: "访问此接口需要认证",
|
Msg: "访问此接口需要认证",
|
||||||
Btn: "点此登录",
|
Btn: "点此登录",
|
||||||
Redirect: "/login",
|
Redirect: "/login",
|
||||||
}))
|
}))
|
||||||
r.GET("/server/list", v.serverList)
|
r.GET("/server/list", v.serverList)
|
||||||
r.GET("/server/details", v.serverDetails)
|
r.GET("/server/details", v.serverDetails)
|
||||||
|
// 不强制认证的 API
|
||||||
|
mr := v.r.Group("monitor")
|
||||||
|
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
||||||
|
MemberOnly: false,
|
||||||
|
IsPage: false,
|
||||||
|
AllowAPI: true,
|
||||||
|
Msg: "访问此接口需要认证",
|
||||||
|
Btn: "点此登录",
|
||||||
|
Redirect: "/login",
|
||||||
|
}))
|
||||||
|
mr.Use(mygin.ValidateViewPassword(mygin.ValidateViewPasswordOption{
|
||||||
|
IsPage: false,
|
||||||
|
AbortWhenFail: true,
|
||||||
|
}))
|
||||||
|
mr.GET("/:id", v.monitorHistoriesById)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverList 获取服务器列表 不传入Query参数则获取全部
|
// serverList 获取服务器列表 不传入Query参数则获取全部
|
||||||
@@ -65,3 +82,31 @@ func (v *apiV1) serverDetails(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.JSON(200, singleton.ServerAPI.GetAllStatus())
|
c.JSON(200, singleton.ServerAPI.GetAllStatus())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *apiV1) monitorHistoriesById(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(400, gin.H{"code": 400, "message": "id参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
server, ok := singleton.ServerList[id]
|
||||||
|
if !ok {
|
||||||
|
c.AbortWithStatusJSON(404, gin.H{
|
||||||
|
"code": 404,
|
||||||
|
"message": "id不存在",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||||
|
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
|
||||||
|
authorized := isMember || isViewPasswordVerfied
|
||||||
|
|
||||||
|
if server.HideForGuest && !authorized {
|
||||||
|
c.AbortWithStatusJSON(403, gin.H{"code": 403, "message": "需要认证"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": server.ID}))
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -43,11 +44,18 @@ type commonPage struct {
|
|||||||
func (cp *commonPage) serve() {
|
func (cp *commonPage) serve() {
|
||||||
cr := cp.r.Group("")
|
cr := cp.r.Group("")
|
||||||
cr.Use(mygin.Authorize(mygin.AuthorizeOption{}))
|
cr.Use(mygin.Authorize(mygin.AuthorizeOption{}))
|
||||||
cr.GET("/terminal/:id", cp.terminal)
|
cr.Use(mygin.PreferredTheme)
|
||||||
cr.POST("/view-password", cp.issueViewPassword)
|
cr.POST("/view-password", cp.issueViewPassword)
|
||||||
cr.Use(cp.checkViewPassword) // 前端查看密码鉴权
|
cr.GET("/terminal/:id", cp.terminal)
|
||||||
|
cr.Use(mygin.ValidateViewPassword(mygin.ValidateViewPasswordOption{
|
||||||
|
IsPage: true,
|
||||||
|
AbortWhenFail: true,
|
||||||
|
}))
|
||||||
cr.GET("/", cp.home)
|
cr.GET("/", cp.home)
|
||||||
cr.GET("/service", cp.service)
|
cr.GET("/service", cp.service)
|
||||||
|
// TODO: 界面直接跳转使用该接口
|
||||||
|
cr.GET("/network/:id", cp.network)
|
||||||
|
cr.GET("/network", cp.network)
|
||||||
cr.GET("/ws", cp.ws)
|
cr.GET("/ws", cp.ws)
|
||||||
cr.POST("/terminal", cp.createTerminal)
|
cr.POST("/terminal", cp.createTerminal)
|
||||||
}
|
}
|
||||||
@@ -59,6 +67,7 @@ type viewPasswordForm struct {
|
|||||||
func (p *commonPage) issueViewPassword(c *gin.Context) {
|
func (p *commonPage) issueViewPassword(c *gin.Context) {
|
||||||
var vpf viewPasswordForm
|
var vpf viewPasswordForm
|
||||||
err := c.ShouldBind(&vpf)
|
err := c.ShouldBind(&vpf)
|
||||||
|
log.Println("bingo", vpf)
|
||||||
var hash []byte
|
var hash []byte
|
||||||
if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword {
|
if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword {
|
||||||
err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
|
err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
|
||||||
@@ -81,31 +90,6 @@ func (p *commonPage) issueViewPassword(c *gin.Context) {
|
|||||||
c.Redirect(http.StatusFound, c.Request.Referer())
|
c.Redirect(http.StatusFound, c.Request.Referer())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *commonPage) checkViewPassword(c *gin.Context) {
|
|
||||||
if singleton.Conf.Site.ViewPassword == "" {
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, authorized := c.Get(model.CtxKeyAuthorizedUser); authorized {
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证查看密码
|
|
||||||
viewPassword, _ := c.Cookie(singleton.Conf.Site.CookieName + "-vp")
|
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(viewPassword), []byte(singleton.Conf.Site.ViewPassword)); err != nil {
|
|
||||||
c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/viewpassword", mygin.CommonEnvironment(c, gin.H{
|
|
||||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
|
|
||||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
|
||||||
}))
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Set(model.CtxKeyViewPasswordVerified, true)
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *commonPage) service(c *gin.Context) {
|
func (p *commonPage) service(c *gin.Context) {
|
||||||
res, _, _ := p.requestGroup.Do("servicePage", func() (interface{}, error) {
|
res, _, _ := p.requestGroup.Do("servicePage", func() (interface{}, error) {
|
||||||
singleton.AlertsLock.RLock()
|
singleton.AlertsLock.RLock()
|
||||||
@@ -114,12 +98,17 @@ func (p *commonPage) service(c *gin.Context) {
|
|||||||
var statsStore map[uint64]model.CycleTransferStats
|
var statsStore map[uint64]model.CycleTransferStats
|
||||||
copier.Copy(&stats, singleton.ServiceSentinelShared.LoadStats())
|
copier.Copy(&stats, singleton.ServiceSentinelShared.LoadStats())
|
||||||
copier.Copy(&statsStore, singleton.AlertsCycleTransferStatsStore)
|
copier.Copy(&statsStore, singleton.AlertsCycleTransferStatsStore)
|
||||||
|
for k, service := range stats {
|
||||||
|
if !service.Monitor.EnableShowInService {
|
||||||
|
delete(stats, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
return []interface {
|
return []interface {
|
||||||
}{
|
}{
|
||||||
stats, statsStore,
|
stats, statsStore,
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/service", mygin.CommonEnvironment(c, gin.H{
|
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/service"), mygin.CommonEnvironment(c, gin.H{
|
||||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesStatus"}),
|
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesStatus"}),
|
||||||
"Services": res.([]interface{})[0],
|
"Services": res.([]interface{})[0],
|
||||||
"CycleTransferStats": res.([]interface{})[1],
|
"CycleTransferStats": res.([]interface{})[1],
|
||||||
@@ -127,6 +116,112 @@ func (p *commonPage) service(c *gin.Context) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cp *commonPage) network(c *gin.Context) {
|
||||||
|
var (
|
||||||
|
monitorHistory *model.MonitorHistory
|
||||||
|
servers []*model.Server
|
||||||
|
serverIdsWithMonitor []uint64
|
||||||
|
monitorInfos = []byte("{}")
|
||||||
|
id uint64
|
||||||
|
)
|
||||||
|
if len(singleton.SortedServerList) > 0 {
|
||||||
|
id = singleton.SortedServerList[0].ID
|
||||||
|
}
|
||||||
|
if err := singleton.DB.Model(&model.MonitorHistory{}).Select("monitor_id, server_id").
|
||||||
|
Where("monitor_id != 0 and server_id != 0").Limit(1).First(&monitorHistory).Error; err != nil {
|
||||||
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Title: "请求失败",
|
||||||
|
Msg: "请求参数有误:" + "server monitor history not found",
|
||||||
|
Link: "/",
|
||||||
|
Btn: "返回重试",
|
||||||
|
}, true)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
if monitorHistory == nil || monitorHistory.ServerID == 0 {
|
||||||
|
if len(singleton.SortedServerList) > 0 {
|
||||||
|
id = singleton.SortedServerList[0].ID
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id = monitorHistory.ServerID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
if idStr != "" {
|
||||||
|
var err error
|
||||||
|
id, err = strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Title: "请求失败",
|
||||||
|
Msg: "请求参数有误:" + err.Error(),
|
||||||
|
Link: "/",
|
||||||
|
Btn: "返回重试",
|
||||||
|
}, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, ok := singleton.ServerList[id]
|
||||||
|
if !ok {
|
||||||
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Title: "请求失败",
|
||||||
|
Msg: "请求参数有误:" + "server id not found",
|
||||||
|
Link: "/",
|
||||||
|
Btn: "返回重试",
|
||||||
|
}, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
monitorHistories := singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": id})
|
||||||
|
monitorInfos, _ = utils.Json.Marshal(monitorHistories)
|
||||||
|
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||||
|
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
|
||||||
|
|
||||||
|
if err := singleton.DB.Model(&model.MonitorHistory{}).
|
||||||
|
Select("distinct(server_id)").
|
||||||
|
Where("server_id != 0").
|
||||||
|
Find(&serverIdsWithMonitor).
|
||||||
|
Error; err != nil {
|
||||||
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Title: "请求失败",
|
||||||
|
Msg: "请求参数有误:" + "no server with monitor histories",
|
||||||
|
Link: "/",
|
||||||
|
Btn: "返回重试",
|
||||||
|
}, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isMember || isViewPasswordVerfied {
|
||||||
|
for _, server := range singleton.SortedServerList {
|
||||||
|
for _, id := range serverIdsWithMonitor {
|
||||||
|
if server.ID == id {
|
||||||
|
servers = append(servers, server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, server := range singleton.SortedServerListForGuest {
|
||||||
|
for _, id := range serverIdsWithMonitor {
|
||||||
|
if server.ID == id {
|
||||||
|
servers = append(servers, server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serversBytes, _ := utils.Json.Marshal(Data{
|
||||||
|
Now: time.Now().Unix() * 1000,
|
||||||
|
Servers: servers,
|
||||||
|
})
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/network"), mygin.CommonEnvironment(c, gin.H{
|
||||||
|
"Servers": string(serversBytes),
|
||||||
|
"MonitorInfos": string(monitorInfos),
|
||||||
|
"CustomCode": singleton.Conf.Site.CustomCode,
|
||||||
|
"MaxTCPPingValue": singleton.Conf.MaxTCPPingValue,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
|
func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
|
||||||
v, err, _ := cp.requestGroup.Do("serverStats", func() (any, error) {
|
v, err, _ := cp.requestGroup.Do("serverStats", func() (any, error) {
|
||||||
singleton.SortedServerLock.RLock()
|
singleton.SortedServerLock.RLock()
|
||||||
@@ -165,7 +260,7 @@ func (cp *commonPage) home(c *gin.Context) {
|
|||||||
}, true)
|
}, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/home", mygin.CommonEnvironment(c, gin.H{
|
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/home"), mygin.CommonEnvironment(c, gin.H{
|
||||||
"Servers": string(stat),
|
"Servers": string(stat),
|
||||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
"CustomCode": singleton.Conf.Site.CustomCode,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type guestPage struct {
|
|||||||
func (gp *guestPage) serve() {
|
func (gp *guestPage) serve() {
|
||||||
gr := gp.r.Group("")
|
gr := gp.r.Group("")
|
||||||
gr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
gr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
||||||
Guest: true,
|
GuestOnly: true,
|
||||||
IsPage: true,
|
IsPage: true,
|
||||||
Msg: "您已登录",
|
Msg: "您已登录",
|
||||||
Btn: "返回首页",
|
Btn: "返回首页",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/naiba/nezha/model"
|
"github.com/naiba/nezha/model"
|
||||||
"github.com/naiba/nezha/pkg/mygin"
|
"github.com/naiba/nezha/pkg/mygin"
|
||||||
@@ -27,7 +28,7 @@ type memberAPI struct {
|
|||||||
func (ma *memberAPI) serve() {
|
func (ma *memberAPI) serve() {
|
||||||
mr := ma.r.Group("")
|
mr := ma.r.Group("")
|
||||||
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
||||||
Member: true,
|
MemberOnly: true,
|
||||||
IsPage: false,
|
IsPage: false,
|
||||||
Msg: "访问此接口需要登录",
|
Msg: "访问此接口需要登录",
|
||||||
Btn: "点此登录",
|
Btn: "点此登录",
|
||||||
@@ -185,7 +186,17 @@ func (ma *memberAPI) delete(c *gin.Context) {
|
|||||||
var err error
|
var err error
|
||||||
switch c.Param("model") {
|
switch c.Param("model") {
|
||||||
case "server":
|
case "server":
|
||||||
|
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
err = singleton.DB.Unscoped().Delete(&model.Server{}, "id = ?", id).Error
|
err = singleton.DB.Unscoped().Delete(&model.Server{}, "id = ?", id).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "server_id = ?", id).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// 删除服务器
|
// 删除服务器
|
||||||
singleton.ServerLock.Lock()
|
singleton.ServerLock.Lock()
|
||||||
@@ -289,6 +300,8 @@ type serverForm struct {
|
|||||||
Tag string
|
Tag string
|
||||||
Note string
|
Note string
|
||||||
HideForGuest string
|
HideForGuest string
|
||||||
|
EnableDDNS string
|
||||||
|
DDNSDomain string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
||||||
@@ -304,6 +317,8 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
|||||||
s.Tag = sf.Tag
|
s.Tag = sf.Tag
|
||||||
s.Note = sf.Note
|
s.Note = sf.Note
|
||||||
s.HideForGuest = sf.HideForGuest == "on"
|
s.HideForGuest = sf.HideForGuest == "on"
|
||||||
|
s.EnableDDNS = sf.EnableDDNS == "on"
|
||||||
|
s.DDNSDomain = sf.DDNSDomain
|
||||||
if s.ID == 0 {
|
if s.ID == 0 {
|
||||||
s.Secret, err = utils.GenerateRandomString(18)
|
s.Secret, err = utils.GenerateRandomString(18)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -383,6 +398,7 @@ type monitorForm struct {
|
|||||||
MaxLatency float32
|
MaxLatency float32
|
||||||
LatencyNotify string
|
LatencyNotify string
|
||||||
EnableTriggerTask string
|
EnableTriggerTask string
|
||||||
|
EnableShowInService string
|
||||||
FailTriggerTasksRaw string
|
FailTriggerTasksRaw string
|
||||||
RecoverTriggerTasksRaw string
|
RecoverTriggerTasksRaw string
|
||||||
}
|
}
|
||||||
@@ -404,6 +420,7 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
|
|||||||
m.LatencyNotify = mf.LatencyNotify == "on"
|
m.LatencyNotify = mf.LatencyNotify == "on"
|
||||||
m.MinLatency = mf.MinLatency
|
m.MinLatency = mf.MinLatency
|
||||||
m.MaxLatency = mf.MaxLatency
|
m.MaxLatency = mf.MaxLatency
|
||||||
|
m.EnableShowInService = mf.EnableShowInService == "on"
|
||||||
m.EnableTriggerTask = mf.EnableTriggerTask == "on"
|
m.EnableTriggerTask = mf.EnableTriggerTask == "on"
|
||||||
m.RecoverTriggerTasksRaw = mf.RecoverTriggerTasksRaw
|
m.RecoverTriggerTasksRaw = mf.RecoverTriggerTasksRaw
|
||||||
m.FailTriggerTasksRaw = mf.FailTriggerTasksRaw
|
m.FailTriggerTasksRaw = mf.FailTriggerTasksRaw
|
||||||
@@ -427,6 +444,13 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
|
|||||||
err = singleton.DB.Save(&m).Error
|
err = singleton.DB.Save(&m).Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err == nil {
|
||||||
|
if m.Cover == 0 {
|
||||||
|
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
|
||||||
|
} else {
|
||||||
|
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id not in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = singleton.ServiceSentinelShared.OnMonitorUpdate(m)
|
err = singleton.ServiceSentinelShared.OnMonitorUpdate(m)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type memberPage struct {
|
|||||||
func (mp *memberPage) serve() {
|
func (mp *memberPage) serve() {
|
||||||
mr := mp.r.Group("")
|
mr := mp.r.Group("")
|
||||||
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
||||||
Member: true,
|
MemberOnly: true,
|
||||||
IsPage: true,
|
IsPage: true,
|
||||||
Msg: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
|
Msg: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
|
||||||
Btn: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
|
Btn: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
|
||||||
@@ -81,7 +81,6 @@ func (mp *memberPage) setting(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/setting", mygin.CommonEnvironment(c, gin.H{
|
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/setting", mygin.CommonEnvironment(c, gin.H{
|
||||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}),
|
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}),
|
||||||
"Languages": model.Languages,
|
"Languages": model.Languages,
|
||||||
"Themes": model.Themes,
|
|
||||||
"DashboardThemes": model.DashboardThemes,
|
"DashboardThemes": model.DashboardThemes,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,10 +49,20 @@ func DispatchTask(serviceSentinelDispatchBus <-chan model.Monitor) {
|
|||||||
workedServerIndex++
|
workedServerIndex++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// 找到合适机器执行任务,跳出循环
|
if task.Cover == model.MonitorCoverIgnoreAll && task.SkipServers[singleton.SortedServerList[workedServerIndex].ID] {
|
||||||
singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
|
singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
|
||||||
workedServerIndex++
|
workedServerIndex++
|
||||||
break
|
continue
|
||||||
|
}
|
||||||
|
if task.Cover == model.MonitorCoverAll && !task.SkipServers[singleton.SortedServerList[workedServerIndex].ID] {
|
||||||
|
singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
|
||||||
|
workedServerIndex++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 找到合适机器执行任务,跳出循环
|
||||||
|
// singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
|
||||||
|
// workedServerIndex++
|
||||||
|
// break
|
||||||
}
|
}
|
||||||
singleton.SortedServerLock.RUnlock()
|
singleton.SortedServerLock.RUnlock()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ require (
|
|||||||
github.com/spf13/viper v1.17.0
|
github.com/spf13/viper v1.17.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/xanzy/go-gitlab v0.94.0
|
github.com/xanzy/go-gitlab v0.94.0
|
||||||
golang.org/x/crypto v0.15.0
|
golang.org/x/crypto v0.17.0
|
||||||
golang.org/x/oauth2 v0.14.0
|
golang.org/x/oauth2 v0.14.0
|
||||||
golang.org/x/sync v0.5.0
|
golang.org/x/sync v0.5.0
|
||||||
golang.org/x/text v0.14.0
|
golang.org/x/text v0.14.0
|
||||||
@@ -80,7 +80,7 @@ require (
|
|||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/net v0.18.0 // indirect
|
golang.org/x/net v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.14.0 // indirect
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect
|
||||||
|
|||||||
@@ -339,8 +339,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
|
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||||
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -478,12 +478,12 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
|
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||||
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
const CtxKeyAuthorizedUser = "ckau"
|
const CtxKeyAuthorizedUser = "ckau"
|
||||||
const CtxKeyViewPasswordVerified = "ckvpv"
|
const CtxKeyViewPasswordVerified = "ckvpv"
|
||||||
|
const CtxKeyPreferredTheme = "ckpt"
|
||||||
const CacheKeyOauth2State = "p:a:state"
|
const CacheKeyOauth2State = "p:a:state"
|
||||||
|
|
||||||
type Common struct {
|
type Common struct {
|
||||||
|
|||||||
@@ -110,6 +110,21 @@ type Config struct {
|
|||||||
|
|
||||||
v *viper.Viper
|
v *viper.Viper
|
||||||
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
|
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
|
||||||
|
MaxTCPPingValue int32
|
||||||
|
AvgPingCount int
|
||||||
|
|
||||||
|
// 动态域名解析更新
|
||||||
|
DDNS struct {
|
||||||
|
Enable bool
|
||||||
|
Provider string
|
||||||
|
AccessID string
|
||||||
|
AccessSecret string
|
||||||
|
WebhookURL string
|
||||||
|
WebhookMethod string
|
||||||
|
WebhookRequestBody string
|
||||||
|
WebhookHeaders string
|
||||||
|
MaxRetries uint32
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read 读取配置文件并应用
|
// Read 读取配置文件并应用
|
||||||
@@ -144,6 +159,21 @@ func (c *Config) Read(path string) error {
|
|||||||
if c.Location == "" {
|
if c.Location == "" {
|
||||||
c.Location = "Asia/Shanghai"
|
c.Location = "Asia/Shanghai"
|
||||||
}
|
}
|
||||||
|
if c.MaxTCPPingValue == 0 {
|
||||||
|
c.MaxTCPPingValue = 1000
|
||||||
|
}
|
||||||
|
if c.AvgPingCount == 0 {
|
||||||
|
c.AvgPingCount = 2
|
||||||
|
}
|
||||||
|
if c.DDNS.Provider == "" {
|
||||||
|
c.DDNS.Provider = "webhook"
|
||||||
|
}
|
||||||
|
if c.DDNS.WebhookMethod == "" {
|
||||||
|
c.DDNS.WebhookMethod = "POST"
|
||||||
|
}
|
||||||
|
if c.DDNS.MaxRetries == 0 {
|
||||||
|
c.DDNS.MaxRetries = 3
|
||||||
|
}
|
||||||
|
|
||||||
c.updateIgnoredIPNotificationID()
|
c.updateIgnoredIPNotificationID()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+4
-2
@@ -4,10 +4,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/naiba/nezha/pkg/utils"
|
|
||||||
pb "github.com/naiba/nezha/proto"
|
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
|
pb "github.com/naiba/nezha/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -49,6 +50,7 @@ type Monitor struct {
|
|||||||
Cover uint8
|
Cover uint8
|
||||||
|
|
||||||
EnableTriggerTask bool `gorm:"default: false"`
|
EnableTriggerTask bool `gorm:"default: false"`
|
||||||
|
EnableShowInService bool `gorm:"default: false"`
|
||||||
FailTriggerTasksRaw string `gorm:"default:'[]'"`
|
FailTriggerTasksRaw string `gorm:"default:'[]'"`
|
||||||
RecoverTriggerTasksRaw string `gorm:"default:'[]'"`
|
RecoverTriggerTasksRaw string `gorm:"default:'[]'"`
|
||||||
FailTriggerTasks []uint64 `gorm:"-" json:"-"` // 失败时执行的触发任务id
|
FailTriggerTasks []uint64 `gorm:"-" json:"-"` // 失败时执行的触发任务id
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
// MonitorHistory 历史监控记录
|
// MonitorHistory 历史监控记录
|
||||||
type MonitorHistory struct {
|
type MonitorHistory struct {
|
||||||
Common
|
ID uint64 `gorm:"primaryKey"`
|
||||||
MonitorID uint64
|
CreatedAt time.Time `gorm:"index;<-:create;index:idx_server_id_created_at_monitor_id_avg_delay"`
|
||||||
AvgDelay float32 // 平均延迟,毫秒
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
MonitorID uint64 `gorm:"index:idx_server_id_created_at_monitor_id_avg_delay"`
|
||||||
|
ServerID uint64 `gorm:"index:idx_server_id_created_at_monitor_id_avg_delay"`
|
||||||
|
AvgDelay float32 `gorm:"index:idx_server_id_created_at_monitor_id_avg_delay"` // 平均延迟,毫秒
|
||||||
Up uint64 // 检查状态良好计数
|
Up uint64 // 检查状态良好计数
|
||||||
Down uint64 // 检查状态异常计数
|
Down uint64 // 检查状态异常计数
|
||||||
Data string
|
Data string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -17,6 +17,8 @@ type Server struct {
|
|||||||
Note string `json:"-"` // 管理员可见备注
|
Note string `json:"-"` // 管理员可见备注
|
||||||
DisplayIndex int // 展示排序,越大越靠前
|
DisplayIndex int // 展示排序,越大越靠前
|
||||||
HideForGuest bool // 对游客隐藏
|
HideForGuest bool // 对游客隐藏
|
||||||
|
EnableDDNS bool // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效
|
||||||
|
DDNSDomain string // DDNS中的前缀 如基础域名为abc.oracle DDNSName为mjj 就会把mjj.abc.oracle解析服务器IP 为空则停用
|
||||||
|
|
||||||
Host *Host `gorm:"-"`
|
Host *Host `gorm:"-"`
|
||||||
State *HostState `gorm:"-"`
|
State *HostState `gorm:"-"`
|
||||||
@@ -51,5 +53,6 @@ func (s Server) Marshal() template.JS {
|
|||||||
tag, _ := utils.Json.Marshal(s.Tag)
|
tag, _ := utils.Json.Marshal(s.Tag)
|
||||||
note, _ := utils.Json.Marshal(s.Note)
|
note, _ := utils.Json.Marshal(s.Note)
|
||||||
secret, _ := utils.Json.Marshal(s.Secret)
|
secret, _ := utils.Json.Marshal(s.Secret)
|
||||||
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest))) // #nosec
|
ddnsDomain, _ := utils.Json.Marshal(s.DDNSDomain)
|
||||||
|
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"DDNSDomain": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), ddnsDomain)) // #nosec
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package ddns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProviderCloudflare struct {
|
||||||
|
Secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||||
|
if domainConfig == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneID, err := provider.getZoneID(domainConfig.FullDomain)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("无法获取 zone ID: %s\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当IPv4和IPv6同时成功才算作成功
|
||||||
|
var resultV4 = true
|
||||||
|
var resultV6 = true
|
||||||
|
if domainConfig.EnableIPv4 {
|
||||||
|
if !provider.addDomainRecord(zoneID, domainConfig, true) {
|
||||||
|
resultV4 = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if domainConfig.EnableIpv6 {
|
||||||
|
if !provider.addDomainRecord(zoneID, domainConfig, false) {
|
||||||
|
resultV6 = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultV4 && resultV6
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *DomainConfig, isIpv4 bool) bool {
|
||||||
|
record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain, isIpv4)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("查找 DNS 记录时出错: %s\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if record == nil {
|
||||||
|
// 添加 DNS 记录
|
||||||
|
return provider.createDNSRecord(zoneID, domainConfig, isIpv4)
|
||||||
|
} else {
|
||||||
|
// 更新 DNS 记录
|
||||||
|
return provider.updateDNSRecord(zoneID, record["id"].(string), domainConfig, isIpv4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderCloudflare) getZoneID(domain string) (string, error) {
|
||||||
|
_, realDomain := SplitDomain(domain)
|
||||||
|
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", realDomain)
|
||||||
|
body, err := provider.sendRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := res["result"].([]interface{})
|
||||||
|
if len(result) > 0 {
|
||||||
|
zoneID := result[0].(map[string]interface{})["id"].(string)
|
||||||
|
return zoneID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("找不到 Zone ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) {
|
||||||
|
var ipType = "A"
|
||||||
|
if !isIPv4 {
|
||||||
|
ipType = "AAAA"
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=%s&name=%s", zoneID, ipType, domain)
|
||||||
|
body, err := provider.sendRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := res["result"].([]interface{})
|
||||||
|
if len(result) > 0 {
|
||||||
|
return result[0].(map[string]interface{}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil // 没有找到 DNS 记录
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||||
|
var ipType = "A"
|
||||||
|
var ipAddr = domainConfig.Ipv4Addr
|
||||||
|
if !isIPv4 {
|
||||||
|
ipType = "AAAA"
|
||||||
|
ipAddr = domainConfig.Ipv6Addr
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID)
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"type": ipType,
|
||||||
|
"name": domainConfig.FullDomain,
|
||||||
|
"content": ipAddr,
|
||||||
|
"ttl": 60,
|
||||||
|
"proxied": false,
|
||||||
|
}
|
||||||
|
jsonData, _ := json.Marshal(data)
|
||||||
|
_, err := provider.sendRequest("POST", url, jsonData)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||||
|
var ipType = "A"
|
||||||
|
var ipAddr = domainConfig.Ipv4Addr
|
||||||
|
if !isIPv4 {
|
||||||
|
ipType = "AAAA"
|
||||||
|
ipAddr = domainConfig.Ipv6Addr
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneID, recordID)
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"type": ipType,
|
||||||
|
"name": domainConfig.FullDomain,
|
||||||
|
"content": ipAddr,
|
||||||
|
"ttl": 60,
|
||||||
|
"proxied": false,
|
||||||
|
}
|
||||||
|
jsonData, _ := json.Marshal(data)
|
||||||
|
_, err := provider.sendRequest("PATCH", url, jsonData)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以下为辅助方法,如发送 HTTP 请求等
|
||||||
|
func (provider ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
|
||||||
|
client := &http.Client{}
|
||||||
|
req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", provider.Secret))
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func(Body io.ReadCloser) {
|
||||||
|
err := Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
|
||||||
|
}
|
||||||
|
}(resp.Body)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package ddns
|
||||||
|
|
||||||
|
type DomainConfig struct {
|
||||||
|
EnableIPv4 bool
|
||||||
|
EnableIpv6 bool
|
||||||
|
FullDomain string
|
||||||
|
Ipv4Addr string
|
||||||
|
Ipv6Addr string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
// UpdateDomain Return is updated
|
||||||
|
UpdateDomain(domainConfig *DomainConfig) bool
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ddns
|
||||||
|
|
||||||
|
type ProviderDummy struct{}
|
||||||
|
|
||||||
|
func (provider ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package ddns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (provider ProviderWebHook) FormatWebhookString(s string, config *DomainConfig, ipType string) string {
|
||||||
|
if config == nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
result := strings.TrimSpace(s)
|
||||||
|
result = strings.Replace(s, "{ip}", config.Ipv4Addr, -1)
|
||||||
|
result = strings.Replace(result, "{domain}", config.FullDomain, -1)
|
||||||
|
result = strings.Replace(result, "{type}", ipType, -1)
|
||||||
|
// remove \r
|
||||||
|
result = strings.Replace(result, "\r", "", -1)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetStringHeadersToRequest(req *http.Request, headers []string) {
|
||||||
|
if req == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, element := range headers {
|
||||||
|
kv := strings.SplitN(element, ":", 2)
|
||||||
|
if len(kv) == 2 {
|
||||||
|
req.Header.Add(kv[0], kv[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitDomain 分割域名为前缀和一级域名
|
||||||
|
func SplitDomain(domain string) (prefix string, topLevelDomain string) {
|
||||||
|
// 带有二级TLD的一些常见例子,需要特别处理
|
||||||
|
secondLevelTLDs := map[string]bool{
|
||||||
|
".co.uk": true, ".com.cn": true, ".gov.cn": true, ".net.cn": true, ".org.cn": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分割域名为"."的各部分
|
||||||
|
parts := strings.Split(domain, ".")
|
||||||
|
|
||||||
|
// 处理特殊情况,例如 ".co.uk"
|
||||||
|
for i := len(parts) - 2; i > 0; i-- {
|
||||||
|
potentialTLD := fmt.Sprintf(".%s.%s", parts[i], parts[i+1])
|
||||||
|
if secondLevelTLDs[potentialTLD] {
|
||||||
|
if i > 1 {
|
||||||
|
return strings.Join(parts[:i-1], "."), strings.Join(parts[i-1:], ".")
|
||||||
|
}
|
||||||
|
return "", domain // 当域名仅为二级TLD时,无前缀
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 常规处理,查找最后一个"."前的所有内容作为前缀
|
||||||
|
if len(parts) > 2 {
|
||||||
|
return strings.Join(parts[:len(parts)-2], "."), strings.Join(parts[len(parts)-2:], ".")
|
||||||
|
}
|
||||||
|
return "", domain // 当域名不包含子域名时,无前缀
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package ddns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProviderWebHook struct {
|
||||||
|
URL string
|
||||||
|
RequestMethod string
|
||||||
|
RequestBody string
|
||||||
|
RequestHeader string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||||
|
if domainConfig == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if domainConfig.FullDomain == "" {
|
||||||
|
log.Println("NEZHA>> Failed to update an empty domain")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
updated := false
|
||||||
|
client := &http.Client{}
|
||||||
|
if domainConfig.EnableIPv4 && domainConfig.Ipv4Addr != "" {
|
||||||
|
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv4")
|
||||||
|
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv4")
|
||||||
|
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv4")
|
||||||
|
headers := strings.Split(header, "\n")
|
||||||
|
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
|
||||||
|
if err == nil && req != nil {
|
||||||
|
SetStringHeadersToRequest(req, headers)
|
||||||
|
if _, err := client.Do(req); err != nil {
|
||||||
|
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
|
||||||
|
} else {
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if domainConfig.EnableIpv6 && domainConfig.Ipv6Addr != "" {
|
||||||
|
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv6")
|
||||||
|
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv6")
|
||||||
|
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv6")
|
||||||
|
headers := strings.Split(header, "\n")
|
||||||
|
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
|
||||||
|
if err == nil && req != nil {
|
||||||
|
SetStringHeadersToRequest(req, headers)
|
||||||
|
if _, err := client.Do(req); err != nil {
|
||||||
|
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
|
||||||
|
} else {
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updated
|
||||||
|
}
|
||||||
+7
-5
@@ -12,8 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type AuthorizeOption struct {
|
type AuthorizeOption struct {
|
||||||
Guest bool
|
GuestOnly bool
|
||||||
Member bool
|
MemberOnly bool
|
||||||
IsPage bool
|
IsPage bool
|
||||||
AllowAPI bool
|
AllowAPI bool
|
||||||
Msg string
|
Msg string
|
||||||
@@ -24,7 +24,7 @@ type AuthorizeOption struct {
|
|||||||
func Authorize(opt AuthorizeOption) func(*gin.Context) {
|
func Authorize(opt AuthorizeOption) func(*gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
var code = http.StatusForbidden
|
var code = http.StatusForbidden
|
||||||
if opt.Guest {
|
if opt.GuestOnly {
|
||||||
code = http.StatusBadRequest
|
code = http.StatusBadRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,13 +67,15 @@ func Authorize(opt AuthorizeOption) func(*gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已登录且只能游客访问
|
// 已登录且只能游客访问
|
||||||
if isLogin && opt.Guest {
|
if isLogin && opt.GuestOnly {
|
||||||
ShowErrorPage(c, commonErr, opt.IsPage)
|
ShowErrorPage(c, commonErr, opt.IsPage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未登录且需要登录
|
// 未登录且需要登录
|
||||||
if !isLogin && opt.Member {
|
if !isLogin && opt.MemberOnly {
|
||||||
ShowErrorPage(c, commonErr, opt.IsPage)
|
ShowErrorPage(c, commonErr, opt.IsPage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func CommonEnvironment(c *gin.Context, data map[string]interface{}) gin.H {
|
|||||||
data["MatchedPath"] = c.MustGet("MatchedPath")
|
data["MatchedPath"] = c.MustGet("MatchedPath")
|
||||||
data["Version"] = singleton.Version
|
data["Version"] = singleton.Version
|
||||||
data["Conf"] = singleton.Conf
|
data["Conf"] = singleton.Conf
|
||||||
|
data["Themes"] = model.Themes
|
||||||
// 是否是管理页面
|
// 是否是管理页面
|
||||||
data["IsAdminPage"] = adminPage[data["MatchedPath"].(string)]
|
data["IsAdminPage"] = adminPage[data["MatchedPath"].(string)]
|
||||||
// 站点标题
|
// 站点标题
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package mygin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/naiba/nezha/model"
|
||||||
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
|
"github.com/naiba/nezha/service/singleton"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PreferredTheme(c *gin.Context) {
|
||||||
|
// 采用前端传入的主题
|
||||||
|
if theme, err := c.Cookie("preferred_theme"); err == nil {
|
||||||
|
if _, has := model.Themes[theme]; has {
|
||||||
|
// 检验自定义主题
|
||||||
|
if theme == "custom" && singleton.Conf.Site.Theme != "custom" && !utils.IsFileExists("resource/template/theme-custom/home.html") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set(model.CtxKeyPreferredTheme, theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPreferredTheme(c *gin.Context, path string) string {
|
||||||
|
if theme, has := c.Get(model.CtxKeyPreferredTheme); has {
|
||||||
|
return fmt.Sprintf("theme-%s%s", theme, path)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("theme-%s%s", singleton.Conf.Site.Theme, path)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package mygin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/naiba/nezha/model"
|
||||||
|
"github.com/naiba/nezha/service/singleton"
|
||||||
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ValidateViewPasswordOption struct {
|
||||||
|
IsPage bool
|
||||||
|
AbortWhenFail bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateViewPassword(opt ValidateViewPasswordOption) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if singleton.Conf.Site.ViewPassword == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, authorized := c.Get(model.CtxKeyAuthorizedUser)
|
||||||
|
if authorized {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewPassword, err := c.Cookie(singleton.Conf.Site.CookieName + "-vp")
|
||||||
|
if err == nil {
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(viewPassword), []byte(singleton.Conf.Site.ViewPassword))
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
c.Set(model.CtxKeyViewPasswordVerified, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !opt.AbortWhenFail {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opt.IsPage {
|
||||||
|
c.HTML(http.StatusOK, GetPreferredTheme(c, "/viewpassword"), CommonEnvironment(c, gin.H{
|
||||||
|
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
|
||||||
|
"CustomCode": singleton.Conf.Site.CustomCode,
|
||||||
|
}))
|
||||||
|
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, model.Response{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: "访问受限",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+23
-2
@@ -398,7 +398,7 @@ other = "Virtualization"
|
|||||||
other = "Swap"
|
other = "Swap"
|
||||||
|
|
||||||
[NetTransfer]
|
[NetTransfer]
|
||||||
other = "Network Transfer"
|
other = "Transfer"
|
||||||
|
|
||||||
[Load]
|
[Load]
|
||||||
other = "Load"
|
other = "Load"
|
||||||
@@ -419,7 +419,7 @@ other = "Last Active"
|
|||||||
other = "Version"
|
other = "Version"
|
||||||
|
|
||||||
[NetSpeed]
|
[NetSpeed]
|
||||||
other = "Network Speed"
|
other = "Speed"
|
||||||
|
|
||||||
[Uptime]
|
[Uptime]
|
||||||
other = "Uptime"
|
other = "Uptime"
|
||||||
@@ -609,3 +609,24 @@ other = "Hide for Guest"
|
|||||||
|
|
||||||
[Menu]
|
[Menu]
|
||||||
other = "Menu"
|
other = "Menu"
|
||||||
|
|
||||||
|
[NetworkSpiter]
|
||||||
|
other = "Network"
|
||||||
|
|
||||||
|
[EnableShowInService]
|
||||||
|
other = "Enable Show in Service"
|
||||||
|
|
||||||
|
[EnableDDNS]
|
||||||
|
other = "Enable DDNS"
|
||||||
|
|
||||||
|
[DDNSDomain]
|
||||||
|
other = "DDNS Domain"
|
||||||
|
|
||||||
|
[Feature]
|
||||||
|
other = "Feature"
|
||||||
|
|
||||||
|
[Template]
|
||||||
|
other = "Template"
|
||||||
|
|
||||||
|
[Stat]
|
||||||
|
other = "Stat"
|
||||||
|
|||||||
Vendored
+21
@@ -609,3 +609,24 @@ other = "Ocultar para Invitados"
|
|||||||
|
|
||||||
[Menu]
|
[Menu]
|
||||||
other = "Menú"
|
other = "Menú"
|
||||||
|
|
||||||
|
[NetworkSpiter]
|
||||||
|
other = "Red"
|
||||||
|
|
||||||
|
[EnableShowInService]
|
||||||
|
other = "Mostrar en servicio"
|
||||||
|
|
||||||
|
[EnableDDNS]
|
||||||
|
other = "Habilitar DDNS"
|
||||||
|
|
||||||
|
[DDNSDomain]
|
||||||
|
other = "Dominio DDNS"
|
||||||
|
|
||||||
|
[Feature]
|
||||||
|
other = "Característica"
|
||||||
|
|
||||||
|
[Template]
|
||||||
|
other = "Plantilla"
|
||||||
|
|
||||||
|
[Stat]
|
||||||
|
other = "Stat"
|
||||||
|
|||||||
Vendored
+21
@@ -609,3 +609,24 @@ other = "对游客隐藏"
|
|||||||
|
|
||||||
[Menu]
|
[Menu]
|
||||||
other = "菜单"
|
other = "菜单"
|
||||||
|
|
||||||
|
[NetworkSpiter]
|
||||||
|
other = "网络"
|
||||||
|
|
||||||
|
[EnableShowInService]
|
||||||
|
other = "在服务中显示"
|
||||||
|
|
||||||
|
[EnableDDNS]
|
||||||
|
other = "启用DDNS"
|
||||||
|
|
||||||
|
[DDNSDomain]
|
||||||
|
other = "DDNS域名"
|
||||||
|
|
||||||
|
[Feature]
|
||||||
|
other = "功能"
|
||||||
|
|
||||||
|
[Template]
|
||||||
|
other = "主题"
|
||||||
|
|
||||||
|
[Stat]
|
||||||
|
other = "信息"
|
||||||
|
|||||||
Vendored
+21
@@ -609,3 +609,24 @@ other = "對遊客隱藏"
|
|||||||
|
|
||||||
[Menu]
|
[Menu]
|
||||||
other = "菜單"
|
other = "菜單"
|
||||||
|
|
||||||
|
[NetworkSpiter]
|
||||||
|
other = "網絡"
|
||||||
|
|
||||||
|
[EnableShowInService]
|
||||||
|
other = "在服務中顯示"
|
||||||
|
|
||||||
|
[EnableDDNS]
|
||||||
|
other = "啟用DDNS"
|
||||||
|
|
||||||
|
[DDNSDomain]
|
||||||
|
other = "DDNS網域"
|
||||||
|
|
||||||
|
[Feature]
|
||||||
|
other = "功能"
|
||||||
|
|
||||||
|
[Template]
|
||||||
|
other = "主題"
|
||||||
|
|
||||||
|
[Stat]
|
||||||
|
other = "信息"
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ function addOrEditServer(server, conf) {
|
|||||||
modal.find("input[name=id]").val(server ? server.ID : null);
|
modal.find("input[name=id]").val(server ? server.ID : null);
|
||||||
modal.find("input[name=name]").val(server ? server.Name : null);
|
modal.find("input[name=name]").val(server ? server.Name : null);
|
||||||
modal.find("input[name=Tag]").val(server ? server.Tag : null);
|
modal.find("input[name=Tag]").val(server ? server.Tag : null);
|
||||||
|
modal.find("input[name=DDNSDomain]").val(server ? server.DDNSDomain : null);
|
||||||
modal
|
modal
|
||||||
.find("input[name=DisplayIndex]")
|
.find("input[name=DisplayIndex]")
|
||||||
.val(server ? server.DisplayIndex : null);
|
.val(server ? server.DisplayIndex : null);
|
||||||
@@ -321,6 +322,11 @@ function addOrEditServer(server, conf) {
|
|||||||
} else {
|
} else {
|
||||||
modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
|
modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
|
||||||
}
|
}
|
||||||
|
if (server && server.EnableDDNS) {
|
||||||
|
modal.find(".ui.enableddns.checkbox").checkbox("set checked");
|
||||||
|
} else {
|
||||||
|
modal.find(".ui.enableddns.checkbox").checkbox("set unchecked");
|
||||||
|
}
|
||||||
showFormModal(".server.modal", "#serverForm", "/api/server");
|
showFormModal(".server.modal", "#serverForm", "/api/server");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +345,11 @@ function addOrEditMonitor(monitor) {
|
|||||||
modal.find("select[name=Type]").val(monitor ? monitor.Type : 1);
|
modal.find("select[name=Type]").val(monitor ? monitor.Type : 1);
|
||||||
modal.find("select[name=Cover]").val(monitor ? monitor.Cover : 0);
|
modal.find("select[name=Cover]").val(monitor ? monitor.Cover : 0);
|
||||||
modal.find("input[name=NotificationTag]").val(monitor ? monitor.NotificationTag : null);
|
modal.find("input[name=NotificationTag]").val(monitor ? monitor.NotificationTag : null);
|
||||||
|
if (monitor && monitor.EnableShowInService) {
|
||||||
|
modal.find(".ui.nb-show-in-service.checkbox").checkbox("set checked")
|
||||||
|
} else {
|
||||||
|
modal.find(".ui.nb-show-in-service.checkbox").checkbox("set unchecked")
|
||||||
|
}
|
||||||
if (monitor && monitor.Notify) {
|
if (monitor && monitor.Notify) {
|
||||||
modal.find(".ui.nb-notify.checkbox").checkbox("set checked");
|
modal.find(".ui.nb-notify.checkbox").checkbox("set checked");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+217
@@ -0,0 +1,217 @@
|
|||||||
|
/* 屏幕适配 */
|
||||||
|
@media only screen and (min-width:1200px) {
|
||||||
|
.ui.container {
|
||||||
|
width:95% !important;
|
||||||
|
font-size: 90% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media only screen and (max-width:767px) {
|
||||||
|
.ui.card>.content>.header:not(.ui),.ui.cards>.card>.content>.header:not(.ui) {
|
||||||
|
margin-top:0.4em !important;
|
||||||
|
}
|
||||||
|
.ui.menu .item>img:not(.ui){
|
||||||
|
width: 2.2rem;
|
||||||
|
}
|
||||||
|
.ui.menu .item:before{
|
||||||
|
width:0.5px;
|
||||||
|
}
|
||||||
|
.ui.menu .item{
|
||||||
|
padding: 0.9rem 0.55rem;
|
||||||
|
}
|
||||||
|
.ui.large.menu{
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i.icon {
|
||||||
|
color:#000;
|
||||||
|
width:1.2em !important;
|
||||||
|
}
|
||||||
|
i.fi {
|
||||||
|
width:0.9em;
|
||||||
|
margin:0px 6px 0px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
content:" " !important;
|
||||||
|
background:fixed !important;
|
||||||
|
z-index:-1 !important;
|
||||||
|
top:0 !important;
|
||||||
|
right:0 !important;
|
||||||
|
bottom:0 !important;
|
||||||
|
left:0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nb-container {
|
||||||
|
padding-top: 75px;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-bottom: 65px;
|
||||||
|
margin-bottom: -47px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app .ui.fluid.accordion {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login.nb-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login.nb-container > .grid {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login.nb-container > .grid .column {
|
||||||
|
max-width: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.menu .item-right:before{
|
||||||
|
width:0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.cards .flag {
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.cards .header > .info.icon {
|
||||||
|
float: right;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.grid {
|
||||||
|
margin-bottom:-0.5em
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.card>.content>.header:not(.ui), .ui.cards>.card>.content>.header:not(.ui){
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.cards .wide.column {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
height:2.3rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.cards .wide.column:nth-child(1) {
|
||||||
|
margin-top:1.2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.cards .wide.column:nth-child(2) {
|
||||||
|
margin-top:1.2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.cards .three.wide.column {
|
||||||
|
text-align: center;
|
||||||
|
width: 22%!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.cards .thirteen.wide.column{
|
||||||
|
width: 78%!important;
|
||||||
|
padding-left:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.cards .description {
|
||||||
|
padding-bottom:0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.cards .flag {
|
||||||
|
margin-right:0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.cards .header > .info.icon {
|
||||||
|
float: right;
|
||||||
|
margin-right:0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.popup:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closePopup{
|
||||||
|
color:rgb(10, 148, 242) !important;
|
||||||
|
position: absolute;
|
||||||
|
top: 7px;
|
||||||
|
right: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.content {
|
||||||
|
margin:0 !important;
|
||||||
|
padding:1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.cards .ui.content.popup {
|
||||||
|
min-width:calc(100%)!important;
|
||||||
|
line-height:2rem !important;
|
||||||
|
border-radius:5px !important;
|
||||||
|
border:1px solid transparent !important;
|
||||||
|
font-family:Arial,Helvetica,sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.cards .outline.icon {
|
||||||
|
margin-right:1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.progress .bar {
|
||||||
|
min-width:1.8em !important;
|
||||||
|
border-radius:5px !important;
|
||||||
|
line-height:1.65em !important;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 0.4em;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-weight: 700;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status .delay-today {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status .delay-today > i {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.2em;
|
||||||
|
height: 1.2em;
|
||||||
|
border-radius: 0.6em;
|
||||||
|
background-color: grey;
|
||||||
|
margin-right: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status .danger {
|
||||||
|
background-color: crimson !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status .good {
|
||||||
|
background-color: rgb(10, 148, 242) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status .warning {
|
||||||
|
background-color: orange !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nezha-primary-btn {
|
||||||
|
background-color: #0338d6 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nezha-primary-font {
|
||||||
|
color: #0338d6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nezha-secondary-font {
|
||||||
|
height: 1em;
|
||||||
|
color: rgb(10, 148, 242) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-alerts.top-center {
|
||||||
|
z-index: 99999999;
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
const mixinsVue = {
|
||||||
|
delimiters: ['@#', '#@'],
|
||||||
|
data: {
|
||||||
|
preferredTemplate: null,
|
||||||
|
isMobile: false
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.isMobile = this.checkIsMobile();
|
||||||
|
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleTemplate(template) {
|
||||||
|
if( template != this.preferredTemplate){
|
||||||
|
this.preferredTemplate = template;
|
||||||
|
this.updateCookie("preferred_theme", template);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateCookie(name, value) {
|
||||||
|
document.cookie = name + "=" + value +"; path=/";
|
||||||
|
},
|
||||||
|
getCookie(name) {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
let cookieValue = null;
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
if (cookie.startsWith(name + '=')) {
|
||||||
|
cookieValue = cookie.substring(name.length + 1, cookie.length);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
},
|
||||||
|
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
|
||||||
|
return window.innerWidth <= 768;
|
||||||
|
},
|
||||||
|
logOut(id) {
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: '/api/logout',
|
||||||
|
data: JSON.stringify({ id: id }),
|
||||||
|
contentType: 'application/json',
|
||||||
|
success: function (resp) {
|
||||||
|
if (resp.code == 200) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('注销失败(Error ' + resp.code + '): ' + resp.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (err) {
|
||||||
|
alert('网络错误: ' + err.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+66
-64
File diff suppressed because one or more lines are too long
+57
-26
File diff suppressed because one or more lines are too long
+183
-30
@@ -6,58 +6,120 @@ body {
|
|||||||
/* 导航部分 开始*/
|
/* 导航部分 开始*/
|
||||||
.navbar {
|
.navbar {
|
||||||
min-height: 40px !important;
|
min-height: 40px !important;
|
||||||
|
margin:0 auto;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-inner{
|
.navbar .container{
|
||||||
|
max-width: 95vw;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pl-md-unset {
|
.navbar-inverse{
|
||||||
max-width: 95vw;
|
background-image: none;
|
||||||
|
background-color: #1C2127;
|
||||||
|
box-shadow: 0 1px 40px -8px #00000080;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-collapse:not([aria-expanded]) .navbar-nav .dropdown-toggle {
|
.navbar-inverse .navbar-toggle:focus,
|
||||||
|
.navbar-inverse .navbar-toggle:hover {
|
||||||
|
background-color: #1C2127;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-collapse:not([aria-expanded]) .navbar-nav .dropdown-toggle {
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-toggle {
|
.navbar .navbar-toggle {
|
||||||
margin-right:0
|
margin-right:0
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar .navbar-brand {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding:12px 0 0 0;
|
padding:12px 0 0 0;
|
||||||
|
margin-right:30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-cell-expand {
|
.navbar .node-cell-expand {
|
||||||
max-width: 420px;
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-cell-expand-label {
|
.navbar .node-cell-expand-label {
|
||||||
margin-right: 5px;
|
/*margin-right: 5px;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown .dropdown-toggle {
|
.navbar .dropdown .dropdown-toggle {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-inverse, .nav.navbar-nav {
|
.navbar .navbar-nav {
|
||||||
background-image: linear-gradient(rgb(60, 60, 60) 0px, rgb(34, 34, 34) 100%) !important;
|
margin:0px -15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-inverse .navbar-nav>li>a {
|
.navbar .navbar-nav>li>a {
|
||||||
color:#f1f1f1;
|
color:#f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-inverse .navbar-brand {
|
.navbar-nav li a span{
|
||||||
font-size: 20px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-collapse{
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 导航部分 结束 */
|
/* 导航部分 结束 */
|
||||||
|
|
||||||
|
|
||||||
|
/* toolbox 开始 */
|
||||||
|
|
||||||
|
.toolbox {
|
||||||
|
position: fixed;
|
||||||
|
bottom:20px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox span{
|
||||||
|
display: block;
|
||||||
|
width: 2.75rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox i{
|
||||||
|
display: block;
|
||||||
|
color: rgba(241,241,241,1);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
width: 2.75rem;
|
||||||
|
line-height: 2.75rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox .toggleView i.show-nogroup {
|
||||||
|
font-size: 1.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox .setTheme i.setTheme-dark {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox .setTheme i.setTheme-light {
|
||||||
|
font-size: 1.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox .showGoTop i.goTop {
|
||||||
|
font-size: 1.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* toolbox 结束 */
|
||||||
|
|
||||||
|
|
||||||
/* 正文部分 开始 */
|
/* 正文部分 开始 */
|
||||||
.content {
|
.content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -75,15 +137,40 @@ body {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table > tbody > tr > td,
|
||||||
|
.table > tbody > tr > th,
|
||||||
|
.table > tfoot > tr > td,
|
||||||
|
.table > tfoot > tr > th,
|
||||||
|
.table > thead > tr > td,
|
||||||
.table > thead > tr > th {
|
.table > thead > tr > th {
|
||||||
|
position: relative;
|
||||||
border:none;
|
border:none;
|
||||||
|
line-height:20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table .node-group-tag {
|
.table > tbody > tr > td:before,
|
||||||
|
.table > tbody > tr > th:before,
|
||||||
|
.table > tfoot > tr > td:before,
|
||||||
|
.table > tfoot > tr > th:before,
|
||||||
|
.table > thead > tr > td:before,
|
||||||
|
.table > thead > tr > th:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0.7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table .node-group-tag th{
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
padding-bottom:15px;
|
padding-bottom:15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table .node-cell-os-text {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
@@ -93,7 +180,6 @@ body {
|
|||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.expandRow > td {
|
.expandRow > td {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
border-top: 0 !important;
|
border-top: 0 !important;
|
||||||
@@ -131,25 +217,49 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.node-cell.network {
|
.node-cell.network {
|
||||||
min-width: 110px;
|
min-width: 100px;
|
||||||
max-width: 110px;
|
max-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-cell.cpu, .node-cell.ram, .node-cell.hdd {
|
.node-cell.traffic {
|
||||||
min-width: 45px;
|
min-width: 100px;
|
||||||
max-width: 90px;
|
}
|
||||||
|
|
||||||
|
.node-cell.cpu, .node-cell.ram, .node-cell.hdd, .node-cell.memory {
|
||||||
|
min-width: 50px;
|
||||||
|
max-width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*正文结束*/
|
/*正文结束*/
|
||||||
|
|
||||||
/* 服务页 正文*/
|
/* 服务页 正文*/
|
||||||
|
.service-status {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status .service-status-th{
|
||||||
|
min-width:60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status .service-name-th{
|
||||||
|
min-width:50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status .service-averagelatency-th{
|
||||||
|
min-width:80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status .service-30daysonline-th{
|
||||||
|
min-width:80px;
|
||||||
|
}
|
||||||
|
|
||||||
.service-status .delay-today {
|
.service-status .delay-today {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-status .delay-today > i {
|
.service-status .delay-today i {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -163,26 +273,29 @@ body {
|
|||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
margin-bottom: -3.25px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: inset 0 2px 2px rgba(0, 0, 0, .1);
|
box-shadow: inset 0 2px 2px rgba(0, 0, 0, .1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-status {
|
.service-status .tooltip-inner {
|
||||||
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* 服务页 正文结束 */
|
/* 服务页 正文结束 */
|
||||||
@media only screen and (max-width: 1200px) {
|
@media only screen and (max-width: 1200px) {
|
||||||
.accordian-body{
|
.accordian-body{
|
||||||
margin: 5px 0px 5px 10px;
|
margin: 5px 0px 5px 10px;
|
||||||
}
|
}
|
||||||
.table .node-group-tag {
|
.table .node-group-tag th{
|
||||||
font-size:16px;
|
font-size:16px;
|
||||||
padding-bottom:6px;
|
padding-bottom:6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 720px) {
|
@media only screen and (max-width: 767px) {
|
||||||
body {
|
body {
|
||||||
font-size: 10px !important;
|
font-size: 10px !important;
|
||||||
padding-top:60px !important;
|
padding-top:60px !important;
|
||||||
@@ -191,6 +304,23 @@ body {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
.navbar .navbar-nav .open .dropdown-menu>li>a {
|
||||||
|
color: #f1f1f1;
|
||||||
|
}
|
||||||
|
.navbar .navbar-nav .open .dropdown-menu {
|
||||||
|
list-style-image: initial;
|
||||||
|
background-color: #181a1b;
|
||||||
|
border-color: rgba(140, 130, 115, 0.15);
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.18) 0px 6px 12px;
|
||||||
|
}
|
||||||
|
.table > tbody > tr > td:before,
|
||||||
|
.table > tbody > tr > th:before,
|
||||||
|
.table > tfoot > tr > td:before,
|
||||||
|
.table > tfoot > tr > th:before,
|
||||||
|
.table > thead > tr > td:before,
|
||||||
|
.table > thead > tr > th:before {
|
||||||
|
height: 0.5px;
|
||||||
|
}
|
||||||
.node-cell.os,
|
.node-cell.os,
|
||||||
.node-cell.uptime,
|
.node-cell.uptime,
|
||||||
.node-cell.traffic{
|
.node-cell.traffic{
|
||||||
@@ -208,10 +338,33 @@ body {
|
|||||||
.accordian-body{
|
.accordian-body{
|
||||||
margin: 5px 0px 5px 10px;
|
margin: 5px 0px 5px 10px;
|
||||||
}
|
}
|
||||||
.table .node-group-tag {
|
.table .node-group-tag th{
|
||||||
font-size:12px;
|
font-size:16px;
|
||||||
padding-bottom:6px;
|
padding-bottom:6px;
|
||||||
}
|
}
|
||||||
|
.service-status .service-status-th{
|
||||||
|
min-width:30px;
|
||||||
|
}
|
||||||
|
.service-status .delay-today{
|
||||||
|
margin-top:4px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.service-status .delay-today i{
|
||||||
|
margin-right:0px;
|
||||||
|
}
|
||||||
|
.service-status .delay-today-text{
|
||||||
|
display: none;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.service-status .service-averagelatency-th{
|
||||||
|
min-width:70px;
|
||||||
|
}
|
||||||
|
.service-status .service-30daysonline-th{
|
||||||
|
min-width:75px;
|
||||||
|
}
|
||||||
|
.toolbox {
|
||||||
|
right: 18px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: 768px) {
|
@media only screen and (min-width: 768px) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+102
-10
@@ -2,27 +2,74 @@ const mixinsVue = {
|
|||||||
data: {
|
data: {
|
||||||
cache: [],
|
cache: [],
|
||||||
theme: "light",
|
theme: "light",
|
||||||
isSystemTheme: false
|
isSystemTheme: false,
|
||||||
|
showGroup: false,
|
||||||
|
showGoTop: false,
|
||||||
|
preferredTemplate: null,
|
||||||
|
isMobile: false
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.initTheme()
|
this.isMobile = this.checkIsMobile();
|
||||||
|
this.initTheme();
|
||||||
|
this.storedShowGroup();
|
||||||
|
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
|
||||||
|
window.addEventListener('scroll', this.handleScroll);
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
window.removeEventListener('scroll', this.handleScroll);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleView() {
|
||||||
|
this.showGroup = !this.showGroup;
|
||||||
|
localStorage.setItem("showGroup", JSON.stringify(this.showGroup));
|
||||||
|
return this.showGroup;
|
||||||
|
},
|
||||||
|
storedShowGroup() {
|
||||||
|
const storedShowGroup = localStorage.getItem("showGroup");
|
||||||
|
if (storedShowGroup !== null) {
|
||||||
|
this.showGroup = JSON.parse(storedShowGroup);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleTemplate(template) {
|
||||||
|
if( template != this.preferredTemplate){
|
||||||
|
this.preferredTemplate = template;
|
||||||
|
this.updateCookie("preferred_theme", template);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateCookie(name, value) {
|
||||||
|
document.cookie = name + "=" + value +"; path=/";
|
||||||
|
},
|
||||||
|
getCookie(name) {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
let cookieValue = null;
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
if (cookie.startsWith(name + '=')) {
|
||||||
|
cookieValue = cookie.substring(name.length + 1, cookie.length);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
},
|
||||||
setTheme(title, store = false) {
|
setTheme(title, store = false) {
|
||||||
this.theme = title
|
this.theme = title;
|
||||||
document.body.setAttribute("theme", title)
|
document.body.setAttribute("theme", title);
|
||||||
if (store) {
|
if (store) {
|
||||||
localStorage.setItem("theme", title)
|
localStorage.setItem("theme", title);
|
||||||
this.isSystemTheme = false
|
this.isSystemTheme = false;
|
||||||
|
if(this.$root.page == 'index') {
|
||||||
|
this.$root.reloadCharts(); //重新载入echarts图表
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setSystemTheme() {
|
setSystemTheme() {
|
||||||
localStorage.removeItem("theme")
|
localStorage.removeItem("theme");
|
||||||
this.initTheme()
|
this.initTheme();
|
||||||
this.isSystemTheme = true
|
this.isSystemTheme = true;
|
||||||
},
|
},
|
||||||
initTheme() {
|
initTheme() {
|
||||||
const storeTheme = localStorage.getItem("theme")
|
const storeTheme = localStorage.getItem("theme");
|
||||||
if (storeTheme === 'dark' || storeTheme === 'light') {
|
if (storeTheme === 'dark' || storeTheme === 'light') {
|
||||||
this.setTheme(storeTheme, true);
|
this.setTheme(storeTheme, true);
|
||||||
} else {
|
} else {
|
||||||
@@ -45,5 +92,50 @@ const mixinsVue = {
|
|||||||
toFixed2(f) {
|
toFixed2(f) {
|
||||||
return f.toFixed(2)
|
return f.toFixed(2)
|
||||||
},
|
},
|
||||||
|
logOut(id) {
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: '/api/logout',
|
||||||
|
data: JSON.stringify({ id: id }),
|
||||||
|
contentType: 'application/json',
|
||||||
|
success: function (resp) {
|
||||||
|
if (resp.code == 200) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('注销失败(Error ' + resp.code + '): ' + resp.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (err) {
|
||||||
|
alert('网络错误: ' + err.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
goTop() {
|
||||||
|
$('html, body').animate({ scrollTop: 0 }, 400);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
handleScroll() {
|
||||||
|
this.showGoTop = window.scrollY >= 100;
|
||||||
|
},
|
||||||
|
groupingData(data, field) {
|
||||||
|
let map = new Map();
|
||||||
|
let dest = [];
|
||||||
|
|
||||||
|
data.forEach(item => {
|
||||||
|
if (!map.has(item[field])) {
|
||||||
|
dest.push({
|
||||||
|
[field]: item[field],
|
||||||
|
data: [item]
|
||||||
|
});
|
||||||
|
map.set(item[field], item);
|
||||||
|
} else {
|
||||||
|
dest.find(dItem => dItem[field] === item[field]).data.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return dest;
|
||||||
|
},
|
||||||
|
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
|
||||||
|
return window.innerWidth <= 768;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
+4
-4
@@ -6,11 +6,11 @@
|
|||||||
{{.Version}}</small>
|
{{.Version}}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
|
||||||
<script src="/static/semantic-ui-alerts.min.js"></script>
|
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||||
<script src="/static/main.js?v20230614"></script>
|
<script src="/static/main.js?v20240224"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
updateLang({{.LANG }});
|
updateLang({{.LANG }});
|
||||||
|
|||||||
Vendored
+2
-2
@@ -9,8 +9,8 @@
|
|||||||
<meta content="telephone=no" name="format-detection">
|
<meta content="telephone=no" name="format-detection">
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
<link rel="stylesheet" type="text/css"
|
<link rel="stylesheet" type="text/css"
|
||||||
href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css">
|
href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.css">
|
||||||
<link href="https://cdn.staticfile.org/font-logos/0.17/font-logos.min.css" type="text/css"
|
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-logos/0.17/font-logos.min.css" type="text/css"
|
||||||
rel="stylesheet" />
|
rel="stylesheet" />
|
||||||
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
|
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314">
|
<link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314">
|
||||||
|
|||||||
Vendored
+1
@@ -15,6 +15,7 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<a class='item{{if eq .MatchedPath "/"}} active{{end}}' href="/"><i class="home icon"></i>{{tr "Home"}}</a>
|
<a class='item{{if eq .MatchedPath "/"}} active{{end}}' href="/"><i class="home icon"></i>{{tr "Home"}}</a>
|
||||||
<a class='item{{if eq .MatchedPath "/service"}} active{{end}}' href="/service"><i class="rss icon"></i>{{tr "Services"}}</a>
|
<a class='item{{if eq .MatchedPath "/service"}} active{{end}}' href="/service"><i class="rss icon"></i>{{tr "Services"}}</a>
|
||||||
|
<a class='item{{if eq .MatchedPath "/network"}} active{{end}}' href="/network"><i class="server icon"></i>{{tr "NetworkSpiter"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="right menu">
|
<div class="right menu">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
|
|||||||
+7
@@ -24,6 +24,12 @@
|
|||||||
<option value="3">TCP-Ping</option>
|
<option value="3">TCP-Ping</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui nb-show-in-service checkbox">
|
||||||
|
<input name="EnableShowInService" type="checkbox" tabindex="0" class="hidden" />
|
||||||
|
<label>{{tr "EnableShowInService"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{tr "Duration"}}</label>
|
<label>{{tr "Duration"}}</label>
|
||||||
<input type="number" name="Duration" placeholder="{{tr "Seconds"}}" />
|
<input type="number" name="Duration" placeholder="{{tr "Seconds"}}" />
|
||||||
@@ -110,3 +116,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
+10
@@ -26,6 +26,16 @@
|
|||||||
<label>{{tr "HideForGuest"}}</label>
|
<label>{{tr "HideForGuest"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui enableddns checkbox">
|
||||||
|
<input name="EnableDDNS" type="checkbox" tabindex="0" />
|
||||||
|
<label>{{tr "EnableDDNS"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "DDNSDomain"}}</label>
|
||||||
|
<input type="text" name="DDNSDomain" placeholder="{{tr "DDNSDomain"}}">
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{tr "Note"}}</label>
|
<label>{{tr "Note"}}</label>
|
||||||
<textarea name="Note"></textarea>
|
<textarea name="Note"></textarea>
|
||||||
|
|||||||
+5
-1
@@ -29,6 +29,8 @@
|
|||||||
<th>IP</th>
|
<th>IP</th>
|
||||||
<th>{{tr "VersionNumber"}}</th>
|
<th>{{tr "VersionNumber"}}</th>
|
||||||
<th>{{tr "HideForGuest"}}</th>
|
<th>{{tr "HideForGuest"}}</th>
|
||||||
|
<th>{{tr "EnableDDNS"}}</th>
|
||||||
|
<th>{{tr "DDNSDomain"}}</th>
|
||||||
<th>{{tr "Secret"}}</th>
|
<th>{{tr "Secret"}}</th>
|
||||||
<th>{{tr "OneKeyInstall"}}</th>
|
<th>{{tr "OneKeyInstall"}}</th>
|
||||||
<th>{{tr "Note"}}</th>
|
<th>{{tr "Note"}}</th>
|
||||||
@@ -45,6 +47,8 @@
|
|||||||
<td>{{$server.Host.IP}}</td>
|
<td>{{$server.Host.IP}}</td>
|
||||||
<td>{{$server.Host.Version}}</td>
|
<td>{{$server.Host.Version}}</td>
|
||||||
<td>{{$server.HideForGuest}}</td>
|
<td>{{$server.HideForGuest}}</td>
|
||||||
|
<td>{{$server.EnableDDNS}}</td>
|
||||||
|
<td>{{$server.DDNSDomain}}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
|
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
|
||||||
<i class="copy icon"></i>
|
<i class="copy icon"></i>
|
||||||
@@ -85,7 +89,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{template "component/server" .}}
|
{{template "component/server" .}}
|
||||||
{{template "common/footer" .}}
|
{{template "common/footer" .}}
|
||||||
<script src="https://cdn.staticfile.org/clipboard.js/2.0.10/clipboard.min.js" type="application/javascript"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/clipboard.js/2.0.10/clipboard.min.js" type="application/javascript"></script>
|
||||||
<script>
|
<script>
|
||||||
var clipboard = new ClipboardJS('.ui.icon.green.mini.button');
|
var clipboard = new ClipboardJS('.ui.icon.green.mini.button');
|
||||||
const checkBoxList = document.querySelectorAll('tbody > tr > td > input.nezha-servers[type=checkbox]')
|
const checkBoxList = document.querySelectorAll('tbody > tr > td > input.nezha-servers[type=checkbox]')
|
||||||
|
|||||||
+4
-4
@@ -8,7 +8,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>tty@{{.ServerName}} - {{.Title}}</title>
|
<title>tty@{{.ServerName}} - {{.Title}}</title>
|
||||||
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
||||||
<link href="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/xterm.css" type="text/css" rel="stylesheet"/>
|
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/xterm.css" type="text/css" rel="stylesheet"/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -28,9 +28,9 @@
|
|||||||
|
|
||||||
<body onresize="onResize()">
|
<body onresize="onResize()">
|
||||||
<div id="terminal-container"></div>
|
<div id="terminal-container"></div>
|
||||||
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/xterm.js" type="application/javascript"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/xterm.js" type="application/javascript"></script>
|
||||||
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/addons/attach/xterm-addon-attach.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/addons/attach/xterm-addon-attach.js"></script>
|
||||||
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/addons/fit/xterm-addon-fit.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/addons/fit/xterm-addon-fit.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let sendResizing = false;
|
let sendResizing = false;
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -8,11 +8,11 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
|
||||||
<script src="/static/semantic-ui-alerts.min.js"></script>
|
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||||
<script src="/static/main.js?v20230614"></script>
|
<script src="/static/main.js?v20240224"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
updateLang({{.LANG }});
|
updateLang({{.LANG }});
|
||||||
|
|||||||
+3
-3
@@ -9,10 +9,10 @@
|
|||||||
<meta content="telephone=no" name="format-detection">
|
<meta content="telephone=no" name="format-detection">
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
<link rel="stylesheet" type="text/css"
|
<link rel="stylesheet" type="text/css"
|
||||||
href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css">
|
href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.css">
|
||||||
<link href="https://cdn.staticfile.org/font-logos/0.17/font-logos.min.css" type="text/css"
|
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-logos/0.17/font-logos.min.css" type="text/css"
|
||||||
rel="stylesheet" />
|
rel="stylesheet" />
|
||||||
<link href="https://cdn.staticfile.org/bootstrap-icons/1.10.3/font/bootstrap-icons.css" type="text/css"
|
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/bootstrap-icons/1.8.1/font/bootstrap-icons.min.css" type="text/css"
|
||||||
rel="stylesheet" />
|
rel="stylesheet" />
|
||||||
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
|
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314">
|
<link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314">
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
<div v-for="server in group.data" :id="server.ID" class="ui card">
|
<div v-for="server in group.data" :id="server.ID" class="ui card">
|
||||||
<div class="content" v-if="server.Host" style="margin-top: 10px; padding-bottom: 5px">
|
<div class="content" v-if="server.Host" style="margin-top: 10px; padding-bottom: 5px">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img v-if="server.Host.CountryCode" style="border-radius: 50%;box-shadow:-1px -1px 2px #eee, 1px 1px 2px #000;width:19px;" :src="'https://cdn.staticfile.org/flag-icon-css/6.6.5/flags/1x1/'+server.Host.CountryCode + '.svg'" alt="国家"/> <i v-if='server.Host.Platform == "darwin"'
|
<img v-if="server.Host.CountryCode" style="border-radius: 50%;box-shadow:-1px -1px 2px #eee, 1px 1px 2px #000;width:19px;" :src="'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/flag-icon-css/4.1.5/flags/1x1/'+server.Host.CountryCode + '.svg'" alt="国家"/> <i v-if='server.Host.Platform == "darwin"'
|
||||||
class="apple icon"></i><i v-else-if='isWindowsPlatform(server.Host.Platform)'
|
class="apple icon"></i><i v-else-if='isWindowsPlatform(server.Host.Platform)'
|
||||||
class="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
|
class="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
|
||||||
@#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
|
@#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
|
||||||
|
|||||||
+5
-4
@@ -10,8 +10,8 @@
|
|||||||
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/theme-daynight/css/main.css?v202108042286">
|
<link rel="stylesheet" href="/static/theme-daynight/css/main.css?v202108042286">
|
||||||
<link href="https://cdn.staticfile.org/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
|
||||||
|
|
||||||
{{if ts .CustomCode}}
|
{{if ts .CustomCode}}
|
||||||
{{.CustomCode|safe}}
|
{{.CustomCode|safe}}
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">{{tr "Home"}}</a></li>
|
<li><a href="/">{{tr "Home"}}</a></li>
|
||||||
<li><a href="/service">{{tr "Services"}}</a></li>
|
<li><a href="/service">{{tr "Services"}}</a></li>
|
||||||
|
<li><a href="/network">{{tr "NetworkSpiter"}}</a></li>
|
||||||
{{if .Admin}}
|
{{if .Admin}}
|
||||||
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
|
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
|
||||||
{{else}}
|
{{else}}
|
||||||
@@ -165,8 +166,8 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="/static/theme-daynight/js/main.js?v202102012266"></script>
|
<script src="/static/theme-daynight/js/main.js?v202102012266"></script>
|
||||||
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/limonte-sweetalert2/11.4.4/sweetalert2.all.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/limonte-sweetalert2/11.4.4/sweetalert2.all.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const initData = JSON.parse('{{.Servers}}').servers;
|
const initData = JSON.parse('{{.Servers}}').servers;
|
||||||
|
|||||||
+268
@@ -0,0 +1,268 @@
|
|||||||
|
{{define "theme-daynight/network"}}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="{{.Conf.Language}}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/theme-daynight/css/main.css?v202108042286">
|
||||||
|
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
|
||||||
|
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
|
||||||
|
|
||||||
|
{{if ts .CustomCode}}
|
||||||
|
{{.CustomCode|safe}}
|
||||||
|
{{end}}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body data-theme="light" data-gridlist="grid">
|
||||||
|
<header>
|
||||||
|
<section class="nav-bar clearfix">
|
||||||
|
<figure class="logo">
|
||||||
|
<a href="/">
|
||||||
|
<img src="/static/logo.svg?v20210804" alt='{{tr "NezhaMonitoring"}}' width="50" height="50">
|
||||||
|
</a>
|
||||||
|
<a href="/">{{.Conf.Site.Brand}}</a>
|
||||||
|
</figure>
|
||||||
|
<div class="icon-container">
|
||||||
|
<div class="row cf">
|
||||||
|
<div class="three col">
|
||||||
|
<div class="hamburger" id="hamburger-icon"><span class="line"></span><span
|
||||||
|
class="line"></span><span class="line"></span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="nav-menu">
|
||||||
|
<ul>
|
||||||
|
<li><a href="/">{{tr "Home"}}</a></li>
|
||||||
|
<li><a href="/service">{{tr "Services"}}</a></li>
|
||||||
|
<li><a href="/network">{{tr "NetworkSpiter"}}</a></li>
|
||||||
|
{{if .Admin}}
|
||||||
|
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
|
||||||
|
{{else}}
|
||||||
|
<li><a href="/login">{{tr "Login"}}</a></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</section>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div id="network">
|
||||||
|
<div class="server-info-container" v-for='server in servers' :id="server.ID" style="font-size: .6em">
|
||||||
|
<div class="info-body" @click="redirectNetwork(server.ID)">
|
||||||
|
<ul class="server-info-body-container">
|
||||||
|
<li>
|
||||||
|
<h3>@#server.Name#@</h3>
|
||||||
|
</li>
|
||||||
|
<li><img :src="'/static/theme-daynight/img/flag/'+(server.Host&&server.Host.CountryCode?server.Host.CountryCode.toUpperCase():'CN')+'.png'"
|
||||||
|
:title="server.Host.CountryCode.toUpperCase()" /></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="network-chart" style="height: 800px;overflow: hidden">
|
||||||
|
<div id="monitor-info-container" style="height: 520px;max-width: 1400px">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-container">
|
||||||
|
<ul>
|
||||||
|
<li><i class="fas fa-sun" title='{{tr "LightMode"}}'></i><span>{{tr "LightMode"}}</span></li>
|
||||||
|
<li><i class="fas fa-moon" title='{{tr "DarkMode"}}'></i><span>{{tr "DarkMode"}}</span></li>
|
||||||
|
<li><i class="fas fa-th" title='{{tr "GridLayout"}}'></i><span>{{tr "GridLayout"}}</span></li>
|
||||||
|
<li><i class="fas fa-list-ul" title='{{tr "ListLayout"}}'></i><span>{{tr "ListLayout"}}</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<section class="dark-light-toggle">
|
||||||
|
<label class="switcher">
|
||||||
|
<input type="checkbox" name="theme" id="dark-light" />
|
||||||
|
<div>
|
||||||
|
<i class="fas fa-adjust"></i>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Back to top button -->
|
||||||
|
<a id="back-to-top"></a>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="footer-container">
|
||||||
|
<div><a href="https://github.com/naiba/nezha" target="_blank">Powered by {{tr "NezhaMonitoring"}} · {{.Version}}</a>
|
||||||
|
<p>© <span id="copyright-date">
|
||||||
|
<script>document.getElementById('copyright-date').appendChild(document.createTextNode(new Date().getFullYear()))</script>
|
||||||
|
</span> · <a href="https://blog.jackiesung.com" target="_blank">Theme designed by Jackie Sung</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/static/theme-daynight/js/main.js?v202102012266"></script>
|
||||||
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||||
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/limonte-sweetalert2/11.4.4/sweetalert2.all.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
|
||||||
|
const initData = JSON.parse('{{.Servers}}').servers;
|
||||||
|
let MaxTCPPingValue = {{.MaxTCPPingValue}};
|
||||||
|
if (MaxTCPPingValue == null) {
|
||||||
|
MaxTCPPingValue = 300;
|
||||||
|
}
|
||||||
|
// 基于准备好的dom,初始化echarts实例
|
||||||
|
var myChart = echarts.init(document.getElementById('monitor-info-container'));
|
||||||
|
// 使用刚指定的配置项和数据显示图表。
|
||||||
|
var statusCards = new Vue({
|
||||||
|
el: '#network',
|
||||||
|
delimiters: ['@#', '#@'],
|
||||||
|
data: {
|
||||||
|
servers: initData,
|
||||||
|
cache: [],
|
||||||
|
option: {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
position: function (pt) {
|
||||||
|
return [pt[0], '10%'];
|
||||||
|
},
|
||||||
|
formatter: function(params){
|
||||||
|
let result = params[0].axisValueLabel + "<br />";
|
||||||
|
params.forEach(function(item){
|
||||||
|
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
|
||||||
|
})
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
confine: true,
|
||||||
|
transitionDuration: 0
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
text: "",
|
||||||
|
textStyle: {}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: '5%',
|
||||||
|
data: [],
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
dataZoom: {
|
||||||
|
yAxisIndex: 'none'
|
||||||
|
},
|
||||||
|
restore: {},
|
||||||
|
saveAsImage: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
start: 94,
|
||||||
|
end: 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xAxis: {
|
||||||
|
type: 'time',
|
||||||
|
boundaryGap: false
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
boundaryGap: [0, '100%']
|
||||||
|
},
|
||||||
|
series: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.DarkMode();
|
||||||
|
this.parseMonitorInfo(monitorInfo);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
DarkMode() {
|
||||||
|
const hour = new Date(Date.now()).getHours()
|
||||||
|
if (hour > 17 || hour < 4) {
|
||||||
|
document.querySelector("input[name=theme]").checked = true;
|
||||||
|
document.getElementsByTagName("BODY")[0].setAttribute('data-theme', 'dark');
|
||||||
|
document.getElementById("monitor-info-container").style.backgroundColor = "#1E1E1E";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
redirectNetwork(id) {
|
||||||
|
this.getMonitorHistory(id)
|
||||||
|
.then(function(monitorInfo) {
|
||||||
|
var vm = network.__vue__;
|
||||||
|
vm.parseMonitorInfo(monitorInfo);
|
||||||
|
})
|
||||||
|
.catch(function(error){
|
||||||
|
window.location.href = "/404";
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getMonitorHistory(id) {
|
||||||
|
return $.ajax({
|
||||||
|
url: "/api/v1/monitor/"+id,
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
parseMonitorInfo(monitorInfo) {
|
||||||
|
let tSeries = [];
|
||||||
|
let tLegendData = [];
|
||||||
|
for (let i = 0; i < monitorInfo.result.length; i++) {
|
||||||
|
let loss = 0;
|
||||||
|
let data = [];
|
||||||
|
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
|
||||||
|
avgDelay = monitorInfo.result[i].avg_delay[j];
|
||||||
|
if (avgDelay > 0.9 * MaxTCPPingValue) {
|
||||||
|
loss += 1
|
||||||
|
}
|
||||||
|
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
|
||||||
|
}
|
||||||
|
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
|
||||||
|
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
|
||||||
|
tLegendData.push(legendName);
|
||||||
|
tSeries.push({
|
||||||
|
name: legendName,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.option.title.text = monitorInfo.result[0].server_name;
|
||||||
|
this.option.series = tSeries;
|
||||||
|
this.option.legend.data = tLegendData;
|
||||||
|
const hour = new Date(Date.now()).getHours()
|
||||||
|
if (hour > 17 || hour < 4) {
|
||||||
|
this.option.legend.textStyle.color = "#F1F1F2";
|
||||||
|
this.option.title.textStyle.color = "#ccc";
|
||||||
|
}
|
||||||
|
myChart.clear();
|
||||||
|
myChart.setOption(this.option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
<style>
|
||||||
|
#network {
|
||||||
|
display: grid;
|
||||||
|
/*grid-template-columns: repeat(5, 1fr);*/
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
|
grid-gap: 1em;
|
||||||
|
width: 100%;
|
||||||
|
width: calc(100vw - 6em);
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 1em auto;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
#monitor-info-container{
|
||||||
|
margin: 0em auto;
|
||||||
|
align-content: start;
|
||||||
|
background-color: #F1F1F2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
+4
-3
@@ -8,9 +8,9 @@
|
|||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
||||||
<link rel="stylesheet" href="/static/theme-daynight/css/service.css?v202108042286" />
|
<link rel="stylesheet" href="/static/theme-daynight/css/service.css?v202108042286" />
|
||||||
<link rel="stylesheet" href="https://cdn.staticfile.org/mdui/1.0.2/css/mdui.min.css"/>
|
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
|
||||||
<link href="https://cdn.staticfile.org/font-awesome/6.0.0/css/all.min.css" rel="stylesheet" />
|
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-awesome/6.0.0/css/all.min.css" rel="stylesheet" />
|
||||||
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
|
||||||
|
|
||||||
{{if ts .CustomCode}}
|
{{if ts .CustomCode}}
|
||||||
{{.CustomCode|safe}}
|
{{.CustomCode|safe}}
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">{{tr "Home"}}</a></li>
|
<li><a href="/">{{tr "Home"}}</a></li>
|
||||||
<li><a href="/service">{{tr "Services"}}</a></li>
|
<li><a href="/service">{{tr "Services"}}</a></li>
|
||||||
|
<li><a href="/network">{{tr "NetworkSpiter"}}</a></li>
|
||||||
{{if .Admin}}
|
{{if .Admin}}
|
||||||
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
|
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{{define "theme-default/footer"}}
|
||||||
|
</div>
|
||||||
|
<div class="ui inverted vertical footer segment">
|
||||||
|
<div class="ui center aligned is-size-7 container">
|
||||||
|
<b>© <a style="color: white;" href="/">{{.Conf.Site.Brand}}</a></b> | <small>Powered by <a
|
||||||
|
href="https://github.com/naiba/nezha" style="color: white;" target="_blank">{{tr "NezhaMonitoring"}}</a>
|
||||||
|
{{.Version}}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
{{define "theme-default/header"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{.Conf.Language}}">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<meta content="telephone=no" name="format-detection">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/assets/font-logos.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/[email protected]/css/flag-icons.min.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/theme-default/css/main.css?v20240226">
|
||||||
|
<link rel="shortcut icon" type="image/png" href="/static/logo.svg" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.js"></script>
|
||||||
|
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
|
||||||
|
<script src="/static/theme-default/js/mixin.js?v20240226"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{end}}
|
||||||
+232
-25
@@ -1,10 +1,10 @@
|
|||||||
{{define "theme-default/home"}}
|
{{define "theme-default/home"}}
|
||||||
{{template "common/header" .}}
|
{{template "theme-default/header" .}}
|
||||||
{{if ts .CustomCode}} {{.CustomCode|safe}} {{end}}
|
{{if ts .CustomCode}} {{.CustomCode|safe}} {{end}}
|
||||||
{{template "common/menu" .}}
|
{{template "theme-default/menu" .}}
|
||||||
<div class="nb-container">
|
<div class="nb-container">
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div id="app">
|
<template v-if="groups">
|
||||||
<div class="ui styled fluid accordion" v-for="group in groups">
|
<div class="ui styled fluid accordion" v-for="group in groups">
|
||||||
<div class="active title">
|
<div class="active title">
|
||||||
<i class="dropdown icon"></i>
|
<i class="dropdown icon"></i>
|
||||||
@@ -15,12 +15,13 @@
|
|||||||
<div v-for="server in group.data" :id="server.ID" class="ui card">
|
<div v-for="server in group.data" :id="server.ID" class="ui card">
|
||||||
<div class="content" v-if="server.Host" style="margin-top: 10px; padding-bottom: 5px">
|
<div class="content" v-if="server.Host" style="margin-top: 10px; padding-bottom: 5px">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<i :class="server.Host.CountryCode + ' flag'"></i> <i v-if='server.Host.Platform == "darwin"'
|
<i :class="'fi fi-' + server.Host.CountryCode"></i> <i v-if='server.Host.Platform == "darwin"'
|
||||||
class="apple icon"></i><i v-else-if='isWindowsPlatform(server.Host.Platform)'
|
class="apple icon"></i><i v-else-if='isWindowsPlatform(server.Host.Platform)'
|
||||||
class="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
|
class="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
|
||||||
@#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
|
@#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
|
||||||
<i class="nezha-secondary-font info circle icon" style="height: 28px"></i>
|
<i @click="togglePopup($event, server.ID)" aria-expanded="false" class="nezha-secondary-font info circle icon"></i>
|
||||||
<div class="ui content popup" style="margin-bottom: 0">
|
<div class="ui content popup" :class="{ 'visible': isActive(server.ID) }" style="margin-bottom: 0;">
|
||||||
|
<i class="closePopup window close icon" @click="closePopup(server.ID)"></i>
|
||||||
{{tr "Platform"}}: @#server.Host.Platform#@-@#server.Host.PlatformVersion#@
|
{{tr "Platform"}}: @#server.Host.Platform#@-@#server.Host.PlatformVersion#@
|
||||||
[<span
|
[<span
|
||||||
v-if="server.Host.Virtualization">@#server.Host.Virtualization#@:</span>@#server.Host.Arch#@]<br />
|
v-if="server.Host.Virtualization">@#server.Host.Virtualization#@:</span>@#server.Host.Arch#@]<br />
|
||||||
@@ -40,7 +41,8 @@
|
|||||||
{{tr "ConnCount"}}: TCP @# server.State.TcpConnCount #@ / UDP @# server.State.UdpConnCount #@<br />
|
{{tr "ConnCount"}}: TCP @# server.State.TcpConnCount #@ / UDP @# server.State.UdpConnCount #@<br />
|
||||||
{{tr "BootTime"}}: @# formatTimestamp(server.Host.BootTime) #@<br />
|
{{tr "BootTime"}}: @# formatTimestamp(server.Host.BootTime) #@<br />
|
||||||
{{tr "LastActive"}}: @# new Date(server.LastActive).toLocaleString() #@<br />
|
{{tr "LastActive"}}: @# new Date(server.LastActive).toLocaleString() #@<br />
|
||||||
{{tr "Version"}}: @#server.Host.Version#@<br />
|
{{tr "Version"}}: @#server.Host.Version#@
|
||||||
|
<div class="chartbox" :key="server.ID" :ref="`chart${server.ID}`" style="width: 100%; height: auto; margin-bottom: 2px;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider" style="margin-bottom: 5px"></div>
|
<div class="ui divider" style="margin-bottom: 5px"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,13 +74,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="three wide column">{{tr "NetSpeed"}}</div>
|
|
||||||
<div class="thirteen wide column">
|
|
||||||
<i class="arrow alternate circle down outline icon"></i>
|
|
||||||
@#formatByteSize(server.State.NetInSpeed)#@/s
|
|
||||||
<i class="arrow alternate circle up outline icon"></i>
|
|
||||||
@#formatByteSize(server.State.NetOutSpeed)#@/s
|
|
||||||
</div>
|
|
||||||
<div class="three wide column">{{tr "DiskUsed"}}</div>
|
<div class="three wide column">{{tr "DiskUsed"}}</div>
|
||||||
<div class="thirteen wide column">
|
<div class="thirteen wide column">
|
||||||
<div :class="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).class">
|
<div :class="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).class">
|
||||||
@@ -88,6 +83,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="three wide column">{{tr "NetSpeed"}}</div>
|
||||||
|
<div class="thirteen wide column">
|
||||||
|
<i class="arrow alternate circle down outline icon"></i>
|
||||||
|
@#formatByteSize(server.State.NetInSpeed)#@/s
|
||||||
|
<i class="arrow alternate circle up outline icon"></i>
|
||||||
|
@#formatByteSize(server.State.NetOutSpeed)#@/s
|
||||||
|
</div>
|
||||||
|
<div class="three wide column">{{tr "NetTransfer"}}</div>
|
||||||
|
<div class="thirteen wide column">
|
||||||
|
<i class="arrow circle down icon"></i>
|
||||||
|
@#formatByteSize(server.State.NetInTransfer)#@
|
||||||
|
|
||||||
|
<i class="arrow circle up icon"></i>
|
||||||
|
@#formatByteSize(server.State.NetOutTransfer)#@
|
||||||
|
</div>
|
||||||
|
<div class="three wide column">{{tr "Stat"}}</div>
|
||||||
|
<div class="thirteen wide column">
|
||||||
|
<i class="bi bi-cpu-fill" style="font-size: 1.1rem; color: #4a86e8;"></i> @#getCoreAndGHz(server.Host.CPU)#@
|
||||||
|
|
||||||
|
<i class="bi bi-memory" style="font-size: 1.1rem; color: #00ac0d;"></i> @#getK2Gb(server.Host.MemTotal)#@
|
||||||
|
|
||||||
|
<i class="bi bi-hdd" style="font-size: 1.1rem; color: #e41e10"></i> @#getK2Gb(server.Host.DiskTotal)#@
|
||||||
|
</div>
|
||||||
|
<div class="three wide column">{{tr "Load"}}</div>
|
||||||
|
<div class="thirteen wide column">
|
||||||
|
<i class="bi bi-activity" style="font-size: 1.1rem; color: #e41e10;"></i>
|
||||||
|
@# toFixed2(server.State.Load1) #@ |
|
||||||
|
@# toFixed2(server.State.Load5) #@ |
|
||||||
|
@# toFixed2(server.State.Load15) #@
|
||||||
|
</div>
|
||||||
<div class="three wide column">{{tr "Uptime"}}</div>
|
<div class="three wide column">{{tr "Uptime"}}</div>
|
||||||
<div class="thirteen wide column">
|
<div class="thirteen wide column">
|
||||||
<i class="clock icon"></i>@#secondToDate(server.State.Uptime)#@
|
<i class="clock icon"></i>@#secondToDate(server.State.Uptime)#@
|
||||||
@@ -103,30 +128,172 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{{template "theme-default/footer" .}}
|
||||||
{{template "common/footer" .}}
|
|
||||||
<script>
|
<script>
|
||||||
const initData = JSON.parse('{{.Servers}}').servers;
|
|
||||||
var statusCards = new Vue({
|
var statusCards = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
delimiters: ['@#', '#@'],
|
delimiters: ['@#', '#@'],
|
||||||
data: {
|
data: {
|
||||||
data: initData,
|
page: 'index',
|
||||||
|
defaultTemplate: {{.Conf.Site.Theme}},
|
||||||
|
templates: {{.Themes}},
|
||||||
|
data: [],
|
||||||
groups: [],
|
groups: [],
|
||||||
cache: [],
|
cache: [],
|
||||||
|
chartDataList: [],
|
||||||
|
activePopup: null,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
mixins: [mixinsVue],
|
||||||
created() {
|
created() {
|
||||||
|
this.data = JSON.parse('{{.Servers}}').servers;
|
||||||
this.group()
|
this.group()
|
||||||
},
|
},
|
||||||
mounted() {
|
methods: {
|
||||||
$('.nezha-secondary-font.info.icon').popup({
|
togglePopup(event, id) {
|
||||||
popup: '.ui.content.popup',
|
// 切换弹出层的激活状态
|
||||||
exclusive: true,
|
this.activePopup = this.activePopup === id ? null : id;
|
||||||
|
this.showCharts(id);
|
||||||
|
},
|
||||||
|
isActive(id) {
|
||||||
|
// 检查弹出层是否处于激活状态
|
||||||
|
return this.activePopup === id;
|
||||||
|
},
|
||||||
|
closePopup(id) {
|
||||||
|
this.activePopup = null;
|
||||||
|
},
|
||||||
|
showCharts(id) {
|
||||||
|
// 发起数据请求
|
||||||
|
const url = `/api/v1/monitor/${id}`;
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.result) { // 数据请求成功,更新数据并渲染图表
|
||||||
|
this.chartDataList[id - 1] = data.result;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.renderCharts(id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('this agent (id:'+ id + ') has no monitor.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
renderCharts(id) {
|
||||||
|
if (!this.chartDataList[id - 1]) return;
|
||||||
|
const MaxTCPPingValue = {{.MaxTCPPingValue}} ? {{.MaxTCPPingValue}} : 300;
|
||||||
|
const isMobile = this.checkIsMobile();
|
||||||
|
const fontSize = isMobile ? 10 : 9;
|
||||||
|
const itemGap = isMobile ? 5 : 2;
|
||||||
|
const itemWidth = isMobile ? 20 : 15;
|
||||||
|
const gridLeft = 25;
|
||||||
|
const gridRight = 12;
|
||||||
|
const fontColor = "rgba(0, 0, 0, 0.68)";
|
||||||
|
const backgroundColor = '';
|
||||||
|
const borderColor = "#ffffff";
|
||||||
|
const chartData = this.chartDataList[id - 1];
|
||||||
|
const chartContainer = this.$refs[`chart${id}`][0];
|
||||||
|
const chart = echarts.init(chartContainer, null, {
|
||||||
|
renderer: 'canvas',
|
||||||
|
useDirtyRect: false,
|
||||||
|
width: 'auto',
|
||||||
|
height: 120,
|
||||||
|
});
|
||||||
|
const xAxisData = chartData[0].created_at.map(time => new Date(time).toLocaleString());
|
||||||
|
const seriesData = chartData.map(item => {
|
||||||
|
let loss = 0;
|
||||||
|
const data = item.avg_delay.map((avgDelay, index) => {
|
||||||
|
if (avgDelay > 0.9 * MaxTCPPingValue) {
|
||||||
|
loss += 1;
|
||||||
|
}
|
||||||
|
return [new Date(item.created_at[index]).toLocaleString(), avgDelay];
|
||||||
|
});
|
||||||
|
const lossRate = ((loss / item.created_at.length) * 100).toFixed(1);
|
||||||
|
item.monitor_name = item.monitor_name + " " + lossRate + "%";
|
||||||
|
return {
|
||||||
|
name: item.monitor_name,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
data: data
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const option = {
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
title: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
show: true,
|
||||||
|
trigger: 'axis',
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: fontColor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: chartData.map(item => item.monitor_name),
|
||||||
|
show: true,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: fontColor
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
cap: 'butt'
|
||||||
|
},
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
itemGap: itemGap,
|
||||||
|
itemWidth: itemWidth,
|
||||||
|
padding: [5,0,5,0]
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'time',
|
||||||
|
data: xAxisData,
|
||||||
|
axisLabel: {
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLabel: {
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
show: false,
|
||||||
|
type: 'slider',
|
||||||
|
start: 0,
|
||||||
|
end: 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: seriesData,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: fontColor
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: '40',
|
||||||
|
bottom: '20',
|
||||||
|
left: gridLeft,
|
||||||
|
right: gridRight
|
||||||
|
}
|
||||||
|
};
|
||||||
|
chart.setOption(option);
|
||||||
|
},
|
||||||
|
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
|
||||||
|
return window.innerWidth <= 768;
|
||||||
|
},
|
||||||
toFixed2(f) {
|
toFixed2(f) {
|
||||||
return f.toFixed(2)
|
return f.toFixed(2)
|
||||||
},
|
},
|
||||||
@@ -227,7 +394,7 @@
|
|||||||
secondToDate(s) {
|
secondToDate(s) {
|
||||||
var d = Math.floor(s / 3600 / 24);
|
var d = Math.floor(s / 3600 / 24);
|
||||||
if (d > 0) {
|
if (d > 0) {
|
||||||
return d + ' {{tr "Day"}}'
|
return d + " {{tr "Day"}}"
|
||||||
}
|
}
|
||||||
var h = Math.floor(s / 3600 % 24);
|
var h = Math.floor(s / 3600 % 24);
|
||||||
var m = Math.floor(s / 60 % 60);
|
var m = Math.floor(s / 60 % 60);
|
||||||
@@ -238,8 +405,48 @@
|
|||||||
return new Date(t * 1000).toLocaleString()
|
return new Date(t * 1000).toLocaleString()
|
||||||
},
|
},
|
||||||
formatByteSize(bs) {
|
formatByteSize(bs) {
|
||||||
const x = readableBytes(bs)
|
const x = this.readableBytes(bs)
|
||||||
return x != "NaN undefined" ? x : '0B'
|
return x != "NaN undefined" ? x : '0B'
|
||||||
|
},
|
||||||
|
readableBytes(bytes) {
|
||||||
|
if (!bytes) {
|
||||||
|
return '0B'
|
||||||
|
}
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
||||||
|
sizes = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"];
|
||||||
|
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + sizes[i];
|
||||||
|
},
|
||||||
|
getCoreAndGHz(str){
|
||||||
|
if((str || []).hasOwnProperty(0) === false){
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
str = str[0];
|
||||||
|
let GHz = str.match(/(\d|\.)+GHz/g);
|
||||||
|
let Core = str.match(/(\d|\.)+ Physical/g);
|
||||||
|
GHz = GHz!==null?GHz.hasOwnProperty(0)===false?'':GHz[0]:''
|
||||||
|
Core = Core!==null?Core.hasOwnProperty(0)===false?'?':Core[0]:'?'
|
||||||
|
if(Core === '?'){
|
||||||
|
let Core = str.match(/(\d|\.)+ Virtual/g);
|
||||||
|
Core = Core!==null?Core.hasOwnProperty(0)===false?'?':Core[0]:'?'
|
||||||
|
return Core.replace('Virtual','Core')
|
||||||
|
}
|
||||||
|
return Core.replace('Physical','Core');
|
||||||
|
},
|
||||||
|
getK2Gb(bs){
|
||||||
|
bs = bs / 1024 /1024 /1024;
|
||||||
|
if(bs>=1){
|
||||||
|
return Math.ceil(bs.toFixed(2)) + 'GB';
|
||||||
|
}else{
|
||||||
|
bs = bs * 1024;
|
||||||
|
return Math.ceil(bs.toFixed(2)) + 'MB';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
listTipsMouseenter(obj,strs,tipsNum=1){
|
||||||
|
this.layerIndex = layer.tips(strs, '#'+obj,{tips: [tipsNum, 'rgb(0 0 0 / 85%)'],time:0});
|
||||||
|
$('#'+obj).attr('layerIndex',this.layerIndex)
|
||||||
|
},
|
||||||
|
listTipsMouseleave(obj){
|
||||||
|
layer.close(this.layerIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
{{define "theme-default/menu"}}
|
||||||
|
<div id="app">
|
||||||
|
<div class="ui large top fixed menu nb-menu" style="z-index:9999999;">
|
||||||
|
<div class="ui container">
|
||||||
|
<a class="item" href="/">
|
||||||
|
<img src="/static/logo.svg?v20210804">
|
||||||
|
</a>
|
||||||
|
<a class='item' href="/"><i class="home icon"></i>{{tr "Home"}}</a>
|
||||||
|
<template v-if="isMobile">
|
||||||
|
<div class="item ui simple dropdown">
|
||||||
|
<div class="text"><i class="bi bi-gear-wide-connected icon" style="margin-right:3px;"></i>{{tr "Feature" }}<i class="dropdown icon" style="margin-right:0px;"></i></div>
|
||||||
|
<div class="menu">
|
||||||
|
<a href="/service" class="item"><i class="rss icon"></i>{{tr "Services" }}</a>
|
||||||
|
<a href="/network" class="item"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a href="/service" class='item'><i class="rss icon"></i>{{tr "Services" }}</a>
|
||||||
|
<a href="/network" class="item"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a>
|
||||||
|
</template>
|
||||||
|
<div class="item ui simple dropdown">
|
||||||
|
<div class="text"><i class="bi bi-incognito icon" style="margin-right:3px;"></i>{{tr "Template" }}<i class="dropdown icon" style="margin-right:0px;"></i></div>
|
||||||
|
<div class="menu">
|
||||||
|
<a v-for="(value, key) in templates" :key="key" @click="toggleTemplate(key)" class="item"><i class="th large icon"></i>@#value#@
|
||||||
|
<i class="check icon" v-if="preferredTemplate === key"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if .Admin}}
|
||||||
|
<div class="item right item-right ui simple dropdown">
|
||||||
|
<div class="text">
|
||||||
|
<i class="user icon" style="margin-right:3px"></i>{{.Admin.Name}}
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
</div>
|
||||||
|
<div class="menu">
|
||||||
|
<a class="item" href="/server"><i class="terminal icon"></i>{{tr "AdminPanel"}}</a>
|
||||||
|
<a class="item" @click="logOut({{.Admin.ID}})"><i class="logout icon"></i>{{tr "Logout"}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<a href="/login" class="item right item-right" style="padding-right:1.2rem"><i class="sign-in icon"></i>{{tr "Login"}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "component/confirm" .}}
|
||||||
|
{{end}}
|
||||||
+230
@@ -0,0 +1,230 @@
|
|||||||
|
{{define "theme-default/network"}}
|
||||||
|
{{template "theme-default/header" .}}
|
||||||
|
{{if ts .CustomCode}}
|
||||||
|
{{.CustomCode|safe}}
|
||||||
|
{{end}}
|
||||||
|
{{template "theme-default/menu" .}}
|
||||||
|
<div class="nb-container">
|
||||||
|
<div class="ui container">
|
||||||
|
<div class="service-status">
|
||||||
|
<table class="ui celled table">
|
||||||
|
<button class="ui nezha-primary-btn button"
|
||||||
|
v-for="server in servers"
|
||||||
|
style="margin-top: 3px"
|
||||||
|
@click="redirectNetwork(server.ID)">
|
||||||
|
@#server.Name#@ <i :class="server.Host.CountryCode + ' flag'"></i>
|
||||||
|
</button>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui container" style="max-width: 95vw">
|
||||||
|
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "theme-default/footer" .}}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
|
||||||
|
const initData = JSON.parse('{{.Servers}}').servers;
|
||||||
|
let MaxTCPPingValue = {{.MaxTCPPingValue}};
|
||||||
|
if (MaxTCPPingValue == null) {
|
||||||
|
MaxTCPPingValue = 1000;
|
||||||
|
}
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
delimiters: ['@#', '#@'],
|
||||||
|
data: {
|
||||||
|
page: 'network',
|
||||||
|
defaultTemplate: {{.Conf.Site.Theme}},
|
||||||
|
templates: {{.Themes}},
|
||||||
|
servers: initData,
|
||||||
|
option: {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
position: function (pt) {
|
||||||
|
return [pt[0], '10%'];
|
||||||
|
},
|
||||||
|
formatter: function(params){
|
||||||
|
let result = params[0].axisValueLabel + "<br />";
|
||||||
|
params.forEach(function(item){
|
||||||
|
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
|
||||||
|
})
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
confine: true,
|
||||||
|
transitionDuration: 0
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
text: "",
|
||||||
|
textStyle: {}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: '5%',
|
||||||
|
data: [],
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
dataZoom: {
|
||||||
|
yAxisIndex: 'none'
|
||||||
|
},
|
||||||
|
restore: {},
|
||||||
|
saveAsImage: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
start: 0,
|
||||||
|
end: 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xAxis: {
|
||||||
|
type: 'time',
|
||||||
|
boundaryGap: false
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
boundaryGap: false
|
||||||
|
},
|
||||||
|
series: [],
|
||||||
|
},
|
||||||
|
chartOnOff: true,
|
||||||
|
},
|
||||||
|
mixins: [mixinsVue],
|
||||||
|
mounted() {
|
||||||
|
this.renderChart();
|
||||||
|
this.parseMonitorInfo(monitorInfo);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getFontLogoClass(str) {
|
||||||
|
if (["almalinux",
|
||||||
|
"alpine",
|
||||||
|
"aosc",
|
||||||
|
"apple",
|
||||||
|
"archlinux",
|
||||||
|
"archlabs",
|
||||||
|
"artix",
|
||||||
|
"budgie",
|
||||||
|
"centos",
|
||||||
|
"coreos",
|
||||||
|
"debian",
|
||||||
|
"deepin",
|
||||||
|
"devuan",
|
||||||
|
"docker",
|
||||||
|
"elementary",
|
||||||
|
"fedora",
|
||||||
|
"ferris",
|
||||||
|
"flathub",
|
||||||
|
"freebsd",
|
||||||
|
"gentoo",
|
||||||
|
"gnu-guix",
|
||||||
|
"illumos",
|
||||||
|
"kali-linux",
|
||||||
|
"linuxmint",
|
||||||
|
"mageia",
|
||||||
|
"mandriva",
|
||||||
|
"manjaro",
|
||||||
|
"nixos",
|
||||||
|
"openbsd",
|
||||||
|
"opensuse",
|
||||||
|
"pop-os",
|
||||||
|
"raspberry-pi",
|
||||||
|
"redhat",
|
||||||
|
"rocky-linux",
|
||||||
|
"sabayon",
|
||||||
|
"slackware",
|
||||||
|
"snappy",
|
||||||
|
"solus",
|
||||||
|
"tux",
|
||||||
|
"ubuntu",
|
||||||
|
"void",
|
||||||
|
"zorin"].indexOf(str)
|
||||||
|
> -1) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
if (['openwrt', 'linux', "immortalwrt"].indexOf(str) > -1) {
|
||||||
|
return 'tux';
|
||||||
|
}
|
||||||
|
if (str == 'amazon') {
|
||||||
|
return 'redhat';
|
||||||
|
}
|
||||||
|
if (str == 'arch') {
|
||||||
|
return 'archlinux';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
redirectNetwork(id) {
|
||||||
|
this.getMonitorHistory(id)
|
||||||
|
.then(function(monitorInfo) {
|
||||||
|
var vm = app.__vue__;
|
||||||
|
vm.parseMonitorInfo(monitorInfo);
|
||||||
|
})
|
||||||
|
.catch(function(error){
|
||||||
|
window.location.href = "/404";
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getMonitorHistory(id) {
|
||||||
|
return $.ajax({
|
||||||
|
url: "/api/v1/monitor/"+id,
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
parseMonitorInfo(monitorInfo) {
|
||||||
|
let tSeries = [];
|
||||||
|
let tLegendData = [];
|
||||||
|
for (let i = 0; i < monitorInfo.result.length; i++) {
|
||||||
|
let loss = 0;
|
||||||
|
let data = [];
|
||||||
|
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
|
||||||
|
avgDelay = Math.round(monitorInfo.result[i].avg_delay[j]);
|
||||||
|
if (avgDelay > 0.9 * MaxTCPPingValue) {
|
||||||
|
loss += 1
|
||||||
|
}
|
||||||
|
if (avgDelay > 0) {
|
||||||
|
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
|
||||||
|
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
|
||||||
|
tLegendData.push(legendName);
|
||||||
|
tSeries.push({
|
||||||
|
name: legendName,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
data: data,
|
||||||
|
markPoint: {
|
||||||
|
data: [
|
||||||
|
{ type: 'max', symbol: 'pin', name: 'Max', itemStyle: { color: '#f00' } },
|
||||||
|
{ type: 'min', symbol: 'pin', name: 'Min', itemStyle: { color: '#0f0' } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.option.title.text = monitorInfo.result[0].server_name;
|
||||||
|
this.option.series = tSeries;
|
||||||
|
this.option.legend.data = tLegendData;
|
||||||
|
this.myChart.clear();
|
||||||
|
this.myChart.setOption(this.option);
|
||||||
|
},
|
||||||
|
isWindowsPlatform(str) {
|
||||||
|
return str.includes('Windows')
|
||||||
|
},
|
||||||
|
renderChart() {
|
||||||
|
this.myChart = echarts.init(this.$refs.chartDom);
|
||||||
|
this.myChart.setOption(this.option);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.myChart.dispose();
|
||||||
|
this.myChart = null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{end}}
|
||||||
+15
-4
@@ -1,9 +1,9 @@
|
|||||||
{{define "theme-default/service"}}
|
{{define "theme-default/service"}}
|
||||||
{{template "common/header" .}}
|
{{template "theme-default/header" .}}
|
||||||
{{if ts .CustomCode}}
|
{{if ts .CustomCode}}
|
||||||
{{.CustomCode|safe}}
|
{{.CustomCode|safe}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "common/menu" .}}
|
{{template "theme-default/menu" .}}
|
||||||
<div class="nb-container">
|
<div class="nb-container">
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="service-status">
|
<div class="service-status">
|
||||||
@@ -76,10 +76,21 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{template "common/footer" .}}
|
{{template "theme-default/footer" .}}
|
||||||
|
<script>
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
delimiters: ['@#', '#@'],
|
||||||
|
data: {
|
||||||
|
page: 'service',
|
||||||
|
defaultTemplate: {{.Conf.Site.Theme}},
|
||||||
|
templates: {{.Themes}}
|
||||||
|
},
|
||||||
|
mixins: [mixinsVue]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
+5
-5
@@ -14,8 +14,8 @@
|
|||||||
<meta http-equiv="Content-Language" content="zh" />
|
<meta http-equiv="Content-Language" content="zh" />
|
||||||
|
|
||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css">
|
<link rel="stylesheet" type="text/css" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdn.staticfile.org/font-awesome/6.0.0/css/all.min.css">
|
<link rel="stylesheet" type="text/css" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-awesome/6.0.0/css/all.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
|
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
|
||||||
<link rel="stylesheet" href="/static/theme-hotaru/css/core.css?v202012121912" type="text/css">
|
<link rel="stylesheet" href="/static/theme-hotaru/css/core.css?v202012121912" type="text/css">
|
||||||
<link rel="stylesheet" href="/static/theme-hotaru/css/main.css?v202101171153" type="text/css">
|
<link rel="stylesheet" href="/static/theme-hotaru/css/main.css?v202101171153" type="text/css">
|
||||||
@@ -141,10 +141,10 @@
|
|||||||
<a href="/server">{{tr "AdminPanel"}}</a>
|
<a href="/server">{{tr "AdminPanel"}}</a>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
|
||||||
<script src="/static/semantic-ui-alerts.min.js"></script>
|
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const initData = JSON.parse('{{.Servers}}').servers;
|
const initData = JSON.parse('{{.Servers}}').servers;
|
||||||
var statusCards = new Vue({
|
var statusCards = new Vue({
|
||||||
|
|||||||
+5
-5
@@ -9,7 +9,7 @@
|
|||||||
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
||||||
|
|
||||||
<!-- MDUI CSS -->
|
<!-- MDUI CSS -->
|
||||||
<link rel="stylesheet" href="https://cdn.staticfile.org/mdui/1.0.2/css/mdui.min.css"/>
|
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
|
||||||
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
|
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
|
||||||
<style>
|
<style>
|
||||||
.mdui-table td, .mdui-table th{padding: 6px;}
|
.mdui-table td, .mdui-table th{padding: 6px;}
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<div class="mdui-col" v-for='server in servers' :id="server.ID">
|
<div class="mdui-col" v-for='server in servers' :id="server.ID">
|
||||||
<div :class="'mdui-card mt' + (server.live?'':' offline')">
|
<div :class="'mdui-card mt' + (server.live?'':' offline')">
|
||||||
<div class="mdui-card-header">
|
<div class="mdui-card-header">
|
||||||
<img class="mdui-card-header-avatar" :src="'https://cdn.staticfile.org/flag-icon-css/4.1.5/flags/1x1/' + (server.Host.CountryCode?server.Host.CountryCode:'cn') + '.svg'"/>
|
<img class="mdui-card-header-avatar" :src="'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/flag-icon-css/4.1.5/flags/1x1/' + (server.Host.CountryCode?server.Host.CountryCode:'cn') + '.svg'"/>
|
||||||
<div class="mdui-card-header-title">@#server.Name#@</div>
|
<div class="mdui-card-header-title">@#server.Name#@</div>
|
||||||
<div class="mdui-card-header-subtitle">@#server.Host.CountryCode.toUpperCase()#@ | @#server.Host.Platform#@ @#server.Host.PlatformVersion#@</div>
|
<div class="mdui-card-header-subtitle">@#server.Host.CountryCode.toUpperCase()#@ | @#server.Host.Platform#@ @#server.Host.PlatformVersion#@</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,9 +153,9 @@
|
|||||||
{{template "theme-mdui/footer" .}}
|
{{template "theme-mdui/footer" .}}
|
||||||
|
|
||||||
<script src="/static/theme-mdui/mdui.js"></script>
|
<script src="/static/theme-mdui/mdui.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/mdui/1.0.2/js/mdui.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/js/mdui.min.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var container=document.querySelector("#container");
|
var container=document.querySelector("#container");
|
||||||
|
|||||||
+1
@@ -12,6 +12,7 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<a href="/" class='mdui-ripple mdui-ripple-white mdui-hoverable{{if eq .MatchedPath "/"}} mdui-tab-active{{end}}'><i class="mdui-icon material-icons">home</i>{{tr "Home"}}</a>
|
<a href="/" class='mdui-ripple mdui-ripple-white mdui-hoverable{{if eq .MatchedPath "/"}} mdui-tab-active{{end}}'><i class="mdui-icon material-icons">home</i>{{tr "Home"}}</a>
|
||||||
<a href="/service" class='mdui-ripple mdui-ripple-white mdui-hoverable{{if eq .MatchedPath "/service"}} mdui-tab-active{{end}}'><i class="mdui-icon material-icons">accessibility</i>{{tr "Services"}}</a>
|
<a href="/service" class='mdui-ripple mdui-ripple-white mdui-hoverable{{if eq .MatchedPath "/service"}} mdui-tab-active{{end}}'><i class="mdui-icon material-icons">accessibility</i>{{tr "Services"}}</a>
|
||||||
|
<a href="/network" class='mdui-ripple mdui-ripple-white mdui-hoverable{{if eq .MatchedPath "/network"}} mdui-tab-active{{end}}'><i class="mdui-icon material-icons">network_check</i>{{tr "NetworkSpiter"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="mdui-toolbar-spacer"></div>
|
<div class="mdui-toolbar-spacer"></div>
|
||||||
{{if .Admin}}
|
{{if .Admin}}
|
||||||
|
|||||||
+255
@@ -0,0 +1,255 @@
|
|||||||
|
{{define "theme-mdui/network"}}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="{{.Conf.Language}}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
||||||
|
|
||||||
|
<!-- MDUI CSS -->
|
||||||
|
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
|
||||||
|
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
|
||||||
|
<style>
|
||||||
|
.mdui-table td, .mdui-table th{padding: 6px;}
|
||||||
|
.progress{width: 10%;min-width: 75px;}
|
||||||
|
.progress-text{font-size: 16px;font-weight: 800;position: relative;top: 4px;left: 6px;}
|
||||||
|
.offline st,.offline at,.offline gt,.offline .progress-text{color: grey;}
|
||||||
|
a{text-decoration:none;color:#333;}.mdui-theme-layout-dark a{color:#fff;}
|
||||||
|
</style>
|
||||||
|
{{if ts .CustomCode}}
|
||||||
|
{{.CustomCode|safe}}
|
||||||
|
{{end}}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{{template "theme-mdui/menu" .}}
|
||||||
|
<div class="nb-container" id="app">
|
||||||
|
<div class="ui container">
|
||||||
|
<div class="service-status">
|
||||||
|
<table class="ui celled table">
|
||||||
|
<button
|
||||||
|
v-for="server in servers"
|
||||||
|
@click="redirectNetwork(server.ID)"
|
||||||
|
class="mdui-btn mdui-btn-raised mdui-color-theme mdui-color-indigo mdui-text-color-white"
|
||||||
|
style="margin-top: 6px;margin-left:5px">
|
||||||
|
<img :src="'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/flag-icon-css/4.1.5/flags/4x3/' + (server.Host.CountryCode?server.Host.CountryCode:'cn') + '.svg'" alt="Flag Icon" style="vertical-align: middle;height:14px"> <span>@#server.Name#@ </span></button>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui container" style="max-width: 95vw">
|
||||||
|
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "theme-mdui/footer" .}}
|
||||||
|
<script src="/static/theme-mdui/mdui.js"></script>
|
||||||
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/js/mdui.min.js"></script>
|
||||||
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
|
||||||
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
|
||||||
|
const initData = JSON.parse('{{.Servers}}').servers;
|
||||||
|
let MaxTCPPingValue = {{.MaxTCPPingValue}};
|
||||||
|
if (MaxTCPPingValue == null) {
|
||||||
|
MaxTCPPingValue = 1000;
|
||||||
|
}
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
delimiters: ['@#', '#@'],
|
||||||
|
data: {
|
||||||
|
servers: initData,
|
||||||
|
option: {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
position: function (pt) {
|
||||||
|
return [pt[0], '10%'];
|
||||||
|
},
|
||||||
|
formatter: function(params){
|
||||||
|
let result = params[0].axisValueLabel + "<br />";
|
||||||
|
params.forEach(function(item){
|
||||||
|
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
|
||||||
|
})
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
confine: true,
|
||||||
|
transitionDuration: 0
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
text: "",
|
||||||
|
textStyle: {}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: '5%',
|
||||||
|
data: [],
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
dataZoom: {
|
||||||
|
yAxisIndex: 'none'
|
||||||
|
},
|
||||||
|
restore: {},
|
||||||
|
saveAsImage: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
start: 0,
|
||||||
|
end: 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xAxis: {
|
||||||
|
type: 'time',
|
||||||
|
boundaryGap: false
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
boundaryGap: false
|
||||||
|
},
|
||||||
|
series: [],
|
||||||
|
},
|
||||||
|
chartOnOff: true,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.renderChart();
|
||||||
|
this.parseMonitorInfo(monitorInfo);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getFontLogoClass(str) {
|
||||||
|
if (["almalinux",
|
||||||
|
"alpine",
|
||||||
|
"aosc",
|
||||||
|
"apple",
|
||||||
|
"archlinux",
|
||||||
|
"archlabs",
|
||||||
|
"artix",
|
||||||
|
"budgie",
|
||||||
|
"centos",
|
||||||
|
"coreos",
|
||||||
|
"debian",
|
||||||
|
"deepin",
|
||||||
|
"devuan",
|
||||||
|
"docker",
|
||||||
|
"elementary",
|
||||||
|
"fedora",
|
||||||
|
"ferris",
|
||||||
|
"flathub",
|
||||||
|
"freebsd",
|
||||||
|
"gentoo",
|
||||||
|
"gnu-guix",
|
||||||
|
"illumos",
|
||||||
|
"kali-linux",
|
||||||
|
"linuxmint",
|
||||||
|
"mageia",
|
||||||
|
"mandriva",
|
||||||
|
"manjaro",
|
||||||
|
"nixos",
|
||||||
|
"openbsd",
|
||||||
|
"opensuse",
|
||||||
|
"pop-os",
|
||||||
|
"raspberry-pi",
|
||||||
|
"redhat",
|
||||||
|
"rocky-linux",
|
||||||
|
"sabayon",
|
||||||
|
"slackware",
|
||||||
|
"snappy",
|
||||||
|
"solus",
|
||||||
|
"tux",
|
||||||
|
"ubuntu",
|
||||||
|
"void",
|
||||||
|
"zorin"].indexOf(str)
|
||||||
|
> -1) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
if (['openwrt', 'linux', "immortalwrt"].indexOf(str) > -1) {
|
||||||
|
return 'tux';
|
||||||
|
}
|
||||||
|
if (str == 'amazon') {
|
||||||
|
return 'redhat';
|
||||||
|
}
|
||||||
|
if (str == 'arch') {
|
||||||
|
return 'archlinux';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
redirectNetwork(id) {
|
||||||
|
this.getMonitorHistory(id)
|
||||||
|
.then(function(monitorInfo) {
|
||||||
|
var vm = app.__vue__;
|
||||||
|
vm.parseMonitorInfo(monitorInfo);
|
||||||
|
})
|
||||||
|
.catch(function(error){
|
||||||
|
window.location.href = "/404";
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getMonitorHistory(id) {
|
||||||
|
return $.ajax({
|
||||||
|
url: "/api/v1/monitor/"+id,
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
parseMonitorInfo(monitorInfo) {
|
||||||
|
let tSeries = [];
|
||||||
|
let tLegendData = [];
|
||||||
|
for (let i = 0; i < monitorInfo.result.length; i++) {
|
||||||
|
let loss = 0;
|
||||||
|
let data = [];
|
||||||
|
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
|
||||||
|
avgDelay = Math.round(monitorInfo.result[i].avg_delay[j]);
|
||||||
|
if (avgDelay > 0.9 * MaxTCPPingValue) {
|
||||||
|
loss += 1
|
||||||
|
}
|
||||||
|
if (avgDelay > 0) {
|
||||||
|
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
|
||||||
|
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
|
||||||
|
tLegendData.push(legendName);
|
||||||
|
tSeries.push({
|
||||||
|
name: legendName,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
data: data,
|
||||||
|
markPoint: {
|
||||||
|
data: [
|
||||||
|
{ type: 'max', symbol: 'pin', name: 'Max', itemStyle: { color: '#f00' } },
|
||||||
|
{ type: 'min', symbol: 'pin', name: 'Min', itemStyle: { color: '#0f0' } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.option.title.text = monitorInfo.result[0].server_name;
|
||||||
|
this.option.series = tSeries;
|
||||||
|
this.option.legend.data = tLegendData;
|
||||||
|
this.myChart.clear();
|
||||||
|
this.myChart.setOption(this.option);
|
||||||
|
},
|
||||||
|
isWindowsPlatform(str) {
|
||||||
|
return str.includes('Windows')
|
||||||
|
},
|
||||||
|
renderChart() {
|
||||||
|
this.myChart = echarts.init(this.$refs.chartDom);
|
||||||
|
this.myChart.setOption(this.option);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.myChart.dispose();
|
||||||
|
this.myChart = null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
||||||
+4
-4
@@ -9,7 +9,7 @@
|
|||||||
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
||||||
|
|
||||||
<!-- MDUI CSS -->
|
<!-- MDUI CSS -->
|
||||||
<link rel="stylesheet" href="https://cdn.staticfile.org/mdui/1.0.2/css/mdui.min.css"/>
|
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
|
||||||
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
|
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
|
||||||
{{if ts .CustomCode}}
|
{{if ts .CustomCode}}
|
||||||
{{.CustomCode|safe}}
|
{{.CustomCode|safe}}
|
||||||
@@ -116,9 +116,9 @@
|
|||||||
{{template "theme-mdui/footer" .}}
|
{{template "theme-mdui/footer" .}}
|
||||||
|
|
||||||
<script src="/static/theme-mdui/mdui.js"></script>
|
<script src="/static/theme-mdui/mdui.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/mdui/1.0.2/js/mdui.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/js/mdui.min.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var container=document.querySelector("#container");
|
var container=document.querySelector("#container");
|
||||||
container.style.minHeight=window.innerHeight-document.body.clientHeight+container.clientHeight+'px';
|
container.style.minHeight=window.innerHeight-document.body.clientHeight+container.clientHeight+'px';
|
||||||
|
|||||||
+3
-3
@@ -8,7 +8,7 @@
|
|||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
|
|
||||||
<!-- MDUI CSS -->
|
<!-- MDUI CSS -->
|
||||||
<link rel="stylesheet" href="https://cdn.staticfile.org/mdui/1.0.2/css/mdui.min.css"/>
|
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
|
||||||
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
|
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
|
||||||
{{if ts .CustomCode}}
|
{{if ts .CustomCode}}
|
||||||
{{.CustomCode|safe}}
|
{{.CustomCode|safe}}
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.staticfile.org/mdui/1.0.2/js/mdui.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/js/mdui.min.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var $input = mdui.$('#container').find('.mdui-textfield-input');
|
var $input = mdui.$('#container').find('.mdui-textfield-input');
|
||||||
var $dialog = new mdui.Dialog(mdui.$('.mdui-dialog'));
|
var $dialog = new mdui.Dialog(mdui.$('.mdui-dialog'));
|
||||||
|
|||||||
+14
-1
@@ -1,7 +1,20 @@
|
|||||||
{{define "theme-server-status/content-footer"}}
|
{{define "theme-server-status/content-footer"}}
|
||||||
<footer class="container" style="padding-bottom: 2rem;">
|
<footer class="container" style="padding-bottom: 2rem;">
|
||||||
<p style="text-align: center; font-size: 10px;">
|
<p style="text-align: center; font-size: 10px;">
|
||||||
{{ .Conf.Site.Brand }} | Theme <a target="_blank" href="https://github.com/cppla/ServerStatus">ServerStatus</a> | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}
|
{{ .Conf.Site.Brand }} | Theme ServerStatus | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
<aside class="toolbox">
|
||||||
|
<span class="toggleView">
|
||||||
|
<i v-if="showGroup" @click="toggleView" class="show-nogroup bi bi-justify"></i>
|
||||||
|
<i v-else @click="toggleView" class="show-group bi bi-view-stacked"></i>
|
||||||
|
</span>
|
||||||
|
<span class="setTheme">
|
||||||
|
<i v-if="theme === 'light'" @click="setTheme('dark', true)" class="setTheme-dark bi bi-moon-fill"></i>
|
||||||
|
<i v-else @click="setTheme('light', true)" class="setTheme-light bi bi-brightness-high-fill"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="showGoTop" class="showGoTop">
|
||||||
|
<i @click="goTop" class="goTop bi bi-arrow-up"></i>
|
||||||
|
</span>
|
||||||
|
</aside>
|
||||||
{{end}}
|
{{end}}
|
||||||
+41
-27
@@ -1,7 +1,6 @@
|
|||||||
{{define "theme-server-status/content-nav"}}
|
{{define "theme-server-status/content-nav"}}
|
||||||
<div role="navigation" class="navbar navbar-inverse navbar-fixed-top">
|
<header role="navigation" class="navbar navbar-inverse navbar-fixed-top" style="z-index:99999999;">
|
||||||
<div class="navbar-inner">
|
<div class="container">
|
||||||
<div class="container pl-md-unset">
|
|
||||||
<div class="navbar-header">
|
<div class="navbar-header">
|
||||||
<button data-target=".navbar-collapse" data-toggle="collapse" class="navbar-toggle" type="button">
|
<button data-target=".navbar-collapse" data-toggle="collapse" class="navbar-toggle" type="button">
|
||||||
<span class="sr-only">Toggle navigation</span>
|
<span class="sr-only">Toggle navigation</span>
|
||||||
@@ -9,39 +8,54 @@
|
|||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
</button>
|
</button>
|
||||||
<a href="/" class="navbar-brand pl-md-unset">
|
<a href="/" class="navbar-brand">
|
||||||
<img src="/static/logo.svg?v20210804" style="height: 2rem;display: inline-block;">
|
<img src="/static/logo.svg" style="height: 2rem;display: inline-block;">
|
||||||
{{.Conf.Site.Brand}}
|
{{.Conf.Site.Brand}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-collapse collapse">
|
<nav id="navbar" class="navbar-collapse collapse">
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
<li><a href="/">{{tr "Home" }}</a></li>
|
<li><a href="/"><i class="home icon"></i>{{tr "Home" }}</a></li>
|
||||||
<li><a href="/service">{{tr "Services" }}</a></li>
|
<template v-if="isMobile">
|
||||||
|
<li class="dropdown">
|
||||||
|
<a data-toggle="dropdown"><i class="bi bi-gear-wide-connected" style="position:relative;top:1px;margin-right:3px;font-size:1.1rem;"></i>{{tr "Feature" }}<b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu" style="min-width:100px;">
|
||||||
|
<li><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
|
||||||
|
<li><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<li><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
|
||||||
|
<li><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
|
||||||
|
</template>
|
||||||
|
<li class="dropdown">
|
||||||
|
<a data-toggle="dropdown"><i class="bi bi-incognito" style="position:relative;top:1px;margin-right:3px;font-size:1.2rem;vertical-align:top;"></i>{{tr "Template" }}<b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li v-for="(value, key) in templates" :key="key">
|
||||||
|
<a @click="toggleTemplate(key)">
|
||||||
|
<i class="list icon"></i>@#value#@
|
||||||
|
<i class="check icon" v-if="preferredTemplate === key"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
<li class="dropdown">
|
|
||||||
<a data-toggle="dropdown" href="#">{{tr "Menu" }}<b class="caret"></b></a>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
{{if .Admin}}
|
{{if .Admin}}
|
||||||
<li><a href="/server">{{tr "AdminPanel" }} ({{.Admin.Name}})</a></li>
|
<li class="dropdown">
|
||||||
|
<a data-toggle="dropdown"><i class="user icon"></i>{{.Admin.Name}}<b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu" style="margin-bottom:20px;">
|
||||||
|
<li><a href="/server"><i class="terminal icon"></i>{{tr "AdminPanel" }}</a></li>
|
||||||
|
<li><a @click="logOut({{.Admin.ID}})"><i class="logout icon"></i>{{tr "Logout"}}</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
{{else}}
|
{{else}}
|
||||||
<li><a href="/login">{{tr "Login" }}</a></li>
|
<li><a href="/login"><i class="sign-in icon"></i>{{tr "Login" }}</a></li>
|
||||||
{{end}}
|
{{end}}
|
||||||
<li><a href="#" @click="setSystemTheme">{{tr "FollowSystem" }}
|
|
||||||
<span style="color: #fff" v-if="isSystemTheme"> ✔️</span></a>
|
|
||||||
</li>
|
|
||||||
<li><a href="#" @click="setTheme('dark', true)">{{tr "DarkMode" }}
|
|
||||||
<span v-if="theme === 'dark' && !isSystemTheme"> ✔️</span></a>
|
|
||||||
</li>
|
|
||||||
<li><a href="#" @click="setTheme('light', true)">{{tr "LightMode" }}
|
|
||||||
<span v-if="theme === 'light' && !isSystemTheme"> ✔️</span></a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
+10
-9
@@ -6,16 +6,16 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/bootstrap.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/bootstrap-theme.min.css">
|
<link rel="stylesheet" href="/static/theme-server-status/css/bootstrap-theme.min.css">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20231207">
|
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20240225">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css">
|
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/light.css">
|
<link rel="stylesheet" href="/static/theme-server-status/css/light.css">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
|
||||||
<link rel="stylesheet" href="https://cdn.staticfile.org/font-logos/0.17/font-logos.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-logos@0.17/assets/font-logos.css">
|
||||||
<link rel="stylesheet" href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/[email protected]/css/flag-icons.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/[email protected]/css/flag-icons.min.css">
|
||||||
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.css">
|
||||||
|
<link rel="shortcut icon" type="image/png" href="/static/logo.svg" />
|
||||||
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||||
<!--[if lt IE 9]>
|
<!--[if lt IE 9]>
|
||||||
<script src="/static/theme-server-status/js/html5shiv.js"></script>
|
<script src="/static/theme-server-status/js/html5shiv.js"></script>
|
||||||
@@ -24,10 +24,11 @@
|
|||||||
{{if ts .CustomCode}}
|
{{if ts .CustomCode}}
|
||||||
{{.CustomCode|safe}}
|
{{.CustomCode|safe}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<script src="/static/theme-server-status/js/jquery.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
|
||||||
<script src="/static/theme-server-status/js/bootstrap.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
|
||||||
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script>
|
||||||
<script src="/static/theme-server-status/js/mixin.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
|
||||||
|
<script src="/static/theme-server-status/js/mixin.js?v20240226"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
{{define "theme-server-status/home-group-false"}}
|
||||||
|
<table class="table table-striped table-condensed table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="node-cell status center">{{tr "Status"}}</th>
|
||||||
|
<th class="node-cell name center">{{tr "Name"}}</th>
|
||||||
|
<th class="node-cell os center">{{tr "Platform"}}</th>
|
||||||
|
<th class="node-cell location center">{{tr "Location"}}</th>
|
||||||
|
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
|
||||||
|
<th class="node-cell load center">{{tr "Load"}}</th>
|
||||||
|
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
|
||||||
|
<th class="node-cell traffic center">{{tr "NetTransfer"}}↓|↑</th>
|
||||||
|
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
|
||||||
|
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
|
||||||
|
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="servers">
|
||||||
|
<template v-for="(node,index) in nodesNoTag">
|
||||||
|
<tr :id="'r'+node.ID" data-toggle="collapse" :data-target="'#rt'+node.ID" class="accordion-toggle" :class="index % 2 === 0 ? 'odd': 'even'"
|
||||||
|
aria-expanded="false" @click="showCharts($event, node.ID)">
|
||||||
|
<td class="node-cell status center">
|
||||||
|
<div class="status-container">
|
||||||
|
<div v-if="node.online" class="status-icon online"></div>
|
||||||
|
<div v-else class="status-icon offline"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="node-cell name center">@#node.name#@</td>
|
||||||
|
<td class="node-cell os center">
|
||||||
|
<i v-if='node.os == "darwin"' class="apple icon"></i>
|
||||||
|
<i v-else-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
|
||||||
|
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
|
||||||
|
<span class="node-cell-os-text">@#node.os#@</span>
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center;" class="node-cell location">
|
||||||
|
<i :class="'fi fi-' + node.location"></i>
|
||||||
|
<span class="node-cell-location-text text-uppercase">@#node.location#@</span>
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
|
||||||
|
<td style="text-align: center;" class="node-cell load">@#node.load#@</td>
|
||||||
|
<td style="text-align: center;" class="node-cell network">@#node.network#@</td>
|
||||||
|
<td style="text-align: center;" class="node-cell traffic">@#node.traffic#@</td>
|
||||||
|
<td class="node-cell cpu">
|
||||||
|
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
|
||||||
|
<div :style="node.cpu.style" :class="node.cpu.class"><small>@#node.cpu.percent#@%</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="node-cell memory">
|
||||||
|
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
|
||||||
|
<div :style="node.memory.style" :class="node.memory.class">
|
||||||
|
<small>@#node.memory.percent#@%</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="node-cell hdd">
|
||||||
|
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
|
||||||
|
<div :style="node.hdd.style" :class="node.hdd.class"><small>@#node.hdd.percent#@%</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
|
||||||
|
<td colspan="16">
|
||||||
|
<div class="accordian-body collapse" :id="'rt'+node.ID">
|
||||||
|
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
|
||||||
|
@#node.host.Platform#@-@#node.host.PlatformVersion#@
|
||||||
|
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@]
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand" v-if="node.host.CPU">
|
||||||
|
<span class="node-cell-expand-label">CPU:</span>
|
||||||
|
@#node.host.CPU.join(",")#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "DiskUsed"}}:</span>
|
||||||
|
@#formatByteSize(node.state.DiskUsed)#@ / @#formatByteSize(node.host.DiskTotal)#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "MemUsed"}}:</span>
|
||||||
|
@#formatByteSize(node.state.MemUsed)#@ / @#formatByteSize(node.host.MemTotal)#@(@#toFixed2(node.state.MemUsed / node.host.MemTotal * 100)#@%)
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "SwapUsed"}}:</span>
|
||||||
|
@#formatByteSize(node.state.SwapUsed)#@ / @#formatByteSize(node.host.SwapTotal)#@
|
||||||
|
<span v-if="node.host.SwapTotal">(@#toFixed2(node.state.SwapUsed / node.host.SwapTotal * 100)#@%)</span>
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span>
|
||||||
|
<i class="arrow alternate circle down outline icon"
|
||||||
|
style="margin: 0"></i>@#formatByteSize(node.state.NetInTransfer)#@
|
||||||
|
<i class="arrow alternate circle up outline icon"
|
||||||
|
style="margin: 0"></i>@#formatByteSize(node.state.NetOutTransfer)#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand load">
|
||||||
|
<span class="node-cell-expand-label">{{tr "Load"}}:</span>
|
||||||
|
@#toFixed2(node.state.Load1)#@ / @#toFixed2(node.state.Load5)#@ / @#toFixed2(node.state.Load15)#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "ProcessCount"}}:</span>
|
||||||
|
@#node.state.ProcessCount#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "ConnCount"}}:</span>
|
||||||
|
TCP @#node.state.TcpConnCount#@ / UDP @#node.state.UdpConnCount#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "BootTime"}}:</span>
|
||||||
|
@#formatTimestamp(node.host.BootTime)#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "LastActive"}}:</span>
|
||||||
|
@#new Date(node.lastActive).toLocaleString()#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "Uptime"}}:</span>
|
||||||
|
@#node.uptime#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "Version"}}:</span>
|
||||||
|
@#node.host.Version#@
|
||||||
|
</span>
|
||||||
|
<span class="node-echarts-expand">
|
||||||
|
<div class="chartbox" chartbox-show="0" :key="node.ID" :ref="`chart${node.ID}`" style="width: 100%; height: auto;"></div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
{{define "theme-server-status/home-group-true"}}
|
||||||
|
<table class="table table-striped table-condensed table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr class="node-group-tag">
|
||||||
|
<th colspan="16" style="border:none;">@#(group.Tag!==''?group.Tag:'{{tr "Default"}}')#@</th>
|
||||||
|
</tr>
|
||||||
|
<tr class="node-group-cell">
|
||||||
|
<th class="node-cell status center">{{tr "Status"}}</th>
|
||||||
|
<th class="node-cell name center">{{tr "Name"}}</th>
|
||||||
|
<th class="node-cell os center">{{tr "Platform"}}</th>
|
||||||
|
<th class="node-cell location center">{{tr "Location"}}</th>
|
||||||
|
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
|
||||||
|
<th class="node-cell load center">{{tr "Load"}}</th>
|
||||||
|
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
|
||||||
|
<th class="node-cell traffic center">{{tr "NetTransfer"}}↓|↑</th>
|
||||||
|
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
|
||||||
|
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
|
||||||
|
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="servers">
|
||||||
|
<template v-for="(node,index) in group.data">
|
||||||
|
<tr :id="'r'+node.ID" data-toggle="collapse" :data-target="'#rt'+node.ID" class="accordion-toggle"
|
||||||
|
:class="index % 2 === 0 ? 'odd': 'even'" aria-expanded="false" @click="showCharts($event, node.ID)">
|
||||||
|
<td class="node-cell status center">
|
||||||
|
<div class="status-container">
|
||||||
|
<div v-if="node.online" class="status-icon online"></div>
|
||||||
|
<div v-else class="status-icon offline"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="node-cell name center">@#node.name#@</td>
|
||||||
|
<td class="node-cell os center">
|
||||||
|
<i v-if='node.os == "darwin"' class="apple icon"></i>
|
||||||
|
<i v-else-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
|
||||||
|
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
|
||||||
|
<span class="node-cell-os-text">@#node.os#@</span>
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center;" class="node-cell location">
|
||||||
|
<i :class="'fi fi-' + node.location"></i>
|
||||||
|
<span class="node-cell-location-text text-uppercase"> @#node.location#@</span>
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
|
||||||
|
<td style="text-align: center;" class="node-cell load">@#node.load#@</td>
|
||||||
|
<td style="text-align: center;" class="node-cell network">@#node.network#@</td>
|
||||||
|
<td style="text-align: center;" class="node-cell traffic">@#node.traffic#@</td>
|
||||||
|
<td class="node-cell cpu">
|
||||||
|
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
|
||||||
|
<div :style="node.cpu.style" :class="node.cpu.class"><small>@#node.cpu.percent#@%</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="node-cell memory">
|
||||||
|
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
|
||||||
|
<div :style="node.memory.style" :class="node.memory.class">
|
||||||
|
<small>@#node.memory.percent#@%</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="node-cell hdd">
|
||||||
|
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
|
||||||
|
<div :style="node.hdd.style" :class="node.hdd.class"><small>@#node.hdd.percent#@%</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
|
||||||
|
<td colspan="16">
|
||||||
|
<div class="accordian-body collapse" :id="'rt'+node.ID">
|
||||||
|
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
|
||||||
|
@#node.host.Platform#@-@#node.host.PlatformVersion#@
|
||||||
|
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@]
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand" v-if="node.host.CPU">
|
||||||
|
<span class="node-cell-expand-label">CPU:</span>
|
||||||
|
@#node.host.CPU.join(",")#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "DiskUsed"}}:</span>
|
||||||
|
@#formatByteSize(node.state.DiskUsed)#@ / @#formatByteSize(node.host.DiskTotal)#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "MemUsed"}}:</span>
|
||||||
|
@#formatByteSize(node.state.MemUsed)#@ / @#formatByteSize(node.host.MemTotal)#@(@#toFixed2(node.state.MemUsed / node.host.MemTotal * 100)#@%)
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "SwapUsed"}}:</span>
|
||||||
|
@#formatByteSize(node.state.SwapUsed)#@ / @#formatByteSize(node.host.SwapTotal)#@
|
||||||
|
<span v-if="node.host.SwapTotal">(@#toFixed2(node.state.SwapUsed / node.host.SwapTotal * 100)#@%)</span>
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span>
|
||||||
|
<i class="arrow alternate circle down outline icon"
|
||||||
|
style="margin: 0"></i>@#formatByteSize(node.state.NetInTransfer)#@
|
||||||
|
<i class="arrow alternate circle up outline icon"
|
||||||
|
style="margin: 0"></i>@#formatByteSize(node.state.NetOutTransfer)#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand load">
|
||||||
|
<span class="node-cell-expand-label">{{tr "Load"}}:</span>
|
||||||
|
@#toFixed2(node.state.Load1)#@ / @#toFixed2(node.state.Load5)#@ / @#toFixed2(node.state.Load15)#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "ProcessCount"}}:</span>
|
||||||
|
@#node.state.ProcessCount#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "ConnCount"}}:</span>
|
||||||
|
TCP @#node.state.TcpConnCount#@ / UDP @#node.state.UdpConnCount#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "BootTime"}}:</span>
|
||||||
|
@#formatTimestamp(node.host.BootTime)#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "LastActive"}}:</span>
|
||||||
|
@#new Date(node.lastActive).toLocaleString()#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "Uptime"}}:</span>
|
||||||
|
@#node.uptime#@
|
||||||
|
</span>
|
||||||
|
<span class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "Version"}}:</span>
|
||||||
|
@#node.host.Version#@
|
||||||
|
</span>
|
||||||
|
<span class="node-echarts-expand">
|
||||||
|
<div class="chartbox" chartbox-show="0" :key="node.ID" :ref="`chart${node.ID}`" style="width: 100%; height: auto;"></div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{end}}
|
||||||
+190
-158
@@ -2,152 +2,40 @@
|
|||||||
{{template "theme-server-status/header" .}}
|
{{template "theme-server-status/header" .}}
|
||||||
<div id="app">
|
<div id="app">
|
||||||
{{template "theme-server-status/content-nav" .}}
|
{{template "theme-server-status/content-nav" .}}
|
||||||
<div class="container table-responsive content" style="max-width: 95vw" v-for="group in nodes">
|
<!-- showGroup true -->
|
||||||
<table class="table table-striped table-condensed table-hover">
|
<template v-if="showGroup">
|
||||||
<thead>
|
<section class="container table-responsive content" style="max-width: 95vw" v-for="group in nodesTag">
|
||||||
<tr>
|
{{template "theme-server-status/home-group-true" .}}
|
||||||
<th class="node-group-tag" colspan="16" style="border:none;">@#(group.Tag!==''?group.Tag:'{{tr "Default"}}')#@</th>
|
</section>
|
||||||
</th>
|
</template>
|
||||||
</tr>
|
<!-- showGroup false -->
|
||||||
<tr>
|
<template v-else>
|
||||||
<th class="node-cell status center">{{tr "Status"}}</th>
|
<section class="container table-responsive content" style="max-width: 95vw">
|
||||||
<th class="node-cell name center">{{tr "Name"}}</th>
|
{{template "theme-server-status/home-group-false" .}}
|
||||||
<th class="node-cell os center">{{tr "Platform"}}</th>
|
</section>
|
||||||
<th class="node-cell location center">{{tr "Location"}}</th>
|
|
||||||
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
|
|
||||||
<th class="node-cell load center">{{tr "Load"}}</th>
|
|
||||||
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
|
|
||||||
<th class="node-cell traffic center">{{tr "NetTransfer"}}↓|↑</th>
|
|
||||||
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
|
|
||||||
<th class="node-cell ram center">{{tr "MemUsed"}}</th>
|
|
||||||
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="servers">
|
|
||||||
<template v-for="(node,index) in group.data">
|
|
||||||
<tr :id="'r'+node.ID" data-toggle="collapse" :data-target="'#rt'+node.ID" class="accordion-toggle"
|
|
||||||
:class="index % 2 === 0 ? 'odd': 'even'">
|
|
||||||
<td class="node-cell status center">
|
|
||||||
<div class="status-container">
|
|
||||||
<div v-if="node.online" class="status-icon online"></div>
|
|
||||||
<div v-else class="status-icon offline"></div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="node-cell name center">@#node.name#@</td>
|
|
||||||
<td class="node-cell os center">
|
|
||||||
<i v-if='node.os == "darwin"' class="apple icon"></i>
|
|
||||||
<i v-else-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
|
|
||||||
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
|
|
||||||
<span class="node-cell-os-text">@#node.os#@</span>
|
|
||||||
</td>
|
|
||||||
<td style="text-align: center;" class="node-cell location">
|
|
||||||
<i :class="'fi fi-' + node.location"></i>
|
|
||||||
<span class="node-cell-location-text text-uppercase"> @#node.location#@</span>
|
|
||||||
</td>
|
|
||||||
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
|
|
||||||
<td style="text-align: center;" class="node-cell load">@#node.load#@</td>
|
|
||||||
<td style="text-align: center;" class="node-cell network">@#node.network#@</td>
|
|
||||||
<td style="text-align: center;" class="node-cell traffic">@#node.traffic#@</td>
|
|
||||||
<td class="node-cell cpu">
|
|
||||||
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
|
|
||||||
<div :style="node.cpu.style" :class="node.cpu.class"><small>@#node.cpu.percent#@%</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="node-cell memory">
|
|
||||||
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
|
|
||||||
<div :style="node.memory.style" :class="node.memory.class">
|
|
||||||
<small>@#node.memory.percent#@%</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="node-cell hdd">
|
|
||||||
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
|
|
||||||
<div :style="node.hdd.style" :class="node.hdd.class"><small>@#node.hdd.percent#@%</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
|
|
||||||
<td colspan="16">
|
|
||||||
<div class="accordian-body collapse" :id="'rt'+node.ID">
|
|
||||||
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
|
|
||||||
<span class="node-cell-expand">
|
|
||||||
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
|
|
||||||
@#node.host.Platform#@-@#node.host.PlatformVersion#@
|
|
||||||
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@]
|
|
||||||
</span>
|
|
||||||
<span class="node-cell-expand" v-if="node.host.CPU">
|
|
||||||
<span class="node-cell-expand-label">CPU:</span>
|
|
||||||
@#node.host.CPU.join(",")#@
|
|
||||||
</span>
|
|
||||||
<span class="node-cell-expand">
|
|
||||||
<span class="node-cell-expand-label">{{tr "DiskUsed"}}:</span>
|
|
||||||
@#formatByteSize(node.state.DiskUsed)#@ / @#formatByteSize(node.host.DiskTotal)#@
|
|
||||||
</span>
|
|
||||||
<span class="node-cell-expand">
|
|
||||||
<span class="node-cell-expand-label">{{tr "MemUsed"}}:</span>
|
|
||||||
@#formatByteSize(node.state.MemUsed)#@ / @#formatByteSize(node.host.MemTotal)#@(@#toFixed2(node.state.MemUsed / node.host.MemTotal * 100)#@%)
|
|
||||||
</span>
|
|
||||||
<span class="node-cell-expand">
|
|
||||||
<span class="node-cell-expand-label">{{tr "SwapUsed"}}:</span>
|
|
||||||
@#formatByteSize(node.state.SwapUsed)#@ / @#formatByteSize(node.host.SwapTotal)#@
|
|
||||||
<span v-if="node.host.SwapTotal">(@#toFixed2(node.state.SwapUsed / node.host.SwapTotal * 100)#@%)</span>
|
|
||||||
</span>
|
|
||||||
<span class="node-cell-expand">
|
|
||||||
<span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span>
|
|
||||||
<i class="arrow alternate circle down outline icon"
|
|
||||||
style="margin: 0"></i>@#formatByteSize(node.state.NetInTransfer)#@
|
|
||||||
<i class="arrow alternate circle up outline icon"
|
|
||||||
style="margin: 0"></i>@#formatByteSize(node.state.NetOutTransfer)#@
|
|
||||||
</span>
|
|
||||||
<span class="node-cell-expand load">
|
|
||||||
<span class="node-cell-expand-label">{{tr "Load"}}:</span>
|
|
||||||
@#toFixed2(node.state.Load1)#@ / @#toFixed2(node.state.Load5)#@ / @#toFixed2(node.state.Load15)#@
|
|
||||||
</span>
|
|
||||||
<span class="node-cell-expand">
|
|
||||||
<span class="node-cell-expand-label">{{tr "ProcessCount"}}:</span>
|
|
||||||
@#node.state.ProcessCount#@
|
|
||||||
</span>
|
|
||||||
<span class="node-cell-expand">
|
|
||||||
<span class="node-cell-expand-label">{{tr "ConnCount"}}:</span>
|
|
||||||
TCP @#node.state.TcpConnCount#@ / UDP @#node.state.UdpConnCount#@
|
|
||||||
</span>
|
|
||||||
<span class="node-cell-expand">
|
|
||||||
<span class="node-cell-expand-label">{{tr "BootTime"}}:</span>
|
|
||||||
@#formatTimestamp(node.host.BootTime)#@
|
|
||||||
</span>
|
|
||||||
<span class="node-cell-expand">
|
|
||||||
<span class="node-cell-expand-label">{{tr "LastActive"}}:</span>
|
|
||||||
@#new Date(node.lastActive).toLocaleString()#@
|
|
||||||
</span>
|
|
||||||
<span class="node-cell-expand">
|
|
||||||
<span class="node-cell-expand-label">{{tr "Version"}}:</span>
|
|
||||||
@#node.host.Version#@
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{{template "theme-server-status/content-footer" .}}
|
{{template "theme-server-status/content-footer" .}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
delimiters: ['@#', '#@'],
|
delimiters: ['@#', '#@'],
|
||||||
data: {
|
data: {
|
||||||
nodes: [],
|
page: 'index',
|
||||||
|
defaultTemplate: {{.Conf.Site.Theme}},
|
||||||
|
templates: {{.Themes}},
|
||||||
|
nodesTag: [],
|
||||||
|
nodesNoTag: [],
|
||||||
|
chartDataList: []
|
||||||
},
|
},
|
||||||
mixins: [mixinsVue],
|
mixins: [mixinsVue],
|
||||||
created() {
|
created() {
|
||||||
const initData = JSON.parse('{{.Servers}}').servers;
|
initData = JSON.parse('{{.Servers}}').servers;
|
||||||
this.nodes = groupingData(this.handleNodes(initData),"Tag");
|
if(this.showGroup) {
|
||||||
this.initTheme()
|
this.nodesTag = this.groupingData(this.handleNodes(initData),"Tag");
|
||||||
|
} else {
|
||||||
|
this.nodesNoTag = this.handleNodes(initData);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.connect();
|
this.connect();
|
||||||
@@ -231,13 +119,18 @@
|
|||||||
return x !== "NaN undefined" ? x : '0B'
|
return x !== "NaN undefined" ? x : '0B'
|
||||||
},
|
},
|
||||||
formatPercent(live, used, total) {
|
formatPercent(live, used, total) {
|
||||||
const percent = live ? (this.toFixed2(used / total * 100) || 0) : 0
|
//const percent = live ? (this.toFixed2(used / total * 100) || 0) : 0
|
||||||
return this.formatPercents(percent)
|
const percent = (this.toFixed2(used / total * 100) || 0)
|
||||||
|
return this.formatPercents(live,percent)
|
||||||
},
|
},
|
||||||
formatPercents(percent) {
|
formatPercents(live,percent) {
|
||||||
|
//if(!live) { percent = 0; }
|
||||||
if (percent <= 0) {
|
if (percent <= 0) {
|
||||||
percent = 0;
|
percent = 0;
|
||||||
}
|
}
|
||||||
|
if (percent >= 100) {
|
||||||
|
percent = 100;
|
||||||
|
}
|
||||||
if (!this.cache[percent]) {
|
if (!this.cache[percent]) {
|
||||||
this.cache[percent] = {
|
this.cache[percent] = {
|
||||||
class: 'progress-bar progress-bar-success',
|
class: 'progress-bar progress-bar-success',
|
||||||
@@ -280,7 +173,11 @@
|
|||||||
data.servers[i].live = data.now - lastActive <= 10 * 1000;
|
data.servers[i].live = data.now - lastActive <= 10 * 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.nodes = groupingData(this.handleNodes(data.servers),"Tag");
|
if(this.showGroup) {
|
||||||
|
this.nodesTag = this.groupingData(this.handleNodes(data.servers),"Tag");
|
||||||
|
} else {
|
||||||
|
this.nodesNoTag = this.handleNodes(data.servers);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
@@ -309,7 +206,7 @@
|
|||||||
load: this.toFixed2(server.State.Load1),
|
load: this.toFixed2(server.State.Load1),
|
||||||
network: this.getNetworkSpeed(server.State.NetInSpeed, server.State.NetOutSpeed),
|
network: this.getNetworkSpeed(server.State.NetInSpeed, server.State.NetOutSpeed),
|
||||||
traffic: this.formatByteSize(server.State.NetInTransfer) + ' | ' + this.formatByteSize(server.State.NetOutTransfer),
|
traffic: this.formatByteSize(server.State.NetInTransfer) + ' | ' + this.formatByteSize(server.State.NetOutTransfer),
|
||||||
cpu: this.formatPercents(this.toFixed2(server.State.CPU)),
|
cpu: this.formatPercents(server.live, this.toFixed2(server.State.CPU)),
|
||||||
memory: this.formatPercent(server.live, server.State.MemUsed, server.Host.MemTotal),
|
memory: this.formatPercent(server.live, server.State.MemUsed, server.Host.MemTotal),
|
||||||
hdd: this.formatPercent(server.live, server.State.DiskUsed, server.Host.DiskTotal),
|
hdd: this.formatPercent(server.live, server.State.DiskUsed, server.Host.DiskTotal),
|
||||||
online: server.live,
|
online: server.live,
|
||||||
@@ -324,29 +221,164 @@
|
|||||||
},
|
},
|
||||||
getNetworkSpeed(netInSpeed, netOutSpeed) {
|
getNetworkSpeed(netInSpeed, netOutSpeed) {
|
||||||
return this.formatByteSize(netInSpeed) + ' | ' + this.formatByteSize(netOutSpeed)
|
return this.formatByteSize(netInSpeed) + ' | ' + this.formatByteSize(netOutSpeed)
|
||||||
}
|
},
|
||||||
}
|
showCharts(event, id) {
|
||||||
|
const chartContainer = this.$refs[`chart${id}`][0];
|
||||||
})
|
const chartboxShow = chartContainer.getAttribute('chartbox-show');
|
||||||
|
chartContainer.setAttribute('chartbox-show', chartboxShow === '0' ? '1' : '0');
|
||||||
function groupingData(data, field) {
|
const isAriaExpandedFalse = event.currentTarget.getAttribute('aria-expanded') === 'false';
|
||||||
let map = new Map();
|
if (!isAriaExpandedFalse) return;
|
||||||
let dest = [];
|
// 发起数据请求
|
||||||
|
const url = `/api/v1/monitor/${id}`;
|
||||||
data.forEach(item => {
|
fetch(url)
|
||||||
if (!map.has(item[field])) {
|
.then(response => response.json())
|
||||||
dest.push({
|
.then(data => {
|
||||||
[field]: item[field],
|
if (data.result) { // 数据请求成功,更新数据并渲染图表
|
||||||
data: [item]
|
this.chartDataList[id - 1] = data.result;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.renderCharts(id);
|
||||||
});
|
});
|
||||||
map.set(item[field], item);
|
|
||||||
} else {
|
} else {
|
||||||
dest.find(dItem => dItem[field] === item[field]).data.push(item);
|
console.log('this agent (id:'+ id + ') has no monitor.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
renderCharts(id, reload = false) {
|
||||||
|
if (!this.chartDataList[id - 1]) return;
|
||||||
|
const chartData = this.chartDataList[id - 1];
|
||||||
|
const chartContainer = this.$refs[`chart${id}`][0];
|
||||||
|
if (reload) { //点击切换亮色/暗色风格模式时,重新载入echarts图表的逻辑,
|
||||||
|
// 第一步,查找已经渲染出的图表容器,并销毁它
|
||||||
|
const existingChart = echarts.getInstanceByDom(chartContainer);
|
||||||
|
if (existingChart) existingChart.dispose();
|
||||||
|
// 第二步,如果图表容器处于不可见状态chartboxShow=0,不重新渲染出新的图表,
|
||||||
|
// 如果图表容器处于可见状态chartboxShow=1,重新渲染出新的图表
|
||||||
|
const chartboxShow = chartContainer.getAttribute('chartbox-show');
|
||||||
|
if ( chartboxShow === '0' ) return;
|
||||||
|
}
|
||||||
|
// 定义图表参数值
|
||||||
|
const MaxTCPPingValue = {{.MaxTCPPingValue}} ? {{.MaxTCPPingValue}} : 300;
|
||||||
|
const isMobile = this.checkIsMobile();
|
||||||
|
const fontSize = isMobile ? 10 : 14;
|
||||||
|
const gridLeft = isMobile ? 25 : 36;
|
||||||
|
const gridRight = isMobile ? 5 : 20;
|
||||||
|
const legendLeft = isMobile ? 'center' : 'center';
|
||||||
|
const legendTop = isMobile ? 5 : 5;
|
||||||
|
const legendPadding= isMobile ? [5,0,5,0] : [5,0,5,0];
|
||||||
|
const systemDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
const theme = localStorage.getItem("theme") ? localStorage.getItem("theme") : systemDarkMode;
|
||||||
|
const chartTheme = theme == "dark" ? "dark" : "default";
|
||||||
|
const fontColor = theme == "dark" ? "#f1f1f1" : "#000000";
|
||||||
|
const backgroundColor = theme == "dark" ? "#1C1D26" : '';
|
||||||
|
const tooltipBackgroundColor = theme == "dark" ? "#1C1D26" : '#ffffff';
|
||||||
|
const tooltipBorderColor = theme == "dark" ? "#31363B" : "#ffffff";
|
||||||
|
// 渲染图表
|
||||||
|
const chart = echarts.init(chartContainer, chartTheme, {
|
||||||
|
renderer: 'canvas',
|
||||||
|
useDirtyRect: false,
|
||||||
|
width: 'auto',
|
||||||
|
height: 300,
|
||||||
|
});
|
||||||
|
const xAxisData = chartData[0].created_at.map(time => new Date(time).toLocaleString());
|
||||||
|
const seriesData = chartData.map(item => {
|
||||||
|
let loss = 0;
|
||||||
|
const data = item.avg_delay.map((avgDelay, index) => {
|
||||||
|
if (avgDelay > 0.9 * MaxTCPPingValue) {
|
||||||
|
loss += 1;
|
||||||
|
}
|
||||||
|
return [new Date(item.created_at[index]).toLocaleString(), avgDelay];
|
||||||
|
});
|
||||||
|
const lossRate = ((loss / item.created_at.length) * 100).toFixed(1);
|
||||||
|
if (!item.monitor_name.includes("%")) {
|
||||||
|
item.monitor_name = item.monitor_name + " " + lossRate + "%";
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: item.monitor_name,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
data: data
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const option = {
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
title: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
backgroundColor: tooltipBackgroundColor,
|
||||||
|
borderColor: tooltipBorderColor,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: fontColor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: chartData.map(item => item.monitor_name),
|
||||||
|
show: true,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: fontColor
|
||||||
|
},
|
||||||
|
top: legendTop,
|
||||||
|
bottom: 0,
|
||||||
|
left: legendLeft,
|
||||||
|
padding: legendPadding
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'time',
|
||||||
|
data: xAxisData,
|
||||||
|
axisLabel: {
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLabel: {
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
type: 'slider',
|
||||||
|
start: 0,
|
||||||
|
end: 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: seriesData,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: fontColor
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: '40',
|
||||||
|
left: gridLeft,
|
||||||
|
right: gridRight
|
||||||
|
}
|
||||||
|
};
|
||||||
|
chart.setOption(option);
|
||||||
|
},
|
||||||
|
reloadCharts() { // 重新加载所有图表
|
||||||
|
const data = JSON.parse('{{.Servers}}').servers;
|
||||||
|
data.forEach(node => {
|
||||||
|
const id = node.ID;
|
||||||
|
const chartData = this.chartDataList[id - 1];
|
||||||
|
if (chartData) {
|
||||||
|
this.renderCharts(id,true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return dest;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
{{template "theme-server-status/footer" .}}
|
{{template "theme-server-status/footer" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
+224
@@ -0,0 +1,224 @@
|
|||||||
|
{{define "theme-server-status/network"}}
|
||||||
|
{{template "theme-server-status/header" .}}
|
||||||
|
<div id="app">
|
||||||
|
{{template "theme-server-status/content-nav" .}}
|
||||||
|
<div class="container table-responsive content" style="max-width: 95vw">
|
||||||
|
<table class="table table-striped table-condensed table-hover">
|
||||||
|
<button class="ui nezha-primary-btn button"
|
||||||
|
v-for="server in servers"
|
||||||
|
style="margin-top: 3px"
|
||||||
|
@click="redirectNetwork(server.ID)">
|
||||||
|
@#server.Name#@ <i :class="'fi fi-' + server.Host.CountryCode"></i><span class="node-cell-location-text text-uppercase"> @#server.Host.CountryCode#@</span>
|
||||||
|
</button>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="container table-responsive content" style="max-width: 95vw">
|
||||||
|
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
|
||||||
|
</div>
|
||||||
|
{{template "theme-server-status/content-footer" .}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
|
||||||
|
const initData = JSON.parse('{{.Servers}}').servers;
|
||||||
|
let MaxTCPPingValue = {{.MaxTCPPingValue}};
|
||||||
|
if (MaxTCPPingValue == null) {
|
||||||
|
MaxTCPPingValue = 1000;
|
||||||
|
}
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
delimiters: ['@#', '#@'],
|
||||||
|
data: {
|
||||||
|
page: 'network',
|
||||||
|
defaultTemplate: {{.Conf.Site.Theme}},
|
||||||
|
templates: {{.Themes}},
|
||||||
|
servers: initData,
|
||||||
|
option: {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
position: function (pt) {
|
||||||
|
return [pt[0], '10%'];
|
||||||
|
},
|
||||||
|
formatter: function(params){
|
||||||
|
let result = params[0].axisValueLabel + "<br />";
|
||||||
|
params.forEach(function(item){
|
||||||
|
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
|
||||||
|
})
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
confine: true,
|
||||||
|
transitionDuration: 0
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
text: "",
|
||||||
|
textStyle: {}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: '5%',
|
||||||
|
data: [],
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
dataZoom: {
|
||||||
|
yAxisIndex: 'none'
|
||||||
|
},
|
||||||
|
restore: {},
|
||||||
|
saveAsImage: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
start: 0,
|
||||||
|
end: 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xAxis: {
|
||||||
|
type: 'time',
|
||||||
|
boundaryGap: false
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
boundaryGap: false
|
||||||
|
},
|
||||||
|
series: [],
|
||||||
|
},
|
||||||
|
chartOnOff: true,
|
||||||
|
},
|
||||||
|
mixins: [mixinsVue],
|
||||||
|
mounted() {
|
||||||
|
this.renderChart();
|
||||||
|
this.parseMonitorInfo(monitorInfo);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getFontLogoClass(str) {
|
||||||
|
if (["almalinux",
|
||||||
|
"alpine",
|
||||||
|
"aosc",
|
||||||
|
"apple",
|
||||||
|
"archlinux",
|
||||||
|
"archlabs",
|
||||||
|
"artix",
|
||||||
|
"budgie",
|
||||||
|
"centos",
|
||||||
|
"coreos",
|
||||||
|
"debian",
|
||||||
|
"deepin",
|
||||||
|
"devuan",
|
||||||
|
"docker",
|
||||||
|
"elementary",
|
||||||
|
"fedora",
|
||||||
|
"ferris",
|
||||||
|
"flathub",
|
||||||
|
"freebsd",
|
||||||
|
"gentoo",
|
||||||
|
"gnu-guix",
|
||||||
|
"illumos",
|
||||||
|
"kali-linux",
|
||||||
|
"linuxmint",
|
||||||
|
"mageia",
|
||||||
|
"mandriva",
|
||||||
|
"manjaro",
|
||||||
|
"nixos",
|
||||||
|
"openbsd",
|
||||||
|
"opensuse",
|
||||||
|
"pop-os",
|
||||||
|
"raspberry-pi",
|
||||||
|
"redhat",
|
||||||
|
"rocky-linux",
|
||||||
|
"sabayon",
|
||||||
|
"slackware",
|
||||||
|
"snappy",
|
||||||
|
"solus",
|
||||||
|
"tux",
|
||||||
|
"ubuntu",
|
||||||
|
"void",
|
||||||
|
"zorin"].indexOf(str)
|
||||||
|
> -1) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
if (['openwrt', 'linux', "immortalwrt"].indexOf(str) > -1) {
|
||||||
|
return 'tux';
|
||||||
|
}
|
||||||
|
if (str == 'amazon') {
|
||||||
|
return 'redhat';
|
||||||
|
}
|
||||||
|
if (str == 'arch') {
|
||||||
|
return 'archlinux';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
redirectNetwork(id) {
|
||||||
|
this.getMonitorHistory(id)
|
||||||
|
.then(function(monitorInfo) {
|
||||||
|
var vm = app.__vue__;
|
||||||
|
vm.parseMonitorInfo(monitorInfo);
|
||||||
|
})
|
||||||
|
.catch(function(error){
|
||||||
|
window.location.href = "/404";
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getMonitorHistory(id) {
|
||||||
|
return $.ajax({
|
||||||
|
url: "/api/v1/monitor/"+id,
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
parseMonitorInfo(monitorInfo) {
|
||||||
|
let tSeries = [];
|
||||||
|
let tLegendData = [];
|
||||||
|
for (let i = 0; i < monitorInfo.result.length; i++) {
|
||||||
|
let loss = 0;
|
||||||
|
let data = [];
|
||||||
|
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
|
||||||
|
avgDelay = Math.round(monitorInfo.result[i].avg_delay[j]);
|
||||||
|
if (avgDelay > 0.9 * MaxTCPPingValue) {
|
||||||
|
loss += 1
|
||||||
|
}
|
||||||
|
if (avgDelay > 0) {
|
||||||
|
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
|
||||||
|
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
|
||||||
|
tLegendData.push(legendName);
|
||||||
|
tSeries.push({
|
||||||
|
name: legendName,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
data: data,
|
||||||
|
markPoint: {
|
||||||
|
data: [
|
||||||
|
{ type: 'max', symbol: 'pin', name: 'Max', itemStyle: { color: '#f00' } },
|
||||||
|
{ type: 'min', symbol: 'pin', name: 'Min', itemStyle: { color: '#0f0' } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.option.title.text = monitorInfo.result[0].server_name;
|
||||||
|
this.option.series = tSeries;
|
||||||
|
this.option.legend.data = tLegendData;
|
||||||
|
this.myChart.clear();
|
||||||
|
this.myChart.setOption(this.option);
|
||||||
|
},
|
||||||
|
isWindowsPlatform(str) {
|
||||||
|
return str.includes('Windows')
|
||||||
|
},
|
||||||
|
renderChart() {
|
||||||
|
this.myChart = echarts.init(this.$refs.chartDom);
|
||||||
|
this.myChart.setOption(this.option);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.myChart.dispose();
|
||||||
|
this.myChart = null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{template "theme-server-status/footer" .}}
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{{define "theme-server-status/service-group-false"}}
|
||||||
|
<table class="table table-striped table-condensed table-hover service-status">
|
||||||
|
<thead>
|
||||||
|
<tr class="node-group-tag">
|
||||||
|
<th colspan="16" style="border:none;">
|
||||||
|
{{tr "ServicesManagement"}}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr class="node-group-cell">
|
||||||
|
<th class="node-cell center service-status-th">{{tr "Status"}}</th>
|
||||||
|
<th class="node-cell center service-name-th">{{tr "Name"}}</th>
|
||||||
|
<th class="node-cell center service-details-th">{{tr "Details"}}</th>
|
||||||
|
<th class="node-cell center service-averagelatency-th">{{tr "AverageLatency"}}</th>
|
||||||
|
<th class="node-cell center service-30daysonline-th">{{tr "30DaysOnline"}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for="service in servicesNoTag">
|
||||||
|
<tr>
|
||||||
|
<td class="node-cell center">
|
||||||
|
<div class="delay-today">
|
||||||
|
<i class="delay-today-icon" :class="service.health.className"></i>
|
||||||
|
<span class="delay-today-text">@#service.health.text#@</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="node-cell center">@#service.name#@</td>
|
||||||
|
<td class="node-cell center service-details-td">
|
||||||
|
<template v-for="(item,index) in service.dayDetail">
|
||||||
|
<div data-toggle="tooltip" data-placement="top" class="service-day-status-icon" :class="item.className"
|
||||||
|
:title="item.text">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<td class="node-cell center">@#service.avgDelay#@</td>
|
||||||
|
<td class="node-cell center">
|
||||||
|
<div class="progress">
|
||||||
|
<div :style="service.totalUpTime.style" :class="service.totalUpTime.className">
|
||||||
|
<small>@#service.totalUpTime.percent#@%</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{{define "theme-server-status/service-group-true"}}
|
||||||
|
<table class="table table-striped table-condensed table-hover service-status">
|
||||||
|
<thead>
|
||||||
|
<tr class="node-group-tag">
|
||||||
|
<th colspan="16" style="border:none;">
|
||||||
|
<span v-if="group.type == 1">HTTP-GET</span>
|
||||||
|
<span v-if="group.type == 2">ICMP-Ping</span>
|
||||||
|
<span v-if="group.type == 3">TCP-Ping</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr class="node-group-cell">
|
||||||
|
<th class="node-cell center service-status-th">{{tr "Status"}}</th>
|
||||||
|
<th class="node-cell center service-name-th">{{tr "Name"}}</th>
|
||||||
|
<th class="node-cell center service-details-th">{{tr "Details"}}</th>
|
||||||
|
<th class="node-cell center service-averagelatency-th">{{tr "AverageLatency"}}</th>
|
||||||
|
<th class="node-cell center service-30daysonline-th">{{tr "30DaysOnline"}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for="service in group.data">
|
||||||
|
<tr>
|
||||||
|
<td class="node-cell center">
|
||||||
|
<div class="delay-today">
|
||||||
|
<i class="delay-today-icon" :class="service.health.className"></i>
|
||||||
|
<span class="delay-today-text">@#service.health.text#@</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="node-cell center">@#service.name#@</td>
|
||||||
|
<td class="node-cell center service-details-td">
|
||||||
|
<template v-for="(item,index) in service.dayDetail">
|
||||||
|
<div data-toggle="tooltip" data-placement="top" class="service-day-status-icon" :class="item.className"
|
||||||
|
:title="item.text">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<td class="node-cell center">@#service.avgDelay#@</td>
|
||||||
|
<td class="node-cell center">
|
||||||
|
<div class="progress">
|
||||||
|
<div :style="service.totalUpTime.style" :class="service.totalUpTime.className">
|
||||||
|
<small>@#service.totalUpTime.percent#@%</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+32
-51
@@ -2,55 +2,28 @@
|
|||||||
{{template "theme-server-status/header" .}}
|
{{template "theme-server-status/header" .}}
|
||||||
<div id="app">
|
<div id="app">
|
||||||
{{template "theme-server-status/content-nav" .}}
|
{{template "theme-server-status/content-nav" .}}
|
||||||
<div class="container content" style="max-width: 95vw">
|
<!-- showGroup true -->
|
||||||
<table class="table table-striped table-condensed service-status">
|
<template v-if="showGroup">
|
||||||
<thead>
|
<section class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;" v-for="group in servicesTag">
|
||||||
<tr>
|
{{template "theme-server-status/service-group-true" .}}
|
||||||
<th class="node-cell center" style="min-width:60px">{{tr "Status"}}</th>
|
</section>
|
||||||
<th class="node-cell center" style="min-width:50px">{{tr "Name"}}</th>
|
|
||||||
<th class="node-cell center">{{tr "Details"}}</th>
|
|
||||||
<th class="node-cell center" style="min-width:80px">{{tr "AverageLatency"}}</th>
|
|
||||||
<th class="node-cell center" style="min-width:80px">{{tr "30DaysOnline"}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="servers">
|
|
||||||
<template v-for="service in services">
|
|
||||||
<tr>
|
|
||||||
<td class="node-cell center">
|
|
||||||
<div class="delay-today">
|
|
||||||
<i class="delay-today" :class="service.health.className"></i>
|
|
||||||
@#service.health.text#@
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="node-cell center">@#service.name#@</td>
|
|
||||||
<td class="node-cell center">
|
|
||||||
<template v-for="(item,index) in service.dayDetail">
|
|
||||||
<div class="service-day-status-icon" :class="item.className"
|
|
||||||
:data-tooltip="item.text">
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</td>
|
<!-- showGroup false -->
|
||||||
<td class="node-cell center">@#service.avgDelay#@</td>
|
<template v-else>
|
||||||
<td class="node-cell center">
|
<section class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;">
|
||||||
<div class="progress">
|
{{template "theme-server-status/service-group-false" .}}
|
||||||
<div :style="service.totalUpTime.style" :class="service.totalUpTime.className">
|
</section>
|
||||||
<small>@#service.totalUpTime.percent#@%</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
<section class="container content table-responsive" style="max-width: 95vw">
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container" style="padding:unset;max-width: 95vw">
|
|
||||||
{{if .CycleTransferStats}}
|
{{if .CycleTransferStats}}
|
||||||
<h4 style="text-align: center;">{{tr "CycleTransferStats"}}</h4>
|
<table class="table table-striped table-condensed table-hover">
|
||||||
<div class="table-responsive content">
|
|
||||||
<table class="table table-striped table-condensed">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr class="node-group-tag">
|
||||||
|
<th colspan="16" style="border:none;">
|
||||||
|
{{tr "CycleTransferStats"}}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr class="node-group-cell">
|
||||||
<th class="node-cell center">ID</th>
|
<th class="node-cell center">ID</th>
|
||||||
<th class="node-cell center">{{tr "Rules"}}</th>
|
<th class="node-cell center">{{tr "Rules"}}</th>
|
||||||
<th class="node-cell center">{{tr "Server"}}</th>
|
<th class="node-cell center">{{tr "Server"}}</th>
|
||||||
@@ -89,23 +62,29 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</section>
|
||||||
{{template "theme-server-status/content-footer" .}}
|
{{template "theme-server-status/content-footer" .}}
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
// 初始化 Tooltip
|
||||||
|
$(document).ready(function(){
|
||||||
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
delimiters: ['@#', '#@'],
|
delimiters: ['@#', '#@'],
|
||||||
data: {
|
data: {
|
||||||
services: []
|
page: 'service',
|
||||||
|
defaultTemplate: {{.Conf.Site.Theme}},
|
||||||
|
templates: {{.Themes}},
|
||||||
|
servicesTag: [],
|
||||||
|
servicesNoTag: [],
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.initData()
|
this.initData();
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
},
|
},
|
||||||
@@ -129,6 +108,7 @@
|
|||||||
const services = []
|
const services = []
|
||||||
{{range $service := .Services}}
|
{{range $service := .Services}}
|
||||||
services.push({
|
services.push({
|
||||||
|
type: '{{$service.Monitor.Type}}',
|
||||||
name: '{{$service.Monitor.Name}}',
|
name: '{{$service.Monitor.Name}}',
|
||||||
currentUp: parseInt('{{$service.CurrentUp}}'),
|
currentUp: parseInt('{{$service.CurrentUp}}'),
|
||||||
currentDown: parseInt('{{$service.CurrentDown}}'),
|
currentDown: parseInt('{{$service.CurrentDown}}'),
|
||||||
@@ -147,7 +127,8 @@
|
|||||||
service.dayDetail = this.getDayTails(service)
|
service.dayDetail = this.getDayTails(service)
|
||||||
service.totalUpTime = this.getProgressInfo(this.getPercent(service.totalUp, service.totalDown))
|
service.totalUpTime = this.getProgressInfo(this.getPercent(service.totalUp, service.totalDown))
|
||||||
}
|
}
|
||||||
this.services = services
|
this.servicesTag = this.groupingData(services,"type");
|
||||||
|
this.servicesNoTag = services;
|
||||||
},
|
},
|
||||||
getPercent(up, down) {
|
getPercent(up, down) {
|
||||||
if (!up) {
|
if (!up) {
|
||||||
|
|||||||
@@ -12,3 +12,13 @@ site:
|
|||||||
brand: "nz_site_title"
|
brand: "nz_site_title"
|
||||||
cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改
|
cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改
|
||||||
theme: "default"
|
theme: "default"
|
||||||
|
ddns:
|
||||||
|
enable: false
|
||||||
|
provider: "webhook"
|
||||||
|
accessid: ""
|
||||||
|
accesssecret: ""
|
||||||
|
webhookmethod: ""
|
||||||
|
webhookurl: ""
|
||||||
|
webhookrequestbody: ""
|
||||||
|
webhookheaders: ""
|
||||||
|
maxretries: 3
|
||||||
|
|||||||
+2
-2
@@ -38,8 +38,8 @@ $download = "https://github.com/$agentrepo/releases/download/$agenttag/$file"
|
|||||||
$nssmdownload="https://github.com/$nssmrepo/releases/download/$nssmtag/nssm.zip"
|
$nssmdownload="https://github.com/$nssmrepo/releases/download/$nssmtag/nssm.zip"
|
||||||
Write-Host "Location:$region,connect directly!" -BackgroundColor DarkRed -ForegroundColor Green
|
Write-Host "Location:$region,connect directly!" -BackgroundColor DarkRed -ForegroundColor Green
|
||||||
}else{
|
}else{
|
||||||
$download = "https://dn-dao-github-mirror.daocloud.io/$agentrepo/releases/download/$agenttag/$file"
|
$download = "https://kkgithub.com/$agentrepo/releases/download/$agenttag/$file"
|
||||||
$nssmdownload="https://dn-dao-github-mirror.daocloud.io/$nssmrepo/releases/download/$nssmtag/nssm.zip"
|
$nssmdownload="https://kkgithub.com/$nssmrepo/releases/download/$nssmtag/nssm.zip"
|
||||||
Write-Host "Location:CN,use mirror address" -BackgroundColor DarkRed -ForegroundColor Green
|
Write-Host "Location:CN,use mirror address" -BackgroundColor DarkRed -ForegroundColor Green
|
||||||
}
|
}
|
||||||
echo $download
|
echo $download
|
||||||
|
|||||||
+47
-14
@@ -14,7 +14,7 @@ NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
|
|||||||
NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
|
NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
|
||||||
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
||||||
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
||||||
NZ_VERSION="v0.15.3"
|
NZ_VERSION="v0.15.8"
|
||||||
|
|
||||||
red='\033[0;31m'
|
red='\033[0;31m'
|
||||||
green='\033[0;32m'
|
green='\033[0;32m'
|
||||||
@@ -50,7 +50,7 @@ pre_check() {
|
|||||||
if [[ -z "${CN}" ]]; then
|
if [[ -z "${CN}" ]]; then
|
||||||
if [[ $(curl -m 10 -s https://ipapi.co/json | grep 'China') != "" ]]; then
|
if [[ $(curl -m 10 -s https://ipapi.co/json | grep 'China') != "" ]]; then
|
||||||
echo "根据ipapi.co提供的信息,当前IP可能在中国"
|
echo "根据ipapi.co提供的信息,当前IP可能在中国"
|
||||||
read -e -r -p "是否选用中国镜像完成安装? [Y/n] " input
|
read -e -r -p "是否选用中国镜像完成安装? [Y/n] (自定义镜像输入 3):" input
|
||||||
case $input in
|
case $input in
|
||||||
[yY][eE][sS] | [yY])
|
[yY][eE][sS] | [yY])
|
||||||
echo "使用中国镜像"
|
echo "使用中国镜像"
|
||||||
@@ -60,6 +60,17 @@ pre_check() {
|
|||||||
[nN][oO] | [nN])
|
[nN][oO] | [nN])
|
||||||
echo "不使用中国镜像"
|
echo "不使用中国镜像"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
[3])
|
||||||
|
echo "使用自定义镜像"
|
||||||
|
read -e -r -p "请输入自定义镜像 (例如:dn-dao-github-mirror.daocloud.io),留空为不使用: " input
|
||||||
|
case $input in
|
||||||
|
*)
|
||||||
|
CUSTOM_MIRROR=$input
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "使用中国镜像"
|
echo "使用中国镜像"
|
||||||
CN=true
|
CN=true
|
||||||
@@ -68,21 +79,28 @@ pre_check() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${CUSTOM_MIRROR}" ]]; then
|
||||||
|
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
|
||||||
|
GITHUB_URL=$CUSTOM_MIRROR
|
||||||
|
Get_Docker_URL="get.docker.com"
|
||||||
|
Get_Docker_Argu=" -s docker --mirror Aliyun"
|
||||||
|
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
|
||||||
|
else
|
||||||
if [[ -z "${CN}" ]]; then
|
if [[ -z "${CN}" ]]; then
|
||||||
GITHUB_RAW_URL="raw.githubusercontent.com/naiba/nezha/master"
|
GITHUB_RAW_URL="raw.githubusercontent.com/naiba/nezha/master"
|
||||||
GITHUB_URL="github.com"
|
GITHUB_URL="github.com"
|
||||||
Get_Docker_URL="get.docker.com"
|
Get_Docker_URL="get.docker.com"
|
||||||
Get_Docker_Argu=" "
|
Get_Docker_Argu=" "
|
||||||
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
|
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
|
||||||
GITHUB_RELEASE_URL="github.com/naiba/nezha/releases/latest/download"
|
|
||||||
else
|
else
|
||||||
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
|
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
|
||||||
GITHUB_URL="dn-dao-github-mirror.daocloud.io"
|
GITHUB_URL="kkgithub.com"
|
||||||
Get_Docker_URL="get.docker.com"
|
Get_Docker_URL="get.docker.com"
|
||||||
Get_Docker_Argu=" -s docker --mirror Aliyun"
|
Get_Docker_Argu=" -s docker --mirror Aliyun"
|
||||||
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
|
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
|
||||||
GITHUB_RELEASE_URL="hub.fgit.cf/naiba/nezha/releases/latest/download"
|
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
confirm() {
|
confirm() {
|
||||||
@@ -141,7 +159,7 @@ install_arch(){
|
|||||||
cd /tmp; git clone https://aur.archlinux.org/libsepol.git; cd libsepol; makepkg -si --noconfirm --asdeps; cd ..;
|
cd /tmp; git clone https://aur.archlinux.org/libsepol.git; cd libsepol; makepkg -si --noconfirm --asdeps; cd ..;
|
||||||
git clone https://aur.archlinux.org/libselinux.git; cd libselinux; makepkg -si --noconfirm; cd ..;
|
git clone https://aur.archlinux.org/libselinux.git; cd libselinux; makepkg -si --noconfirm; cd ..;
|
||||||
rm -rf libsepol libselinux'
|
rm -rf libsepol libselinux'
|
||||||
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent&&userdel nezha-agent
|
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent && userdel -r nezha-agent
|
||||||
echo -e "${red}提示: ${plain}已删除用户nezha-agent,请务必手动核查一遍!\n"
|
echo -e "${red}提示: ${plain}已删除用户nezha-agent,请务必手动核查一遍!\n"
|
||||||
;;
|
;;
|
||||||
[nN][oO] | [nN])
|
[nN][oO] | [nN])
|
||||||
@@ -251,7 +269,8 @@ install_dashboard_standalone() {
|
|||||||
|
|
||||||
selinux() {
|
selinux() {
|
||||||
#判断当前的状态
|
#判断当前的状态
|
||||||
if [ "$os_alpine" != 1 ];then
|
command -v getenforce >/dev/null 2>&1
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
getenforce | grep '[Ee]nfor'
|
getenforce | grep '[Ee]nfor'
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo -e "SELinux是开启状态,正在关闭!"
|
echo -e "SELinux是开启状态,正在关闭!"
|
||||||
@@ -510,7 +529,6 @@ modify_dashboard_config_standalone() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
echo -e "面板配置 ${green}修改成功,请稍等重启生效${plain}"
|
echo -e "面板配置 ${green}修改成功,请稍等重启生效${plain}"
|
||||||
|
|
||||||
restart_and_update_standalone
|
restart_and_update_standalone
|
||||||
@@ -559,7 +577,7 @@ restart_and_update_standalone() {
|
|||||||
rc-service nezha-dashboard stop
|
rc-service nezha-dashboard stop
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wget -qO app.zip $GITHUB_RELEASE_URL/dashboard-linux-$os_arch.zip >/dev/null 2>&1 && unzip -qq app.zip && mv dist/dashboard-linux-$os_arch app && rm -r app.zip dist
|
wget -qO app.zip https://${GITHUB_URL}/naiba/nezha/releases/latest/download/dashboard-linux-$os_arch.zip >/dev/null 2>&1 && unzip -qq app.zip && mv dist/dashboard-linux-$os_arch app && rm -r app.zip dist
|
||||||
|
|
||||||
if [ "$os_alpine" != 1 ]; then
|
if [ "$os_alpine" != 1 ]; then
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
@@ -797,14 +815,29 @@ select_version() {
|
|||||||
DOCKER_COMPOSE_COMMAND=""
|
DOCKER_COMPOSE_COMMAND=""
|
||||||
if docker compose version >/dev/null 2>&1; then
|
if docker compose version >/dev/null 2>&1; then
|
||||||
DOCKER_COMPOSE_COMMAND="docker compose"
|
DOCKER_COMPOSE_COMMAND="docker compose"
|
||||||
elif command -v docker-compose >/dev/null 2>&1; then
|
|
||||||
DOCKER_COMPOSE_COMMAND="docker-compose"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n $DOCKER_COMPOSE_COMMAND ]]; then
|
|
||||||
if $DOCKER_COMPOSE_COMMAND ls | grep -qw "$NZ_DASHBOARD_PATH/docker-compose.yaml" >/dev/null 2>&1; then
|
if $DOCKER_COMPOSE_COMMAND ls | grep -qw "$NZ_DASHBOARD_PATH/docker-compose.yaml" >/dev/null 2>&1; then
|
||||||
|
NEZHA_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -w "nezha-dashboard")
|
||||||
|
if [ -n "$NEZHA_IMAGES" ]; then
|
||||||
|
echo "存在带有 nezha-dashboard 仓库的 Docker 镜像:"
|
||||||
|
echo "$NEZHA_IMAGES"
|
||||||
IS_DOCKER_NEZHA=1
|
IS_DOCKER_NEZHA=1
|
||||||
return
|
return
|
||||||
|
else
|
||||||
|
echo "未找到带有 nezha-dashboard 仓库的 Docker 镜像。"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
DOCKER_COMPOSE_COMMAND="docker-compose"
|
||||||
|
if $DOCKER_COMPOSE_COMMAND -f "$NZ_DASHBOARD_PATH/docker-compose.yaml" config >/dev/null 2>&1; then
|
||||||
|
NEZHA_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -w "nezha-dashboard")
|
||||||
|
if [ -n "$NEZHA_IMAGES" ]; then
|
||||||
|
echo "存在带有 nezha-dashboard 仓库的 Docker 镜像:"
|
||||||
|
echo "$NEZHA_IMAGES"
|
||||||
|
IS_DOCKER_NEZHA=1
|
||||||
|
return
|
||||||
|
else
|
||||||
|
echo "未找到带有 nezha-dashboard 仓库的 Docker 镜像。"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+46
-14
@@ -14,7 +14,7 @@ NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
|
|||||||
NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
|
NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
|
||||||
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
||||||
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
||||||
NZ_VERSION="v0.15.3"
|
NZ_VERSION="v0.15.8"
|
||||||
|
|
||||||
red='\033[0;31m'
|
red='\033[0;31m'
|
||||||
green='\033[0;32m'
|
green='\033[0;32m'
|
||||||
@@ -50,7 +50,7 @@ pre_check() {
|
|||||||
if [[ -z "${CN}" ]]; then
|
if [[ -z "${CN}" ]]; then
|
||||||
if [[ $(curl -m 10 -s https://ipapi.co/json | grep 'China') != "" ]]; then
|
if [[ $(curl -m 10 -s https://ipapi.co/json | grep 'China') != "" ]]; then
|
||||||
echo "According to the information provided by ipapi.co, the current IP may be in China"
|
echo "According to the information provided by ipapi.co, the current IP may be in China"
|
||||||
read -e -r -p "Is the installation done with a Chinese Mirror? [Y/n] " input
|
read -e -r -p "Is the installation done with a Chinese Mirror? [Y/n] (Custom Mirror Input 3):" input
|
||||||
case $input in
|
case $input in
|
||||||
[yY][eE][sS] | [yY])
|
[yY][eE][sS] | [yY])
|
||||||
echo "Use Chinese Mirror"
|
echo "Use Chinese Mirror"
|
||||||
@@ -60,6 +60,17 @@ pre_check() {
|
|||||||
[nN][oO] | [nN])
|
[nN][oO] | [nN])
|
||||||
echo "No Use Chinese Mirror"
|
echo "No Use Chinese Mirror"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
[3])
|
||||||
|
echo "Use Custom Mirror"
|
||||||
|
read -e -r -p "Please enter a custom image (e.g. :dn-dao-github-mirror.daocloud.io), leave blank to nouse: " input
|
||||||
|
case $input in
|
||||||
|
*)
|
||||||
|
CUSTOM_MIRROR=$input
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "No Use Chinese Mirror"
|
echo "No Use Chinese Mirror"
|
||||||
;;
|
;;
|
||||||
@@ -67,20 +78,26 @@ pre_check() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${CUSTOM_MIRROR}" ]]; then
|
||||||
|
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
|
||||||
|
GITHUB_URL=$CUSTOM_MIRROR
|
||||||
|
Get_Docker_URL="get.docker.com"
|
||||||
|
Get_Docker_Argu=" -s docker --mirror Aliyun"
|
||||||
|
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
|
||||||
|
else
|
||||||
if [[ -z "${CN}" ]]; then
|
if [[ -z "${CN}" ]]; then
|
||||||
GITHUB_RAW_URL="raw.githubusercontent.com/naiba/nezha/master"
|
GITHUB_RAW_URL="raw.githubusercontent.com/naiba/nezha/master"
|
||||||
GITHUB_URL="github.com"
|
GITHUB_URL="github.com"
|
||||||
Get_Docker_URL="get.docker.com"
|
Get_Docker_URL="get.docker.com"
|
||||||
Get_Docker_Argu=" "
|
Get_Docker_Argu=" "
|
||||||
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
|
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
|
||||||
GITHUB_RELEASE_URL="github.com/naiba/nezha/releases/latest/download"
|
|
||||||
else
|
else
|
||||||
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
|
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
|
||||||
GITHUB_URL="dn-dao-github-mirror.daocloud.io"
|
GITHUB_URL="kkgithub.com"
|
||||||
Get_Docker_URL="get.docker.com"
|
Get_Docker_URL="get.docker.com"
|
||||||
Get_Docker_Argu=" -s docker --mirror Aliyun"
|
Get_Docker_Argu=" -s docker --mirror Aliyun"
|
||||||
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
|
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
|
||||||
GITHUB_RELEASE_URL="hub.fgit.cf/naiba/nezha/releases/latest/download"
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +157,7 @@ install_arch(){
|
|||||||
cd /tmp; git clone https://aur.archlinux.org/libsepol.git; cd libsepol; makepkg -si --noconfirm --asdeps; cd ..;
|
cd /tmp; git clone https://aur.archlinux.org/libsepol.git; cd libsepol; makepkg -si --noconfirm --asdeps; cd ..;
|
||||||
git clone https://aur.archlinux.org/libselinux.git; cd libselinux; makepkg -si --noconfirm; cd ..;
|
git clone https://aur.archlinux.org/libselinux.git; cd libselinux; makepkg -si --noconfirm; cd ..;
|
||||||
rm -rf libsepol libselinux'
|
rm -rf libsepol libselinux'
|
||||||
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent&&userdel nezha-agent
|
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent && userdel -r nezha-agent
|
||||||
echo -e "${red}Info: ${plain}user nezha-agent has been deleted, Be sure to check it manually!\n"
|
echo -e "${red}Info: ${plain}user nezha-agent has been deleted, Be sure to check it manually!\n"
|
||||||
;;
|
;;
|
||||||
[nN][oO] | [nN])
|
[nN][oO] | [nN])
|
||||||
@@ -249,7 +266,8 @@ install_dashboard_standalone() {
|
|||||||
|
|
||||||
selinux() {
|
selinux() {
|
||||||
#Check SELinux
|
#Check SELinux
|
||||||
if [ "$os_alpine" != 1 ];then
|
command -v getenforce >/dev/null 2>&1
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
getenforce | grep '[Ee]nfor'
|
getenforce | grep '[Ee]nfor'
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo -e "SELinux running,closing now!"
|
echo -e "SELinux running,closing now!"
|
||||||
@@ -508,7 +526,6 @@ modify_dashboard_config_standalone() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
echo -e "Dashboard configuration ${green} modified successfully, please wait for Dashboard self-restart to take effect${plain}"
|
echo -e "Dashboard configuration ${green} modified successfully, please wait for Dashboard self-restart to take effect${plain}"
|
||||||
|
|
||||||
restart_and_update_standalone
|
restart_and_update_standalone
|
||||||
@@ -557,7 +574,7 @@ restart_and_update_standalone() {
|
|||||||
rc-service nezha-dashboard stop
|
rc-service nezha-dashboard stop
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wget -qO app.zip $GITHUB_RELEASE_URL/dashboard-linux-$os_arch.zip >/dev/null 2>&1 && unzip -qq app.zip && mv dist/dashboard-linux-$os_arch app && rm -r app.zip dist
|
wget -qO app.zip https://${GITHUB_URL}/naiba/nezha/releases/latest/download/dashboard-linux-$os_arch.zip >/dev/null 2>&1 && unzip -qq app.zip && mv dist/dashboard-linux-$os_arch app && rm -r app.zip dist
|
||||||
|
|
||||||
if [ "$os_alpine" != 1 ]; then
|
if [ "$os_alpine" != 1 ]; then
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
@@ -795,14 +812,29 @@ select_version() {
|
|||||||
DOCKER_COMPOSE_COMMAND=""
|
DOCKER_COMPOSE_COMMAND=""
|
||||||
if docker compose version >/dev/null 2>&1; then
|
if docker compose version >/dev/null 2>&1; then
|
||||||
DOCKER_COMPOSE_COMMAND="docker compose"
|
DOCKER_COMPOSE_COMMAND="docker compose"
|
||||||
elif command -v docker-compose >/dev/null 2>&1; then
|
|
||||||
DOCKER_COMPOSE_COMMAND="docker-compose"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n $DOCKER_COMPOSE_COMMAND ]]; then
|
|
||||||
if $DOCKER_COMPOSE_COMMAND ls | grep -qw "$NZ_DASHBOARD_PATH/docker-compose.yaml" >/dev/null 2>&1; then
|
if $DOCKER_COMPOSE_COMMAND ls | grep -qw "$NZ_DASHBOARD_PATH/docker-compose.yaml" >/dev/null 2>&1; then
|
||||||
|
NEZHA_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -w "nezha-dashboard")
|
||||||
|
if [ -n "$NEZHA_IMAGES" ]; then
|
||||||
|
echo "Docker image with nezha-dashboard repository exists:"
|
||||||
|
echo "$NEZHA_IMAGES"
|
||||||
IS_DOCKER_NEZHA=1
|
IS_DOCKER_NEZHA=1
|
||||||
return
|
return
|
||||||
|
else
|
||||||
|
echo "No Docker images with the nezha-dashboard repository were found."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
DOCKER_COMPOSE_COMMAND="docker-compose"
|
||||||
|
if $DOCKER_COMPOSE_COMMAND -f "$NZ_DASHBOARD_PATH/docker-compose.yaml" config >/dev/null 2>&1; then
|
||||||
|
NEZHA_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -w "nezha-dashboard")
|
||||||
|
if [ -n "$NEZHA_IMAGES" ]; then
|
||||||
|
echo "Docker image with nezha-dashboard repository exists:"
|
||||||
|
echo "$NEZHA_IMAGES"
|
||||||
|
IS_DOCKER_NEZHA=1
|
||||||
|
return
|
||||||
|
else
|
||||||
|
echo "No Docker images with the nezha-dashboard repository were found."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
#!/sbin/openrc-run
|
#!/sbin/openrc-run
|
||||||
|
supervisor=supervise-daemon
|
||||||
|
name=nezha-agent
|
||||||
|
supervise_daemon_args="--stdout /var/log/${name}.log --stderr /var/log/${name}.err"
|
||||||
SERVER="nz_grpc_host:nz_grpc_port" #Dashboard 地址 ip:port
|
SERVER="nz_grpc_host:nz_grpc_port" #Dashboard 地址 ip:port
|
||||||
SECRET="nz_client_secret" #SECRET
|
SECRET="nz_client_secret" #SECRET
|
||||||
NZ_BASE_PATH="/opt/nezha"
|
NZ_BASE_PATH="/opt/nezha"
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package rpc
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/naiba/nezha/pkg/ddns"
|
||||||
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
@@ -110,6 +113,36 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
|
|||||||
host := model.PB2Host(r)
|
host := model.PB2Host(r)
|
||||||
singleton.ServerLock.RLock()
|
singleton.ServerLock.RLock()
|
||||||
defer singleton.ServerLock.RUnlock()
|
defer singleton.ServerLock.RUnlock()
|
||||||
|
|
||||||
|
// 检查并更新DDNS
|
||||||
|
if singleton.Conf.DDNS.Enable &&
|
||||||
|
singleton.ServerList[clientID].EnableDDNS &&
|
||||||
|
singleton.ServerList[clientID].Host != nil &&
|
||||||
|
host.IP != "" &&
|
||||||
|
singleton.ServerList[clientID].Host.IP != host.IP {
|
||||||
|
|
||||||
|
serverDomain := singleton.ServerList[clientID].DDNSDomain
|
||||||
|
provider, err := singleton.GetDDNSProviderFromString(singleton.Conf.DDNS.Provider)
|
||||||
|
if err == nil && serverDomain != "" {
|
||||||
|
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
|
||||||
|
maxRetries := int(singleton.Conf.DDNS.MaxRetries)
|
||||||
|
config := &ddns.DomainConfig{
|
||||||
|
EnableIPv4: true,
|
||||||
|
EnableIpv6: true,
|
||||||
|
FullDomain: serverDomain,
|
||||||
|
Ipv4Addr: ipv4,
|
||||||
|
Ipv6Addr: ipv6,
|
||||||
|
}
|
||||||
|
go singleton.RetryableUpdateDomain(provider, config, maxRetries)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 虽然会在启动时panic, 可以断言不会走这个分支, 但是考虑到动态加载配置或者其它情况, 这里输出一下方便检查奇奇怪怪的BUG
|
||||||
|
log.Printf("NEZHA>> 未找到对应的DDNS提供者(%s), 请前往config.yml检查你的设置\n", singleton.Conf.DDNS.Provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送IP变动通知
|
||||||
if singleton.Conf.EnableIPChangeNotification &&
|
if singleton.Conf.EnableIPChangeNotification &&
|
||||||
((singleton.Conf.Cover == model.ConfigCoverAll && !singleton.Conf.IgnoredIPNotificationServerIDs[clientID]) ||
|
((singleton.Conf.Cover == model.ConfigCoverAll && !singleton.Conf.IgnoredIPNotificationServerIDs[clientID]) ||
|
||||||
(singleton.Conf.Cover == model.ConfigCoverIgnoreAll && singleton.Conf.IgnoredIPNotificationServerIDs[clientID])) &&
|
(singleton.Conf.Cover == model.ConfigCoverIgnoreAll && singleton.Conf.IgnoredIPNotificationServerIDs[clientID])) &&
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package singleton
|
package singleton
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/naiba/nezha/model"
|
"github.com/naiba/nezha/model"
|
||||||
"github.com/naiba/nezha/pkg/utils"
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -12,6 +14,9 @@ var (
|
|||||||
ApiLock sync.RWMutex
|
ApiLock sync.RWMutex
|
||||||
|
|
||||||
ServerAPI = &ServerAPIService{}
|
ServerAPI = &ServerAPIService{}
|
||||||
|
MonitorAPI = &MonitorAPIService{}
|
||||||
|
|
||||||
|
once = &sync.Once{}
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerAPIService struct{}
|
type ServerAPIService struct{}
|
||||||
@@ -51,6 +56,23 @@ type ServerInfoResponse struct {
|
|||||||
Result []*CommonServerInfo `json:"result"`
|
Result []*CommonServerInfo `json:"result"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MonitorAPIService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorInfoResponse struct {
|
||||||
|
CommonResponse
|
||||||
|
Result []*MonitorInfo `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorInfo struct {
|
||||||
|
MonitorID uint64 `json:"monitor_id"`
|
||||||
|
ServerID uint64 `json:"server_id"`
|
||||||
|
MonitorName string `json:"monitor_name"`
|
||||||
|
ServerName string `json:"server_name"`
|
||||||
|
CreatedAt []int64 `json:"created_at"`
|
||||||
|
AvgDelay []float32 `json:"avg_delay"`
|
||||||
|
}
|
||||||
|
|
||||||
func InitAPI() {
|
func InitAPI() {
|
||||||
ApiTokenList = make(map[string]*model.ApiToken)
|
ApiTokenList = make(map[string]*model.ApiToken)
|
||||||
UserIDToApiTokenList = make(map[uint64][]string)
|
UserIDToApiTokenList = make(map[uint64][]string)
|
||||||
@@ -203,3 +225,45 @@ func (s *ServerAPIService) GetAllList() *ServerInfoResponse {
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MonitorAPIService) GetMonitorHistories(query map[string]any) *MonitorInfoResponse {
|
||||||
|
var (
|
||||||
|
resultMap = make(map[uint64]*MonitorInfo)
|
||||||
|
monitorHistories []*model.MonitorHistory
|
||||||
|
sortedMonitorIDs []uint64
|
||||||
|
)
|
||||||
|
res := &MonitorInfoResponse{
|
||||||
|
CommonResponse: CommonResponse{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := DB.Model(&model.MonitorHistory{}).Select("monitor_id, created_at, server_id, avg_delay").
|
||||||
|
Where(query).Where("created_at >= ?", time.Now().Add(-24*time.Hour)).Order("monitor_id, created_at").
|
||||||
|
Scan(&monitorHistories).Error; err != nil {
|
||||||
|
res.CommonResponse = CommonResponse{
|
||||||
|
Code: 500,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, history := range monitorHistories {
|
||||||
|
infos, ok := resultMap[history.MonitorID]
|
||||||
|
if !ok {
|
||||||
|
infos = &MonitorInfo{
|
||||||
|
MonitorID: history.MonitorID,
|
||||||
|
ServerID: history.ServerID,
|
||||||
|
MonitorName: ServiceSentinelShared.monitors[history.MonitorID].Name,
|
||||||
|
ServerName: ServerList[history.ServerID].Name,
|
||||||
|
}
|
||||||
|
resultMap[history.MonitorID] = infos
|
||||||
|
sortedMonitorIDs = append(sortedMonitorIDs, history.MonitorID)
|
||||||
|
}
|
||||||
|
infos.CreatedAt = append(infos.CreatedAt, history.CreatedAt.Truncate(time.Minute).Unix()*1000)
|
||||||
|
infos.AvgDelay = append(infos.AvgDelay, history.AvgDelay)
|
||||||
|
}
|
||||||
|
for _, monitorID := range sortedMonitorIDs {
|
||||||
|
res.Result = append(res.Result, resultMap[monitorID])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package singleton
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
ddns2 "github.com/naiba/nezha/pkg/ddns"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RetryableUpdateDomain(provider ddns2.Provider, config *ddns2.DomainConfig, maxRetries int) bool {
|
||||||
|
if nil == config {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for retries := 0; retries < maxRetries; retries++ {
|
||||||
|
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)\n", config.FullDomain, retries+1, maxRetries)
|
||||||
|
if provider.UpdateDomain(config) {
|
||||||
|
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功\n", config.FullDomain)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败\n", config.FullDomain)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) {
|
||||||
|
switch provider {
|
||||||
|
case "webhook":
|
||||||
|
return ddns2.ProviderWebHook{
|
||||||
|
URL: Conf.DDNS.WebhookURL,
|
||||||
|
RequestMethod: Conf.DDNS.WebhookMethod,
|
||||||
|
RequestBody: Conf.DDNS.WebhookRequestBody,
|
||||||
|
RequestHeader: Conf.DDNS.WebhookHeaders,
|
||||||
|
}, nil
|
||||||
|
case "dummy":
|
||||||
|
return ddns2.ProviderDummy{}, nil
|
||||||
|
case "cloudflare":
|
||||||
|
return ddns2.ProviderCloudflare{
|
||||||
|
Secret: Conf.DDNS.AccessSecret,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider))
|
||||||
|
}
|
||||||
@@ -8,9 +8,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
|
||||||
"github.com/naiba/nezha/model"
|
"github.com/naiba/nezha/model"
|
||||||
pb "github.com/naiba/nezha/proto"
|
pb "github.com/naiba/nezha/proto"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -36,12 +37,13 @@ func NewServiceSentinel(serviceSentinelDispatchBus chan<- model.Monitor) {
|
|||||||
ServiceSentinelShared = &ServiceSentinel{
|
ServiceSentinelShared = &ServiceSentinel{
|
||||||
serviceReportChannel: make(chan ReportData, 200),
|
serviceReportChannel: make(chan ReportData, 200),
|
||||||
serviceStatusToday: make(map[uint64]*_TodayStatsOfMonitor),
|
serviceStatusToday: make(map[uint64]*_TodayStatsOfMonitor),
|
||||||
serviceCurrentStatusIndex: make(map[uint64]int),
|
serviceCurrentStatusIndex: make(map[uint64]*indexStore),
|
||||||
serviceCurrentStatusData: make(map[uint64][]*pb.TaskResult),
|
serviceCurrentStatusData: make(map[uint64][]*pb.TaskResult),
|
||||||
lastStatus: make(map[uint64]int),
|
lastStatus: make(map[uint64]int),
|
||||||
serviceResponseDataStoreCurrentUp: make(map[uint64]uint64),
|
serviceResponseDataStoreCurrentUp: make(map[uint64]uint64),
|
||||||
serviceResponseDataStoreCurrentDown: make(map[uint64]uint64),
|
serviceResponseDataStoreCurrentDown: make(map[uint64]uint64),
|
||||||
serviceResponseDataStoreCurrentAvgDelay: make(map[uint64]float32),
|
serviceResponseDataStoreCurrentAvgDelay: make(map[uint64]float32),
|
||||||
|
serviceResponsePing: make(map[uint64]map[uint64]*pingStore),
|
||||||
monitors: make(map[uint64]*model.Monitor),
|
monitors: make(map[uint64]*model.Monitor),
|
||||||
sslCertCache: make(map[uint64]string),
|
sslCertCache: make(map[uint64]string),
|
||||||
// 30天数据缓存
|
// 30天数据缓存
|
||||||
@@ -95,11 +97,12 @@ type ServiceSentinel struct {
|
|||||||
|
|
||||||
serviceResponseDataStoreLock sync.RWMutex
|
serviceResponseDataStoreLock sync.RWMutex
|
||||||
serviceStatusToday map[uint64]*_TodayStatsOfMonitor // [monitor_id] -> _TodayStatsOfMonitor
|
serviceStatusToday map[uint64]*_TodayStatsOfMonitor // [monitor_id] -> _TodayStatsOfMonitor
|
||||||
serviceCurrentStatusIndex map[uint64]int // [monitor_id] -> 该监控ID对应的 serviceCurrentStatusData 的最新索引下标
|
serviceCurrentStatusIndex map[uint64]*indexStore // [monitor_id] -> 该监控ID对应的 serviceCurrentStatusData 的最新索引下标
|
||||||
serviceCurrentStatusData map[uint64][]*pb.TaskResult // [monitor_id] -> []model.MonitorHistory
|
serviceCurrentStatusData map[uint64][]*pb.TaskResult // [monitor_id] -> []model.MonitorHistory
|
||||||
serviceResponseDataStoreCurrentUp map[uint64]uint64 // [monitor_id] -> 当前服务在线计数
|
serviceResponseDataStoreCurrentUp map[uint64]uint64 // [monitor_id] -> 当前服务在线计数
|
||||||
serviceResponseDataStoreCurrentDown map[uint64]uint64 // [monitor_id] -> 当前服务离线计数
|
serviceResponseDataStoreCurrentDown map[uint64]uint64 // [monitor_id] -> 当前服务离线计数
|
||||||
serviceResponseDataStoreCurrentAvgDelay map[uint64]float32 // [monitor_id] -> 当前服务离线计数
|
serviceResponseDataStoreCurrentAvgDelay map[uint64]float32 // [monitor_id] -> 当前服务离线计数
|
||||||
|
serviceResponsePing map[uint64]map[uint64]*pingStore // [monitor_id] -> ClientID -> delay
|
||||||
lastStatus map[uint64]int
|
lastStatus map[uint64]int
|
||||||
sslCertCache map[uint64]string
|
sslCertCache map[uint64]string
|
||||||
|
|
||||||
@@ -111,6 +114,16 @@ type ServiceSentinel struct {
|
|||||||
monthlyStatus map[uint64]*model.ServiceItemResponse // [monitor_id] -> model.ServiceItemResponse
|
monthlyStatus map[uint64]*model.ServiceItemResponse // [monitor_id] -> model.ServiceItemResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type indexStore struct {
|
||||||
|
index int
|
||||||
|
t time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type pingStore struct {
|
||||||
|
count int
|
||||||
|
ping float32
|
||||||
|
}
|
||||||
|
|
||||||
func (ss *ServiceSentinel) refreshMonthlyServiceStatus() {
|
func (ss *ServiceSentinel) refreshMonthlyServiceStatus() {
|
||||||
// 刷新数据防止无人访问
|
// 刷新数据防止无人访问
|
||||||
ss.LoadStats()
|
ss.LoadStats()
|
||||||
@@ -326,6 +339,34 @@ func (ss *ServiceSentinel) worker() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mh := r.Data
|
mh := r.Data
|
||||||
|
if mh.Type == model.TaskTypeTCPPing || mh.Type == model.TaskTypeICMPPing {
|
||||||
|
monitorTcpMap, ok := ss.serviceResponsePing[mh.GetId()]
|
||||||
|
if !ok {
|
||||||
|
monitorTcpMap = make(map[uint64]*pingStore)
|
||||||
|
ss.serviceResponsePing[mh.GetId()] = monitorTcpMap
|
||||||
|
}
|
||||||
|
ts, ok := monitorTcpMap[r.Reporter]
|
||||||
|
if !ok {
|
||||||
|
ts = &pingStore{}
|
||||||
|
}
|
||||||
|
ts.count++
|
||||||
|
ts.ping = (ts.ping*float32(ts.count-1) + mh.Delay) / float32(ts.count)
|
||||||
|
if ts.count == Conf.AvgPingCount {
|
||||||
|
if ts.ping > float32(Conf.MaxTCPPingValue) {
|
||||||
|
ts.ping = float32(Conf.MaxTCPPingValue)
|
||||||
|
}
|
||||||
|
ts.count = 0
|
||||||
|
if err := DB.Create(&model.MonitorHistory{
|
||||||
|
MonitorID: mh.GetId(),
|
||||||
|
AvgDelay: ts.ping,
|
||||||
|
Data: mh.Data,
|
||||||
|
ServerID: r.Reporter,
|
||||||
|
}).Error; err != nil {
|
||||||
|
log.Println("NEZHA>> 服务监控数据持久化失败:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
monitorTcpMap[r.Reporter] = ts
|
||||||
|
}
|
||||||
ss.serviceResponseDataStoreLock.Lock()
|
ss.serviceResponseDataStoreLock.Lock()
|
||||||
// 写入当天状态
|
// 写入当天状态
|
||||||
if mh.Successful {
|
if mh.Successful {
|
||||||
@@ -336,9 +377,20 @@ func (ss *ServiceSentinel) worker() {
|
|||||||
} else {
|
} else {
|
||||||
ss.serviceStatusToday[mh.GetId()].Down++
|
ss.serviceStatusToday[mh.GetId()].Down++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentTime := time.Now()
|
||||||
|
if ss.serviceCurrentStatusIndex[mh.GetId()] == nil {
|
||||||
|
ss.serviceCurrentStatusIndex[mh.GetId()] = &indexStore{
|
||||||
|
t: currentTime,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
// 写入当前数据
|
// 写入当前数据
|
||||||
ss.serviceCurrentStatusData[mh.GetId()][ss.serviceCurrentStatusIndex[mh.GetId()]] = mh
|
if ss.serviceCurrentStatusIndex[mh.GetId()].t.Before(currentTime) {
|
||||||
ss.serviceCurrentStatusIndex[mh.GetId()]++
|
ss.serviceCurrentStatusIndex[mh.GetId()].t = currentTime.Add(30 * time.Second)
|
||||||
|
ss.serviceCurrentStatusData[mh.GetId()][ss.serviceCurrentStatusIndex[mh.GetId()].index] = mh
|
||||||
|
ss.serviceCurrentStatusIndex[mh.GetId()].index++
|
||||||
|
}
|
||||||
|
|
||||||
// 更新当前状态
|
// 更新当前状态
|
||||||
ss.serviceResponseDataStoreCurrentUp[mh.GetId()] = 0
|
ss.serviceResponseDataStoreCurrentUp[mh.GetId()] = 0
|
||||||
@@ -365,8 +417,11 @@ func (ss *ServiceSentinel) worker() {
|
|||||||
stateCode := GetStatusCode(upPercent)
|
stateCode := GetStatusCode(upPercent)
|
||||||
|
|
||||||
// 数据持久化
|
// 数据持久化
|
||||||
if ss.serviceCurrentStatusIndex[mh.GetId()] == _CurrentStatusSize {
|
if ss.serviceCurrentStatusIndex[mh.GetId()].index == _CurrentStatusSize {
|
||||||
ss.serviceCurrentStatusIndex[mh.GetId()] = 0
|
ss.serviceCurrentStatusIndex[mh.GetId()] = &indexStore{
|
||||||
|
index: 0,
|
||||||
|
t: currentTime,
|
||||||
|
}
|
||||||
if err := DB.Create(&model.MonitorHistory{
|
if err := DB.Create(&model.MonitorHistory{
|
||||||
MonitorID: mh.GetId(),
|
MonitorID: mh.GetId(),
|
||||||
AvgDelay: ss.serviceResponseDataStoreCurrentAvgDelay[mh.GetId()],
|
AvgDelay: ss.serviceResponseDataStoreCurrentAvgDelay[mh.GetId()],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package singleton
|
package singleton
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -46,6 +47,21 @@ func InitConfigFromPath(path string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
ValidateConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig 验证配置文件有效性
|
||||||
|
func ValidateConfig() {
|
||||||
|
// 如果DDNS启用则检查Provider是否存在, 不存在直接退出
|
||||||
|
if Conf.DDNS.Enable {
|
||||||
|
_, err := GetDDNSProviderFromString(Conf.DDNS.Provider)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if Conf.DDNS.MaxRetries < 1 || Conf.DDNS.MaxRetries > 10 {
|
||||||
|
panic(fmt.Errorf("DDNS.MaxRetries值域为[1, 10]的整数, 当前为 %d", Conf.DDNS.MaxRetries))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitDBFromPath 从给出的文件路径中加载数据库
|
// InitDBFromPath 从给出的文件路径中加载数据库
|
||||||
@@ -99,6 +115,10 @@ func RecordTransferHourlyUsage() {
|
|||||||
func CleanMonitorHistory() {
|
func CleanMonitorHistory() {
|
||||||
// 清理已被删除的服务器的监控记录与流量记录
|
// 清理已被删除的服务器的监控记录与流量记录
|
||||||
DB.Unscoped().Delete(&model.MonitorHistory{}, "created_at < ? OR monitor_id NOT IN (SELECT `id` FROM monitors)", time.Now().AddDate(0, 0, -30))
|
DB.Unscoped().Delete(&model.MonitorHistory{}, "created_at < ? OR monitor_id NOT IN (SELECT `id` FROM monitors)", time.Now().AddDate(0, 0, -30))
|
||||||
|
// 由于网络监控记录的数据较多,并且前端仅使用了 1 天的数据
|
||||||
|
// 考虑到 sqlite 数据量问题,仅保留一天数据,
|
||||||
|
// server_id = 0 的数据会用于/service页面的可用性展示
|
||||||
|
DB.Unscoped().Delete(&model.MonitorHistory{}, "(created_at < ? AND server_id != 0) OR monitor_id NOT IN (SELECT `id` FROM monitors)", time.Now().AddDate(0, 0, -1))
|
||||||
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (SELECT `id` FROM servers)")
|
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (SELECT `id` FROM servers)")
|
||||||
// 计算可清理流量记录的时长
|
// 计算可清理流量记录的时长
|
||||||
var allServerKeep time.Time
|
var allServerKeep time.Time
|
||||||
|
|||||||
Reference in New Issue
Block a user