diff --git a/README.md b/README.md
index 8711dc1..618ec9e 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,8 @@
sudo ./nezha.sh
```
-## 主题自定义
+## 使用说明
+### 自定义 CSS
- 默认主题更改进度条颜色示例
@@ -42,6 +43,10 @@
}
```
+### 通知
+
+
+
## 常见问题
### 数据备份恢复
diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go
index b5d5a3b..c5d38ed 100644
--- a/cmd/dashboard/controller/member_api.go
+++ b/cmd/dashboard/controller/member_api.go
@@ -1,6 +1,7 @@
package controller
import (
+ "encoding/json"
"fmt"
"net/http"
"strconv"
@@ -30,8 +31,10 @@ func (ma *memberAPI) serve() {
mr.POST("/logout", ma.logout)
mr.POST("/server", ma.addOrEditServer)
+ mr.POST("/notification", ma.addOrEditNotification)
mr.POST("/setting", ma.updateSetting)
mr.DELETE("/server/:id", ma.delete)
+ mr.DELETE("/notification/:id", ma.deleteNotification)
}
func (ma *memberAPI) delete(c *gin.Context) {
@@ -58,6 +61,27 @@ func (ma *memberAPI) delete(c *gin.Context) {
})
}
+func (ma *memberAPI) deleteNotification(c *gin.Context) {
+ id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
+ if id < 1 {
+ c.JSON(http.StatusOK, model.Response{
+ Code: http.StatusBadRequest,
+ Message: "错误的 Notification ID",
+ })
+ return
+ }
+ if err := dao.DB.Delete(&model.Notification{}, "id = ?", id).Error; err != nil {
+ c.JSON(http.StatusOK, model.Response{
+ Code: http.StatusBadRequest,
+ Message: fmt.Sprintf("数据库错误:%s", err),
+ })
+ return
+ }
+ c.JSON(http.StatusOK, model.Response{
+ Code: http.StatusOK,
+ })
+}
+
type serverForm struct {
ID uint64
Name string `binding:"required"`
@@ -96,6 +120,51 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
})
}
+type notificationForm struct {
+ ID uint64
+ Name string
+ URL string
+ RequestMethod int
+ RequestType int
+ RequestBody string
+ VerifySSL string
+}
+
+func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
+ var nf notificationForm
+ var n model.Notification
+ err := c.ShouldBindJSON(&nf)
+ if err == nil {
+ var data map[string]string
+ err = json.Unmarshal([]byte(nf.RequestBody), &data)
+ }
+ if err == nil {
+ n.Name = nf.Name
+ n.RequestMethod = nf.RequestMethod
+ n.RequestType = nf.RequestType
+ n.RequestBody = nf.RequestBody
+ n.URL = nf.URL
+ verifySSL := nf.VerifySSL == "on"
+ n.VerifySSL = &verifySSL
+ n.ID = nf.ID
+ if n.ID == 0 {
+ err = dao.DB.Create(&n).Error
+ } else {
+ err = dao.DB.Save(&n).Error
+ }
+ }
+ if err != nil {
+ c.JSON(http.StatusOK, model.Response{
+ Code: http.StatusBadRequest,
+ Message: fmt.Sprintf("请求错误:%s", err),
+ })
+ return
+ }
+ c.JSON(http.StatusOK, model.Response{
+ Code: http.StatusOK,
+ })
+}
+
type logoutForm struct {
ID uint64
}
diff --git a/cmd/dashboard/controller/member_page.go b/cmd/dashboard/controller/member_page.go
index 07dfaba..5dcadd1 100644
--- a/cmd/dashboard/controller/member_page.go
+++ b/cmd/dashboard/controller/member_page.go
@@ -4,6 +4,7 @@ import (
"net/http"
"github.com/gin-gonic/gin"
+ "github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/service/dao"
)
@@ -22,6 +23,7 @@ func (mp *memberPage) serve() {
Redirect: "/login",
}))
mr.GET("/server", mp.server)
+ mr.GET("/notification", mp.notification)
mr.GET("/setting", mp.setting)
}
@@ -34,6 +36,15 @@ func (mp *memberPage) server(c *gin.Context) {
}))
}
+func (mp *memberPage) notification(c *gin.Context) {
+ var nf []model.Notification
+ dao.DB.Find(&nf)
+ c.HTML(http.StatusOK, "dashboard/notification", mygin.CommonEnvironment(c, gin.H{
+ "Title": "通知管理",
+ "Notifications": nf,
+ }))
+}
+
func (mp *memberPage) setting(c *gin.Context) {
c.HTML(http.StatusOK, "dashboard/setting", mygin.CommonEnvironment(c, gin.H{
"Title": "系统设置",
diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go
index 5f3e22d..a615f10 100644
--- a/cmd/dashboard/main.go
+++ b/cmd/dashboard/main.go
@@ -34,7 +34,7 @@ func init() {
}
func initDB() {
- dao.DB.AutoMigrate(model.Server{}, model.User{})
+ dao.DB.AutoMigrate(model.Server{}, model.User{}, model.Notification{})
// load cache
var servers []model.Server
dao.DB.Find(&servers)
diff --git a/model/notification.go b/model/notification.go
new file mode 100644
index 0000000..2f12fcc
--- /dev/null
+++ b/model/notification.go
@@ -0,0 +1,100 @@
+package model
+
+import (
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+)
+
+const (
+ _ = iota
+ NotificationRequestTypeJSON
+ NotificationRequestTypeForm
+)
+
+const (
+ _ = iota
+ NotificationRequestMethodGET
+ NotificationRequestMethodPOST
+)
+
+type NotificatonSender struct {
+ Rule *Rule
+ Server *Server
+ State *State
+}
+
+type Notification struct {
+ Common
+ Name string
+ URL string
+ RequestMethod int
+ RequestType int
+ RequestBody string `gorm:"type:longtext" `
+ VerifySSL *bool
+}
+
+func (n *Notification) Send(sender *NotificatonSender, message string) {
+ var verifySSL bool
+
+ if n.VerifySSL != nil && *n.VerifySSL {
+ verifySSL = true
+ }
+
+ var err error
+ transCfg := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: verifySSL},
+ }
+ client := &http.Client{Transport: transCfg, Timeout: time.Minute * 10}
+ var reqURL *url.URL
+ reqURL, err = url.Parse(n.URL)
+ var data map[string]string
+ if err == nil && (n.RequestMethod == NotificationRequestMethodGET || n.RequestType == NotificationRequestTypeForm) {
+ err = json.Unmarshal([]byte(n.RequestBody), &data)
+ }
+
+ if err == nil {
+ if n.RequestMethod == NotificationRequestMethodGET {
+ for k, v := range data {
+ reqURL.Query().Set(k, replaceParamsInString(v, sender))
+ }
+ client.Get(reqURL.String())
+ } else {
+ if n.RequestType == NotificationRequestTypeForm {
+ params := url.Values{}
+ for k, v := range data {
+ params.Add(k, replaceParamsInString(v, sender))
+ }
+ client.PostForm(reqURL.String(), params)
+ } else {
+ jsonValue := replaceParamsInJSON(n.RequestBody, sender)
+ if err == nil {
+ client.Post(reqURL.String(), "application/json", strings.NewReader(jsonValue))
+ }
+ }
+ }
+ }
+}
+
+func replaceParamsInString(str string, sender *NotificatonSender) string {
+ str = strings.ReplaceAll(str, "#CPU#", fmt.Sprintf("%2f%%", sender.State.CPU))
+ return str
+}
+
+func replaceParamsInJSON(str string, sender *NotificatonSender) string {
+ str = strings.ReplaceAll(str, "#CPU#", fmt.Sprintf("%2f%%", sender.State.CPU))
+ return str
+}
+
+func jsonEscape(raw interface{}) string {
+ b, _ := json.Marshal(raw)
+ strb := string(b)
+ if strings.HasPrefix(strb, "\"") {
+ return strb[1 : len(strb)-1]
+ }
+ return strb
+}
diff --git a/model/rule.go b/model/rule.go
new file mode 100644
index 0000000..78b9979
--- /dev/null
+++ b/model/rule.go
@@ -0,0 +1,10 @@
+package model
+
+type Rule struct {
+ Common
+ Name string
+ Type string // 指标类型,cpu、memory、swap、disk、net_in、net_out、net_all、transfer_in、transfer_out、transfer_all、offline
+ Min uint64 // 最小阈值
+ Max uint64 // 最大阈值
+ Duration uint64 // 持续时间
+}
diff --git a/model/server.go b/model/server.go
index 643d1ea..284e39c 100644
--- a/model/server.go
+++ b/model/server.go
@@ -2,6 +2,7 @@ package model
import (
"fmt"
+ "html/template"
"time"
pb "github.com/naiba/nezha/proto"
@@ -21,6 +22,6 @@ type Server struct {
StreamClose chan<- error `gorm:"-" json:"-"`
}
-func (s Server) Marshal() string {
- return fmt.Sprintf(`{"ID":%d,"Name":"%s","Secret":"%s"}`, s.ID, s.Name, s.Secret)
+func (s Server) Marshal() template.JS {
+ return template.JS(fmt.Sprintf(`{"ID":%d,"Name":"%s","Secret":"%s"}`, s.ID, s.Name, s.Secret))
}
diff --git a/resource/static/main.js b/resource/static/main.js
index 1353053..26cd723 100644
--- a/resource/static/main.js
+++ b/resource/static/main.js
@@ -42,7 +42,7 @@ function showFormModal(modelSelector, formID, URL, getData) {
form.children('.message').remove()
btn.toggleClass('loading')
const data = getData ? getData() : $(formID).serializeArray().reduce(function (obj, item) {
- obj[item.name] = (item.name.endsWith('_id') || item.name === 'id') ? parseInt(item.value) : item.value;
+ obj[item.name] = (item.name.endsWith('_id') || item.name === 'id' || item.name === 'ID' || item.name === 'RequestType' || item.name === 'RequestMethod') ? parseInt(item.value) : item.value;
return obj;
}, {});
$.post(URL, JSON.stringify(data)).done(function (resp) {
@@ -70,11 +70,26 @@ function showFormModal(modelSelector, formID, URL, getData) {
}).modal('show')
}
+function addOrEditNotification(notification) {
+ const modal = $('.notification.modal')
+ modal.children('.header').text((notification ? '修改' : '添加') + '通知方式')
+ modal.find('.positive.button').html(notification ? '修改 ' : '添加 ')
+ modal.find('input[name=ID]').val(notification ? notification.ID : null)
+ modal.find('input[name=Name]').val(notification ? notification.Name : null)
+ modal.find('input[name=URL]').val(notification ? notification.URL : null)
+ modal.find('textarea[name=RequestBody]').val(notification ? notification.RequestBody : null)
+ modal.find('select[name=RequestMethod]').val(notification ? notification.RequestMethod : 1)
+ modal.find('select[name=RequestType]').val(notification ? notification.RequestType : 1)
+ if (notification && notification.VerifySSL) {
+ modal.find('.ui.checkbox').checkbox('set checked')
+ } else {
+ modal.find('.ui.checkbox').checkbox('set unchecked')
+ }
+ showFormModal('.notification.modal', '#notificationForm', '/api/notification')
+}
+
function addOrEditServer(server) {
const modal = $('.server.modal')
- if (server) {
- server = JSON.parse(server)
- }
modal.children('.header').text((server ? '修改' : '添加') + '服务器')
modal.find('.positive.button').html(server ? '修改 ' : '添加 ')
modal.find('input[name=id]').val(server ? server.ID : null)
diff --git a/resource/static/theme-hotaru/css/darkmode.css b/resource/static/theme-hotaru/css/darkmode.css
index 7c01654..83a518a 100644
--- a/resource/static/theme-hotaru/css/darkmode.css
+++ b/resource/static/theme-hotaru/css/darkmode.css
@@ -1,21 +1,3 @@
-/* Dark mode */
-
-#darkmodeButton {
- background-color: black;
- width: 3rem;
- height: 3rem;
- position: fixed;
- bottom: 2rem;
- right: 0.5rem;
- z-index: 9999;
- border-radius: 50%;
- outline: 0;
-}
-
-body.dark #darkmodeButton {
- background-color: white;
-}
-
body.dark {
background: #263236;
color: #aaa;
@@ -38,7 +20,8 @@ body.dark .panel {
color: #aaa;
}
-body.dark .panel h3, body.dark .panel span {
+body.dark .panel h3,
+body.dark .panel span {
color: #aaa;
}
diff --git a/resource/template/common/menu.html b/resource/template/common/menu.html
index eeed1bb..723b8d8 100644
--- a/resource/template/common/menu.html
+++ b/resource/template/common/menu.html
@@ -4,10 +4,11 @@
- 首页
+ 首页
{{if .Admin}}
- 服务器
- 设置
+ 服务器
+ 通知
+ 设置
{{end}}
diff --git a/resource/template/dashboard/notification.html b/resource/template/dashboard/notification.html
new file mode 100644
index 0000000..8b3fdbc
--- /dev/null
+++ b/resource/template/dashboard/notification.html
@@ -0,0 +1,88 @@
+{{define "dashboard/notification"}}
+{{template "common/header" .}}
+{{template "common/menu" .}}
+
+
+
+
+
+
+ ID
+ 备注
+ URL
+ 管理
+
+
+
+ {{range $notification := .Notifications}}
+
+ {{$notification.ID}}
+ {{$notification.Name}}
+ {{$notification.URL}}
+
+
+
+
+
+
+
+
+
+
+
+ {{end}}
+
+
+
+
+
+
+ ID
+ 备注
+ URL
+ 管理
+
+
+
+ {{range $notification := .Notifications}}
+
+ {{$notification.ID}}
+ {{$notification.Name}}
+ {{$notification.URL}}
+
+
+
+
+
+
+
+
+
+
+
+ {{end}}
+
+
+
+
+
+{{template "component/notification"}}
+{{template "common/footer" .}}
+
+{{end}}
\ No newline at end of file
diff --git a/resource/template/dashboard/setting.html b/resource/template/dashboard/setting.html
index 5ac37c1..add9657 100644
--- a/resource/template/dashboard/setting.html
+++ b/resource/template/dashboard/setting.html
@@ -20,7 +20,7 @@
自定义CSS
-
+
保存
diff --git a/resource/template/theme-hotaru/home.html b/resource/template/theme-hotaru/home.html
index 26663c5..55cf093 100644
--- a/resource/template/theme-hotaru/home.html
+++ b/resource/template/theme-hotaru/home.html
@@ -134,8 +134,6 @@
-
-
Powered by 哪吒面板 build ·
{{.Version}}
@@ -202,30 +200,6 @@
if (hour > 17 || hour < 4) {
document.body.classList.add('dark');
}
- return
- //darkmode
- if (document.getElementById("darkmodeButton")) {
- var night = parseInt(document.cookie.replace(/(?:(?:^|.*;\s*)dark\s*=\s*([^;]*).*$)|^.*$/, "$1") || '0');
- if (night) {
- document.body.classList.add('dark');
- console.log('Dark mode on', night);
- }
- document.getElementById("darkmodeButton").onclick = function () {
- night = parseInt(document.cookie.replace(/(?:(?:^|.*;\s*)dark\s*=\s*([^;]*).*$)|^.*$/, "$1") || '0');
- if (!night) {
- document.body.classList.add('dark');
- document.cookie = "dark=1";
- console.log('Dark mode on', night);
- } else {
- document.body.classList.remove('dark');
- document.cookie = "dark=0";
- console.log('Dark mode off', night);
- }
- }
- } else {
- document.cookie && (document.cookie = "");
- console.log('Darkmode not Support');
- }
},
secondToDate(s) {
var d = Math.floor(s / 3600 / 24);