Merge branch 'naiba:master' into master
This commit is contained in:
		
						commit
						3a395e66c0
					
				
							
								
								
									
										26
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								README.md
									
									
									
									
									
								
							@ -4,7 +4,7 @@
 | 
			
		||||
  <br>
 | 
			
		||||
  <small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
 | 
			
		||||
  <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.6-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.9-brightgreen?style=for-the-badge&logo=linux">
 | 
			
		||||
  <br>
 | 
			
		||||
  <br>
 | 
			
		||||
  <p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
 | 
			
		||||
@ -68,6 +68,9 @@ You can change the dashboard language in the settings page (`/setting`) after th
 | 
			
		||||
<a href="https://github.com/spiritLHLS" title="spiritlhl">
 | 
			
		||||
  <img src="https://avatars.githubusercontent.com/u/103393591?v=4" width="50;" alt="spiritlhl"/>
 | 
			
		||||
</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/liuyanxi975" title="刘颜溪">
 | 
			
		||||
  <img src="https://avatars.githubusercontent.com/u/24417037?v=4" width="50;" alt="刘颜溪"/>
 | 
			
		||||
</a>
 | 
			
		||||
@ -83,15 +86,15 @@ You can change the dashboard language in the settings page (`/setting`) after th
 | 
			
		||||
<a href="https://github.com/hhhkkk520" title="Kris">
 | 
			
		||||
  <img src="https://avatars.githubusercontent.com/u/52115472?v=4" width="50;" alt="Kris"/>
 | 
			
		||||
</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/Mmx233" title="Mmx">
 | 
			
		||||
  <img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/>
 | 
			
		||||
</a>
 | 
			
		||||
<a href="https://github.com/rootmelo92118" title="rootmelo92118">
 | 
			
		||||
  <img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/>
 | 
			
		||||
</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/iilemon" title="Sean">
 | 
			
		||||
  <img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/>
 | 
			
		||||
</a>
 | 
			
		||||
@ -101,6 +104,9 @@ You can change the dashboard language in the settings page (`/setting`) after th
 | 
			
		||||
<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/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/Creling" title="Creling">
 | 
			
		||||
  <img src="https://avatars.githubusercontent.com/u/43109504?v=4" width="50;" alt="Creling"/>
 | 
			
		||||
</a>
 | 
			
		||||
@ -110,15 +116,18 @@ You can change the dashboard language in the settings page (`/setting`) after th
 | 
			
		||||
<a href="https://github.com/colour93" title="玖叁">
 | 
			
		||||
  <img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/>
 | 
			
		||||
</a>
 | 
			
		||||
<a href="https://github.com/arkylin" title="凌">
 | 
			
		||||
  <img src="https://avatars.githubusercontent.com/u/35104502?v=4" width="50;" alt="凌"/>
 | 
			
		||||
</a>
 | 
			
		||||
<a href="https://github.com/ysicing" title="缘生">
 | 
			
		||||
  <img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/>
 | 
			
		||||
</a>
 | 
			
		||||
<a href="https://github.com/xykt" title="xykt">
 | 
			
		||||
  <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/nap0o" title="nap0o">
 | 
			
		||||
  <img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/>
 | 
			
		||||
</a>
 | 
			
		||||
<a href="https://github.com/yuanweize" title="I">
 | 
			
		||||
  <img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/>
 | 
			
		||||
</a>
 | 
			
		||||
@ -146,6 +155,9 @@ You can change the dashboard language in the settings page (`/setting`) after th
 | 
			
		||||
<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>
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/mygin"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
)
 | 
			
		||||
