diff --git a/README.md b/README.md index 94e2521..ab195f8 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ -
- +
+

-    +   
-

:trollface: 哪吒监控 一站式轻监控轻运维系统。支持系统状态、HTTP(SSL 证书变更、即将到期、到期)、TCP、Ping 监控报警,命令批量执行和计划任务。

+
+

:trollface: 哪吒监控 一站式轻监控轻运维系统。支持系统状态、HTTP(SSL 证书变更、即将到期、到期)、TCP、Ping 监控报警,命令批量执行和计划任务。

-\>> 交流论坛:[打杂社区](https://daza.net/c/nezha) (Lemmy) - \>> QQ 交流群:872069346 **加群要求:已搭建好哪吒监控 & 有 2+ 服务器** \>> [我们的用户](https://www.google.com/search?q="powered+by+哪吒监控%7C哪吒面板"&filter=0) (Google) @@ -102,6 +101,9 @@ URL 里面也可放置占位符,请求时会进行简单的字符串替换。 - net_in_speed(入站网速)、net_out_speed(出站网速)、net_all_speed(双向网速)、transfer_in(入站流量)、transfer_out(出站流量)、transfer_all(双向流量):Min/Max 数值为字节(1kb=1024,1mb = 1024\*1024) - offline:不支持 Min/Max 参数 - Duration:持续秒数,监控比较简陋,取持续时间内的 70% 采样结果 +- Cover + - `0` 监控所有,通过 `Ignore` 忽略特定服务器 + - `1` 忽略所有,通过 `Ignore` 监控特定服务器 - Ignore: `{"1": true, "2":false}` 忽略此规则的服务器 ID 列表,比如忽略服务器 ID 5 的离线通知 `[{"Type":"offline","Duration":10, "Ignore":{"5": true}}]` diff --git a/cmd/agent/monitor/myip.go b/cmd/agent/monitor/myip.go index 4da05e5..cdf8962 100644 --- a/cmd/agent/monitor/myip.go +++ b/cmd/agent/monitor/myip.go @@ -1,18 +1,13 @@ package monitor import ( - "context" "encoding/json" - "errors" "fmt" "io/ioutil" - "net" "net/http" - "strings" - "sync" "time" - "github.com/miekg/dns" + "github.com/naiba/nezha/pkg/utils" ) type geoIP struct { @@ -24,14 +19,16 @@ var ( ipv4Servers = []string{ "https://api-ipv4.ip.sb/geoip", "https://ip4.seeip.org/geoip", + "https://ipapi.co/json", } ipv6Servers = []string{ "https://ip6.seeip.org/geoip", "https://api-ipv6.ip.sb/geoip", + "https://ipapi.co/json", } cachedIP, cachedCountry string - httpClientV4 = newHTTPClient(time.Second*20, time.Second*5, time.Second*10, false) - httpClientV6 = newHTTPClient(time.Second*20, time.Second*5, time.Second*10, true) + httpClientV4 = utils.NewSingleStackHTTPClient(time.Second*20, time.Second*5, time.Second*10, false) + httpClientV6 = utils.NewSingleStackHTTPClient(time.Second*20, time.Second*5, time.Second*10, true) ) func UpdateIP() { @@ -73,93 +70,3 @@ func fetchGeoIP(servers []string, isV6 bool) geoIP { } return ip } - -func newHTTPClient(httpTimeout, dialTimeout, keepAliveTimeout time.Duration, ipv6 bool) *http.Client { - dialer := &net.Dialer{ - Timeout: dialTimeout, - KeepAlive: keepAliveTimeout, - } - - transport := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - ForceAttemptHTTP2: false, - DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) { - ip, err := resolveIP(addr, ipv6) - if err != nil { - return nil, err - } - return dialer.DialContext(ctx, network, ip) - }, - } - - return &http.Client{ - Transport: transport, - Timeout: httpTimeout, - } -} - -func resolveIP(addr string, ipv6 bool) (string, error) { - url := strings.Split(addr, ":") - - m := new(dns.Msg) - if ipv6 { - m.SetQuestion(dns.Fqdn(url[0]), dns.TypeAAAA) - } else { - m.SetQuestion(dns.Fqdn(url[0]), dns.TypeA) - } - m.RecursionDesired = true - - dnsServers := []string{"2606:4700:4700::1001", "2001:4860:4860::8844"} - if !ipv6 { - dnsServers = []string{"1.0.0.1", "8.8.4.4"} - } - - var wg sync.WaitGroup - var resolveLock sync.RWMutex - var ipv4Resolved, ipv6Resolved bool - - wg.Add(len(dnsServers)) - for i := 0; i < len(dnsServers); i++ { - go func(i int) { - defer wg.Done() - c := new(dns.Client) - c.Timeout = time.Second * 3 - r, _, err := c.Exchange(m, net.JoinHostPort(dnsServers[i], "53")) - if err != nil { - return - } - resolveLock.Lock() - defer resolveLock.Unlock() - if ipv6 && ipv6Resolved { - return - } - if !ipv6 && ipv4Resolved { - return - } - for _, ans := range r.Answer { - if ipv6 { - if aaaa, ok := ans.(*dns.AAAA); ok { - url[0] = "[" + aaaa.AAAA.String() + "]" - ipv6Resolved = true - } - } else { - if a, ok := ans.(*dns.A); ok { - url[0] = a.A.String() - ipv4Resolved = true - } - } - } - }(i) - } - wg.Wait() - - if ipv6 && !ipv6Resolved { - return "", errors.New("the AAAA record not resolved") - } - - if !ipv6 && !ipv4Resolved { - return "", errors.New("the A record not resolved") - } - - return strings.Join(url, ":"), nil -} diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go index 09b5c50..c39ef07 100644 --- a/cmd/dashboard/controller/member_api.go +++ b/cmd/dashboard/controller/member_api.go @@ -15,7 +15,6 @@ import ( "github.com/naiba/nezha/model" "github.com/naiba/nezha/pkg/mygin" "github.com/naiba/nezha/pkg/utils" - pb "github.com/naiba/nezha/proto" "github.com/naiba/nezha/service/dao" ) @@ -196,6 +195,7 @@ type monitorForm struct { Name string Target string Type uint8 + Cover uint8 Notify string SkipServersRaw string } @@ -210,6 +210,7 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) { m.Type = mf.Type m.ID = mf.ID m.SkipServersRaw = mf.SkipServersRaw + m.Cover = mf.Cover m.Notify = mf.Notify == "on" } if err == nil { @@ -239,6 +240,7 @@ type cronForm struct { Scheduler string Command string ServersRaw string + Cover uint8 PushSuccessful string } @@ -253,6 +255,7 @@ func (ma *memberAPI) addOrEditCron(c *gin.Context) { cr.ServersRaw = cf.ServersRaw cr.PushSuccessful = cf.PushSuccessful == "on" cr.ID = cf.ID + cr.Cover = cf.Cover err = json.Unmarshal([]byte(cf.ServersRaw), &cr.Servers) } if err == nil { @@ -281,21 +284,7 @@ func (ma *memberAPI) addOrEditCron(c *gin.Context) { dao.Cron.Remove(crOld.CronID) } - cr.CronID, err = dao.Cron.AddFunc(cr.Scheduler, func() { - dao.ServerLock.RLock() - defer dao.ServerLock.RUnlock() - for j := 0; j < len(cr.Servers); j++ { - if dao.ServerList[cr.Servers[j]].TaskStream != nil { - dao.ServerList[cr.Servers[j]].TaskStream.Send(&pb.Task{ - Id: cr.ID, - Data: cr.Command, - Type: model.TaskTypeCommand, - }) - } else { - dao.SendNotification(fmt.Sprintf("计划任务:%s,服务器:%s 离线,无法执行。", cr.Name, dao.ServerList[cr.Servers[j]].Name), false) - } - } - }) + cr.CronID, err = dao.Cron.AddFunc(cr.Scheduler, dao.CronTrigger(cr)) if err != nil { panic(err) } @@ -318,7 +307,7 @@ func (ma *memberAPI) manualTrigger(c *gin.Context) { return } - dao.CronTrigger(&cr) + dao.ManualTrigger(&cr) c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 6680f56..ce3ae2a 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "time" "github.com/patrickmn/go-cache" @@ -12,7 +11,6 @@ import ( "github.com/naiba/nezha/cmd/dashboard/controller" "github.com/naiba/nezha/cmd/dashboard/rpc" "github.com/naiba/nezha/model" - pb "github.com/naiba/nezha/proto" "github.com/naiba/nezha/service/dao" ) @@ -84,21 +82,13 @@ func loadCrons() { var err error for i := 0; i < len(crons); i++ { cr := crons[i] - cr.CronID, err = dao.Cron.AddFunc(cr.Scheduler, func() { - dao.ServerLock.RLock() - defer dao.ServerLock.RUnlock() - for j := 0; j < len(cr.Servers); j++ { - if dao.ServerList[cr.Servers[j]].TaskStream != nil { - dao.ServerList[cr.Servers[j]].TaskStream.Send(&pb.Task{ - Id: cr.ID, - Data: cr.Command, - Type: model.TaskTypeCommand, - }) - } else { - dao.SendNotification(fmt.Sprintf("计划任务:%s,服务器:%s 离线,无法执行。", cr.Name, dao.ServerList[cr.Servers[j]].Name), false) - } - } - }) + + crIgnoreMap := make(map[uint64]bool) + for j := 0; j < len(cr.Servers); j++ { + crIgnoreMap[cr.Servers[j]] = true + } + + cr.CronID, err = dao.Cron.AddFunc(cr.Scheduler, dao.CronTrigger(cr)) if err != nil { panic(err) } diff --git a/cmd/dashboard/rpc/rpc.go b/cmd/dashboard/rpc/rpc.go index 19a3fb4..2a3bfd0 100644 --- a/cmd/dashboard/rpc/rpc.go +++ b/cmd/dashboard/rpc/rpc.go @@ -7,6 +7,7 @@ import ( "google.golang.org/grpc" + "github.com/naiba/nezha/model" pb "github.com/naiba/nezha/proto" "github.com/naiba/nezha/service/dao" rpcService "github.com/naiba/nezha/service/rpc" @@ -39,13 +40,21 @@ func DispatchTask(duration time.Duration) { } hasAliveAgent = false } - // 1. 如果此任务不可使用此服务器请求,跳过这个服务器(有些 IPv6 only 开了 NAT64 的机器请求 IPv4 总会出问题) - // 2. 如果服务器不在线,跳过这个服务器 - if tasks[i].SkipServers[dao.SortedServerList[index].ID] || dao.SortedServerList[index].TaskStream == nil { + + // 1. 如果服务器不在线,跳过这个服务器 + if dao.SortedServerList[index].TaskStream == nil { i-- index++ continue } + // 2. 如果此任务不可使用此服务器请求,跳过这个服务器(有些 IPv6 only 开了 NAT64 的机器请求 IPv4 总会出问题) + if (tasks[i].Cover == model.MonitorCoverAll && tasks[i].SkipServers[dao.SortedServerList[index].ID]) || + (tasks[i].Cover == model.MonitorCoverIgnoreAll && !tasks[i].SkipServers[dao.SortedServerList[index].ID]) { + i-- + index++ + continue + } + hasAliveAgent = true dao.SortedServerList[index].TaskStream.Send(tasks[i].PB()) index++ diff --git a/model/alertrule.go b/model/alertrule.go index 82bd686..61e0919 100644 --- a/model/alertrule.go +++ b/model/alertrule.go @@ -1,79 +1,11 @@ package model import ( - "bytes" "encoding/json" - "fmt" - "time" "gorm.io/gorm" ) -const ( - RuleCheckPass = 1 - RuleCheckFail = 0 -) - -type Rule struct { - // 指标类型,cpu、memory、swap、disk、net_in_speed、net_out_speed - // net_all_speed、transfer_in、transfer_out、transfer_all、offline - Type string `json:"type,omitempty"` - Min uint64 `json:"min,omitempty"` // 最小阈值 (百分比、字节 kb ÷ 1024) - Max uint64 `json:"max,omitempty"` // 最大阈值 (百分比、字节 kb ÷ 1024) - Duration uint64 `json:"duration,omitempty"` // 持续时间 (秒) - Ignore map[uint64]bool `json:"ignore,omitempty"` //忽略此规则的ID列表 -} - -func percentage(used, total uint64) uint64 { - if total == 0 { - return 0 - } - return used * 100 / total -} - -// Snapshot 未通过规则返回 struct{}{}, 通过返回 nil -func (u *Rule) Snapshot(server *Server) interface{} { - if u.Ignore[server.ID] { - return nil - } - var src uint64 - switch u.Type { - case "cpu": - src = uint64(server.State.CPU) - case "memory": - src = percentage(server.State.MemUsed, server.Host.MemTotal) - case "swap": - src = percentage(server.State.SwapUsed, server.Host.SwapTotal) - case "disk": - src = percentage(server.State.DiskUsed, server.Host.DiskTotal) - case "net_in_speed": - src = server.State.NetInSpeed - case "net_out_speed": - src = server.State.NetOutSpeed - case "net_all_speed": - src = server.State.NetOutSpeed + server.State.NetOutSpeed - case "transfer_in": - src = server.State.NetInTransfer - case "transfer_out": - src = server.State.NetOutTransfer - case "transfer_all": - src = server.State.NetOutTransfer + server.State.NetInTransfer - case "offline": - if server.LastActive.IsZero() { - src = 0 - } else { - src = uint64(server.LastActive.Unix()) - } - } - - if u.Type == "offline" && uint64(time.Now().Unix())-src > 6 { - return struct{}{} - } else if (u.Max > 0 && src > u.Max) || (u.Min > 0 && src < u.Min) { - return struct{}{} - } - return nil -} - type AlertRule struct { Common Name string @@ -103,8 +35,7 @@ func (r *AlertRule) Snapshot(server *Server) []interface{} { return point } -func (r *AlertRule) Check(points [][]interface{}) (int, string) { - var dist bytes.Buffer +func (r *AlertRule) Check(points [][]interface{}) (int, bool) { var max int var count int for i := 0; i < len(r.Rules); i++ { @@ -125,11 +56,11 @@ func (r *AlertRule) Check(points [][]interface{}) (int, string) { } if fail/total > 0.7 { count++ - dist.WriteString(fmt.Sprintf("%+v\n", r.Rules[i])) + break } } if count == len(r.Rules) { - return max, dist.String() + return max, false } - return max, "" + return max, true } diff --git a/model/cron.go b/model/cron.go index d8ef23f..c03b7f0 100644 --- a/model/cron.go +++ b/model/cron.go @@ -8,6 +8,11 @@ import ( "gorm.io/gorm" ) +const ( + CronCoverIgnoreAll = iota + CronCoverAll +) + type Cron struct { Common Name string @@ -17,6 +22,7 @@ type Cron struct { PushSuccessful bool // 推送成功的通知 LastExecutedAt time.Time // 最后一次执行时间 LastResult bool // 最后一次执行结果 + Cover uint8 CronID cron.EntryID `gorn:"-"` ServersRaw string diff --git a/model/monitor.go b/model/monitor.go index 49ceb3d..4003332 100644 --- a/model/monitor.go +++ b/model/monitor.go @@ -15,6 +15,11 @@ const ( TaskTypeCommand ) +const ( + MonitorCoverAll = iota + MonitorCoverIgnoreAll +) + type Monitor struct { Common Name string @@ -22,8 +27,8 @@ type Monitor struct { Target string SkipServersRaw string Notify bool - - SkipServers map[uint64]bool `gorm:"-" json:"-"` + Cover uint8 + SkipServers map[uint64]bool `gorm:"-" json:"-"` } func (m *Monitor) PB() *pb.Task { diff --git a/model/rule.go b/model/rule.go new file mode 100644 index 0000000..9731fae --- /dev/null +++ b/model/rule.go @@ -0,0 +1,77 @@ +package model + +import "time" + +const ( + RuleCoverAll = iota + RuleCoverIgnoreAll +) + +type Rule struct { + // 指标类型,cpu、memory、swap、disk、net_in_speed、net_out_speed + // net_all_speed、transfer_in、transfer_out、transfer_all、offline + Type string `json:"type,omitempty"` + Min uint64 `json:"min,omitempty"` // 最小阈值 (百分比、字节 kb ÷ 1024) + Max uint64 `json:"max,omitempty"` // 最大阈值 (百分比、字节 kb ÷ 1024) + Duration uint64 `json:"duration,omitempty"` // 持续时间 (秒) + Cover uint64 `json:"cover,omitempty"` // 覆盖范围 RuleCoverAll/IgnoreAll + Ignore map[uint64]bool `json:"ignore,omitempty"` // 覆盖范围的排除 +} + +func percentage(used, total uint64) uint64 { + if total == 0 { + return 0 + } + return used * 100 / total +} + +// Snapshot 未通过规则返回 struct{}{}, 通过返回 nil +func (u *Rule) Snapshot(server *Server) interface{} { + // 监控全部但是排除了此服务器 + if u.Cover == RuleCoverAll && u.Ignore[server.ID] { + return nil + } + // 忽略全部但是指定监控了此服务器 + if u.Cover == RuleCoverIgnoreAll && !u.Ignore[server.ID] { + return nil + } + + var src uint64 + + switch u.Type { + case "cpu": + src = uint64(server.State.CPU) + case "memory": + src = percentage(server.State.MemUsed, server.Host.MemTotal) + case "swap": + src = percentage(server.State.SwapUsed, server.Host.SwapTotal) + case "disk": + src = percentage(server.State.DiskUsed, server.Host.DiskTotal) + case "net_in_speed": + src = server.State.NetInSpeed + case "net_out_speed": + src = server.State.NetOutSpeed + case "net_all_speed": + src = server.State.NetOutSpeed + server.State.NetOutSpeed + case "transfer_in": + src = server.State.NetInTransfer + case "transfer_out": + src = server.State.NetOutTransfer + case "transfer_all": + src = server.State.NetOutTransfer + server.State.NetInTransfer + case "offline": + if server.LastActive.IsZero() { + src = 0 + } else { + src = uint64(server.LastActive.Unix()) + } + } + + if u.Type == "offline" && uint64(time.Now().Unix())-src > 6 { + return struct{}{} + } else if (u.Max > 0 && src > u.Max) || (u.Min > 0 && src < u.Min) { + return struct{}{} + } + + return nil +} diff --git a/pkg/utils/http.go b/pkg/utils/http.go new file mode 100644 index 0000000..3ae3a4c --- /dev/null +++ b/pkg/utils/http.go @@ -0,0 +1,106 @@ +package utils + +import ( + "context" + "errors" + "net" + "net/http" + "strings" + "sync" + "time" + + "github.com/miekg/dns" +) + +func NewSingleStackHTTPClient(httpTimeout, dialTimeout, keepAliveTimeout time.Duration, ipv6 bool) *http.Client { + dialer := &net.Dialer{ + Timeout: dialTimeout, + KeepAlive: keepAliveTimeout, + } + + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + ForceAttemptHTTP2: false, + DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) { + ip, err := resolveIP(addr, ipv6) + if err != nil { + return nil, err + } + return dialer.DialContext(ctx, network, ip) + }, + } + + return &http.Client{ + Transport: transport, + Timeout: httpTimeout, + } +} + +func resolveIP(addr string, ipv6 bool) (string, error) { + url := strings.Split(addr, ":") + + m := new(dns.Msg) + if ipv6 { + m.SetQuestion(dns.Fqdn(url[0]), dns.TypeAAAA) + } else { + m.SetQuestion(dns.Fqdn(url[0]), dns.TypeA) + } + m.RecursionDesired = true + + dnsServers := []string{"2606:4700:4700::1001", "2001:4860:4860::8844", "2400:3200::1", "2400:3200:baba::1"} + if !ipv6 { + dnsServers = []string{"1.0.0.1", "8.8.4.4", "223.5.5.5", "223.6.6.6"} + } + + var wg sync.WaitGroup + var resolveLock sync.RWMutex + var ipv4Resolved, ipv6Resolved bool + + wg.Add(len(dnsServers) + 1) + go func() { + + }() + for i := 0; i < len(dnsServers); i++ { + go func(i int) { + defer wg.Done() + c := new(dns.Client) + c.Timeout = time.Second * 3 + r, _, err := c.Exchange(m, net.JoinHostPort(dnsServers[i], "53")) + if err != nil { + return + } + resolveLock.Lock() + defer resolveLock.Unlock() + if ipv6 && ipv6Resolved { + return + } + if !ipv6 && ipv4Resolved { + return + } + for _, ans := range r.Answer { + if ipv6 { + if aaaa, ok := ans.(*dns.AAAA); ok { + url[0] = "[" + aaaa.AAAA.String() + "]" + ipv6Resolved = true + } + } else { + if a, ok := ans.(*dns.A); ok { + url[0] = a.A.String() + ipv4Resolved = true + } + } + } + }(i) + } + wg.Wait() + + if ipv6 && !ipv6Resolved { + return "", errors.New("the AAAA record not resolved") + } + + if !ipv6 && !ipv4Resolved { + return "", errors.New("the A record not resolved") + } + + return strings.Join(url, ":"), nil +} diff --git a/resource/static/brand.png b/resource/static/brand.png index 5eaea20..7e760ea 100644 Binary files a/resource/static/brand.png and b/resource/static/brand.png differ diff --git a/resource/static/main.js b/resource/static/main.js index 30343fd..356142c 100644 --- a/resource/static/main.js +++ b/resource/static/main.js @@ -41,61 +41,53 @@ function showFormModal(modelSelector, formID, URL, getData) { const data = getData ? getData() : $(formID) - .serializeArray() - .reduce(function (obj, item) { - // ID 类的数据 - if ( - item.name.endsWith("_id") || - item.name === "id" || - item.name === "ID" || - item.name === "RequestType" || - item.name === "RequestMethod" || - item.name === "DisplayIndex" || - item.name === "Type" - ) { - obj[item.name] = parseInt(item.value); - } else { - obj[item.name] = item.value; - } + .serializeArray() + .reduce(function (obj, item) { + // ID 类的数据 + if ( + item.name.endsWith("_id") || + item.name === "id" || + item.name === "ID" || + item.name === "RequestType" || + item.name === "RequestMethod" || + item.name === "DisplayIndex" || + item.name === "Type" || + item.name === "Cover" + ) { + obj[item.name] = parseInt(item.value); + } else { + obj[item.name] = item.value; + } - if (item.name.endsWith("ServersRaw")) { - if (item.value.length > 2) { - obj[item.name] = JSON.stringify( - [...item.value.matchAll(/\d+/gm)].map((k) => - parseInt(k[0]) - ) - ); - } + if (item.name.endsWith("ServersRaw")) { + if (item.value.length > 2) { + obj[item.name] = JSON.stringify( + [...item.value.matchAll(/\d+/gm)].map((k) => + parseInt(k[0]) + ) + ); } + } - return obj; - }, {}); + return obj; + }, {}); $.post(URL, JSON.stringify(data)) .done(function (resp) { if (resp.code == 200) { - if (resp.message) { - $.suiAlert({ - title: "操作成功", - type: "success", - description: resp.message, - time: "3", - position: "top-center", - }); - } - window.location.reload(); + window.location.reload() } else { form.append( `
操作失败

` + - resp.message + - `

` + resp.message + + `

` ); } }) .fail(function (err) { form.append( `
网络错误

` + - err.responseText + - `

` + err.responseText + + `

` ); }) .always(function () { @@ -197,6 +189,7 @@ function addOrEditMonitor(monitor) { modal.find("input[name=Name]").val(monitor ? monitor.Name : null); modal.find("input[name=Target]").val(monitor ? monitor.Target : null); modal.find("select[name=Type]").val(monitor ? monitor.Type : 1); + modal.find("select[name=Cover]").val(monitor ? monitor.Cover : 0); if (monitor && monitor.Notify) { modal.find(".ui.nb-notify.checkbox").checkbox("set checked"); } else { @@ -210,10 +203,10 @@ function addOrEditMonitor(monitor) { for (let i = 0; i < serverList.length; i++) { node.after( 'ID:' + - serverList[i] + - '' + serverList[i] + + '" style="display: inline-block !important;">ID:' + + serverList[i] + + '' ); } } @@ -245,10 +238,10 @@ function addOrEditCron(cron) { for (let i = 0; i < serverList.length; i++) { node.after( 'ID:' + - serverList[i] + - '' + serverList[i] + + '" style="display: inline-block !important;">ID:' + + serverList[i] + + '' ); } } @@ -367,5 +360,5 @@ $(document).ready(() => { cache: false, }, }); - } catch (error) {} + } catch (error) { } }); diff --git a/resource/template/component/cron.html b/resource/template/component/cron.html index 527d93d..a0fe16a 100644 --- a/resource/template/component/cron.html +++ b/resource/template/component/cron.html @@ -17,7 +17,14 @@
- + + +
+
+ diff --git a/resource/template/component/monitor.html b/resource/template/component/monitor.html index 58661e5..b2ace1c 100644 --- a/resource/template/component/monitor.html +++ b/resource/template/component/monitor.html @@ -25,7 +25,14 @@
- + + +
+
+