diff --git a/README.md b/README.md
index 6c30292..4a6bb4d 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
\>> QQ 交流群: ~~955957790~~ 已解散,**自 2021 年 3 月 26 日起不再提供技术支持,接受 PR。**
-\>> 交流论坛:正在选型搭建中…… 欢迎想建设社区的铁子与奶爸取得联系。
+\>> 交流论坛:~~正在选型搭建中……~~ NodeBB 是理想选择,目前没精力维护。
\>> [我们的用户](https://www.google.com/search?q="powered+by+哪吒监控%7C哪吒面板"&filter=0) (Google)
diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go
index 4fd1cb1..5bf15fc 100644
--- a/cmd/dashboard/controller/member_api.go
+++ b/cmd/dashboard/controller/member_api.go
@@ -191,11 +191,12 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
}
type monitorForm struct {
- ID uint64
- Name string
- Target string
- Type uint8
- Notify string
+ ID uint64
+ Name string
+ Target string
+ Type uint8
+ Notify string
+ SkipServersRaw string
}
func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
@@ -207,6 +208,7 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
m.Target = mf.Target
m.Type = mf.Type
m.ID = mf.ID
+ m.SkipServersRaw = mf.SkipServersRaw
m.Notify = mf.Notify == "on"
}
if err == nil {
diff --git a/cmd/dashboard/rpc/rpc.go b/cmd/dashboard/rpc/rpc.go
index c95ff38..19a3fb4 100644
--- a/cmd/dashboard/rpc/rpc.go
+++ b/cmd/dashboard/rpc/rpc.go
@@ -39,7 +39,9 @@ func DispatchTask(duration time.Duration) {
}
hasAliveAgent = false
}
- if dao.SortedServerList[index].TaskStream == nil {
+ // 1. 如果此任务不可使用此服务器请求,跳过这个服务器(有些 IPv6 only 开了 NAT64 的机器请求 IPv4 总会出问题)
+ // 2. 如果服务器不在线,跳过这个服务器
+ if tasks[i].SkipServers[dao.SortedServerList[index].ID] || dao.SortedServerList[index].TaskStream == nil {
i--
index++
continue
diff --git a/model/monitor.go b/model/monitor.go
index 819d155..49ceb3d 100644
--- a/model/monitor.go
+++ b/model/monitor.go
@@ -1,7 +1,10 @@
package model
import (
+ "encoding/json"
+
pb "github.com/naiba/nezha/proto"
+ "gorm.io/gorm"
)
const (
@@ -14,10 +17,13 @@ const (
type Monitor struct {
Common
- Name string
- Type uint8
- Target string
- Notify bool
+ Name string
+ Type uint8
+ Target string
+ SkipServersRaw string
+ Notify bool
+
+ SkipServers map[uint64]bool `gorm:"-" json:"-"`
}
func (m *Monitor) PB() *pb.Task {
@@ -27,3 +33,15 @@ func (m *Monitor) PB() *pb.Task {
Data: m.Target,
}
}
+
+func (m *Monitor) AfterFind(tx *gorm.DB) error {
+ var skipServers []uint64
+ if err := json.Unmarshal([]byte(m.SkipServersRaw), &skipServers); err != nil {
+ return err
+ }
+ m.SkipServers = make(map[uint64]bool)
+ for i := 0; i < len(skipServers); i++ {
+ m.SkipServers[skipServers[i]] = true
+ }
+ return nil
+}
diff --git a/resource/static/main.js b/resource/static/main.js
index 47ce0f1..f36bb93 100644
--- a/resource/static/main.js
+++ b/resource/static/main.js
@@ -1,278 +1,368 @@
function readableBytes(bytes) {
- var i = Math.floor(Math.log(bytes) / Math.log(1024)),
- sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
- return (bytes / Math.pow(1024, i)).toFixed(0) + ' ' + sizes[i];
+ var i = Math.floor(Math.log(bytes) / Math.log(1024)),
+ sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
+ return (bytes / Math.pow(1024, i)).toFixed(0) + " " + sizes[i];
}
-const confirmBtn = $('.mini.confirm.modal .positive.button')
+const confirmBtn = $(".mini.confirm.modal .positive.button");
function showConfirm(title, content, callFn, extData) {
- const modal = $('.mini.confirm.modal')
- modal.children('.header').text(title)
- modal.children('.content').text(content)
- if (confirmBtn.hasClass('loading')) {
- return false
- }
- modal.modal({
- closable: true,
- onApprove: function () {
- confirmBtn.toggleClass('loading')
- callFn(extData)
- return false
- }
- }).modal('show')
+ const modal = $(".mini.confirm.modal");
+ modal.children(".header").text(title);
+ modal.children(".content").text(content);
+ if (confirmBtn.hasClass("loading")) {
+ return false;
+ }
+ modal
+ .modal({
+ closable: true,
+ onApprove: function () {
+ confirmBtn.toggleClass("loading");
+ callFn(extData);
+ return false;
+ },
+ })
+ .modal("show");
}
function showFormModal(modelSelector, formID, URL, getData) {
- $(modelSelector).modal({
- closable: true,
- onApprove: function () {
- let success = false
- const btn = $(modelSelector + ' .positive.button')
- const form = $(modelSelector + ' form')
- if (btn.hasClass('loading')) {
- return success
- }
- form.children('.message').remove()
- btn.toggleClass('loading')
- const data = getData ? getData() : $(formID).serializeArray().reduce(function (obj, item) {
+ $(modelSelector)
+ .modal({
+ closable: true,
+ onApprove: function () {
+ let success = false;
+ const btn = $(modelSelector + " .positive.button");
+ const form = $(modelSelector + " form");
+ if (btn.hasClass("loading")) {
+ return success;
+ }
+ form.children(".message").remove();
+ btn.toggleClass("loading");
+ 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);
+ 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;
+ obj[item.name] = item.value;
}
- if (item.name == 'ServersRaw') {
- if (item.value.length > 2) {
- obj[item.name] = '[' + item.value.substr(3, item.value.length - 1) + ']'
- }
+ if (item.name.endsWith("ServersRaw")) {
+ if (item.value.length > 2) {
+ obj[item.name] =
+ "[" + item.value.substr(3, item.value.length - 1) + "]";
+ }
}
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()
- } else {
- form.append(``)
- }
- }).fail(function (err) {
- form.append(``)
- }).always(function () {
- btn.toggleClass('loading')
- });
- return success
- }
- }).modal('show')
+ }, {});
+ $.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();
+ } else {
+ form.append(
+ ``
+ );
+ }
+ })
+ .fail(function (err) {
+ form.append(
+ `` +
+ err.responseText +
+ `
`
+ );
+ })
+ .always(function () {
+ btn.toggleClass("loading");
+ });
+ return success;
+ },
+ })
+ .modal("show");
}
function addOrEditAlertRule(rule) {
- const modal = $('.rule.modal')
- modal.children('.header').text((rule ? '修改' : '添加') + '报警规则')
- modal.find('.positive.button').html(rule ? '修改' : '添加')
- modal.find('input[name=ID]').val(rule ? rule.ID : null)
- modal.find('input[name=Name]').val(rule ? rule.Name : null)
- modal.find('textarea[name=RulesRaw]').val(rule ? rule.RulesRaw : null)
- if (rule && rule.Enable) {
- modal.find('.ui.rule-enable.checkbox').checkbox('set checked')
- } else {
- modal.find('.ui.rule-enable.checkbox').checkbox('set unchecked')
- }
- showFormModal('.rule.modal', '#ruleForm', '/api/alert-rule')
+ const modal = $(".rule.modal");
+ modal.children(".header").text((rule ? "修改" : "添加") + "报警规则");
+ modal
+ .find(".positive.button")
+ .html(
+ rule ? '修改' : '添加'
+ );
+ modal.find("input[name=ID]").val(rule ? rule.ID : null);
+ modal.find("input[name=Name]").val(rule ? rule.Name : null);
+ modal.find("textarea[name=RulesRaw]").val(rule ? rule.RulesRaw : null);
+ if (rule && rule.Enable) {
+ modal.find(".ui.rule-enable.checkbox").checkbox("set checked");
+ } else {
+ modal.find(".ui.rule-enable.checkbox").checkbox("set unchecked");
+ }
+ showFormModal(".rule.modal", "#ruleForm", "/api/alert-rule");
}
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.nf-ssl.checkbox').checkbox('set checked')
- } else {
- modal.find('.ui.nf-ssl.checkbox').checkbox('set unchecked')
- }
- showFormModal('.notification.modal', '#notificationForm', '/api/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.nf-ssl.checkbox").checkbox("set checked");
+ } else {
+ modal.find(".ui.nf-ssl.checkbox").checkbox("set unchecked");
+ }
+ showFormModal(
+ ".notification.modal",
+ "#notificationForm",
+ "/api/notification"
+ );
}
function addOrEditServer(server) {
- const modal = $('.server.modal')
- modal.children('.header').text((server ? '修改' : '添加') + '服务器')
- modal.find('.positive.button').html(server ? '修改' : '添加')
- 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=DisplayIndex]').val(server ? server.DisplayIndex : null)
- modal.find('textarea[name=Note]').val(server ? server.Note : null)
- if (server) {
- modal.find('.secret.field').attr('style', '')
- modal.find('input[name=secret]').val(server.Secret)
- } else {
- modal.find('.secret.field').attr('style', 'display:none')
- modal.find('input[name=secret]').val('')
- }
- showFormModal('.server.modal', '#serverForm', '/api/server')
+ const modal = $(".server.modal");
+ modal.children(".header").text((server ? "修改" : "添加") + "服务器");
+ modal
+ .find(".positive.button")
+ .html(
+ server ? '修改' : '添加'
+ );
+ 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=DisplayIndex]")
+ .val(server ? server.DisplayIndex : null);
+ modal.find("textarea[name=Note]").val(server ? server.Note : null);
+ if (server) {
+ modal.find(".secret.field").attr("style", "");
+ modal.find("input[name=secret]").val(server.Secret);
+ } else {
+ modal.find(".secret.field").attr("style", "display:none");
+ modal.find("input[name=secret]").val("");
+ }
+ showFormModal(".server.modal", "#serverForm", "/api/server");
}
function addOrEditMonitor(monitor) {
- const modal = $('.monitor.modal')
- modal.children('.header').text((monitor ? '修改' : '添加') + '监控')
- modal.find('.positive.button').html(monitor ? '修改' : '添加')
- modal.find('input[name=ID]').val(monitor ? monitor.ID : null)
- 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)
- if (monitor && monitor.Notify) {
- modal.find(".ui.nb-notify.checkbox").checkbox("set checked");
- } else {
- modal.find(".ui.nb-notify.checkbox").checkbox("set unchecked");
+ const modal = $(".monitor.modal");
+ modal.children(".header").text((monitor ? "修改" : "添加") + "监控");
+ modal
+ .find(".positive.button")
+ .html(
+ monitor ? '修改' : '添加'
+ );
+ modal.find("input[name=ID]").val(monitor ? monitor.ID : null);
+ 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);
+ if (monitor && monitor.Notify) {
+ modal.find(".ui.nb-notify.checkbox").checkbox("set checked");
+ } else {
+ modal.find(".ui.nb-notify.checkbox").checkbox("set unchecked");
+ }
+ var servers;
+ if (monitor) {
+ servers = monitor.SkipServersRaw || "[]";
+ const serverList = JSON.parse(servers);
+ const node = modal.find("i.dropdown.icon");
+ for (let i = 0; i < serverList.length; i++) {
+ node.after(
+ 'ID:' +
+ serverList[i] +
+ ''
+ );
}
- showFormModal('.monitor.modal', '#monitorForm', '/api/monitor')
+ }
+ modal
+ .find("input[name=SkipServersRaw]")
+ .val(monitor ? "[]," + servers.substr(1, servers.length - 2) : "[]");
+ showFormModal(".monitor.modal", "#monitorForm", "/api/monitor");
}
function addOrEditCron(cron) {
- const modal = $('.cron.modal')
- modal.children('.header').text((cron ? '修改' : '添加') + '计划任务')
- modal.find('.positive.button').html(cron ? '修改' : '添加')
- modal.find('input[name=ID]').val(cron ? cron.ID : null)
- modal.find('input[name=Name]').val(cron ? cron.Name : null)
- modal.find('input[name=Scheduler]').val(cron ? cron.Scheduler : null)
- modal.find('a.ui.label.visible').each((i, el) => {
- el.remove()
- })
- var servers
- if (cron) {
- servers = cron.ServersRaw
- serverList = JSON.parse(servers)
- const node = modal.find('i.dropdown.icon')
- for (let i = 0; i < serverList.length; i++) {
- node.after('ID:' + serverList[i] + '')
- }
+ const modal = $(".cron.modal");
+ modal.children(".header").text((cron ? "修改" : "添加") + "计划任务");
+ modal
+ .find(".positive.button")
+ .html(
+ cron ? '修改' : '添加'
+ );
+ modal.find("input[name=ID]").val(cron ? cron.ID : null);
+ modal.find("input[name=Name]").val(cron ? cron.Name : null);
+ modal.find("input[name=Scheduler]").val(cron ? cron.Scheduler : null);
+ modal.find("a.ui.label.visible").each((i, el) => {
+ el.remove();
+ });
+ var servers;
+ if (cron) {
+ servers = cron.ServersRaw;
+ const serverList = JSON.parse(servers);
+ const node = modal.find("i.dropdown.icon");
+ for (let i = 0; i < serverList.length; i++) {
+ node.after(
+ 'ID:' +
+ serverList[i] +
+ ''
+ );
}
- modal.find('input[name=ServersRaw]').val(cron ? '[],' + servers.substr(1, servers.length - 2) : '[]')
- modal.find('textarea[name=Command]').val(cron ? cron.Command : null)
- if (cron && cron.PushSuccessful) {
- modal.find('.ui.push-successful.checkbox').checkbox('set checked')
- } else {
- modal.find('.ui.push-successful.checkbox').checkbox('set unchecked')
- }
- showFormModal('.cron.modal', '#cronForm', '/api/cron')
+ }
+ modal
+ .find("input[name=ServersRaw]")
+ .val(cron ? "[]," + servers.substr(1, servers.length - 2) : "[]");
+ modal.find("textarea[name=Command]").val(cron ? cron.Command : null);
+ if (cron && cron.PushSuccessful) {
+ modal.find(".ui.push-successful.checkbox").checkbox("set checked");
+ } else {
+ modal.find(".ui.push-successful.checkbox").checkbox("set unchecked");
+ }
+ showFormModal(".cron.modal", "#cronForm", "/api/cron");
}
function deleteRequest(api) {
- $.ajax({
- url: api,
- type: 'DELETE',
- }).done(resp => {
- if (resp.code == 200) {
- if (resp.message) {
- alert(resp.message)
- } else {
- alert('删除成功')
- }
- window.location.reload()
+ $.ajax({
+ url: api,
+ type: "DELETE",
+ })
+ .done((resp) => {
+ if (resp.code == 200) {
+ if (resp.message) {
+ alert(resp.message);
} else {
- alert('删除失败 ' + resp.code + ':' + resp.message)
- confirmBtn.toggleClass('loading')
+ alert("删除成功");
}
- }).fail(err => {
- alert('网络错误:' + err.responseText)
+ window.location.reload();
+ } else {
+ alert("删除失败 " + resp.code + ":" + resp.message);
+ confirmBtn.toggleClass("loading");
+ }
+ })
+ .fail((err) => {
+ alert("网络错误:" + err.responseText);
});
}
function manualTrigger(btn, cronId) {
- $(btn).toggleClass('loading')
- $.ajax({
- url: '/api/cron/' + cronId + '/manual',
- type: 'GET',
- }).done(resp => {
- $(btn).toggleClass('loading')
- if (resp.code == 200) {
- $.suiAlert({
- title: '触发成功,等待执行结果',
- type: 'success',
- description: resp.message,
- time: '3',
- position: 'top-center',
- });
- } else {
- $.suiAlert({
- title: '触发失败 ',
- type: 'error',
- description: resp.code + ':' + resp.message,
- time: '3',
- position: 'top-center',
- });
- }
- }).fail(err => {
- $(btn).toggleClass('loading')
+ $(btn).toggleClass("loading");
+ $.ajax({
+ url: "/api/cron/" + cronId + "/manual",
+ type: "GET",
+ })
+ .done((resp) => {
+ $(btn).toggleClass("loading");
+ if (resp.code == 200) {
$.suiAlert({
- title: '触发失败 ',
- type: 'error',
- description: '网络错误:' + err.responseText,
- time: '3',
- position: 'top-center',
+ title: "触发成功,等待执行结果",
+ type: "success",
+ description: resp.message,
+ time: "3",
+ position: "top-center",
});
+ } else {
+ $.suiAlert({
+ title: "触发失败 ",
+ type: "error",
+ description: resp.code + ":" + resp.message,
+ time: "3",
+ position: "top-center",
+ });
+ }
+ })
+ .fail((err) => {
+ $(btn).toggleClass("loading");
+ $.suiAlert({
+ title: "触发失败 ",
+ type: "error",
+ description: "网络错误:" + err.responseText,
+ time: "3",
+ position: "top-center",
+ });
});
}
function logout(id) {
- $.post('/api/logout', JSON.stringify({ id: id })).done(function (resp) {
- if (resp.code == 200) {
- $.suiAlert({
- title: '注销成功',
- type: 'success',
- description: '如需继续访问请使用 GitHub 再次登录',
- time: '3',
- position: 'top-center',
- });
- window.location.reload()
- } else {
- $.suiAlert({
- title: '注销失败',
- description: resp.code + ':' + resp.message,
- type: 'error',
- time: '3',
- position: 'top-center',
- });
- }
- }).fail(function (err) {
+ $.post("/api/logout", JSON.stringify({ id: id }))
+ .done(function (resp) {
+ if (resp.code == 200) {
$.suiAlert({
- title: '网络错误',
- description: err.responseText,
- type: 'error',
- time: '3',
- position: 'top-center',
+ title: "注销成功",
+ type: "success",
+ description: "如需继续访问请使用 GitHub 再次登录",
+ time: "3",
+ position: "top-center",
});
+ window.location.reload();
+ } else {
+ $.suiAlert({
+ title: "注销失败",
+ description: resp.code + ":" + resp.message,
+ type: "error",
+ time: "3",
+ position: "top-center",
+ });
+ }
})
+ .fail(function (err) {
+ $.suiAlert({
+ title: "网络错误",
+ description: err.responseText,
+ type: "error",
+ time: "3",
+ position: "top-center",
+ });
+ });
}
$(document).ready(() => {
- try {
- $('.ui.servers.search.dropdown').dropdown({
- clearable: true,
- apiSettings: {
- url: '/api/search-server?word={query}',
- cache: false,
- },
- })
- } catch (error) {
- }
-})
\ No newline at end of file
+ try {
+ $(".ui.servers.search.dropdown").dropdown({
+ clearable: true,
+ apiSettings: {
+ url: "/api/search-server?word={query}",
+ cache: false,
+ },
+ });
+ } catch (error) {}
+});
diff --git a/resource/template/common/footer.html b/resource/template/common/footer.html
index 3c5a4ef..afbd9b0 100644
--- a/resource/template/common/footer.html
+++ b/resource/template/common/footer.html
@@ -9,7 +9,7 @@
-
+