Compare commits

...
38 Commits
Author SHA1 Message Date
Ko no dioandLvGJ 626613441d fix: add toggle to service (#317)
* fix: short nework word

* fix: add toggle on service

* fix: delete console log update main.js version

---------

Co-authored-by: LvGJ <[email protected]>
2024-02-13 23:09:40 +08:00
github-actions[bot] b86b73a433 update contributors[no ci] 2024-02-12 06:16:26 +00:00
Ko no dioandLvGJ e8b8e59bd7 feat: add network monitor hitory (#316) · 三网ping
* feat: add network monitor hitory

* fix: revert proto change and add indexStore

* fix: update monitor delete unuse monitor history

* fix: delete unuse monitor type

---------

Co-authored-by: LvGJ <[email protected]>
2024-02-12 14:16:04 +08:00
naiba c9bcba6f28 ♻️ 更换可用镜像地址 2024-02-09 20:59:22 +08:00
github-actions[bot] ad9dc5470a update contributors[no ci] 2024-01-18 05:35:21 +00:00
黑歌 8539fb6559 replace daocloud with fgit (#315)
* Update add custom mirror

* Update add custom mirror

* bump install version
2024-01-18 13:34:58 +08:00
github-actions[bot] 11bcc47d49 update contributors[no ci] 2023-12-26 02:26:27 +00:00
Crazy bb789f7652 add supervise daemon for alpine (#313)
alpine在自动更新agent以后不会自动重启agent,导致批量掉线,添加守护进程以后问题解决
2023-12-26 10:26:11 +08:00
dependabot[bot] 51c1e41123 Bump golang.org/x/crypto from 0.15.0 to 0.17.0 (#312)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.15.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.15.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 09:43:33 +08:00
naiba 83efdc6258 replace statifile cdn with bytedance cdn 2023-12-11 21:53:56 +08:00
github-actions[bot] bf58ad24b5 update contributors[no ci] 2023-12-08 13:17:02 +00:00
spiritlhl 53581d48fc 更新判断面板是否是通过docker安装的逻辑 (#311)
* 更新判断面板是否是通过docker安装的逻辑

* Updated logic for determining whether a panel is installed via docker or not
2023-12-08 21:16:36 +08:00
naiba b00a00e791 Add comment about missing riscv64 image in latest Alpine image 2023-12-08 09:55:17 +08:00
github-actions[bot] b91f29077c update contributors[no ci] 2023-12-07 13:59:01 +00:00
nap0oandSuperHsiao 99111a3115 improve server-status theme (#310)
* Add files via upload

* Add files via upload

* Add files via upload

* improve theme-server-status

1.前台分组展示agent
2.一些小优化

---------

Co-authored-by: SuperHsiao <[email protected]>
2023-12-07 21:58:42 +08:00
github-actions[bot] 30652c3d79 update contributors[no ci] 2023-12-07 13:41:51 +00:00
Mmx ac0103a1ee Improve: use alpine as dockerfile base image (#309)
* build: use alpine as base image

* build: use alpine edge instead of latest
2023-12-07 21:41:26 +08:00
github-actions[bot] f59b31590e update contributors[no ci] 2023-12-06 14:42:42 +00:00
Applexad 1bcddf1944 installer: fix docker compose command determination (#307) 2023-12-06 22:42:22 +08:00
卖女孩的小火柴 d790ca04d6 Fix select_version (#306) 2023-12-06 21:35:24 +08:00
github-actions[bot] 2106ca41e1 update contributors[no ci] 2023-12-06 01:35:54 +00:00
Mmx 927bf13ce4 fix: field name "VerifySSL" to "SkipVerifySSL" in Transport config (#305) 2023-12-06 09:35:38 +08:00
github-actions[bot] 859e263815 update contributors[no ci] 2023-12-05 15:23:19 +00:00
Mmx 1dcd899591 Fix Coroutine Leaks and Proxy Handling in http.Client (#304)
* fix: builtin variable conflict

* feat: add pkg/utils/http HttpClientSkipTlsVerify HttpClient

* fix: realtime create http Transport

* fix: http keepalive connection may not reusable

* feat: allow http request use proxy from environment
2023-12-05 23:22:25 +08:00
naiba 3059bf2d49 Update installer version to v0.15.2 2023-12-04 10:23:56 +08:00
naiba 74255717ef Update destination repository URL in sync workflow 2023-12-04 10:21:13 +08:00
github-actions[bot] 8fa59a4639 update contributors[no ci] 2023-11-30 05:16:55 +00:00
Applexad b3e38e8924 Exit after installing agent (#303)
If entered with install_agent argument, variable IS_DOCKER_NEZHA will not be set, thus leading select menu to appear, which could be annoying.
2023-11-30 13:16:39 +08:00
github-actions[bot] 2280e0267c update contributors[no ci] 2023-11-30 02:49:24 +00:00
Applexad 1871c481e4 Fix various bugs in fresh installation (#302) 2023-11-30 10:49:06 +08:00
naiba c3d1eec6bf Update GitHub Actions workflow to include specific paths 2023-11-30 09:52:24 +08:00
github-actions[bot] 6fc8ec9446 update contributors[no ci] 2023-11-30 01:39:22 +00:00
Applexad 52703f38cd Add standalone installation method & OpenRC support for nezha-agent (#300)
* Add standalone installation method & OpenRC support for nezha-agent

* Add missing translations & more precise query

* bump installer version
2023-11-30 09:39:00 +08:00
卖女孩的小火柴 d0707b9b05 Fix: get.daocloud.io 503 (#301) 2023-11-30 09:31:39 +08:00
naiba ad99a5d032 Export custom static folder 2023-11-29 17:13:45 +08:00
naiba a823da1f29 📦 static link 2023-11-29 15:45:32 +08:00
naiba b0b7e9da61 Update badges in README 2023-11-29 09:44:47 +08:00
naiba 6a25f39e36 🐛 fix third-party template loading 2023-11-29 09:42:51 +08:00
63 changed files with 2752 additions and 600 deletions
+2 -2
View File
@@ -30,12 +30,12 @@ jobs:
v: true v: true
x: false x: false
race: false race: false
ldflags: -s -w -X github.com/naiba/nezha/service/singleton.Version=${{ steps.extract_branch.outputs.tag }} ldflags: -s -w --extldflags '-static -fpic' -X github.com/naiba/nezha/service/singleton.Version=${{ steps.extract_branch.outputs.tag }}
buildmode: default buildmode: default
- name: fix dist - name: fix dist
run: | run: |
cp dist/dashboard-linux-arm-7 dist/dashboard-linux-arm mv dist/dashboard-linux-arm-7 dist/dashboard-linux-arm
- name: Log in to the GHCR - name: Log in to the GHCR
uses: docker/login-action@master uses: docker/login-action@master
+1 -1
View File
@@ -11,6 +11,6 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: adambirds/[email protected] - uses: adambirds/[email protected]
with: with:
destination_repository: git@jihulab.com:nezha/dashboard.git destination_repository: git@gitee.com:naibahq/nezha.git
destination_branch_name: master destination_branch_name: master
destination_ssh_key: ${{ secrets.GITLAB_SSH_KEY }} destination_ssh_key: ${{ secrets.GITLAB_SSH_KEY }}
+15 -3
View File
@@ -17,10 +17,22 @@ jobs:
- name: Unit test - name: Unit test
run: | run: |
go test -v ./... go test -v ./...
- name: Build test
run: |
go build cmd/dashboard/main.go
- name: Run Gosec Security Scanner - name: Run Gosec Security Scanner
uses: securego/gosec@master uses: securego/gosec@master
with: with:
args: --exclude=G104,G402 ./... args: --exclude=G104,G402 ./...
- name: xgo build
uses: crazy-max/ghaction-xgo@v2
with:
xgo_version: latest
go_version: 1.21
dest: dist
pkg: cmd/dashboard
prefix: dashboard
targets: linux/amd64,linux/arm64,linux/arm-7,linux/s390x,linux/riscv64 # linux/386,
v: true
x: false
race: false
ldflags: -s -w --extldflags '-static -fpic' -X github.com/naiba/nezha/service/singleton.Version=test
buildmode: default
+20 -11
View File
@@ -4,14 +4,12 @@ on:
push: push:
branches: branches:
- master - master
paths-ignore: paths:
- ".github/workflows/codeql-analysis.yml" - "**.go"
- ".github/workflows/test-on-pr.yml" - "go.mod"
- ".github/workflows/contributors.yml" - "go.sum"
- ".github/workflows/sync.yml" - "resource/**"
- ".github/workflows/release.yml" - ".github/workflows/test.yml"
- "README.md"
- ".goreleaser.yml"
jobs: jobs:
tests: tests:
@@ -26,10 +24,21 @@ jobs:
- name: Unit test - name: Unit test
run: | run: |
go test -v ./... go test -v ./...
- name: Build test
run: |
go build cmd/dashboard/main.go
- name: Run Gosec Security Scanner - name: Run Gosec Security Scanner
uses: securego/gosec@master uses: securego/gosec@master
with: with:
args: --exclude=G104,G402 ./... args: --exclude=G104,G402 ./...
- name: xgo build
uses: crazy-max/ghaction-xgo@v2
with:
xgo_version: latest
go_version: 1.21
dest: dist
pkg: cmd/dashboard
prefix: dashboard
targets: linux/amd64,linux/arm64,linux/arm-7,linux/s390x,linux/riscv64 # linux/386,
v: true
x: false
race: false
ldflags: -s -w --extldflags '-static -fpic' -X github.com/naiba/nezha/service/singleton.Version=test
buildmode: default
+1 -1
View File
@@ -19,4 +19,4 @@
/cmd/dashboard/main /cmd/dashboard/main
/config.yml /config.yml
/resource/template/theme-custom /resource/template/theme-custom
/resource/static/theme-custom /resource/static/custom
+10 -10
View File
@@ -1,18 +1,18 @@
FROM ubuntu:focal-20221130 # latest 镜像还没有 riscv64 https://hub.docker.com/_/alpine/tags
FROM alpine:edge
ENV TZ="Asia/Shanghai"
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
COPY ./script/entrypoint.sh /entrypoint.sh RUN apk update && \
apk upgrade --no-cache && \
apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo 'Asia/Shanghai' >/etc/timezone && \
rm -rf /var/cache/apk/*
RUN export DEBIAN_FRONTEND="noninteractive" && \ COPY ./script/entrypoint.sh /entrypoint.sh
apt update && apt install -y ca-certificates tzdata && \ RUN chmod +x /entrypoint.sh
update-ca-certificates && \
ln -fs /usr/share/zoneinfo/$TZ /etc/localtime && \
dpkg-reconfigure tzdata && \
chmod +x /entrypoint.sh
WORKDIR /dashboard WORKDIR /dashboard
COPY dist/dashboard-${TARGETOS}-${TARGETARCH} ./app COPY dist/dashboard-${TARGETOS}-${TARGETARCH} ./app
+65 -50
View File
@@ -4,7 +4,7 @@
<br> <br>
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small> <small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
<br><br> <br><br>
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?style=for-the-badge&logo=github&label=Dashboard">&nbsp;<img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.15.0-brightgreen?style=for-the-badge&logo=linux"> <img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard">&nbsp;<img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.15.6-brightgreen?style=for-the-badge&logo=linux">
<br> <br>
<br> <br>
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p> <p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
@@ -65,17 +65,23 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/cantoblanco" title="Kris"> <a href="https://github.com/cantoblanco" title="Kris">
<img src="https://avatars.githubusercontent.com/u/116849421?v=4" width="50;" alt="Kris"/> <img src="https://avatars.githubusercontent.com/u/116849421?v=4" width="50;" alt="Kris"/>
</a> </a>
<a href="https://github.com/CosmosZ-code" title="CosmosZ-code"> <a href="https://github.com/spiritLHLS" title="spiritlhl">
<img src="https://avatars.githubusercontent.com/u/81398224?v=4" width="50;" alt="CosmosZ-code"/> <img src="https://avatars.githubusercontent.com/u/103393591?v=4" width="50;" alt="spiritlhl"/>
</a> </a>
<a href="https://github.com/liuyanxi975" title="刘颜溪"> <a href="https://github.com/liuyanxi975" title="刘颜溪">
<img src="https://avatars.githubusercontent.com/u/24417037?v=4" width="50;" alt="刘颜溪"/> <img src="https://avatars.githubusercontent.com/u/24417037?v=4" width="50;" alt="刘颜溪"/>
</a> </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>
<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/hhhkkk520" title="Kris"> <a href="https://github.com/hhhkkk520" title="Kris">
<img src="https://avatars.githubusercontent.com/u/52115472?v=4" width="50;" alt="Kris"/> <img src="https://avatars.githubusercontent.com/u/52115472?v=4" width="50;" alt="Kris"/>
</a> </a>
<a href="https://github.com/spiritLHLS" title="spiritlhl"> <a href="https://github.com/Mmx233" title="Mmx">
<img src="https://avatars.githubusercontent.com/u/103393591?v=4" width="50;" alt="spiritlhl"/> <img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/>
</a> </a>
<a href="https://github.com/rootmelo92118" title="rootmelo92118"> <a href="https://github.com/rootmelo92118" title="rootmelo92118">
<img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/> <img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/>
@@ -83,62 +89,71 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/1ridic" title="1ridic"> <a href="https://github.com/1ridic" title="1ridic">
<img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/> <img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/>
</a> </a>
<a href="https://github.com/coreff" title="Core F"> <a href="https://github.com/iilemon" title="Sean">
<img src="https://avatars.githubusercontent.com/u/38347122?v=4" width="50;" alt="Core F"/> <img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/>
</a>
<a href="https://github.com/Creling" title="Creling">
<img src="https://avatars.githubusercontent.com/u/43109504?v=4" width="50;" alt="Creling"/>
</a>
<a href="https://github.com/ch8o" title="no-name-now">
<img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/>
</a> </a>
<a href="https://github.com/fscarmen" title="fscarmen"> <a href="https://github.com/fscarmen" title="fscarmen">
<img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/> <img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/>
</a> </a>
<a href="https://github.com/iilemon" title="Sean"> <a href="https://github.com/ch8o" title="no-name-now">
<img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/> <img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/>
</a> </a>
<a href="https://github.com/Es-dese" title="Esdese"> <a href="https://github.com/Creling" title="Creling">
<img src="https://avatars.githubusercontent.com/u/71542548?v=4" width="50;" alt="Esdese"/> <img src="https://avatars.githubusercontent.com/u/43109504?v=4" width="50;" alt="Creling"/>
</a> </a>
<a href="https://github.com/GreenTeodoro839" title="GreenTeodoro839"> <a href="https://github.com/coreff" title="Core F">
<img src="https://avatars.githubusercontent.com/u/77104800?v=4" width="50;" alt="GreenTeodoro839"/> <img src="https://avatars.githubusercontent.com/u/38347122?v=4" width="50;" alt="Core F"/>
</a> </a>
<a href="https://github.com/techotaku" title="Ian Li"> <a href="https://github.com/colour93" title="玖叁">
<img src="https://avatars.githubusercontent.com/u/1948179?v=4" width="50;" alt="Ian Li"/> <img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/>
</a>
<a href="https://github.com/KorenKrita" title="KorenKrita">
<img src="https://avatars.githubusercontent.com/u/22239339?v=4" width="50;" alt="KorenKrita"/>
</a>
<a href="https://github.com/MartijnLindeman" title="Martijn Lindeman">
<img src="https://avatars.githubusercontent.com/u/78365708?v=4" width="50;" alt="Martijn Lindeman"/>
</a>
<a href="https://github.com/nickfox-taterli" title="Tater Li">
<img src="https://avatars.githubusercontent.com/u/19658596?v=4" width="50;" alt="Tater Li"/>
</a>
<a href="https://github.com/hmsjy2017" title="Tony">
<img src="https://avatars.githubusercontent.com/u/42692274?v=4" width="50;" alt="Tony"/>
</a>
<a href="https://github.com/adminsama" title="adminsama">
<img src="https://avatars.githubusercontent.com/u/60880076?v=4" width="50;" alt="adminsama"/>
</a>
<a href="https://github.com/acgpiano" title="Acgpiano">
<img src="https://avatars.githubusercontent.com/u/15900800?v=4" width="50;" alt="Acgpiano"/>
</a>
<a href="https://github.com/guoyongchang" title="guoyongchang">
<img src="https://avatars.githubusercontent.com/u/10484506?v=4" width="50;" alt="guoyongchang"/>
</a>
<a href="https://github.com/yuanweize" title="I">
<img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/>
</a>
<a href="https://github.com/unclezs" title="unclezs">
<img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/>
</a> </a>
<a href="https://github.com/ysicing" title="缘生"> <a href="https://github.com/ysicing" title="缘生">
<img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/> <img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/>
</a> </a>
<a href="https://github.com/colour93" title="玖叁"> <a href="https://github.com/unclezs" title="unclezs">
<img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/> <img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/>
</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/yuanweize" title="I">
<img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/>
</a>
<a href="https://github.com/guoyongchang" title="guoyongchang">
<img src="https://avatars.githubusercontent.com/u/10484506?v=4" width="50;" alt="guoyongchang"/>
</a>
<a href="https://github.com/acgpiano" title="Acgpiano">
<img src="https://avatars.githubusercontent.com/u/15900800?v=4" width="50;" alt="Acgpiano"/>
</a>
<a href="https://github.com/adminsama" title="adminsama">
<img src="https://avatars.githubusercontent.com/u/60880076?v=4" width="50;" alt="adminsama"/>
</a>
<a href="https://github.com/hmsjy2017" title="Tony">
<img src="https://avatars.githubusercontent.com/u/42692274?v=4" width="50;" alt="Tony"/>
</a>
<a href="https://github.com/nickfox-taterli" title="Tater Li">
<img src="https://avatars.githubusercontent.com/u/19658596?v=4" width="50;" alt="Tater Li"/>
</a>
<a href="https://github.com/MartijnLindeman" title="Martijn Lindeman">
<img src="https://avatars.githubusercontent.com/u/78365708?v=4" width="50;" alt="Martijn Lindeman"/>
</a>
<a href="https://github.com/KorenKrita" title="KorenKrita">
<img src="https://avatars.githubusercontent.com/u/22239339?v=4" width="50;" alt="KorenKrita"/>
</a>
<a href="https://github.com/lvgj-stack" title="Ko no dio">
<img src="https://avatars.githubusercontent.com/u/38449861?v=4" width="50;" alt="Ko no dio"/>
</a>
<a href="https://github.com/techotaku" title="Ian Li">
<img src="https://avatars.githubusercontent.com/u/1948179?v=4" width="50;" alt="Ian Li"/>
</a>
<a href="https://github.com/GreenTeodoro839" title="GreenTeodoro839">
<img src="https://avatars.githubusercontent.com/u/77104800?v=4" width="50;" alt="GreenTeodoro839"/>
</a>
<a href="https://github.com/Es-dese" title="Esdese">
<img src="https://avatars.githubusercontent.com/u/71542548?v=4" width="50;" alt="Esdese"/>
</a>
<a href="https://github.com/wwng2333" title="Crazy">
<img src="https://avatars.githubusercontent.com/u/17147265?v=4" width="50;" alt="Crazy"/>
</a><!--GAMFC_DELIMITER_END--> </a><!--GAMFC_DELIMITER_END-->
## Star History ## Star History
+25 -4
View File
@@ -1,11 +1,13 @@
package controller package controller
import ( import (
"github.com/gin-gonic/gin"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/service/singleton"
"strconv" "strconv"
"strings" "strings"
"github.com/gin-gonic/gin"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/service/singleton"
) )
type apiV1 struct { type apiV1 struct {
@@ -25,7 +27,8 @@ func (v *apiV1) serve() {
})) }))
r.GET("/server/list", v.serverList) r.GET("/server/list", v.serverList)
r.GET("/server/details", v.serverDetails) r.GET("/server/details", v.serverDetails)
mr := v.r.Group("monitor")
mr.GET("/:id", v.monitorHistoriesById)
} }
// serverList 获取服务器列表 不传入Query参数则获取全部 // serverList 获取服务器列表 不传入Query参数则获取全部
@@ -65,3 +68,21 @@ func (v *apiV1) serverDetails(c *gin.Context) {
} }
c.JSON(200, singleton.ServerAPI.GetAllStatus()) c.JSON(200, singleton.ServerAPI.GetAllStatus())
} }
func (v *apiV1) monitorHistoriesById(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"code": 400, "message": "id参数错误"})
return
}
server, ok := singleton.ServerList[id]
if !ok {
c.AbortWithStatusJSON(404, gin.H{
"code": 404,
"message": "id不存在",
})
return
}
c.JSON(200, singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": server.ID}))
}
+110
View File
@@ -5,6 +5,7 @@ import (
"log" "log"
"net/http" "net/http"
"regexp" "regexp"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -48,6 +49,9 @@ func (cp *commonPage) serve() {
cr.Use(cp.checkViewPassword) // 前端查看密码鉴权 cr.Use(cp.checkViewPassword) // 前端查看密码鉴权
cr.GET("/", cp.home) cr.GET("/", cp.home)
cr.GET("/service", cp.service) cr.GET("/service", cp.service)
// TODO: 界面直接跳转使用该接口
cr.GET("/network/:id", cp.network)
cr.GET("/network", cp.network)
cr.GET("/ws", cp.ws) cr.GET("/ws", cp.ws)
cr.POST("/terminal", cp.createTerminal) cr.POST("/terminal", cp.createTerminal)
} }
@@ -127,6 +131,112 @@ func (p *commonPage) service(c *gin.Context) {
})) }))
} }
func (cp *commonPage) network(c *gin.Context) {
var (
monitorHistory *model.MonitorHistory
servers []*model.Server
serverIdsWithMonitor []uint64
monitorInfos = []byte("{}")
id uint64
)
if len(singleton.SortedServerList) > 0 {
id = singleton.SortedServerList[0].ID
}
if err := singleton.DB.Model(&model.MonitorHistory{}).Select("monitor_id, server_id").
Where("monitor_id != 0 and server_id != 0").Limit(1).First(&monitorHistory).Error; err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + "server monitor history not found",
Link: "/",
Btn: "返回重试",
}, true)
return
} else {
if monitorHistory == nil || monitorHistory.ServerID == 0 {
if len(singleton.SortedServerList) > 0 {
id = singleton.SortedServerList[0].ID
}
} else {
id = monitorHistory.ServerID
}
}
idStr := c.Param("id")
if idStr != "" {
var err error
id, err = strconv.ParseUint(idStr, 10, 64)
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + err.Error(),
Link: "/",
Btn: "返回重试",
}, true)
return
}
_, ok := singleton.ServerList[id]
if !ok {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + "server id not found",
Link: "/",
Btn: "返回重试",
}, true)
return
}
}
monitorHistories := singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": id})
monitorInfos, _ = utils.Json.Marshal(monitorHistories)
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
if err := singleton.DB.Model(&model.MonitorHistory{}).
Select("distinct(server_id)").
Where("server_id != 0").
Find(&serverIdsWithMonitor).
Error; err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + "no server with monitor histories",
Link: "/",
Btn: "返回重试",
}, true)
return
}
if isMember || isViewPasswordVerfied {
for _, server := range singleton.SortedServerList {
for _, id := range serverIdsWithMonitor {
if server.ID == id {
servers = append(servers, server)
}
}
}
} else {
for _, server := range singleton.SortedServerListForGuest {
for _, id := range serverIdsWithMonitor {
if server.ID == id {
servers = append(servers, server)
}
}
}
}
serversBytes, _ := utils.Json.Marshal(Data{
Now: time.Now().Unix() * 1000,
Servers: servers,
})
c.HTML(http.StatusOK, "theme-"+singleton.Conf.Site.Theme+"/network", mygin.CommonEnvironment(c, gin.H{
"Servers": string(serversBytes),
"MonitorInfos": string(monitorInfos),
"CustomCode": singleton.Conf.Site.CustomCode,
"MaxTCPPingValue": singleton.Conf.MaxTCPPingValue,
}))
}
func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) { func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
v, err, _ := cp.requestGroup.Do("serverStats", func() (any, error) { v, err, _ := cp.requestGroup.Do("serverStats", func() (any, error) {
singleton.SortedServerLock.RLock() singleton.SortedServerLock.RLock()
+26 -7
View File
@@ -4,7 +4,9 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io/fs" "io/fs"
"log"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -16,7 +18,6 @@ import (
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/naiba/nezha/pkg/mygin" "github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/resource" "github.com/naiba/nezha/resource"
"github.com/naiba/nezha/service/singleton" "github.com/naiba/nezha/service/singleton"
) )
@@ -30,12 +31,7 @@ func ServeWeb(port uint) *http.Server {
if err != nil { if err != nil {
panic(err) panic(err)
} }
if utils.IsFileExists("resource/template") { tmpl = loadThirdPartyTemplates(tmpl)
tmpl, err = tmpl.ParseGlob("resource/template/**/*.html")
if err != nil {
panic(err)
}
}
r.SetHTMLTemplate(tmpl) r.SetHTMLTemplate(tmpl)
if singleton.Conf.Debug { if singleton.Conf.Debug {
gin.SetMode(gin.DebugMode) gin.SetMode(gin.DebugMode)
@@ -47,6 +43,7 @@ func ServeWeb(port uint) *http.Server {
panic(err) panic(err)
} }
r.StaticFS("/static", http.FS(staticFs)) r.StaticFS("/static", http.FS(staticFs))
r.Static("/static-custom", "resource/static/custom")
routers(r) routers(r)
page404 := func(c *gin.Context) { page404 := func(c *gin.Context) {
@@ -87,6 +84,28 @@ func routers(r *gin.Engine) {
} }
} }
func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
var ret = tmpl
themes, err := os.ReadDir("resource/template")
if err != nil {
log.Printf("NEZHA>> Error reading themes folder: %v", err)
return ret
}
for _, theme := range themes {
if !theme.IsDir() {
continue
}
// load templates
t, err := ret.ParseGlob(fmt.Sprintf("resource/template/%s/*.html", theme.Name()))
if err != nil {
log.Printf("NEZHA>> Error parsing templates %s error: %v", theme.Name(), err)
continue
}
ret = t
}
return ret
}
var funcMap = template.FuncMap{ var funcMap = template.FuncMap{
"tr": func(id string, dataAndCount ...interface{}) string { "tr": func(id string, dataAndCount ...interface{}) string {
conf := i18n.LocalizeConfig{ conf := i18n.LocalizeConfig{
+18
View File
@@ -11,6 +11,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
"gorm.io/gorm"
"github.com/naiba/nezha/model" "github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin" "github.com/naiba/nezha/pkg/mygin"
@@ -185,7 +186,17 @@ func (ma *memberAPI) delete(c *gin.Context) {
var err error var err error
switch c.Param("model") { switch c.Param("model") {
case "server": case "server":
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
err = singleton.DB.Unscoped().Delete(&model.Server{}, "id = ?", id).Error err = singleton.DB.Unscoped().Delete(&model.Server{}, "id = ?", id).Error
if err != nil {
return err
}
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "server_id = ?", id).Error
if err != nil {
return err
}
return nil
})
if err == nil { if err == nil {
// 删除服务器 // 删除服务器
singleton.ServerLock.Lock() singleton.ServerLock.Lock()
@@ -383,6 +394,7 @@ type monitorForm struct {
MaxLatency float32 MaxLatency float32
LatencyNotify string LatencyNotify string
EnableTriggerTask string EnableTriggerTask string
EnableShowInService string
FailTriggerTasksRaw string FailTriggerTasksRaw string
RecoverTriggerTasksRaw string RecoverTriggerTasksRaw string
} }
@@ -404,6 +416,7 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
m.LatencyNotify = mf.LatencyNotify == "on" m.LatencyNotify = mf.LatencyNotify == "on"
m.MinLatency = mf.MinLatency m.MinLatency = mf.MinLatency
m.MaxLatency = mf.MaxLatency m.MaxLatency = mf.MaxLatency
m.EnableShowInService = mf.EnableShowInService == "on"
m.EnableTriggerTask = mf.EnableTriggerTask == "on" m.EnableTriggerTask = mf.EnableTriggerTask == "on"
m.RecoverTriggerTasksRaw = mf.RecoverTriggerTasksRaw m.RecoverTriggerTasksRaw = mf.RecoverTriggerTasksRaw
m.FailTriggerTasksRaw = mf.FailTriggerTasksRaw m.FailTriggerTasksRaw = mf.FailTriggerTasksRaw
@@ -427,6 +440,11 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
err = singleton.DB.Save(&m).Error err = singleton.DB.Save(&m).Error
} }
} }
if m.Cover == 0 {
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
} else {
err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id not in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
}
} }
if err == nil { if err == nil {
err = singleton.ServiceSentinelShared.OnMonitorUpdate(m) err = singleton.ServiceSentinelShared.OnMonitorUpdate(m)
+12 -2
View File
@@ -49,10 +49,20 @@ func DispatchTask(serviceSentinelDispatchBus <-chan model.Monitor) {
workedServerIndex++ workedServerIndex++
continue continue
} }
// 找到合适机器执行任务,跳出循环 if task.Cover == model.MonitorCoverIgnoreAll && task.SkipServers[singleton.SortedServerList[workedServerIndex].ID] {
singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB()) singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
workedServerIndex++ workedServerIndex++
break continue
}
if task.Cover == model.MonitorCoverAll && !task.SkipServers[singleton.SortedServerList[workedServerIndex].ID] {
singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
workedServerIndex++
continue
}
// 找到合适机器执行任务,跳出循环
// singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
// workedServerIndex++
// break
} }
singleton.SortedServerLock.RUnlock() singleton.SortedServerLock.RUnlock()
} }
+2 -2
View File
@@ -24,7 +24,7 @@ require (
github.com/spf13/viper v1.17.0 github.com/spf13/viper v1.17.0
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
github.com/xanzy/go-gitlab v0.94.0 github.com/xanzy/go-gitlab v0.94.0
golang.org/x/crypto v0.15.0 golang.org/x/crypto v0.17.0
golang.org/x/oauth2 v0.14.0 golang.org/x/oauth2 v0.14.0
golang.org/x/sync v0.5.0 golang.org/x/sync v0.5.0
golang.org/x/text v0.14.0 golang.org/x/text v0.14.0
@@ -80,7 +80,7 @@ require (
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.18.0 // indirect golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.14.0 // indirect golang.org/x/sys v0.15.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect
+6 -6
View File
@@ -339,8 +339,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -478,12 +478,12 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+6 -6
View File
@@ -85,13 +85,13 @@ func (r *AlertRule) Snapshot(cycleTransferStats *CycleTransferStats, server *Ser
// Check 传入包含当前报警规则下所有type检查结果的空接口 返回报警持续时间与是否通过报警检查(通过则返回true) // Check 传入包含当前报警规则下所有type检查结果的空接口 返回报警持续时间与是否通过报警检查(通过则返回true)
func (r *AlertRule) Check(points [][]interface{}) (int, bool) { func (r *AlertRule) Check(points [][]interface{}) (int, bool) {
var max int // 报警持续时间 var maxNum int // 报警持续时间
var count int // 检查未通过的个数 var count int // 检查未通过的个数
for i := 0; i < len(r.Rules); i++ { for i := 0; i < len(r.Rules); i++ {
if r.Rules[i].IsTransferDurationRule() { if r.Rules[i].IsTransferDurationRule() {
// 循环区间流量报警 // 循环区间流量报警
if max < 1 { if maxNum < 1 {
max = 1 maxNum = 1
} }
for j := len(points[i]) - 1; j >= 0; j-- { for j := len(points[i]) - 1; j >= 0; j-- {
if points[i][j] != nil { if points[i][j] != nil {
@@ -104,8 +104,8 @@ func (r *AlertRule) Check(points [][]interface{}) (int, bool) {
total := 0.0 total := 0.0
fail := 0.0 fail := 0.0
num := int(r.Rules[i].Duration) num := int(r.Rules[i].Duration)
if num > max { if num > maxNum {
max = num maxNum = num
} }
if len(points) < num { if len(points) < num {
continue continue
@@ -124,5 +124,5 @@ func (r *AlertRule) Check(points [][]interface{}) (int, bool) {
} }
} }
// 仅当所有检查均未通过时 返回false // 仅当所有检查均未通过时 返回false
return max, count != len(r.Rules) return maxNum, count != len(r.Rules)
} }
+8
View File
@@ -110,6 +110,8 @@ type Config struct {
v *viper.Viper v *viper.Viper
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内) IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
MaxTCPPingValue int32
AvgPingCount int
} }
// Read 读取配置文件并应用 // Read 读取配置文件并应用
@@ -144,6 +146,12 @@ func (c *Config) Read(path string) error {
if c.Location == "" { if c.Location == "" {
c.Location = "Asia/Shanghai" c.Location = "Asia/Shanghai"
} }
if c.MaxTCPPingValue == 0 {
c.MaxTCPPingValue = 300
}
if c.AvgPingCount == 0 {
c.AvgPingCount = 2
}
c.updateIgnoredIPNotificationID() c.updateIgnoredIPNotificationID()
return nil return nil
+4 -2
View File
@@ -4,10 +4,11 @@ import (
"fmt" "fmt"
"log" "log"
"github.com/naiba/nezha/pkg/utils"
pb "github.com/naiba/nezha/proto"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/naiba/nezha/pkg/utils"
pb "github.com/naiba/nezha/proto"
) )
const ( const (
@@ -49,6 +50,7 @@ type Monitor struct {
Cover uint8 Cover uint8
EnableTriggerTask bool `gorm:"default: false"` EnableTriggerTask bool `gorm:"default: false"`
EnableShowInService bool `gorm:"default: false"`
FailTriggerTasksRaw string `gorm:"default:'[]'"` FailTriggerTasksRaw string `gorm:"default:'[]'"`
RecoverTriggerTasksRaw string `gorm:"default:'[]'"` RecoverTriggerTasksRaw string `gorm:"default:'[]'"`
FailTriggerTasks []uint64 `gorm:"-" json:"-"` // 失败时执行的触发任务id FailTriggerTasks []uint64 `gorm:"-" json:"-"` // 失败时执行的触发任务id
+9
View File
@@ -1,9 +1,18 @@
package model package model
const (
Cycle = iota
Hour
Day
Week
Month
)
// MonitorHistory 历史监控记录 // MonitorHistory 历史监控记录
type MonitorHistory struct { type MonitorHistory struct {
Common Common
MonitorID uint64 MonitorID uint64
ServerID uint64
AvgDelay float32 // 平均延迟,毫秒 AvgDelay float32 // 平均延迟,毫秒
Up uint64 // 检查状态良好计数 Up uint64 // 检查状态良好计数
Down uint64 // 检查状态异常计数 Down uint64 // 检查状态异常计数
+8 -10
View File
@@ -1,7 +1,6 @@
package model package model
import ( import (
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -111,17 +110,14 @@ func (n *Notification) setRequestHeader(req *http.Request) error {
} }
func (ns *NotificationServerBundle) Send(message string) error { func (ns *NotificationServerBundle) Send(message string) error {
var verifySSL bool var client *http.Client
n := ns.Notification n := ns.Notification
if n.VerifySSL != nil && *n.VerifySSL { if n.VerifySSL != nil && *n.VerifySSL {
verifySSL = true client = utils.HttpClient
} else {
client = utils.HttpClientSkipTlsVerify
} }
transCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: verifySSL},
}
client := &http.Client{Transport: transCfg, Timeout: time.Minute * 10}
reqBody, err := ns.reqBody(message) reqBody, err := ns.reqBody(message)
if err != nil { if err != nil {
return err return err
@@ -147,13 +143,15 @@ func (ns *NotificationServerBundle) Send(message string) error {
if err != nil { if err != nil {
return err return err
} }
if resp.StatusCode < 200 || resp.StatusCode > 299 {
defer func() { defer func() {
_ = resp.Body.Close() _ = resp.Body.Close()
}() }()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
body, _ := io.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("%d@%s %s", resp.StatusCode, resp.Status, string(body)) return fmt.Errorf("%d@%s %s", resp.StatusCode, resp.Status, string(body))
} else {
_, _ = io.Copy(io.Discard, resp.Body)
} }
return nil return nil
+47
View File
@@ -0,0 +1,47 @@
package utils
import (
"crypto/tls"
"net/http"
"time"
)
var (
HttpClientSkipTlsVerify *http.Client
HttpClient *http.Client
)
func init() {
HttpClientSkipTlsVerify = httpClient(_httpClient{
Transport: httpTransport(_httpTransport{
SkipVerifySSL: true,
}),
})
HttpClient = httpClient(_httpClient{
Transport: httpTransport(_httpTransport{
SkipVerifySSL: false,
}),
})
}
type _httpTransport struct {
SkipVerifySSL bool
}
func httpTransport(conf _httpTransport) *http.Transport {
return &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: conf.SkipVerifySSL},
Proxy: http.ProxyFromEnvironment,
}
}
type _httpClient struct {
Transport *http.Transport
}
func httpClient(conf _httpClient) *http.Client {
return &http.Client{
Transport: conf.Transport,
Timeout: time.Minute * 10,
}
}
+6
View File
@@ -609,3 +609,9 @@ other = "Hide for Guest"
[Menu] [Menu]
other = "Menu" other = "Menu"
[NetworkSpiter]
other = "Network"
[EnableShowInService]
other = "Enable Show in Service"
+6
View File
@@ -609,3 +609,9 @@ other = "Ocultar para Invitados"
[Menu] [Menu]
other = "Menú" other = "Menú"
[NetworkSpiter]
other = "Red"
[EnableShowInService]
other = "Mostrar en servicio"
+6
View File
@@ -609,3 +609,9 @@ other = "对游客隐藏"
[Menu] [Menu]
other = "菜单" other = "菜单"
[NetworkSpiter]
other = "网络"
[EnableShowInService]
other = "在服务中显示"
+6
View File
@@ -609,3 +609,9 @@ other = "對遊客隱藏"
[Menu] [Menu]
other = "菜單" other = "菜單"
[NetworkSpiter]
other = "網絡"
[EnableShowInService]
other = "在服務中顯示"
+5
View File
@@ -339,6 +339,11 @@ function addOrEditMonitor(monitor) {
modal.find("select[name=Type]").val(monitor ? monitor.Type : 1); modal.find("select[name=Type]").val(monitor ? monitor.Type : 1);
modal.find("select[name=Cover]").val(monitor ? monitor.Cover : 0); modal.find("select[name=Cover]").val(monitor ? monitor.Cover : 0);
modal.find("input[name=NotificationTag]").val(monitor ? monitor.NotificationTag : null); modal.find("input[name=NotificationTag]").val(monitor ? monitor.NotificationTag : null);
if (monitor && monitor.EnableShowInService) {
modal.find(".ui.nb-show-in-service.checkbox").checkbox("set checked")
} else {
modal.find(".ui.nb-show-in-service.checkbox").checkbox("set unchecked")
}
if (monitor && monitor.Notify) { if (monitor && monitor.Notify) {
modal.find(".ui.nb-notify.checkbox").checkbox("set checked"); modal.find(".ui.nb-notify.checkbox").checkbox("set checked");
} else { } else {
File diff suppressed because one or more lines are too long
+25
View File
@@ -27,6 +27,10 @@ body[theme="light"] .table {
background: #ffffff; background: #ffffff;
} }
body[theme="light"] .table>thead>tr>th.node-group-tag {
background-color:#ffffff;
}
body[theme="light"] .table-striped tbody > tr.even > td, body[theme="light"] .table-striped tbody > tr.even > td,
body[theme="light"] .table-striped tbody > tr.even > th { body[theme="light"] .table-striped tbody > tr.even > th {
background-color: #F9F9F9; background-color: #F9F9F9;
@@ -37,10 +41,25 @@ body[theme="light"] .table-striped tbody > tr.odd > th {
background-color: #FFF; background-color: #FFF;
} }
body[theme="light"] .table>tbody>tr>td,
body[theme="light"] .table>tbody>tr>th,
body[theme="light"] .table>tfoot>tr>td,
body[theme="light"] .table>tfoot>tr>th,
body[theme="light"] .table>thead>tr>td,
body[theme="light"] .table>thead>tr>th {
line-height:20px;
}
body[theme="light"] .progress-bar { body[theme="light"] .progress-bar {
color: #000; color: #000;
} }
body[theme="light"] .progress-offline .progress-bar-success {
background-image: linear-gradient(grey 0px, grey 100%);
}
}
body[theme="light"] .table-hover > tbody > tr:hover > td { body[theme="light"] .table-hover > tbody > tr:hover > td {
background: #E6E6E6; background: #E6E6E6;
} }
@@ -76,3 +95,9 @@ body[theme="light"] .service-status .warning {
body[theme="light"] .service-day-status-icon { body[theme="light"] .service-day-status-icon {
background-color: #ebebeb; background-color: #ebebeb;
} }
body[theme="light"] footer p a,
body[theme="light"] footer p a:hover {
color: #333333;
text-decoration: none;
}
+49 -83
View File
@@ -1,5 +1,5 @@
body { body {
padding-top: 70px !important; padding-top:70px !important;
padding-bottom: 30px !important; padding-bottom: 30px !important;
} }
@@ -8,14 +8,27 @@ body {
min-height: 40px !important; min-height: 40px !important;
} }
.navbar-inner{
margin:0 auto;
}
.pl-md-unset {
max-width: 95vw;
}
.navbar-collapse:not([aria-expanded]) .navbar-nav .dropdown-toggle { .navbar-collapse:not([aria-expanded]) .navbar-nav .dropdown-toggle {
margin-top: 18px; margin-top: 18px;
padding: 0 !important; padding: 0 !important;
} }
.navbar-toggle {
margin-right:0
}
.navbar-brand { .navbar-brand {
font-size: 20px; font-size: 20px;
text-align: center; text-align: center;
padding:12px 0 0 0;
} }
.node-cell-expand { .node-cell-expand {
@@ -32,18 +45,17 @@ body {
padding-top: 10px; padding-top: 10px;
} }
.dropdown {
margin-right: 10px;
}
.navbar-inverse, .nav.navbar-nav { .navbar-inverse, .nav.navbar-nav {
background-image: linear-gradient(rgb(60, 60, 60) 0px, rgb(34, 34, 34) 100%) !important; background-image: linear-gradient(rgb(60, 60, 60) 0px, rgb(34, 34, 34) 100%) !important;
} }
.navbar-inverse .navbar-nav>li>a {
color:#f1f1f1;
}
.navbar-inverse .navbar-brand { .navbar-inverse .navbar-brand {
font-size: 20px; font-size: 20px;
} }
/* 导航部分 结束 */ /* 导航部分 结束 */
/* 正文部分 开始 */ /* 正文部分 开始 */
@@ -63,6 +75,15 @@ body {
text-align: left; text-align: left;
} }
.table>thead>tr>th{
border:none;
}
.table .node-group-tag {
font-size: 18px;
padding-bottom:15px;
}
.progress { .progress {
margin-bottom: 0; margin-bottom: 0;
} }
@@ -78,6 +99,10 @@ body {
border-top: 0 !important; border-top: 0 !important;
} }
.accordian-body{
margin: 10px 0px 10px 18px;
}
.node-cell.center { .node-cell.center {
text-align: center !important; text-align: center !important;
} }
@@ -147,104 +172,45 @@ body {
} }
/* 服务页 正文结束 */ /* 服务页 正文结束 */
@media only screen and (max-width: 1200px) { @media only screen and (max-width: 1200px) {
.node-cell.os, .accordian-body{
.node-cell.location, margin: 5px 0px 5px 10px;
.node-cell.uptime { }
display: none; .table .node-group-tag {
visibility: hidden; font-size:16px;
padding-bottom:6px;
} }
} }
@media only screen and (max-width: 720px) { @media only screen and (max-width: 720px) {
body { body {
font-size: 10px !important; font-size: 10px !important;
padding-top:60px !important;
} }
.content { .content {
padding: 0; padding: 0;
margin-bottom: 10px;
} }
.node-cell.os, .node-cell.os,
.node-cell.location,
.node-cell.uptime {
display: none;
visibility: hidden;
}
.service-day-status-icon {
width: 8px;
height: 8px;
margin-right: 2px;
border-radius: 3px;
}
}
@media only screen and (max-width: 620px) {
body {
font-size: 10px !important;
}
.content {
padding: 0;
}
.node-cell.type,
.node-cell.location,
.node-cell.uptime, .node-cell.uptime,
.node-cell.traffic { .node-cell.traffic{
display: none; display: none;
visibility: hidden; visibility: hidden;
} }
.node-cell.location{
.service-day-status-icon { display: table-cell;
visibility: visible;
} }
} .node-cell-os-text,.node-cell-location-text{
@media only screen and (max-width: 533px) {
body {
font-size: 10px !important;
}
.content {
padding: 0;
}
.node-cell.os,
.node-cell.location,
.node-cell.uptime,
.node-cell.traffic {
display: none; display: none;
visibility: hidden; visibility: hidden;
} }
.accordian-body{
.service-day-status-icon { margin: 5px 0px 5px 10px;
} }
} .table .node-group-tag {
font-size:12px;
@media only screen and (max-width: 450px) { padding-bottom:6px;
body {
font-size: 10px !important;
}
.content {
padding: 0;
}
.node-cell.type,
.node-cell.location,
.node-cell.uptime,
.node-cell.traffic {
display: none;
visibility: hidden;
}
.node-cell.cpu,
.node-cell.ram,
.node-cell.hdd {
min-width: 25px;
max-width: 50px;
} }
} }
+4 -4
View File
@@ -6,11 +6,11 @@
{{.Version}}</small> {{.Version}}</small>
</div> </div>
</div> </div>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script> <script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="/static/main.js?v20230614"></script> <script src="/static/main.js?v20240213"></script>
<script> <script>
(function () { (function () {
updateLang({{.LANG }}); updateLang({{.LANG }});
+2 -2
View File
@@ -9,8 +9,8 @@
<meta content="telephone=no" name="format-detection"> <meta content="telephone=no" name="format-detection">
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" type="text/css" <link rel="stylesheet" type="text/css"
href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css"> href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.css">
<link href="https://cdn.staticfile.org/font-logos/0.17/font-logos.min.css" type="text/css" <link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-logos/0.17/font-logos.min.css" type="text/css"
rel="stylesheet" /> rel="stylesheet" />
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css"> <link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
<link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314"> <link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314">
+1
View File
@@ -15,6 +15,7 @@
{{else}} {{else}}
<a class='item{{if eq .MatchedPath "/"}} active{{end}}' href="/"><i class="home icon"></i>{{tr "Home"}}</a> <a class='item{{if eq .MatchedPath "/"}} active{{end}}' href="/"><i class="home icon"></i>{{tr "Home"}}</a>
<a class='item{{if eq .MatchedPath "/service"}} active{{end}}' href="/service"><i class="rss icon"></i>{{tr "Services"}}</a> <a class='item{{if eq .MatchedPath "/service"}} active{{end}}' href="/service"><i class="rss icon"></i>{{tr "Services"}}</a>
<a class='item{{if eq .MatchedPath "/network"}} active{{end}}' href="/network"><i class="server icon"></i>{{tr "NetworkSpiter"}}</a>
{{end}} {{end}}
<div class="right menu"> <div class="right menu">
<div class="item"> <div class="item">
+7
View File
@@ -24,6 +24,12 @@
<option value="3">TCP-Ping</option> <option value="3">TCP-Ping</option>
</select> </select>
</div> </div>
<div class="field">
<div class="ui nb-show-in-service checkbox">
<input name="EnableShowInService" type="checkbox" tabindex="0" class="hidden" />
<label>{{tr "EnableShowInService"}}</label>
</div>
</div>
<div class="field"> <div class="field">
<label>{{tr "Duration"}}</label> <label>{{tr "Duration"}}</label>
<input type="number" name="Duration" placeholder="{{tr "Seconds"}}" /> <input type="number" name="Duration" placeholder="{{tr "Seconds"}}" />
@@ -110,3 +116,4 @@
</div> </div>
</div> </div>
{{end}} {{end}}
+1 -1
View File
@@ -85,7 +85,7 @@
</div> </div>
{{template "component/server" .}} {{template "component/server" .}}
{{template "common/footer" .}} {{template "common/footer" .}}
<script src="https://cdn.staticfile.org/clipboard.js/2.0.10/clipboard.min.js" type="application/javascript"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/clipboard.js/2.0.10/clipboard.min.js" type="application/javascript"></script>
<script> <script>
var clipboard = new ClipboardJS('.ui.icon.green.mini.button'); var clipboard = new ClipboardJS('.ui.icon.green.mini.button');
const checkBoxList = document.querySelectorAll('tbody > tr > td > input.nezha-servers[type=checkbox]') const checkBoxList = document.querySelectorAll('tbody > tr > td > input.nezha-servers[type=checkbox]')
+4 -4
View File
@@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>tty@{{.ServerName}} - {{.Title}}</title> <title>tty@{{.ServerName}} - {{.Title}}</title>
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" /> <link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link href="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/xterm.css" type="text/css" rel="stylesheet"/> <link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/xterm.css" type="text/css" rel="stylesheet"/>
</head> </head>
<style> <style>
@@ -28,9 +28,9 @@
<body onresize="onResize()"> <body onresize="onResize()">
<div id="terminal-container"></div> <div id="terminal-container"></div>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/xterm.js" type="application/javascript"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/xterm.js" type="application/javascript"></script>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/addons/attach/xterm-addon-attach.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/addons/attach/xterm-addon-attach.js"></script>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/addons/fit/xterm-addon-fit.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/addons/fit/xterm-addon-fit.js"></script>
<script> <script>
let sendResizing = false; let sendResizing = false;
+4 -4
View File
@@ -8,11 +8,11 @@
</small> </small>
</div> </div>
</div> </div>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script> <script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="/static/main.js?v20230614"></script> <script src="/static/main.js?v20240213"></script>
<script> <script>
(function () { (function () {
updateLang({{.LANG }}); updateLang({{.LANG }});
+3 -3
View File
@@ -9,10 +9,10 @@
<meta content="telephone=no" name="format-detection"> <meta content="telephone=no" name="format-detection">
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" type="text/css" <link rel="stylesheet" type="text/css"
href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css"> href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.css">
<link href="https://cdn.staticfile.org/font-logos/0.17/font-logos.min.css" type="text/css" <link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-logos/0.17/font-logos.min.css" type="text/css"
rel="stylesheet" /> rel="stylesheet" />
<link href="https://cdn.staticfile.org/bootstrap-icons/1.10.3/font/bootstrap-icons.css" type="text/css" <link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/bootstrap-icons/1.8.1/font/bootstrap-icons.min.css" type="text/css"
rel="stylesheet" /> rel="stylesheet" />
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css"> <link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
<link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314"> <link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314">
+1 -1
View File
@@ -15,7 +15,7 @@
<div v-for="server in group.data" :id="server.ID" class="ui card"> <div v-for="server in group.data" :id="server.ID" class="ui card">
<div class="content" v-if="server.Host" style="margin-top: 10px; padding-bottom: 5px"> <div class="content" v-if="server.Host" style="margin-top: 10px; padding-bottom: 5px">
<div class="header"> <div class="header">
<img v-if="server.Host.CountryCode" style="border-radius: 50%;box-shadow:-1px -1px 2px #eee, 1px 1px 2px #000;width:19px;" :src="'https://cdn.staticfile.org/flag-icon-css/6.6.5/flags/1x1/'+server.Host.CountryCode + '.svg'" alt="国家"/>&nbsp;<i v-if='server.Host.Platform == "darwin"' <img v-if="server.Host.CountryCode" style="border-radius: 50%;box-shadow:-1px -1px 2px #eee, 1px 1px 2px #000;width:19px;" :src="'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/flag-icon-css/4.1.5/flags/1x1/'+server.Host.CountryCode + '.svg'" alt="国家"/>&nbsp;<i v-if='server.Host.Platform == "darwin"'
class="apple icon"></i><i v-else-if='isWindowsPlatform(server.Host.Platform)' class="apple icon"></i><i v-else-if='isWindowsPlatform(server.Host.Platform)'
class="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i> class="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
@#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@ @#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
+5 -4
View File
@@ -10,8 +10,8 @@
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" /> <link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link rel="stylesheet" href="/static/theme-daynight/css/main.css?v202108042286"> <link rel="stylesheet" href="/static/theme-daynight/css/main.css?v202108042286">
<link href="https://cdn.staticfile.org/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> <link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
{{if ts .CustomCode}} {{if ts .CustomCode}}
{{.CustomCode|safe}} {{.CustomCode|safe}}
@@ -39,6 +39,7 @@
<ul> <ul>
<li><a href="/">{{tr "Home"}}</a></li> <li><a href="/">{{tr "Home"}}</a></li>
<li><a href="/service">{{tr "Services"}}</a></li> <li><a href="/service">{{tr "Services"}}</a></li>
<li><a href="/network">{{tr "NetworkSpiter"}}</a></li>
{{if .Admin}} {{if .Admin}}
<li><a href="/server">{{tr "AdminPanel"}}</a></li> <li><a href="/server">{{tr "AdminPanel"}}</a></li>
{{else}} {{else}}
@@ -165,8 +166,8 @@
</footer> </footer>
<script src="/static/theme-daynight/js/main.js?v202102012266"></script> <script src="/static/theme-daynight/js/main.js?v202102012266"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="https://cdn.staticfile.org/limonte-sweetalert2/11.4.4/sweetalert2.all.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/limonte-sweetalert2/11.4.4/sweetalert2.all.min.js"></script>
<script> <script>
const initData = JSON.parse('{{.Servers}}').servers; const initData = JSON.parse('{{.Servers}}').servers;
+268
View File
@@ -0,0 +1,268 @@
{{define "theme-daynight/network"}}
<!doctype html>
<html lang="{{.Conf.Language}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>{{.Title}}</title>
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link rel="stylesheet" href="/static/theme-daynight/css/main.css?v202108042286">
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
{{if ts .CustomCode}}
{{.CustomCode|safe}}
{{end}}
</head>
<body data-theme="light" data-gridlist="grid">
<header>
<section class="nav-bar clearfix">
<figure class="logo">
<a href="/">
<img src="/static/logo.svg?v20210804" alt='{{tr "NezhaMonitoring"}}' width="50" height="50">
</a>
<a href="/">{{.Conf.Site.Brand}}</a>
</figure>
<div class="icon-container">
<div class="row cf">
<div class="three col">
<div class="hamburger" id="hamburger-icon"><span class="line"></span><span
class="line"></span><span class="line"></span></div>
</div>
</div>
</div>
<nav class="nav-menu">
<ul>
<li><a href="/">{{tr "Home"}}</a></li>
<li><a href="/service">{{tr "Services"}}</a></li>
<li><a href="/network">{{tr "NetworkSpiter"}}</a></li>
{{if .Admin}}
<li><a href="/server">{{tr "AdminPanel"}}</a></li>
{{else}}
<li><a href="/login">{{tr "Login"}}</a></li>
{{end}}
</ul>
</nav>
</section>
</header>
<main>
<div id="network">
<div class="server-info-container" v-for='server in servers' :id="server.ID" style="font-size: .6em">
<div class="info-body" @click="redirectNetwork(server.ID)">
<ul class="server-info-body-container">
<li>
<h3>@#server.Name#@</h3>
</li>
<li><img :src="'/static/theme-daynight/img/flag/'+(server.Host&&server.Host.CountryCode?server.Host.CountryCode.toUpperCase():'CN')+'.png'"
:title="server.Host.CountryCode.toUpperCase()" /></li>
</ul>
</div>
</div>
</div>
<div class="network-chart" style="height: 800px;overflow: hidden">
<div id="monitor-info-container" style="height: 520px;max-width: 1400px">
</div>
</div>
<div class="sidebar-container">
<ul>
<li><i class="fas fa-sun" title='{{tr "LightMode"}}'></i><span>{{tr "LightMode"}}</span></li>
<li><i class="fas fa-moon" title='{{tr "DarkMode"}}'></i><span>{{tr "DarkMode"}}</span></li>
<li><i class="fas fa-th" title='{{tr "GridLayout"}}'></i><span>{{tr "GridLayout"}}</span></li>
<li><i class="fas fa-list-ul" title='{{tr "ListLayout"}}'></i><span>{{tr "ListLayout"}}</span></li>
</ul>
</div>
</main>
<section class="dark-light-toggle">
<label class="switcher">
<input type="checkbox" name="theme" id="dark-light" />
<div>
<i class="fas fa-adjust"></i>
</div>
</label>
</section>
<!-- Back to top button -->
<a id="back-to-top"></a>
<footer>
<div class="footer-container">
<div><a href="https://github.com/naiba/nezha" target="_blank">Powered by {{tr "NezhaMonitoring"}} · {{.Version}}</a>
<p>&copy; <span id="copyright-date">
<script>document.getElementById('copyright-date').appendChild(document.createTextNode(new Date().getFullYear()))</script>
</span> · <a href="https://blog.jackiesung.com" target="_blank">Theme designed by Jackie Sung</a>
</p>
</div>
</div>
</footer>
<script src="/static/theme-daynight/js/main.js?v202102012266"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/limonte-sweetalert2/11.4.4/sweetalert2.all.min.js"></script>
<script>
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
const initData = JSON.parse('{{.Servers}}').servers;
let MaxTCPPingValue = {{.MaxTCPPingValue}};
if (MaxTCPPingValue == null) {
MaxTCPPingValue = 300;
}
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('monitor-info-container'));
// 使用刚指定的配置项和数据显示图表。
var statusCards = new Vue({
el: '#network',
delimiters: ['@#', '#@'],
data: {
servers: initData,
cache: [],
option: {
tooltip: {
trigger: 'axis',
position: function (pt) {
return [pt[0], '10%'];
},
formatter: function(params){
let result = params[0].axisValueLabel + "<br />";
params.forEach(function(item){
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
})
return result;
},
confine: true,
transitionDuration: 0
},
title: {
left: 'center',
text: "",
textStyle: {}
},
legend: {
top: '5%',
data: [],
textStyle: {
fontSize: 14
}
},
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
dataZoom: [
{
start: 94,
end: 100
}
],
xAxis: {
type: 'time',
boundaryGap: false
},
yAxis: {
type: 'value',
boundaryGap: [0, '100%']
},
series: [],
}
},
mounted() {
this.DarkMode();
this.parseMonitorInfo(monitorInfo);
},
methods: {
DarkMode() {
const hour = new Date(Date.now()).getHours()
if (hour > 17 || hour < 4) {
document.querySelector("input[name=theme]").checked = true;
document.getElementsByTagName("BODY")[0].setAttribute('data-theme', 'dark');
document.getElementById("monitor-info-container").style.backgroundColor = "#1E1E1E";
}
},
redirectNetwork(id) {
this.getMonitorHistory(id)
.then(function(monitorInfo) {
var vm = network.__vue__;
vm.parseMonitorInfo(monitorInfo);
})
.catch(function(error){
window.location.href = "/404";
})
},
getMonitorHistory(id) {
return $.ajax({
url: "/api/v1/monitor/"+id,
method: "GET"
});
},
parseMonitorInfo(monitorInfo) {
let tSeries = [];
let tLegendData = [];
for (let i = 0; i < monitorInfo.result.length; i++) {
let loss = 0;
let data = [];
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
avgDelay = monitorInfo.result[i].avg_delay[j];
if (avgDelay > 0.9 * MaxTCPPingValue) {
loss += 1
}
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
}
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
tLegendData.push(legendName);
tSeries.push({
name: legendName,
type: 'line',
smooth: true,
symbol: 'none',
data: data
});
}
this.option.title.text = monitorInfo.result[0].server_name;
this.option.series = tSeries;
this.option.legend.data = tLegendData;
const hour = new Date(Date.now()).getHours()
if (hour > 17 || hour < 4) {
this.option.legend.textStyle.color = "#F1F1F2";
this.option.title.textStyle.color = "#ccc";
}
myChart.clear();
myChart.setOption(this.option);
}
}
});
</script>
</body>
<style>
#network {
display: grid;
/*grid-template-columns: repeat(5, 1fr);*/
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
grid-gap: 1em;
width: 100%;
width: calc(100vw - 6em);
max-width: 1400px;
margin: 1em auto;
align-content: start;
}
#monitor-info-container{
margin: 0em auto;
align-content: start;
background-color: #F1F1F2;
}
</style>
</html>
{{end}}
+6 -3
View File
@@ -8,9 +8,9 @@
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" /> <link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link rel="stylesheet" href="/static/theme-daynight/css/service.css?v202108042286" /> <link rel="stylesheet" href="/static/theme-daynight/css/service.css?v202108042286" />
<link rel="stylesheet" href="https://cdn.staticfile.org/mdui/1.0.2/css/mdui.min.css"/> <link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
<link href="https://cdn.staticfile.org/font-awesome/6.0.0/css/all.min.css" rel="stylesheet" /> <link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-awesome/6.0.0/css/all.min.css" rel="stylesheet" />
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
{{if ts .CustomCode}} {{if ts .CustomCode}}
{{.CustomCode|safe}} {{.CustomCode|safe}}
@@ -43,6 +43,7 @@
<ul> <ul>
<li><a href="/">{{tr "Home"}}</a></li> <li><a href="/">{{tr "Home"}}</a></li>
<li><a href="/service">{{tr "Services"}}</a></li> <li><a href="/service">{{tr "Services"}}</a></li>
<li><a href="/network">{{tr "NetworkSpiter"}}</a></li>
{{if .Admin}} {{if .Admin}}
<li><a href="/server">{{tr "AdminPanel"}}</a></li> <li><a href="/server">{{tr "AdminPanel"}}</a></li>
{{else}} {{else}}
@@ -60,6 +61,7 @@
</section> </section>
<section class="monitor-container"> <section class="monitor-container">
{{range $service := .Services}} {{range $service := .Services}}
{{if $service.Monitor.EnableShowInService}}
<section class="monitor-info-container"> <section class="monitor-info-container">
<div class="monitor-state"> <div class="monitor-state">
<span class="monitor-state-dot {{className (divU64 $service.CurrentUp (addU64 $service.CurrentUp $service.CurrentDown))}}"></span> <span class="monitor-state-dot {{className (divU64 $service.CurrentUp (addU64 $service.CurrentUp $service.CurrentDown))}}"></span>
@@ -81,6 +83,7 @@
</div> </div>
</section> </section>
{{end}} {{end}}
{{end}}
</section> </section>
{{if .CycleTransferStats}} {{if .CycleTransferStats}}
<h2 class="mdui-m-t-5 mdui-text-center">{{tr "CycleTransferStats"}}</h2> <h2 class="mdui-m-t-5 mdui-text-center">{{tr "CycleTransferStats"}}</h2>
+219
View File
@@ -0,0 +1,219 @@
{{define "theme-default/network"}}
{{template "common/header" .}}
{{if ts .CustomCode}}
{{.CustomCode|safe}}
{{end}}
{{template "common/menu" .}}
<div class="nb-container" id="app">
<div class="ui container">
<div class="service-status">
<table class="ui celled table">
<button class="ui nezha-primary-btn button"
v-for="server in servers"
style="margin-top: 3px"
@click="redirectNetwork(server.ID)">
@#server.Name#@ &nbsp;<i :class="server.Host.CountryCode + ' flag'"></i>
</button>
</table>
</div>
</div>
<div class="ui container" style="max-width: 95vw">
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
</div>
</div>
{{template "common/footer" .}}
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
<script>
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
const initData = JSON.parse('{{.Servers}}').servers;
let MaxTCPPingValue = {{.MaxTCPPingValue}};
if (MaxTCPPingValue == null) {
MaxTCPPingValue = 300;
}
new Vue({
el: '#app',
delimiters: ['@#', '#@'],
data: {
servers: initData,
option: {
tooltip: {
trigger: 'axis',
position: function (pt) {
return [pt[0], '10%'];
},
formatter: function(params){
let result = params[0].axisValueLabel + "<br />";
params.forEach(function(item){
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
})
return result;
},
confine: true,
transitionDuration: 0
},
title: {
left: 'center',
text: "",
textStyle: {}
},
legend: {
top: '5%',
data: [],
textStyle: {
fontSize: 14
}
},
backgroundColor: 'rgba(255, 255, 255, 0.8)',
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
dataZoom: [
{
start: 94,
end: 100
}
],
xAxis: {
type: 'time',
boundaryGap: false
},
yAxis: {
type: 'value',
boundaryGap: [0, '100%']
},
series: [],
},
chartOnOff: true,
},
mounted() {
this.renderChart();
this.parseMonitorInfo(monitorInfo);
},
methods: {
getFontLogoClass(str) {
if (["almalinux",
"alpine",
"aosc",
"apple",
"archlinux",
"archlabs",
"artix",
"budgie",
"centos",
"coreos",
"debian",
"deepin",
"devuan",
"docker",
"elementary",
"fedora",
"ferris",
"flathub",
"freebsd",
"gentoo",
"gnu-guix",
"illumos",
"kali-linux",
"linuxmint",
"mageia",
"mandriva",
"manjaro",
"nixos",
"openbsd",
"opensuse",
"pop-os",
"raspberry-pi",
"redhat",
"rocky-linux",
"sabayon",
"slackware",
"snappy",
"solus",
"tux",
"ubuntu",
"void",
"zorin"].indexOf(str)
> -1) {
return str;
}
if (['openwrt', 'linux', "immortalwrt"].indexOf(str) > -1) {
return 'tux';
}
if (str == 'amazon') {
return 'redhat';
}
if (str == 'arch') {
return 'archlinux';
}
return '';
},
redirectNetwork(id) {
this.getMonitorHistory(id)
.then(function(monitorInfo) {
var vm = app.__vue__;
vm.parseMonitorInfo(monitorInfo);
})
.catch(function(error){
window.location.href = "/404";
})
},
getMonitorHistory(id) {
return $.ajax({
url: "/api/v1/monitor/"+id,
method: "GET"
});
},
parseMonitorInfo(monitorInfo) {
let tSeries = [];
let tLegendData = [];
for (let i = 0; i < monitorInfo.result.length; i++) {
let loss = 0;
let data = [];
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
avgDelay = monitorInfo.result[i].avg_delay[j];
if (avgDelay > 0.9 * MaxTCPPingValue) {
loss += 1
}
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
}
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
tLegendData.push(legendName);
tSeries.push({
name: legendName,
type: 'line',
smooth: true,
symbol: 'none',
data: data
});
}
this.option.title.text = monitorInfo.result[0].server_name;
this.option.series = tSeries;
this.option.legend.data = tLegendData;
this.myChart.clear();
this.myChart.setOption(this.option);
},
isWindowsPlatform(str) {
return str.includes('Windows')
},
renderChart() {
this.myChart = echarts.init(this.$refs.chartDom);
this.myChart.setOption(this.option);
},
},
beforeDestroy() {
this.myChart.dispose();
this.myChart = null;
},
});
</script>
{{end}}
+2 -1
View File
@@ -18,6 +18,7 @@
</thead> </thead>
<tbody> <tbody>
{{range $service := .Services}} {{range $service := .Services}}
{{if $service.Monitor.EnableShowInService}}
<tr> <tr>
<td class="ui center aligned">{{$service.Monitor.Name}}</td> <td class="ui center aligned">{{$service.Monitor.Name}}</td>
<td class="ui center aligned"> <td class="ui center aligned">
@@ -33,7 +34,7 @@
class="delay-today {{className (divU64 $service.CurrentUp (addU64 $service.CurrentUp $service.CurrentDown))}}"></i> class="delay-today {{className (divU64 $service.CurrentUp (addU64 $service.CurrentUp $service.CurrentDown))}}"></i>
{{statusName (divU64 $service.CurrentUp (addU64 $service.CurrentUp $service.CurrentDown))}} {{statusName (divU64 $service.CurrentUp (addU64 $service.CurrentUp $service.CurrentDown))}}
</td> </td>
</tr> {{end}} </tr> {{end}} {{end}}
</tbody> </tbody>
</table> </table>
+5 -5
View File
@@ -14,8 +14,8 @@
<meta http-equiv="Content-Language" content="zh" /> <meta http-equiv="Content-Language" content="zh" />
<!-- Styles --> <!-- Styles -->
<link rel="stylesheet" type="text/css" href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css"> <link rel="stylesheet" type="text/css" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.css">
<link rel="stylesheet" type="text/css" href="https://cdn.staticfile.org/font-awesome/6.0.0/css/all.min.css"> <link rel="stylesheet" type="text/css" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css"> <link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
<link rel="stylesheet" href="/static/theme-hotaru/css/core.css?v202012121912" type="text/css"> <link rel="stylesheet" href="/static/theme-hotaru/css/core.css?v202012121912" type="text/css">
<link rel="stylesheet" href="/static/theme-hotaru/css/main.css?v202101171153" type="text/css"> <link rel="stylesheet" href="/static/theme-hotaru/css/main.css?v202101171153" type="text/css">
@@ -141,10 +141,10 @@
<a href="/server">{{tr "AdminPanel"}}</a> <a href="/server">{{tr "AdminPanel"}}</a>
</p> </p>
</footer> </footer>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script> <script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script> <script>
const initData = JSON.parse('{{.Servers}}').servers; const initData = JSON.parse('{{.Servers}}').servers;
var statusCards = new Vue({ var statusCards = new Vue({
+5 -5
View File
@@ -9,7 +9,7 @@
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" /> <link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<!-- MDUI CSS --> <!-- MDUI CSS -->
<link rel="stylesheet" href="https://cdn.staticfile.org/mdui/1.0.2/css/mdui.min.css"/> <link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css"> <link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
<style> <style>
.mdui-table td, .mdui-table th{padding: 6px;} .mdui-table td, .mdui-table th{padding: 6px;}
@@ -37,7 +37,7 @@
<div class="mdui-col" v-for='server in servers' :id="server.ID"> <div class="mdui-col" v-for='server in servers' :id="server.ID">
<div :class="'mdui-card mt' + (server.live?'':' offline')"> <div :class="'mdui-card mt' + (server.live?'':' offline')">
<div class="mdui-card-header"> <div class="mdui-card-header">
<img class="mdui-card-header-avatar" :src="'https://cdn.staticfile.org/flag-icon-css/4.1.5/flags/1x1/' + (server.Host.CountryCode?server.Host.CountryCode:'cn') + '.svg'"/> <img class="mdui-card-header-avatar" :src="'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/flag-icon-css/4.1.5/flags/1x1/' + (server.Host.CountryCode?server.Host.CountryCode:'cn') + '.svg'"/>
<div class="mdui-card-header-title">@#server.Name#@</div> <div class="mdui-card-header-title">@#server.Name#@</div>
<div class="mdui-card-header-subtitle">@#server.Host.CountryCode.toUpperCase()#@ | @#server.Host.Platform#@ @#server.Host.PlatformVersion#@</div> <div class="mdui-card-header-subtitle">@#server.Host.CountryCode.toUpperCase()#@ | @#server.Host.Platform#@ @#server.Host.PlatformVersion#@</div>
</div> </div>
@@ -153,9 +153,9 @@
{{template "theme-mdui/footer" .}} {{template "theme-mdui/footer" .}}
<script src="/static/theme-mdui/mdui.js"></script> <script src="/static/theme-mdui/mdui.js"></script>
<script src="https://cdn.staticfile.org/mdui/1.0.2/js/mdui.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/js/mdui.min.js"></script>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script> <script>
var container=document.querySelector("#container"); var container=document.querySelector("#container");
+4 -4
View File
@@ -9,7 +9,7 @@
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" /> <link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<!-- MDUI CSS --> <!-- MDUI CSS -->
<link rel="stylesheet" href="https://cdn.staticfile.org/mdui/1.0.2/css/mdui.min.css"/> <link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css"> <link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
{{if ts .CustomCode}} {{if ts .CustomCode}}
{{.CustomCode|safe}} {{.CustomCode|safe}}
@@ -116,9 +116,9 @@
{{template "theme-mdui/footer" .}} {{template "theme-mdui/footer" .}}
<script src="/static/theme-mdui/mdui.js"></script> <script src="/static/theme-mdui/mdui.js"></script>
<script src="https://cdn.staticfile.org/mdui/1.0.2/js/mdui.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/js/mdui.min.js"></script>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script> <script>
var container=document.querySelector("#container"); var container=document.querySelector("#container");
container.style.minHeight=window.innerHeight-document.body.clientHeight+container.clientHeight+'px'; container.style.minHeight=window.innerHeight-document.body.clientHeight+container.clientHeight+'px';
+3 -3
View File
@@ -8,7 +8,7 @@
<title>{{.Title}}</title> <title>{{.Title}}</title>
<!-- MDUI CSS --> <!-- MDUI CSS -->
<link rel="stylesheet" href="https://cdn.staticfile.org/mdui/1.0.2/css/mdui.min.css"/> <link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css"> <link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
{{if ts .CustomCode}} {{if ts .CustomCode}}
{{.CustomCode|safe}} {{.CustomCode|safe}}
@@ -34,8 +34,8 @@
</div> </div>
</div> </div>
<script src="https://cdn.staticfile.org/mdui/1.0.2/js/mdui.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/js/mdui.min.js"></script>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.min.js"></script>
<script> <script>
var $input = mdui.$('#container').find('.mdui-textfield-input'); var $input = mdui.$('#container').find('.mdui-textfield-input');
var $dialog = new mdui.Dialog(mdui.$('.mdui-dialog')); var $dialog = new mdui.Dialog(mdui.$('.mdui-dialog'));
+1 -2
View File
@@ -1,8 +1,7 @@
{{define "theme-server-status/content-footer"}} {{define "theme-server-status/content-footer"}}
<footer class="container" style="padding-bottom: 2rem;"> <footer class="container" style="padding-bottom: 2rem;">
<p style="text-align: center; font-size: 10px;"> <p style="text-align: center; font-size: 10px;">
{{ .Conf.Site.Brand }} | Theme <a href="https://github.com/cppla/ServerStatus">ServerStatus</a> | Powered by <a {{ .Conf.Site.Brand }} | Theme <a target="_blank" href="https://github.com/cppla/ServerStatus">ServerStatus</a> | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}
href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}
</p> </p>
</footer> </footer>
{{end}} {{end}}
@@ -18,6 +18,7 @@
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li><a href="/">{{tr "Home" }}</a></li> <li><a href="/">{{tr "Home" }}</a></li>
<li><a href="/service">{{tr "Services" }}</a></li> <li><a href="/service">{{tr "Services" }}</a></li>
<li><a href="/network">{{tr "NetworkSpiter" }}</a></li>
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li class="dropdown"> <li class="dropdown">
@@ -45,3 +46,4 @@
</div> </div>
</div> </div>
{{end}} {{end}}
+8 -8
View File
@@ -1,7 +1,6 @@
{{define "theme-server-status/header"}} {{define "theme-server-status/header"}}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{.Conf.Language}}"> <html lang="{{.Conf.Language}}">
<head> <head>
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
<meta charset="utf-8"> <meta charset="utf-8">
@@ -9,11 +8,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/theme-server-status/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/theme-server-status/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/theme-server-status/css/bootstrap-theme.min.css"> <link rel="stylesheet" href="/static/theme-server-status/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20231109"> <link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20231207">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css"> <link rel="stylesheet" href="/static/theme-server-status/css/dark.css">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css"> <link rel="stylesheet" href="/static/theme-server-status/css/light.css">
<link href="https://cdn.staticfile.org/font-logos/0.17/font-logos.min.css" type="text/css" rel="stylesheet" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
<link rel="stylesheet" type="text/css" href="https://cdn.staticfile.org/semantic-ui/2.4.1/semantic.min.css"> <link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-logos/0.17/font-logos.min.css">
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/[email protected]/css/flag-icons.min.css">
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" /> <link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]> <!--[if lt IE 9]>
@@ -23,12 +24,11 @@
{{if ts .CustomCode}} {{if ts .CustomCode}}
{{.CustomCode|safe}} {{.CustomCode|safe}}
{{end}} {{end}}
<script src="/static/theme-server-status/js/jquery.min.js"></script> <script src="/static/theme-server-status/js/jquery.min.js"></script>
<script src="/static/theme-server-status/js/bootstrap.min.js"></script> <script src="/static/theme-server-status/js/bootstrap.min.js"></script>
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="/static/theme-server-status/js/mixin.js"></script> <script src="/static/theme-server-status/js/mixin.js"></script>
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
</head> </head>
<body> <body>
{{end}} {{end}}
+79 -60
View File
@@ -2,26 +2,30 @@
{{template "theme-server-status/header" .}} {{template "theme-server-status/header" .}}
<div id="app"> <div id="app">
{{template "theme-server-status/content-nav" .}} {{template "theme-server-status/content-nav" .}}
<div class="container table-responsive content" style="max-width: 95vw"> <div class="container table-responsive content" style="max-width: 95vw" v-for="group in nodes">
<table class="table table-striped table-condensed table-hover"> <table class="table table-striped table-condensed table-hover">
<thead> <thead>
<tr> <tr>
<th class="node-cell status center">🍀 {{tr "Status"}}</th> <th class="node-group-tag" colspan="16" style="border:none;">@#(group.Tag!==''?group.Tag:'{{tr "Default"}}')#@</th>
<th class="node-cell name">🚀 {{tr "Name"}}</th> </th>
<th class="node-cell os">🗂 {{tr "Platform"}}</th> </tr>
<th class="node-cell location center">🌍 {{tr "Location"}}</th> <tr>
<th class="node-cell uptime center">⏱️ {{tr "Uptime"}}</th> <th class="node-cell status center">{{tr "Status"}}</th>
<th class="node-cell load center">📋{{tr "Load"}}</th> <th class="node-cell name center">{{tr "Name"}}</th>
<th class="node-cell network center">🚦 {{tr "NetSpeed"}}↓|↑</th> <th class="node-cell os center">{{tr "Platform"}}</th>
<th class="node-cell traffic center">📊 {{tr "NetTransfer"}}↓|↑</th> <th class="node-cell location center">{{tr "Location"}}</th>
<th class="node-cell cpu center">🎯 {{tr "CpuUsed"}}</th> <th class="node-cell uptime center">{{tr "Uptime"}}</th>
<th class="node-cell ram center">⚡ {{tr "MemUsed"}}</th> <th class="node-cell load center">{{tr "Load"}}</th>
<th class="node-cell hdd center">💾 {{tr "DiskUsed"}}</th> <th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
<th class="node-cell traffic center">{{tr "NetTransfer"}}↓|↑</th>
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
<th class="node-cell ram center">{{tr "MemUsed"}}</th>
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
</tr> </tr>
</thead> </thead>
<tbody id="servers"> <tbody id="servers">
<template v-for="(node,index) in nodes"> <template v-for="(node,index) in group.data">
<tr :id="'r'+index" data-toggle="collapse" :data-target="'#rt'+index" class="accordion-toggle" <tr :id="'r'+node.ID" data-toggle="collapse" :data-target="'#rt'+node.ID" class="accordion-toggle"
:class="index % 2 === 0 ? 'odd': 'even'"> :class="index % 2 === 0 ? 'odd': 'even'">
<td class="node-cell status center"> <td class="node-cell status center">
<div class="status-container"> <div class="status-container">
@@ -29,36 +33,36 @@
<div v-else class="status-icon offline"></div> <div v-else class="status-icon offline"></div>
</div> </div>
</td> </td>
<td class="node-cell name">@#node.name#@</td> <td class="node-cell name center">@#node.name#@</td>
<td class="node-cell os"> <td class="node-cell os center">
<i v-if='node.os == "darwin"' class="apple icon"></i> <i v-if='node.os == "darwin"' class="apple icon"></i>
<i v-else-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i> <i v-else-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i> <i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
@#node.os#@ <span class="node-cell-os-text">@#node.os#@</span>
</td> </td>
<td style="text-align: center;" class="node-cell location"> <td style="text-align: center;" class="node-cell location">
<i :class="node.location + ' flag'"></i>&nbsp; <i :class="'fi fi-' + node.location"></i>
<span class="text-uppercase">@#node.location#@</span> <span class="node-cell-location-text text-uppercase">&nbsp;@#node.location#@</span>
</td> </td>
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td> <td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
<td style="text-align: center;" class="node-cell load">@#node.load#@</td> <td style="text-align: center;" class="node-cell load">@#node.load#@</td>
<td style="text-align: center;" class="node-cell network">@#node.network#@</td> <td style="text-align: center;" class="node-cell network">@#node.network#@</td>
<td style="text-align: center;" class="node-cell traffic">@#node.traffic#@</td> <td style="text-align: center;" class="node-cell traffic">@#node.traffic#@</td>
<td class="node-cell cpu"> <td class="node-cell cpu">
<div class="progress"> <div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<div :style="node.cpu.style" :class="node.cpu.class"><small>@#node.cpu.percent#@%</small> <div :style="node.cpu.style" :class="node.cpu.class"><small>@#node.cpu.percent#@%</small>
</div> </div>
</div> </div>
</td> </td>
<td class="node-cell memory"> <td class="node-cell memory">
<div class="progress"> <div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<div :style="node.memory.style" :class="node.memory.class"> <div :style="node.memory.style" :class="node.memory.class">
<small>@#node.memory.percent#@%</small> <small>@#node.memory.percent#@%</small>
</div> </div>
</div> </div>
</td> </td>
<td class="node-cell hdd"> <td class="node-cell hdd">
<div class="progress"> <div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<div :style="node.hdd.style" :class="node.hdd.class"><small>@#node.hdd.percent#@%</small> <div :style="node.hdd.style" :class="node.hdd.class"><small>@#node.hdd.percent#@%</small>
</div> </div>
</div> </div>
@@ -66,24 +70,16 @@
</tr> </tr>
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'"> <tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
<td colspan="16"> <td colspan="16">
<div class="accordian-body collapse" :id="'rt'+index"> <div class="accordian-body collapse" :id="'rt'+node.ID">
<div style="display: flex;align-items: center;justify-content: center;flex-direction: column;"> <div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
<div style="display: flex;align-items: flex-start;justify-content: center;flex-direction: column; width: 450px;max-width: 90vw">
<span class="node-cell-expand"> <span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Platform"}}:</span> <span class="node-cell-expand-label">{{tr "Platform"}}:</span>
@#node.host.Platform#@ @#node.host.Platform#@-@#node.host.PlatformVersion#@
</span> [<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@]
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Location"}}:</span>
<i :class="node.location + ' flag'"></i>
</span> </span>
<span class="node-cell-expand" v-if="node.host.CPU"> <span class="node-cell-expand" v-if="node.host.CPU">
<span class="node-cell-expand-label">CPU:</span> <span class="node-cell-expand-label">CPU:</span>
@#node.host.CPU.join(",")#@ @#node.host.CPU.join(",")#@
</span>
<span class="node-cell-expand load">
<span class="node-cell-expand-label">{{tr "Load"}}:</span>
@#toFixed2(node.state.Load1)#@ / @#toFixed2(node.state.Load5)#@ /@#toFixed2(node.state.Load15)#@
</span> </span>
<span class="node-cell-expand"> <span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "DiskUsed"}}:</span> <span class="node-cell-expand-label">{{tr "DiskUsed"}}:</span>
@@ -93,6 +89,11 @@
<span class="node-cell-expand-label">{{tr "MemUsed"}}:</span> <span class="node-cell-expand-label">{{tr "MemUsed"}}:</span>
@#formatByteSize(node.state.MemUsed)#@ / @#formatByteSize(node.host.MemTotal)#@(@#toFixed2(node.state.MemUsed / node.host.MemTotal * 100)#@%) @#formatByteSize(node.state.MemUsed)#@ / @#formatByteSize(node.host.MemTotal)#@(@#toFixed2(node.state.MemUsed / node.host.MemTotal * 100)#@%)
</span> </span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "SwapUsed"}}:</span>
@#formatByteSize(node.state.SwapUsed)#@ / @#formatByteSize(node.host.SwapTotal)#@
<span v-if="node.host.SwapTotal">(@#toFixed2(node.state.SwapUsed / node.host.SwapTotal * 100)#@%)</span>
</span>
<span class="node-cell-expand"> <span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span> <span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span>
<i class="arrow alternate circle down outline icon" <i class="arrow alternate circle down outline icon"
@@ -100,22 +101,9 @@
<i class="arrow alternate circle up outline icon" <i class="arrow alternate circle up outline icon"
style="margin: 0"></i>@#formatByteSize(node.state.NetOutTransfer)#@ style="margin: 0"></i>@#formatByteSize(node.state.NetOutTransfer)#@
</span> </span>
<span class="node-cell-expand"> <span class="node-cell-expand load">
<span class="node-cell-expand-label">{{tr "SwapUsed"}}:</span> <span class="node-cell-expand-label">{{tr "Load"}}:</span>
@#formatByteSize(node.state.SwapUsed)#@ / @#formatByteSize(node.host.SwapTotal)#@ @#toFixed2(node.state.Load1)#@ / @#toFixed2(node.state.Load5)#@ / @#toFixed2(node.state.Load15)#@
<span v-if="node.host.SwapTotal">(@#toFixed2(node.state.SwapUsed / node.host.SwapTotal * 100)#@%)</span>
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "BootTime"}}:</span>
@#formatTimestamp(node.host.BootTime)#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "LastActive"}}:</span>
@#new Date(node.lastActive).toLocaleString()#@
</span>
<span class="node-cell-expand" v-if="node.host.Virtualization">
<span class="node-cell-expand-label">{{tr "Virtualization"}}:</span>
@#node.host.Virtualization#@
</span> </span>
<span class="node-cell-expand"> <span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "ProcessCount"}}:</span> <span class="node-cell-expand-label">{{tr "ProcessCount"}}:</span>
@@ -125,19 +113,29 @@
<span class="node-cell-expand-label">{{tr "ConnCount"}}:</span> <span class="node-cell-expand-label">{{tr "ConnCount"}}:</span>
TCP @#node.state.TcpConnCount#@ / UDP @#node.state.UdpConnCount#@ TCP @#node.state.TcpConnCount#@ / UDP @#node.state.UdpConnCount#@
</span> </span>
</div> <span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "BootTime"}}:</span>
@#formatTimestamp(node.host.BootTime)#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "LastActive"}}:</span>
@#new Date(node.lastActive).toLocaleString()#@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Version"}}:</span>
@#node.host.Version#@
</span>
</div> </div>
</div> </div>
</td> </td>
</tr> </tr>
</template> </template>
</tbody> </tbody>
</table> </table>
<br/>
</div> </div>
{{template "theme-server-status/content-footer" .}} {{template "theme-server-status/content-footer" .}}
</div> </div>
<script> <script>
new Vue({ new Vue({
el: '#app', el: '#app',
@@ -148,7 +146,7 @@
mixins: [mixinsVue], mixins: [mixinsVue],
created() { created() {
const initData = JSON.parse('{{.Servers}}').servers; const initData = JSON.parse('{{.Servers}}').servers;
this.nodes = this.handleNodes(initData); this.nodes = groupingData(this.handleNodes(initData),"Tag");
this.initTheme() this.initTheme()
}, },
mounted() { mounted() {
@@ -233,17 +231,17 @@
return x !== "NaN undefined" ? x : '0B' return x !== "NaN undefined" ? x : '0B'
}, },
formatPercent(live, used, total) { formatPercent(live, used, total) {
const percent = live ? (this.toFixed2(used / total * 100) || 0) : 1 const percent = live ? (this.toFixed2(used / total * 100) || 0) : 0
return this.formatPercents(percent) return this.formatPercents(percent)
}, },
formatPercents(percent) { formatPercents(percent) {
if (percent <= 0) { if (percent <= 0) {
percent = 1; percent = 0;
} }
if (!this.cache[percent]) { if (!this.cache[percent]) {
this.cache[percent] = { this.cache[percent] = {
class: 'progress-bar progress-bar-success', class: 'progress-bar progress-bar-success',
style: `width: ${parseInt(percent)}%`, style: `width: ${parseInt(percent)+1}%`,
percent, percent,
} }
if (percent < 80) { if (percent < 80) {
@@ -282,7 +280,7 @@
data.servers[i].live = data.now - lastActive <= 10 * 1000; data.servers[i].live = data.now - lastActive <= 10 * 1000;
} }
} }
this.nodes = this.handleNodes(data.servers) this.nodes = groupingData(this.handleNodes(data.servers),"Tag");
} }
ws.onclose = () => { ws.onclose = () => {
setTimeout(function () { setTimeout(function () {
@@ -296,13 +294,14 @@
handleNodes(servers) { handleNodes(servers) {
let nodes = [] let nodes = []
servers?.forEach(server => { servers?.forEach(server => {
let platform = server.Host.Platform let platform = server.Host.Platform;
if (this.isWindowsPlatform(server.Host.Platform)) { if (this.isWindowsPlatform(platform)) {
platform = "windows" platform = "windows"
}else if (platform === "immortalwrt") { }else if (platform === "immortalwrt") {
platform = "openwrt" platform = "openwrt"
} }
let node = { let node = {
ID: server.ID,
name: server.Name, name: server.Name,
os: platform, os: platform,
location: server.Host.CountryCode, location: server.Host.CountryCode,
@@ -317,6 +316,7 @@
state: server.State, state: server.State,
host: server.Host, host: server.Host,
lastActive: server.LastActive, lastActive: server.LastActive,
Tag: server.Tag,
} }
nodes.push(node) nodes.push(node)
}) })
@@ -328,6 +328,25 @@
} }
}) })
function groupingData(data, field) {
let map = new Map();
let dest = [];
data.forEach(item => {
if (!map.has(item[field])) {
dest.push({
[field]: item[field],
data: [item]
});
map.set(item[field], item);
} else {
dest.find(dItem => dItem[field] === item[field]).data.push(item);
}
});
return dest;
}
</script> </script>
{{template "theme-server-status/footer" .}} {{template "theme-server-status/footer" .}}
{{end}} {{end}}
+217
View File
@@ -0,0 +1,217 @@
{{define "theme-server-status/network"}}
{{template "theme-server-status/header" .}}
<div id="app">
{{template "theme-server-status/content-nav" .}}
<div class="container table-responsive content" style="max-width: 95vw">
<table class="table table-striped table-condensed table-hover">
<button class="ui nezha-primary-btn button"
v-for="server in servers"
style="margin-top: 3px"
@click="redirectNetwork(server.ID)">
@#server.Name#@&nbsp;&nbsp;<i :class="'fi fi-' + server.Host.CountryCode"></i><span class="node-cell-location-text text-uppercase">&nbsp;&nbsp;@#server.Host.CountryCode#@</span>
</button>
</table>
</div>
<div class="container table-responsive content" style="max-width: 95vw">
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
</div>
{{template "theme-server-status/content-footer" .}}
</div>
<script>
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
const initData = JSON.parse('{{.Servers}}').servers;
let MaxTCPPingValue = {{.MaxTCPPingValue}};
if (MaxTCPPingValue == null) {
MaxTCPPingValue = 300;
}
new Vue({
el: '#app',
delimiters: ['@#', '#@'],
data: {
servers: initData,
option: {
tooltip: {
trigger: 'axis',
position: function (pt) {
return [pt[0], '10%'];
},
formatter: function(params){
let result = params[0].axisValueLabel + "<br />";
params.forEach(function(item){
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
})
return result;
},
confine: true,
transitionDuration: 0
},
title: {
left: 'center',
text: "",
textStyle: {}
},
legend: {
top: '5%',
data: [],
textStyle: {
fontSize: 14
}
},
backgroundColor: 'rgba(255, 255, 255, 0.8)',
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
dataZoom: [
{
start: 94,
end: 100
}
],
xAxis: {
type: 'time',
boundaryGap: false
},
yAxis: {
type: 'value',
boundaryGap: [0, '100%']
},
series: [],
},
chartOnOff: true,
},
mixins: [mixinsVue],
created() {
this.initTheme();
},
mounted() {
this.renderChart();
this.parseMonitorInfo(monitorInfo);
},
methods: {
getFontLogoClass(str) {
if (["almalinux",
"alpine",
"aosc",
"apple",
"archlinux",
"archlabs",
"artix",
"budgie",
"centos",
"coreos",
"debian",
"deepin",
"devuan",
"docker",
"elementary",
"fedora",
"ferris",
"flathub",
"freebsd",
"gentoo",
"gnu-guix",
"illumos",
"kali-linux",
"linuxmint",
"mageia",
"mandriva",
"manjaro",
"nixos",
"openbsd",
"opensuse",
"pop-os",
"raspberry-pi",
"redhat",
"rocky-linux",
"sabayon",
"slackware",
"snappy",
"solus",
"tux",
"ubuntu",
"void",
"zorin"].indexOf(str)
> -1) {
return str;
}
if (['openwrt', 'linux', "immortalwrt"].indexOf(str) > -1) {
return 'tux';
}
if (str == 'amazon') {
return 'redhat';
}
if (str == 'arch') {
return 'archlinux';
}
return '';
},
redirectNetwork(id) {
this.getMonitorHistory(id)
.then(function(monitorInfo) {
var vm = app.__vue__;
vm.parseMonitorInfo(monitorInfo);
})
.catch(function(error){
window.location.href = "/404";
})
},
getMonitorHistory(id) {
return $.ajax({
url: "/api/v1/monitor/"+id,
method: "GET"
});
},
parseMonitorInfo(monitorInfo) {
let tSeries = [];
let tLegendData = [];
for (let i = 0; i < monitorInfo.result.length; i++) {
let loss = 0;
let data = [];
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
avgDelay = monitorInfo.result[i].avg_delay[j];
if (avgDelay > 0.9 * MaxTCPPingValue) {
loss += 1
}
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
}
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
tLegendData.push(legendName);
tSeries.push({
name: legendName,
type: 'line',
smooth: true,
symbol: 'none',
data: data
});
}
this.option.title.text = monitorInfo.result[0].server_name;
this.option.series = tSeries;
this.option.legend.data = tLegendData;
this.myChart.clear();
this.myChart.setOption(this.option);
},
isWindowsPlatform(str) {
return str.includes('Windows')
},
renderChart() {
this.myChart = echarts.init(this.$refs.chartDom);
this.myChart.setOption(this.option);
},
},
beforeDestroy() {
this.myChart.dispose();
this.myChart = null;
},
});
</script>
{{template "theme-server-status/footer" .}}
{{end}}
+10 -9
View File
@@ -6,23 +6,23 @@
<table class="table table-striped table-condensed service-status"> <table class="table table-striped table-condensed service-status">
<thead> <thead>
<tr> <tr>
<th class="node-cell" style="min-width: 60px">🍀 {{tr "Status"}}</th> <th class="node-cell center" style="min-width:60px">{{tr "Status"}}</th>
<th class="node-cell" style="min-width: 60px">🚀 {{tr "Name"}}</th> <th class="node-cell center" style="min-width:50px">{{tr "Name"}}</th>
<th class="node-cell center">🗂 {{tr "Details"}}</th> <th class="node-cell center">{{tr "Details"}}</th>
<th class="node-cell center" style="min-width: 80px">⚡️{{tr "AverageLatency"}}</th> <th class="node-cell center" style="min-width:80px">{{tr "AverageLatency"}}</th>
<th class="node-cell center" style="min-width: 80px">⏱️ {{tr "30DaysOnline"}}</th> <th class="node-cell center" style="min-width:80px">{{tr "30DaysOnline"}}</th>
</tr> </tr>
</thead> </thead>
<tbody id="servers"> <tbody id="servers">
<template v-for="service in services"> <template v-for="service in services">
<tr> <tr v-if="service.enableShow === 'true'">
<td style="text-align: left" class="node-cell"> <td class="node-cell center">
<div class="delay-today"> <div class="delay-today">
<i class="delay-today" :class="service.health.className"></i> <i class="delay-today" :class="service.health.className"></i>
@#service.health.text#@ @#service.health.text#@
</div> </div>
</td> </td>
<td class="node-cell">@#service.name#@</td> <td class="node-cell center">@#service.name#@</td>
<td class="node-cell center"> <td class="node-cell center">
<template v-for="(item,index) in service.dayDetail"> <template v-for="(item,index) in service.dayDetail">
<div class="service-day-status-icon" :class="item.className" <div class="service-day-status-icon" :class="item.className"
@@ -44,7 +44,7 @@
</table> </table>
</div> </div>
<div class="container" style="margin-bottom: 20px;padding:unset;max-width: 95vw"> <div class="container" style="padding:unset;max-width: 95vw">
{{if .CycleTransferStats}} {{if .CycleTransferStats}}
<h4 style="text-align: center;">{{tr "CycleTransferStats"}}</h4> <h4 style="text-align: center;">{{tr "CycleTransferStats"}}</h4>
<div class="table-responsive content"> <div class="table-responsive content">
@@ -137,6 +137,7 @@
delay: '{{$service.Delay}}'.replaceAll("[","").replaceAll("]","").split(" "), delay: '{{$service.Delay}}'.replaceAll("[","").replaceAll("]","").split(" "),
up: '{{$service.Up}}'.replaceAll("[","").replaceAll("]","").split(" "), up: '{{$service.Up}}'.replaceAll("[","").replaceAll("]","").split(" "),
down: '{{$service.Down}}'.replaceAll("[","").replaceAll("]","").split(" "), down: '{{$service.Down}}'.replaceAll("[","").replaceAll("]","").split(" "),
enableShow: '{{$service.Monitor.EnableShowInService}}',
}) })
{{end}} {{end}}
// @formatter:on // @formatter:on
+1 -2
View File
@@ -6,10 +6,9 @@ services:
restart: always restart: always
volumes: volumes:
- ./data:/dashboard/data - ./data:/dashboard/data
- ./static-custom/static:/dashboard/resource/static/custom:ro
- ./theme-custom/template:/dashboard/resource/template/theme-custom:ro - ./theme-custom/template:/dashboard/resource/template/theme-custom:ro
- ./theme-custom/static:/dashboard/resource/static/theme-custom:ro
- ./dashboard-custom/template:/dashboard/resource/template/dashboard-custom:ro - ./dashboard-custom/template:/dashboard/resource/template/dashboard-custom:ro
- ./dashboard-custom/static:/dashboard/resource/static/dashboard-custom:ro
ports: ports:
- nz_site_port:80 - nz_site_port:80
- nz_grpc_port:nz_grpc_port - nz_grpc_port:nz_grpc_port
+2 -2
View File
@@ -38,8 +38,8 @@ $download = "https://github.com/$agentrepo/releases/download/$agenttag/$file"
$nssmdownload="https://github.com/$nssmrepo/releases/download/$nssmtag/nssm.zip" $nssmdownload="https://github.com/$nssmrepo/releases/download/$nssmtag/nssm.zip"
Write-Host "Location:$region,connect directly!" -BackgroundColor DarkRed -ForegroundColor Green Write-Host "Location:$region,connect directly!" -BackgroundColor DarkRed -ForegroundColor Green
}else{ }else{
$download = "https://dn-dao-github-mirror.daocloud.io/$agentrepo/releases/download/$agenttag/$file" $download = "https://kkgithub.com/$agentrepo/releases/download/$agenttag/$file"
$nssmdownload="https://dn-dao-github-mirror.daocloud.io/$nssmrepo/releases/download/$nssmtag/nssm.zip" $nssmdownload="https://kkgithub.com/$nssmrepo/releases/download/$nssmtag/nssm.zip"
Write-Host "Location:CN,use mirror address" -BackgroundColor DarkRed -ForegroundColor Green Write-Host "Location:CN,use mirror address" -BackgroundColor DarkRed -ForegroundColor Green
} }
echo $download echo $download
+474 -27
View File
@@ -11,7 +11,10 @@ NZ_BASE_PATH="/opt/nezha"
NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard" NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent" NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service" NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
NZ_VERSION="v0.15.0" NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.15.6"
red='\033[0;31m' red='\033[0;31m'
green='\033[0;32m' green='\033[0;32m'
@@ -47,7 +50,7 @@ pre_check() {
if [[ -z "${CN}" ]]; then if [[ -z "${CN}" ]]; then
if [[ $(curl -m 10 -s https://ipapi.co/json | grep 'China') != "" ]]; then if [[ $(curl -m 10 -s https://ipapi.co/json | grep 'China') != "" ]]; then
echo "根据ipapi.co提供的信息,当前IP可能在中国" echo "根据ipapi.co提供的信息,当前IP可能在中国"
read -e -r -p "是否选用中国镜像完成安装? [Y/n] " input read -e -r -p "是否选用中国镜像完成安装? [Y/n] (自定义镜像输入 3):" input
case $input in case $input in
[yY][eE][sS] | [yY]) [yY][eE][sS] | [yY])
echo "使用中国镜像" echo "使用中国镜像"
@@ -57,6 +60,17 @@ pre_check() {
[nN][oO] | [nN]) [nN][oO] | [nN])
echo "不使用中国镜像" echo "不使用中国镜像"
;; ;;
[3])
echo "使用自定义镜像"
read -e -r -p "请输入自定义镜像 (例如:dn-dao-github-mirror.daocloud.io),留空为不使用: " input
case $input in
*)
CUSTOM_MIRROR=$input
;;
esac
;;
*) *)
echo "使用中国镜像" echo "使用中国镜像"
CN=true CN=true
@@ -65,6 +79,13 @@ pre_check() {
fi fi
fi fi
if [[ -n "${CUSTOM_MIRROR}" ]]; then
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
GITHUB_URL=$CUSTOM_MIRROR
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" -s docker --mirror Aliyun"
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
else
if [[ -z "${CN}" ]]; then if [[ -z "${CN}" ]]; then
GITHUB_RAW_URL="raw.githubusercontent.com/naiba/nezha/master" GITHUB_RAW_URL="raw.githubusercontent.com/naiba/nezha/master"
GITHUB_URL="github.com" GITHUB_URL="github.com"
@@ -72,12 +93,14 @@ pre_check() {
Get_Docker_Argu=" " Get_Docker_Argu=" "
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard" Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
else else
GITHUB_RAW_URL="jihulab.com/nezha/dashboard/-/raw/master" GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
GITHUB_URL="dn-dao-github-mirror.daocloud.io" GITHUB_URL="kkgithub.com"
Get_Docker_URL="get.daocloud.io/docker" Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" -s docker --mirror Aliyun" Get_Docker_Argu=" -s docker --mirror Aliyun"
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard" Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
fi fi
fi
} }
confirm() { confirm() {
@@ -124,7 +147,8 @@ install_base() {
(command -v git >/dev/null 2>&1 && command -v curl >/dev/null 2>&1 && command -v wget >/dev/null 2>&1 && command -v unzip >/dev/null 2>&1 && command -v getenforce >/dev/null 2>&1) || (command -v git >/dev/null 2>&1 && command -v curl >/dev/null 2>&1 && command -v wget >/dev/null 2>&1 && command -v unzip >/dev/null 2>&1 && command -v getenforce >/dev/null 2>&1) ||
(install_soft curl wget git unzip) (install_soft curl wget git unzip)
} }
install_arch(){
install_arch() {
echo -e "${green}提示: ${plain} Arch安装libselinux需添加nezha-agent用户,安装完会自动删除,建议手动检查一次\n" echo -e "${green}提示: ${plain} Arch安装libselinux需添加nezha-agent用户,安装完会自动删除,建议手动检查一次\n"
read -e -r -p "是否安装libselinux? [Y/n] " input read -e -r -p "是否安装libselinux? [Y/n] " input
case $input in case $input in
@@ -135,7 +159,7 @@ install_arch(){
cd /tmp; git clone https://aur.archlinux.org/libsepol.git; cd libsepol; makepkg -si --noconfirm --asdeps; cd ..; cd /tmp; git clone https://aur.archlinux.org/libsepol.git; cd libsepol; makepkg -si --noconfirm --asdeps; cd ..;
git clone https://aur.archlinux.org/libselinux.git; cd libselinux; makepkg -si --noconfirm; cd ..; git clone https://aur.archlinux.org/libselinux.git; cd libselinux; makepkg -si --noconfirm; cd ..;
rm -rf libsepol libselinux' rm -rf libsepol libselinux'
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent&&userdel nezha-agent sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent && userdel nezha-agent
echo -e "${red}提示: ${plain}已删除用户nezha-agent,请务必手动核查一遍!\n" echo -e "${red}提示: ${plain}已删除用户nezha-agent,请务必手动核查一遍!\n"
;; ;;
[nN][oO] | [nN]) [nN][oO] | [nN])
@@ -162,7 +186,7 @@ install_dashboard() {
echo -e "> 安装面板" echo -e "> 安装面板"
# 哪吒监控文件夹 # 哪吒监控文件夹
if [ ! -d "${NZ_DASHBOARD_PATH}" ]; then if [ ! -d "${NZ_DASHBOARD_PATH}/docker-compose.yaml" ]; then
mkdir -p $NZ_DASHBOARD_PATH mkdir -p $NZ_DASHBOARD_PATH
else else
echo "您可能已经安装过面板端,重复安装会覆盖数据,请注意备份。" echo "您可能已经安装过面板端,重复安装会覆盖数据,请注意备份。"
@@ -204,11 +228,50 @@ install_dashboard() {
fi fi
} }
selinux(){ install_dashboard_standalone() {
install_base
echo -e "> 安装面板"
# 哪吒监控文件夹
if [ ! -d "${NZ_DASHBOARD_PATH}/app" ]; then
mkdir -p $NZ_DASHBOARD_PATH
else
echo "您可能已经安装过面板端,重复安装会覆盖数据,请注意备份。"
read -e -r -p "是否退出安装? [Y/n] " input
case $input in
[yY][eE][sS] | [yY])
echo "退出安装"
exit 0
;;
[nN][oO] | [nN])
echo "继续安装"
;;
*)
echo "退出安装"
exit 0
;;
esac
fi
if [ ! -d "${NZ_DASHBOARD_PATH}/resource/template/theme-custom" ] || [ ! -d "${NZ_DASHBOARD_PATH}/resource/static/custom" ]; then
mkdir -p "${NZ_DASHBOARD_PATH}/resource/template/theme-custom" "${NZ_DASHBOARD_PATH}/resource/static/custom" >/dev/null 2>&1
fi
chmod 777 -R $NZ_DASHBOARD_PATH
modify_dashboard_config_standalone 0
if [[ $# == 0 ]]; then
before_show_menu
fi
}
selinux() {
#判断当前的状态 #判断当前的状态
if [ "$os_alpine" != 1 ];then if [ "$os_alpine" != 1 ]; then
getenforce | grep '[Ee]nfor' getenforce | grep '[Ee]nfor'
if [ $? -eq 0 ];then if [ $? -eq 0 ]; then
echo -e "SELinux是开启状态,正在关闭!" echo -e "SELinux是开启状态,正在关闭!"
setenforce 0 &>/dev/null setenforce 0 &>/dev/null
find_key="SELINUX=" find_key="SELINUX="
@@ -269,12 +332,19 @@ install_agent() {
modify_agent_config() { modify_agent_config() {
echo -e "> 修改Agent配置" echo -e "> 修改Agent配置"
if [ "$os_alpine" != 1 ];then if [ "$os_alpine" != 1 ]; then
wget -t 2 -T 10 -O $NZ_AGENT_SERVICE https://${GITHUB_RAW_URL}/script/nezha-agent.service >/dev/null 2>&1 wget -t 2 -T 10 -O $NZ_AGENT_SERVICE https://${GITHUB_RAW_URL}/script/nezha-agent.service >/dev/null 2>&1
if [[ $? != 0 ]]; then if [[ $? != 0 ]]; then
echo -e "${red}文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}${plain}" echo -e "${red}文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}${plain}"
return 0 return 0
fi fi
else
wget -t 2 -T 10 -O $NZ_AGENT_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-agent >/dev/null 2>&1
chmod +x $NZ_AGENT_SERVICERC
if [[ $? != 0 ]]; then
echo -e "${red}Fail to download service, please check if the network can link ${GITHUB_RAW_URL}${plain}"
return 0
fi
fi fi
if [ $# -lt 3 ]; then if [ $# -lt 3 ]; then
@@ -283,7 +353,7 @@ modify_agent_config() {
read -ep "请输入面板RPC端口 (默认值 5555): " nz_grpc_port && read -ep "请输入面板RPC端口 (默认值 5555): " nz_grpc_port &&
read -ep "请输入Agent 密钥: " nz_client_secret && read -ep "请输入Agent 密钥: " nz_client_secret &&
read -ep "是否启用针对 gRPC 端口的 SSL/TLS加密 (--tls),需要请按 [y],默认是不需要,不理解用户可回车跳过: " nz_grpc_proxy read -ep "是否启用针对 gRPC 端口的 SSL/TLS加密 (--tls),需要请按 [y],默认是不需要,不理解用户可回车跳过: " nz_grpc_proxy
grep -qiw 'Y' <<< "${nz_grpc_proxy}" && args='--tls' grep -qiw 'Y' <<<"${nz_grpc_proxy}" && args='--tls'
if [[ -z "${nz_grpc_host}" || -z "${nz_client_secret}" ]]; then if [[ -z "${nz_grpc_host}" || -z "${nz_client_secret}" ]]; then
echo -e "${red}所有选项都不能为空${plain}" echo -e "${red}所有选项都不能为空${plain}"
before_show_menu before_show_menu
@@ -302,29 +372,32 @@ modify_agent_config() {
fi fi
fi fi
if [ "$os_alpine" != 1 ];then if [ "$os_alpine" != 1 ]; then
sed -i "s/nz_grpc_host/${nz_grpc_host}/" ${NZ_AGENT_SERVICE} sed -i "s/nz_grpc_host/${nz_grpc_host}/" ${NZ_AGENT_SERVICE}
sed -i "s/nz_grpc_port/${nz_grpc_port}/" ${NZ_AGENT_SERVICE} sed -i "s/nz_grpc_port/${nz_grpc_port}/" ${NZ_AGENT_SERVICE}
sed -i "s/nz_client_secret/${nz_client_secret}/" ${NZ_AGENT_SERVICE} sed -i "s/nz_client_secret/${nz_client_secret}/" ${NZ_AGENT_SERVICE}
[ -n "${args}" ] && sed -i "/ExecStart/ s/$/ ${args}/" ${NZ_AGENT_SERVICE} [ -n "${args}" ] && sed -i "/ExecStart/ s/$/ ${args}/" ${NZ_AGENT_SERVICE}
else else
echo "@reboot ${NZ_AGENT_PATH}/nezha-agent -s ${nz_grpc_host}:${nz_grpc_port} -p ${nz_client_secret} ${args}" >> /etc/crontabs/root sed -i "s/nz_grpc_host/${nz_grpc_host}/" ${NZ_AGENT_SERVICERC}
crond sed -i "s/nz_grpc_port/${nz_grpc_port}/" ${NZ_AGENT_SERVICERC}
sed -i "s/nz_client_secret/${nz_client_secret}/" ${NZ_AGENT_SERVICERC}
[ -n "${args}" ] && sed -i "/command_args/ s/\"$/ ${args}\"/" ${NZ_AGENT_SERVICERC}
fi fi
echo -e "Agent配置 ${green}修改成功,请稍等重启生效${plain}" echo -e "Agent配置 ${green}修改成功,请稍等重启生效${plain}"
if [ "$os_alpine" != 1 ];then if [ "$os_alpine" != 1 ]; then
systemctl daemon-reload systemctl daemon-reload
systemctl enable nezha-agent systemctl enable nezha-agent
systemctl restart nezha-agent systemctl restart nezha-agent
else else
nohup ${NZ_AGENT_PATH}/nezha-agent -s ${nz_grpc_host}:${nz_grpc_port} -p ${nz_client_secret} ${args} >/dev/null 2>&1 & rc-update add nezha-agent
rc-service nezha-agent restart
fi fi
if [[ $# == 0 ]]; then #if [[ $# == 0 ]]; then
before_show_menu # before_show_menu
fi #fi
} }
modify_dashboard_config() { modify_dashboard_config() {
@@ -393,6 +466,77 @@ modify_dashboard_config() {
fi fi
} }
modify_dashboard_config_standalone() {
echo -e "> 修改面板配置"
echo -e "正在下载配置模板"
wget -t 2 -T 10 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1
if [[ $? != 0 ]]; then
echo -e "${red}下载文件失败,请检查本机能否连接 ${GITHUB_RAW_URL}${plain}"
return 0
fi
echo "关于 GitHub Oauth2 应用:在 https://github.com/settings/developers 创建,无需审核,Callback 填 http(s)://域名或IP/oauth2/callback" &&
echo "关于 Gitee Oauth2 应用:在 https://gitee.com/oauth/applications 创建,无需审核,Callback 填 http(s)://域名或IP/oauth2/callback" &&
read -ep "请输入 OAuth2 提供商(github/gitlab/jihulab/gitee,默认 github): " nz_oauth2_type &&
read -ep "请输入 Oauth2 应用的 Client ID: " nz_github_oauth_client_id &&
read -ep "请输入 Oauth2 应用的 Client Secret: " nz_github_oauth_client_secret &&
read -ep "请输入 GitHub/Gitee 登录名作为管理员,多个以逗号隔开: " nz_admin_logins &&
read -ep "请输入站点标题: " nz_site_title &&
read -ep "请输入站点访问端口: (默认 8008)" nz_site_port &&
read -ep "请输入用于 Agent 接入的 RPC 端口: (默认 5555)" nz_grpc_port
if [[ -z "${nz_admin_logins}" || -z "${nz_github_oauth_client_id}" || -z "${nz_github_oauth_client_secret}" || -z "${nz_site_title}" ]]; then
echo -e "${red}所有选项都不能为空${plain}"
before_show_menu
return 1
fi
if [[ -z "${nz_site_port}" ]]; then
nz_site_port=8008
fi
if [[ -z "${nz_grpc_port}" ]]; then
nz_grpc_port=5555
fi
if [[ -z "${nz_oauth2_type}" ]]; then
nz_oauth2_type=github
fi
sed -i "s/nz_oauth2_type/${nz_oauth2_type}/" /tmp/nezha-config.yaml
sed -i "s/nz_admin_logins/${nz_admin_logins}/" /tmp/nezha-config.yaml
sed -i "s/nz_grpc_port/${nz_grpc_port}/" /tmp/nezha-config.yaml
sed -i "s/nz_github_oauth_client_id/${nz_github_oauth_client_id}/" /tmp/nezha-config.yaml
sed -i "s/nz_github_oauth_client_secret/${nz_github_oauth_client_secret}/" /tmp/nezha-config.yaml
sed -i "s/nz_language/zh-CN/" /tmp/nezha-config.yaml
sed -i "s/nz_site_title/${nz_site_title}/" /tmp/nezha-config.yaml
sed -i "s/80/${nz_site_port}/" /tmp/nezha-config.yaml
mkdir -p $NZ_DASHBOARD_PATH/data
mv -f /tmp/nezha-config.yaml ${NZ_DASHBOARD_PATH}/data/config.yaml
echo -e "正在下载服务文件"
if [ "$os_alpine" != 1 ]; then
wget -t 2 -T 10 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1
else
wget -t 2 -T 10 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1
chmod +x $NZ_DASHBOARD_SERVICERC
if [[ $? != 0 ]]; then
echo -e "${red}文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}${plain}"
return 0
fi
fi
echo -e "面板配置 ${green}修改成功,请稍等重启生效${plain}"
restart_and_update_standalone
if [[ $# == 0 ]]; then
before_show_menu
fi
}
restart_and_update() { restart_and_update() {
echo -e "> 重启并更新面板" echo -e "> 重启并更新面板"
@@ -421,6 +565,40 @@ restart_and_update() {
fi fi
} }
restart_and_update_standalone() {
echo -e "> 重启并更新面板"
cd $NZ_DASHBOARD_PATH
if [ "$os_alpine" != 1 ]; then
systemctl stop nezha-dashboard
else
rc-service nezha-dashboard stop
fi
wget -qO app.zip https://${GITHUB_URL}/naiba/nezha/releases/latest/download/dashboard-linux-$os_arch.zip >/dev/null 2>&1 && unzip -qq app.zip && mv dist/dashboard-linux-$os_arch app && rm -r app.zip dist
if [ "$os_alpine" != 1 ]; then
systemctl daemon-reload
systemctl enable nezha-dashboard
systemctl restart nezha-dashboard
else
rc-update add nezha-dashboard
rc-service nezha-dashboard restart
fi
if [[ $? == 0 ]]; then
echo -e "${green}哪吒监控 重启成功${plain}"
echo -e "默认管理面板地址:${yellow}域名:站点访问端口${plain}"
else
echo -e "${red}重启失败,可能是因为启动时间超过了两秒,请稍后查看日志信息${plain}"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
}
start_dashboard() { start_dashboard() {
echo -e "> 启动面板" echo -e "> 启动面板"
@@ -442,6 +620,26 @@ start_dashboard() {
fi fi
} }
start_dashboard_standalone() {
echo -e "> 启动面板"
if [ "$os_alpine" != 1 ]; then
systemctl start nezha-dashboard
else
rc-service nezha-dashboard start
fi
if [[ $? == 0 ]]; then
echo -e "${green}哪吒监控 启动成功${plain}"
else
echo -e "${red}启动失败,请稍后查看日志信息${plain}"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
}
stop_dashboard() { stop_dashboard() {
echo -e "> 停止面板" echo -e "> 停止面板"
@@ -463,6 +661,26 @@ stop_dashboard() {
fi fi
} }
stop_dashboard_standalone() {
echo -e "> 停止面板"
if [ "$os_alpine" != 1 ]; then
systemctl stop nezha-dashboard
else
rc-service nezha-dashboard stop
fi
if [[ $? == 0 ]]; then
echo -e "${green}哪吒监控 停止成功${plain}"
else
echo -e "${red}停止失败,请稍后查看日志信息${plain}"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
}
show_dashboard_log() { show_dashboard_log() {
echo -e "> 获取面板日志" echo -e "> 获取面板日志"
@@ -478,6 +696,20 @@ show_dashboard_log() {
fi fi
} }
show_dashboard_log_standalone() {
echo -e "> 获取面板日志"
if [ "$os_alpine" != 1 ]; then
journalctl -xf -u nezha-dashboard.service
else
echo "很抱歉,OpenRC 无此功能"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
}
uninstall_dashboard() { uninstall_dashboard() {
echo -e "> 卸载管理面板" echo -e "> 卸载管理面板"
@@ -489,8 +721,32 @@ uninstall_dashboard() {
fi fi
rm -rf $NZ_DASHBOARD_PATH rm -rf $NZ_DASHBOARD_PATH
docker rmi -f ghcr.io/naiba/nezha-dashboard > /dev/null 2>&1 docker rmi -f ghcr.io/naiba/nezha-dashboard >/dev/null 2>&1
docker rmi -f registry.cn-shanghai.aliyuncs.com/naibahq/nezha-dashboard > /dev/null 2>&1 docker rmi -f registry.cn-shanghai.aliyuncs.com/naibahq/nezha-dashboard >/dev/null 2>&1
clean_all
if [[ $# == 0 ]]; then
before_show_menu
fi
}
uninstall_dashboard_standalone() {
echo -e "> 卸载管理面板"
rm -rf $NZ_DASHBOARD_PATH
if [ "$os_alpine" != 1 ]; then
systemctl stop nezha-dashboard
else
rc-service nezha-dashboard stop
fi
if [ "$os_alpine" != 1 ]; then
rm $NZ_DASHBOARD_SERVICE
else
rm $NZ_DASHBOARD_SERVICERC
fi
clean_all clean_all
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
@@ -501,7 +757,11 @@ uninstall_dashboard() {
show_agent_log() { show_agent_log() {
echo -e "> 获取Agent日志" echo -e "> 获取Agent日志"
if [ "$os_alpine" != 1 ]; then
journalctl -xf -u nezha-agent.service journalctl -xf -u nezha-agent.service
else
echo "很抱歉,OpenRC 无此功能"
fi
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
before_show_menu before_show_menu
@@ -511,14 +771,15 @@ show_agent_log() {
uninstall_agent() { uninstall_agent() {
echo -e "> 卸载Agent" echo -e "> 卸载Agent"
if [ "$os_alpine" != 1 ];then if [ "$os_alpine" != 1 ]; then
systemctl disable nezha-agent.service systemctl disable nezha-agent.service
systemctl stop nezha-agent.service systemctl stop nezha-agent.service
rm -rf $NZ_AGENT_SERVICE rm -rf $NZ_AGENT_SERVICE
systemctl daemon-reload systemctl daemon-reload
else else
grep -q 'nezha-agent' /var/spool/cron/crontabs/root && sed -i "/nezha-agent/d" /var/spool/cron/crontabs/root rc-update del nezha-agent
pkill nezha-agent rc-service nezha-agent stop
rm -rf $NZ_AGENT_SERVICERC
fi fi
rm -rf $NZ_AGENT_PATH rm -rf $NZ_AGENT_PATH
@@ -532,7 +793,11 @@ uninstall_agent() {
restart_agent() { restart_agent() {
echo -e "> 重启Agent" echo -e "> 重启Agent"
if [ "$os_alpine" != 1 ]; then
systemctl restart nezha-agent.service systemctl restart nezha-agent.service
else
rc-service nezha-agent restart
fi
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
before_show_menu before_show_menu
@@ -545,6 +810,59 @@ clean_all() {
fi fi
} }
select_version() {
DOCKER_COMPOSE_COMMAND=""
if docker compose version >/dev/null 2>&1; then
DOCKER_COMPOSE_COMMAND="docker compose"
if $DOCKER_COMPOSE_COMMAND ls | grep -qw "$NZ_DASHBOARD_PATH/docker-compose.yaml" >/dev/null 2>&1; then
NEZHA_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -w "nezha-dashboard")
if [ -n "$NEZHA_IMAGES" ]; then
echo "存在带有 nezha-dashboard 仓库的 Docker 镜像:"
echo "$NEZHA_IMAGES"
IS_DOCKER_NEZHA=1
return
else
echo "未找到带有 nezha-dashboard 仓库的 Docker 镜像。"
fi
fi
elif command -v docker-compose >/dev/null 2>&1; then
DOCKER_COMPOSE_COMMAND="docker-compose"
if $DOCKER_COMPOSE_COMMAND -f "$NZ_DASHBOARD_PATH/docker-compose.yaml" config >/dev/null 2>&1; then
NEZHA_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -w "nezha-dashboard")
if [ -n "$NEZHA_IMAGES" ]; then
echo "存在带有 nezha-dashboard 仓库的 Docker 镜像:"
echo "$NEZHA_IMAGES"
IS_DOCKER_NEZHA=1
return
else
echo "未找到带有 nezha-dashboard 仓库的 Docker 镜像。"
fi
fi
fi
if [[ -f $NZ_DASHBOARD_PATH/app ]]; then
IS_DOCKER_NEZHA=0
else
echo -e "${yellow}请自行选择您的安装方式(如果你是安装Agent,输入哪个都是一样的):\n1. Docker\n2. 独立安装${plain}"
while true; do
read -e -r -p "请输入数字 [1-2]:" option
case "${option}" in
1)
IS_DOCKER_NEZHA=1
break
;;
2)
IS_DOCKER_NEZHA=0
break
;;
*)
echo "输入有误,请重新输入"
;;
esac
done
fi
}
show_usage() { show_usage() {
echo "哪吒监控 管理脚本使用方法: " echo "哪吒监控 管理脚本使用方法: "
echo "--------------------------------------------------------" echo "--------------------------------------------------------"
@@ -589,7 +907,7 @@ show_menu() {
${green}0.${plain} 退出脚本 ${green}0.${plain} 退出脚本
" "
echo && read -ep "请输入选择 [0-13]: " num echo && read -ep "请输入选择 [0-13]: " num
if [[ $IS_DOCKER_NEZHA == 1 ]]; then
case "${num}" in case "${num}" in
0) 0)
exit 0 exit 0
@@ -637,11 +955,63 @@ show_menu() {
echo -e "${red}请输入正确的数字 [0-13]${plain}" echo -e "${red}请输入正确的数字 [0-13]${plain}"
;; ;;
esac esac
elif [[ $IS_DOCKER_NEZHA == 0 ]]; then
case "${num}" in
0)
exit 0
;;
1)
install_dashboard_standalone
;;
2)
modify_dashboard_config_standalone
;;
3)
start_dashboard_standalone
;;
4)
stop_dashboard_standalone
;;
5)
restart_and_update_standalone
;;
6)
show_dashboard_log_standalone
;;
7)
uninstall_dashboard_standalone
;;
8)
install_agent
;;
9)
modify_agent_config
;;
10)
show_agent_log
;;
11)
uninstall_agent
;;
12)
restart_agent
;;
13)
update_script
;;
*)
echo -e "${red}请输入正确的数字 [0-13]${plain}"
;;
esac
else
select_version
fi
} }
pre_check pre_check
if [[ $# > 0 ]]; then if [[ $# > 0 ]]; then
if [[ $IS_DOCKER_NEZHA == 1 ]]; then
case $1 in case $1 in
"install_dashboard") "install_dashboard")
install_dashboard 0 install_dashboard 0
@@ -689,6 +1059,83 @@ if [[ $# > 0 ]]; then
;; ;;
*) show_usage ;; *) show_usage ;;
esac esac
elif [[ $IS_DOCKER_NEZHA == 0 ]]; then
case $1 in
"install_dashboard")
install_dashboard_standalone 0
;;
"modify_dashboard_config")
modify_dashboard_config_standalone 0
;;
"start_dashboard")
start_dashboard_standalone 0
;;
"stop_dashboard")
stop_dashboard_standalone 0
;;
"restart_and_update")
restart_and_update_standalone 0
;;
"show_dashboard_log")
show_dashboard_log_standalone 0
;;
"uninstall_dashboard")
uninstall_dashboard_standalone 0
;;
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
;;
"modify_agent_config")
modify_agent_config 0
;;
"show_agent_log")
show_agent_log 0
;;
"uninstall_agent")
uninstall_agent 0
;;
"restart_agent")
restart_agent 0
;;
"update_script")
update_script 0
;;
*) show_usage ;;
esac
else
case $1 in
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
;;
"modify_agent_config")
modify_agent_config 0
;;
"show_agent_log")
show_agent_log 0
;;
"uninstall_agent")
uninstall_agent 0
;;
"restart_agent")
restart_agent 0
;;
"update_script")
update_script 0
;;
*) show_usage ;;
esac
fi
else else
select_version
show_menu show_menu
fi fi
+475 -31
View File
@@ -11,7 +11,10 @@ NZ_BASE_PATH="/opt/nezha"
NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard" NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent" NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service" NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
NZ_VERSION="v0.15.0" NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.15.6"
red='\033[0;31m' red='\033[0;31m'
green='\033[0;32m' green='\033[0;32m'
@@ -47,7 +50,7 @@ pre_check() {
if [[ -z "${CN}" ]]; then if [[ -z "${CN}" ]]; then
if [[ $(curl -m 10 -s https://ipapi.co/json | grep 'China') != "" ]]; then if [[ $(curl -m 10 -s https://ipapi.co/json | grep 'China') != "" ]]; then
echo "According to the information provided by ipapi.co, the current IP may be in China" echo "According to the information provided by ipapi.co, the current IP may be in China"
read -e -r -p "Is the installation done with a Chinese Mirror? [Y/n] " input read -e -r -p "Is the installation done with a Chinese Mirror? [Y/n] (Custom Mirror Input 3):" input
case $input in case $input in
[yY][eE][sS] | [yY]) [yY][eE][sS] | [yY])
echo "Use Chinese Mirror" echo "Use Chinese Mirror"
@@ -57,6 +60,17 @@ pre_check() {
[nN][oO] | [nN]) [nN][oO] | [nN])
echo "No Use Chinese Mirror" echo "No Use Chinese Mirror"
;; ;;
[3])
echo "Use Custom Mirror"
read -e -r -p "Please enter a custom image (e.g. :dn-dao-github-mirror.daocloud.io), leave blank to nouse: " input
case $input in
*)
CUSTOM_MIRROR=$input
;;
esac
;;
*) *)
echo "No Use Chinese Mirror" echo "No Use Chinese Mirror"
;; ;;
@@ -64,6 +78,13 @@ pre_check() {
fi fi
fi fi
if [[ -n "${CUSTOM_MIRROR}" ]]; then
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
GITHUB_URL=$CUSTOM_MIRROR
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" -s docker --mirror Aliyun"
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
else
if [[ -z "${CN}" ]]; then if [[ -z "${CN}" ]]; then
GITHUB_RAW_URL="raw.githubusercontent.com/naiba/nezha/master" GITHUB_RAW_URL="raw.githubusercontent.com/naiba/nezha/master"
GITHUB_URL="github.com" GITHUB_URL="github.com"
@@ -71,17 +92,18 @@ pre_check() {
Get_Docker_Argu=" " Get_Docker_Argu=" "
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard" Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
else else
GITHUB_RAW_URL="jihulab.com/nezha/dashboard/-/raw/master" GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
GITHUB_URL="dn-dao-github-mirror.daocloud.io" GITHUB_URL="kkgithub.com"
Get_Docker_URL="get.daocloud.io/docker" Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" -s docker --mirror Aliyun" Get_Docker_Argu=" -s docker --mirror Aliyun"
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard" Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
fi fi
fi
} }
confirm() { confirm() {
if [[ $# > 1 ]]; then if [[ $# > 1 ]]; then
echo && read -e -p "$1 [Default$2]: " temp echo && read -e -p "$1 [默认$2]: " temp
if [[ x"${temp}" == x"" ]]; then if [[ x"${temp}" == x"" ]]; then
temp=$2 temp=$2
fi fi
@@ -101,7 +123,7 @@ update_script() {
curl -sL https://${GITHUB_RAW_URL}/script/install_en.sh -o /tmp/nezha.sh curl -sL https://${GITHUB_RAW_URL}/script/install_en.sh -o /tmp/nezha.sh
new_version=$(cat /tmp/nezha.sh | grep "NZ_VERSION" | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g') new_version=$(cat /tmp/nezha.sh | grep "NZ_VERSION" | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$new_version" ]; then if [ ! -n "$new_version" ]; then
echo -e "Script failed to get, please check if the network can link https://${GITHUB_RAW_URL}/script/install_en.sh" echo -e "Script failed to get, please check if the network can link https://${GITHUB_RAW_URL}/script/install.sh"
return 1 return 1
fi fi
echo -e "The current latest version is: ${new_version}" echo -e "The current latest version is: ${new_version}"
@@ -121,9 +143,10 @@ before_show_menu() {
install_base() { install_base() {
(command -v git >/dev/null 2>&1 && command -v curl >/dev/null 2>&1 && command -v wget >/dev/null 2>&1 && command -v unzip >/dev/null 2>&1 && command -v getenforce >/dev/null 2>&1) || (command -v git >/dev/null 2>&1 && command -v curl >/dev/null 2>&1 && command -v wget >/dev/null 2>&1 && command -v unzip >/dev/null 2>&1 && command -v getenforce >/dev/null 2>&1) ||
(install_soft curl wget git unzip) (install_soft curl wget git unzip tzdata)
} }
install_arch(){
install_arch() {
echo -e "${green}Info: ${plain} Archlinux needs to add nezha-agent user to install libselinux. It will be deleted automatically after installation. It is recommended to check manually\n" echo -e "${green}Info: ${plain} Archlinux needs to add nezha-agent user to install libselinux. It will be deleted automatically after installation. It is recommended to check manually\n"
read -e -r -p "Do you need to install libselinux? [Y/n] " input read -e -r -p "Do you need to install libselinux? [Y/n] " input
case $input in case $input in
@@ -134,7 +157,7 @@ install_arch(){
cd /tmp; git clone https://aur.archlinux.org/libsepol.git; cd libsepol; makepkg -si --noconfirm --asdeps; cd ..; cd /tmp; git clone https://aur.archlinux.org/libsepol.git; cd libsepol; makepkg -si --noconfirm --asdeps; cd ..;
git clone https://aur.archlinux.org/libselinux.git; cd libselinux; makepkg -si --noconfirm; cd ..; git clone https://aur.archlinux.org/libselinux.git; cd libselinux; makepkg -si --noconfirm; cd ..;
rm -rf libsepol libselinux' rm -rf libsepol libselinux'
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent&&userdel nezha-agent sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent && userdel nezha-agent
echo -e "${red}Info: ${plain}user nezha-agent has been deleted, Be sure to check it manually!\n" echo -e "${red}Info: ${plain}user nezha-agent has been deleted, Be sure to check it manually!\n"
;; ;;
[nN][oO] | [nN]) [nN][oO] | [nN])
@@ -147,7 +170,6 @@ install_arch(){
esac esac
} }
install_soft() { install_soft() {
(command -v yum >/dev/null 2>&1 && yum makecache && yum install $* selinux-policy -y) || (command -v yum >/dev/null 2>&1 && yum makecache && yum install $* selinux-policy -y) ||
(command -v apt >/dev/null 2>&1 && apt update && apt install $* selinux-utils -y) || (command -v apt >/dev/null 2>&1 && apt update && apt install $* selinux-utils -y) ||
@@ -162,7 +184,7 @@ install_dashboard() {
echo -e "> Install Panel" echo -e "> Install Panel"
# Nezha Monitoring Folder # Nezha Monitoring Folder
if [ ! -d "${NZ_DASHBOARD_PATH}" ]; then if [ ! -d "${NZ_DASHBOARD_PATH}/docker-compose.yaml" ]; then
mkdir -p $NZ_DASHBOARD_PATH mkdir -p $NZ_DASHBOARD_PATH
else else
echo "You may have already installed the dashboard, repeated installation will overwrite the data, please pay attention to backup." echo "You may have already installed the dashboard, repeated installation will overwrite the data, please pay attention to backup."
@@ -203,11 +225,50 @@ install_dashboard() {
fi fi
} }
selinux(){ install_dashboard_standalone() {
install_base
echo -e "> Install Panel"
# Nezha Monitoring Folder
if [ ! -d "${NZ_DASHBOARD_PATH}/app" ]; then
mkdir -p $NZ_DASHBOARD_PATH
else
echo "You may have already installed the dashboard, repeated installation will overwrite the data, please pay attention to backup."
read -e -r -p "Exit the installation? [Y/n] " input
case $input in
[yY][eE][sS] | [yY])
echo "Exit the installation."
exit 0
;;
[nN][oO] | [nN])
echo "Continue."
;;
*)
echo "Exit the installation."
exit 0
;;
esac
fi
if [ ! -d "${NZ_DASHBOARD_PATH}/resource/template/theme-custom" ] || [ ! -d "${NZ_DASHBOARD_PATH}/resource/static/custom" ]; then
mkdir -p "${NZ_DASHBOARD_PATH}/resource/template/theme-custom" "${NZ_DASHBOARD_PATH}/resource/static/custom" >/dev/null 2>&1
fi
chmod 777 -R $NZ_DASHBOARD_PATH
modify_dashboard_config_standalone 0
if [[ $# == 0 ]]; then
before_show_menu
fi
}
selinux() {
#Check SELinux #Check SELinux
if [ "$os_alpine" != 1 ];then if [ "$os_alpine" != 1 ]; then
getenforce | grep '[Ee]nfor' getenforce | grep '[Ee]nfor'
if [ $? -eq 0 ];then if [ $? -eq 0 ]; then
echo -e "SELinux running,closing now!" echo -e "SELinux running,closing now!"
setenforce 0 &>/dev/null setenforce 0 &>/dev/null
find_key="SELINUX=" find_key="SELINUX="
@@ -268,12 +329,19 @@ install_agent() {
modify_agent_config() { modify_agent_config() {
echo -e "> Modify Agent Configuration" echo -e "> Modify Agent Configuration"
if [ "$os_alpine" != 1 ];then if [ "$os_alpine" != 1 ]; then
wget -t 2 -T 10 -O $NZ_AGENT_SERVICE https://${GITHUB_RAW_URL}/script/nezha-agent.service >/dev/null 2>&1 wget -t 2 -T 10 -O $NZ_AGENT_SERVICE https://${GITHUB_RAW_URL}/script/nezha-agent.service >/dev/null 2>&1
if [[ $? != 0 ]]; then if [[ $? != 0 ]]; then
echo -e "${red}Fail to download service, please check if the network can link ${GITHUB_RAW_URL}${plain}" echo -e "${red}Fail to download service, please check if the network can link ${GITHUB_RAW_URL}${plain}"
return 0 return 0
fi fi
else
wget -t 2 -T 10 -O $NZ_AGENT_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-agent >/dev/null 2>&1
chmod +x $NZ_AGENT_SERVICERC
if [[ $? != 0 ]]; then
echo -e "${red}Fail to download service, please check if the network can link ${GITHUB_RAW_URL}${plain}"
return 0
fi
fi fi
if [ $# -lt 3 ]; then if [ $# -lt 3 ]; then
@@ -282,7 +350,7 @@ modify_agent_config() {
read -ep "Please enter the panel RPC port (default 5555): " nz_grpc_port && read -ep "Please enter the panel RPC port (default 5555): " nz_grpc_port &&
read -ep "Please enter the Agent secret: " nz_client_secret && read -ep "Please enter the Agent secret: " nz_client_secret &&
read -ep "Do you want to enable SSL/TLS encryption for the gRPC port (--tls)? Press [y] if yes, the default is not required, and users can press Enter to skip if you don't understand: " nz_grpc_proxy read -ep "Do you want to enable SSL/TLS encryption for the gRPC port (--tls)? Press [y] if yes, the default is not required, and users can press Enter to skip if you don't understand: " nz_grpc_proxy
grep -qiw 'Y' <<< "${nz_grpc_proxy}" && args='--tls' grep -qiw 'Y' <<<"${nz_grpc_proxy}" && args='--tls'
if [[ -z "${nz_grpc_host}" || -z "${nz_client_secret}" ]]; then if [[ -z "${nz_grpc_host}" || -z "${nz_client_secret}" ]]; then
echo -e "${red}All options cannot be empty${plain}" echo -e "${red}All options cannot be empty${plain}"
before_show_menu before_show_menu
@@ -301,29 +369,32 @@ modify_agent_config() {
fi fi
fi fi
if [ "$os_alpine" != 1 ];then if [ "$os_alpine" != 1 ]; then
sed -i "s/nz_grpc_host/${nz_grpc_host}/" ${NZ_AGENT_SERVICE} sed -i "s/nz_grpc_host/${nz_grpc_host}/" ${NZ_AGENT_SERVICE}
sed -i "s/nz_grpc_port/${nz_grpc_port}/" ${NZ_AGENT_SERVICE} sed -i "s/nz_grpc_port/${nz_grpc_port}/" ${NZ_AGENT_SERVICE}
sed -i "s/nz_client_secret/${nz_client_secret}/" ${NZ_AGENT_SERVICE} sed -i "s/nz_client_secret/${nz_client_secret}/" ${NZ_AGENT_SERVICE}
[ -n "${args}" ] && sed -i "/ExecStart/ s/$/ ${args}/" ${NZ_AGENT_SERVICE} [ -n "${args}" ] && sed -i "/ExecStart/ s/$/ ${args}/" ${NZ_AGENT_SERVICE}
else else
echo "@reboot ${NZ_AGENT_PATH}/nezha-agent -s ${nz_grpc_host}:${nz_grpc_port} -p ${nz_client_secret} ${args}" >> /etc/crontabs/root sed -i "s/nz_grpc_host/${nz_grpc_host}/" ${NZ_AGENT_SERVICERC}
crond sed -i "s/nz_grpc_port/${nz_grpc_port}/" ${NZ_AGENT_SERVICERC}
sed -i "s/nz_client_secret/${nz_client_secret}/" ${NZ_AGENT_SERVICERC}
[ -n "${args}" ] && sed -i "/command_args/ s/\"$/ ${args}\"/" ${NZ_AGENT_SERVICERC}
fi fi
echo -e "Agent configuration ${green} modified successfully, please wait for agent self-restart to take effect${plain}" echo -e "Agent configuration ${green} modified successfully, please wait for agent self-restart to take effect${plain}"
if [ "$os_alpine" != 1 ];then if [ "$os_alpine" != 1 ]; then
systemctl daemon-reload systemctl daemon-reload
systemctl enable nezha-agent systemctl enable nezha-agent
systemctl restart nezha-agent systemctl restart nezha-agent
else else
nohup ${NZ_AGENT_PATH}/nezha-agent -s ${nz_grpc_host}:${nz_grpc_port} -p ${nz_client_secret} ${args} >/dev/null 2>&1 & rc-update add nezha-agent
rc-service nezha-agent restart
fi fi
if [[ $# == 0 ]]; then #if [[ $# == 0 ]]; then
before_show_menu # before_show_menu
fi #fi
} }
modify_dashboard_config() { modify_dashboard_config() {
@@ -392,6 +463,77 @@ modify_dashboard_config() {
fi fi
} }
modify_dashboard_config_standalone() {
echo -e "> Modify Panel Configuration"
echo -e "Download configuration template"
wget -t 2 -T 10 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1
if [[ $? != 0 ]]; then
echo -e "${red}File failed to get, please check if the network can link ${GITHUB_RAW_URL}${plain}"
return 0
fi
echo "About the GitHub Oauth2 application: create it at https://github.com/settings/developers, no review required, and fill in the http(s)://domain_or_IP/oauth2/callback" &&
echo "(Not recommended) About the Gitee Oauth2 application: create it at https://gitee.com/oauth/applications, no auditing required, and fill in the http(s)://domain_or_IP/oauth2/callback" &&
read -ep "Please enter the OAuth2 provider (github/gitlab/jihulab/gitee, default github): " nz_oauth2_type &&
read -ep "Please enter the Client ID of the Oauth2 application: " nz_github_oauth_client_id &&
read -ep "Please enter the Client Secret of the Oauth2 application: " nz_github_oauth_client_secret &&
read -ep "Please enter your GitHub/Gitee login name as the administrator, separated by commas: " nz_admin_logins &&
read -ep "Please enter the site title: " nz_site_title &&
read -ep "Please enter the site access port: (default 8008)" nz_site_port &&
read -ep "Please enter the RPC port to be used for Agent access: (default 5555)" nz_grpc_port
if [[ -z "${nz_admin_logins}" || -z "${nz_github_oauth_client_id}" || -z "${nz_github_oauth_client_secret}" || -z "${nz_site_title}" ]]; then
echo -e "${red}All options cannot be empty${plain}"
before_show_menu
return 1
fi
if [[ -z "${nz_site_port}" ]]; then
nz_site_port=8008
fi
if [[ -z "${nz_grpc_port}" ]]; then
nz_grpc_port=5555
fi
if [[ -z "${nz_oauth2_type}" ]]; then
nz_oauth2_type=github
fi
sed -i "s/nz_oauth2_type/${nz_oauth2_type}/" /tmp/nezha-config.yaml
sed -i "s/nz_admin_logins/${nz_admin_logins}/" /tmp/nezha-config.yaml
sed -i "s/nz_grpc_port/${nz_grpc_port}/" /tmp/nezha-config.yaml
sed -i "s/nz_github_oauth_client_id/${nz_github_oauth_client_id}/" /tmp/nezha-config.yaml
sed -i "s/nz_github_oauth_client_secret/${nz_github_oauth_client_secret}/" /tmp/nezha-config.yaml
sed -i "s/nz_language/zh-CN/" /tmp/nezha-config.yaml
sed -i "s/nz_site_title/${nz_site_title}/" /tmp/nezha-config.yaml
sed -i "s/80/${nz_site_port}/" /tmp/nezha-config.yaml
mkdir -p $NZ_DASHBOARD_PATH/data
mv -f /tmp/nezha-config.yaml ${NZ_DASHBOARD_PATH}/data/config.yaml
echo -e "Downloading service file"
if [ "$os_alpine" != 1 ]; then
wget -t 2 -T 10 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1
else
wget -t 2 -T 10 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1
chmod +x $NZ_DASHBOARD_SERVICERC
if [[ $? != 0 ]]; then
echo -e "${red}File failed to get, please check if the network can link ${GITHUB_RAW_URL}${plain}"
return 0
fi
fi
echo -e "Dashboard configuration ${green} modified successfully, please wait for Dashboard self-restart to take effect${plain}"
restart_and_update_standalone
if [[ $# == 0 ]]; then
before_show_menu
fi
}
restart_and_update() { restart_and_update() {
echo -e "> Restart and Update the Panel" echo -e "> Restart and Update the Panel"
@@ -420,6 +562,40 @@ restart_and_update() {
fi fi
} }
restart_and_update_standalone() {
echo -e "> Restart and Update the Panel"
cd $NZ_DASHBOARD_PATH
if [ "$os_alpine" != 1 ]; then
systemctl stop nezha-dashboard
else
rc-service nezha-dashboard stop
fi
wget -qO app.zip https://${GITHUB_URL}/naiba/nezha/releases/latest/download/dashboard-linux-$os_arch.zip >/dev/null 2>&1 && unzip -qq app.zip && mv dist/dashboard-linux-$os_arch app && rm -r app.zip dist
if [ "$os_alpine" != 1 ]; then
systemctl daemon-reload
systemctl enable nezha-dashboard
systemctl restart nezha-dashboard
else
rc-update add nezha-dashboard
rc-service nezha-dashboard restart
fi
if [[ $? == 0 ]]; then
echo -e "${green}Nezha Monitoring Restart Successful${plain}"
echo -e "Default panel address: ${yellow}domain:Site_access_port${plain}"
else
echo -e "${red}The restart failed, probably because the boot time exceeded two seconds, please check the log information later${plain}"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
}
start_dashboard() { start_dashboard() {
echo -e "> Start Panel" echo -e "> Start Panel"
@@ -441,6 +617,26 @@ start_dashboard() {
fi fi
} }
start_dashboard_standalone() {
echo -e "> Start Panel"
if [ "$os_alpine" != 1 ]; then
systemctl start nezha-dashboard
else
rc-service nezha-dashboard start
fi
if [[ $? == 0 ]]; then
echo -e "${green}Nezha Monitoring Start Successful${plain}"
else
echo -e "${red}Failed to start, please check the log message later${plain}"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
}
stop_dashboard() { stop_dashboard() {
echo -e "> Stop Panel" echo -e "> Stop Panel"
@@ -462,6 +658,26 @@ stop_dashboard() {
fi fi
} }
stop_dashboard_standalone() {
echo -e "> Stop Panel"
if [ "$os_alpine" != 1 ]; then
systemctl stop nezha-dashboard
else
rc-service nezha-dashboard stop
fi
if [[ $? == 0 ]]; then
echo -e "${green}Nezha Monitoring Stop Successful${plain}"
else
echo -e "${red}Failed to stop, please check the log message later${plain}"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
}
show_dashboard_log() { show_dashboard_log() {
echo -e "> View Panel Log" echo -e "> View Panel Log"
@@ -477,6 +693,20 @@ show_dashboard_log() {
fi fi
} }
show_dashboard_log_standalone() {
echo -e "> View Panel Log"
if [ "$os_alpine" != 1 ]; then
journalctl -xf -u nezha-dashboard.service
else
echo "Sorry, OpenRC do not have this feature yet."
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
}
uninstall_dashboard() { uninstall_dashboard() {
echo -e "> Uninstall Panel" echo -e "> Uninstall Panel"
@@ -488,7 +718,32 @@ uninstall_dashboard() {
fi fi
rm -rf $NZ_DASHBOARD_PATH rm -rf $NZ_DASHBOARD_PATH
docker rmi -f ghcr.io/naiba/nezha-dashboard > /dev/null 2>&1 docker rmi -f ghcr.io/naiba/nezha-dashboard >/dev/null 2>&1
docker rmi -f registry.cn-shanghai.aliyuncs.com/naibahq/nezha-dashboard >/dev/null 2>&1
clean_all
if [[ $# == 0 ]]; then
before_show_menu
fi
}
uninstall_dashboard_standalone() {
echo -e "> Uninstall Panel"
rm -rf $NZ_DASHBOARD_PATH
if [ "$os_alpine" != 1 ]; then
systemctl stop nezha-dashboard
else
rc-service nezha-dashboard stop
fi
if [ "$os_alpine" != 1 ]; then
rm $NZ_DASHBOARD_SERVICE
else
rm $NZ_DASHBOARD_SERVICERC
fi
clean_all clean_all
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
@@ -497,9 +752,13 @@ uninstall_dashboard() {
} }
show_agent_log() { show_agent_log() {
echo -e "> View Agent Log" echo -e "> > View Agent Log"
if [ "$os_alpine" != 1 ]; then
journalctl -xf -u nezha-agent.service journalctl -xf -u nezha-agent.service
else
echo "Sorry, OpenRC do not have this feature yet."
fi
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
before_show_menu before_show_menu
@@ -509,14 +768,15 @@ show_agent_log() {
uninstall_agent() { uninstall_agent() {
echo -e "> Uninstall Agent" echo -e "> Uninstall Agent"
if [ "$os_alpine" != 1 ];then if [ "$os_alpine" != 1 ]; then
systemctl disable nezha-agent.service systemctl disable nezha-agent.service
systemctl stop nezha-agent.service systemctl stop nezha-agent.service
rm -rf $NZ_AGENT_SERVICE rm -rf $NZ_AGENT_SERVICE
systemctl daemon-reload systemctl daemon-reload
else else
grep -q 'nezha-agent' /var/spool/cron/crontabs/root && sed -i "/nezha-agent/d" /var/spool/cron/crontabs/root rc-update del nezha-agent
pkill nezha-agent rc-service nezha-agent stop
rm -rf $NZ_AGENT_SERVICERC
fi fi
rm -rf $NZ_AGENT_PATH rm -rf $NZ_AGENT_PATH
@@ -530,7 +790,11 @@ uninstall_agent() {
restart_agent() { restart_agent() {
echo -e "> Restart Agent" echo -e "> Restart Agent"
if [ "$os_alpine" != 1 ]; then
systemctl restart nezha-agent.service systemctl restart nezha-agent.service
else
rc-service nezha-agent restart
fi
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
before_show_menu before_show_menu
@@ -543,6 +807,59 @@ clean_all() {
fi fi
} }
select_version() {
DOCKER_COMPOSE_COMMAND=""
if docker compose version >/dev/null 2>&1; then
DOCKER_COMPOSE_COMMAND="docker compose"
if $DOCKER_COMPOSE_COMMAND ls | grep -qw "$NZ_DASHBOARD_PATH/docker-compose.yaml" >/dev/null 2>&1; then
NEZHA_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -w "nezha-dashboard")
if [ -n "$NEZHA_IMAGES" ]; then
echo "Docker image with nezha-dashboard repository exists:"
echo "$NEZHA_IMAGES"
IS_DOCKER_NEZHA=1
return
else
echo "No Docker images with the nezha-dashboard repository were found."
fi
fi
elif command -v docker-compose >/dev/null 2>&1; then
DOCKER_COMPOSE_COMMAND="docker-compose"
if $DOCKER_COMPOSE_COMMAND -f "$NZ_DASHBOARD_PATH/docker-compose.yaml" config >/dev/null 2>&1; then
NEZHA_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -w "nezha-dashboard")
if [ -n "$NEZHA_IMAGES" ]; then
echo "Docker image with nezha-dashboard repository exists:"
echo "$NEZHA_IMAGES"
IS_DOCKER_NEZHA=1
return
else
echo "No Docker images with the nezha-dashboard repository were found."
fi
fi
fi
if [[ -f $NZ_DASHBOARD_PATH/app ]]; then
IS_DOCKER_NEZHA=0
else
echo -e "${yellow}Select your installation method(Input anything is ok if you are installing agent):\n1. Docker\n2. Standalone${plain}"
while true; do
read -e -r -p "Input a number [1-2]: " option
case "${option}" in
1)
IS_DOCKER_NEZHA=1
break
;;
2)
IS_DOCKER_NEZHA=0
break
;;
*)
echo "Wrong input, try again"
;;
esac
done
fi
}
show_usage() { show_usage() {
echo "Nezha Monitor Management Script Usage: " echo "Nezha Monitor Management Script Usage: "
echo "--------------------------------------------------------" echo "--------------------------------------------------------"
@@ -587,7 +904,7 @@ show_menu() {
${green}0.${plain} Exit Script ${green}0.${plain} Exit Script
" "
echo && read -ep "Please enter [0-13]: " num echo && read -ep "Please enter [0-13]: " num
if [[ $IS_DOCKER_NEZHA == 1 ]]; then
case "${num}" in case "${num}" in
0) 0)
exit 0 exit 0
@@ -635,11 +952,61 @@ show_menu() {
echo -e "${red}Please enter the correct number [0-13]${plain}" echo -e "${red}Please enter the correct number [0-13]${plain}"
;; ;;
esac esac
elif [[ $IS_DOCKER_NEZHA == 0 ]]; then
case "${num}" in
0)
exit 0
;;
1)
install_dashboard_standalone
;;
2)
modify_dashboard_config_standalone
;;
3)
start_dashboard_standalone
;;
4)
stop_dashboard_standalone
;;
5)
restart_and_update_standalone
;;
6)
show_dashboard_log_standalone
;;
7)
uninstall_dashboard_standalone
;;
8)
install_agent
;;
9)
modify_agent_config
;;
10)
show_agent_log
;;
11)
uninstall_agent
;;
12)
restart_agent
;;
13)
update_script
;;
*)
echo -e "${red}Please enter the correct number [0-13]${plain}"
;;
esac
fi
} }
pre_check pre_check
if [[ $# > 0 ]]; then if [[ $# > 0 ]]; then
if [[ $IS_DOCKER_NEZHA == 1 ]]; then
case $1 in case $1 in
"install_dashboard") "install_dashboard")
install_dashboard 0 install_dashboard 0
@@ -687,6 +1054,83 @@ if [[ $# > 0 ]]; then
;; ;;
*) show_usage ;; *) show_usage ;;
esac esac
elif [[ $IS_DOCKER_NEZHA == 0 ]]; then
case $1 in
"install_dashboard")
install_dashboard_standalone 0
;;
"modify_dashboard_config")
modify_dashboard_config_standalone 0
;;
"start_dashboard")
start_dashboard_standalone 0
;;
"stop_dashboard")
stop_dashboard_standalone 0
;;
"restart_and_update")
restart_and_update_standalone 0
;;
"show_dashboard_log")
show_dashboard_log_standalone 0
;;
"uninstall_dashboard")
uninstall_dashboard_standalone 0
;;
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
;;
"modify_agent_config")
modify_agent_config 0
;;
"show_agent_log")
show_agent_log 0
;;
"uninstall_agent")
uninstall_agent 0
;;
"restart_agent")
restart_agent 0
;;
"update_script")
update_script 0
;;
*) show_usage ;;
esac
else
case $1 in
"install_agent")
shift
if [ $# -ge 3 ]; then
install_agent "$@"
else
install_agent 0
fi
;;
"modify_agent_config")
modify_agent_config 0
;;
"show_agent_log")
show_agent_log 0
;;
"uninstall_agent")
uninstall_agent 0
;;
"restart_agent")
restart_agent 0
;;
"update_script")
update_script 0
;;
*) show_usage ;;
esac
fi
else else
select_version
show_menu show_menu
fi fi
+62
View File
@@ -0,0 +1,62 @@
#!/sbin/openrc-run
supervisor=supervise-daemon
name=nezha-agent
supervise_daemon_args="--stdout /var/log/${name}.log --stderr /var/log/${name}.err"
SERVER="nz_grpc_host:nz_grpc_port" #Dashboard 地址 ip:port
SECRET="nz_client_secret" #SECRET
NZ_BASE_PATH="/opt/nezha"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
pidfile="/run/${RC_SVCNAME}.pid"
command="/opt/nezha/agent/nezha-agent"
command_args="-s ${SERVER} -p ${SECRET}"
command_background=true
depend() {
need net
}
checkconfig() {
GITHUB_URL="github.com"
if [ ! -f "${NZ_AGENT_PATH}/nezha-agent" ]; then
if [[ $(uname -m | grep 'x86_64') != "" ]]; then
os_arch="amd64"
elif [[ $(uname -m | grep 'i386\|i686') != "" ]]; then
os_arch="386"
elif [[ $(uname -m | grep 'aarch64\|armv8b\|armv8l') != "" ]]; then
os_arch="arm64"
elif [[ $(uname -m | grep 'arm') != "" ]]; then
os_arch="arm"
elif [[ $(uname -m | grep 's390x') != "" ]]; then
os_arch="s390x"
elif [[ $(uname -m | grep 'riscv64') != "" ]]; then
os_arch="riscv64"
fi
local version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
fi
if [ ! -n "$version" ]; then
echo -e "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/nezhahq/agent/releases/latest"
return 0
else
echo -e "当前最新版本为: ${version}"
fi
wget -t 2 -T 10 -O nezha-agent_linux_${os_arch}.zip https://${GITHUB_URL}/nezhahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip >/dev/null 2>&1
if [[ $? != 0 ]]; then
echo -e "Release 下载失败,请检查本机能否连接 ${GITHUB_URL}${plain}"
return 0
fi
mkdir -p $NZ_AGENT_PATH
chmod 755 -R $NZ_AGENT_PATH
unzip -qo nezha-agent_linux_${os_arch}.zip && mv nezha-agent $NZ_AGENT_PATH && rm -rf nezha-agent_linux_${os_arch}.zip README.md
fi
if [ ! -x "${NZ_AGENT_PATH}/nezha-agent" ]; then
chmod +x ${NZ_AGENT_PATH}/nezha-agent
fi
}
start_pre() {
if [ "${RC_CMD}" != "restart" ]; then
checkconfig || return $?
fi
}
+16
View File
@@ -0,0 +1,16 @@
#!/sbin/openrc-run
pidfile="/run/${RC_SVCNAME}.pid"
command="/opt/nezha/dashboard/app"
command_args=""
command_background=true
directory="/opt/nezha/dashboard"
depend() {
need net
}
reload() {
ebegin "Reloading ${RC_SVCNAME}"
start-stop-daemon --signal HUP --pidfile "${pidfile}"
eend $?
}
+3 -3
View File
@@ -1,5 +1,5 @@
[Unit] [Unit]
Description=Nezha Dashborad Description=Nezha Dashboard
After=syslog.target After=syslog.target
After=network.target After=network.target
After=mariadb.service mysqld.service postgresql.service memcached.service redis.service After=mariadb.service mysqld.service postgresql.service memcached.service redis.service
@@ -14,8 +14,8 @@ After=mariadb.service mysqld.service postgresql.service memcached.service redis.
Type=simple Type=simple
#User=root #User=root
#Group=root #Group=root
WorkingDirectory=/root/nezha/dashboard/ WorkingDirectory=/opt/nezha/dashboard/
ExecStart=/root/nezha/dashboard/nezha-dashboard-linux-amd64 ExecStart=/opt/nezha/dashboard/app
Restart=always Restart=always
#Environment=DEBUG=true #Environment=DEBUG=true
+65 -1
View File
@@ -1,9 +1,11 @@
package singleton package singleton
import ( import (
"sync"
"time"
"github.com/naiba/nezha/model" "github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/utils" "github.com/naiba/nezha/pkg/utils"
"sync"
) )
var ( var (
@@ -12,6 +14,9 @@ var (
ApiLock sync.RWMutex ApiLock sync.RWMutex
ServerAPI = &ServerAPIService{} ServerAPI = &ServerAPIService{}
MonitorAPI = &MonitorAPIService{}
once = &sync.Once{}
) )
type ServerAPIService struct{} type ServerAPIService struct{}
@@ -51,6 +56,23 @@ type ServerInfoResponse struct {
Result []*CommonServerInfo `json:"result"` Result []*CommonServerInfo `json:"result"`
} }
type MonitorAPIService struct {
}
type MonitorInfoResponse struct {
CommonResponse
Result []*MonitorInfo `json:"result"`
}
type MonitorInfo struct {
MonitorID uint64 `json:"monitor_id"`
ServerID uint64 `json:"server_id"`
MonitorName string `json:"monitor_name"`
ServerName string `json:"server_name"`
CreatedAt []int64 `json:"created_at"`
AvgDelay []float32 `json:"avg_delay"`
}
func InitAPI() { func InitAPI() {
ApiTokenList = make(map[string]*model.ApiToken) ApiTokenList = make(map[string]*model.ApiToken)
UserIDToApiTokenList = make(map[uint64][]string) UserIDToApiTokenList = make(map[uint64][]string)
@@ -203,3 +225,45 @@ func (s *ServerAPIService) GetAllList() *ServerInfoResponse {
} }
return res return res
} }
func (m *MonitorAPIService) GetMonitorHistories(query map[string]any) *MonitorInfoResponse {
var (
resultMap = make(map[uint64]*MonitorInfo)
monitorHistories []*model.MonitorHistory
sortedMonitorIDs []uint64
)
res := &MonitorInfoResponse{
CommonResponse: CommonResponse{
Code: 0,
Message: "success",
},
}
if err := DB.Model(&model.MonitorHistory{}).Select("monitor_id, created_at, server_id, avg_delay").
Where(query).Where("created_at >= ?", time.Now().Add(-24*time.Hour)).Order("monitor_id, created_at").
Scan(&monitorHistories).Error; err != nil {
res.CommonResponse = CommonResponse{
Code: 500,
Message: err.Error(),
}
} else {
for _, history := range monitorHistories {
infos, ok := resultMap[history.MonitorID]
if !ok {
infos = &MonitorInfo{
MonitorID: history.MonitorID,
ServerID: history.ServerID,
MonitorName: ServiceSentinelShared.monitors[history.MonitorID].Name,
ServerName: ServerList[history.ServerID].Name,
}
resultMap[history.MonitorID] = infos
sortedMonitorIDs = append(sortedMonitorIDs, history.MonitorID)
}
infos.CreatedAt = append(infos.CreatedAt, history.CreatedAt.Truncate(time.Minute).Unix()*1000)
infos.AvgDelay = append(infos.AvgDelay, history.AvgDelay)
}
for _, monitorID := range sortedMonitorIDs {
res.Result = append(res.Result, resultMap[monitorID])
}
}
return res
}
+62 -7
View File
@@ -8,9 +8,10 @@ import (
"sync" "sync"
"time" "time"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/naiba/nezha/model" "github.com/naiba/nezha/model"
pb "github.com/naiba/nezha/proto" pb "github.com/naiba/nezha/proto"
"github.com/nicksnyder/go-i18n/v2/i18n"
) )
const ( const (
@@ -36,12 +37,13 @@ func NewServiceSentinel(serviceSentinelDispatchBus chan<- model.Monitor) {
ServiceSentinelShared = &ServiceSentinel{ ServiceSentinelShared = &ServiceSentinel{
serviceReportChannel: make(chan ReportData, 200), serviceReportChannel: make(chan ReportData, 200),
serviceStatusToday: make(map[uint64]*_TodayStatsOfMonitor), serviceStatusToday: make(map[uint64]*_TodayStatsOfMonitor),
serviceCurrentStatusIndex: make(map[uint64]int), serviceCurrentStatusIndex: make(map[uint64]*indexStore),
serviceCurrentStatusData: make(map[uint64][]*pb.TaskResult), serviceCurrentStatusData: make(map[uint64][]*pb.TaskResult),
lastStatus: make(map[uint64]int), lastStatus: make(map[uint64]int),
serviceResponseDataStoreCurrentUp: make(map[uint64]uint64), serviceResponseDataStoreCurrentUp: make(map[uint64]uint64),
serviceResponseDataStoreCurrentDown: make(map[uint64]uint64), serviceResponseDataStoreCurrentDown: make(map[uint64]uint64),
serviceResponseDataStoreCurrentAvgDelay: make(map[uint64]float32), serviceResponseDataStoreCurrentAvgDelay: make(map[uint64]float32),
serviceResponsePing: make(map[uint64]map[uint64]*pingStore),
monitors: make(map[uint64]*model.Monitor), monitors: make(map[uint64]*model.Monitor),
sslCertCache: make(map[uint64]string), sslCertCache: make(map[uint64]string),
// 30天数据缓存 // 30天数据缓存
@@ -95,11 +97,12 @@ type ServiceSentinel struct {
serviceResponseDataStoreLock sync.RWMutex serviceResponseDataStoreLock sync.RWMutex
serviceStatusToday map[uint64]*_TodayStatsOfMonitor // [monitor_id] -> _TodayStatsOfMonitor serviceStatusToday map[uint64]*_TodayStatsOfMonitor // [monitor_id] -> _TodayStatsOfMonitor
serviceCurrentStatusIndex map[uint64]int // [monitor_id] -> 该监控ID对应的 serviceCurrentStatusData 的最新索引下标 serviceCurrentStatusIndex map[uint64]*indexStore // [monitor_id] -> 该监控ID对应的 serviceCurrentStatusData 的最新索引下标
serviceCurrentStatusData map[uint64][]*pb.TaskResult // [monitor_id] -> []model.MonitorHistory serviceCurrentStatusData map[uint64][]*pb.TaskResult // [monitor_id] -> []model.MonitorHistory
serviceResponseDataStoreCurrentUp map[uint64]uint64 // [monitor_id] -> 当前服务在线计数 serviceResponseDataStoreCurrentUp map[uint64]uint64 // [monitor_id] -> 当前服务在线计数
serviceResponseDataStoreCurrentDown map[uint64]uint64 // [monitor_id] -> 当前服务离线计数 serviceResponseDataStoreCurrentDown map[uint64]uint64 // [monitor_id] -> 当前服务离线计数
serviceResponseDataStoreCurrentAvgDelay map[uint64]float32 // [monitor_id] -> 当前服务离线计数 serviceResponseDataStoreCurrentAvgDelay map[uint64]float32 // [monitor_id] -> 当前服务离线计数
serviceResponsePing map[uint64]map[uint64]*pingStore // [monitor_id] -> ClientID -> delay
lastStatus map[uint64]int lastStatus map[uint64]int
sslCertCache map[uint64]string sslCertCache map[uint64]string
@@ -111,6 +114,16 @@ type ServiceSentinel struct {
monthlyStatus map[uint64]*model.ServiceItemResponse // [monitor_id] -> model.ServiceItemResponse monthlyStatus map[uint64]*model.ServiceItemResponse // [monitor_id] -> model.ServiceItemResponse
} }
type indexStore struct {
index int
t time.Time
}
type pingStore struct {
count int
ping float32
}
func (ss *ServiceSentinel) refreshMonthlyServiceStatus() { func (ss *ServiceSentinel) refreshMonthlyServiceStatus() {
// 刷新数据防止无人访问 // 刷新数据防止无人访问
ss.LoadStats() ss.LoadStats()
@@ -326,6 +339,34 @@ func (ss *ServiceSentinel) worker() {
continue continue
} }
mh := r.Data mh := r.Data
if mh.Type == model.TaskTypeTCPPing || mh.Type == model.TaskTypeICMPPing {
monitorTcpMap, ok := ss.serviceResponsePing[mh.GetId()]
if !ok {
monitorTcpMap = make(map[uint64]*pingStore)
ss.serviceResponsePing[mh.GetId()] = monitorTcpMap
}
ts, ok := monitorTcpMap[r.Reporter]
if !ok {
ts = &pingStore{}
}
ts.count++
ts.ping = (ts.ping*float32(ts.count-1) + mh.Delay) / float32(ts.count)
if ts.count == Conf.AvgPingCount {
if ts.ping > float32(Conf.MaxTCPPingValue) {
ts.ping = float32(Conf.MaxTCPPingValue)
}
ts.count = 0
if err := DB.Create(&model.MonitorHistory{
MonitorID: mh.GetId(),
AvgDelay: ts.ping,
Data: mh.Data,
ServerID: r.Reporter,
}).Error; err != nil {
log.Println("NEZHA>> 服务监控数据持久化失败:", err)
}
}
monitorTcpMap[r.Reporter] = ts
}
ss.serviceResponseDataStoreLock.Lock() ss.serviceResponseDataStoreLock.Lock()
// 写入当天状态 // 写入当天状态
if mh.Successful { if mh.Successful {
@@ -336,9 +377,20 @@ func (ss *ServiceSentinel) worker() {
} else { } else {
ss.serviceStatusToday[mh.GetId()].Down++ ss.serviceStatusToday[mh.GetId()].Down++
} }
currentTime := time.Now()
if ss.serviceCurrentStatusIndex[mh.GetId()] == nil {
ss.serviceCurrentStatusIndex[mh.GetId()] = &indexStore{
t: currentTime,
index: 0,
}
}
// 写入当前数据 // 写入当前数据
ss.serviceCurrentStatusData[mh.GetId()][ss.serviceCurrentStatusIndex[mh.GetId()]] = mh if ss.serviceCurrentStatusIndex[mh.GetId()].t.Before(currentTime) {
ss.serviceCurrentStatusIndex[mh.GetId()]++ ss.serviceCurrentStatusIndex[mh.GetId()].t = currentTime.Add(30 * time.Second)
ss.serviceCurrentStatusData[mh.GetId()][ss.serviceCurrentStatusIndex[mh.GetId()].index] = mh
ss.serviceCurrentStatusIndex[mh.GetId()].index++
}
// 更新当前状态 // 更新当前状态
ss.serviceResponseDataStoreCurrentUp[mh.GetId()] = 0 ss.serviceResponseDataStoreCurrentUp[mh.GetId()] = 0
@@ -365,8 +417,11 @@ func (ss *ServiceSentinel) worker() {
stateCode := GetStatusCode(upPercent) stateCode := GetStatusCode(upPercent)
// 数据持久化 // 数据持久化
if ss.serviceCurrentStatusIndex[mh.GetId()] == _CurrentStatusSize { if ss.serviceCurrentStatusIndex[mh.GetId()].index == _CurrentStatusSize {
ss.serviceCurrentStatusIndex[mh.GetId()] = 0 ss.serviceCurrentStatusIndex[mh.GetId()] = &indexStore{
index: 0,
t: currentTime,
}
if err := DB.Create(&model.MonitorHistory{ if err := DB.Create(&model.MonitorHistory{
MonitorID: mh.GetId(), MonitorID: mh.GetId(),
AvgDelay: ss.serviceResponseDataStoreCurrentAvgDelay[mh.GetId()], AvgDelay: ss.serviceResponseDataStoreCurrentAvgDelay[mh.GetId()],
+4
View File
@@ -99,6 +99,10 @@ func RecordTransferHourlyUsage() {
func CleanMonitorHistory() { func CleanMonitorHistory() {
// 清理已被删除的服务器的监控记录与流量记录 // 清理已被删除的服务器的监控记录与流量记录
DB.Unscoped().Delete(&model.MonitorHistory{}, "created_at < ? OR monitor_id NOT IN (SELECT `id` FROM monitors)", time.Now().AddDate(0, 0, -30)) DB.Unscoped().Delete(&model.MonitorHistory{}, "created_at < ? OR monitor_id NOT IN (SELECT `id` FROM monitors)", time.Now().AddDate(0, 0, -30))
// 由于网络监控记录的数据较多,并且前端仅使用了 1 天的数据
// 考虑到 sqlite 数据量问题,仅保留一天数据,
// server_id = 0 的数据会用于/service页面的可用性展示
DB.Unscoped().Delete(&model.MonitorHistory{}, "(created_at < ? AND server_id != 0) OR monitor_id NOT IN (SELECT `id` FROM monitors)", time.Now().AddDate(0, 0, -1))
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (SELECT `id` FROM servers)") DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (SELECT `id` FROM servers)")
// 计算可清理流量记录的时长 // 计算可清理流量记录的时长
var allServerKeep time.Time var allServerKeep time.Time