From 4b0c0ad288617398fcbabbdefed09de7d3d5e74f Mon Sep 17 00:00:00 2001 From: naiba Date: Mon, 21 Jun 2021 21:30:42 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E4=BC=98=E5=8C=96=E5=BF=BD?= =?UTF-8?q?=E7=95=A5=E8=A7=84=E5=88=99=E9=85=8D=E7=BD=AE=E5=92=8C=20Agent?= =?UTF-8?q?=20=E8=8E=B7=E5=8F=96=20IP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +-- cmd/agent/monitor/myip.go | 103 ++-------------------- cmd/dashboard/controller/member_api.go | 23 ++--- cmd/dashboard/main.go | 24 ++--- cmd/dashboard/rpc/rpc.go | 15 +++- model/alertrule.go | 77 +--------------- model/cron.go | 6 ++ model/monitor.go | 9 +- model/rule.go | 77 ++++++++++++++++ pkg/utils/http.go | 106 +++++++++++++++++++++++ resource/static/brand.png | Bin 16017 -> 9805 bytes resource/static/main.js | 91 +++++++++---------- resource/template/component/cron.html | 12 ++- resource/template/component/monitor.html | 9 +- resource/template/dashboard/cron.html | 4 +- resource/template/dashboard/monitor.html | 22 ++--- resource/template/dashboard/server.html | 2 + service/dao/alertsentinel.go | 31 +++++-- service/dao/dao.go | 32 ++++++- service/dao/notification.go | 4 + 20 files changed, 370 insertions(+), 291 deletions(-) create mode 100644 model/rule.go create mode 100644 pkg/utils/http.go 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 5eaea208440db959410e25340836e6d57342a50c..7e760ea07953488b83c50dc2670b045dce02ba88 100644 GIT binary patch literal 9805 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 literal 16017 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 z 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 UN=|);NO2*GvT8DAQs%+`2NNy{AOHXW 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 @@
- + + +
+
+