@ -16,18 +17,31 @@ type apiV1 struct {
 | 
			
		||||
 | 
			
		||||
func (v *apiV1) serve() {
 | 
			
		||||
	r := v.r.Group("")
 | 
			
		||||
	// API
 | 
			
		||||
	// 强制认证的 API
 | 
			
		||||
	r.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		Member:   true,
 | 
			
		||||
		IsPage:   false,
 | 
			
		||||
		AllowAPI: true,
 | 
			
		||||
		Msg:      "访问此接口需要认证",
 | 
			
		||||
		Btn:      "点此登录",
 | 
			
		||||
		Redirect: "/login",
 | 
			
		||||
		MemberOnly: true,
 | 
			
		||||
		AllowAPI:   true,
 | 
			
		||||
		IsPage:     false,
 | 
			
		||||
		Msg:        "访问此接口需要认证",
 | 
			
		||||
		Btn:        "点此登录",
 | 
			
		||||
		Redirect:   "/login",
 | 
			
		||||
	}))
 | 
			
		||||
	r.GET("/server/list", v.serverList)
 | 
			
		||||
	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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -84,5 +98,15 @@ func (v *apiV1) monitorHistoriesById(c *gin.Context) {
 | 
			
		||||
		})
 | 
			
		||||
		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}))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -44,9 +44,13 @@ type commonPage struct {
 | 
			
		||||
func (cp *commonPage) serve() {
 | 
			
		||||
	cr := cp.r.Group("")
 | 
			
		||||
	cr.Use(mygin.Authorize(mygin.AuthorizeOption{}))
 | 
			
		||||
	cr.GET("/terminal/:id", cp.terminal)
 | 
			
		||||
	cr.Use(mygin.PreferredTheme)
 | 
			
		||||
	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("/service", cp.service)
 | 
			
		||||
	// TODO: 界面直接跳转使用该接口
 | 
			
		||||
@ -63,6 +67,7 @@ type viewPasswordForm struct {
 | 
			
		||||
func (p *commonPage) issueViewPassword(c *gin.Context) {
 | 
			
		||||
	var vpf viewPasswordForm
 | 
			
		||||
	err := c.ShouldBind(&vpf)
 | 
			
		||||
	log.Println("bingo", vpf)
 | 
			
		||||
	var hash []byte
 | 
			
		||||
	if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword {
 | 
			
		||||
		err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
 | 
			
		||||
@ -85,31 +90,6 @@ func (p *commonPage) issueViewPassword(c *gin.Context) {
 | 
			
		||||
	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) {
 | 
			
		||||
	res, _, _ := p.requestGroup.Do("servicePage", func() (interface{}, error) {
 | 
			
		||||
		singleton.AlertsLock.RLock()
 | 
			
		||||
@ -128,7 +108,7 @@ func (p *commonPage) service(c *gin.Context) {
 | 
			
		||||
			stats, statsStore,
 | 
			
		||||
		}, 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"}),
 | 
			
		||||
		"Services":           res.([]interface{})[0],
 | 
			
		||||
		"CycleTransferStats": res.([]interface{})[1],
 | 
			
		||||
@ -234,7 +214,7 @@ func (cp *commonPage) network(c *gin.Context) {
 | 
			
		||||
		Servers: servers,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/network", mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
	c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/network"), mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
		"Servers":         string(serversBytes),
 | 
			
		||||
		"MonitorInfos":    string(monitorInfos),
 | 
			
		||||
		"CustomCode":      singleton.Conf.Site.CustomCode,
 | 
			
		||||
@ -280,7 +260,7 @@ func (cp *commonPage) home(c *gin.Context) {
 | 
			
		||||
		}, true)
 | 
			
		||||
		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),
 | 
			
		||||
		"CustomCode": singleton.Conf.Site.CustomCode,
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
@ -19,11 +19,11 @@ type guestPage struct {
 | 
			
		||||
func (gp *guestPage) serve() {
 | 
			
		||||
	gr := gp.r.Group("")
 | 
			
		||||
	gr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		Guest:    true,
 | 
			
		||||
		IsPage:   true,
 | 
			
		||||
		Msg:      "您已登录",
 | 
			
		||||
		Btn:      "返回首页",
 | 
			
		||||
		Redirect: "/",
 | 
			
		||||
		GuestOnly: true,
 | 
			
		||||
		IsPage:    true,
 | 
			
		||||
		Msg:       "您已登录",
 | 
			
		||||
		Btn:       "返回首页",
 | 
			
		||||
		Redirect:  "/",
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	gr.GET("/login", gp.login)
 | 
			
		||||
 | 
			
		||||
@ -28,11 +28,11 @@ type memberAPI struct {
 | 
			
		||||
func (ma *memberAPI) serve() {
 | 
			
		||||
	mr := ma.r.Group("")
 | 
			
		||||
	mr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		Member:   true,
 | 
			
		||||
		IsPage:   false,
 | 
			
		||||
		Msg:      "访问此接口需要登录",
 | 
			
		||||
		Btn:      "点此登录",
 | 
			
		||||
		Redirect: "/login",
 | 
			
		||||
		MemberOnly: true,
 | 
			
		||||
		IsPage:     false,
 | 
			
		||||
		Msg:        "访问此接口需要登录",
 | 
			
		||||
		Btn:        "点此登录",
 | 
			
		||||
		Redirect:   "/login",
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	mr.GET("/search-server", ma.searchServer)
 | 
			
		||||
@ -300,6 +300,8 @@ type serverForm struct {
 | 
			
		||||
	Tag          string
 | 
			
		||||
	Note         string
 | 
			
		||||
	HideForGuest string
 | 
			
		||||
	EnableDDNS   string
 | 
			
		||||
	DDNSDomain   string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ma *memberAPI) addOrEditServer(c *gin.Context) {
 | 
			
		||||
@ -315,6 +317,8 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
 | 
			
		||||
		s.Tag = sf.Tag
 | 
			
		||||
		s.Note = sf.Note
 | 
			
		||||
		s.HideForGuest = sf.HideForGuest == "on"
 | 
			
		||||
		s.EnableDDNS = sf.EnableDDNS == "on"
 | 
			
		||||
		s.DDNSDomain = sf.DDNSDomain
 | 
			
		||||
		if s.ID == 0 {
 | 
			
		||||
			s.Secret, err = utils.GenerateRandomString(18)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
@ -440,10 +444,12 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
 | 
			
		||||
				err = singleton.DB.Save(&m).Error
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		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 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 {
 | 
			
		||||
@ -852,8 +858,9 @@ type settingForm struct {
 | 
			
		||||
	GRPCHost                string
 | 
			
		||||
	Cover                   uint8
 | 
			
		||||
 | 
			
		||||
	EnableIPChangeNotification  string
 | 
			
		||||
	EnablePlainIPInNotification string
 | 
			
		||||
	EnableIPChangeNotification      string
 | 
			
		||||
	EnablePlainIPInNotification     string
 | 
			
		||||
	DisableSwitchTemplateInFrontend string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ma *memberAPI) updateSetting(c *gin.Context) {
 | 
			
		||||
@ -901,6 +908,7 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
 | 
			
		||||
	singleton.Conf.Language = sf.Language
 | 
			
		||||
	singleton.Conf.EnableIPChangeNotification = sf.EnableIPChangeNotification == "on"
 | 
			
		||||
	singleton.Conf.EnablePlainIPInNotification = sf.EnablePlainIPInNotification == "on"
 | 
			
		||||
	singleton.Conf.DisableSwitchTemplateInFrontend = sf.DisableSwitchTemplateInFrontend == "on"
 | 
			
		||||
	singleton.Conf.Cover = sf.Cover
 | 
			
		||||
	singleton.Conf.GRPCHost = sf.GRPCHost
 | 
			
		||||
	singleton.Conf.IgnoredIPNotification = sf.IgnoredIPNotification
 | 
			
		||||
 | 
			
		||||
@ -17,11 +17,11 @@ type memberPage struct {
 | 
			
		||||
func (mp *memberPage) serve() {
 | 
			
		||||
	mr := mp.r.Group("")
 | 
			
		||||
	mr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		Member:   true,
 | 
			
		||||
		IsPage:   true,
 | 
			
		||||
		Msg:      singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
 | 
			
		||||
		Btn:      singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
 | 
			
		||||
		Redirect: "/login",
 | 
			
		||||
		MemberOnly: true,
 | 
			
		||||
		IsPage:     true,
 | 
			
		||||
		Msg:        singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
 | 
			
		||||
		Btn:        singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
 | 
			
		||||
		Redirect:   "/login",
 | 
			
		||||
	}))
 | 
			
		||||
	mr.GET("/server", mp.server)
 | 
			
		||||
	mr.GET("/monitor", mp.monitor)
 | 
			
		||||
@ -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{
 | 
			
		||||
		"Title":           singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}),
 | 
			
		||||
		"Languages":       model.Languages,
 | 
			
		||||
		"Themes":          model.Themes,
 | 
			
		||||
		"DashboardThemes": model.DashboardThemes,
 | 
			
		||||
	}))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import (
 | 
			
		||||
 | 
			
		||||
const CtxKeyAuthorizedUser = "ckau"
 | 
			
		||||
const CtxKeyViewPasswordVerified = "ckvpv"
 | 
			
		||||
const CtxKeyPreferredTheme = "ckpt"
 | 
			
		||||
const CacheKeyOauth2State = "p:a:state"
 | 
			
		||||
 | 
			
		||||
type Common struct {
 | 
			
		||||
 | 
			
		||||
@ -98,7 +98,8 @@ type Config struct {
 | 
			
		||||
	ProxyGRPCPort uint
 | 
			
		||||
	TLS           bool
 | 
			
		||||
 | 
			
		||||
	EnablePlainIPInNotification bool // 通知信息IP不打码
 | 
			
		||||
	EnablePlainIPInNotification     bool // 通知信息IP不打码
 | 
			
		||||
	DisableSwitchTemplateInFrontend bool // 前台禁用切换模板功能
 | 
			
		||||
 | 
			
		||||
	// IP变更提醒
 | 
			
		||||
	EnableIPChangeNotification bool
 | 
			
		||||
@ -112,6 +113,19 @@ type Config struct {
 | 
			
		||||
	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 读取配置文件并应用
 | 
			
		||||
@ -152,6 +166,15 @@ func (c *Config) Read(path string) error {
 | 
			
		||||
	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()
 | 
			
		||||
	return nil
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,8 @@ type Server struct {
 | 
			
		||||
	Note         string `json:"-"` // 管理员可见备注
 | 
			
		||||
	DisplayIndex int    // 展示排序,越大越靠前
 | 
			
		||||
	HideForGuest bool   // 对游客隐藏
 | 
			
		||||
	EnableDDNS   bool   // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效
 | 
			
		||||
	DDNSDomain   string // DDNS中的前缀 如基础域名为abc.oracle DDNSName为mjj 就会把mjj.abc.oracle解析服务器IP 为空则停用
 | 
			
		||||
 | 
			
		||||
	Host       *Host      `gorm:"-"`
 | 
			
		||||
	State      *HostState `gorm:"-"`
 | 
			
		||||
@ -51,5 +53,6 @@ func (s Server) Marshal() template.JS {
 | 
			
		||||
	tag, _ := utils.Json.Marshal(s.Tag)
 | 
			
		||||
	note, _ := utils.Json.Marshal(s.Note)
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										177
									
								
								pkg/ddns/cloudflare.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								pkg/ddns/cloudflare.go
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								pkg/ddns/ddns.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								pkg/ddns/ddns.go
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								pkg/ddns/dummy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								pkg/ddns/dummy.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
package ddns
 | 
			
		||||
 | 
			
		||||
type ProviderDummy struct{}
 | 
			
		||||
 | 
			
		||||
func (provider ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								pkg/ddns/helper.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								pkg/ddns/helper.go
									
									
									
									
									
										Normal file
									
								
							@ -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 // 当域名不包含子域名时,无前缀
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								pkg/ddns/webhook.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								pkg/ddns/webhook.go
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
}
 | 
			
		||||
@ -12,19 +12,19 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AuthorizeOption struct {
 | 
			
		||||
	Guest    bool
 | 
			
		||||
	Member   bool
 | 
			
		||||
	IsPage   bool
 | 
			
		||||
	AllowAPI bool
 | 
			
		||||
	Msg      string
 | 
			
		||||
	Redirect string
 | 
			
		||||
	Btn      string
 | 
			
		||||
	GuestOnly  bool
 | 
			
		||||
	MemberOnly bool
 | 
			
		||||
	IsPage     bool
 | 
			
		||||
	AllowAPI   bool
 | 
			
		||||
	Msg        string
 | 
			
		||||
	Redirect   string
 | 
			
		||||
	Btn        string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Authorize(opt AuthorizeOption) func(*gin.Context) {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		var code = http.StatusForbidden
 | 
			
		||||
		if opt.Guest {
 | 
			
		||||
		if opt.GuestOnly {
 | 
			
		||||
			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)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 未登录且需要登录
 | 
			
		||||
		if !isLogin && opt.Member {
 | 
			
		||||
		if !isLogin && opt.MemberOnly {
 | 
			
		||||
			ShowErrorPage(c, commonErr, opt.IsPage)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ func CommonEnvironment(c *gin.Context, data map[string]interface{}) gin.H {
 | 
			
		||||
	data["MatchedPath"] = c.MustGet("MatchedPath")
 | 
			
		||||
	data["Version"] = singleton.Version
 | 
			
		||||
	data["Conf"] = singleton.Conf
 | 
			
		||||
	data["Themes"] = model.Themes
 | 
			
		||||
	// 是否是管理页面
 | 
			
		||||
	data["IsAdminPage"] = adminPage[data["MatchedPath"].(string)]
 | 
			
		||||
	// 站点标题
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										30
									
								
								pkg/mygin/preferred_theme.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								pkg/mygin/preferred_theme.go
									
									
									
									
									
										Normal file
									
								
							@ -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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										52
									
								
								pkg/mygin/view_password.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								pkg/mygin/view_password.go
									
									
									
									
									
										Normal file
									
								
							@ -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()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								resource/l10n/en-US.toml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								resource/l10n/en-US.toml
									
									
									
									
										vendored
									
									
								
							@ -398,7 +398,7 @@ other = "Virtualization"
 | 
			
		||||
other = "Swap"
 | 
			
		||||
 | 
			
		||||
[NetTransfer]
 | 
			
		||||
other = "Network Transfer"
 | 
			
		||||
other = "Transfer"
 | 
			
		||||
 | 
			
		||||
[Load]
 | 
			
		||||
other = "Load"
 | 
			
		||||
@ -419,7 +419,7 @@ other = "Last Active"
 | 
			
		||||
other = "Version"
 | 
			
		||||
 | 
			
		||||
[NetSpeed]
 | 
			
		||||
other = "Network Speed"
 | 
			
		||||
other = "Speed"
 | 
			
		||||
 | 
			
		||||
[Uptime]
 | 
			
		||||
other = "Uptime"
 | 
			
		||||
@ -614,4 +614,22 @@ other = "Menu"
 | 
			
		||||
other = "Network"
 | 
			
		||||
 | 
			
		||||
[EnableShowInService]
 | 
			
		||||
other = "Enable Show in Service"
 | 
			
		||||
other = "Enable Show in Service"
 | 
			
		||||
 | 
			
		||||
[EnableDDNS]
 | 
			
		||||
other = "Enable DDNS"
 | 
			
		||||
 | 
			
		||||
[DDNSDomain]
 | 
			
		||||
other = "DDNS Domain"
 | 
			
		||||
 | 
			
		||||
[Feature]
 | 
			
		||||
other = "Feature"
 | 
			
		||||
 | 
			
		||||
[Template]
 | 
			
		||||
other = "Template"
 | 
			
		||||
 | 
			
		||||
[Stat]
 | 
			
		||||
other = "Stat"
 | 
			
		||||
 | 
			
		||||
[DisableSwitchTemplateInFrontend]
 | 
			
		||||
other = "Disable Switch Template in Frontend"
 | 
			
		||||
							
								
								
									
										22
									
								
								resource/l10n/es-ES.toml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								resource/l10n/es-ES.toml
									
									
									
									
										vendored
									
									
								
							@ -203,7 +203,7 @@ other = "Agregar Servidor"
 | 
			
		||||
other = "Editar Grupo de Servidores en Lote"
 | 
			
		||||
 | 
			
		||||
[BatchDeleteServer]
 | 
			
		||||
other = "Eliminar Servidores en Lote""
 | 
			
		||||
other = "Eliminar Servidores en Lote"
 | 
			
		||||
 | 
			
		||||
[InputServerGroupName]
 | 
			
		||||
other = "Ingrese Nombre del Grupo de Servidores"
 | 
			
		||||
@ -614,4 +614,22 @@ other = "Menú"
 | 
			
		||||
other = "Red"
 | 
			
		||||
 | 
			
		||||
[EnableShowInService]
 | 
			
		||||
other = "Mostrar en servicio"
 | 
			
		||||
other = "Mostrar en servicio"
 | 
			
		||||
 | 
			
		||||
[EnableDDNS]
 | 
			
		||||
other = "Habilitar DDNS"
 | 
			
		||||
 | 
			
		||||
[DDNSDomain]
 | 
			
		||||
other = "Dominio DDNS"
 | 
			
		||||
 | 
			
		||||
[Feature]
 | 
			
		||||
other = "Característica"
 | 
			
		||||
 | 
			
		||||
[Template]
 | 
			
		||||
other = "Plantilla"
 | 
			
		||||
 | 
			
		||||
[Stat]
 | 
			
		||||
other = "Stat"
 | 
			
		||||
 | 
			
		||||
[DisableSwitchTemplateInFrontend]
 | 
			
		||||
other = "Deshabilitar Cambio de Plantilla en Frontend"
 | 
			
		||||
							
								
								
									
										18
									
								
								resource/l10n/zh-CN.toml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								resource/l10n/zh-CN.toml
									
									
									
									
										vendored
									
									
								
							@ -615,3 +615,21 @@ other = "网络"
 | 
			
		||||
 | 
			
		||||
[EnableShowInService]
 | 
			
		||||
other = "在服务中显示"
 | 
			
		||||
 | 
			
		||||
[EnableDDNS]
 | 
			
		||||
other = "启用DDNS"
 | 
			
		||||
 | 
			
		||||
[DDNSDomain]
 | 
			
		||||
other = "DDNS域名"
 | 
			
		||||
 | 
			
		||||
[Feature]
 | 
			
		||||
other = "功能"
 | 
			
		||||
 | 
			
		||||
[Template]
 | 
			
		||||
other = "主题"
 | 
			
		||||
 | 
			
		||||
[Stat]
 | 
			
		||||
other = "信息"
 | 
			
		||||
 | 
			
		||||
[DisableSwitchTemplateInFrontend]
 | 
			
		||||
other = "禁止前台切换模板"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								resource/l10n/zh-TW.toml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								resource/l10n/zh-TW.toml
									
									
									
									
										vendored
									
									
								
							@ -614,4 +614,22 @@ other = "菜單"
 | 
			
		||||
other = "網絡"
 | 
			
		||||
 | 
			
		||||
[EnableShowInService]
 | 
			
		||||
other = "在服務中顯示"
 | 
			
		||||
other = "在服務中顯示"
 | 
			
		||||
 | 
			
		||||
[EnableDDNS]
 | 
			
		||||
other = "啟用DDNS"
 | 
			
		||||
 | 
			
		||||
[DDNSDomain]
 | 
			
		||||
other = "DDNS網域"
 | 
			
		||||
 | 
			
		||||
[Feature]
 | 
			
		||||
other = "功能"
 | 
			
		||||
 | 
			
		||||
[Template]
 | 
			
		||||
other = "主題"
 | 
			
		||||
 | 
			
		||||
[Stat]
 | 
			
		||||
other = "信息"
 | 
			
		||||
 | 
			
		||||
[DisableSwitchTemplateInFrontend]
 | 
			
		||||
other = "禁止前台切換主題"
 | 
			
		||||
 | 
			
		||||
@ -302,6 +302,7 @@ function addOrEditServer(server, conf) {
 | 
			
		||||
  modal.find("input[name=id]").val(server ? server.ID : 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=DDNSDomain]").val(server ? server.DDNSDomain : null);
 | 
			
		||||
  modal
 | 
			
		||||
    .find("input[name=DisplayIndex]")
 | 
			
		||||
    .val(server ? server.DisplayIndex : null);
 | 
			
		||||
@ -321,6 +322,11 @@ function addOrEditServer(server, conf) {
 | 
			
		||||
  } else {
 | 
			
		||||
    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");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										218
									
								
								resource/static/theme-default/css/main.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								resource/static/theme-default/css/main.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,218 @@
 | 
			
		||||
/* 屏幕适配 */
 | 
			
		||||
@media only screen and (min-width:1200px) {
 | 
			
		||||
  .ui.container {
 | 
			
		||||
    width:95% !important;
 | 
			
		||||
    font-size: 90% !important;
 | 
			
		||||
    max-width: 1300px !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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								resource/static/theme-default/js/mixin.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								resource/static/theme-default/js/mixin.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
			
		||||
const mixinsVue = {
 | 
			
		||||
    delimiters: ['@#', '#@'],
 | 
			
		||||
    data: {
 | 
			
		||||
        preferredTemplate: null,
 | 
			
		||||
        isMobile: false,
 | 
			
		||||
        adaptedTemplates: [
 | 
			
		||||
            { key: 'default', name: 'Default', icon: 'th large' },
 | 
			
		||||
            { key: 'angel-kanade', name: 'AngelKanade', icon: 'square' },
 | 
			
		||||
            { key: 'server-status', name: 'SeverStatus', icon: 'list' }
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    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
											
										
									
								
							
							
								
								
									
										134
									
								
								resource/static/theme-server-status/css/dark.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										134
									
								
								resource/static/theme-server-status/css/dark.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										219
									
								
								resource/static/theme-server-status/css/main.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										219
									
								
								resource/static/theme-server-status/css/main.css
									
									
									
									
										vendored
									
									
								
							@ -6,58 +6,120 @@ body {
 | 
			
		||||
/* 导航部分 开始*/
 | 
			
		||||
.navbar {
 | 
			
		||||
    min-height: 40px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar-inner{
 | 
			
		||||
    margin:0 auto;
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pl-md-unset {
 | 
			
		||||
.navbar .container{
 | 
			
		||||
    max-width: 95vw;
 | 
			
		||||
    margin: 0 auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar-collapse:not([aria-expanded]) .navbar-nav .dropdown-toggle {
 | 
			
		||||
.navbar-inverse{
 | 
			
		||||
    background-image: none;
 | 
			
		||||
    background-color: #1C2127;
 | 
			
		||||
    box-shadow: 0 1px 40px -8px #00000080;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
    padding: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar-toggle {
 | 
			
		||||
.navbar .navbar-toggle {
 | 
			
		||||
    margin-right:0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar-brand {
 | 
			
		||||
.navbar .navbar-brand {
 | 
			
		||||
    font-size: 20px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    padding:12px 0 0 0;
 | 
			
		||||
    margin-right:30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.node-cell-expand {
 | 
			
		||||
    max-width: 420px;
 | 
			
		||||
.navbar .node-cell-expand {
 | 
			
		||||
    word-break: break-all;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.node-cell-expand-label {
 | 
			
		||||
    margin-right: 5px;
 | 
			
		||||
.navbar .node-cell-expand-label {
 | 
			
		||||
    /*margin-right: 5px;*/
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown .dropdown-toggle {
 | 
			
		||||
.navbar .dropdown .dropdown-toggle {
 | 
			
		||||
    padding-bottom: 10px;
 | 
			
		||||
    padding-top: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar-inverse, .nav.navbar-nav {
 | 
			
		||||
    background-image: linear-gradient(rgb(60, 60, 60) 0px, rgb(34, 34, 34) 100%) !important;
 | 
			
		||||
.navbar .navbar-nav {
 | 
			
		||||
    margin:0px -15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar-inverse .navbar-nav>li>a {
 | 
			
		||||
.navbar .navbar-nav>li>a {
 | 
			
		||||
    color:#f1f1f1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar-inverse .navbar-brand {
 | 
			
		||||
    font-size: 20px;
 | 
			
		||||
.navbar-nav li a span{
 | 
			
		||||
    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 {
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
@ -75,15 +137,40 @@ body {
 | 
			
		||||
    text-align: left;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table>thead>tr>th{
 | 
			
		||||
  border:none;  
 | 
			
		||||
.table > tbody > tr > td,
 | 
			
		||||
.table > tbody > tr > th,
 | 
			
		||||
.table > tfoot > tr > td,
 | 
			
		||||
.table > tfoot > tr > th,
 | 
			
		||||
.table > thead > tr > td,
 | 
			
		||||
.table > thead > tr > th {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    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;
 | 
			
		||||
    padding-bottom:15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table .node-cell-os-text {
 | 
			
		||||
    text-transform: capitalize;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.progress {
 | 
			
		||||
    margin-bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
@ -93,7 +180,6 @@ body {
 | 
			
		||||
    padding-left: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.expandRow > td {
 | 
			
		||||
    padding: 0 !important;
 | 
			
		||||
    border-top: 0 !important;
 | 
			
		||||
@ -131,25 +217,49 @@ body {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.node-cell.network {
 | 
			
		||||
    min-width: 110px;
 | 
			
		||||
    max-width: 110px;
 | 
			
		||||
    min-width: 100px;
 | 
			
		||||
    max-width: 100px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.node-cell.cpu, .node-cell.ram, .node-cell.hdd {
 | 
			
		||||
    min-width: 45px;
 | 
			
		||||
    max-width: 90px;
 | 
			
		||||
.node-cell.traffic {
 | 
			
		||||
    min-width: 100px;   
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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 {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: left;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.service-status .delay-today > i {
 | 
			
		||||
.service-status .delay-today i {
 | 
			
		||||
    width: 12px;
 | 
			
		||||
    height: 12px;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
@ -163,26 +273,29 @@ body {
 | 
			
		||||
    width: 18px;
 | 
			
		||||
    height: 18px;
 | 
			
		||||
    margin-right: 4px;
 | 
			
		||||
    margin-bottom: -3.25px;
 | 
			
		||||
    border-radius: 3px;
 | 
			
		||||
    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) {
 | 
			
		||||
    .accordian-body{
 | 
			
		||||
        margin: 5px 0px 5px 10px;        
 | 
			
		||||
    }
 | 
			
		||||
    .table .node-group-tag {
 | 
			
		||||
    .table .node-group-tag th{
 | 
			
		||||
        font-size:16px;
 | 
			
		||||
        padding-bottom:6px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media only screen and (max-width: 720px) {
 | 
			
		||||
@media only screen and (max-width: 767px) {
 | 
			
		||||
    body {
 | 
			
		||||
        font-size: 10px !important;
 | 
			
		||||
        padding-top:60px !important;
 | 
			
		||||
@ -191,6 +304,23 @@ body {
 | 
			
		||||
        padding: 0;
 | 
			
		||||
        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.uptime,
 | 
			
		||||
    .node-cell.traffic{
 | 
			
		||||
@ -208,10 +338,33 @@ body {
 | 
			
		||||
    .accordian-body{
 | 
			
		||||
        margin: 5px 0px 5px 10px;        
 | 
			
		||||
    }
 | 
			
		||||
    .table .node-group-tag {
 | 
			
		||||
        font-size:12px;
 | 
			
		||||
    .table .node-group-tag th{
 | 
			
		||||
        font-size:16px;
 | 
			
		||||
        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) {
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										117
									
								
								resource/static/theme-server-status/js/mixin.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										117
									
								
								resource/static/theme-server-status/js/mixin.js
									
									
									
									
										vendored
									
									
								
							@ -2,27 +2,79 @@ const mixinsVue = {
 | 
			
		||||
    data: {
 | 
			
		||||
        cache: [],
 | 
			
		||||
        theme: "light",
 | 
			
		||||
        isSystemTheme: false
 | 
			
		||||
        isSystemTheme: false,
 | 
			
		||||
        showGroup: false,
 | 
			
		||||
        showGoTop: false,
 | 
			
		||||
        preferredTemplate: null,
 | 
			
		||||
        isMobile: false,
 | 
			
		||||
        adaptedTemplates: [
 | 
			
		||||
            { key: 'default', name: 'Default', icon: 'th large' },
 | 
			
		||||
            { key: 'angel-kanade', name: 'AngelKanade', icon: 'square' },
 | 
			
		||||
            { key: 'server-status', name: 'SeverStatus', icon: 'list' }
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    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: {
 | 
			
		||||
        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) {
 | 
			
		||||
            this.theme = title
 | 
			
		||||
            document.body.setAttribute("theme", title)
 | 
			
		||||
            this.theme = title;
 | 
			
		||||
            document.body.setAttribute("theme", title);
 | 
			
		||||
            if (store) {
 | 
			
		||||
                localStorage.setItem("theme", title)
 | 
			
		||||
                this.isSystemTheme = false
 | 
			
		||||
                localStorage.setItem("theme", title);
 | 
			
		||||
                this.isSystemTheme = false;
 | 
			
		||||
                if(this.$root.page == 'index') {
 | 
			
		||||
                    this.$root.reloadCharts(); //重新载入echarts图表
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        setSystemTheme() {
 | 
			
		||||
            localStorage.removeItem("theme")
 | 
			
		||||
            this.initTheme()
 | 
			
		||||
            this.isSystemTheme = true
 | 
			
		||||
            localStorage.removeItem("theme");
 | 
			
		||||
            this.initTheme();
 | 
			
		||||
            this.isSystemTheme = true;
 | 
			
		||||
        },
 | 
			
		||||
        initTheme() {
 | 
			
		||||
            const storeTheme = localStorage.getItem("theme")
 | 
			
		||||
            const storeTheme = localStorage.getItem("theme");
 | 
			
		||||
            if (storeTheme === 'dark' || storeTheme === 'light') {
 | 
			
		||||
                this.setTheme(storeTheme, true);
 | 
			
		||||
            } else {
 | 
			
		||||
@ -45,5 +97,50 @@ const mixinsVue = {
 | 
			
		||||
        toFixed2(f) {
 | 
			
		||||
            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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								resource/template/common/footer.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								resource/template/common/footer.html
									
									
									
									
										vendored
									
									
								
							@ -10,7 +10,7 @@
 | 
			
		||||
<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="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
 | 
			
		||||
<script src="/static/main.js?v20240213"></script>
 | 
			
		||||
<script src="/static/main.js?v20240224"></script>
 | 
			
		||||
<script>
 | 
			
		||||
    (function () {
 | 
			
		||||
        updateLang({{.LANG }});
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								resource/template/component/server.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								resource/template/component/server.html
									
									
									
									
										vendored
									
									
								
							@ -26,6 +26,16 @@
 | 
			
		||||
                    <label>{{tr "HideForGuest"}}</label>
 | 
			
		||||
                </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">
 | 
			
		||||
                <label>{{tr "Note"}}</label>
 | 
			
		||||
                <textarea name="Note"></textarea>
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,8 @@
 | 
			
		||||
                    <th>IP</th>
 | 
			
		||||
                    <th>{{tr "VersionNumber"}}</th>
 | 
			
		||||
                    <th>{{tr "HideForGuest"}}</th>
 | 
			
		||||
                    <th>{{tr "EnableDDNS"}}</th>
 | 
			
		||||
                    <th>{{tr "DDNSDomain"}}</th>
 | 
			
		||||
                    <th>{{tr "Secret"}}</th>
 | 
			
		||||
                    <th>{{tr "OneKeyInstall"}}</th>
 | 
			
		||||
                    <th>{{tr "Note"}}</th>
 | 
			
		||||
@ -45,6 +47,8 @@
 | 
			
		||||
                    <td>{{$server.Host.IP}}</td>
 | 
			
		||||
                    <td>{{$server.Host.Version}}</td>
 | 
			
		||||
                    <td>{{$server.HideForGuest}}</td>
 | 
			
		||||
                    <td>{{$server.EnableDDNS}}</td>
 | 
			
		||||
                    <td>{{$server.DDNSDomain}}</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
 | 
			
		||||
                            <i class="copy icon"></i>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								resource/template/dashboard-default/setting.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								resource/template/dashboard-default/setting.html
									
									
									
									
										vendored
									
									
								
							@ -70,18 +70,24 @@
 | 
			
		||||
                    <input type="text" name="IPChangeNotificationTag" placeholder="" value="{{.Conf.IPChangeNotificationTag}}">
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="field">
 | 
			
		||||
                    <div class="ui nf-ssl checkbox ip-change">
 | 
			
		||||
                    <div class="ui checkbox ip-change">
 | 
			
		||||
                        <input name="EnableIPChangeNotification" type="checkbox" tabindex="0" class="hidden">
 | 
			
		||||
                        <label>{{tr "Enable"}}</label>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="field">
 | 
			
		||||
                <div class="ui nf-ssl checkbox plain-ip">
 | 
			
		||||
                <div class="ui checkbox plain-ip">
 | 
			
		||||
                    <input name="EnablePlainIPInNotification" type="checkbox" tabindex="0" class="hidden">
 | 
			
		||||
                    <label>{{tr "NotificationMessagesDoNotHideIP"}}</label>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="field">
 | 
			
		||||
                <div class="ui checkbox disable-switch-template">
 | 
			
		||||
                    <input name="DisableSwitchTemplateInFrontend" type="checkbox" tabindex="0" class="hidden">
 | 
			
		||||
                    <label>{{tr "DisableSwitchTemplateInFrontend"}}</label>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <button class="ui button" type="submit">{{tr "Save"}}</button>
 | 
			
		||||
        </form>
 | 
			
		||||
    </div>
 | 
			
		||||
@ -128,5 +134,8 @@
 | 
			
		||||
    {{if .Conf.EnablePlainIPInNotification}}
 | 
			
		||||
    $('.checkbox.plain-ip').checkbox('set checked')
 | 
			
		||||
    {{ end }}
 | 
			
		||||
    {{if .Conf.DisableSwitchTemplateInFrontend }}
 | 
			
		||||
    $('.checkbox.disable-switch-template').checkbox('set checked')
 | 
			
		||||
    {{ end }}
 | 
			
		||||
</script>
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
{{define "theme-angel-kanade/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> | 
 | 
			
		||||
@ -12,13 +13,13 @@
 | 
			
		||||
<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="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
 | 
			
		||||
<script src="/static/main.js?v20240213"></script>
 | 
			
		||||
<script src="/static/main.js?v20240224"></script>
 | 
			
		||||
<script src="/static/theme-default/js/mixin.js?v20240302"></script>
 | 
			
		||||
<script>
 | 
			
		||||
    (function () {
 | 
			
		||||
        updateLang({{.LANG }});
 | 
			
		||||
    })();
 | 
			
		||||
</script>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
{{end}}
 | 
			
		||||
							
								
								
									
										201
									
								
								resource/template/theme-angel-kanade/home.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										201
									
								
								resource/template/theme-angel-kanade/home.html
									
									
									
									
										vendored
									
									
								
							@ -3,118 +3,116 @@
 | 
			
		||||
{{if ts .CustomCode}} {{.CustomCode|safe}} {{end}}
 | 
			
		||||
{{template "theme-angel-kanade/menu" .}}
 | 
			
		||||
<div class="nb-container">
 | 
			
		||||
  <div class="ui container">
 | 
			
		||||
    <div id="app">
 | 
			
		||||
      <div class="ui styled fluid accordion" v-for="group in groups">
 | 
			
		||||
        <div class="active title">
 | 
			
		||||
          <i class="dropdown icon"></i>
 | 
			
		||||
          @#(group.Tag!==''?group.Tag:'{{tr "Default"}}')#@
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="active content">
 | 
			
		||||
          <div class="ui four stackable status cards">
 | 
			
		||||
            <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="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://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="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
 | 
			
		||||
                  @#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
 | 
			
		||||
                  <i class="nezha-secondary-font info circle icon" style="height: 28px"></i>
 | 
			
		||||
                  <div class="ui content popup" style="margin-bottom: 0">
 | 
			
		||||
                    {{tr "Platform"}}: @#server.Host.Platform#@-@#server.Host.PlatformVersion#@
 | 
			
		||||
                    [<span
 | 
			
		||||
                      v-if="server.Host.Virtualization">@#server.Host.Virtualization#@:</span>@#server.Host.Arch#@]<br />
 | 
			
		||||
                    CPU: @#server.Host.CPU#@<br />
 | 
			
		||||
                    {{tr "DiskUsed"}}:
 | 
			
		||||
                    @#formatByteSize(server.State.DiskUsed)#@/@#formatByteSize(server.Host.DiskTotal)#@<br />
 | 
			
		||||
                    {{tr "MemUsed"}}:
 | 
			
		||||
                    @#formatByteSize(server.State.MemUsed)#@/@#formatByteSize(server.Host.MemTotal)#@<br />
 | 
			
		||||
                    {{tr "SwapUsed"}}:
 | 
			
		||||
                    @#formatByteSize(server.State.SwapUsed)#@/@#formatByteSize(server.Host.SwapTotal)#@<br />
 | 
			
		||||
                    {{tr "NetTransfer"}}: <i
 | 
			
		||||
                      class="arrow alternate circle down outline icon"></i>@#formatByteSize(server.State.NetInTransfer)#@<i
 | 
			
		||||
                      class="arrow alternate circle up outline icon"></i>@#formatByteSize(server.State.NetOutTransfer)#@<br />
 | 
			
		||||
                    {{tr "Load"}}: @# toFixed2(server.State.Load1) #@/@# toFixed2(server.State.Load5) #@/@#
 | 
			
		||||
                    toFixed2(server.State.Load15) #@<br />
 | 
			
		||||
                    {{tr "ProcessCount"}}: @# server.State.ProcessCount #@<br />
 | 
			
		||||
                    {{tr "ConnCount"}}: TCP @# server.State.TcpConnCount #@ / UDP @# server.State.UdpConnCount #@<br />
 | 
			
		||||
                    {{tr "BootTime"}}: @# formatTimestamp(server.Host.BootTime) #@<br />
 | 
			
		||||
                    {{tr "LastActive"}}: @# new Date(server.LastActive).toLocaleString() #@<br />
 | 
			
		||||
                    {{tr "Version"}}: @#server.Host.Version#@<br />
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="ui divider" style="margin-bottom: 5px"></div>
 | 
			
		||||
  <div class="ui container"> 
 | 
			
		||||
    <div class="ui styled fluid accordion" v-for="group in groups">
 | 
			
		||||
      <div class="active title">
 | 
			
		||||
        <i class="dropdown icon"></i>
 | 
			
		||||
        @#(group.Tag!==''?group.Tag:'{{tr "Default"}}')#@
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="active content">
 | 
			
		||||
        <div class="ui four stackable status cards">
 | 
			
		||||
          <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="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://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="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
 | 
			
		||||
                @#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
 | 
			
		||||
                <i class="nezha-secondary-font info circle icon" style="height: 28px"></i>
 | 
			
		||||
                <div class="ui content popup" style="margin-bottom: 0">
 | 
			
		||||
                  {{tr "Platform"}}: @#server.Host.Platform#@-@#server.Host.PlatformVersion#@
 | 
			
		||||
                  [<span
 | 
			
		||||
                    v-if="server.Host.Virtualization">@#server.Host.Virtualization#@:</span>@#server.Host.Arch#@]<br />
 | 
			
		||||
                  CPU: @#server.Host.CPU#@<br />
 | 
			
		||||
                  {{tr "DiskUsed"}}:
 | 
			
		||||
                  @#formatByteSize(server.State.DiskUsed)#@/@#formatByteSize(server.Host.DiskTotal)#@<br />
 | 
			
		||||
                  {{tr "MemUsed"}}:
 | 
			
		||||
                  @#formatByteSize(server.State.MemUsed)#@/@#formatByteSize(server.Host.MemTotal)#@<br />
 | 
			
		||||
                  {{tr "SwapUsed"}}:
 | 
			
		||||
                  @#formatByteSize(server.State.SwapUsed)#@/@#formatByteSize(server.Host.SwapTotal)#@<br />
 | 
			
		||||
                  {{tr "NetTransfer"}}: <i
 | 
			
		||||
                    class="arrow alternate circle down outline icon"></i>@#formatByteSize(server.State.NetInTransfer)#@<i
 | 
			
		||||
                    class="arrow alternate circle up outline icon"></i>@#formatByteSize(server.State.NetOutTransfer)#@<br />
 | 
			
		||||
                  {{tr "Load"}}: @# toFixed2(server.State.Load1) #@/@# toFixed2(server.State.Load5) #@/@#
 | 
			
		||||
                  toFixed2(server.State.Load15) #@<br />
 | 
			
		||||
                  {{tr "ProcessCount"}}: @# server.State.ProcessCount #@<br />
 | 
			
		||||
                  {{tr "ConnCount"}}: TCP @# server.State.TcpConnCount #@ / UDP @# server.State.UdpConnCount #@<br />
 | 
			
		||||
                  {{tr "BootTime"}}: @# formatTimestamp(server.Host.BootTime) #@<br />
 | 
			
		||||
                  {{tr "LastActive"}}: @# new Date(server.LastActive).toLocaleString() #@<br />
 | 
			
		||||
                  {{tr "Version"}}: @#server.Host.Version#@<br />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="description">
 | 
			
		||||
                  <div class="ui grid">
 | 
			
		||||
                    <div class="three wide column">CPU</div>
 | 
			
		||||
                    <div class="thirteen wide column">
 | 
			
		||||
                      <div :class="formatPercent(server.live,server.State.CPU, 100).class">
 | 
			
		||||
                        <div class="bar" :style="formatPercent(server.live,server.State.CPU, 100).style">
 | 
			
		||||
                          <small>@#formatPercent(server.live,server.State.CPU,100).percent#@%</small>
 | 
			
		||||
                        </div>
 | 
			
		||||
                <div class="ui divider" style="margin-bottom: 5px"></div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="description">
 | 
			
		||||
                <div class="ui grid">
 | 
			
		||||
                  <div class="three wide column">CPU</div>
 | 
			
		||||
                  <div class="thirteen wide column">
 | 
			
		||||
                    <div :class="formatPercent(server.live,server.State.CPU, 100).class">
 | 
			
		||||
                      <div class="bar" :style="formatPercent(server.live,server.State.CPU, 100).style">
 | 
			
		||||
                        <small>@#formatPercent(server.live,server.State.CPU,100).percent#@%</small>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="three wide column">{{tr "MemUsed"}}</div>
 | 
			
		||||
                    <div class="thirteen wide column">
 | 
			
		||||
                      <div :class="formatPercent(server.live,server.State.MemUsed, server.Host.MemTotal).class">
 | 
			
		||||
                        <div class="bar"
 | 
			
		||||
                          :style="formatPercent(server.live,server.State.MemUsed, server.Host.MemTotal).style">
 | 
			
		||||
                          <small>@#parseInt(server.State?server.State.MemUsed/server.Host.MemTotal*100:0)#@%</small>
 | 
			
		||||
                        </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="three wide column">{{tr "MemUsed"}}</div>
 | 
			
		||||
                  <div class="thirteen wide column">
 | 
			
		||||
                    <div :class="formatPercent(server.live,server.State.MemUsed, server.Host.MemTotal).class">
 | 
			
		||||
                      <div class="bar"
 | 
			
		||||
                        :style="formatPercent(server.live,server.State.MemUsed, server.Host.MemTotal).style">
 | 
			
		||||
                        <small>@#parseInt(server.State?server.State.MemUsed/server.Host.MemTotal*100:0)#@%</small>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="three wide column">{{tr "SwapUsed"}}</div>
 | 
			
		||||
                    <div class="thirteen wide column">
 | 
			
		||||
                      <div :class="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).class">
 | 
			
		||||
                        <div class="bar"
 | 
			
		||||
                          :style="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).style">
 | 
			
		||||
                          <small>@#parseInt(server.State?server.State.SwapUsed/server.Host.SwapTotal*100:0)#@%</small>
 | 
			
		||||
                        </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="three wide column">{{tr "SwapUsed"}}</div>
 | 
			
		||||
                  <div class="thirteen wide column">
 | 
			
		||||
                    <div :class="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).class">
 | 
			
		||||
                      <div class="bar"
 | 
			
		||||
                        :style="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).style">
 | 
			
		||||
                        <small>@#parseInt(server.State?server.State.SwapUsed/server.Host.SwapTotal*100:0)#@%</small>
 | 
			
		||||
                      </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 "DiskUsed"}}</div>
 | 
			
		||||
                    <div class="thirteen wide column">
 | 
			
		||||
                      <div :class="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).class">
 | 
			
		||||
                        <div class="bar"
 | 
			
		||||
                          :style="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).style">
 | 
			
		||||
                          <small>@#parseInt(server.State?server.State.DiskUsed/server.Host.DiskTotal*100:0)#@%</small>
 | 
			
		||||
                        </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 "DiskUsed"}}</div>
 | 
			
		||||
                  <div class="thirteen wide column">
 | 
			
		||||
                    <div :class="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).class">
 | 
			
		||||
                      <div class="bar"
 | 
			
		||||
                        :style="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).style">
 | 
			
		||||
                        <small>@#parseInt(server.State?server.State.DiskUsed/server.Host.DiskTotal*100:0)#@%</small>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="three wide column">{{tr "Info"}}</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> @#getByteToGB(server.Host.MemTotal)#@
 | 
			
		||||
                       
 | 
			
		||||
                      <i class="bi bi-hdd-rack-fill" style="font-size: 1.1rem; color: #980000"></i> @#getByteToGB(server.Host.DiskTotal)#@
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="three wide column">{{tr "Uptime"}}</div>
 | 
			
		||||
                    <div class="thirteen wide column">
 | 
			
		||||
                      <i class="clock icon"></i>@#secondToDate(server.State.Uptime)#@
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="three wide column">{{tr "Info"}}</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> @#getByteToGB(server.Host.MemTotal)#@
 | 
			
		||||
                     
 | 
			
		||||
                    <i class="bi bi-hdd-rack-fill" style="font-size: 1.1rem; color: #980000"></i> @#getByteToGB(server.Host.DiskTotal)#@
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="three wide column">{{tr "Uptime"}}</div>
 | 
			
		||||
                  <div class="thirteen wide column">
 | 
			
		||||
                    <i class="clock icon"></i>@#secondToDate(server.State.Uptime)#@
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="content" v-else>
 | 
			
		||||
                <p>@#server.Name#@</p>
 | 
			
		||||
                <p>{{tr "ServerIsOffline"}}</p>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="content" v-else>
 | 
			
		||||
              <p>@#server.Name#@</p>
 | 
			
		||||
              <p>{{tr "ServerIsOffline"}}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
@ -129,10 +127,13 @@
 | 
			
		||||
    el: '#app',
 | 
			
		||||
    delimiters: ['@#', '#@'],
 | 
			
		||||
    data: {
 | 
			
		||||
      defaultTemplate: {{.Conf.Site.Theme}},
 | 
			
		||||
      templates: {{.Themes}},
 | 
			
		||||
      data: initData,
 | 
			
		||||
      groups: [],
 | 
			
		||||
      cache: [],
 | 
			
		||||
    },
 | 
			
		||||
    mixins: [mixinsVue],
 | 
			
		||||
    created() {
 | 
			
		||||
      this.group()
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								resource/template/theme-angel-kanade/menu.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								resource/template/theme-angel-kanade/menu.html
									
									
									
									
										vendored
									
									
								
							@ -1,4 +1,5 @@
 | 
			
		||||
{{define "theme-angel-kanade/menu"}}
 | 
			
		||||
<div id="app">
 | 
			
		||||
<div class="ui large top fixed menu nb-menu">
 | 
			
		||||
    <div class="ui container">
 | 
			
		||||
        <a class="item" href="/">
 | 
			
		||||
@ -15,6 +16,17 @@
 | 
			
		||||
        {{else}}
 | 
			
		||||
        <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>
 | 
			
		||||
            {{ if not .Conf.DisableSwitchTemplateInFrontend }}        
 | 
			
		||||
            <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="(item, index) in adaptedTemplates" :key="index" @click="toggleTemplate(item.key)" class="item">
 | 
			
		||||
                        <i :class="item.icon + ' icon'"></i>@#item.name#@
 | 
			
		||||
                        <i class="check icon" v-if="preferredTemplate === item.key"></i>
 | 
			
		||||
                    </a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {{ end }}
 | 
			
		||||
        {{end}}
 | 
			
		||||
        <div class="right menu">
 | 
			
		||||
            <div class="item">
 | 
			
		||||
 | 
			
		||||
@ -76,10 +76,20 @@
 | 
			
		||||
                    {{end}}
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
 | 
			
		||||
            {{end}}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{{template "theme-angel-kanade/footer" .}}
 | 
			
		||||
<script>
 | 
			
		||||
    new Vue({
 | 
			
		||||
        el: '#app',
 | 
			
		||||
        delimiters: ['@#', '#@'],
 | 
			
		||||
        data: {
 | 
			
		||||
            defaultTemplate: {{.Conf.Site.Theme}},
 | 
			
		||||
            templates: {{.Themes}}
 | 
			
		||||
        },
 | 
			
		||||
        mixins: [mixinsVue]
 | 
			
		||||
    })
 | 
			
		||||
</script>
 | 
			
		||||
{{end}}
 | 
			
		||||
@ -109,7 +109,7 @@
 | 
			
		||||
<script>
 | 
			
		||||
        const monitorInfo =  JSON.parse('{{.MonitorInfos}}');
 | 
			
		||||
        const initData = JSON.parse('{{.Servers}}').servers;
 | 
			
		||||
        let MaxTCPPingValue = {{.MaxTCPPingValue}};
 | 
			
		||||
        let MaxTCPPingValue = {{.Conf.MaxTCPPingValue}};
 | 
			
		||||
	    if (MaxTCPPingValue == null) {
 | 
			
		||||
	        MaxTCPPingValue = 300;
 | 
			
		||||
	    }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								resource/template/theme-default/footer.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								resource/template/theme-default/footer.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
{{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>
 | 
			
		||||
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
 | 
			
		||||
<script>
 | 
			
		||||
    function showSwitchTemplate(list, currentBackendTheme) {
 | 
			
		||||
        console.log(list, currentBackendTheme);
 | 
			
		||||
        console.log("currentBackendTheme:",currentBackendTheme);
 | 
			
		||||
    }
 | 
			
		||||
    showSwitchTemplate({{ .Themes }}, {{ .Conf.Site.Theme }})
 | 
			
		||||
</script>
 | 
			
		||||
{{ end }}
 | 
			
		||||
<script>
 | 
			
		||||
</script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
{{end}}
 | 
			
		||||
							
								
								
									
										25
									
								
								resource/template/theme-default/header.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								resource/template/theme-default/header.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -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/semantic-ui@2.4.1/dist/semantic.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.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.0.0/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/jquery@3.7.1/dist/jquery.min.js"></script>
 | 
			
		||||
    <script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.1/dist/semantic.min.js"></script>
 | 
			
		||||
    <script src="/static/semantic-ui-alerts.min.js"></script>
 | 
			
		||||
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script>
 | 
			
		||||
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
 | 
			
		||||
    <script src="/static/theme-default/js/mixin.js?v20240302"></script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
{{end}}
 | 
			
		||||
							
								
								
									
										268
									
								
								resource/template/theme-default/home.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										268
									
								
								resource/template/theme-default/home.html
									
									
									
									
										vendored
									
									
								
							@ -1,10 +1,10 @@
 | 
			
		||||
{{define "theme-default/home"}}
 | 
			
		||||
{{template "common/header" .}}
 | 
			
		||||
{{template "theme-default/header" .}}
 | 
			
		||||
{{if ts .CustomCode}} {{.CustomCode|safe}} {{end}}
 | 
			
		||||
{{template "common/menu" .}}
 | 
			
		||||
{{template "theme-default/menu" .}}
 | 
			
		||||
<div class="nb-container">
 | 
			
		||||
  <div class="ui container">
 | 
			
		||||
    <div id="app">
 | 
			
		||||
  <div class="ui container">  
 | 
			
		||||
    <template v-if="groups">
 | 
			
		||||
      <div class="ui styled fluid accordion" v-for="group in groups">
 | 
			
		||||
        <div class="active title">
 | 
			
		||||
          <i class="dropdown icon"></i>
 | 
			
		||||
@ -15,12 +15,13 @@
 | 
			
		||||
            <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="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="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
 | 
			
		||||
                  @#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
 | 
			
		||||
                  <i class="nezha-secondary-font info circle icon" style="height: 28px"></i>
 | 
			
		||||
                  <div class="ui content popup" style="margin-bottom: 0">
 | 
			
		||||
                  <i @click="togglePopup($event, server.ID)" aria-expanded="false" class="nezha-secondary-font info circle icon"></i>                 
 | 
			
		||||
                  <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#@
 | 
			
		||||
                    [<span
 | 
			
		||||
                      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 "BootTime"}}: @# formatTimestamp(server.Host.BootTime) #@<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 class="ui divider" style="margin-bottom: 5px"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
@ -72,13 +74,6 @@
 | 
			
		||||
                        </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="thirteen wide column">
 | 
			
		||||
                      <div :class="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).class">
 | 
			
		||||
@ -88,6 +83,36 @@
 | 
			
		||||
                        </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="thirteen wide column">
 | 
			
		||||
                      <i class="clock icon"></i>@#secondToDate(server.State.Uptime)#@
 | 
			
		||||
@ -103,30 +128,177 @@
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    </template>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
{{template "common/footer" .}}
 | 
			
		||||
{{template "theme-default/footer" .}}
 | 
			
		||||
<script>
 | 
			
		||||
  const initData = JSON.parse('{{.Servers}}').servers;
 | 
			
		||||
  var statusCards = new Vue({
 | 
			
		||||
    el: '#app',
 | 
			
		||||
    delimiters: ['@#', '#@'],
 | 
			
		||||
    data: {
 | 
			
		||||
      data: initData,
 | 
			
		||||
      page: 'index',
 | 
			
		||||
      defaultTemplate: {{.Conf.Site.Theme}},
 | 
			
		||||
      templates: {{.Themes}},
 | 
			
		||||
      servers: [],
 | 
			
		||||
      groups: [],
 | 
			
		||||
      cache: [],
 | 
			
		||||
      chartDataList: [], 
 | 
			
		||||
      activePopup: null,
 | 
			
		||||
    },
 | 
			
		||||
    mixins: [mixinsVue],
 | 
			
		||||
    created() {
 | 
			
		||||
      this.servers = JSON.parse('{{.Servers}}').servers;
 | 
			
		||||
      this.group()
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
      $('.nezha-secondary-font.info.icon').popup({
 | 
			
		||||
        popup: '.ui.content.popup',
 | 
			
		||||
        exclusive: true,
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
      togglePopup(event, id) {
 | 
			
		||||
        // 切换弹出层的激活状态
 | 
			
		||||
        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);
 | 
			
		||||
          });
 | 
			
		||||
      },
 | 
			
		||||
      renderCharts(id) {
 | 
			
		||||
        if (!this.chartDataList[id - 1]) return;
 | 
			
		||||
        const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
 | 
			
		||||
        const isMobile = this.checkIsMobile();
 | 
			
		||||
        const fontSize = isMobile ? 10 : 9;
 | 
			
		||||
        const itemGap = isMobile ? 6 : 6;
 | 
			
		||||
        const itemWidth = isMobile ? 10 : 10;
 | 
			
		||||
        const itemHeight = isMobile ? 10 : 10;
 | 
			
		||||
        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 && avgDelay < MaxTCPPingValue){
 | 
			
		||||
              loss += avgDelay > 0.9 * MaxTCPPingValue ? 1 : 0;
 | 
			
		||||
              return [item.created_at[index], avgDelay];
 | 
			
		||||
            }else{
 | 
			
		||||
              loss += 1;
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          const lossRate = ((loss / item.created_at.length) * 100).toFixed(1);
 | 
			
		||||
          item.monitor_name = item.monitor_name.includes("%") ? item.monitor_name : `${item.monitor_name} ${lossRate}%`;
 | 
			
		||||
          return {
 | 
			
		||||
            name: item.monitor_name,
 | 
			
		||||
            type: 'line',
 | 
			
		||||
            smooth: true,
 | 
			
		||||
            symbol: 'none',
 | 
			
		||||
            data: data,
 | 
			
		||||
            connectNulls: true
 | 
			
		||||
          };
 | 
			
		||||
        });
 | 
			
		||||
        const option = {
 | 
			
		||||
          backgroundColor: backgroundColor,
 | 
			
		||||
          title: {
 | 
			
		||||
            show: false
 | 
			
		||||
          },
 | 
			
		||||
          tooltip: {
 | 
			
		||||
            show: true,
 | 
			
		||||
            trigger: 'axis',
 | 
			
		||||
            textStyle: {
 | 
			
		||||
              fontSize: fontSize,
 | 
			
		||||
              color: fontColor
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          legend: {
 | 
			
		||||
            icon: 'rect',
 | 
			
		||||
            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,
 | 
			
		||||
            itemHeight: itemHeight,
 | 
			
		||||
            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: '30',
 | 
			
		||||
            bottom: '20',
 | 
			
		||||
            left: gridLeft,
 | 
			
		||||
            right: gridRight
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
        chart.setOption(option);
 | 
			
		||||
      },
 | 
			
		||||
      checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
 | 
			
		||||
        return window.innerWidth <= 768;
 | 
			
		||||
      },
 | 
			
		||||
      toFixed2(f) {
 | 
			
		||||
        return f.toFixed(2)
 | 
			
		||||
      },
 | 
			
		||||
@ -191,7 +363,7 @@
 | 
			
		||||
        return '';
 | 
			
		||||
      },
 | 
			
		||||
      group() {
 | 
			
		||||
        this.groups = groupingData(this.data, "Tag")
 | 
			
		||||
        this.groups = groupingData(this.servers, "Tag")
 | 
			
		||||
      },
 | 
			
		||||
      formatPercent(live, used, total) {
 | 
			
		||||
        const percent = live ? (parseInt(used / total * 100) || 0) : -1
 | 
			
		||||
@ -227,7 +399,7 @@
 | 
			
		||||
      secondToDate(s) {
 | 
			
		||||
        var d = Math.floor(s / 3600 / 24);
 | 
			
		||||
        if (d > 0) {
 | 
			
		||||
          return d + ' {{tr "Day"}}'
 | 
			
		||||
          return d + " {{tr "Day"}}"
 | 
			
		||||
        }
 | 
			
		||||
        var h = Math.floor(s / 3600 % 24);
 | 
			
		||||
        var m = Math.floor(s / 60 % 60);
 | 
			
		||||
@ -238,8 +410,48 @@
 | 
			
		||||
        return new Date(t * 1000).toLocaleString()
 | 
			
		||||
      },
 | 
			
		||||
      formatByteSize(bs) {
 | 
			
		||||
        const x = readableBytes(bs)
 | 
			
		||||
        const x = this.readableBytes(bs)
 | 
			
		||||
        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)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										50
									
								
								resource/template/theme-default/menu.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								resource/template/theme-default/menu.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
			
		||||
{{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>
 | 
			
		||||
        {{ if not .Conf.DisableSwitchTemplateInFrontend }}        
 | 
			
		||||
            <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="(item, index) in adaptedTemplates" :key="index" @click="toggleTemplate(item.key)" class="item">
 | 
			
		||||
                        <i :class="item.icon + ' icon'"></i>@#item.name#@
 | 
			
		||||
                        <i class="check icon" v-if="preferredTemplate === item.key"></i>
 | 
			
		||||
                    </a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        {{ end }}
 | 
			
		||||
        {{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}}
 | 
			
		||||
							
								
								
									
										15
									
								
								resource/template/theme-default/network.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								resource/template/theme-default/network.html
									
									
									
									
										vendored
									
									
								
							@ -1,10 +1,10 @@
 | 
			
		||||
{{define "theme-default/network"}}
 | 
			
		||||
{{template "common/header" .}}
 | 
			
		||||
{{template "theme-default/header" .}}
 | 
			
		||||
{{if ts .CustomCode}}
 | 
			
		||||
{{.CustomCode|safe}}
 | 
			
		||||
{{end}}
 | 
			
		||||
{{template "common/menu" .}}
 | 
			
		||||
<div class="nb-container" id="app">
 | 
			
		||||
{{template "theme-default/menu" .}}
 | 
			
		||||
<div class="nb-container">
 | 
			
		||||
    <div class="ui container">
 | 
			
		||||
        <div class="service-status">
 | 
			
		||||
            <table class="ui celled table">
 | 
			
		||||
@ -22,13 +22,12 @@
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{{template "common/footer" .}}
 | 
			
		||||
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
 | 
			
		||||
{{template "theme-default/footer" .}}
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    const monitorInfo =  JSON.parse('{{.MonitorInfos}}');
 | 
			
		||||
    const initData = JSON.parse('{{.Servers}}').servers;
 | 
			
		||||
    let MaxTCPPingValue = {{.MaxTCPPingValue}};
 | 
			
		||||
    let MaxTCPPingValue = {{.Conf.MaxTCPPingValue}};
 | 
			
		||||
    if (MaxTCPPingValue == null) {
 | 
			
		||||
        MaxTCPPingValue = 1000;
 | 
			
		||||
    }
 | 
			
		||||
@ -36,6 +35,9 @@
 | 
			
		||||
        el: '#app',
 | 
			
		||||
        delimiters: ['@#', '#@'],
 | 
			
		||||
        data: {
 | 
			
		||||
            page: 'network',
 | 
			
		||||
            defaultTemplate: {{.Conf.Site.Theme}},
 | 
			
		||||
            templates: {{.Themes}},
 | 
			
		||||
            servers: initData,
 | 
			
		||||
            option: {
 | 
			
		||||
                tooltip: {
 | 
			
		||||
@ -93,6 +95,7 @@
 | 
			
		||||
            },
 | 
			
		||||
            chartOnOff: true,
 | 
			
		||||
        },
 | 
			
		||||
        mixins: [mixinsVue],
 | 
			
		||||
        mounted() {
 | 
			
		||||
            this.renderChart();
 | 
			
		||||
             this.parseMonitorInfo(monitorInfo);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										19
									
								
								resource/template/theme-default/service.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								resource/template/theme-default/service.html
									
									
									
									
										vendored
									
									
								
							@ -1,9 +1,9 @@
 | 
			
		||||
{{define "theme-default/service"}}
 | 
			
		||||
{{template "common/header" .}}
 | 
			
		||||
{{template "theme-default/header" .}}
 | 
			
		||||
{{if ts .CustomCode}}
 | 
			
		||||
{{.CustomCode|safe}}
 | 
			
		||||
{{end}}
 | 
			
		||||
{{template "common/menu" .}}
 | 
			
		||||
{{template "theme-default/menu" .}}
 | 
			
		||||
<div class="nb-container">
 | 
			
		||||
    <div class="ui container">
 | 
			
		||||
        <div class="service-status">
 | 
			
		||||
@ -76,10 +76,21 @@
 | 
			
		||||
                    {{end}}
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
 | 
			
		||||
            {{end}}
 | 
			
		||||
        </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}}
 | 
			
		||||
							
								
								
									
										1
									
								
								resource/template/theme-mdui/menu.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								resource/template/theme-mdui/menu.html
									
									
									
									
										vendored
									
									
								
							@ -12,6 +12,7 @@
 | 
			
		||||
      {{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="/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}}
 | 
			
		||||
      <div class="mdui-toolbar-spacer"></div>
 | 
			
		||||
      {{if .Admin}}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										255
									
								
								resource/template/theme-mdui/network.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								resource/template/theme-mdui/network.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -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 = {{.Conf.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}}
 | 
			
		||||
@ -1,7 +1,20 @@
 | 
			
		||||
{{define "theme-server-status/content-footer"}}
 | 
			
		||||
<footer class="container" style="padding-bottom: 2rem;">
 | 
			
		||||
    <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>
 | 
			
		||||
</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}}
 | 
			
		||||
@ -1,49 +1,63 @@
 | 
			
		||||
{{define "theme-server-status/content-nav"}}
 | 
			
		||||
<div role="navigation" class="navbar navbar-inverse navbar-fixed-top">
 | 
			
		||||
    <div class="navbar-inner">
 | 
			
		||||
        <div class="container pl-md-unset">
 | 
			
		||||
            <div class="navbar-header">
 | 
			
		||||
                <button data-target=".navbar-collapse" data-toggle="collapse" class="navbar-toggle" type="button">
 | 
			
		||||
                    <span class="sr-only">Toggle navigation</span>
 | 
			
		||||
                    <span class="icon-bar"></span>
 | 
			
		||||
                    <span class="icon-bar"></span>
 | 
			
		||||
                    <span class="icon-bar"></span>
 | 
			
		||||
                </button>
 | 
			
		||||
                <a href="/" class="navbar-brand pl-md-unset">
 | 
			
		||||
                    <img src="/static/logo.svg?v20210804" style="height: 2rem;display: inline-block;">
 | 
			
		||||
                    {{.Conf.Site.Brand}}
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="navbar-collapse collapse">
 | 
			
		||||
                <ul class="nav navbar-nav">
 | 
			
		||||
                    <li><a href="/">{{tr "Home" }}</a></li>
 | 
			
		||||
                    <li><a href="/service">{{tr "Services" }}</a></li>
 | 
			
		||||
                    <li><a href="/network">{{tr "NetworkSpiter" }}</a></li>
 | 
			
		||||
                </ul>
 | 
			
		||||
                <ul class="nav navbar-nav navbar-right">
 | 
			
		||||
<header role="navigation" class="navbar navbar-inverse navbar-fixed-top" style="z-index:99999999;">
 | 
			
		||||
    <div class="container">
 | 
			
		||||
        <div class="navbar-header">
 | 
			
		||||
            <button data-target=".navbar-collapse" data-toggle="collapse" class="navbar-toggle" type="button">
 | 
			
		||||
                <span class="sr-only">Toggle navigation</span>
 | 
			
		||||
                <span class="icon-bar"></span>
 | 
			
		||||
                <span class="icon-bar"></span>
 | 
			
		||||
                <span class="icon-bar"></span>
 | 
			
		||||
            </button>
 | 
			
		||||
            <a href="/" class="navbar-brand">
 | 
			
		||||
                <img src="/static/logo.svg" style="height: 2rem;display: inline-block;">
 | 
			
		||||
                {{.Conf.Site.Brand}}
 | 
			
		||||
            </a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <nav id="navbar" class="navbar-collapse collapse">
 | 
			
		||||
            <ul class="nav navbar-nav">
 | 
			
		||||
                <li><a href="/"><i class="home icon"></i>{{tr "Home" }}</a></li>
 | 
			
		||||
                <template v-if="isMobile"> 
 | 
			
		||||
                    <li class="dropdown">
 | 
			
		||||
                        <a data-toggle="dropdown" href="#">{{tr "Menu" }}<b class="caret"></b></a>
 | 
			
		||||
                        <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>
 | 
			
		||||
                {{ if not .Conf.DisableSwitchTemplateInFrontend }}
 | 
			
		||||
                    <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">
 | 
			
		||||
                            {{if .Admin}}
 | 
			
		||||
                            <li><a href="/server">{{tr "AdminPanel" }} ({{.Admin.Name}})</a></li>
 | 
			
		||||
                            {{else}}
 | 
			
		||||
                            <li><a href="/login">{{tr "Login" }}</a></li>
 | 
			
		||||
                            {{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 v-for="(item, index) in adaptedTemplates" :key="index">
 | 
			
		||||
                                <a @click="toggleTemplate(item.key)">
 | 
			
		||||
                                    <i :class="item.icon + ' icon'" style="font-size:1em"></i>@#item.name#@
 | 
			
		||||
                                    <i class="check icon" v-if="preferredTemplate === item.key"></i>
 | 
			
		||||
                                </a>
 | 
			
		||||
                            </li>
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
                {{ end }}
 | 
			
		||||
            </ul>
 | 
			
		||||
            <ul class="nav navbar-nav navbar-right">
 | 
			
		||||
                {{if .Admin}}
 | 
			
		||||
                <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}}
 | 
			
		||||
                <li><a href="/login"><i class="sign-in icon"></i>{{tr "Login" }}</a></li>
 | 
			
		||||
                {{end}}
 | 
			
		||||
            </ul>
 | 
			
		||||
        </nav>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
</header>
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,16 +6,16 @@
 | 
			
		||||
    <meta charset="utf-8">
 | 
			
		||||
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
			
		||||
    <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/bootstrap@3.4.1/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/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/light.css">
 | 
			
		||||
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-logos/0.17/font-logos.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.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.jsdelivr.net/gh/lipis/flag-icons@7.0.0/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/semantic-ui@2.4.1/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 -->
 | 
			
		||||
    <!--[if lt IE 9]>
 | 
			
		||||
    <script src="/static/theme-server-status/js/html5shiv.js"></script>
 | 
			
		||||
@ -24,11 +24,11 @@
 | 
			
		||||
    {{if ts .CustomCode}}
 | 
			
		||||
    {{.CustomCode|safe}}
 | 
			
		||||
    {{end}}
 | 
			
		||||
    <script src="/static/theme-server-status/js/jquery.min.js"></script>
 | 
			
		||||
    <script src="/static/theme-server-status/js/bootstrap.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/theme-server-status/js/mixin.js"></script>
 | 
			
		||||
    <script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
 | 
			
		||||
    <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
 | 
			
		||||
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js"></script>
 | 
			
		||||
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script>
 | 
			
		||||
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
 | 
			
		||||
    <script src="/static/theme-server-status/js/mixin.js?v20240302"></script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
{{end}}
 | 
			
		||||
{{end}}
 | 
			
		||||
							
								
								
									
										134
									
								
								resource/template/theme-server-status/home-group-false.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								resource/template/theme-server-status/home-group-false.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -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}}
 | 
			
		||||
							
								
								
									
										137
									
								
								resource/template/theme-server-status/home-group-true.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								resource/template/theme-server-status/home-group-true.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -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}}
 | 
			
		||||
							
								
								
									
										398
									
								
								resource/template/theme-server-status/home.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										398
									
								
								resource/template/theme-server-status/home.html
									
									
									
									
										vendored
									
									
								
							@ -1,156 +1,58 @@
 | 
			
		||||
{{define "theme-server-status/home"}}
 | 
			
		||||
{{template "theme-server-status/header" .}}
 | 
			
		||||
<div id="app">
 | 
			
		||||
    {{template "theme-server-status/content-nav" .}}
 | 
			
		||||
    <div class="container table-responsive content" style="max-width: 95vw" v-for="group in nodes">
 | 
			
		||||
        <table class="table table-striped table-condensed table-hover">
 | 
			
		||||
            <thead>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <th class="node-group-tag" colspan="16" style="border:none;">@#(group.Tag!==''?group.Tag:'{{tr "Default"}}')#@</th>
 | 
			
		||||
                </th>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <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 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>
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
    </div>
 | 
			
		||||
    {{template "theme-server-status/content-nav" .}}   
 | 
			
		||||
    <!--  showGroup true -->
 | 
			
		||||
    <template v-if="showGroup">
 | 
			
		||||
        <section class="container table-responsive content" style="max-width: 95vw" v-for="group in nodesTag">
 | 
			
		||||
            {{template "theme-server-status/home-group-true" .}} 
 | 
			
		||||
        </section>
 | 
			
		||||
    </template>
 | 
			
		||||
    <!--  showGroup false -->
 | 
			
		||||
    <template v-else>
 | 
			
		||||
        <section class="container table-responsive content" style="max-width: 95vw">
 | 
			
		||||
            {{template "theme-server-status/home-group-false" .}}
 | 
			
		||||
        </section>
 | 
			
		||||
    </template>
 | 
			
		||||
    {{template "theme-server-status/content-footer" .}}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    new Vue({
 | 
			
		||||
        el: '#app',
 | 
			
		||||
        delimiters: ['@#', '#@'],
 | 
			
		||||
        data: {
 | 
			
		||||
            nodes: [],
 | 
			
		||||
            page: 'index',
 | 
			
		||||
            defaultTemplate: {{.Conf.Site.Theme}},
 | 
			
		||||
            templates: {{.Themes}},
 | 
			
		||||
            cache: [],
 | 
			
		||||
            servers: [],
 | 
			
		||||
            nodesTag: [],
 | 
			
		||||
            nodesNoTag: [],
 | 
			
		||||
            chartDataList: [],
 | 
			
		||||
            ws: null
 | 
			
		||||
        },
 | 
			
		||||
        mixins: [mixinsVue],
 | 
			
		||||
        created() {
 | 
			
		||||
            const initData = JSON.parse('{{.Servers}}').servers;           
 | 
			
		||||
            this.nodes = groupingData(this.handleNodes(initData),"Tag");
 | 
			
		||||
            this.initTheme()
 | 
			
		||||
            this.servers = JSON.parse('{{.Servers}}').servers;
 | 
			
		||||
            if(this.showGroup) {
 | 
			
		||||
                this.nodesTag = this.groupingData(this.handleNodes(this.servers),"Tag");
 | 
			
		||||
            } else {
 | 
			
		||||
                this.nodesNoTag = this.handleNodes(this.servers);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        mounted() {
 | 
			
		||||
            // 初始化时建立WebSocket连接
 | 
			
		||||
            this.connect();
 | 
			
		||||
            // 监听页面可见性变化
 | 
			
		||||
            document.addEventListener("visibilitychange", () => {
 | 
			
		||||
                if (document.visibilityState === "visible") {
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        if (document.hasFocus()) {
 | 
			
		||||
                            this.connect();
 | 
			
		||||
                        }
 | 
			
		||||
                    }, 200);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        methods: {
 | 
			
		||||
            isWindowsPlatform(str) {
 | 
			
		||||
@ -231,13 +133,18 @@
 | 
			
		||||
                return x !== "NaN undefined" ? x : '0B'
 | 
			
		||||
            },
 | 
			
		||||
            formatPercent(live, used, total) {
 | 
			
		||||
                const percent = live ? (this.toFixed2(used / total * 100) || 0) : 0
 | 
			
		||||
                return this.formatPercents(percent)
 | 
			
		||||
                //const percent = live ? (this.toFixed2(used / total * 100) || 0) : 0
 | 
			
		||||
                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) {
 | 
			
		||||
                    percent = 0;
 | 
			
		||||
                }
 | 
			
		||||
                if (percent >= 100) {
 | 
			
		||||
                    percent = 100;
 | 
			
		||||
                }
 | 
			
		||||
                if (!this.cache[percent]) {
 | 
			
		||||
                    this.cache[percent] = {
 | 
			
		||||
                        class: 'progress-bar progress-bar-success',
 | 
			
		||||
@ -263,12 +170,18 @@
 | 
			
		||||
                return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + sizes[i];
 | 
			
		||||
            },
 | 
			
		||||
            connect() {
 | 
			
		||||
                const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws"
 | 
			
		||||
                const ws = new WebSocket(wsProtocol + '://' + window.location.host + '/ws');
 | 
			
		||||
                ws.onopen = function () {
 | 
			
		||||
                // 如果已经存在 WebSocket 连接并且处于开启状态,则不重复建立连接
 | 
			
		||||
                if (this.ws && this.ws.readyState === WebSocket.OPEN) {
 | 
			
		||||
                    console.log('Closing old WebSocket connection...');
 | 
			
		||||
                    this.ws.close();
 | 
			
		||||
                }
 | 
			
		||||
                const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
 | 
			
		||||
                this.ws = new WebSocket(wsProtocol + '://' + window.location.host + '/ws');
 | 
			
		||||
 | 
			
		||||
                this.ws.onopen = function () {
 | 
			
		||||
                    console.log("Connection open ...")
 | 
			
		||||
                }
 | 
			
		||||
                ws.onmessage = (evt) => {
 | 
			
		||||
                this.ws.onmessage = (evt) => {
 | 
			
		||||
                    let jsonData = evt.data
 | 
			
		||||
                    const data = JSON.parse(jsonData)
 | 
			
		||||
                    for (let i = 0; i < data.servers?.length; i++) {
 | 
			
		||||
@ -279,16 +192,20 @@
 | 
			
		||||
                            const lastActive = new Date(ns.LastActive).getTime()
 | 
			
		||||
                            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 = () => {
 | 
			
		||||
                    setTimeout(function () {
 | 
			
		||||
                        this.connect()
 | 
			
		||||
                this.ws.onclose = () => {
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        this.connect();
 | 
			
		||||
                    }, 5000);
 | 
			
		||||
                }
 | 
			
		||||
                ws.onerror = function () {
 | 
			
		||||
                    ws.close()
 | 
			
		||||
                };
 | 
			
		||||
                this.ws.onerror = () => {
 | 
			
		||||
                    this.ws.close()
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            handleNodes(servers) {
 | 
			
		||||
@ -309,7 +226,7 @@
 | 
			
		||||
                        load: this.toFixed2(server.State.Load1),
 | 
			
		||||
                        network: this.getNetworkSpeed(server.State.NetInSpeed, server.State.NetOutSpeed),
 | 
			
		||||
                        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),
 | 
			
		||||
                        hdd: this.formatPercent(server.live, server.State.DiskUsed, server.Host.DiskTotal),
 | 
			
		||||
                        online: server.live,
 | 
			
		||||
@ -324,29 +241,164 @@
 | 
			
		||||
            },
 | 
			
		||||
            getNetworkSpeed(netInSpeed, 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');
 | 
			
		||||
                const isAriaExpandedFalse = event.currentTarget.getAttribute('aria-expanded') === 'false';
 | 
			
		||||
                if (!isAriaExpandedFalse) return;
 | 
			
		||||
                // 发起数据请求
 | 
			
		||||
                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);
 | 
			
		||||
                    });
 | 
			
		||||
            },
 | 
			
		||||
            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 = {{.Conf.MaxTCPPingValue}} ? {{.Conf.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 && avgDelay < MaxTCPPingValue){
 | 
			
		||||
                            loss += avgDelay > 0.9 * MaxTCPPingValue ? 1 : 0;
 | 
			
		||||
                            return [item.created_at[index], avgDelay];
 | 
			
		||||
                        }else{
 | 
			
		||||
                            loss += 1;
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                    const lossRate = ((loss / item.created_at.length) * 100).toFixed(1);
 | 
			
		||||
                    item.monitor_name = item.monitor_name.includes("%") ? item.monitor_name : `${item.monitor_name} ${lossRate}%`;
 | 
			
		||||
                    return {
 | 
			
		||||
                        name: item.monitor_name,
 | 
			
		||||
                        type: 'line',
 | 
			
		||||
                        smooth: true,
 | 
			
		||||
                        symbol: 'none',
 | 
			
		||||
                        data: data,
 | 
			
		||||
                        connectNulls: true
 | 
			
		||||
                    };
 | 
			
		||||
                });
 | 
			
		||||
                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() { // 重新加载所有图表
 | 
			
		||||
                this.servers.forEach(node => {
 | 
			
		||||
                    const id = node.ID;
 | 
			
		||||
                    const chartData = this.chartDataList[id - 1];
 | 
			
		||||
                    if (chartData) {
 | 
			
		||||
                        this.renderCharts(id,true);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    function 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;
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
{{template "theme-server-status/footer" .}}
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -21,14 +21,17 @@
 | 
			
		||||
<script>
 | 
			
		||||
    const monitorInfo =  JSON.parse('{{.MonitorInfos}}');
 | 
			
		||||
    const initData = JSON.parse('{{.Servers}}').servers;
 | 
			
		||||
    let MaxTCPPingValue = {{.MaxTCPPingValue}};
 | 
			
		||||
    let MaxTCPPingValue = {{.Conf.MaxTCPPingValue}};
 | 
			
		||||
    if (MaxTCPPingValue == null) {
 | 
			
		||||
        MaxTCPPingValue = 300;
 | 
			
		||||
        MaxTCPPingValue = 1000;
 | 
			
		||||
    }
 | 
			
		||||
    new Vue({
 | 
			
		||||
        el: '#app',
 | 
			
		||||
        delimiters: ['@#', '#@'],
 | 
			
		||||
        data: {
 | 
			
		||||
            page: 'network',
 | 
			
		||||
            defaultTemplate: {{.Conf.Site.Theme}},
 | 
			
		||||
            templates: {{.Themes}},
 | 
			
		||||
            servers: initData,
 | 
			
		||||
            option: {
 | 
			
		||||
                tooltip: {
 | 
			
		||||
@ -70,7 +73,7 @@
 | 
			
		||||
                },
 | 
			
		||||
                dataZoom: [
 | 
			
		||||
                    {
 | 
			
		||||
                        start: 94,
 | 
			
		||||
                        start: 0,
 | 
			
		||||
                        end: 100
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
@ -80,19 +83,16 @@
 | 
			
		||||
                },
 | 
			
		||||
                yAxis: {
 | 
			
		||||
                    type: 'value',
 | 
			
		||||
                    boundaryGap: [0, '100%']
 | 
			
		||||
                    boundaryGap: false
 | 
			
		||||
                },
 | 
			
		||||
                series: [],
 | 
			
		||||
            },
 | 
			
		||||
            chartOnOff: true,
 | 
			
		||||
        },
 | 
			
		||||
        mixins: [mixinsVue],
 | 
			
		||||
        created() {
 | 
			
		||||
            this.initTheme();
 | 
			
		||||
        },
 | 
			
		||||
        mounted() {
 | 
			
		||||
            this.renderChart();
 | 
			
		||||
             this.parseMonitorInfo(monitorInfo);
 | 
			
		||||
            this.parseMonitorInfo(monitorInfo);
 | 
			
		||||
        },
 | 
			
		||||
        methods: {
 | 
			
		||||
            getFontLogoClass(str) {
 | 
			
		||||
@ -153,15 +153,15 @@
 | 
			
		||||
                return '';
 | 
			
		||||
            },
 | 
			
		||||
            redirectNetwork(id) {
 | 
			
		||||
                    this.getMonitorHistory(id)
 | 
			
		||||
                    .then(function(monitorInfo) {
 | 
			
		||||
                          var vm = app.__vue__;
 | 
			
		||||
                          vm.parseMonitorInfo(monitorInfo);
 | 
			
		||||
                    })
 | 
			
		||||
                    .catch(function(error){
 | 
			
		||||
                        window.location.href = "/404";
 | 
			
		||||
                    })
 | 
			
		||||
                },
 | 
			
		||||
                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,
 | 
			
		||||
@ -175,11 +175,13 @@
 | 
			
		||||
                    let loss = 0;
 | 
			
		||||
                    let data = [];
 | 
			
		||||
                    for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
 | 
			
		||||
                        avgDelay = monitorInfo.result[i].avg_delay[j];
 | 
			
		||||
                        avgDelay = Math.round(monitorInfo.result[i].avg_delay[j]);
 | 
			
		||||
                        if (avgDelay > 0.9 * MaxTCPPingValue) {
 | 
			
		||||
                            loss += 1
 | 
			
		||||
                        }
 | 
			
		||||
                        data.push([monitorInfo.result[i].created_at[j], avgDelay]);
 | 
			
		||||
                        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 + "%";
 | 
			
		||||
@ -189,7 +191,13 @@
 | 
			
		||||
                            type: 'line',
 | 
			
		||||
                            smooth: true,
 | 
			
		||||
                            symbol: 'none',
 | 
			
		||||
                            data: data
 | 
			
		||||
                            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;
 | 
			
		||||
@ -214,4 +222,3 @@
 | 
			
		||||
</script>
 | 
			
		||||
{{template "theme-server-status/footer" .}}
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										46
									
								
								resource/template/theme-server-status/service-group-false.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								resource/template/theme-server-status/service-group-false.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -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}}
 | 
			
		||||
							
								
								
									
										51
									
								
								resource/template/theme-server-status/service-group-true.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								resource/template/theme-server-status/service-group-true.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -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}}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										145
									
								
								resource/template/theme-server-status/service.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										145
									
								
								resource/template/theme-server-status/service.html
									
									
									
									
										vendored
									
									
								
							@ -2,110 +2,89 @@
 | 
			
		||||
{{template "theme-server-status/header" .}}
 | 
			
		||||
<div id="app">
 | 
			
		||||
    {{template "theme-server-status/content-nav" .}}
 | 
			
		||||
    <div class="container content" style="max-width: 95vw">
 | 
			
		||||
        <table class="table table-striped table-condensed service-status">
 | 
			
		||||
    <!--  showGroup true -->
 | 
			
		||||
    <template v-if="showGroup">
 | 
			
		||||
        <section class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;" v-for="group in servicesTag">
 | 
			
		||||
            {{template "theme-server-status/service-group-true" .}} 
 | 
			
		||||
        </section>
 | 
			
		||||
    </template>
 | 
			
		||||
    <!--  showGroup false -->
 | 
			
		||||
    <template v-else>
 | 
			
		||||
        <section class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;">
 | 
			
		||||
            {{template "theme-server-status/service-group-false" .}}
 | 
			
		||||
        </section>
 | 
			
		||||
    </template>
 | 
			
		||||
    <section class="container content table-responsive" style="max-width: 95vw">
 | 
			
		||||
        {{if .CycleTransferStats}}
 | 
			
		||||
        <table class="table table-striped table-condensed table-hover">
 | 
			
		||||
            <thead>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <th class="node-cell center" style="min-width:60px">{{tr "Status"}}</th>
 | 
			
		||||
                <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>
 | 
			
		||||
                <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">{{tr "Rules"}}</th>
 | 
			
		||||
                    <th class="node-cell center">{{tr "Server"}}</th>
 | 
			
		||||
                    <th class="node-cell center">{{tr "From"}}</th>
 | 
			
		||||
                    <th class="node-cell center">{{tr "To"}}</th>
 | 
			
		||||
                    <th class="node-cell center">MAX</th>
 | 
			
		||||
                    <th class="node-cell center">MIN</th>
 | 
			
		||||
                    <th class="node-cell center">{{tr "NextCheck"}}</th>
 | 
			
		||||
                    <th class="node-cell center">{{tr "CurrentUsage"}}</th>
 | 
			
		||||
                    <th class="node-cell center">{{tr "Transleft"}}</th>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody id="servers">
 | 
			
		||||
            <template v-for="service in services">
 | 
			
		||||
            <tbody>
 | 
			
		||||
                {{range $id, $stats := .CycleTransferStats}}
 | 
			
		||||
                {{range $innerId, $transfer := $stats.Transfer}}
 | 
			
		||||
                {{$TransLeftPercent := TransLeftPercent (UintToFloat $transfer) (UintToFloat $stats.Max)}}
 | 
			
		||||
                <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>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td class="node-cell center">@#service.avgDelay#@</td>
 | 
			
		||||
                    <td class="node-cell center">{{$id}}</td>
 | 
			
		||||
                    <td class="node-cell center">{{$stats.Name}}</td>
 | 
			
		||||
                    <td class="node-cell center">{{index $stats.ServerName $innerId}}</td>
 | 
			
		||||
                    <td class="node-cell center">{{$stats.From|tf}}</td>
 | 
			
		||||
                    <td class="node-cell center">{{$stats.To|tf}}</td>
 | 
			
		||||
                    <td class="node-cell center">{{$stats.Max|bf}}</td>
 | 
			
		||||
                    <td class="node-cell center">{{$stats.Min|bf}}</td>
 | 
			
		||||
                    <td class="node-cell center">{{(index $stats.NextUpdate $innerId)|sft}}</td>
 | 
			
		||||
                    <td class="node-cell center">{{$transfer|bf}}</td>
 | 
			
		||||
                    <td class="node-cell center">
 | 
			
		||||
                        <div class="progress">
 | 
			
		||||
                            <div :style="service.totalUpTime.style" :class="service.totalUpTime.className">
 | 
			
		||||
                                <small>@#service.totalUpTime.percent#@%</small>
 | 
			
		||||
                            <div style="width: {{$TransLeftPercent}}%" :class="'progress-bar progress-bar-' + toSSBar('{{TransClassName $TransLeftPercent}}')">
 | 
			
		||||
                                <small style="display: inline-block;width: max-content;">{{TransLeft $stats.Max $transfer}} / {{$TransLeftPercent}} %</small>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </template>
 | 
			
		||||
                {{end}}
 | 
			
		||||
                {{end}}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="container" style="padding:unset;max-width: 95vw">
 | 
			
		||||
        {{if .CycleTransferStats}}
 | 
			
		||||
        <h4 style="text-align: center;">{{tr "CycleTransferStats"}}</h4>
 | 
			
		||||
        <div class="table-responsive content">
 | 
			
		||||
            <table class="table table-striped table-condensed">
 | 
			
		||||
                <thead>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th class="node-cell center">ID</th>
 | 
			
		||||
                        <th class="node-cell center">{{tr "Rules"}}</th>
 | 
			
		||||
                        <th class="node-cell center">{{tr "Server"}}</th>
 | 
			
		||||
                        <th class="node-cell center">{{tr "From"}}</th>
 | 
			
		||||
                        <th class="node-cell center">{{tr "To"}}</th>
 | 
			
		||||
                        <th class="node-cell center">MAX</th>
 | 
			
		||||
                        <th class="node-cell center">MIN</th>
 | 
			
		||||
                        <th class="node-cell center">{{tr "NextCheck"}}</th>
 | 
			
		||||
                        <th class="node-cell center">{{tr "CurrentUsage"}}</th>
 | 
			
		||||
                        <th class="node-cell center">{{tr "Transleft"}}</th>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody>
 | 
			
		||||
                    {{range $id, $stats := .CycleTransferStats}}
 | 
			
		||||
                    {{range $innerId, $transfer := $stats.Transfer}}
 | 
			
		||||
                    {{$TransLeftPercent := TransLeftPercent (UintToFloat $transfer) (UintToFloat $stats.Max)}}
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <td class="node-cell center">{{$id}}</td>
 | 
			
		||||
                        <td class="node-cell center">{{$stats.Name}}</td>
 | 
			
		||||
                        <td class="node-cell center">{{index $stats.ServerName $innerId}}</td>
 | 
			
		||||
                        <td class="node-cell center">{{$stats.From|tf}}</td>
 | 
			
		||||
                        <td class="node-cell center">{{$stats.To|tf}}</td>
 | 
			
		||||
                        <td class="node-cell center">{{$stats.Max|bf}}</td>
 | 
			
		||||
                        <td class="node-cell center">{{$stats.Min|bf}}</td>
 | 
			
		||||
                        <td class="node-cell center">{{(index $stats.NextUpdate $innerId)|sft}}</td>
 | 
			
		||||
                        <td class="node-cell center">{{$transfer|bf}}</td>
 | 
			
		||||
                        <td class="node-cell center">
 | 
			
		||||
                            <div class="progress">
 | 
			
		||||
                                <div style="width: {{$TransLeftPercent}}%" :class="'progress-bar progress-bar-' + toSSBar('{{TransClassName $TransLeftPercent}}')">
 | 
			
		||||
                                    <small style="display: inline-block;width: max-content;">{{TransLeft $stats.Max $transfer}} / {{$TransLeftPercent}} %</small>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                    {{end}}
 | 
			
		||||
                    {{end}}
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
        </div>
 | 
			
		||||
        {{end}}
 | 
			
		||||
    </div>
 | 
			
		||||
    </section>
 | 
			
		||||
    {{template "theme-server-status/content-footer" .}}
 | 
			
		||||
</div>
 | 
			
		||||
<script>
 | 
			
		||||
 | 
			
		||||
    // 初始化 Tooltip
 | 
			
		||||
    $(document).ready(function(){
 | 
			
		||||
        $('[data-toggle="tooltip"]').tooltip();
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
<script>
 | 
			
		||||
    new Vue({
 | 
			
		||||
        el: '#app',
 | 
			
		||||
        delimiters: ['@#', '#@'],
 | 
			
		||||
        data: {
 | 
			
		||||
            services: []
 | 
			
		||||
            page: 'service',
 | 
			
		||||
            defaultTemplate: {{.Conf.Site.Theme}},
 | 
			
		||||
            templates: {{.Themes}},
 | 
			
		||||
            servicesTag: [],
 | 
			
		||||
            servicesNoTag: [],
 | 
			
		||||
        },
 | 
			
		||||
        created() {
 | 
			
		||||
            this.initData()
 | 
			
		||||
            this.initData();
 | 
			
		||||
        },
 | 
			
		||||
        mounted() {
 | 
			
		||||
        },
 | 
			
		||||
@ -129,6 +108,7 @@
 | 
			
		||||
                const services = []
 | 
			
		||||
                {{range $service := .Services}}
 | 
			
		||||
                services.push({
 | 
			
		||||
                    type: '{{$service.Monitor.Type}}',
 | 
			
		||||
                    name: '{{$service.Monitor.Name}}',
 | 
			
		||||
                    currentUp: parseInt('{{$service.CurrentUp}}'),
 | 
			
		||||
                    currentDown: parseInt('{{$service.CurrentDown}}'),
 | 
			
		||||
@ -147,7 +127,8 @@
 | 
			
		||||
                    service.dayDetail = this.getDayTails(service)
 | 
			
		||||
                    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) {
 | 
			
		||||
                if (!up) {
 | 
			
		||||
 | 
			
		||||
@ -12,3 +12,13 @@ site:
 | 
			
		||||
  brand: "nz_site_title"
 | 
			
		||||
  cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改
 | 
			
		||||
  theme: "default"
 | 
			
		||||
ddns:
 | 
			
		||||
  enable: false
 | 
			
		||||
  provider: "webhook"
 | 
			
		||||
  accessid: ""
 | 
			
		||||
  accesssecret: ""
 | 
			
		||||
  webhookmethod: ""
 | 
			
		||||
  webhookurl: ""
 | 
			
		||||
  webhookrequestbody: ""
 | 
			
		||||
  webhookheaders: ""
 | 
			
		||||
  maxretries: 3
 | 
			
		||||
 | 
			
		||||
@ -38,8 +38,8 @@ $download = "https://github.com/$agentrepo/releases/download/$agenttag/$file"
 | 
			
		||||
$nssmdownload="https://github.com/$nssmrepo/releases/download/$nssmtag/nssm.zip"
 | 
			
		||||
Write-Host "Location:$region,connect directly!" -BackgroundColor DarkRed -ForegroundColor Green
 | 
			
		||||
}else{
 | 
			
		||||
$download = "https://kkgithub.com/$agentrepo/releases/download/$agenttag/$file"
 | 
			
		||||
$nssmdownload="https://kkgithub.com/$nssmrepo/releases/download/$nssmtag/nssm.zip"
 | 
			
		||||
$download = "https://dn-dao-github-mirror.daocloud.io/$agentrepo/releases/download/$agenttag/$file"
 | 
			
		||||
$nssmdownload="https://dn-dao-github-mirror.daocloud.io/$nssmrepo/releases/download/$nssmtag/nssm.zip"
 | 
			
		||||
Write-Host "Location:CN,use mirror address" -BackgroundColor DarkRed -ForegroundColor Green
 | 
			
		||||
}
 | 
			
		||||
echo $download
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
 | 
			
		||||
NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
 | 
			
		||||
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
 | 
			
		||||
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
 | 
			
		||||
NZ_VERSION="v0.15.6"
 | 
			
		||||
NZ_VERSION="v0.15.9"
 | 
			
		||||
 | 
			
		||||
red='\033[0;31m'
 | 
			
		||||
green='\033[0;32m'
 | 
			
		||||
@ -94,7 +94,7 @@ pre_check() {
 | 
			
		||||
            Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
 | 
			
		||||
        else
 | 
			
		||||
            GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
 | 
			
		||||
            GITHUB_URL="kkgithub.com"
 | 
			
		||||
            GITHUB_URL="dn-dao-github-mirror.daocloud.io"
 | 
			
		||||
            Get_Docker_URL="get.docker.com"
 | 
			
		||||
            Get_Docker_Argu=" -s docker --mirror Aliyun"
 | 
			
		||||
            Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
 | 
			
		||||
@ -159,7 +159,7 @@ install_arch() {
 | 
			
		||||
                                        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 ..;
 | 
			
		||||
                                        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"
 | 
			
		||||
        ;;
 | 
			
		||||
    [nN][oO] | [nN])
 | 
			
		||||
@ -269,7 +269,8 @@ install_dashboard_standalone() {
 | 
			
		||||
 | 
			
		||||
selinux() {
 | 
			
		||||
    #判断当前的状态
 | 
			
		||||
    if [ "$os_alpine" != 1 ]; then
 | 
			
		||||
    command -v getenforce >/dev/null 2>&1
 | 
			
		||||
    if [ $? -eq 0 ]; then
 | 
			
		||||
        getenforce | grep '[Ee]nfor'
 | 
			
		||||
        if [ $? -eq 0 ]; then
 | 
			
		||||
            echo -e "SELinux是开启状态,正在关闭!"
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
 | 
			
		||||
NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
 | 
			
		||||
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
 | 
			
		||||
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
 | 
			
		||||
NZ_VERSION="v0.15.6"
 | 
			
		||||
NZ_VERSION="v0.15.9"
 | 
			
		||||
 | 
			
		||||
red='\033[0;31m'
 | 
			
		||||
green='\033[0;32m'
 | 
			
		||||
@ -93,7 +93,7 @@ pre_check() {
 | 
			
		||||
            Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
 | 
			
		||||
        else
 | 
			
		||||
            GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
 | 
			
		||||
            GITHUB_URL="kkgithub.com"
 | 
			
		||||
            GITHUB_URL="dn-dao-github-mirror.daocloud.io"
 | 
			
		||||
            Get_Docker_URL="get.docker.com"
 | 
			
		||||
            Get_Docker_Argu=" -s docker --mirror Aliyun"
 | 
			
		||||
            Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
 | 
			
		||||
@ -157,7 +157,7 @@ install_arch() {
 | 
			
		||||
                                        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 ..;
 | 
			
		||||
                                        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"
 | 
			
		||||
        ;;
 | 
			
		||||
    [nN][oO] | [nN])
 | 
			
		||||
@ -266,7 +266,8 @@ install_dashboard_standalone() {
 | 
			
		||||
 | 
			
		||||
selinux() {
 | 
			
		||||
    #Check SELinux
 | 
			
		||||
    if [ "$os_alpine" != 1 ]; then
 | 
			
		||||
    command -v getenforce >/dev/null 2>&1
 | 
			
		||||
    if [ $? -eq 0 ]; then
 | 
			
		||||
        getenforce | grep '[Ee]nfor'
 | 
			
		||||
        if [ $? -eq 0 ]; then
 | 
			
		||||
            echo -e "SELinux running,closing now!"
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,9 @@ package rpc
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/ddns"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/utils"
 | 
			
		||||
	"log"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/jinzhu/copier"
 | 
			
		||||
@ -110,6 +113,36 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
 | 
			
		||||
	host := model.PB2Host(r)
 | 
			
		||||
	singleton.ServerLock.RLock()
 | 
			
		||||
	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 &&
 | 
			
		||||
		((singleton.Conf.Cover == model.ConfigCoverAll && !singleton.Conf.IgnoredIPNotificationServerIDs[clientID]) ||
 | 
			
		||||
			(singleton.Conf.Cover == model.ConfigCoverIgnoreAll && singleton.Conf.IgnoredIPNotificationServerIDs[clientID])) &&
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										42
									
								
								service/singleton/ddns.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								service/singleton/ddns.go
									
									
									
									
									
										Normal file
									
								
							@ -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))
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
package singleton
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
@ -46,6 +47,21 @@ func InitConfigFromPath(path string) {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		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 从给出的文件路径中加载数据库
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user