Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4c6f4c57d | ||
|
|
4df60c6955 | ||
|
|
0501e120f6 | ||
|
|
cfa425bb33 | ||
|
|
0613b0022d | ||
|
|
740d15bf79 | ||
|
|
7ace5d04fa | ||
|
|
a38d7bb51d | ||
|
|
61e16f96c5 |
@@ -11,6 +11,8 @@
|
||||
<p>Supports <b>monitoring</b> system status, HTTP (SSL certificate change, upcoming expiration, expired), TCP, Ping and supports <b>push alerts</b>, run scheduled tasks and <b>web terminal</b>.</p>
|
||||
</div>
|
||||
|
||||
\>> Telegram Channel: [哪吒监控(中文通知频道)](https://t.me/nezhanews)
|
||||
|
||||
\>> Telegram Group: [Nezha Monitoring Global (English Only)](https://t.me/nezhamonitoring_global), [哪吒监控(中文群组)](https://t.me/nezhamonitoring)
|
||||
|
||||
\>> [Use Cases | 我们的用户](https://www.google.com/search?q=%22powered+by+Nezha+Monitoring%22+OR+%22powered+by+%E5%93%AA%E5%90%92%E7%9B%91%E6%8E%A7%22) (Google)
|
||||
@@ -42,13 +44,13 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
||||
<a href="https://github.com/AkkiaS7" title="Akkia"><img src="https://avatars.githubusercontent.com/u/68485070?v=4" width="50;" alt="Akkia"/></a>
|
||||
<a href="https://github.com/Erope" title="卖女孩的小火柴"><img src="https://avatars.githubusercontent.com/u/44471469?v=4" width="50;" alt="卖女孩的小火柴"/></a>
|
||||
<a href="https://github.com/dysf888" title="黑歌"><img src="https://avatars.githubusercontent.com/u/47450409?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/uubulb" title="UUBulb"><img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/></a>
|
||||
<a href="https://github.com/MikoyChinese" title="MikoyChinese"><img src="https://avatars.githubusercontent.com/u/22676744?v=4" width="50;" alt="MikoyChinese"/></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/nap0o" title="nap0o"><img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/></a>
|
||||
<a href="https://github.com/MikoyChinese" title="MikoyChinese"><img src="https://avatars.githubusercontent.com/u/22676744?v=4" width="50;" alt="MikoyChinese"/></a>
|
||||
<a href="https://github.com/JackieSung4ev" title="JackieSung4ev"><img src="https://avatars.githubusercontent.com/u/24974735?v=4" width="50;" alt="JackieSung4ev"/></a>
|
||||
<a href="https://github.com/lemoeo" title="Lemoe"><img src="https://avatars.githubusercontent.com/u/18618627?v=4" width="50;" alt="Lemoe"/></a>
|
||||
<a href="https://github.com/cantoblanco" title="Kris"><img src="https://avatars.githubusercontent.com/u/116849421?v=4" width="50;" alt="Kris"/></a>
|
||||
<a href="https://github.com/lemoeo" title="Lemoe"><img src="https://avatars.githubusercontent.com/u/18618627?v=4" width="50;" alt="Lemoe"/></a>
|
||||
<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/liuyanxi975" title="刘颜溪"><img src="https://avatars.githubusercontent.com/u/24417037?v=4" width="50;" alt="刘颜溪"/></a>
|
||||
<a href="https://github.com/CosmosZ-code" title="CosmosZ-code"><img src="https://avatars.githubusercontent.com/u/81398224?v=4" width="50;" alt="CosmosZ-code"/></a>
|
||||
|
||||
+28
-3
@@ -42,6 +42,19 @@ func percentage(used, total uint64) float64 {
|
||||
return float64(used) * 100 / float64(total)
|
||||
}
|
||||
|
||||
func maxSliceValue(slice []float64) float64 {
|
||||
if len(slice) != 0 {
|
||||
max := slice[0]
|
||||
for _, val := range slice {
|
||||
if max < val {
|
||||
max = val
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Snapshot 未通过规则返回 struct{}{}, 通过返回 nil
|
||||
func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) interface{} {
|
||||
// 监控全部但是排除了此服务器
|
||||
@@ -63,6 +76,8 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
|
||||
switch u.Type {
|
||||
case "cpu":
|
||||
src = float64(server.State.CPU)
|
||||
case "gpu":
|
||||
src = float64(server.State.GPU)
|
||||
case "memory":
|
||||
src = percentage(server.State.MemUsed, server.Host.MemTotal)
|
||||
case "swap":
|
||||
@@ -91,21 +106,21 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
|
||||
src = float64(server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn))
|
||||
if u.CycleInterval != 0 {
|
||||
var res NResult
|
||||
db.Model(&Transfer{}).Select("SUM(`in`) AS n").Where("created_at > ? AND server_id = ?", u.GetTransferDurationStart(), server.ID).Scan(&res)
|
||||
db.Model(&Transfer{}).Select("SUM(`in`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
|
||||
src += float64(res.N)
|
||||
}
|
||||
case "transfer_out_cycle":
|
||||
src = float64(server.State.NetOutTransfer - uint64(server.PrevHourlyTransferOut))
|
||||
if u.CycleInterval != 0 {
|
||||
var res NResult
|
||||
db.Model(&Transfer{}).Select("SUM(`out`) AS n").Where("created_at > ? AND server_id = ?", u.GetTransferDurationStart(), server.ID).Scan(&res)
|
||||
db.Model(&Transfer{}).Select("SUM(`out`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
|
||||
src += float64(res.N)
|
||||
}
|
||||
case "transfer_all_cycle":
|
||||
src = float64(server.State.NetOutTransfer - uint64(server.PrevHourlyTransferOut) + server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn))
|
||||
if u.CycleInterval != 0 {
|
||||
var res NResult
|
||||
db.Model(&Transfer{}).Select("SUM(`in`+`out`) AS n").Where("created_at > ? AND server_id = ?", u.GetTransferDurationStart(), server.ID).Scan(&res)
|
||||
db.Model(&Transfer{}).Select("SUM(`in`+`out`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
|
||||
src += float64(res.N)
|
||||
}
|
||||
case "load1":
|
||||
@@ -120,6 +135,16 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
|
||||
src = float64(server.State.UdpConnCount)
|
||||
case "process_count":
|
||||
src = float64(server.State.ProcessCount)
|
||||
case "temperature_max":
|
||||
var temp []float64
|
||||
if server.State.Temperatures != nil {
|
||||
for _, tempStat := range server.State.Temperatures {
|
||||
if tempStat.Temperature != 0 {
|
||||
temp = append(temp, tempStat.Temperature)
|
||||
}
|
||||
}
|
||||
src = maxSliceValue(temp)
|
||||
}
|
||||
}
|
||||
|
||||
// 循环区间流量检测 · 更新下次需要检测时间
|
||||
|
||||
@@ -13,7 +13,7 @@ type ProviderCloudflare struct {
|
||||
Secret string
|
||||
}
|
||||
|
||||
func (provider ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
if domainConfig == nil {
|
||||
return false
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func (provider ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool
|
||||
return resultV4 && resultV6
|
||||
}
|
||||
|
||||
func (provider ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *DomainConfig, isIpv4 bool) bool {
|
||||
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)
|
||||
@@ -58,7 +58,7 @@ func (provider ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *
|
||||
}
|
||||
}
|
||||
|
||||
func (provider ProviderCloudflare) getZoneID(domain string) (string, error) {
|
||||
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)
|
||||
@@ -81,7 +81,7 @@ func (provider ProviderCloudflare) getZoneID(domain string) (string, error) {
|
||||
return "", fmt.Errorf("找不到 Zone ID")
|
||||
}
|
||||
|
||||
func (provider ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) {
|
||||
func (provider *ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) {
|
||||
var ipType = "A"
|
||||
if !isIPv4 {
|
||||
ipType = "AAAA"
|
||||
@@ -106,7 +106,7 @@ func (provider ProviderCloudflare) findDNSRecord(zoneID string, domain string, i
|
||||
return nil, nil // 没有找到 DNS 记录
|
||||
}
|
||||
|
||||
func (provider ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
func (provider *ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
var ipType = "A"
|
||||
var ipAddr = domainConfig.Ipv4Addr
|
||||
if !isIPv4 {
|
||||
@@ -126,7 +126,7 @@ func (provider ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (provider ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
func (provider *ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
var ipType = "A"
|
||||
var ipAddr = domainConfig.Ipv4Addr
|
||||
if !isIPv4 {
|
||||
@@ -147,7 +147,7 @@ func (provider ProviderCloudflare) updateDNSRecord(zoneID string, recordID strin
|
||||
}
|
||||
|
||||
// 以下为辅助方法,如发送 HTTP 请求等
|
||||
func (provider ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
|
||||
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 {
|
||||
|
||||
+1
-1
@@ -2,6 +2,6 @@ package ddns
|
||||
|
||||
type ProviderDummy struct{}
|
||||
|
||||
func (provider ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
+10
-10
@@ -23,7 +23,7 @@ type ProviderTencentCloud struct {
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
func (provider ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
if domainConfig == nil {
|
||||
return false
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func (provider ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bo
|
||||
return resultV4 && resultV6
|
||||
}
|
||||
|
||||
func (provider ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig, isIpv4 bool) bool {
|
||||
func (provider *ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig, isIpv4 bool) bool {
|
||||
record, err := provider.findDNSRecord(domainConfig.FullDomain, isIpv4)
|
||||
if err != nil {
|
||||
log.Printf("查找 DNS 记录时出错: %s\n", err)
|
||||
@@ -66,7 +66,7 @@ func (provider ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig,
|
||||
return provider.updateDNSRecord(domainConfig.FullDomain, record["RecordList"].([]interface{})[0].(map[string]interface{})["RecordId"].(float64), domainConfig, isIpv4)
|
||||
}
|
||||
|
||||
func (provider ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (map[string]interface{}, error) {
|
||||
func (provider *ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (map[string]interface{}, error) {
|
||||
var ipType = "A"
|
||||
if !isIPv4 {
|
||||
ipType = "AAAA"
|
||||
@@ -95,7 +95,7 @@ func (provider ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (provider ProviderTencentCloud) createDNSRecord(domain string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
func (provider *ProviderTencentCloud) createDNSRecord(domain string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
var ipType = "A"
|
||||
var ipAddr = domainConfig.Ipv4Addr
|
||||
if !isIPv4 {
|
||||
@@ -117,7 +117,7 @@ func (provider ProviderTencentCloud) createDNSRecord(domain string, domainConfig
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (provider ProviderTencentCloud) updateDNSRecord(domain string, recordID float64, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
func (provider *ProviderTencentCloud) updateDNSRecord(domain string, recordID float64, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
var ipType = "A"
|
||||
var ipAddr = domainConfig.Ipv4Addr
|
||||
if !isIPv4 {
|
||||
@@ -141,7 +141,7 @@ func (provider ProviderTencentCloud) updateDNSRecord(domain string, recordID flo
|
||||
}
|
||||
|
||||
// 以下为辅助方法,如发送 HTTP 请求等
|
||||
func (provider ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
|
||||
func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
@@ -173,18 +173,18 @@ func (provider ProviderTencentCloud) sendRequest(action string, data []byte) ([]
|
||||
|
||||
// https://github.com/jeessy2/ddns-go/blob/master/util/tencent_cloud_signer.go
|
||||
|
||||
func (provider ProviderTencentCloud) sha256hex(s string) string {
|
||||
func (provider *ProviderTencentCloud) sha256hex(s string) string {
|
||||
b := sha256.Sum256([]byte(s))
|
||||
return hex.EncodeToString(b[:])
|
||||
}
|
||||
|
||||
func (provider ProviderTencentCloud) hmacsha256(s, key string) string {
|
||||
func (provider *ProviderTencentCloud) hmacsha256(s, key string) string {
|
||||
hashed := hmac.New(sha256.New, []byte(key))
|
||||
hashed.Write([]byte(s))
|
||||
return string(hashed.Sum(nil))
|
||||
}
|
||||
|
||||
func (provider ProviderTencentCloud) WriteString(strs ...string) string {
|
||||
func (provider *ProviderTencentCloud) WriteString(strs ...string) string {
|
||||
var b strings.Builder
|
||||
for _, str := range strs {
|
||||
b.WriteString(str)
|
||||
@@ -193,7 +193,7 @@ func (provider ProviderTencentCloud) WriteString(strs ...string) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (provider ProviderTencentCloud) signRequest(secretId string, secretKey string, r *http.Request, action string, payload string) {
|
||||
func (provider *ProviderTencentCloud) signRequest(secretId string, secretKey string, r *http.Request, action string, payload string) {
|
||||
algorithm := "TC3-HMAC-SHA256"
|
||||
service := "dnspod"
|
||||
host := provider.WriteString(service, ".tencentcloudapi.com")
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ type ProviderWebHook struct {
|
||||
RequestHeader string
|
||||
}
|
||||
|
||||
func (provider ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
if domainConfig == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
+30
-52
@@ -7,6 +7,7 @@ const mixinsVue = {
|
||||
showGoTop: false,
|
||||
preferredTemplate: null,
|
||||
isMobile: false,
|
||||
staticUrl: '/static/theme-server-status',
|
||||
adaptedTemplates: [
|
||||
{ key: 'default', name: 'Default', icon: 'th large' },
|
||||
{ key: 'angel-kanade', name: 'AngelKanade', icon: 'square' },
|
||||
@@ -15,8 +16,8 @@ const mixinsVue = {
|
||||
},
|
||||
created() {
|
||||
this.isMobile = this.checkIsMobile();
|
||||
this.initTheme();
|
||||
this.storedShowGroup();
|
||||
this.theme = this.initTheme();
|
||||
this.showGroup = this.initShowGroup();
|
||||
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
@@ -24,22 +25,36 @@ const mixinsVue = {
|
||||
window.removeEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
methods: {
|
||||
toggleView() {
|
||||
this.showGroup = !this.showGroup;
|
||||
localStorage.setItem("showGroup", JSON.stringify(this.showGroup));
|
||||
return this.showGroup;
|
||||
initTheme() {
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
const theme = (storedTheme === 'dark' || storedTheme === 'light') ? storedTheme : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
this.setTheme(theme);
|
||||
return theme;
|
||||
},
|
||||
storedShowGroup() {
|
||||
setTheme(theme) {
|
||||
document.body.setAttribute("theme", theme);
|
||||
this.theme = theme;
|
||||
localStorage.setItem("theme", theme);
|
||||
// 重新赋值全局调色
|
||||
this.colors = this.theme == "dark" ? this.colorsDark : this.colorsLight;
|
||||
|
||||
if(this.$root.page == 'index') {
|
||||
this.reloadCharts(); // 重新载入echarts图表
|
||||
}
|
||||
},
|
||||
initShowGroup() {
|
||||
const storedShowGroup = localStorage.getItem("showGroup");
|
||||
if (storedShowGroup !== null) {
|
||||
this.showGroup = JSON.parse(storedShowGroup);
|
||||
}
|
||||
const showGroup = storedShowGroup !== null ? JSON.parse(storedShowGroup) : false;
|
||||
if (storedShowGroup === null) {
|
||||
localStorage.setItem("showGroup", showGroup);
|
||||
}
|
||||
return showGroup;
|
||||
},
|
||||
toggleTemplate(template) {
|
||||
if( template != this.preferredTemplate){
|
||||
this.preferredTemplate = template;
|
||||
this.updateCookie("preferred_theme", template);
|
||||
window.location.reload();
|
||||
toggleShowGroup() {
|
||||
this.showGroup = !this.showGroup;
|
||||
localStorage.setItem("showGroup", this.showGroup);
|
||||
if (this.$root.page == 'service') {
|
||||
this.$root.initTooltip();
|
||||
}
|
||||
},
|
||||
updateCookie(name, value) {
|
||||
@@ -57,43 +72,6 @@ const mixinsVue = {
|
||||
}
|
||||
return cookieValue;
|
||||
},
|
||||
setTheme(title, store = false) {
|
||||
this.theme = title;
|
||||
document.body.setAttribute("theme", title);
|
||||
if (store) {
|
||||
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;
|
||||
},
|
||||
initTheme() {
|
||||
const storeTheme = localStorage.getItem("theme");
|
||||
if (storeTheme === 'dark' || storeTheme === 'light') {
|
||||
this.setTheme(storeTheme, true);
|
||||
} else {
|
||||
this.isSystemTheme = true
|
||||
const handleChange = (mediaQueryListEvent) => {
|
||||
if (localStorage.getItem("theme")) {
|
||||
return
|
||||
}
|
||||
if (mediaQueryListEvent.matches) {
|
||||
this.setTheme('dark');
|
||||
} else {
|
||||
this.setTheme('light');
|
||||
}
|
||||
}
|
||||
const mediaQueryListDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
this.setTheme(mediaQueryListDark.matches ? 'dark' : 'light');
|
||||
mediaQueryListDark.addEventListener("change", handleChange);
|
||||
}
|
||||
},
|
||||
toFixed2(f) {
|
||||
return f.toFixed(2)
|
||||
},
|
||||
|
||||
+1277
File diff suppressed because it is too large
Load Diff
+243
File diff suppressed because one or more lines are too long
+242
File diff suppressed because one or more lines are too long
+245
File diff suppressed because one or more lines are too long
+244
File diff suppressed because one or more lines are too long
+1
@@ -180,6 +180,7 @@
|
||||
'TC0H', //CPU Heatsink 温度,代表 CPU 散热器的温度
|
||||
'TC0P', //CPU Proximity 温度,代表 CPU 附近的温度
|
||||
'k10temp', //AMD K10(Phenom、Athlon、Sempron 等)系列处理器的温度监测
|
||||
'k10temp_tctl', //AMD K10 (Athlon II、Phenom II 等)系列处理器的温度监测
|
||||
'coretemp_package_id_0', //整个封装处理器温度
|
||||
'cpu_thermal_zone', //全志
|
||||
'cpu-thermal', //树莓派(博通)
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
<i @click="showMapChart" data-toggle="modal" data-target="#mapChartBox" class="bi bi-geo-alt"></i>
|
||||
</span>
|
||||
<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>
|
||||
<i v-if="showGroup" @click="toggleShowGroup" class="show-nogroup bi bi-justify"></i>
|
||||
<i v-else @click="toggleShowGroup" 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>
|
||||
<i v-if="theme === 'light'" @click="setTheme('dark')" class="setTheme-dark bi bi-moon-fill"></i>
|
||||
<i v-else @click="setTheme('light')" 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>
|
||||
|
||||
+1
-2
@@ -20,8 +20,7 @@
|
||||
<script src="https://unpkg.com/[email protected]/dist/js/bootstrap.min.js"></script>
|
||||
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
|
||||
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
|
||||
<script src="https://unpkg.com/[email protected]/map/js/world.js"></script>
|
||||
<script src="/static/theme-server-status/js/mixin.js?v20240302"></script>
|
||||
<script src="/static/theme-server-status/js/mixin.js?v20240707"></script>
|
||||
<!-- 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>
|
||||
|
||||
+74
-55
File diff suppressed because one or more lines are too long
+117
-98
@@ -4,74 +4,74 @@
|
||||
{{template "theme-server-status/content-nav" .}}
|
||||
<!-- 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">
|
||||
<section v-if="servicesTag.length === 0" class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;">
|
||||
<p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p>
|
||||
</section>
|
||||
<section v-else 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;">
|
||||
<section v-if="servicesNoTag.length === 0" class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;">
|
||||
<p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p>
|
||||
</section>
|
||||
<section v-else 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 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>
|
||||
{{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>
|
||||
{{end}}
|
||||
</section>
|
||||
{{if .CycleTransferStats}}
|
||||
<section class="container content table-responsive" style="max-width: 95vw">
|
||||
<table class="table table-striped table-condensed table-hover">
|
||||
<thead>
|
||||
<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>
|
||||
{{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>
|
||||
</section>
|
||||
{{end}}
|
||||
{{template "theme-server-status/content-footer" .}}
|
||||
</div>
|
||||
<script>
|
||||
// 初始化 Tooltip
|
||||
$(document).ready(function(){
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
@@ -80,13 +80,16 @@
|
||||
page: 'service',
|
||||
defaultTemplate: {{.Conf.Site.Theme}},
|
||||
templates: {{.Themes}},
|
||||
services: {{.Services}},
|
||||
servicesTag: [],
|
||||
servicesNoTag: [],
|
||||
servicesNoTag: []
|
||||
},
|
||||
created() {
|
||||
this.initData();
|
||||
this.servicesTag = this.groupingData(this.initData(this.services),"type");
|
||||
this.servicesNoTag = this.initData(this.services);
|
||||
},
|
||||
mounted() {
|
||||
this.initTooltip();
|
||||
},
|
||||
mixins: [mixinsVue],
|
||||
methods: {
|
||||
@@ -103,32 +106,36 @@
|
||||
}
|
||||
return n
|
||||
},
|
||||
initData() {
|
||||
// @formatter:off
|
||||
const services = []
|
||||
{{range $service := .Services}}
|
||||
services.push({
|
||||
type: '{{$service.Monitor.Type}}',
|
||||
name: '{{$service.Monitor.Name}}',
|
||||
currentUp: parseInt('{{$service.CurrentUp}}'),
|
||||
currentDown: parseInt('{{$service.CurrentDown}}'),
|
||||
totalUp: parseInt('{{$service.TotalUp}}'),
|
||||
totalDown: parseInt('{{$service.TotalDown}}'),
|
||||
delay: '{{$service.Delay}}'.replaceAll("[","").replaceAll("]","").split(" "),
|
||||
up: '{{$service.Up}}'.replaceAll("[","").replaceAll("]","").split(" "),
|
||||
down: '{{$service.Down}}'.replaceAll("[","").replaceAll("]","").split(" "),
|
||||
})
|
||||
{{end}}
|
||||
// @formatter:on
|
||||
for (let i = 0; i < services.length; i++) {
|
||||
const service = services[i];
|
||||
service.avgDelay = parseInt(service.delay[service.delay.length - 1]) + "ms"
|
||||
service.health = this.getStateInfo(this.getPercent(service.currentUp, service.currentDown))
|
||||
service.dayDetail = this.getDayTails(service)
|
||||
service.totalUpTime = this.getProgressInfo(this.getPercent(service.totalUp, service.totalDown))
|
||||
initData(services) {
|
||||
let nodes = [];
|
||||
for (let key in services) {
|
||||
const service = services[key];
|
||||
let node = {
|
||||
type: service.Monitor.Type,
|
||||
name: service.Monitor.Name,
|
||||
currentUp: parseInt(service.CurrentUp),
|
||||
currentDown: parseInt(service.CurrentDown),
|
||||
totalUp: parseInt(service.TotalUp),
|
||||
totalDown: parseInt(service.TotalDown),
|
||||
up: service.Up,
|
||||
down: service.Down,
|
||||
delay: service.Delay,
|
||||
avgDelay: parseInt(this.getAvgDelay(service.Delay)) + "ms",
|
||||
health: this.getStateInfo(this.getPercent(service.CurrentUp, service.CurrentDown)),
|
||||
totalUpTime: this.getProgressInfo(this.getPercent(service.TotalUp, service.TotalDown))
|
||||
};
|
||||
nodes.push(node);
|
||||
};
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
node.dayDetail = this.getDayTails(node)
|
||||
}
|
||||
this.servicesTag = this.groupingData(services,"type");
|
||||
this.servicesNoTag = services;
|
||||
return nodes
|
||||
},
|
||||
initTooltip() {
|
||||
$(document).ready(function(){
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
},
|
||||
getPercent(up, down) {
|
||||
if (!up) {
|
||||
@@ -151,24 +158,24 @@
|
||||
return this.toFixed2(currentUp / total * 100)
|
||||
},
|
||||
getDayTails(service) {
|
||||
const result = []
|
||||
const result = [];
|
||||
for (let i = 0; i < service.up.length; i++) {
|
||||
const up = service.up[i]
|
||||
const down = service.down[i]
|
||||
const delay = service.delay[i]
|
||||
let percent = this.getPercent(up, down)
|
||||
const up = service.up[i];
|
||||
const down = service.down[i];
|
||||
let delay = Number(service.delay[i]);
|
||||
let percent = this.getPercent(up, down);
|
||||
if (percent <= 0) {
|
||||
percent = 0;
|
||||
}
|
||||
let className = this.getStateInfo(percent).className
|
||||
let available = '{{tr "Availability"}}'
|
||||
let averageLatency = '{{tr "AverageLatency"}}'
|
||||
const text = `${this.beforeDay(service.up.length - i - 1)},${available}:${percent}%,${averageLatency}:${delay}ms`
|
||||
let className = this.getStateInfo(percent).className;
|
||||
let available = '{{tr "Availability"}}';
|
||||
let averageLatency = '{{tr "AverageLatency"}}';
|
||||
const text = `${this.beforeDay(service.up.length - i - 1)},${available}:${Number(percent).toFixed(3)}%,${averageLatency}:${Number(delay).toFixed(3)}ms`;
|
||||
result.push({
|
||||
text, className
|
||||
})
|
||||
});
|
||||
}
|
||||
return result
|
||||
return result;
|
||||
},
|
||||
beforeDay(days) {
|
||||
const today = new Date();
|
||||
@@ -203,8 +210,9 @@
|
||||
return result;
|
||||
},
|
||||
getProgressInfo(percent) {
|
||||
const result = this.getStateInfo(percent)
|
||||
const result = this.getStateInfo(percent);
|
||||
result.style = `width: ${parseInt(percent)}%`;
|
||||
result.percent = Number(percent).toFixed(2);
|
||||
const className = result.className;
|
||||
if (className === "good") {
|
||||
result.className = 'progress-bar progress-bar-success'
|
||||
@@ -218,8 +226,19 @@
|
||||
}
|
||||
return result
|
||||
},
|
||||
getAvgDelay(array) {
|
||||
const { sum, count } = array.reduce((acc, value) => {
|
||||
if (value !== 0) {
|
||||
acc.sum += value;
|
||||
acc.count += 1;
|
||||
}
|
||||
return acc;
|
||||
}, { sum: 0, count: 0 });
|
||||
|
||||
return count > 0 ? sum / count : 0;
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{{template "theme-server-status/footer" .}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
+2
-2
@@ -24,8 +24,8 @@ os_arch=""
|
||||
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
|
||||
|
||||
sudo() {
|
||||
EUID=$(id -ru)
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
myEUID=$(id -ru)
|
||||
if [ "$myEUID" -ne 0 ]; then
|
||||
if command -v sudo > /dev/null 2>&1; then
|
||||
command sudo "$@"
|
||||
else
|
||||
|
||||
@@ -24,8 +24,8 @@ os_arch=""
|
||||
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
|
||||
|
||||
sudo() {
|
||||
EUID=$(id -ru)
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
myEUID=$(id -ru)
|
||||
if [ "$myEUID" -ne 0 ]; then
|
||||
if command -v sudo > /dev/null 2>&1; then
|
||||
command sudo "$@"
|
||||
else
|
||||
|
||||
+14
-14
@@ -1,10 +1,10 @@
|
||||
package singleton
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
ddns2 "github.com/naiba/nezha/pkg/ddns"
|
||||
"log"
|
||||
|
||||
ddns2 "github.com/naiba/nezha/pkg/ddns"
|
||||
)
|
||||
|
||||
func RetryableUpdateDomain(provider ddns2.Provider, config *ddns2.DomainConfig, maxRetries int) bool {
|
||||
@@ -25,54 +25,54 @@ func RetryableUpdateDomain(provider ddns2.Provider, config *ddns2.DomainConfig,
|
||||
func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) {
|
||||
switch provider {
|
||||
case "webhook":
|
||||
return ddns2.ProviderWebHook{
|
||||
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
|
||||
return &ddns2.ProviderDummy{}, nil
|
||||
case "cloudflare":
|
||||
return ddns2.ProviderCloudflare{
|
||||
return &ddns2.ProviderCloudflare{
|
||||
Secret: Conf.DDNS.AccessSecret,
|
||||
}, nil
|
||||
case "tencentcloud":
|
||||
return ddns2.ProviderTencentCloud{
|
||||
return &ddns2.ProviderTencentCloud{
|
||||
SecretID: Conf.DDNS.AccessID,
|
||||
SecretKey: Conf.DDNS.AccessSecret,
|
||||
}, nil
|
||||
}
|
||||
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider))
|
||||
return &ddns2.ProviderDummy{}, fmt.Errorf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider)
|
||||
}
|
||||
|
||||
func GetDDNSProviderFromProfile(profileName string) (ddns2.Provider, error) {
|
||||
profile, ok := Conf.DDNS.Profiles[profileName]
|
||||
if !ok {
|
||||
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("未找到配置项 %s", profileName))
|
||||
return &ddns2.ProviderDummy{}, fmt.Errorf("未找到配置项 %s", profileName)
|
||||
}
|
||||
|
||||
switch profile.Provider {
|
||||
case "webhook":
|
||||
return ddns2.ProviderWebHook{
|
||||
return &ddns2.ProviderWebHook{
|
||||
URL: profile.WebhookURL,
|
||||
RequestMethod: profile.WebhookMethod,
|
||||
RequestBody: profile.WebhookRequestBody,
|
||||
RequestHeader: profile.WebhookHeaders,
|
||||
}, nil
|
||||
case "dummy":
|
||||
return ddns2.ProviderDummy{}, nil
|
||||
return &ddns2.ProviderDummy{}, nil
|
||||
case "cloudflare":
|
||||
return ddns2.ProviderCloudflare{
|
||||
return &ddns2.ProviderCloudflare{
|
||||
Secret: profile.AccessSecret,
|
||||
}, nil
|
||||
case "tencentcloud":
|
||||
return ddns2.ProviderTencentCloud{
|
||||
return &ddns2.ProviderTencentCloud{
|
||||
SecretID: profile.AccessID,
|
||||
SecretKey: profile.AccessSecret,
|
||||
}, nil
|
||||
}
|
||||
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", profile.Provider))
|
||||
return &ddns2.ProviderDummy{}, fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
|
||||
}
|
||||
|
||||
func ValidateDDNSProvidersFromProfiles() error {
|
||||
@@ -80,7 +80,7 @@ func ValidateDDNSProvidersFromProfiles() error {
|
||||
providers := make(map[string]string)
|
||||
for profileName, profile := range Conf.DDNS.Profiles {
|
||||
if _, ok := validProviders[profile.Provider]; !ok {
|
||||
return errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", profile.Provider))
|
||||
return fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
|
||||
}
|
||||
providers[profileName] = profile.Provider
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func RecordTransferHourlyUsage() {
|
||||
ServerLock.Lock()
|
||||
defer ServerLock.Unlock()
|
||||
now := time.Now()
|
||||
nowTrimSeconds := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, Loc)
|
||||
nowTrimSeconds := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
||||
var txs []model.Transfer
|
||||
for id, server := range ServerList {
|
||||
tx := model.Transfer{
|
||||
@@ -136,7 +136,7 @@ func CleanMonitorHistory() {
|
||||
if !rule.IsTransferDurationRule() {
|
||||
continue
|
||||
}
|
||||
dataCouldRemoveBefore := rule.GetTransferDurationStart()
|
||||
dataCouldRemoveBefore := rule.GetTransferDurationStart().UTC()
|
||||
// 判断规则影响的机器范围
|
||||
if rule.Cover == model.RuleCoverAll {
|
||||
// 更新全局可以清理的数据点
|
||||
@@ -155,12 +155,12 @@ func CleanMonitorHistory() {
|
||||
}
|
||||
}
|
||||
for id, couldRemove := range specialServerKeep {
|
||||
DB.Unscoped().Delete(&model.Transfer{}, "server_id = ? AND created_at < ?", id, couldRemove)
|
||||
DB.Unscoped().Delete(&model.Transfer{}, "server_id = ? AND datetime(`created_at`) < datetime(?)", id, couldRemove)
|
||||
}
|
||||
if allServerKeep.IsZero() {
|
||||
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?)", specialServerIDs)
|
||||
} else {
|
||||
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?) AND created_at < ?", specialServerIDs, allServerKeep)
|
||||
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?) AND datetime(`created_at`) < datetime(?)", specialServerIDs, allServerKeep)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user