Compare commits

...
62 Commits
Author SHA1 Message Date
nap0o f32f127dfc improve: status-server主题network页 (#422) 2024-09-11 21:50:40 +08:00
nap0o f78ba281fb Default主题fixbug (#421)
* default主题fixbug
1.补上误删的网页title
2.修复network页纵坐标数值显示不全
3.修复network,多个legend导致的遮挡主图表问题

* fix

* fix

* fix

* fix

* fix

* fix
2024-09-07 14:44:30 +08:00
github-actions[bot] 7f9f9a9129 update contributors[no ci] 2024-09-06 15:31:54 +00:00
nap0o b701efd9b5 server-status和default主题:feat & improve & fix (#420)
* feat & improve & fix
1. 增加WebAppManifest文件,实现把哪吒监控网页伪装成app放在移动端桌面
2. vps地图分布图增加数据异步加载loading效果
3. 修复echart图表纵坐标轴因数值过大显示不全bug

* 刷新CDN缓存

* fix footer

* 4.提升service页echarts图表相关体验
5.用设置基准1vh的方法解决footer页脚位置在移动端各种浏览器显示不一致的问题
6.修复section标签的使用位置

* 刷新CDN缓存
2024-09-06 23:31:38 +08:00
UUBulb 511f43784e fix dashboard install script (#419) 2024-09-03 08:54:07 +08:00
UUBulb 3f6112a919 fix: docker image (#418) 2024-09-02 23:19:43 +08:00
UUBulb 23751440c6 [ci][docker]: use matrix to reduce build time & change base image to busybox (#417)
* ci: use matrix to reduce build time

* docker: change base image to busybox

* reuse workflows

* only download ipinfo db on tag pushes

* goreleaser
2024-09-02 22:13:13 +08:00
UUBulb 989c9f9740 fix/fm: reset upload state after completion (#416) 2024-08-28 23:23:23 +08:00
UUBulb 71508f8724 修复多CPU在默认主题和AngelKanade下显示的问题,完善内网穿透本地化文本 (#415) 2024-08-25 11:33:25 +08:00
UUBulb eb6dd2855e refactor: ddns (#414)
* refactor ddns

* update webhook
2024-08-24 11:11:06 +08:00
naiba 64da3c7438 chore: back to attach addon 2024-08-21 23:42:05 +08:00
naiba fd85f599ba chore: 移除 trzsz 支持 (不能上传) 2024-08-21 22:20:47 +08:00
UUBulb cae443d5c8 fm: store file to OPFS temporarily (#413) 2024-08-21 22:19:40 +08:00
github-actions[bot] 0ec1bb2c54 update contributors[no ci] 2024-08-20 14:25:53 +00:00
UUBulb 47f8447a22 feat: framed fm for webshell (#411)
* feat: framed fm for webshell

* 1MB buffer
2024-08-20 22:25:29 +08:00
UUBulb 9c986d06cb ci增加重传机制 (#410) 2024-08-18 08:56:52 +08:00
nap0o e9428d5757 improve & fix : 主题优化及bug修复 (#409)
* 主题优化及bug修复

default主题
1.修复主box过宽导致的系列问题(pc和移动端)
2.network页面适配深色模式和浅色模式

status-server主题
1. network页面折线图取数逻辑优化,丢包率一直是100%时,不显示丢包markline线
2. 隐藏所有table横向滚动条
3. 关闭折线图hover效果,大幅提升echarts图表渲染速度
4. 移动端页底显示位置优化

* 刷新cdn缓存

* 修复default深色模式在ios设备不生效bug

* 1. 恢复主baox 1680px宽度,用其他方式修复上一版存在bug(移动端左右留空不一致等)
2. 首页echarts自动适配深色浅色模式

* fix
2024-08-17 11:14:28 +08:00
naiba daab64d232 feat: 管理后台自定义代码 2024-08-14 00:24:17 +08:00
naiba da8fb57268 feat: 默认主题 dark mode 2024-08-13 23:50:55 +08:00
naiba fda0dad5f1 chore: set ws binaryType 2024-08-13 13:53:44 +08:00
github-actions[bot] c6fb3196ea update contributors[no ci] 2024-08-13 05:51:41 +00:00
hiDandelion 711d162a46 Add replacing fields to notification content in notification.go (#408) 2024-08-13 13:51:19 +08:00
naiba d667ce8afe feat: WebSSH 上传下载文件 (by trzsz) 2024-08-13 00:01:39 +08:00
naiba 6abc0fce51 fix: 流量统计异常 2024-08-12 10:06:55 +08:00
naiba b1d77a1d27 improve: 优化可能造成流量统计异常的情况 2024-08-11 10:35:19 +08:00
nap0o c18e0e420e server-status主题重写network页面 (#407)
* 修复浏览器高分辨率下网络页echart图表显示不全

* server-status主题重写network页面
1. 点击“网络监控”弹出选择服务器
2. 增加服务器搜索筛选功能
3. 自适应lenged高度
4. 适配深色模式与浅色模式
5. 适配半透明模式
6. 适配pc段与移动端

* 刷新cdn缓存

* 增加disposeCharts逻辑

* 增加disposeCharts逻辑

* 标题增加服务器所在地区国旗展示

* 国旗样式修复

* 修复国旗样式

* echart图标高度适配不同分辨率浏览器
2024-08-10 23:19:52 +08:00
nap0o d9097540c3 server-status主题优化 (#405)
* server-status主题优化
1.前台增加半透明样式切换按钮,同时适配了深色和浅色模式
2.右小角功能区增加收纳,点击展开,滚动页面关闭
3.vps世界分布地图全屏展示
4.修改默认页底版权信息位置,从跟随container box修改为默认置于页面底部,container box到底底部后再跟随
5.修改container box默认宽度和最大宽度,适配高分辨率显示器
6.优化样式文件结构,删除无用的样式文件
7.一些小优化

* 修改默认背景图

* 修改背景图片

* 1.echart图表适配半透明样式切换
2.echart图表移动端设置更细折线
3.修改半透明样式下的默认背景图

* echart Y轴移动端更细刻度线

* fixbug

* 修改半透明样式默认背景图片为本地调用
2024-08-09 21:49:45 +08:00
UUBulb 6b05a21641 ws: ignore all DDNS fields (#406) 2024-08-09 21:47:29 +08:00
UUBulb f289b76f47 installer: fix geo_check (#404) 2024-08-05 10:21:14 +08:00
github-actions[bot] 89fe4ec621 update contributors[no ci] 2024-08-03 01:35:15 +00:00
Mingzhuo Yin fca2f60380 chore: missing gpu info line break in theme-angel-kanade (#403) 2024-08-03 09:34:57 +08:00
naiba 6a70e67ef9 feat: 在面板重启时通知agent更新信息 2024-08-02 19:41:39 +08:00
naiba 22842ebc75 chore: typo 2024-07-28 23:39:43 +08:00
UUBulb aef968250f chore: wrong name (#399) 2024-07-28 23:17:25 +08:00
UUBulb 100600d594 ci: only keep the latest release (#398) 2024-07-28 23:08:16 +08:00
naiba fe234f4f38 improve: Handle case when host is not found in LookupGeoIP 2024-07-28 15:11:50 +08:00
naiba df9487b5fc fix: CountryCode 被冲掉 2024-07-28 14:39:14 +08:00
UUBulbandnaiba 26d81f6d7a feat: use embed geoip database (#396)
* feat: use embed geoip database

* mention ipinfo

* only read once

* comments

comments

* chore: installer version

---------

Co-authored-by: naiba <[email protected]>
2024-07-28 13:59:58 +08:00
UUBulb 9d96c58ff9 api: add DisPlayIndex info (#397) 2024-07-28 13:43:28 +08:00
nap0o 5bb7efdeb9 🎉 通过自定义代码实现server-status主题深色模式半透明样式的前置准备 (#395) 2024-07-24 23:15:17 +08:00
github-actions[bot] 519882c4a9 update contributors[no ci] 2024-07-23 10:27:07 +00:00
MadDogOwner ed96432b79 installer: improve region detection reliability using multiple APIs (#394) 2024-07-23 18:26:45 +08:00
github-actions[bot] 1c057d93bd update contributors[no ci] 2024-07-19 22:49:08 +00:00
UUBulb 6005004681 installer: use multiple geoip api to determine location (#393) 2024-07-20 06:48:53 +08:00
Spetrum d8f4236404 Switch IP checking service to ip-api.com (#391) 2024-07-17 21:21:04 +08:00
UUBulb f95191c8af fix: concurrent write to single WebSocket connection (#392) 2024-07-17 10:06:23 +08:00
naiba 34fedd91d2 fix: dashboard standalon-alone install 2024-07-15 16:01:24 +08:00
naiba f675b884f5 feat: fetch version from api for dashboard standalone install 2024-07-14 22:52:53 +08:00
naiba 0b13fec029 fix: workflow branch name 2024-07-14 19:52:04 +08:00
naiba 67b788a969 内置HTTP内网穿透 2024-07-14 19:41:50 +08:00
naiba b63f693661 fix: branch name 2024-07-14 14:46:08 +08:00
github-actions[bot] ac64aa7355 update contributors[no ci] 2024-07-14 06:38:29 +00:00
UUBulb 05d69d5c07 ci: sync release to gitee (#390) 2024-07-14 14:38:12 +08:00
naiba ce624a0ca6 fix: loop delay 2024-07-14 13:04:35 +08:00
naiba 1c91fcffac feat: 去除 webTerminal 的 websocket 依赖 2024-07-14 12:47:36 +08:00
UUBulb 417f972659 installer: restore the China mirror of agent (#389) 2024-07-14 07:34:45 +08:00
naiba ada344cd6b chore: go 1.21.x 2024-07-13 13:11:40 +08:00
naiba 9d6682fd58 fix: build 2024-07-13 13:00:48 +08:00
github-actions[bot] 22c687c044 update contributors[no ci] 2024-07-13 04:52:14 +00:00
Tao Chen e1513203b4 添加OIDC支持 (#387)
* add general OIDC

* use "github.com/coreos/go-oidc/v3/oidc" to simplify oidc config

* fix: check if https by X-Forwarded-Proto

* recovery config.yaml
2024-07-13 12:51:59 +08:00
nap0o 17373e2dee 🐛 fixbug (#388)
* 修复bug:在server-status主题无法切换

* 替换哪吒面板后台cdn提供商为unpkg

* 替换哪吒面板后台cdn提供商为unpkg

* 加快Agent详情下拉展示速度
2024-07-12 22:29:11 +08:00
github-actions[bot] e7c6984c72 update contributors[no ci] 2024-07-10 15:14:11 +00:00
117 changed files with 5027 additions and 2009 deletions
+135
View File
@@ -0,0 +1,135 @@
import os
import time
import requests
import hashlib
from github import Github
def get_github_latest_release():
g = Github()
repo = g.get_repo("naiba/nezha")
release = repo.get_latest_release()
if release:
print(f"Latest release tag is: {release.tag_name}")
print(f"Latest release info is: {release.body}")
files = []
for asset in release.get_assets():
url = asset.browser_download_url
name = asset.name
response = requests.get(url)
if response.status_code == 200:
with open(name, 'wb') as f:
f.write(response.content)
print(f"Downloaded {name}")
else:
print(f"Failed to download {name}")
file_abs_path = get_abs_path(asset.name)
files.append(file_abs_path)
sync_to_gitee(release.tag_name, release.body, files)
else:
print("No releases found.")
def delete_gitee_releases(latest_id, client, uri, token):
get_data = {
'access_token': token
}
release_info = []
release_response = client.get(uri, json=get_data)
if release_response.status_code == 200:
release_info = release_response.json()
else:
print(
f"Request failed with status code {release_response.status_code}")
release_ids = []
for block in release_info:
if 'id' in block:
release_ids.append(block['id'])
print(f'Current release ids: {release_ids}')
release_ids.remove(latest_id)
for id in release_ids:
release_uri = f"{uri}/{id}"
delete_data = {
'access_token': token
}
delete_response = client.delete(release_uri, json=delete_data)
if delete_response.status_code == 204:
print(f'Successfully deleted release #{id}.')
else:
raise ValueError(
f"Request failed with status code {delete_response.status_code}")
def sync_to_gitee(tag: str, body: str, files: slice):
release_id = ""
owner = "naibahq"
repo = "nezha"
release_api_uri = f"https://gitee.com/api/v5/repos/{owner}/{repo}/releases"
api_client = requests.Session()
api_client.headers.update({
'Accept': 'application/json',
'Content-Type': 'application/json'
})
access_token = os.environ['GITEE_TOKEN']
release_data = {
'access_token': access_token,
'tag_name': tag,
'name': tag,
'body': body,
'prerelease': False,
'target_commitish': 'master'
}
release_api_response = api_client.post(release_api_uri, json=release_data)
if release_api_response.status_code == 201:
release_info = release_api_response.json()
release_id = release_info.get('id')
else:
print(
f"Request failed with status code {release_api_response.status_code}")
print(f"Gitee release id: {release_id}")
asset_api_uri = f"{release_api_uri}/{release_id}/attach_files"
for file_path in files:
success = False
while not success:
files = {
'file': open(file_path, 'rb')
}
asset_api_response = requests.post(
asset_api_uri, params={'access_token': access_token}, files=files)
if asset_api_response.status_code == 201:
asset_info = asset_api_response.json()
asset_name = asset_info.get('name')
print(f"Successfully uploaded {asset_name}!")
success = True
else:
print(
f"Request failed with status code {asset_api_response.status_code}")
# 仅保留最新 Release 以防超出 Gitee 仓库配额
try:
delete_gitee_releases(release_id, api_client,
release_api_uri, access_token)
except ValueError as e:
print(e)
api_client.close()
print("Sync is completed!")
def get_abs_path(path: str):
wd = os.getcwd()
return os.path.join(wd, path)
get_github_latest_release()
+122 -56
View File
@@ -4,46 +4,138 @@ on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
build:
strategy:
fail-fast: true
matrix:
goos: [linux, windows]
goarch: [amd64]
include:
- goos: linux
goarch: s390x
- goos: linux
goarch: arm64
name: Build artifacts
runs-on: ubuntu-latest
container:
image: goreleaser/goreleaser-cross:v1.21
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
outputs:
tag_name: ${{ steps.extract_branch.outputs.tag }}
steps:
- run: git config --global --add safe.directory /__w/nezha/nezha
- uses: actions/checkout@v4
- name: Fetch IPInfo GeoIP Database
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
env:
IPINFO_TOKEN: ${{ secrets.IPINFO_TOKEN }}
run: |
rm pkg/geoip/geoip.db
wget -qO pkg/geoip/geoip.db https://ipinfo.io/data/free/country.mmdb?token=${IPINFO_TOKEN}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.21.x"
- name: Build
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: '~> v2'
args: build --single-target --clean --skip=validate
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dashboard-${{ env.GOOS }}-${{ env.GOARCH }}
path: |
./dist/*/*
release:
runs-on: ubuntu-latest
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
needs: build
name: Release
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: ./assets
- name: Archive and compress binaries
run: |
for file in assets/*/*/*; do
if [ -f "$file" ]; then
chmod +x "$file"
zip -jr "$file.zip" "$file"
fi
done
- name: Release
uses: ncipollo/release-action@v1
with:
artifacts: "assets/*/*/*.zip"
generateReleaseNotes: true
- name: Purge jsdelivr cache
run: |
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/install.sh
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/nezha-agent.service
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/docker-compose.yaml
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/config.yaml
LOWER_USERNAME=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
curl -s https://purge.jsdelivr.net/gh/$LOWER_USERNAME/nezha@master/script/install.sh
curl -s https://purge.jsdelivr.net/gh/$LOWER_USERNAME/nezha@master/script/nezha-agent.service
curl -s https://purge.jsdelivr.net/gh/$LOWER_USERNAME/nezha@master/script/docker-compose.yaml
curl -s https://purge.jsdelivr.net/gh/$LOWER_USERNAME/nezha@master/script/config.yaml
- name: Trigger sync
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
GH_DEBUG: api
run: |
gh workflow run sync-release.yml
release-docker:
runs-on: ubuntu-latest
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
needs: build
name: Release Docker images
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: ./assets
- name: Fix permissions
run: |
chmod -R +x ./assets/*
mkdir dist
mv ./assets/*/*/* ./dist
- name: Extract branch name
run: |
export TAG_NAME=$(echo ${GITHUB_REF#refs/tags/})
echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT
id: extract_branch
- name: xgo build
uses: crazy-max/ghaction-xgo@v2
with:
xgo_version: latest
go_version: 1.20.x
dest: dist
pkg: cmd/dashboard
prefix: dashboard
targets: linux/amd64,linux/arm64,linux/arm-7,linux/s390x,linux/riscv64,windows/amd64,windows/386,windows/arm64 # linux/386,
v: true
x: false
race: false
tags: timetzdata
ldflags: -s -w --extldflags '-static -fpic' -X github.com/naiba/nezha/service/singleton.Version=${{ steps.extract_branch.outputs.tag }}
buildmode: default
- name: fix dist
run: |
mv dist/dashboard-linux-arm-7 dist/dashboard-linux-arm
- name: Log in to the GHCR
- name: Log into GHCR
uses: docker/login-action@master
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
password: ${{ github.token }}
- name: Log in to the AliyunCS
uses: docker/login-action@master
@@ -60,13 +152,13 @@ jobs:
- name: Set up image name
run: |
GHRC_IMAGE_NAME=$(echo "ghcr.io/${{ github.repository_owner }}/nezha-dashboard" | tr '[:upper:]' '[:lower:]')
GHCR_IMAGE_NAME=$(echo "ghcr.io/${{ github.repository_owner }}/nezha-dashboard" | tr '[:upper:]' '[:lower:]')
if [ ${{ github.repository_owner }} = "naiba" ]
then ALI_IMAGE_NAME=$(echo "registry.cn-shanghai.aliyuncs.com/naibahq/nezha-dashboard")
else ALI_IMAGE_NAME=$(echo "registry.cn-shanghai.aliyuncs.com/${{ github.repository_owner }}/nezha-dashboard" | tr '[:upper:]' '[:lower:]')
fi
echo "::set-output name=GHRC_IMAGE_NAME::$GHRC_IMAGE_NAME"
echo "::set-output name=ALI_IMAGE_NAME::$ALI_IMAGE_NAME"
echo "GHCR_IMAGE_NAME=$GHCR_IMAGE_NAME" >> $GITHUB_OUTPUT
echo "ALI_IMAGE_NAME=$ALI_IMAGE_NAME" >> $GITHUB_OUTPUT
id: image-name
- name: Build dasbboard image And Push
@@ -74,36 +166,10 @@ jobs:
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm,linux/s390x,linux/riscv64 # linux/386,
platforms: linux/amd64,linux/arm64,linux/s390x
push: true
tags: |
${{ steps.image-name.outputs.GHRC_IMAGE_NAME }}:latest
${{ steps.image-name.outputs.GHRC_IMAGE_NAME }}:${{ steps.extract_branch.outputs.tag }}
${{ steps.image-name.outputs.GHCR_IMAGE_NAME }}:latest
${{ steps.image-name.outputs.GHCR_IMAGE_NAME }}:${{ steps.extract_branch.outputs.tag }}
${{ steps.image-name.outputs.ALI_IMAGE_NAME }}:latest
${{ steps.image-name.outputs.ALI_IMAGE_NAME }}:${{ steps.extract_branch.outputs.tag }}
- name: Compress dist files
run: |
for file in dist/*; do
if [ -f "$file" ]; then
zip -r "$file.zip" "$file"
fi
done
- name: Release
uses: ncipollo/release-action@v1
with:
artifacts: "dist/*.zip"
generateReleaseNotes: true
- name: Purge jsdelivr cache
run: |
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/install.sh
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/nezha-agent.service
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/docker-compose.yaml
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/config.yaml
LOWER_USERNAME=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
curl -s https://purge.jsdelivr.net/gh/$LOWER_USERNAME/nezha@master/script/install.sh
curl -s https://purge.jsdelivr.net/gh/$LOWER_USERNAME/nezha@master/script/nezha-agent.service
curl -s https://purge.jsdelivr.net/gh/$LOWER_USERNAME/nezha@master/script/docker-compose.yaml
curl -s https://purge.jsdelivr.net/gh/$LOWER_USERNAME/nezha@master/script/config.yaml
+16
View File
@@ -0,0 +1,16 @@
name: Sync Release to Gitee
on:
workflow_dispatch:
jobs:
sync-release-to-gitee:
runs-on: ubuntu-latest
env:
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Sync to Gitee
run: |
pip3 install PyGitHub
python3 .github/sync.py
-39
View File
@@ -1,39 +0,0 @@
name: Run Tests on PR
on:
pull_request:
branches:
- master
jobs:
tests:
runs-on: ubuntu-latest
env:
GO111MODULE: on
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: "^1.21.3"
- name: Unit test
run: |
go test -v ./...
- name: Run Gosec Security Scanner
uses: securego/gosec@master
with:
args: --exclude=G104,G402 ./...
- name: xgo build
uses: crazy-max/ghaction-xgo@v2
with:
xgo_version: latest
go_version: 1.20.x
dest: dist
pkg: cmd/dashboard
prefix: dashboard
targets: linux/amd64,linux/arm64,linux/arm-7,linux/s390x,linux/riscv64,windows/amd64,windows/386,windows/arm64 # linux/386,
v: true
x: false
race: false
tags: timetzdata
ldflags: -s -w --extldflags '-static -fpic' -X github.com/naiba/nezha/service/singleton.Version=test
buildmode: default
+18 -18
View File
@@ -10,36 +10,36 @@ on:
- "go.sum"
- "resource/**"
- ".github/workflows/test.yml"
pull_request:
branches:
- master
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
os: [ubuntu, windows, macos]
runs-on: ${{ matrix.os }}-latest
env:
GO111MODULE: on
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: "^1.21.3"
go-version: "1.21.x"
- name: Unit test
run: |
go test -v ./...
- name: Build test
run: go build -v ./cmd/dashboard
- name: Run Gosec Security Scanner
if: runner.os == 'Linux'
uses: securego/gosec@master
with:
args: --exclude=G104,G402 ./...
- name: xgo build
uses: crazy-max/ghaction-xgo@v2
with:
xgo_version: latest
go_version: 1.20.x
dest: dist
pkg: cmd/dashboard
prefix: dashboard
targets: linux/amd64,linux/arm64,linux/arm-7,linux/s390x,linux/riscv64,windows/amd64,windows/386,windows/arm64 # linux/386,
v: true
x: false
race: false
tags: timetzdata
ldflags: -s -w --extldflags '-static -fpic' -X github.com/naiba/nezha/service/singleton.Version=test
buildmode: default
+71
View File
@@ -0,0 +1,71 @@
version: 2
before:
hooks:
- go mod tidy -v
builds:
- id: linux_arm64
env:
- CGO_ENABLED=1
- CC=aarch64-linux-gnu-gcc
ldflags:
- -s -w
- -X github.com/naiba/nezha/service/singleton.Version={{.Version}}
- -extldflags "-static -fpic"
flags:
- -trimpath
goos:
- linux
goarch:
- arm64
main: ./cmd/dashboard
binary: dashboard-{{ .Os }}-{{ .Arch }}
- id: linux_amd64
env:
- CGO_ENABLED=1
- CC=x86_64-linux-gnu-gcc
ldflags:
- -s -w
- -X github.com/naiba/nezha/service/singleton.Version={{.Version}}
- -extldflags "-static -fpic"
flags:
- -trimpath
goos:
- linux
goarch:
- amd64
main: ./cmd/dashboard
binary: dashboard-{{ .Os }}-{{ .Arch }}
- id: linux_s390x
env:
- CGO_ENABLED=1
- CC=s390x-linux-gnu-gcc
ldflags:
- -s -w
- -X github.com/naiba/nezha/service/singleton.Version={{.Version}}
- -extldflags "-static -fpic"
flags:
- -trimpath
goos:
- linux
goarch:
- s390x
main: ./cmd/dashboard
binary: dashboard-{{ .Os }}-{{ .Arch }}
- id: windows_amd64
env:
- CGO_ENABLED=1
- CC=x86_64-w64-mingw32-gcc
ldflags:
- -s -w
- -X github.com/naiba/nezha/service/singleton.Version={{.Version}}
- -extldflags "-static -fpic"
flags:
- -trimpath
goos:
- windows
goarch:
- amd64
main: ./cmd/dashboard
binary: dashboard-{{ .Os }}-{{ .Arch }}
snapshot:
version_template: "dashboard"
+7 -9
View File
@@ -1,16 +1,12 @@
# latest 镜像还没有 riscv64 https://hub.docker.com/_/alpine/tags
FROM alpine:edge
FROM alpine AS certs
RUN apk update && apk add ca-certificates
FROM busybox:stable-musl
ARG TARGETOS
ARG TARGETARCH
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/*
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
COPY ./script/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
@@ -19,4 +15,6 @@ COPY dist/dashboard-${TARGETOS}-${TARGETARCH} ./app
VOLUME ["/dashboard/data"]
EXPOSE 80 5555
ARG TZ=Asia/Shanghai
ENV TZ=$TZ
ENTRYPOINT ["/entrypoint.sh"]
+24 -16
View File
@@ -4,7 +4,7 @@
<br>
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
<br><br>
<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.17.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.19.0-brightgreen?style=for-the-badge&logo=linux">
<br>
<br>
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
@@ -43,10 +43,10 @@ You can change the dashboard language in the settings page (`/setting`) after th
<!--GAMFC_DELIMITER--><a href="https://github.com/naiba" title="naiba"><img src="https://avatars.githubusercontent.com/u/29243953?v=4" width="50;" alt="naiba"/></a>
<a href="https://github.com/AkkiaS7" title="Akkia"><img src="https://avatars.githubusercontent.com/u/68485070?v=4" width="50;" alt="Akkia"/></a>
<a href="https://github.com/Erope" title="卖女孩的小火柴"><img src="https://avatars.githubusercontent.com/u/44471469?v=4" width="50;" alt="卖女孩的小火柴"/></a>
<a href="https://github.com/dysf888" title="黑歌"><img src="https://avatars.githubusercontent.com/u/47450409?v=4" width="50;" alt="黑歌"/></a>
<a href="https://github.com/uubulb" title="UUBulb"><img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/></a>
<a href="https://github.com/xykt" title="xykt"><img src="https://avatars.githubusercontent.com/u/152045469?v=4" width="50;" alt="xykt"/></a>
<a href="https://github.com/nap0o" title="nap0o"><img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/></a>
<a href="https://github.com/dysf888" title="黑歌"><img src="https://avatars.githubusercontent.com/u/47450409?v=4" width="50;" alt="黑歌"/></a>
<a href="https://github.com/xykt" title="xykt"><img src="https://avatars.githubusercontent.com/u/152045469?v=4" width="50;" alt="xykt"/></a>
<a href="https://github.com/MikoyChinese" title="MikoyChinese"><img src="https://avatars.githubusercontent.com/u/22676744?v=4" width="50;" alt="MikoyChinese"/></a>
<a href="https://github.com/JackieSung4ev" title="JackieSung4ev"><img src="https://avatars.githubusercontent.com/u/24974735?v=4" width="50;" alt="JackieSung4ev"/></a>
<a href="https://github.com/cantoblanco" title="Kris"><img src="https://avatars.githubusercontent.com/u/116849421?v=4" width="50;" alt="Kris"/></a>
@@ -66,29 +66,37 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/DarcJC" title="Darc Z."><img src="https://avatars.githubusercontent.com/u/53445798?v=4" width="50;" alt="Darc Z."/></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/coreff" title="Core F"><img src="https://avatars.githubusercontent.com/u/38347122?v=4" width="50;" alt="Core F"/></a>
<a href="https://github.com/colour93" title="玖叁"><img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/></a>
<a href="https://github.com/arkylin" title="凌"><img src="https://avatars.githubusercontent.com/u/35104502?v=4" width="50;" alt="凌"/></a>
<a href="https://github.com/zhucaidan" title="zhucaidan"><img src="https://avatars.githubusercontent.com/u/47970938?v=4" width="50;" alt="zhucaidan"/></a>
<a href="https://github.com/ysicing" title="缘生"><img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/></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 href="https://github.com/lvyaoting" title="lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50;" alt="lvyaoting"/></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/eya46" title="eya46"><img src="https://avatars.githubusercontent.com/u/61458340?v=4" width="50;" alt="eya46"/></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/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/eya46" title="eya46"><img src="https://avatars.githubusercontent.com/u/61458340?v=4" width="50;" alt="eya46"/></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/hiDandelion" title="hiDandelion"><img src="https://avatars.githubusercontent.com/u/77157418?v=4" width="50;" alt="hiDandelion"/></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/lvyaoting" title="lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50;" alt="lvyaoting"/></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 href="https://github.com/ysicing" title="缘生"><img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/></a>
<a href="https://github.com/zhucaidan" title="zhucaidan"><img src="https://avatars.githubusercontent.com/u/47970938?v=4" width="50;" alt="zhucaidan"/></a>
<a href="https://github.com/arkylin" title="凌"><img src="https://avatars.githubusercontent.com/u/35104502?v=4" width="50;" alt="凌"/></a>
<a href="https://github.com/colour93" title="玖叁"><img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/></a>
<a href="https://github.com/IamTaoChen" title="Tao Chen"><img src="https://avatars.githubusercontent.com/u/42793494?v=4" width="50;" alt="Tao Chen"/></a>
<a href="https://github.com/Septrum101" title="Spetrum"><img src="https://avatars.githubusercontent.com/u/11692994?v=4" width="50;" alt="Spetrum"/></a>
<a href="https://github.com/dreamingsleeping" title="Nanjing Hopefun Network Technology Co. Ltd."><img src="https://avatars.githubusercontent.com/u/13828658?v=4" width="50;" alt="Nanjing Hopefun Network Technology Co. Ltd."/></a>
<a href="https://github.com/silver-ymz" title="Mingzhuo Yin"><img src="https://avatars.githubusercontent.com/u/78400701?v=4" width="50;" alt="Mingzhuo Yin"/></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/xrgzs" title="MadDogOwner"><img src="https://avatars.githubusercontent.com/u/26499123?v=4" width="50;" alt="MadDogOwner"/></a>
<a href="https://github.com/funnyzak" title="Leon"><img src="https://avatars.githubusercontent.com/u/2562087?v=4" width="50;" alt="Leon"/></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/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>
<a href="https://github.com/wwng2333" title=":D"><img src="https://avatars.githubusercontent.com/u/17147265?v=4" width="50;" alt=":D"/></a>
<a href="https://github.com/wellcoming" title="Coming"><img src="https://avatars.githubusercontent.com/u/74850890?v=4" width="50;" alt="Coming"/></a><!--GAMFC_DELIMITER_END-->
## Special Thanks
- [IPInfo](https://ipinfo.io/) for providing an accurate GeoIP Database.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=naiba/nezha&type=Timeline)](https://star-history.com/#naiba/nezha&Timeline)
+159 -216
View File
@@ -3,12 +3,8 @@ package controller
import (
"errors"
"fmt"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
@@ -24,22 +20,13 @@ import (
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/pkg/websocketx"
"github.com/naiba/nezha/proto"
"github.com/naiba/nezha/service/rpc"
"github.com/naiba/nezha/service/singleton"
)
type terminalContext struct {
agentConn *websocketx.Conn
userConn *websocketx.Conn
serverID uint64
host string
useSSL bool
}
type commonPage struct {
r *gin.Engine
terminals map[string]*terminalContext
terminalsLock *sync.Mutex
requestGroup singleflight.Group
r *gin.Engine
requestGroup singleflight.Group
}
func (cp *commonPage) serve() {
@@ -59,6 +46,8 @@ func (cp *commonPage) serve() {
cr.GET("/network", cp.network)
cr.GET("/ws", cp.ws)
cr.POST("/terminal", cp.createTerminal)
cr.GET("/file", cp.createFM)
cr.GET("/file/:id", cp.fm)
}
type viewPasswordForm struct {
@@ -68,7 +57,6 @@ type viewPasswordForm struct {
func (p *commonPage) issueViewPassword(c *gin.Context) {
var vpf viewPasswordForm
err := c.ShouldBind(&vpf)
log.Println("bingo", vpf)
var hash []byte
if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword {
err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
@@ -113,7 +101,6 @@ func (p *commonPage) service(c *gin.Context) {
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesStatus"}),
"Services": res.([]interface{})[0],
"CycleTransferStats": res.([]interface{})[1],
"CustomCode": singleton.Conf.Site.CustomCode,
}))
}
@@ -218,7 +205,6 @@ func (cp *commonPage) network(c *gin.Context) {
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/network"), mygin.CommonEnvironment(c, gin.H{
"Servers": string(serversBytes),
"MonitorInfos": string(monitorInfos),
"CustomCode": singleton.Conf.Site.CustomCode,
"MaxTCPPingValue": singleton.Conf.MaxTCPPingValue,
}))
}
@@ -268,14 +254,13 @@ func (cp *commonPage) home(c *gin.Context) {
return
}
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/home"), mygin.CommonEnvironment(c, gin.H{
"Servers": string(stat),
"CustomCode": singleton.Conf.Site.CustomCode,
"Servers": string(stat),
}))
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
ReadBufferSize: 32768,
WriteBufferSize: 32768,
}
type Data struct {
@@ -283,8 +268,6 @@ type Data struct {
Servers []*model.Server `json:"servers,omitempty"`
}
var cloudflareCookiesValidator = regexp.MustCompile("^[A-Za-z0-9-_]+$")
func (cp *commonPage) ws(c *gin.Context) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
@@ -321,10 +304,8 @@ func (cp *commonPage) ws(c *gin.Context) {
}
func (cp *commonPage) terminal(c *gin.Context) {
terminalID := c.Param("id")
cp.terminalsLock.Lock()
if terminalID == "" || cp.terminals[terminalID] == nil {
cp.terminalsLock.Unlock()
streamId := c.Param("id")
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "无权访问",
@@ -334,104 +315,7 @@ func (cp *commonPage) terminal(c *gin.Context) {
}, true)
return
}
terminal := cp.terminals[terminalID]
cp.terminalsLock.Unlock()
defer func() {
// 清理 context
cp.terminalsLock.Lock()
defer cp.terminalsLock.Unlock()
delete(cp.terminals, terminalID)
}()
var isAgent bool
if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized {
singleton.ServerLock.RLock()
_, hasID := singleton.SecretToID[c.Request.Header.Get("Secret")]
singleton.ServerLock.RUnlock()
if !hasID {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "无权访问",
Msg: "用户未登录或非法终端",
Link: "/",
Btn: "返回首页",
}, true)
return
}
if terminal.userConn == nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "无权访问",
Msg: "用户不在线",
Link: "/",
Btn: "返回首页",
}, true)
return
}
if terminal.agentConn != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
Title: "连接已存在",
Msg: "Websocket协议切换失败",
Link: "/",
Btn: "返回首页",
}, true)
return
}
isAgent = true
} else {
singleton.ServerLock.RLock()
server := singleton.ServerList[terminal.serverID]
singleton.ServerLock.RUnlock()
if server == nil || server.TaskStream == nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "服务器不存在或处于离线状态",
Link: "/server",
Btn: "返回重试",
}, true)
return
}
cloudflareCookies, _ := c.Cookie("CF_Authorization")
// Cloudflare Cookies 合法性验证
// 其应该包含.分隔的三组BASE64-URL编码
if cloudflareCookies != "" {
encodedCookies := strings.Split(cloudflareCookies, ".")
if len(encodedCookies) == 3 {
for i := 0; i < 3; i++ {
if !cloudflareCookiesValidator.MatchString(encodedCookies[i]) {
cloudflareCookies = ""
break
}
}
} else {
cloudflareCookies = ""
}
}
terminalData, _ := utils.Json.Marshal(&model.TerminalTask{
Host: terminal.host,
UseSSL: terminal.useSSL,
Session: terminalID,
Cookie: cloudflareCookies,
})
if err := server.TaskStream.Send(&proto.Task{
Type: model.TaskTypeTerminal,
Data: string(terminalData),
}); err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "Agent信令下发失败",
Link: "/server",
Btn: "返回重试",
}, true)
return
}
}
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
@@ -447,36 +331,7 @@ func (cp *commonPage) terminal(c *gin.Context) {
return
}
defer wsConn.Close()
conn := &websocketx.Conn{Conn: wsConn}
log.Printf("NEZHA>> terminal connected %t %q", isAgent, c.Request.URL)
defer log.Printf("NEZHA>> terminal disconnected %t %q", isAgent, c.Request.URL)
if isAgent {
terminal.agentConn = conn
defer func() {
// Agent断开链接时断开用户连接
if terminal.userConn != nil {
terminal.userConn.Close()
}
}()
} else {
terminal.userConn = conn
defer func() {
// 用户断开链接时断开 Agent 连接
if terminal.agentConn != nil {
terminal.agentConn.Close()
}
}()
}
deadlineCh := make(chan interface{})
go func() {
// 对方连接超时
connectDeadline := time.NewTimer(time.Second * 15)
<-connectDeadline.C
deadlineCh <- struct{}{}
}()
conn := websocketx.NewConn(wsConn)
go func() {
// PING 保活
@@ -488,58 +343,11 @@ func (cp *commonPage) terminal(c *gin.Context) {
}
}()
dataCh := make(chan []byte)
errorCh := make(chan error)
go func() {
for {
msgType, data, err := conn.ReadMessage()
if err != nil {
errorCh <- err
return
}
// 将文本消息转换为命令输入
if msgType == websocket.TextMessage {
data = append([]byte{0}, data...)
}
dataCh <- data
}
}()
var dataBuffer [][]byte
var distConn *websocketx.Conn
checkDistConn := func() {
if distConn == nil {
if isAgent {
distConn = terminal.userConn
} else {
distConn = terminal.agentConn
}
}
if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil {
return
}
for {
select {
case <-deadlineCh:
checkDistConn()
if distConn == nil {
return
}
case <-errorCh:
return
case data := <-dataCh:
dataBuffer = append(dataBuffer, data)
checkDistConn()
if distConn != nil {
for i := 0; i < len(dataBuffer); i++ {
err = distConn.WriteMessage(websocket.BinaryMessage, dataBuffer[i])
if err != nil {
return
}
}
dataBuffer = dataBuffer[:0]
}
}
}
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
}
type createTerminalRequest struct {
@@ -571,7 +379,7 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
return
}
id, err := uuid.GenerateUUID()
streamId, err := uuid.GenerateUUID()
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
@@ -585,6 +393,8 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
return
}
rpc.NezhaHandlerSingleton.CreateStream(streamId)
singleton.ServerLock.RLock()
server := singleton.ServerList[createTerminalReq.ID]
singleton.ServerLock.RUnlock()
@@ -599,17 +409,150 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
return
}
cp.terminalsLock.Lock()
defer cp.terminalsLock.Unlock()
cp.terminals[id] = &terminalContext{
serverID: createTerminalReq.ID,
host: createTerminalReq.Host,
useSSL: createTerminalReq.Protocol == "https:",
terminalData, _ := utils.Json.Marshal(&model.TerminalTask{
StreamID: streamId,
})
if err := server.TaskStream.Send(&proto.Task{
Type: model.TaskTypeTerminalGRPC,
Data: string(terminalData),
}); err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "Agent信令下发失败",
Link: "/server",
Btn: "返回重试",
}, true)
return
}
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/terminal", mygin.CommonEnvironment(c, gin.H{
"SessionID": id,
"SessionID": streamId,
"ServerName": server.Name,
"ServerID": server.ID,
}))
}
func (cp *commonPage) fm(c *gin.Context) {
streamId := c.Param("id")
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "无权访问",
Msg: "FM会话不存在",
Link: "/",
Btn: "返回首页",
}, true)
return
}
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "NetworkError",
}),
Msg: "Websocket协议切换失败",
Link: "/",
Btn: "返回首页",
}, true)
return
}
defer wsConn.Close()
conn := websocketx.NewConn(wsConn)
go func() {
// PING 保活
for {
if err = conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
return
}
time.Sleep(time.Second * 10)
}
}()
if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil {
return
}
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
}
func (cp *commonPage) createFM(c *gin.Context) {
IdString := c.Query("id")
if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "无权访问",
Msg: "用户未登录",
Link: "/login",
Btn: "去登录",
}, true)
return
}
streamId, err := uuid.GenerateUUID()
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "SystemError",
}),
Msg: "生成会话ID失败",
Link: "/server",
Btn: "返回重试",
}, true)
return
}
rpc.NezhaHandlerSingleton.CreateStream(streamId)
serverId, err := strconv.Atoi(IdString)
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "请求参数有误:" + err.Error(),
Link: "/server",
Btn: "返回重试",
}, true)
return
}
singleton.ServerLock.RLock()
server := singleton.ServerList[uint64(serverId)]
singleton.ServerLock.RUnlock()
if server == nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "服务器不存在或处于离线状态",
Link: "/server",
Btn: "返回重试",
}, true)
return
}
fmData, _ := utils.Json.Marshal(&model.TaskFM{
StreamID: streamId,
})
if err := server.TaskStream.Send(&proto.Task{
Type: model.TaskTypeFM,
Data: string(fmData),
}); err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "Agent信令下发失败",
Link: "/server",
Btn: "返回重试",
}, true)
return
}
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/file", mygin.CommonEnvironment(c, gin.H{
"SessionID": streamId,
}))
}
+88 -16
View File
@@ -9,43 +9,54 @@ import (
"os"
"strconv"
"strings"
"sync"
"time"
"code.cloudfoundry.org/bytefmt"
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
"github.com/hashicorp/go-uuid"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/proto"
"github.com/naiba/nezha/resource"
"github.com/naiba/nezha/service/rpc"
"github.com/naiba/nezha/service/singleton"
)
func ServeWeb(port uint) *http.Server {
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
tmpl := template.New("").Funcs(funcMap)
var err error
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
if err != nil {
panic(err)
}
tmpl = loadThirdPartyTemplates(tmpl)
r.SetHTMLTemplate(tmpl)
if singleton.Conf.Debug {
gin.SetMode(gin.DebugMode)
pprof.Register(r)
}
r.Use(mygin.RecordPath)
staticFs, err := fs.Sub(resource.StaticFS, "static")
if err != nil {
panic(err)
r.Use(natGateway)
if os.Getenv("NZ_LOCAL_TEMPLATE") == "true" {
r.SetFuncMap(funcMap)
r.Use(mygin.RecordPath)
r.Static("/static", "resource/static")
r.LoadHTMLGlob("resource/template/**/*.html")
} else {
tmpl := template.New("").Funcs(funcMap)
var err error
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
if err != nil {
panic(err)
}
tmpl = loadThirdPartyTemplates(tmpl)
r.SetHTMLTemplate(tmpl)
r.Use(mygin.RecordPath)
staticFs, err := fs.Sub(resource.StaticFS, "static")
if err != nil {
panic(err)
}
r.StaticFS("/static", http.FS(staticFs))
}
r.StaticFS("/static", http.FS(staticFs))
r.Static("/static-custom", "resource/static/custom")
routers(r)
page404 := func(c *gin.Context) {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusNotFound,
@@ -68,7 +79,7 @@ func ServeWeb(port uint) *http.Server {
func routers(r *gin.Engine) {
// 通用页面
cp := commonPage{r: r, terminals: make(map[string]*terminalContext), terminalsLock: new(sync.Mutex)}
cp := commonPage{r: r}
cp.serve()
// 游客页面
gp := guestPage{r}
@@ -239,3 +250,64 @@ var funcMap = template.FuncMap{
return singleton.StatusCodeToString(singleton.GetStatusCode(val))
},
}
func natGateway(c *gin.Context) {
natConfig := singleton.GetNATConfigByDomain(c.Request.Host)
if natConfig == nil {
return
}
singleton.ServerLock.RLock()
server := singleton.ServerList[natConfig.ServerID]
singleton.ServerLock.RUnlock()
if server == nil || server.TaskStream == nil {
c.Writer.WriteString("server not found or not connected")
c.Abort()
return
}
streamId, err := uuid.GenerateUUID()
if err != nil {
c.Writer.WriteString(fmt.Sprintf("stream id error: %v", err))
c.Abort()
return
}
rpc.NezhaHandlerSingleton.CreateStream(streamId)
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
taskData, err := utils.Json.Marshal(model.TaskNAT{
StreamID: streamId,
Host: natConfig.Host,
})
if err != nil {
c.Writer.WriteString(fmt.Sprintf("task data error: %v", err))
c.Abort()
return
}
if err := server.TaskStream.Send(&proto.Task{
Type: model.TaskTypeNAT,
Data: string(taskData),
}); err != nil {
c.Writer.WriteString(fmt.Sprintf("send task error: %v", err))
c.Abort()
return
}
w, err := utils.NewRequestWrapper(c.Request, c.Writer)
if err != nil {
c.Writer.WriteString(fmt.Sprintf("request wrapper error: %v", err))
c.Abort()
return
}
if err := rpc.NezhaHandlerSingleton.UserConnected(streamId, w); err != nil {
c.Writer.WriteString(fmt.Sprintf("user connected error: %v", err))
c.Abort()
return
}
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
c.Abort()
}
+7
View File
@@ -35,6 +35,10 @@ func (gp *guestPage) serve() {
}
func (gp *guestPage) login(c *gin.Context) {
if singleton.Conf.Oauth2.OidcAutoLogin {
c.Redirect(http.StatusFound, "/oauth2/login")
return
}
LoginType := "GitHub"
RegistrationLink := "https://github.com/join"
if singleton.Conf.Oauth2.Type == model.ConfigTypeGitee {
@@ -52,6 +56,9 @@ func (gp *guestPage) login(c *gin.Context) {
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeCloudflare {
LoginType = "Cloudflare"
RegistrationLink = "https://dash.cloudflare.com/sign-up/teams"
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeOidc {
LoginType = singleton.Conf.Oauth2.OidcDisplayName
RegistrationLink = singleton.Conf.Oauth2.OidcRegisterURL
}
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/login", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
+54
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
@@ -45,6 +46,7 @@ func (ma *memberAPI) serve() {
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
mr.POST("/batch-delete-server", ma.batchDeleteServer)
mr.POST("/notification", ma.addOrEditNotification)
mr.POST("/nat", ma.addOrEditNAT)
mr.POST("/alert-rule", ma.addOrEditAlertRule)
mr.POST("/setting", ma.updateSetting)
mr.DELETE("/:model/:id", ma.delete)
@@ -209,6 +211,11 @@ func (ma *memberAPI) delete(c *gin.Context) {
if err == nil {
singleton.OnDeleteNotification(id)
}
case "nat":
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
if err == nil {
singleton.OnNATUpdate()
}
case "monitor":
err = singleton.DB.Unscoped().Delete(&model.Monitor{}, "id = ?", id).Error
if err == nil {
@@ -378,6 +385,7 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
} else {
s.Host = &model.Host{}
s.State = &model.HostState{}
s.TaskCloseLock = new(sync.Mutex)
singleton.ServerLock.Lock()
singleton.SecretToID[s.Secret] = s.ID
singleton.ServerList[s.ID] = &s
@@ -733,6 +741,45 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
})
}
type natForm struct {
ID uint64
Name string
ServerID uint64
Host string
Domain string
}
func (ma *memberAPI) addOrEditNAT(c *gin.Context) {
var nf natForm
var n model.NAT
err := c.ShouldBindJSON(&nf)
if err == nil {
n.Name = nf.Name
n.ID = nf.ID
n.Domain = nf.Domain
n.Host = nf.Host
n.ServerID = nf.ServerID
}
if err == nil {
if n.ID == 0 {
err = singleton.DB.Create(&n).Error
} else {
err = singleton.DB.Save(&n).Error
}
}
if err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
singleton.OnNATUpdate()
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
}
type alertRuleForm struct {
ID uint64
Name string
@@ -847,6 +894,11 @@ func (ma *memberAPI) logout(c *gin.Context) {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
if oidcLogoutUrl := singleton.Conf.Oauth2.OidcLogoutURL; oidcLogoutUrl != "" {
// 重定向到 OIDC 退出登录地址。不知道为什么,这里的重定向不生效
c.Redirect(http.StatusOK, oidcLogoutUrl)
}
}
type settingForm struct {
@@ -856,6 +908,7 @@ type settingForm struct {
Theme string
DashboardTheme string
CustomCode string
CustomCodeDashboard string
ViewPassword string
IgnoredIPNotification string
IPChangeNotificationTag string // IP变更提醒的通知组
@@ -921,6 +974,7 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
singleton.Conf.Site.Theme = sf.Theme
singleton.Conf.Site.DashboardTheme = sf.DashboardTheme
singleton.Conf.Site.CustomCode = sf.CustomCode
singleton.Conf.Site.CustomCodeDashboard = sf.CustomCodeDashboard
singleton.Conf.Site.ViewPassword = sf.ViewPassword
singleton.Conf.Oauth2.Admin = sf.Admin
// 保证NotificationTag不为空
+10
View File
@@ -27,6 +27,7 @@ func (mp *memberPage) serve() {
mr.GET("/monitor", mp.monitor)
mr.GET("/cron", mp.cron)
mr.GET("/notification", mp.notification)
mr.GET("/nat", mp.nat)
mr.GET("/setting", mp.setting)
mr.GET("/api", mp.api)
}
@@ -77,6 +78,15 @@ func (mp *memberPage) notification(c *gin.Context) {
}))
}
func (mp *memberPage) nat(c *gin.Context) {
var data []model.NAT
singleton.DB.Find(&data)
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/nat", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "NAT"}),
"NAT": data,
}))
}
func (mp *memberPage) setting(c *gin.Context) {
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/setting", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}),
+66 -14
View File
@@ -2,7 +2,6 @@ package controller
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
@@ -10,25 +9,27 @@ import (
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/naiba/nezha/pkg/oidc/cloudflare"
myOidc "github.com/naiba/nezha/pkg/oidc/general"
"code.gitea.io/sdk/gitea"
"github.com/gin-gonic/gin"
GitHubAPI "github.com/google/go-github/v47/github"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/service/singleton"
"github.com/patrickmn/go-cache"
"github.com/xanzy/go-gitlab"
"golang.org/x/oauth2"
GitHubOauth2 "golang.org/x/oauth2/github"
GitlabOauth2 "golang.org/x/oauth2/gitlab"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/service/singleton"
)
type oauth2controller struct {
r gin.IRoutes
r gin.IRoutes
oidcProvider *oidc.Provider
}
func (oa *oauth2controller) serve() {
@@ -88,6 +89,27 @@ func (oa *oauth2controller) getCommonOauth2Config(c *gin.Context) *oauth2.Config
},
RedirectURL: oa.getRedirectURL(c),
}
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeOidc {
var err error
oa.oidcProvider, err = oidc.NewProvider(c.Request.Context(), singleton.Conf.Oauth2.OidcIssuer)
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusBadRequest,
Title: fmt.Sprintf("Cannot get OIDC infomaion from issuer from %s", singleton.Conf.Oauth2.OidcIssuer),
Msg: err.Error(),
}, true)
return nil
}
scopes := strings.Split(singleton.Conf.Oauth2.OidcScopes, ",")
scopes = append(scopes, oidc.ScopeOpenID)
uniqueScopes := removeDuplicates(scopes)
return &oauth2.Config{
ClientID: singleton.Conf.Oauth2.ClientID,
ClientSecret: singleton.Conf.Oauth2.ClientSecret,
Scopes: uniqueScopes,
Endpoint: oa.oidcProvider.Endpoint(),
RedirectURL: oa.getRedirectURL(c),
}
} else {
return &oauth2.Config{
ClientID: singleton.Conf.Oauth2.ClientID,
@@ -100,7 +122,8 @@ func (oa *oauth2controller) getCommonOauth2Config(c *gin.Context) *oauth2.Config
func (oa *oauth2controller) getRedirectURL(c *gin.Context) string {
scheme := "http://"
if strings.HasPrefix(c.Request.Referer(), "https://") {
referer := c.Request.Referer()
if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto == "https" || strings.HasPrefix(referer, "https://") {
scheme = "https://"
}
return scheme + c.Request.Host + "/oauth2/callback"
@@ -175,11 +198,22 @@ func (oa *oauth2controller) callback(c *gin.Context) {
if err == nil {
defer resp.Body.Close()
var cloudflareUserInfo *cloudflare.UserInfo
if err := json.NewDecoder(resp.Body).Decode(&cloudflareUserInfo); err == nil {
if err := utils.Json.NewDecoder(resp.Body).Decode(&cloudflareUserInfo); err == nil {
user = cloudflareUserInfo.MapToNezhaUser()
}
}
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeOidc {
userInfo, err := oa.oidcProvider.UserInfo(c.Request.Context(), oauth2.StaticTokenSource(otk))
if err == nil {
loginClaim := singleton.Conf.Oauth2.OidcLoginClaim
groupClain := singleton.Conf.Oauth2.OidcGroupClaim
adminGroups := strings.Split(singleton.Conf.Oauth2.AdminGroups, ",")
autoCreate := singleton.Conf.Oauth2.OidcAutoCreate
var oidceUserInfo *myOidc.UserInfo
if err := userInfo.Claims(&oidceUserInfo); err == nil {
user = oidceUserInfo.MapToNezhaUser(loginClaim, groupClain, adminGroups, autoCreate)
}
}
} else {
var client *GitHubAPI.Client
oc := oauth2Config.Client(ctx, otk)
@@ -212,10 +246,15 @@ func (oa *oauth2controller) callback(c *gin.Context) {
return
}
var isAdmin bool
for _, admin := range strings.Split(singleton.Conf.Oauth2.Admin, ",") {
if admin != "" && strings.EqualFold(user.Login, admin) {
isAdmin = true
break
if user.SuperAdmin {
isAdmin = true
} else {
for _, admin := range strings.Split(singleton.Conf.Oauth2.Admin, ",") {
if admin != "" && strings.EqualFold(user.Login, admin) {
isAdmin = true
break
}
}
}
if !isAdmin {
@@ -242,3 +281,16 @@ func (oa *oauth2controller) callback(c *gin.Context) {
"URL": "/",
}))
}
func removeDuplicates(elements []string) []string {
encountered := map[string]bool{}
result := []string{}
for _, v := range elements {
if !encountered[v] {
encountered[v] = true
result = append(result, v)
}
}
return result
}
+20
View File
@@ -4,10 +4,13 @@ import (
"context"
"fmt"
"log"
"time"
_ "time/tzdata"
"github.com/naiba/nezha/cmd/dashboard/controller"
"github.com/naiba/nezha/cmd/dashboard/rpc"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/proto"
"github.com/naiba/nezha/service/singleton"
"github.com/ory/graceful"
flag "github.com/spf13/pflag"
@@ -59,6 +62,7 @@ func main() {
return
}
// TODO 使用 cmux 在同一端口服务 HTTP 和 gRPC
singleton.CleanMonitorHistory()
go rpc.ServeRPC(singleton.Conf.GRPCPort)
serviceSentinelDispatchBus := make(chan model.Monitor) // 用于传递服务监控任务信息的channel
@@ -67,6 +71,7 @@ func main() {
go singleton.AlertSentinelStart()
singleton.NewServiceSentinel(serviceSentinelDispatchBus)
srv := controller.ServeWeb(singleton.Conf.HTTPPort)
go dispatchReportInfoTask()
if err := graceful.Graceful(func() error {
return srv.ListenAndServe()
}, func(c context.Context) error {
@@ -79,3 +84,18 @@ func main() {
log.Printf("NEZHA>> ERROR: %v", err)
}
}
func dispatchReportInfoTask() {
time.Sleep(time.Second * 15)
singleton.ServerLock.RLock()
defer singleton.ServerLock.RUnlock()
for _, server := range singleton.ServerList {
if server == nil || server.TaskStream == nil {
continue
}
server.TaskStream.Send(&proto.Task{
Type: model.TaskTypeReportHostInfo,
Data: "",
})
}
}
+2 -3
View File
@@ -14,9 +14,8 @@ import (
func ServeRPC(port uint) {
server := grpc.NewServer()
pb.RegisterNezhaServiceServer(server, &rpcService.NezhaHandler{
Auth: &rpcService.AuthHandler{},
})
rpcService.NezhaHandlerSingleton = rpcService.NewNezhaHandler()
pb.RegisterNezhaServiceServer(server, rpcService.NezhaHandlerSingleton)
listen, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
panic(err)
+10 -8
View File
@@ -1,11 +1,12 @@
module github.com/naiba/nezha
go 1.20
go 1.21
require (
code.cloudfoundry.org/bytefmt v0.0.0-20240425163905-bcdc1ad063ea
code.gitea.io/sdk/gitea v0.18.0
github.com/BurntSushi/toml v1.3.2
github.com/coreos/go-oidc/v3 v3.11.0
github.com/gin-contrib/pprof v1.4.0
github.com/gin-gonic/gin v1.9.1
github.com/google/go-github/v47 v47.1.0
@@ -15,19 +16,19 @@ require (
github.com/json-iterator/go v1.1.12
github.com/nicksnyder/go-i18n/v2 v2.4.0
github.com/ory/graceful v0.1.3
github.com/oschwald/maxminddb-golang v1.13.1
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/robfig/cron/v3 v3.0.1
github.com/samber/lo v1.39.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.18.2
github.com/xanzy/go-gitlab v0.103.0
golang.org/x/crypto v0.22.0
golang.org/x/net v0.24.0
golang.org/x/oauth2 v0.19.0
golang.org/x/crypto v0.25.0
golang.org/x/net v0.27.0
golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.7.0
golang.org/x/text v0.14.0
golang.org/x/text v0.16.0
google.golang.org/grpc v1.63.0
google.golang.org/protobuf v1.33.0
google.golang.org/protobuf v1.34.2
gorm.io/driver/sqlite v1.5.5
gorm.io/gorm v1.25.10
sigs.k8s.io/yaml v1.4.0
@@ -41,6 +42,7 @@ require (
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
@@ -74,7 +76,7 @@ require (
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
+39 -17
View File
@@ -10,14 +10,19 @@ github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZX
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
@@ -31,9 +36,13 @@ github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
@@ -44,6 +53,7 @@ github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXS
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
@@ -52,17 +62,20 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v47 v47.1.0 h1:Cacm/WxQBOa9lF0FT0EMjZ2BWMetQ1TQfyurn4yF1z8=
github.com/google/go-github/v47 v47.1.0/go.mod h1:VPZBXNbFSJGjyjFRUKo9vZGawTajnWzC/YjGw/oFKi0=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
@@ -86,6 +99,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -96,6 +110,7 @@ github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNa
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -111,9 +126,13 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
github.com/ory/graceful v0.1.3 h1:FaeXcHZh168WzS+bqruqWEw/HgXWLdNv2nJ+fbhxbhc=
github.com/ory/graceful v0.1.3/go.mod h1:4zFz687IAF7oNHHiB586U4iL+/4aV09o/PYLE34t2bA=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
@@ -124,17 +143,17 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
@@ -156,8 +175,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
@@ -179,16 +199,16 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -199,19 +219,21 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.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.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
@@ -219,8 +241,8 @@ google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8=
google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+35 -11
View File
@@ -38,6 +38,7 @@ const (
ConfigTypeJihulab = "jihulab"
ConfigTypeGitea = "gitea"
ConfigTypeCloudflare = "cloudflare"
ConfigTypeOidc = "oidc"
)
const (
@@ -79,19 +80,30 @@ type Config struct {
Debug bool // debug模式开关
Language string // 系统语言,默认 zh-CN
Site struct {
Brand string // 站点名称
CookieName string // 浏览器 Cookie 名称
Theme string
DashboardTheme string
CustomCode string
ViewPassword string // 前台查看密码
Brand string // 站点名称
CookieName string // 浏览器 Cookie 名称
Theme string
DashboardTheme string
CustomCode string
CustomCodeDashboard string
ViewPassword string // 前台查看密码
}
Oauth2 struct {
Type string
Admin string // 管理员用户名列表
ClientID string
ClientSecret string
Endpoint string
Type string
Admin string // 管理员用户名列表
AdminGroups string // 管理员用户组列表
ClientID string
ClientSecret string
Endpoint string
OidcDisplayName string // for OIDC Display Name
OidcIssuer string // for OIDC Issuer
OidcLogoutURL string // for OIDC Logout URL
OidcRegisterURL string // for OIDC Register URL
OidcLoginClaim string // for OIDC Claim
OidcGroupClaim string // for OIDC Group Claim
OidcScopes string // for OIDC Scopes
OidcAutoCreate bool // for OIDC Auto Create
OidcAutoLogin bool // for OIDC Auto Login
}
HTTPPort uint
GRPCPort uint
@@ -181,6 +193,18 @@ func (c *Config) Read(path string) error {
if c.DDNS.MaxRetries == 0 {
c.DDNS.MaxRetries = 3
}
if c.Oauth2.OidcScopes == "" {
c.Oauth2.OidcScopes = "openid,profile,email"
}
if c.Oauth2.OidcLoginClaim == "" {
c.Oauth2.OidcLoginClaim = "sub"
}
if c.Oauth2.OidcDisplayName == "" {
c.Oauth2.OidcDisplayName = "OIDC"
}
if c.Oauth2.OidcGroupClaim == "" {
c.Oauth2.OidcGroupClaim = "groups"
}
c.updateIgnoredIPNotificationID()
return nil
+16 -10
View File
@@ -13,24 +13,30 @@ import (
const (
_ = iota
TaskTypeHTTPGET
TaskTypeHTTPGet
TaskTypeICMPPing
TaskTypeTCPPing
TaskTypeCommand
TaskTypeTerminal
TaskTypeUpgrade
TaskTypeKeepalive
TaskTypeTerminalGRPC
TaskTypeNAT
TaskTypeReportHostInfo
TaskTypeFM
)
type TerminalTask struct {
// websocket 主机名
Host string `json:"host,omitempty"`
// 是否启用 SSL
UseSSL bool `json:"use_ssl,omitempty"`
// 会话标识
Session string `json:"session,omitempty"`
// Agent在连接Server时需要的额外Cookie信息
Cookie string `json:"cookie,omitempty"`
StreamID string
}
type TaskNAT struct {
StreamID string
Host string
}
type TaskFM struct {
StreamID string
}
const (
@@ -120,7 +126,7 @@ func (m *Monitor) AfterFind(tx *gorm.DB) error {
// IsServiceSentinelNeeded 判断该任务类型是否需要进行服务监控 需要则返回true
func IsServiceSentinelNeeded(t uint64) bool {
return t != TaskTypeCommand && t != TaskTypeTerminal && t != TaskTypeUpgrade
return t != TaskTypeCommand && t != TaskTypeTerminalGRPC && t != TaskTypeUpgrade
}
func (m *Monitor) InitSkipServers() error {
+9
View File
@@ -0,0 +1,9 @@
package model
type NAT struct {
Common
Name string
ServerID uint64
Host string
Domain string `gorm:"unique"`
}
+9
View File
@@ -170,14 +170,23 @@ func (ns *NotificationServerBundle) replaceParamsInString(str string, message st
if ns.Server != nil {
str = strings.ReplaceAll(str, "#SERVER.NAME#", mod(ns.Server.Name))
str = strings.ReplaceAll(str, "#SERVER.ID#", mod(fmt.Sprintf("%d", ns.Server.ID)))
str = strings.ReplaceAll(str, "#SERVER.CPU#", mod(fmt.Sprintf("%f", ns.Server.State.CPU)))
str = strings.ReplaceAll(str, "#SERVER.MEM#", mod(fmt.Sprintf("%d", ns.Server.State.MemUsed)))
str = strings.ReplaceAll(str, "#SERVER.SWAP#", mod(fmt.Sprintf("%d", ns.Server.State.SwapUsed)))
str = strings.ReplaceAll(str, "#SERVER.DISK#", mod(fmt.Sprintf("%d", ns.Server.State.DiskUsed)))
str = strings.ReplaceAll(str, "#SERVER.MEMUSED#", mod(fmt.Sprintf("%d", ns.Server.State.MemUsed)))
str = strings.ReplaceAll(str, "#SERVER.SWAPUSED#", mod(fmt.Sprintf("%d", ns.Server.State.SwapUsed)))
str = strings.ReplaceAll(str, "#SERVER.DISKUSED#", mod(fmt.Sprintf("%d", ns.Server.State.DiskUsed)))
str = strings.ReplaceAll(str, "#SERVER.MEMTOTAL#", mod(fmt.Sprintf("%d", ns.Server.Host.MemTotal)))
str = strings.ReplaceAll(str, "#SERVER.SWAPTOTAL#", mod(fmt.Sprintf("%d", ns.Server.Host.SwapTotal)))
str = strings.ReplaceAll(str, "#SERVER.DISKTOTAL#", mod(fmt.Sprintf("%d", ns.Server.Host.DiskTotal)))
str = strings.ReplaceAll(str, "#SERVER.NETINSPEED#", mod(fmt.Sprintf("%d", ns.Server.State.NetInSpeed)))
str = strings.ReplaceAll(str, "#SERVER.NETOUTSPEED#", mod(fmt.Sprintf("%d", ns.Server.State.NetOutSpeed)))
str = strings.ReplaceAll(str, "#SERVER.TRANSFERIN#", mod(fmt.Sprintf("%d", ns.Server.State.NetInTransfer)))
str = strings.ReplaceAll(str, "#SERVER.TRANSFEROUT#", mod(fmt.Sprintf("%d", ns.Server.State.NetOutTransfer)))
str = strings.ReplaceAll(str, "#SERVER.NETINTRANSFER#", mod(fmt.Sprintf("%d", ns.Server.State.NetInTransfer)))
str = strings.ReplaceAll(str, "#SERVER.NETOUTTRANSFER#", mod(fmt.Sprintf("%d", ns.Server.State.NetOutTransfer)))
str = strings.ReplaceAll(str, "#SERVER.LOAD1#", mod(fmt.Sprintf("%f", ns.Server.State.Load1)))
str = strings.ReplaceAll(str, "#SERVER.LOAD5#", mod(fmt.Sprintf("%f", ns.Server.State.Load5)))
str = strings.ReplaceAll(str, "#SERVER.LOAD15#", mod(fmt.Sprintf("%f", ns.Server.State.Load15)))
+5 -5
View File
@@ -72,11 +72,11 @@ func execCase(t *testing.T, item testSt) {
UdpConnCount: 0,
ProcessCount: 0,
},
LastActive: time.Time{},
TaskClose: nil,
TaskStream: nil,
PrevHourlyTransferIn: 0,
PrevHourlyTransferOut: 0,
LastActive: time.Time{},
TaskClose: nil,
TaskStream: nil,
PrevTransferInSnapshot: 0,
PrevTransferOutSnapshot: 0,
}
ns := NotificationServerBundle{
Notification: &n,
+7 -17
View File
@@ -1,10 +1,13 @@
package model
import (
"slices"
"strings"
"time"
"gorm.io/gorm"
"github.com/naiba/nezha/pkg/utils"
)
const (
@@ -42,19 +45,6 @@ func percentage(used, total uint64) float64 {
return float64(used) * 100 / float64(total)
}
func maxSliceValue(slice []float64) float64 {
if len(slice) != 0 {
max := slice[0]
for _, val := range slice {
if max < val {
max = val
}
}
return max
}
return 0
}
// Snapshot 未通过规则返回 struct{}{}, 通过返回 nil
func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) interface{} {
// 监控全部但是排除了此服务器
@@ -103,21 +93,21 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
src = float64(server.LastActive.Unix())
}
case "transfer_in_cycle":
src = float64(server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn))
src = float64(utils.Uint64SubInt64(server.State.NetInTransfer, server.PrevTransferInSnapshot))
if u.CycleInterval != 0 {
var res NResult
db.Model(&Transfer{}).Select("SUM(`in`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
src += float64(res.N)
}
case "transfer_out_cycle":
src = float64(server.State.NetOutTransfer - uint64(server.PrevHourlyTransferOut))
src = float64(utils.Uint64SubInt64(server.State.NetOutTransfer, server.PrevTransferOutSnapshot))
if u.CycleInterval != 0 {
var res NResult
db.Model(&Transfer{}).Select("SUM(`out`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
src += float64(res.N)
}
case "transfer_all_cycle":
src = float64(server.State.NetOutTransfer - uint64(server.PrevHourlyTransferOut) + server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn))
src = float64(utils.Uint64SubInt64(server.State.NetOutTransfer, server.PrevTransferOutSnapshot) + utils.Uint64SubInt64(server.State.NetInTransfer, server.PrevTransferInSnapshot))
if u.CycleInterval != 0 {
var res NResult
db.Model(&Transfer{}).Select("SUM(`in`+`out`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
@@ -143,7 +133,7 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
temp = append(temp, tempStat.Temperature)
}
}
src = maxSliceValue(temp)
src = slices.Max(temp)
}
}
+14 -11
View File
@@ -3,6 +3,7 @@ package model
import (
"fmt"
"html/template"
"sync"
"time"
"github.com/naiba/nezha/pkg/utils"
@@ -17,21 +18,22 @@ type Server struct {
Note string `json:"-"` // 管理员可见备注
DisplayIndex int // 展示排序,越大越靠前
HideForGuest bool // 对游客隐藏
EnableDDNS bool // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效
EnableIPv4 bool // 是否启用DDNS IPv4
EnableIpv6 bool // 是否启用DDNS IPv6
DDNSDomain string // DDNS中的前缀 如基础域名为abc.oracle DDNSName为mjj 就会把mjj.abc.oracle解析服务器IP 为空则停用
DDNSProfile string // DDNS配置
EnableDDNS bool `json:"-"` // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效
EnableIPv4 bool `json:"-"` // 是否启用DDNS IPv4
EnableIpv6 bool `json:"-"` // 是否启用DDNS IPv6
DDNSDomain string `json:"-"` // DDNS中的前缀 如基础域名为abc.oracle DDNSName为mjj 就会把mjj.abc.oracle解析服务器IP 为空则停用
DDNSProfile string `json:"-"` // DDNS配置
Host *Host `gorm:"-"`
State *HostState `gorm:"-"`
LastActive time.Time `gorm:"-"`
TaskClose chan error `gorm:"-" json:"-"`
TaskStream pb.NezhaService_RequestTaskServer `gorm:"-" json:"-"`
TaskClose chan error `gorm:"-" json:"-"`
TaskCloseLock *sync.Mutex `gorm:"-" json:"-"`
TaskStream pb.NezhaService_RequestTaskServer `gorm:"-" json:"-"`
PrevHourlyTransferIn int64 `gorm:"-" json:"-"` // 上次数据点时的入站使用量
PrevHourlyTransferOut int64 `gorm:"-" json:"-"` // 上次数据点时的出站使用量
PrevTransferInSnapshot int64 `gorm:"-" json:"-"` // 上次数据点时的入站使用量
PrevTransferOutSnapshot int64 `gorm:"-" json:"-"` // 上次数据点时的出站使用量
}
func (s *Server) CopyFromRunningServer(old *Server) {
@@ -39,9 +41,10 @@ func (s *Server) CopyFromRunningServer(old *Server) {
s.State = old.State
s.LastActive = old.LastActive
s.TaskClose = old.TaskClose
s.TaskCloseLock = old.TaskCloseLock
s.TaskStream = old.TaskStream
s.PrevHourlyTransferIn = old.PrevHourlyTransferIn
s.PrevHourlyTransferOut = old.PrevHourlyTransferOut
s.PrevTransferInSnapshot = old.PrevTransferInSnapshot
s.PrevTransferOutSnapshot = old.PrevTransferOutSnapshot
}
func boolToString(b bool) string {
+131 -83
View File
@@ -2,169 +2,217 @@ package ddns
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"github.com/naiba/nezha/pkg/utils"
)
const baseEndpoint = "https://api.cloudflare.com/client/v4/zones"
type ProviderCloudflare struct {
Secret string
secret string
zoneId string
recordId string
domainConfig *DomainConfig
}
func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool {
if domainConfig == nil {
return false
}
type cfReq struct {
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
TTL uint32 `json:"ttl"`
Proxied bool `json:"proxied"`
}
zoneID, err := provider.getZoneID(domainConfig.FullDomain)
type cfResp struct {
Result []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"result"`
}
func NewProviderCloudflare(s string) *ProviderCloudflare {
return &ProviderCloudflare{
secret: s,
}
}
func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) error {
if domainConfig == nil {
return fmt.Errorf("获取 DDNS 配置失败")
}
provider.domainConfig = domainConfig
err := provider.getZoneID()
if err != nil {
log.Printf("无法获取 zone ID: %s\n", err)
return false
return fmt.Errorf("无法获取 zone ID: %s", err)
}
// 当IPv4和IPv6同时成功才算作成功
var resultV4 = true
var resultV6 = true
if domainConfig.EnableIPv4 {
if !provider.addDomainRecord(zoneID, domainConfig, true) {
resultV4 = false
if provider.domainConfig.EnableIPv4 {
if err = provider.addDomainRecord(true); err != nil {
return err
}
}
if domainConfig.EnableIpv6 {
if !provider.addDomainRecord(zoneID, domainConfig, false) {
resultV6 = false
if provider.domainConfig.EnableIpv6 {
if err = provider.addDomainRecord(false); err != nil {
return err
}
}
return resultV4 && resultV6
return nil
}
func (provider *ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *DomainConfig, isIpv4 bool) bool {
record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain, isIpv4)
func (provider *ProviderCloudflare) addDomainRecord(isIpv4 bool) error {
err := provider.findDNSRecord(isIpv4)
if err != nil {
log.Printf("查找 DNS 记录时出错: %s\n", err)
return false
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
}
if record == nil {
if provider.recordId == "" {
// 添加 DNS 记录
return provider.createDNSRecord(zoneID, domainConfig, isIpv4)
return provider.createDNSRecord(isIpv4)
} else {
// 更新 DNS 记录
return provider.updateDNSRecord(zoneID, record["id"].(string), domainConfig, isIpv4)
return provider.updateDNSRecord(isIpv4)
}
}
func (provider *ProviderCloudflare) getZoneID(domain string) (string, error) {
_, realDomain := SplitDomain(domain)
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", realDomain)
body, err := provider.sendRequest("GET", url, nil)
func (provider *ProviderCloudflare) getZoneID() error {
_, realDomain := splitDomain(provider.domainConfig.FullDomain)
zu, _ := url.Parse(baseEndpoint)
q := zu.Query()
q.Set("name", realDomain)
zu.RawQuery = q.Encode()
body, err := provider.sendRequest("GET", zu.String(), nil)
if err != nil {
return "", err
return err
}
var res map[string]interface{}
err = json.Unmarshal(body, &res)
res := &cfResp{}
err = utils.Json.Unmarshal(body, res)
if err != nil {
return "", err
return err
}
result := res["result"].([]interface{})
result := res.Result
if len(result) > 0 {
zoneID := result[0].(map[string]interface{})["id"].(string)
return zoneID, nil
provider.zoneId = result[0].ID
return nil
}
return "", fmt.Errorf("找不到 Zone ID")
return fmt.Errorf("找不到 Zone ID")
}
func (provider *ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) {
var ipType = "A"
if !isIPv4 {
func (provider *ProviderCloudflare) findDNSRecord(isIPv4 bool) error {
var ipType string
if isIPv4 {
ipType = "A"
} else {
ipType = "AAAA"
}
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=%s&name=%s", zoneID, ipType, domain)
body, err := provider.sendRequest("GET", url, nil)
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
du, _ := url.Parse(de)
q := du.Query()
q.Set("name", provider.domainConfig.FullDomain)
q.Set("type", ipType)
du.RawQuery = q.Encode()
body, err := provider.sendRequest("GET", du.String(), nil)
if err != nil {
return nil, err
return err
}
var res map[string]interface{}
err = json.Unmarshal(body, &res)
res := &cfResp{}
err = utils.Json.Unmarshal(body, res)
if err != nil {
return nil, err
return err
}
result := res["result"].([]interface{})
result := res.Result
if len(result) > 0 {
return result[0].(map[string]interface{}), nil
provider.recordId = result[0].ID
return nil
}
return nil, nil // 没有找到 DNS 记录
return nil
}
func (provider *ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
func (provider *ProviderCloudflare) createDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = domainConfig.Ipv6Addr
ipAddr = provider.domainConfig.Ipv6Addr
}
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID)
data := map[string]interface{}{
"type": ipType,
"name": domainConfig.FullDomain,
"content": ipAddr,
"ttl": 60,
"proxied": false,
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
data := &cfReq{
Name: provider.domainConfig.FullDomain,
Type: ipType,
Content: ipAddr,
TTL: 60,
Proxied: false,
}
jsonData, _ := json.Marshal(data)
_, err := provider.sendRequest("POST", url, jsonData)
return err == nil
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("POST", de, jsonData)
return err
}
func (provider *ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
func (provider *ProviderCloudflare) updateDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = domainConfig.Ipv6Addr
ipAddr = provider.domainConfig.Ipv6Addr
}
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneID, recordID)
data := map[string]interface{}{
"type": ipType,
"name": domainConfig.FullDomain,
"content": ipAddr,
"ttl": 60,
"proxied": false,
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records", provider.recordId)
data := &cfReq{
Name: provider.domainConfig.FullDomain,
Type: ipType,
Content: ipAddr,
TTL: 60,
Proxied: false,
}
jsonData, _ := json.Marshal(data)
_, err := provider.sendRequest("PATCH", url, jsonData)
return err == nil
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("PATCH", de, jsonData)
return err
}
// 以下为辅助方法,如发送 HTTP 请求等
func (provider *ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", provider.Secret))
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", provider.secret))
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
resp, err := utils.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s", err.Error())
}
}(resp.Body)
+9 -1
View File
@@ -1,5 +1,7 @@
package ddns
import "golang.org/x/net/publicsuffix"
type DomainConfig struct {
EnableIPv4 bool
EnableIpv6 bool
@@ -10,5 +12,11 @@ type DomainConfig struct {
type Provider interface {
// UpdateDomain Return is updated
UpdateDomain(domainConfig *DomainConfig) bool
UpdateDomain(*DomainConfig) error
}
func splitDomain(domain string) (prefix string, realDomain string) {
realDomain, _ = publicsuffix.EffectiveTLDPlusOne(domain)
prefix = domain[:len(domain)-len(realDomain)-1]
return prefix, realDomain
}
+2 -2
View File
@@ -2,6 +2,6 @@ package ddns
type ProviderDummy struct{}
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
return false
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) error {
return nil
}
-40
View File
@@ -1,40 +0,0 @@
package ddns
import (
"golang.org/x/net/publicsuffix"
"net/http"
"strings"
)
func (provider ProviderWebHook) FormatWebhookString(s string, config *DomainConfig, ipType string) string {
if config == nil {
return s
}
result := strings.TrimSpace(s)
result = strings.Replace(s, "{ip}", config.Ipv4Addr, -1)
result = strings.Replace(result, "{domain}", config.FullDomain, -1)
result = strings.Replace(result, "{type}", ipType, -1)
// remove \r
result = strings.Replace(result, "\r", "", -1)
return result
}
func SetStringHeadersToRequest(req *http.Request, headers []string) {
if req == nil {
return
}
for _, element := range headers {
kv := strings.SplitN(element, ":", 2)
if len(kv) == 2 {
req.Header.Add(kv[0], kv[1])
}
}
}
// SplitDomain 分割域名为前缀和一级域名
func SplitDomain(domain string) (prefix string, realDomain string) {
realDomain, _ = publicsuffix.EffectiveTLDPlusOne(domain)
prefix = domain[:len(domain)-len(realDomain)-1]
return prefix, realDomain
}
+119 -84
View File
@@ -5,145 +5,180 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/naiba/nezha/pkg/utils"
)
const (
url = "https://dnspod.tencentcloudapi.com"
)
const te = "https://dnspod.tencentcloudapi.com"
type ProviderTencentCloud struct {
SecretID string
SecretKey string
secretID string
secretKey string
domainConfig *DomainConfig
resp *tcResp
}
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bool {
if domainConfig == nil {
return false
type tcReq struct {
RecordType string `json:"RecordType"`
Domain string `json:"Domain"`
RecordLine string `json:"RecordLine"`
Subdomain string `json:"Subdomain,omitempty"`
SubDomain string `json:"SubDomain,omitempty"` // As is
Value string `json:"Value,omitempty"`
TTL uint32 `json:"TTL,omitempty"`
RecordId uint64 `json:"RecordId,omitempty"`
}
type tcResp struct {
Response struct {
RecordList []struct {
RecordId uint64
Value string
}
Error struct {
Code string
}
}
}
func NewProviderTencentCloud(id, key string) *ProviderTencentCloud {
return &ProviderTencentCloud{
secretID: id,
secretKey: key,
}
}
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) error {
if domainConfig == nil {
return fmt.Errorf("获取 DDNS 配置失败")
}
provider.domainConfig = domainConfig
// 当IPv4和IPv6同时成功才算作成功
var resultV4 = true
var resultV6 = true
if domainConfig.EnableIPv4 {
if !provider.addDomainRecord(domainConfig, true) {
resultV4 = false
var err error
if provider.domainConfig.EnableIPv4 {
if err = provider.addDomainRecord(true); err != nil {
return err
}
}
if domainConfig.EnableIpv6 {
if !provider.addDomainRecord(domainConfig, false) {
resultV6 = false
if provider.domainConfig.EnableIpv6 {
if err = provider.addDomainRecord(false); err != nil {
return err
}
}
return resultV4 && resultV6
return err
}
func (provider *ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig, isIpv4 bool) bool {
record, err := provider.findDNSRecord(domainConfig.FullDomain, isIpv4)
func (provider *ProviderTencentCloud) addDomainRecord(isIpv4 bool) error {
err := provider.findDNSRecord(isIpv4)
if err != nil {
log.Printf("查找 DNS 记录时出错: %s\n", err)
return false
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
}
if errResponse, ok := record["Error"].(map[string]interface{}); ok {
if errCode, ok := errResponse["Code"].(string); ok && errCode == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
// 添加 DNS 记录
return provider.createDNSRecord(domainConfig.FullDomain, domainConfig, isIpv4)
} else {
log.Printf("查询 DNS 记录时出错,错误代码为: %s\n", errCode)
}
if provider.resp.Response.Error.Code == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
return provider.createDNSRecord(isIpv4)
} else if provider.resp.Response.Error.Code != "" {
return fmt.Errorf("查询 DNS 记录时出错,错误代码为: %s", provider.resp.Response.Error.Code)
}
// 默认情况下更新 DNS 记录
return provider.updateDNSRecord(domainConfig.FullDomain, record["RecordList"].([]interface{})[0].(map[string]interface{})["RecordId"].(float64), domainConfig, isIpv4)
return provider.updateDNSRecord(isIpv4)
}
func (provider *ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (map[string]interface{}, error) {
var ipType = "A"
if !isIPv4 {
func (provider *ProviderTencentCloud) findDNSRecord(isIPv4 bool) error {
var ipType string
if isIPv4 {
ipType = "A"
} else {
ipType = "AAAA"
}
_, realDomain := SplitDomain(domain)
prefix, _ := SplitDomain(domain)
data := map[string]interface{}{
"RecordType": ipType,
"Domain": realDomain,
"RecordLine": "默认",
"Subdomain": prefix,
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: ipType,
Domain: realDomain,
RecordLine: "默认",
Subdomain: prefix,
}
jsonData, _ := json.Marshal(data)
jsonData, _ := utils.Json.Marshal(data)
body, err := provider.sendRequest("DescribeRecordList", jsonData)
if err != nil {
return nil, err
return err
}
var res map[string]interface{}
err = json.Unmarshal(body, &res)
provider.resp = &tcResp{}
err = utils.Json.Unmarshal(body, provider.resp)
if err != nil {
return nil, err
return err
}
result := res["Response"].(map[string]interface{})
return result, nil
return nil
}
func (provider *ProviderTencentCloud) createDNSRecord(domain string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
func (provider *ProviderTencentCloud) createDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = domainConfig.Ipv6Addr
ipAddr = provider.domainConfig.Ipv6Addr
}
_, realDomain := SplitDomain(domain)
prefix, _ := SplitDomain(domain)
data := map[string]interface{}{
"RecordType": ipType,
"RecordLine": "默认",
"Domain": realDomain,
"SubDomain": prefix,
"Value": ipAddr,
"TTL": 600,
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: ipType,
RecordLine: "默认",
Domain: realDomain,
SubDomain: prefix,
Value: ipAddr,
TTL: 600,
}
jsonData, _ := json.Marshal(data)
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("CreateRecord", jsonData)
return err == nil
return err
}
func (provider *ProviderTencentCloud) updateDNSRecord(domain string, recordID float64, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
func (provider *ProviderTencentCloud) updateDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = domainConfig.Ipv6Addr
ipAddr = provider.domainConfig.Ipv6Addr
}
_, realDomain := SplitDomain(domain)
prefix, _ := SplitDomain(domain)
data := map[string]interface{}{
"RecordType": ipType,
"RecordLine": "默认",
"Domain": realDomain,
"SubDomain": prefix,
"Value": ipAddr,
"TTL": 600,
"RecordId": recordID,
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: ipType,
RecordLine: "默认",
Domain: realDomain,
SubDomain: prefix,
Value: ipAddr,
TTL: 600,
RecordId: provider.resp.Response.RecordList[0].RecordId,
}
jsonData, _ := json.Marshal(data)
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("ModifyRecord", jsonData)
return err == nil
return err
}
// 以下为辅助方法,如发送 HTTP 请求等
func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
req, err := http.NewRequest("POST", te, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
@@ -151,8 +186,8 @@ func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-TC-Version", "2021-03-23")
provider.signRequest(provider.SecretID, provider.SecretKey, req, action, string(data))
resp, err := client.Do(req)
provider.signRequest(provider.secretID, provider.secretKey, req, action, string(data))
resp, err := utils.HttpClient.Do(req)
if err != nil {
return nil, err
}
+91 -40
View File
@@ -2,58 +2,109 @@ package ddns
import (
"bytes"
"log"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/naiba/nezha/pkg/utils"
)
type ProviderWebHook struct {
URL string
RequestMethod string
RequestBody string
RequestHeader string
url string
requestMethod string
requestBody string
requestHeader string
domainConfig *DomainConfig
}
func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool {
func NewProviderWebHook(s, rm, rb, rh string) *ProviderWebHook {
return &ProviderWebHook{
url: s,
requestMethod: rm,
requestBody: rb,
requestHeader: rh,
}
}
func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) error {
if domainConfig == nil {
return false
return fmt.Errorf("获取 DDNS 配置失败")
}
provider.domainConfig = domainConfig
if provider.domainConfig.FullDomain == "" {
return fmt.Errorf("failed to update an empty domain")
}
if domainConfig.FullDomain == "" {
log.Println("NEZHA>> Failed to update an empty domain")
return false
}
updated := false
client := &http.Client{}
if domainConfig.EnableIPv4 && domainConfig.Ipv4Addr != "" {
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv4")
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv4")
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv4")
headers := strings.Split(header, "\n")
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
if err == nil && req != nil {
SetStringHeadersToRequest(req, headers)
if _, err := client.Do(req); err != nil {
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
} else {
updated = true
}
if provider.domainConfig.EnableIPv4 && provider.domainConfig.Ipv4Addr != "" {
req, err := provider.prepareRequest(true)
if err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
if _, err := utils.HttpClient.Do(req); err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
}
if domainConfig.EnableIpv6 && domainConfig.Ipv6Addr != "" {
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv6")
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv6")
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv6")
headers := strings.Split(header, "\n")
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
if err == nil && req != nil {
SetStringHeadersToRequest(req, headers)
if _, err := client.Do(req); err != nil {
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
} else {
updated = true
}
if provider.domainConfig.EnableIpv6 && provider.domainConfig.Ipv6Addr != "" {
req, err := provider.prepareRequest(false)
if err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
if _, err := utils.HttpClient.Do(req); err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
}
return updated
return nil
}
func (provider *ProviderWebHook) prepareRequest(isIPv4 bool) (*http.Request, error) {
u, err := url.Parse(provider.url)
if err != nil {
return nil, fmt.Errorf("failed parsing url: %v", err)
}
// Only handle queries here
q := u.Query()
for p, vals := range q {
for n, v := range vals {
vals[n] = provider.formatWebhookString(v, isIPv4)
}
q[p] = vals
}
u.RawQuery = q.Encode()
body := provider.formatWebhookString(provider.requestBody, isIPv4)
header := provider.formatWebhookString(provider.requestHeader, isIPv4)
headers := strings.Split(header, "\n")
req, err := http.NewRequest(provider.requestMethod, u.String(), bytes.NewBufferString(body))
if err != nil {
return nil, fmt.Errorf("failed creating new request: %v", err)
}
utils.SetStringHeadersToRequest(req, headers)
return req, nil
}
func (provider *ProviderWebHook) formatWebhookString(s string, isIPv4 bool) string {
var ipAddr, ipType string
if isIPv4 {
ipAddr = provider.domainConfig.Ipv4Addr
ipType = "ipv4"
} else {
ipAddr = provider.domainConfig.Ipv6Addr
ipType = "ipv6"
}
r := strings.NewReplacer(
"{ip}", ipAddr,
"{domain}", provider.domainConfig.FullDomain,
"{type}", ipType,
"\r", "",
)
result := r.Replace(strings.TrimSpace(s))
return result
}
+1
View File
@@ -0,0 +1 @@
stub
+54
View File
@@ -0,0 +1,54 @@
package geoip
import (
"embed"
"fmt"
"log"
"net"
"strings"
maxminddb "github.com/oschwald/maxminddb-golang"
)
//go:embed geoip.db
var geoDBFS embed.FS
var (
dbData []byte
err error
)
type IPInfo struct {
Country string `maxminddb:"country"`
CountryName string `maxminddb:"country_name"`
Continent string `maxminddb:"continent"`
ContinentName string `maxminddb:"continent_name"`
}
func init() {
dbData, err = geoDBFS.ReadFile("geoip.db")
if err != nil {
log.Printf("NEZHA>> Failed to open geoip database: %v", err)
}
}
func Lookup(ip net.IP, record *IPInfo) (string, error) {
db, err := maxminddb.FromBytes(dbData)
if err != nil {
return "", err
}
defer db.Close()
err = db.Lookup(ip, record)
if err != nil {
return "", err
}
if record.Country != "" {
return strings.ToLower(record.Country), nil
} else if record.Continent != "" {
return strings.ToLower(record.Continent), nil
}
return "", fmt.Errorf("IP not found")
}
+65
View File
@@ -0,0 +1,65 @@
package grpcx
import (
"context"
"io"
"sync/atomic"
"github.com/naiba/nezha/proto"
)
var _ io.ReadWriteCloser = &IOStreamWrapper{}
type IOStream interface {
Recv() (*proto.IOStreamData, error)
Send(*proto.IOStreamData) error
Context() context.Context
}
type IOStreamWrapper struct {
IOStream
dataBuf []byte
closed *atomic.Bool
closeCh chan struct{}
}
func NewIOStreamWrapper(stream IOStream) *IOStreamWrapper {
return &IOStreamWrapper{
IOStream: stream,
closeCh: make(chan struct{}),
closed: new(atomic.Bool),
}
}
func (iw *IOStreamWrapper) Read(p []byte) (n int, err error) {
if len(iw.dataBuf) > 0 {
n := copy(p, iw.dataBuf)
iw.dataBuf = iw.dataBuf[n:]
return n, nil
}
var data *proto.IOStreamData
if data, err = iw.Recv(); err != nil {
return 0, err
}
n = copy(p, data.Data)
if n < len(data.Data) {
iw.dataBuf = data.Data[n:]
}
return n, nil
}
func (iw *IOStreamWrapper) Write(p []byte) (n int, err error) {
err = iw.Send(&proto.IOStreamData{Data: p})
return len(p), err
}
func (iw *IOStreamWrapper) Close() error {
if iw.closed.CompareAndSwap(false, true) {
close(iw.closeCh)
}
return nil
}
func (iw *IOStreamWrapper) Wait() {
<-iw.closeCh
}
+3
View File
@@ -16,6 +16,7 @@ var adminPage = map[string]bool{
"/monitor": true,
"/setting": true,
"/notification": true,
"/nat": true,
"/cron": true,
"/api": true,
}
@@ -25,6 +26,8 @@ func CommonEnvironment(c *gin.Context, data map[string]interface{}) gin.H {
data["Version"] = singleton.Version
data["Conf"] = singleton.Conf
data["Themes"] = model.Themes
data["CustomCode"] = singleton.Conf.Site.CustomCode
data["CustomCodeDashboard"] = singleton.Conf.Site.CustomCodeDashboard
// 是否是管理页面
data["IsAdminPage"] = adminPage[data["MatchedPath"].(string)]
// 站点标题
+1 -2
View File
@@ -37,8 +37,7 @@ func ValidateViewPassword(opt ValidateViewPasswordOption) gin.HandlerFunc {
}
if opt.IsPage {
c.HTML(http.StatusOK, GetPreferredTheme(c, "/viewpassword"), CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
"CustomCode": singleton.Conf.Site.CustomCode,
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
}))
} else {
+56
View File
@@ -0,0 +1,56 @@
package general
import (
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/service/singleton"
)
type UserInfo struct {
Sub string `json:"sub"`
Username string `json:"preferred_username"`
Email string `json:"email"`
Name string `json:"name"`
Groups []string `json:"groups,omitempty"`
Roles []string `json:"roles,omitempty"`
}
func (u UserInfo) MapToNezhaUser(loginClaim string, groupClaim string, adminGroups []string, autoCreate bool) model.User {
var user model.User
var login string
var groups []string
var isAdmin bool
if loginClaim == "email" {
login = u.Email
} else if loginClaim == "preferred_username" {
login = u.Username
} else {
login = u.Sub
}
if groupClaim == "roles" {
groups = u.Roles
} else {
groups = u.Groups
}
// Check if user is admin
adminGroupSet := make(map[string]struct{}, len(adminGroups))
for _, adminGroup := range adminGroups {
adminGroupSet[adminGroup] = struct{}{}
}
for _, group := range groups {
if _, found := adminGroupSet[group]; found {
isAdmin = true
break
}
}
result := singleton.DB.Where("login = ?", login).First(&user)
user.Login = login
user.Email = u.Email
user.Name = u.Name
user.SuperAdmin = isAdmin
if result.Error != nil && autoCreate {
singleton.DB.Create(&user)
} else if result.Error != nil {
return model.User{}
}
return user
}
+56
View File
@@ -0,0 +1,56 @@
package utils
import (
"bytes"
"io"
"net"
"net/http"
"github.com/gin-gonic/gin"
)
var _ io.ReadWriteCloser = &RequestWrapper{}
type RequestWrapper struct {
req *http.Request
reader *bytes.Buffer
writer net.Conn
}
func NewRequestWrapper(req *http.Request, writer gin.ResponseWriter) (*RequestWrapper, error) {
conn, _, err := writer.Hijack()
if err != nil {
return nil, err
}
buf := bytes.NewBuffer(nil)
if err = req.Write(buf); err != nil {
return nil, err
}
return &RequestWrapper{
req: req,
reader: buf,
writer: conn,
}, nil
}
func (rw *RequestWrapper) Read(p []byte) (int, error) {
count, err := rw.reader.Read(p)
if err == nil {
return count, nil
}
if err != io.EOF {
return count, err
}
// request 数据读完之后等待客户端断开连接或 grpc 超时
return rw.writer.Read(p)
}
func (rw *RequestWrapper) Write(p []byte) (int, error) {
return rw.writer.Write(p)
}
func (rw *RequestWrapper) Close() error {
rw.req.Body.Close()
rw.writer.Close()
return nil
}
+23
View File
@@ -3,6 +3,7 @@ package utils
import (
"crypto/rand"
"math/big"
"net/http"
"os"
"regexp"
"strings"
@@ -76,3 +77,25 @@ func GenerateRandomString(n int) (string, error) {
}
return string(ret), nil
}
func Uint64SubInt64(a uint64, b int64) uint64 {
if b < 0 {
return a + uint64(-b)
}
if a < uint64(b) {
return 0
}
return a - uint64(b)
}
func SetStringHeadersToRequest(req *http.Request, headers []string) {
if req == nil {
return
}
for _, element := range headers {
kv := strings.SplitN(element, ":", 2)
if len(kv) == 2 {
req.Header.Add(kv[0], kv[1])
}
}
}
+41 -10
View File
@@ -1,25 +1,56 @@
package websocketx
import (
"io"
"sync"
"github.com/gorilla/websocket"
"github.com/samber/lo"
)
var _ io.ReadWriteCloser = &Conn{}
type Conn struct {
*websocket.Conn
writeLock sync.Mutex
writeLock *sync.Mutex
dataBuf []byte
}
func (conn *Conn) WriteMessage(msgType int, data []byte) error {
func NewConn(conn *websocket.Conn) *Conn {
return &Conn{Conn: conn, writeLock: new(sync.Mutex)}
}
func (conn *Conn) Write(data []byte) (int, error) {
conn.writeLock.Lock()
defer conn.writeLock.Unlock()
var err error
lo.TryCatchWithErrorValue(func() error {
return conn.Conn.WriteMessage(msgType, data)
}, func(res any) {
err = res.(error)
})
return err
if err := conn.Conn.WriteMessage(websocket.BinaryMessage, data); err != nil {
return 0, err
}
return len(data), nil
}
func (conn *Conn) WriteMessage(messageType int, data []byte) error {
conn.writeLock.Lock()
defer conn.writeLock.Unlock()
return conn.Conn.WriteMessage(messageType, data)
}
func (conn *Conn) Read(data []byte) (int, error) {
if len(conn.dataBuf) > 0 {
n := copy(data, conn.dataBuf)
conn.dataBuf = conn.dataBuf[n:]
return n, nil
}
mType, innerData, err := conn.Conn.ReadMessage()
if err != nil {
return 0, err
}
// 将文本消息转换为命令输入
if mType == websocket.TextMessage {
innerData = append([]byte{0}, innerData...)
}
n := copy(data, innerData)
if n < len(innerData) {
conn.dataBuf = innerData[n:]
}
return n, nil
}
+156 -12
View File
@@ -35,7 +35,7 @@ type Host struct {
Virtualization string `protobuf:"bytes,8,opt,name=virtualization,proto3" json:"virtualization,omitempty"`
BootTime uint64 `protobuf:"varint,9,opt,name=boot_time,json=bootTime,proto3" json:"boot_time,omitempty"`
Ip string `protobuf:"bytes,10,opt,name=ip,proto3" json:"ip,omitempty"`
CountryCode string `protobuf:"bytes,11,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
CountryCode string `protobuf:"bytes,11,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` // deprecated
Version string `protobuf:"bytes,12,opt,name=version,proto3" json:"version,omitempty"`
Gpu []string `protobuf:"bytes,13,rep,name=gpu,proto3" json:"gpu,omitempty"`
}
@@ -582,6 +582,108 @@ func (x *Receipt) GetProced() bool {
return false
}
type IOStreamData struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
}
func (x *IOStreamData) Reset() {
*x = IOStreamData{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_nezha_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *IOStreamData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IOStreamData) ProtoMessage() {}
func (x *IOStreamData) ProtoReflect() protoreflect.Message {
mi := &file_proto_nezha_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use IOStreamData.ProtoReflect.Descriptor instead.
func (*IOStreamData) Descriptor() ([]byte, []int) {
return file_proto_nezha_proto_rawDescGZIP(), []int{6}
}
func (x *IOStreamData) GetData() []byte {
if x != nil {
return x.Data
}
return nil
}
type GeoIP struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
CountryCode string `protobuf:"bytes,2,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
}
func (x *GeoIP) Reset() {
*x = GeoIP{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_nezha_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GeoIP) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIP) ProtoMessage() {}
func (x *GeoIP) ProtoReflect() protoreflect.Message {
mi := &file_proto_nezha_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
func (*GeoIP) Descriptor() ([]byte, []int) {
return file_proto_nezha_proto_rawDescGZIP(), []int{7}
}
func (x *GeoIP) GetIp() string {
if x != nil {
return x.Ip
}
return ""
}
func (x *GeoIP) GetCountryCode() string {
if x != nil {
return x.CountryCode
}
return ""
}
var File_proto_nezha_proto protoreflect.FileDescriptor
var file_proto_nezha_proto_rawDesc = []byte{
@@ -663,7 +765,13 @@ var file_proto_nezha_proto_rawDesc = []byte{
0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x75,
0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x22, 0x21, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x65,
0x69, 0x70, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x08, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x64, 0x32, 0xd6, 0x01, 0x0a, 0x0c,
0x01, 0x28, 0x08, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x0c, 0x49,
0x4f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x64,
0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22,
0x3a, 0x0a, 0x05, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e,
0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x32, 0xbf, 0x02, 0x0a, 0x0c,
0x4e, 0x65, 0x7a, 0x68, 0x61, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x33, 0x0a, 0x11,
0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x53, 0x74, 0x61, 0x74,
0x65, 0x12, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a,
@@ -677,8 +785,14 @@ var file_proto_nezha_proto_rawDesc = []byte{
0x63, 0x65, 0x69, 0x70, 0x74, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48,
0x6f, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x73, 0x6b,
0x22, 0x00, 0x30, 0x01, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x22, 0x00, 0x30, 0x01, 0x12, 0x3a, 0x0a, 0x08, 0x49, 0x4f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x4f, 0x53, 0x74, 0x72, 0x65, 0x61,
0x6d, 0x44, 0x61, 0x74, 0x61, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x4f,
0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01,
0x12, 0x2b, 0x0a, 0x0b, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x12,
0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x1a, 0x0c, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x22, 0x00, 0x42, 0x09, 0x5a,
0x07, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -693,7 +807,7 @@ func file_proto_nezha_proto_rawDescGZIP() []byte {
return file_proto_nezha_proto_rawDescData
}
var file_proto_nezha_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_proto_nezha_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_proto_nezha_proto_goTypes = []interface{}{
(*Host)(nil), // 0: proto.Host
(*State)(nil), // 1: proto.State
@@ -701,6 +815,8 @@ var file_proto_nezha_proto_goTypes = []interface{}{
(*Task)(nil), // 3: proto.Task
(*TaskResult)(nil), // 4: proto.TaskResult
(*Receipt)(nil), // 5: proto.Receipt
(*IOStreamData)(nil), // 6: proto.IOStreamData
(*GeoIP)(nil), // 7: proto.GeoIP
}
var file_proto_nezha_proto_depIdxs = []int32{
2, // 0: proto.State.temperatures:type_name -> proto.State_SensorTemperature
@@ -708,12 +824,16 @@ var file_proto_nezha_proto_depIdxs = []int32{
0, // 2: proto.NezhaService.ReportSystemInfo:input_type -> proto.Host
4, // 3: proto.NezhaService.ReportTask:input_type -> proto.TaskResult
0, // 4: proto.NezhaService.RequestTask:input_type -> proto.Host
5, // 5: proto.NezhaService.ReportSystemState:output_type -> proto.Receipt
5, // 6: proto.NezhaService.ReportSystemInfo:output_type -> proto.Receipt
5, // 7: proto.NezhaService.ReportTask:output_type -> proto.Receipt
3, // 8: proto.NezhaService.RequestTask:output_type -> proto.Task
5, // [5:9] is the sub-list for method output_type
1, // [1:5] is the sub-list for method input_type
6, // 5: proto.NezhaService.IOStream:input_type -> proto.IOStreamData
7, // 6: proto.NezhaService.LookupGeoIP:input_type -> proto.GeoIP
5, // 7: proto.NezhaService.ReportSystemState:output_type -> proto.Receipt
5, // 8: proto.NezhaService.ReportSystemInfo:output_type -> proto.Receipt
5, // 9: proto.NezhaService.ReportTask:output_type -> proto.Receipt
3, // 10: proto.NezhaService.RequestTask:output_type -> proto.Task
6, // 11: proto.NezhaService.IOStream:output_type -> proto.IOStreamData
7, // 12: proto.NezhaService.LookupGeoIP:output_type -> proto.GeoIP
7, // [7:13] is the sub-list for method output_type
1, // [1:7] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
@@ -797,6 +917,30 @@ func file_proto_nezha_proto_init() {
return nil
}
}
file_proto_nezha_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*IOStreamData); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_nezha_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GeoIP); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -804,7 +948,7 @@ func file_proto_nezha_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_nezha_proto_rawDesc,
NumEnums: 0,
NumMessages: 6,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
+12 -1
View File
@@ -8,6 +8,8 @@ service NezhaService {
rpc ReportSystemInfo(Host)returns(Receipt){}
rpc ReportTask(TaskResult)returns(Receipt){}
rpc RequestTask(Host)returns(stream Task){}
rpc IOStream(stream IOStreamData)returns(stream IOStreamData){}
rpc LookupGeoIP(GeoIP)returns(GeoIP){}
}
message Host {
@@ -21,7 +23,7 @@ message Host {
string virtualization = 8;
uint64 boot_time = 9;
string ip = 10;
string country_code = 11;
string country_code = 11; // deprecated
string version = 12;
repeated string gpu = 13;
}
@@ -68,3 +70,12 @@ message TaskResult {
message Receipt{
bool proced = 1;
}
message IOStreamData {
bytes data = 1;
}
message GeoIP {
string ip = 1;
string country_code = 2;
}
+107 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc v3.21.12
// - protoc v5.26.1
// source: proto/nezha.proto
package proto
@@ -23,6 +23,8 @@ const (
NezhaService_ReportSystemInfo_FullMethodName = "/proto.NezhaService/ReportSystemInfo"
NezhaService_ReportTask_FullMethodName = "/proto.NezhaService/ReportTask"
NezhaService_RequestTask_FullMethodName = "/proto.NezhaService/RequestTask"
NezhaService_IOStream_FullMethodName = "/proto.NezhaService/IOStream"
NezhaService_LookupGeoIP_FullMethodName = "/proto.NezhaService/LookupGeoIP"
)
// NezhaServiceClient is the client API for NezhaService service.
@@ -33,6 +35,8 @@ type NezhaServiceClient interface {
ReportSystemInfo(ctx context.Context, in *Host, opts ...grpc.CallOption) (*Receipt, error)
ReportTask(ctx context.Context, in *TaskResult, opts ...grpc.CallOption) (*Receipt, error)
RequestTask(ctx context.Context, in *Host, opts ...grpc.CallOption) (NezhaService_RequestTaskClient, error)
IOStream(ctx context.Context, opts ...grpc.CallOption) (NezhaService_IOStreamClient, error)
LookupGeoIP(ctx context.Context, in *GeoIP, opts ...grpc.CallOption) (*GeoIP, error)
}
type nezhaServiceClient struct {
@@ -102,6 +106,46 @@ func (x *nezhaServiceRequestTaskClient) Recv() (*Task, error) {
return m, nil
}
func (c *nezhaServiceClient) IOStream(ctx context.Context, opts ...grpc.CallOption) (NezhaService_IOStreamClient, error) {
stream, err := c.cc.NewStream(ctx, &NezhaService_ServiceDesc.Streams[1], NezhaService_IOStream_FullMethodName, opts...)
if err != nil {
return nil, err
}
x := &nezhaServiceIOStreamClient{stream}
return x, nil
}
type NezhaService_IOStreamClient interface {
Send(*IOStreamData) error
Recv() (*IOStreamData, error)
grpc.ClientStream
}
type nezhaServiceIOStreamClient struct {
grpc.ClientStream
}
func (x *nezhaServiceIOStreamClient) Send(m *IOStreamData) error {
return x.ClientStream.SendMsg(m)
}
func (x *nezhaServiceIOStreamClient) Recv() (*IOStreamData, error) {
m := new(IOStreamData)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *nezhaServiceClient) LookupGeoIP(ctx context.Context, in *GeoIP, opts ...grpc.CallOption) (*GeoIP, error) {
out := new(GeoIP)
err := c.cc.Invoke(ctx, NezhaService_LookupGeoIP_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// NezhaServiceServer is the server API for NezhaService service.
// All implementations should embed UnimplementedNezhaServiceServer
// for forward compatibility
@@ -110,6 +154,8 @@ type NezhaServiceServer interface {
ReportSystemInfo(context.Context, *Host) (*Receipt, error)
ReportTask(context.Context, *TaskResult) (*Receipt, error)
RequestTask(*Host, NezhaService_RequestTaskServer) error
IOStream(NezhaService_IOStreamServer) error
LookupGeoIP(context.Context, *GeoIP) (*GeoIP, error)
}
// UnimplementedNezhaServiceServer should be embedded to have forward compatible implementations.
@@ -128,6 +174,12 @@ func (UnimplementedNezhaServiceServer) ReportTask(context.Context, *TaskResult)
func (UnimplementedNezhaServiceServer) RequestTask(*Host, NezhaService_RequestTaskServer) error {
return status.Errorf(codes.Unimplemented, "method RequestTask not implemented")
}
func (UnimplementedNezhaServiceServer) IOStream(NezhaService_IOStreamServer) error {
return status.Errorf(codes.Unimplemented, "method IOStream not implemented")
}
func (UnimplementedNezhaServiceServer) LookupGeoIP(context.Context, *GeoIP) (*GeoIP, error) {
return nil, status.Errorf(codes.Unimplemented, "method LookupGeoIP not implemented")
}
// UnsafeNezhaServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to NezhaServiceServer will
@@ -215,6 +267,50 @@ func (x *nezhaServiceRequestTaskServer) Send(m *Task) error {
return x.ServerStream.SendMsg(m)
}
func _NezhaService_IOStream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(NezhaServiceServer).IOStream(&nezhaServiceIOStreamServer{stream})
}
type NezhaService_IOStreamServer interface {
Send(*IOStreamData) error
Recv() (*IOStreamData, error)
grpc.ServerStream
}
type nezhaServiceIOStreamServer struct {
grpc.ServerStream
}
func (x *nezhaServiceIOStreamServer) Send(m *IOStreamData) error {
return x.ServerStream.SendMsg(m)
}
func (x *nezhaServiceIOStreamServer) Recv() (*IOStreamData, error) {
m := new(IOStreamData)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func _NezhaService_LookupGeoIP_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GeoIP)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NezhaServiceServer).LookupGeoIP(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NezhaService_LookupGeoIP_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NezhaServiceServer).LookupGeoIP(ctx, req.(*GeoIP))
}
return interceptor(ctx, in, info, handler)
}
// NezhaService_ServiceDesc is the grpc.ServiceDesc for NezhaService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -234,6 +330,10 @@ var NezhaService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ReportTask",
Handler: _NezhaService_ReportTask_Handler,
},
{
MethodName: "LookupGeoIP",
Handler: _NezhaService_LookupGeoIP_Handler,
},
},
Streams: []grpc.StreamDesc{
{
@@ -241,6 +341,12 @@ var NezhaService_ServiceDesc = grpc.ServiceDesc{
Handler: _NezhaService_RequestTask_Handler,
ServerStreams: true,
},
{
StreamName: "IOStream",
Handler: _NezhaService_IOStream_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "proto/nezha.proto",
}
+39
View File
@@ -331,6 +331,9 @@ other = "Frontend Theme"
[CustomCodes]
other = "Custom Codes (Style and Script)"
[CustomCodesDashboard]
other = "Custom Codes for Dashboard"
[AccessPassword]
other = "Frontend Access Password"
@@ -648,3 +651,39 @@ other = "Disable Switch Template in Frontend"
[ServersOnWorldMap]
other = "Servers On World Map"
[NAT]
other = "NAT Traversal"
[LocalService]
other = "Local service"
[LocalServicePlaceholder]
other = "192.168.1.1:80 (with port)"
[BindHostname]
other = "Bind hostname"
[NetworkSpiterList]
other = "Network Monitor"
[Refresh]
other = "Refresh"
[CopyPath]
other = "Copy Path"
[Goto]
other = "Go to"
[GotoHeadline]
other = "Go to a Folder"
[GotoGo]
other = "Go"
[GotoClose]
other = "Cancel"
[FMError]
other = "Agent returned an error, please view the console for details. To open a new connection, reopen the FM again."
+40 -1
View File
@@ -331,6 +331,9 @@ other = "Tema de Frontend"
[CustomCodes]
other = "Códigos Personalizados (Incluye style y script)"
[CustomCodesDashboard]
other = "Custom Codes for Dashboard"
[AccessPassword]
other = "Contraseña de Acceso al Frontend"
@@ -647,4 +650,40 @@ other = "Temperatura"
other = "Deshabilitar Cambio de Plantilla en Frontend"
[ServersOnWorldMap]
other = "Servidores en el mapa mundial"
other = "Servidores en el mapa mundial"
[NAT]
other = "NAT traversal"
[LocalService]
other = "Servicio de red local"
[LocalServicePlaceholder]
other = "192.168.1.1:80 (con puerto)"
[BindHostname]
other = "Vincular nombre de host"
[NetworkSpiterList]
other = "Monitor de red"
[Refresh]
other = "Actualizar"
[CopyPath]
other = "Copiar ruta"
[Goto]
other = "Ir a"
[GotoHeadline]
other = "Ir a una carpeta"
[GotoGo]
other = "Ir"
[GotoClose]
other = "Cancelar"
[FMError]
other = "Agent devolvió un error, consulte la consola para obtener más detalles. Para abrir una nueva conexión, vuelva a abrir el FM."
+39
View File
@@ -331,6 +331,9 @@ other = "前台界面主题"
[CustomCodes]
other = "自定义代码(包括 style 和 script"
[CustomCodesDashboard]
other = "Custom Codes for Dashboard"
[AccessPassword]
other = "前台访问密码"
@@ -648,3 +651,39 @@ other = "禁止前台切换模板"
[ServersOnWorldMap]
other = "服务器世界分布图"
[NAT]
other = "内网穿透"
[LocalService]
other = "内网服务"
[LocalServicePlaceholder]
other = "192.168.1.1:80(带端口)"
[BindHostname]
other = "绑定域名"
[NetworkSpiterList]
other = "网络监控"
[Refresh]
other = "刷新"
[CopyPath]
other = "复制路径"
[Goto]
other = "跳往"
[GotoHeadline]
other = "跳往文件夹"
[GotoGo]
other = "确认"
[GotoClose]
other = "取消"
[FMError]
other = "Agent 返回了错误,请查看控制台获取详细信息。要建立新连接,请重新打开 FM。"
+57 -18
View File
@@ -50,7 +50,7 @@ other = "新增計劃任務"
other = "名稱"
[Scheduler]
other = "計劃"
other = "排程"
[BackUp]
other = "備份"
@@ -80,37 +80,37 @@ other = "特定伺服器"
other = "輸入ID/名稱以搜尋"
[NotificationMethodGroup]
other = "通知方式組"
other = "通知組"
[PushSuccessMessages]
other = "推送成功的息"
other = "推送成功的息"
[TaskType]
other = "任務類型"
[CronTask]
other = "計劃任務"
other = "排程任務"
[TriggerTask]
other = "觸發任務"
[TheFormaOfTheScheduleIs]
other = "計劃的格式為:"
other = "排程的格式為:"
[SecondsMinutesHoursDaysMonthsWeeksSeeDetails]
other = "秒 分 時 天 月 星期,詳情見"
[ScheduleExpressionFormat]
other = "計劃表達式格式"
other = "排程表達式格式"
[IntroductionOfCommands]
other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>&</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。"
other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>&</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。"
[AddMonitor]
other = "新增監控"
[Blog]
other = "博客"
other = "部落格"
[Target]
other = "目標"
@@ -158,7 +158,7 @@ other = "新增通知方式"
other = "分組"
[DoNotSendTestMessages]
other = "不發送測試息"
other = "不發送測試息"
[RequestMethod]
other = "請求方式"
@@ -221,7 +221,7 @@ other = "排序"
other = "越大越靠前"
[Secret]
other = "鑰"
other = "鑰"
[Note]
other = "備註"
@@ -254,10 +254,10 @@ other = "忽略所有"
other = "觸發執行"
[DeleteScheduledTask]
other = "刪除計劃任務"
other = "刪除排程任務"
[ConfirmToDeleteThisScheduledTask]
other = "確認刪除此計劃任務?"
other = "確認刪除此排程任務?"
[AccessDenied]
other = "訪問被拒絕"
@@ -331,6 +331,9 @@ other = "前台界面主題"
[CustomCodes]
other = "自定義代碼(包括 style 和 script"
[CustomCodesDashboard]
other = "Custom Codes for Dashboard"
[AccessPassword]
other = "前台訪問密碼"
@@ -404,7 +407,7 @@ other = "流量"
other = "負載"
[ProcessCount]
other = "程數"
other = "程數"
[ConnCount]
other = "連接數"
@@ -419,7 +422,7 @@ other = "活動"
other = "版本"
[NetSpeed]
other = "網"
other = "網"
[Uptime]
other = "在線"
@@ -455,7 +458,7 @@ other = "狀態"
other = "可用性"
[AverageLatency]
other = "平均應時間"
other = "平均應時間"
[CycleTransferStats]
other = "周期性流量統計"
@@ -521,7 +524,7 @@ other = "發生錯誤"
other = "系統錯誤"
[NetworkError]
other = "網錯誤"
other = "網錯誤"
[ServicesStatus]
other = "服務狀態"
@@ -533,7 +536,7 @@ other = "伺服器管理"
other = "服務監控"
[ScheduledTasks]
other = "計劃任務"
other = "排程任務"
[ApiManagement]
other = "API 管理"
@@ -611,7 +614,7 @@ other = "對遊客隱藏"
other = "菜單"
[NetworkSpiter]
other = "網"
other = "網"
[EnableShowInService]
other = "在服務中顯示"
@@ -648,3 +651,39 @@ other = "禁止前台切換主題"
[ServersOnWorldMap]
other = "伺服器世界分布圖"
[NAT]
other = "NAT穿透"
[LocalService]
other = "內網服務"
[LocalServicePlaceholder]
other = "192.168.1.1:80(帶埠號)"
[BindHostname]
other = "綁定網域"
[NetworkSpiterList]
other = "網路監控"
[Refresh]
other = "重新整理"
[CopyPath]
other = "複製路徑"
[Goto]
other = "跳至"
[GotoHeadline]
other = "跳至資料夾"
[GotoGo]
other = "確定"
[GotoClose]
other = "取消"
[FMError]
other = "Agent 回傳了錯誤,請查看主控台獲取詳細資訊。要建立新連線,請重新開啟 FM。"
+141
View File
@@ -0,0 +1,141 @@
html[nz-theme='dark'] body {
background-color: #121212 !important;
color: #fff !important;
}
html[nz-theme='dark'] .ui.menu {
background-color: #282828 !important;
}
html[nz-theme='dark'] .ui.menu * {
color: #fff !important;
}
html[nz-theme='dark'] .accordion {
background-color: #282828 !important;
}
html[nz-theme='dark'] .accordion .title {
color: #fff !important;
}
html[nz-theme='dark'] .ui.card {
background-color: #3f3f3f !important;
border: none !important;
box-shadow: none !important;
}
html[nz-theme='dark'] .header {
color: #fff !important;
}
html[nz-theme='dark'] .description {
color: #fff !important;
}
html[nz-theme='dark'] .icon {
color: #fff !important;
}
html[nz-theme='dark'] .ui.popup {
background-color: #575757 !important;
color: #fff !important;
}
html[nz-theme='dark'] .ui.table {
background-color: #282828 !important;
color: #fff !important;
}
html[nz-theme='dark'] .ui thead th {
background-color: #3f3f3f !important;
color: #fff !important;
}
html[nz-theme='dark'] .ui.buttons .button {
background-color: #3f3f3f !important;
color: #fff !important;
}
html[nz-theme='dark'] .ui.modal {
background-color: #282828 !important;
}
html[nz-theme='dark'] .ui.modal * {
color: #fff !important;
}
html[nz-theme='dark'] textarea,
html[nz-theme='dark'] input,
html[nz-theme='dark'] select,
html[nz-theme='dark'] .dropdown {
background-color: #3f3f3f !important;
color: #fff !important;
}
html[nz-theme='dark'] .ui.message {
background-color: unset !important;
}
html[nz-theme='dark'] .ui.dropdown .menu {
background-color: #575757 !important;
color: #fff !important;
}
html[nz-theme='dark'] .ui.modal>.header {
background-color: #3f3f3f !important;
}
html[nz-theme='dark'] .ui.modal>.content {
background-color: #282828 !important;
}
html[nz-theme='dark'] .ui.modal>.actions {
background-color: #3f3f3f !important;
}
html[nz-theme='dark'] #alert {
background-color: #3f3f3f !important;
color: #fff !important;
}
html[nz-theme='dark'] .ui.form .field>label {
color: unset !important;
}
html[nz-theme='dark'] .ui.segment {
background-color: #3f3f3f !important;
}
html[nz-theme='dark'] .ui.segment textarea,
html[nz-theme='dark'] input,
html[nz-theme='dark'] select,
html[nz-theme='dark'] .dropdown {
background-color: #575757 !important;
color: #fff !important;
}
html[nz-theme='dark'] form label {
color: #fff !important;
}
html[nz-theme='dark'] .ui.inverted.segment {
background-color: #121212 !important;
}
html[nz-theme='dark'] .ui.inverted.segment * {
color: #8b8b8b !important;
}
html[nz-theme='dark'] .menu .dropdown {
background-color: #282828 !important;
color: #fff !important;
}
html[nz-theme='dark'] .ui.menu .ui.dropdown .menu>.item {
color: #fff !important;
}
html[nz-theme='dark'] .login .ui.message{
color: #8b8b8b !important;
}
+70
View File
@@ -0,0 +1,70 @@
let receivedLength = 0;
let expectedLength = 0;
let root;
let draftHandle;
let accessHandle;
const Operation = Object.freeze({
WriteHeader: 1,
WriteChunks: 2,
DeleteFiles: 3
});
onmessage = async function (event) {
try {
const { operation, arrayBuffer, fileName } = event.data;
switch (operation) {
case Operation.WriteHeader: {
const dataView = new DataView(arrayBuffer);
expectedLength = Number(dataView.getBigUint64(4, false));
receivedLength = 0;
// Create a new temporary file
root = await navigator.storage.getDirectory();
draftHandle = await root.getFileHandle(fileName, { create: true });
accessHandle = await draftHandle.createSyncAccessHandle();
// Inform that file handle is created
const dataChunk = arrayBuffer.slice(12);
receivedLength += dataChunk.byteLength;
accessHandle.write(dataChunk, { at: 0 });
const progress = 'got handle';
postMessage({ type: 'progress', progress: progress });
break;
}
case Operation.WriteChunks: {
if (!accessHandle) {
throw new Error('accessHandle is undefined');
}
const dataChunk = arrayBuffer;
accessHandle.write(dataChunk, { at: receivedLength });
receivedLength += dataChunk.byteLength;
if (receivedLength === expectedLength) {
accessHandle.flush();
accessHandle.close();
const fileBlob = await draftHandle.getFile();
const blob = new Blob([fileBlob], { type: 'application/octet-stream' });
postMessage({ type: 'result', blob: blob, fileName: fileName });
}
break;
}
case Operation.DeleteFiles: {
for await (const [name, handle] of root.entries()) {
if (handle.kind === 'file') {
await root.removeEntry(name);
} else if (handle.kind === 'directory') {
await root.removeEntry(name, { recursive: true });
}
}
break;
}
}
} catch (error) {
postMessage({ error: error.message });
}
};
+5
View File
@@ -9,6 +9,11 @@ td {
word-break: break-all;
}
.ui.container{
width: 95vw !important;
max-width: 1680px !important;
}
.nb-container {
padding-top: 75px;
min-height: 100vh;
+23
View File
@@ -91,6 +91,7 @@ function showFormModal(modelSelector, formID, URL, getData) {
item.name.endsWith("_id") ||
item.name === "id" ||
item.name === "ID" ||
item.name === "ServerID" ||
item.name === "RequestType" ||
item.name === "RequestMethod" ||
item.name === "TriggerMode" ||
@@ -255,6 +256,28 @@ function addOrEditNotification(notification) {
);
}
function addOrEditNAT(nat) {
const modal = $(".nat.modal");
modal.children(".header").text((nat ? LANG.Edit : LANG.Add));
modal
.find(".nezha-primary-btn.button")
.html(
nat
? LANG.Edit + '<i class="edit icon"></i>'
: LANG.Add + '<i class="add icon"></i>'
);
modal.find("input[name=ID]").val(nat ? nat.ID : null);
modal.find("input[name=ServerID]").val(nat ? nat.ServerID : null);
modal.find("input[name=Name]").val(nat ? nat.Name : null);
modal.find("input[name=Host]").val(nat ? nat.Host : null);
modal.find("input[name=Domain]").val(nat ? nat.Domain : null);
showFormModal(
".nat.modal",
"#natForm",
"/api/nat"
);
}
function connectToServer(id) {
post('/terminal', { Host: window.location.host, Protocol: window.location.protocol, ID: id })
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+36
View File
@@ -0,0 +1,36 @@
{
"name": "Nezha",
"short_name": "Nezha",
"icons": [
{
"src": "/static/manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"start_url": "/",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#000000",
"lang": "eu-US",
"dir": "auto"
}
+36
View File
@@ -0,0 +1,36 @@
{
"name": "哪吒监控",
"short_name": "哪吒监控",
"icons": [
{
"src": "/static/manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"start_url": "/",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#000000",
"lang": "zh-CN",
"dir": "auto"
}
+55 -60
View File
@@ -1,46 +1,35 @@
/* 屏幕适配 */
@media only screen and (min-width:1200px) {
.ui.container {
width:95% !important;
font-size: 90% !important;
max-width: 1300px !important;
}
}
@media only screen and (max-width:767px) {
.ui.card>.content>.header:not(.ui),.ui.cards>.card>.content>.header:not(.ui) {
margin-top:0.4em !important;
.ui.card>.content>.header:not(.ui),
.ui.cards>.card>.content>.header:not(.ui) {
margin-top: 0.4em !important;
}
.ui.menu .item>img:not(.ui){
.ui.menu .item>img:not(.ui) {
width: 2.2rem;
}
.ui.menu .item:before{
width:0.5px;
.ui.menu .item:before {
width: 0.5px;
}
.ui.menu .item{
.ui.menu .item {
padding: 0.9rem 0.55rem;
}
.ui.large.menu{
.ui.large.menu {
font-size: 1rem;
}
}
i.icon {
color:#000;
width:1.2em !important;
}
i.fi {
width:0.9em;
margin:0px 6px 0px 2px;
color: #000;
width: 1.2em !important;
}
body {
content:" " !important;
background:fixed !important;
z-index:-1 !important;
top:0 !important;
right:0 !important;
bottom:0 !important;
left:0 !important;
i.fi {
width: 0.9em;
margin: 0px 6px 0px 2px;
}
td {
@@ -55,6 +44,11 @@ td {
margin-bottom: -47px;
}
.ui.container {
width: 95%;
max-width: 1680px !important;
}
#app .ui.fluid.accordion {
margin-bottom: 1rem;
}
@@ -65,79 +59,80 @@ td {
padding-top: unset;
}
.login.nb-container > .grid {
.login.nb-container>.grid {
width: 100%;
margin: 0 auto;
}
.login.nb-container > .grid .column {
.login.nb-container>.grid .column {
max-width: 450px;
}
.ui.menu .item-right:before{
width:0px;
.ui.menu .item-right:before {
width: 0px;
}
.status.cards .flag {
margin-right: 0 !important;
}
.status.cards .header > .info.icon {
.status.cards .header>.info.icon {
float: right;
margin-right: 0;
}
.ui.grid {
margin-bottom:-0.5em
margin-bottom: -0.5em
}
.ui.card>.content>.header:not(.ui), .ui.cards>.card>.content>.header:not(.ui){
.ui.card>.content>.header:not(.ui),
.ui.cards>.card>.content>.header:not(.ui) {
line-height: 1em;
}
.status.cards .wide.column {
padding-top: 0 !important;
padding-bottom: 0 !important;
height:2.3rem !important;
height: 2.3rem !important;
}
.status.cards .wide.column:nth-child(1) {
margin-top:1.2rem !important;
margin-top: 1.2rem !important;
}
.status.cards .wide.column:nth-child(2) {
margin-top:1.2rem !important;
margin-top: 1.2rem !important;
}
.status.cards .three.wide.column {
text-align: center;
width: 22%!important;
width: 22% !important;
}
.status.cards .thirteen.wide.column{
width: 78%!important;
padding-left:0;
.status.cards .thirteen.wide.column {
width: 78% !important;
padding-left: 0;
}
.status.cards .description {
padding-bottom:0 !important;
padding-bottom: 0 !important;
}
.status.cards .flag {
margin-right:0.5rem !important;
margin-right: 0.5rem !important;
}
.status.cards .header > .info.icon {
.status.cards .header>.info.icon {
float: right;
margin-right:0 !important;
margin-right: 0 !important;
}
.ui.popup:before {
display: none;
}
.closePopup{
color:rgb(10, 148, 242) !important;
.closePopup {
color: rgb(10, 148, 242) !important;
position: absolute;
top: 7px;
right: 10px;
@@ -150,26 +145,26 @@ td {
}
.ui.content {
margin:0 !important;
padding:1em !important;
margin: 0 !important;
padding: 1em !important;
}
.status.cards .ui.content.popup {
min-width:calc(100%)!important;
line-height:2rem !important;
border-radius:5px !important;
border:1px solid transparent !important;
font-family:Arial,Helvetica,sans-serif !important;
min-width: calc(100%) !important;
line-height: 2rem !important;
border-radius: 5px !important;
border: 1px solid transparent !important;
font-family: Arial, Helvetica, sans-serif !important;
}
.status.cards .outline.icon {
margin-right:1px !important;
margin-right: 1px !important;
}
.ui.progress .bar {
min-width:1.8em !important;
border-radius:5px !important;
line-height:1.65em !important;
min-width: 1.8em !important;
border-radius: 5px !important;
line-height: 1.65em !important;
text-align: right;
padding-right: 0.4em;
color: rgba(255, 255, 255, 0.7);
@@ -182,7 +177,7 @@ td {
align-items: center;
}
.service-status .delay-today > i {
.service-status .delay-today>i {
display: inline-block;
width: 1.2em;
height: 1.2em;
+11
View File
@@ -13,7 +13,18 @@ const mixinsVue = {
this.isMobile = this.checkIsMobile();
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
},
mounted() {
this.initDropdown();
},
methods: {
initDropdown() {
if(this.isMobile) $('.ui.dropdown').dropdown({
action: 'hide',
on: 'click',
duration: 100,
direction: 'direction'
});
},
toggleTemplate(template) {
if( template != this.preferredTemplate){
this.preferredTemplate = template;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+87 -81
View File
@@ -1,5 +1,6 @@
body[theme="dark"] {
background: #31363b;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
background-color: rgba(49, 54, 59, 1);
color: #f1f1f1;
}
@@ -7,14 +8,9 @@ body[theme="dark"] .navbar .navbar-brand {
color: #ffffff;
}
body[theme="dark"] .navbar .navbar-brand {
color: #ffffff;
}
body[theme="dark"] .navbar .dropdown-menu {
list-style-image: initial;
background-color: #171a1e;
border-color: #31363b;
background-color: rgba(23, 26, 30, 1);
border-color: rgba(49, 54, 59, 1);
box-shadow: rgba(0, 0, 0, 0.18) 0px 6px 12px;
}
@@ -24,31 +20,25 @@ body[theme="dark"] .navbar .dropdown-menu > li > a {
body[theme="dark"] .navbar .dropdown-menu > li > a:focus,
body[theme="dark"] .navbar .dropdown-menu > li > a:hover {
background-color: #25282a;
background-image: linear-gradient(#1c1d26 0, #1c1d26 100%);
background-color: rgba(37, 40, 42, 1);
background-image: linear-gradient(rgba(28, 29, 38, 1) 0, rgba(28, 29, 38, 1) 100%);
}
body[theme="dark"] .navbar .navbar-nav > .open > a:focus,
body[theme="dark"] .navbar .navbar-nav > .open > a:hover,
body[theme="dark"] .navbar .navbar-nav > .active > a,
body[theme="dark"] .navbar .navbar-nav > .open > a{
background-image: none !important;
box-shadow: none !important;
background-color: transparent !important;
background-image: none;
box-shadow: none;
background-color: transparent;
}
body[theme="dark"] .navbar .navbar-nav .open .dropdown-menu>li>a {
body[theme="dark"] .navbar .navbar-nav .open .dropdown-menu > li > a {
color: #f1f1f1;
}
body[theme="dark"] .toolbox i{
color: rgba(241,241,241,1);
background-color: rgba(0,0,0,0.5);
}
body[theme="dark"] .content {
background-image: initial;
background-color: #1c1d26;
background-color: rgba(28, 29, 38, 1);
border: none;
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
-webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
@@ -56,58 +46,10 @@ body[theme="dark"] .content {
}
body[theme="dark"] .table {
background-image: initial;
background-color: #1c1d26;
background-color: rgba(28, 29, 38, 1);
border-color: #545b5e;
}
body[theme="dark"] .table>thead>tr.node-group-tag>th {
background-color: #1c1d26;
}
body[theme="dark"] .table-striped tbody > tr.even > td,
body[theme="dark"] .table-striped tbody > tr.even > th {
background-color: #1c1d26;
}
body[theme="dark"] .table-striped tbody > tr.odd > td,
body[theme="dark"] .table-striped tbody > tr.odd > th {
background-color: #1c1d26;
}
body[theme="dark"] .progress {
background-image: linear-gradient(#2c2c2c 0,#1c1d26 100%);
background-color: #1c1d26;
}
body[theme="dark"] .progress-bar {
color: white;
}
body[theme="dark"] .progress-bar-success {
background-image: linear-gradient(#4d853a 0, #367e36 100%);
}
body[theme="dark"] .progress-offline .progress-bar-success {
background-image: linear-gradient(grey 0, grey 100%);
}
body[theme="dark"] .progress-bar-warning {
background-image: linear-gradient(#995f0d 0, #b5710f 100%);
}
body[theme="dark"] .progress-offline .progress-bar-warning {
background-image: linear-gradient(grey 0, grey 100%);
}
body[theme="dark"] .progress-bar-danger {
background-image: linear-gradient(#942320 0, #a12623 100%);
}
body[theme="dark"] .progress-offline .progress-bar-danger {
background-image: linear-gradient(grey 0, grey 100%);
}
body[theme="dark"] .table > tbody > tr > td:before,
body[theme="dark"] .table > tfoot > tr > td:before,
body[theme="dark"] .table > thead > tr > td:before,
@@ -115,25 +57,75 @@ body[theme="dark"] .table > thead > tr.node-group-cell > th:before{
background-color: rgba(58, 62, 65, 1);
}
body[theme="dark"] .table > tbody > tr.expandRow > td:before,
body[theme="dark"] .table > thead > tr.node-group-tag > th:before{
background-color: #1c1d26;
body[theme="dark"] .table-striped tbody > tr,
body[theme="dark"] .table-striped tbody > tr.even > td,
body[theme="dark"] .table-striped tbody > tr.even > th {
background-color: rgba(28, 29, 38, 1);
}
body[theme="dark"] .table-striped tbody > tr,
body[theme="dark"] .table-striped tbody > tr.odd > td,
body[theme="dark"] .table-striped tbody > tr.odd > th {
background-color: rgba(34, 35, 46, 1);
}
body[theme="dark"] .table-hover > tbody > tr:hover{
background-color: unset;
}
body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
background-color: #171a1e;
background-color: rgba(48, 50, 65, 1);
}
body[theme="dark"] .table-striped > tbody > tr:nth-of-type(2n+1) {
background-color: #1c1d26;
/* expandRow展开部分样式 */
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before{
background-color: rgba(34, 35, 46, 1);
}
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before{
background-color: rgba(28, 29, 38, 1);
}
/* expandRow展开部分样式结束 */
body[theme="dark"] .progress {
background-image: linear-gradient(#2c2c2c 0,rgba(28, 29, 38, 1) 100%);
background-color: rgba(28, 29, 38, 1);
}
body[theme="dark"] .progress-bar {
color: #ffffff;
}
body[theme="dark"] .progress-bar-success {
background-image: linear-gradient(to right, rgba(77, 133, 58, 1) 0, rgba(54, 126, 54, 1) 100%);
}
body[theme="dark"] .progress-bar-warning {
background-image: linear-gradient(to right, rgba(153, 95, 13, 1) 0, rgba(181, 113, 15, 1) 100%);
}
body[theme="dark"] .progress-bar-danger {
background-image: linear-gradient(to right, rgba(145, 35, 32, 1) 0, rgba(161, 38, 35, 1) 100%);
}
body[theme="dark"] .progress-offline .progress-bar-success {
background-image: linear-gradient(rgba(128, 128, 128, 1) 0, rgba(128, 128, 128, 1) 100%);
}
body[theme="dark"] .progress-offline .progress-bar-warning {
background-image: linear-gradient(rgba(128, 128, 128, 1) 0, rgba(128, 128, 128, 1) 100%);
}
body[theme="dark"] .progress-offline .progress-bar-danger {
background-image: linear-gradient(rgba(128, 128, 128, 1) 0, rgba(128, 128, 128, 1) 100%);
}
body[theme="dark"] .node-cell.status .status-icon.online {
background-image: linear-gradient(#4d853a 0, #367e36 100%);
background-image: linear-gradient(rgba(77, 133, 58, 1) 0, rgba(54, 126, 54, 1) 100%);
}
body[theme="dark"] .node-cell.status .status-icon.offline {
background-image: linear-gradient(#9b2522 0, #9b2522 100%);
background-image: linear-gradient(rgba(155, 37, 34, 1) 0, rgba(161, 38, 35, 1) 100%);
}
body[theme="dark"] .service-status .danger {
@@ -149,7 +141,7 @@ body[theme="dark"] .service-status .warning {
}
body[theme="dark"] .service-day-status-icon {
background-color: grey;
background-color: rgba(128, 128, 128, 1);
}
body[theme="dark"] footer p a,
@@ -159,7 +151,12 @@ body[theme="dark"] footer p a:hover {
}
body[theme="dark"] .modal-content{
background-color: #1c1d26;
background-color: rgba(28, 29, 38, 1);
color: #f1f1f1;
}
body[theme="dark"] #earthChartBox .modal-content{
background-color: rgba(0, 0, 0, 1);
color: #f1f1f1;
}
@@ -167,6 +164,15 @@ body[theme="dark"] .modal-header{
border-bottom-color:#080808;
}
body[theme="dark"] .modal-header i{
body[theme="dark"] .modal-header i.xclose{
color: #f1f1f1;
}
body[theme="dark"] .toolbox i{
color: rgba(241, 241, 241, 1);
background-color: rgba(0, 0, 0, 0.5);
}
body[theme="dark"] .network-box .network-box-header{
border-bottom: 1px solid rgba(110, 112, 121, 0.25);
}
+123
View File
@@ -0,0 +1,123 @@
body[theme="dark"]::before {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-image: url(/static/theme-server-status/img/bg.jpg);
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: cover;
z-index: -1;
}
body[theme="dark"] {
background-color: rgba(0, 0, 0, 0.8);
color: #f1f1f1;
}
body[theme="dark"] .navbar {
/** 顶部导航条 背景 **/
background-color: rgba(0, 0, 0, 0.8);
box-shadow: none;
border: none;
}
body[theme="dark"] .navbar .navbar-brand {
color: #ffffff;
}
body[theme="dark"] .navbar .dropdown-menu {
/** 二级导航下拉 背景 **/
background-color: rgba(0, 0, 0, 0.85);
border-top: none;
border-color: #31363b;
box-shadow: rgba(0, 0, 0, 0.18) 0px 6px 12px;
}
body[theme="dark"] .navbar .dropdown-menu > li > a {
color: #c8c3bc;
}
body[theme="dark"] .navbar .dropdown-menu > li > a:focus,
body[theme="dark"] .navbar .dropdown-menu > li > a:hover {
/** 二级导航鼠标悬停选中背景 **/
background-color: rgba(0, 0, 0, 0.95);
background-image: linear-gradient(#1c1d26 0, #1c1d26 100%);
}
body[theme="dark"] .navbar .navbar-nav .open .dropdown-menu > li > a {
color: #f1f1f1;
}
body[theme="dark"] .table,
body[theme="dark"] .table-condensed > tbody > tr,
body[theme="dark"] .table-hover > tbody > tr,
body[theme="dark"] .table-hover > tbody > tr:hover,
body[theme="dark"] .table-striped tbody > tr.even,
body[theme="dark"] .table-striped tbody > tr.odd,
body[theme="dark"] .table-striped tbody > tr.even > td,
body[theme="dark"] .table-striped tbody > tr.even > th,
body[theme="dark"] .table-striped tbody > tr.odd > td,
body[theme="dark"] .table-striped tbody > tr.odd > th,
body[theme="dark"] .table-striped tbody > tr.even > td:hover,
body[theme="dark"] .table-striped tbody > tr.even > th:hover,
body[theme="dark"] .table-striped tbody > tr.odd > td:hover,
body[theme="dark"] .table-striped tbody > tr.odd > th:hover,
body[theme="dark"] .table-striped tbody > tr.expandRow:hover {
background-color: transparent !important;
}
body[theme="dark"] .content {
/** 主box 背景 **/
background-color: rgba(28, 29, 38, 0.8);
border: none;
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
-webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
}
body[theme="dark"] .table > thead > tr.node-group-tag > th,
body[theme="dark"] .table > thead > tr.node-group-tag > th:before {
background: unset;
}
body[theme="dark"] .table > tbody > tr > td:before,
body[theme="dark"] .table > tfoot > tr > td:before,
body[theme="dark"] .table > thead > tr > td:before,
body[theme="dark"] .table > thead > tr.node-group-cell > th:before{
/** border-bottom 颜色 **/
background-color: rgba(155, 155, 155, 0.1);
}
body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
background-color: unset;
}
/* expandRow展开部分样式 */
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before{
background-color: unset;
}
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before{
background-color: unset;
}
/* expandRow展开部分样式结束 */
body[theme="dark"] .progress {
background-image: none;
background-color: rgba(255, 255, 255, 0.075);
}
body[theme="dark"] .progress-bar-success {
background-image: linear-gradient(to right, rgba(77, 133, 58, 0.9) 0, rgba(54, 126, 54, 0.9) 100%);
}
body[theme="dark"] .progress-bar-warning {
background-image: linear-gradient(to right, rgba(153, 95, 13, 0.9) 0, rgba(181, 113, 15, 0.9) 100%);
}
body[theme="dark"] .progress-bar-danger {
background-image: linear-gradient(to right, rgba(145, 35, 32, 0.8) 0, rgba(161, 38, 35, 0.8) 100%);
}
+108 -44
View File
File diff suppressed because one or more lines are too long
+134
View File
@@ -0,0 +1,134 @@
body[theme="light"]::before {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-image: url(/static/theme-server-status/img/bg.jpg);
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: cover;
z-index: -1;
}
body[theme="light"] {
background-image: unset;
}
body[theme="light"]::after {
content: "";
position: fixed;
}
body[theme="light"] .navbar {
/** 顶部导航条 背景 **/
background-color: rgba(255, 255, 255, 0.9);
border-bottom: 1px #cecece solid;
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
}
body[theme="light"] .navbar .dropdown-menu {
/** 二级导航下拉 **/
background-color: rgba(255, 255, 255, 0.8);
border-color: rgba(0, 0, 0, .05);
}
body[theme="light"] .navbar .dropdown-menu > li > a:focus,
body[theme="light"] .navbar .dropdown-menu > li > a:hover{
/** 二级导航鼠标悬停选中背景 **/
background-color: rgba(0, 0, 0, .05);
}
body[theme="light"] .navbar .navbar-collapse{
border-color:rgba(0, 0, 0, 0.085);
}
body[theme="light"] .navbar .navbar-toggle .icon-bar{
background-color: rgba(0, 0, 0, 0.9);
}
body[theme="light"] .navbar .navbar-toggle:focus,
body[theme="light"] .navbar .navbar-toggle:hover {
background-color: unset;
}
body[theme="light"] .table,
body[theme="light"] .table-condensed > tbody > tr,
body[theme="light"] .table-hover > tbody > tr,
body[theme="light"] .table-hover > tbody > tr:hover,
body[theme="light"] .table-striped tbody > tr.even,
body[theme="light"] .table-striped tbody > tr.odd,
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.odd > td,
body[theme="light"] .table-striped tbody > tr.odd > th,
body[theme="light"] .table-striped tbody > tr.even > td:hover,
body[theme="light"] .table-striped tbody > tr.even > th:hover,
body[theme="light"] .table-striped tbody > tr.odd > td:hover,
body[theme="light"] .table-striped tbody > tr.odd > th:hover,
body[theme="light"] .table-striped tbody > tr.expandRow:hover {
background-color: transparent !important;
}
body[theme="light"] .content {
/** 主box 背景 **/
background-color: rgba(245, 245, 245, 0.8);
border: none;
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
}
body[theme="light"] .table > thead > tr.node-group-tag > th,
body[theme="light"] .table > thead > tr.node-group-tag > th:before {
background: unset;
}
body[theme="light"] .table > tbody > tr > td:before,
body[theme="light"] .table > tfoot > tr > td:before,
body[theme="light"] .table > thead > tr > td:before,
body[theme="light"] .table > thead > tr.node-group-cell > th:before{
/** border-bottom 颜色 **/
background-color: rgba(0, 0, 0, 0.075);
}
body[theme="light"] .table-hover > tbody > tr:not(.expandRow):hover > td {
background-color: unset;
}
/* expandRow展开部分样式 */
body[theme="light"] .table > tbody > tr.expandRow.odd > td:before{
background-color: unset;
}
body[theme="light"] .table > tbody > tr.expandRow.even > td:before{
background-color: unset;
}
/* expandRow展开部分样式结束 */
body[theme="light"] .progress {
background-image: none;
background-color: rgba(0, 0, 0, 0.015);
}
body[theme="light"] .progress-bar-success {
background-image: linear-gradient(to right, rgba(50, 205, 50, 0.6) 0, rgba(68, 157, 68, 0.6) 100%);
}
body[theme="light"] .progress-bar-warning {
background-image: linear-gradient(to right, rgba(236, 151, 31, 0.6) 0, rgba(255, 125, 80, 0.6) 100%);
}
body[theme="light"] .progress-bar-danger {
background-image: linear-gradient(to right, rgba(255, 70, 0, 0.6) 0, rgba(201, 48, 44,0.6) 100%);
}
body[theme="light"] .toolbox i{
background-color: rgba(255, 255, 255, .55);
}
@media only screen and (max-width: 767px) {
body[theme="light"] .navbar .navbar-nav .open .dropdown-menu {
background-color: rgba(235, 235, 235, 0.75);
}
}
+205 -35
View File
@@ -3,16 +3,26 @@ body {
padding-bottom: 30px !important;
}
.container-fluid{
max-width: 1680px;
width: 95vw;
margin: 0 auto;
}
.container-fluid::-webkit-scrollbar {
display: none;
}
.nezha {
min-height: calc(100vh - 100px);
}
/* 导航部分 开始*/
.navbar {
min-height: 40px !important;
margin:0 auto;
font-size: 14px;
}
.navbar .container{
max-width: 95vw;
margin: 0 auto;
z-index:99999999;
}
.navbar-inverse{
@@ -46,8 +56,8 @@ body {
word-break: break-all;
}
.navbar .node-cell-expand-label {
/*margin-right: 5px;*/
.navbar .dropdown a{
cursor: pointer;
}
.navbar .dropdown .dropdown-toggle {
@@ -59,7 +69,7 @@ body {
margin:0px -15px;
}
.navbar .navbar-nav>li>a {
.navbar .navbar-nav > li > a {
color:#f1f1f1;
}
@@ -70,12 +80,10 @@ body {
.navbar .navbar-collapse{
max-height: 500px;
}
/* 导航部分 结束 */
/* toolbox 开始 */
.toolbox {
position: fixed;
bottom:20px;
@@ -116,7 +124,6 @@ body {
.toolbox .showGoTop i.goTop {
font-size: 1.55rem;
}
/* toolbox 结束 */
@@ -133,6 +140,10 @@ body {
border-radius: 3px;
}
.table-responsive{
border: none;
}
.table th, .table td {
text-align: left;
}
@@ -162,6 +173,15 @@ body {
height: 0.7px;
}
table.monitor-detail{
margin:5px 0px 10px 0px;
}
tr.accordion-toggle{
cursor:pointer;
}
.table .network-thead-tr th,
.table .node-group-tag th{
font-size: 18px;
padding-bottom:15px;
@@ -234,12 +254,22 @@ body {
cursor: pointer;
}
/*agent详情下拉速度*/
.collapsing{
-webkit-transition-property:height,visibility;
-o-transition-property:height,visibility;
transition-property:height,visibility;
-webkit-transition-duration:.15s;
-o-transition-duration:.15s;
transition-duration:.15s;
-webkit-transition-timing-function: linear;
-o-transition-timing-function: linear;
transition-timing-function: linear
}
/*正文结束*/
/* 服务页 正文*/
.service-status {
}
.service-status {}
.service-status .service-status-th{
min-width:60px;
@@ -285,61 +315,189 @@ body {
.service-status .tooltip-inner {
max-width: 500px;
}
td.ping-network-quality {
width: 600px;
}
/* 服务页 正文结束 */
/* 服务器分布图 */
#mapChartBox{
z-index: 999999999;
/* 网络页 正文*/
.network-box .btn-group.open .dropdown-toggle{
box-shadow:unset;
}
.modal-content{
.network-box .network-box-header{
font-size: 18px;
padding: 5px 0px 15px 5px;
cursor: pointer;
width: 100%;
}
.network-box .network-box-header .dropdown-menu {
max-height: 22.5em;
overflow-y: auto;
z-index:99999998;
min-width: 200px;
padding: 0px;
}
.network-box .network-box-header .dropdown-menu::-webkit-scrollbar {
display: none;
}
.network-box .network-box-header .dropdown-menu li{
height: 2em;
cursor: pointer;
}
.network-box .network-box-header .dropdown-menu li.input-group{
margin: 0 auto;
display: flex;
flex-direction: column;
pointer-events: auto;
position: sticky;
top: 0;
z-index: 99999997;
}
.network-box .network-box-header .dropdown-menu li.input-group input{
width: 100%;
height: 2em;
border-top: none;
border-left: none;
border-right: none;
}
.network-box .network-box-header .dropdown-menu li a {
padding: 5px 5px 5px 15px;
}
.network-box .network-box-header .dropdown-menu li.hidden {
height: 0;
display: none;
visibility:hidden;
}
.network-box .network-box-header .dropdown-menu li.visible {
display: block;
}
.network-box .chartTitle {
cursor: pointer;
text-align: center;
font-size: 18px;
margin: 18px 0px 15px 0px;
}
.network-box .chartTitle i.chartCountryCode{
font-size: 16px;
border-radius: 12.5%;
}
/* 网络页 正文结束 */
/* 地图版服务器分布图 */
#mapChartBox{
z-index: 999999999;
padding-left: 0 !important;
}
.modal-open .modal{
overflow-x: hidden;
overflow-y: hidden;
}
.modal-dialog-centered{
margin: 0 auto;
width: 1134px;
max-width: 1134px;
width: 100%;
max-width: 100%;
display: flex;
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 */
min-height: calc(100%) /* 确保模态框占据整个视口高度 */
}
.modal-content{
display: flex;
flex-direction: column;
pointer-events: auto;
border-radius: unset;
border: none;
}
.modal-header{
position: relative;
}
.modal-header i{
.modal-header h4{
cursor: pointer;
}
.modal-header i.xclose{
cursor: pointer;
position: absolute;
font-size: 2em;
top: 13px;
right: 13px;
font-size: 30px;
top: 12px;
right: 10px;
}
.modal-body{
padding: 0px;
}
a.template-item, a.cdn-item{
display: block;
}
footer{
padding-bottom: 20px;
}
footer p{
text-align: center;
font-size: 10px;
}
@media only screen and (max-width: 1200px) {
.accordian-body{
margin: 5px 0px 5px 10px;
}
table.monitor-detail{
margin:0px 0px 2px 0px;
}
.table .network-thead-tr th,
.table .node-group-tag th{
font-size:16px;
padding-bottom:6px;
}
}
/* 彩虹旗 */
.fi-rb {
background-image: url(/static/theme-server-status/img/rb.png);
}
@media only screen and (max-width: 767px) {
body {
font-size: 10px !important;
padding-top:60px !important;
}
.nezha {
min-height: calc(100vh - 90px);
min-height: calc(var(--vh, 1vh) * 100 - 90px);
}
#chartbox {
min-height: calc(100vh - 170px);
min-height: calc(var(--vh, 1vh) * 100 - 170px);
}
.content {
padding: 0;
margin-bottom: 10px;
}
footer{
padding-bottom: 10px;
}
.cycle-transfer-stats .dataTables_wrapper{
margin-bottom: -10px;
}
.navbar .navbar-nav .open .dropdown-menu>li>a {
color: #f1f1f1;
}
@@ -374,10 +532,17 @@ body {
.accordian-body{
margin: 5px 0px 5px 10px;
}
table.monitor-detail{
margin:0px 0px 2px 0px;
}
.table .network-thead-tr th,
.table .node-group-tag th{
font-size:16px;
font-size:14px;
padding-bottom:6px;
}
.table tr.network td{
font-size: 12px;
}
.service-status .service-status-th{
min-width:30px;
}
@@ -401,15 +566,20 @@ body {
.toolbox {
right: 18px;
}
.modal-dialog-centered{
width: 95%;
td.ping-network-quality {
width: 110px;
}
.modal-title{
line-height: 0.5;
.network-box .network-box-header {
margin: 8px 0px 0px 8px;
font-size: 16px;
}
.modal-header i{
top: 8px;
right: 8px;
.network-box .chartTitle {
font-size: 16px;
margin: 10px 0px 10px 0px;
}
.network-box .chartTitle i.chartCountryCode{
font-size: 15px;
border-radius: 12.5%;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

-326
View File
@@ -1,326 +0,0 @@
/**
* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
*/
;(function(window, document) {
/*jshint evil:true */
/** version */
var version = '3.7.3';
/** Preset options */
var options = window.html5 || {};
/** Used to skip problem elements */
var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i;
/** Not all elements can be cloned in IE **/
var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i;
/** Detect whether the browser supports default html5 styles */
var supportsHtml5Styles;
/** Name of the expando, to work with multiple documents or to re-shiv one document */
var expando = '_html5shiv';
/** The id for the documents expando */
var expanID = 0;
/** Cached data for each document */
var expandoData = {};
/** Detect whether the browser supports unknown elements */
var supportsUnknownElements;
(function() {
try {
var a = document.createElement('a');
a.innerHTML = '<xyz></xyz>';
//if the hidden property is implemented we can assume, that the browser supports basic HTML5 Styles
supportsHtml5Styles = ('hidden' in a);
supportsUnknownElements = a.childNodes.length == 1 || (function() {
// assign a false positive if unable to shiv
(document.createElement)('a');
var frag = document.createDocumentFragment();
return (
typeof frag.cloneNode == 'undefined' ||
typeof frag.createDocumentFragment == 'undefined' ||
typeof frag.createElement == 'undefined'
);
}());
} catch(e) {
// assign a false positive if detection fails => unable to shiv
supportsHtml5Styles = true;
supportsUnknownElements = true;
}
}());
/*--------------------------------------------------------------------------*/
/**
* Creates a style sheet with the given CSS text and adds it to the document.
* @private
* @param {Document} ownerDocument The document.
* @param {String} cssText The CSS text.
* @returns {StyleSheet} The style element.
*/
function addStyleSheet(ownerDocument, cssText) {
var p = ownerDocument.createElement('p'),
parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement;
p.innerHTML = 'x<style>' + cssText + '</style>';
return parent.insertBefore(p.lastChild, parent.firstChild);
}
/**
* Returns the value of `html5.elements` as an array.
* @private
* @returns {Array} An array of shived element node names.
*/
function getElements() {
var elements = html5.elements;
return typeof elements == 'string' ? elements.split(' ') : elements;
}
/**
* Extends the built-in list of html5 elements
* @memberOf html5
* @param {String|Array} newElements whitespace separated list or array of new element names to shiv
* @param {Document} ownerDocument The context document.
*/
function addElements(newElements, ownerDocument) {
var elements = html5.elements;
if(typeof elements != 'string'){
elements = elements.join(' ');
}
if(typeof newElements != 'string'){
newElements = newElements.join(' ');
}
html5.elements = elements +' '+ newElements;
shivDocument(ownerDocument);
}
/**
* Returns the data associated to the given document
* @private
* @param {Document} ownerDocument The document.
* @returns {Object} An object of data.
*/
function getExpandoData(ownerDocument) {
var data = expandoData[ownerDocument[expando]];
if (!data) {
data = {};
expanID++;
ownerDocument[expando] = expanID;
expandoData[expanID] = data;
}
return data;
}
/**
* returns a shived element for the given nodeName and document
* @memberOf html5
* @param {String} nodeName name of the element
* @param {Document|DocumentFragment} ownerDocument The context document.
* @returns {Object} The shived element.
*/
function createElement(nodeName, ownerDocument, data){
if (!ownerDocument) {
ownerDocument = document;
}
if(supportsUnknownElements){
return ownerDocument.createElement(nodeName);
}
if (!data) {
data = getExpandoData(ownerDocument);
}
var node;
if (data.cache[nodeName]) {
node = data.cache[nodeName].cloneNode();
} else if (saveClones.test(nodeName)) {
node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode();
} else {
node = data.createElem(nodeName);
}
// Avoid adding some elements to fragments in IE < 9 because
// * Attributes like `name` or `type` cannot be set/changed once an element
// is inserted into a document/fragment
// * Link elements with `src` attributes that are inaccessible, as with
// a 403 response, will cause the tab/window to crash
// * Script elements appended to fragments will execute when their `src`
// or `text` property is set
return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node;
}
/**
* returns a shived DocumentFragment for the given document
* @memberOf html5
* @param {Document} ownerDocument The context document.
* @returns {Object} The shived DocumentFragment.
*/
function createDocumentFragment(ownerDocument, data){
if (!ownerDocument) {
ownerDocument = document;
}
if(supportsUnknownElements){
return ownerDocument.createDocumentFragment();
}
data = data || getExpandoData(ownerDocument);
var clone = data.frag.cloneNode(),
i = 0,
elems = getElements(),
l = elems.length;
for(;i<l;i++){
clone.createElement(elems[i]);
}
return clone;
}
/**
* Shivs the `createElement` and `createDocumentFragment` methods of the document.
* @private
* @param {Document|DocumentFragment} ownerDocument The document.
* @param {Object} data of the document.
*/
function shivMethods(ownerDocument, data) {
if (!data.cache) {
data.cache = {};
data.createElem = ownerDocument.createElement;
data.createFrag = ownerDocument.createDocumentFragment;
data.frag = data.createFrag();
}
ownerDocument.createElement = function(nodeName) {
//abort shiv
if (!html5.shivMethods) {
return data.createElem(nodeName);
}
return createElement(nodeName, ownerDocument, data);
};
ownerDocument.createDocumentFragment = Function('h,f', 'return function(){' +
'var n=f.cloneNode(),c=n.createElement;' +
'h.shivMethods&&(' +
// unroll the `createElement` calls
getElements().join().replace(/[\w\-:]+/g, function(nodeName) {
data.createElem(nodeName);
data.frag.createElement(nodeName);
return 'c("' + nodeName + '")';
}) +
');return n}'
)(html5, data.frag);
}
/*--------------------------------------------------------------------------*/
/**
* Shivs the given document.
* @memberOf html5
* @param {Document} ownerDocument The document to shiv.
* @returns {Document} The shived document.
*/
function shivDocument(ownerDocument) {
if (!ownerDocument) {
ownerDocument = document;
}
var data = getExpandoData(ownerDocument);
if (html5.shivCSS && !supportsHtml5Styles && !data.hasCSS) {
data.hasCSS = !!addStyleSheet(ownerDocument,
// corrects block display not defined in IE6/7/8/9
'article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}' +
// adds styling not present in IE6/7/8/9
'mark{background:#FF0;color:#000}' +
// hides non-rendered elements
'template{display:none}'
);
}
if (!supportsUnknownElements) {
shivMethods(ownerDocument, data);
}
return ownerDocument;
}
/*--------------------------------------------------------------------------*/
/**
* The `html5` object is exposed so that more elements can be shived and
* existing shiving can be detected on iframes.
* @type Object
* @example
*
* // options can be changed before the script is included
* html5 = { 'elements': 'mark section', 'shivCSS': false, 'shivMethods': false };
*/
var html5 = {
/**
* An array or space separated string of node names of the elements to shiv.
* @memberOf html5
* @type Array|String
*/
'elements': options.elements || 'abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video',
/**
* current version of html5shiv
*/
'version': version,
/**
* A flag to indicate that the HTML5 style sheet should be inserted.
* @memberOf html5
* @type Boolean
*/
'shivCSS': (options.shivCSS !== false),
/**
* Is equal to true if a browser supports creating unknown/HTML5 elements
* @memberOf html5
* @type boolean
*/
'supportsUnknownElements': supportsUnknownElements,
/**
* A flag to indicate that the document's `createElement` and `createDocumentFragment`
* methods should be overwritten.
* @memberOf html5
* @type Boolean
*/
'shivMethods': (options.shivMethods !== false),
/**
* A string to describe the type of `html5` object ("default" or "default print").
* @memberOf html5
* @type String
*/
'type': 'default',
// shivs the document according to the specified `html5` object options
'shivDocument': shivDocument,
//creates a shived element
createElement: createElement,
//creates a shived documentFragment
createDocumentFragment: createDocumentFragment,
//extends list of elements
addElements: addElements
};
/*--------------------------------------------------------------------------*/
// expose html5
window.html5 = html5;
// shiv the document
shivDocument(document);
if(typeof module == 'object' && module.exports){
module.exports = html5;
}
}(typeof window !== "undefined" ? window : this, document));
+42 -3
View File
@@ -1,30 +1,49 @@
const mixinsVue = {
data: {
cache: [],
isMobile: false,
theme: "light",
isSystemTheme: false,
showGroup: false,
showGoTop: false,
showTools: false,
preferredTemplate: null,
isMobile: false,
semiTransparent: false,
staticUrl: '/static/theme-server-status',
adaptedTemplates: [
{ key: 'default', name: 'Default', icon: 'th large' },
{ key: 'angel-kanade', name: 'AngelKanade', icon: 'square' },
{ key: 'server-status', name: 'ServerStatus', icon: 'list' }
]
],
colors: [],
colorsDark: ['#4992FF', '#08C091', '#FDDD5F', '#FF6E76', '#58D9F9', '#7CFFB2', '#FF8A44', '#8D48E3', '#DD79FF', '#5470C6', '#3BA272', '#FAC758', '#EE6666', '#72C0DE', '#91CC76', '#FB8352', '#9A60B4', '#EA7BCC'],
colorsLight: ['#5470C6', '#3BA272', '#FAC758', '#EE6666', '#72C0DE', '#91CC76', '#FB8352', '#9A60B4', '#EA7BCC', '#4992FF', '#08C091', '#FDDD5F', '#FF6E76', '#58D9F9', '#7CFFB2', '#FF8A44', '#8D48E3', '#DD79FF'],
},
created() {
this.isMobile = this.checkIsMobile();
this.theme = this.initTheme();
this.showGroup = this.initShowGroup();
this.semiTransparent = this.initSemiTransparent();
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
this.colors = this.theme == "dark" ? this.colorsDark : this.colorsLight;
this.setBenchmarkHeight();
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.setBenchmarkHeight());
},
destroyed() {
window.removeEventListener('scroll', this.handleScroll);
},
methods: {
toggleTemplate(template) {
if( template != this.preferredTemplate){
this.preferredTemplate = template;
this.updateCookie("preferred_theme", template);
window.location.reload();
}
},
toggleShowTools() {
this.showTools = !this.showTools;
},
initTheme() {
const storedTheme = localStorage.getItem("theme");
const theme = (storedTheme === 'dark' || storedTheme === 'light') ? storedTheme : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
@@ -38,7 +57,7 @@ const mixinsVue = {
// 重新赋值全局调色
this.colors = this.theme == "dark" ? this.colorsDark : this.colorsLight;
if(this.$root.page == 'index') {
if(this.$root.page == 'index' || this.$root.page == 'network') {
this.reloadCharts(); // 重新载入echarts图表
}
},
@@ -57,6 +76,21 @@ const mixinsVue = {
this.$root.initTooltip();
}
},
initSemiTransparent() {
const storedSemiTransparent = localStorage.getItem("semiTransparent");
const semiTransparent = storedSemiTransparent !== null ? JSON.parse(storedSemiTransparent) : false;
if (storedSemiTransparent === null) {
localStorage.setItem("semiTransparent", semiTransparent);
}
return semiTransparent;
},
toggleSemiTransparent(){
this.semiTransparent = !this.semiTransparent;
localStorage.setItem("semiTransparent", this.semiTransparent);
if(this.$root.page == 'index' || this.$root.page == 'network') {
this.reloadCharts(); // 重新载入echarts图表
}
},
updateCookie(name, value) {
document.cookie = name + "=" + value +"; path=/";
},
@@ -99,6 +133,7 @@ const mixinsVue = {
},
handleScroll() {
this.showGoTop = window.scrollY >= 100;
if(this.showTools) this.showTools = false;
},
groupingData(data, field) {
let map = new Map();
@@ -119,6 +154,10 @@ const mixinsVue = {
},
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
return window.innerWidth <= 768;
},
setBenchmarkHeight() {
let vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
}
}
-5
View File
@@ -1,5 +0,0 @@
/*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl
* Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT
* */
!function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='&shy;<style media="'+a+'"> #mq-test-1 { width: 42px; }</style>',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b<s.length;b++){var c=s[b],e=c.href,f=c.media,g=c.rel&&"stylesheet"===c.rel.toLowerCase();e&&g&&!o[e]&&(c.styleSheet&&c.styleSheet.rawCssText?(v(c.styleSheet.rawCssText,e,f),o[e]=!0):(!/^([a-zA-Z:]*\/\/)/.test(e)&&!r||e.replace(RegExp.$1,"").split("/")[0]===a.location.host)&&("//"===e.substring(0,2)&&(e=a.location.protocol+e),d.push({href:e,media:f})))}w()};x(),c.update=x,c.getEmValue=t,a.addEventListener?a.addEventListener("resize",b,!1):a.attachEvent&&a.attachEvent("onresize",b)}}(this);
+4 -4
View File
@@ -6,11 +6,11 @@
{{.Version}}</small>
</div>
</div>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.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="https://unpkg.com/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://unpkg.com/semantic-ui@2.4.0/dist/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.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?v20240330"></script>
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
<script src="/static/main.js?v20240714"></script>
<script>
(function () {
updateLang({{.LANG }});
+10 -9
View File
@@ -1,21 +1,22 @@
{{define "common/header"}}
<!DOCTYPE html>
<html lang="{{.Conf.Language}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta content="telephone=no" name="format-detection">
<title>{{.Title}}</title>
<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 href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-logos/0.17/font-logos.min.css" type="text/css"
rel="stylesheet" />
<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="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link rel="stylesheet" type="text/css" href="https://unpkg.com/[email protected]/dist/semantic.min.css">
<link rel="stylesheet" type="text/css" href="https://unpkg.com/[email protected]/assets/font-logos.css">
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
<link rel="stylesheet" type="text/css" href="/static/main.css?v20240813">
<link rel="stylesheet" type="text/css" href="/static/darkmode.css?v20240813">
<script>
document.documentElement.setAttribute('nz-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
</script>
</head>
{{if ts .CustomCodeDashboard}} {{.CustomCodeDashboard|safe}} {{end}}
<body>
{{end}}
{{end}}
+1
View File
@@ -9,6 +9,7 @@
<a class='item{{if eq .MatchedPath "/monitor"}} active{{end}}' href="/monitor"><i class="rss icon"></i>{{tr "Services"}}</a>
<a class='item{{if eq .MatchedPath "/cron"}} active{{end}}' href="/cron"><i class="clock icon"></i>{{tr "Task"}}</a>
<a class='item{{if eq .MatchedPath "/notification"}} active{{end}}' href="/notification"><i class="bell icon"></i>{{tr "Notification"}}</a>
<a class='item{{if eq .MatchedPath "/nat"}} active{{end}}' href="/nat"><i class="exchange icon"></i>{{tr "NAT"}}</a>
<a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting">
<i class="settings icon"></i>{{tr "Settings"}}
</a>
+1 -1
View File
@@ -10,7 +10,7 @@
</div>
</form>
</div>
<div class=" actions">
<div class="actions">
<div class="ui negative button">{{tr "Cancel"}}</div>
<button class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
</button>
+1 -1
View File
@@ -60,7 +60,7 @@
</p>
</div>
</div>
<div class=" actions">
<div class="actions">
<div class="ui negative button">{{tr "Cancel"}}</div>
<button class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
</button>
+31
View File
@@ -0,0 +1,31 @@
{{define "component/nat"}}
<div class="ui tiny nat modal transition hidden">
<div class="header">Add</div>
<div class="content">
<form id="natForm" class="ui form">
<input type="hidden" name="ID">
<div class="field">
<label>{{tr "Name"}}</label>
<input type="text" name="Name">
</div>
<div class="field">
<label>Agent ID</label>
<input type="number" name="ServerID" placeholder="1">
</div>
<div class="field">
<label>{{tr "LocalService"}}</label>
<input type="text" name="Host" placeholder="{{tr "LocalServicePlaceholder"}}">
</div>
<div class="field">
<label>{{tr "BindHostname"}}</label>
<input type="text" name="Domain" placeholder="router.app.yourdomain.com">
</div>
</form>
</div>
<div class="actions">
<div class="ui negative button">{{tr "Cancel"}}</div>
<button class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
</button>
</div>
</div>
{{end}}
+1 -1
View File
@@ -71,7 +71,7 @@
</div>
</form>
</div>
<div class=" actions">
<div class="actions">
<div class="ui negative button">{{tr "Cancel"}}</div>
<button class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
</button>
+1 -1
View File
@@ -10,7 +10,7 @@
</button>
</div>
</div>
<table class="ui very basic table">
<table class="ui basic table">
<thead>
<tr>
<th>{{tr "Token"}}</th>
+1 -1
View File
@@ -10,7 +10,7 @@
</button>
</div>
</div>
<table class="ui very basic table">
<table class="ui basic table">
<thead>
<tr>
<th>ID</th>
+495
View File
@@ -0,0 +1,495 @@
{{define "dashboard-default/file"}}
<!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.0">
<title>File List</title>
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://unpkg.com/mdui@2/mdui.css" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<script src="https://unpkg.com/mdui@2/mdui.global.js"></script>
</head>
<style>
body {
font-family: 'Roboto', sans-serif;
}
.file-list {
list-style-type: none;
padding: 0;
}
.file-item {
margin: 5px 0;
border: 1px solid #ccc;
border-radius: 5px;
overflow: hidden;
text-overflow: ellipsis;
display: block;
width: 100%;
box-sizing: border-box;
}
#top-app-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
}
#current-directory {
font-size: 1rem;
font-weight: normal;
margin: 0 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - 100px);
box-sizing: border-box;
}
</style>
<body>
<div id="top-app-bar">
<mdui-dropdown>
<mdui-button-icon slot="trigger" icon="menu"></mdui-button-icon>
<mdui-menu>
<mdui-menu-item id="refresh">{{tr "Refresh"}}</mdui-menu-item>
<mdui-menu-item id="copy">{{tr "CopyPath"}}</mdui-menu-item>
<mdui-menu-item id="goto">{{tr "Goto"}}</mdui-menu-item>
</mdui-menu>
</mdui-dropdown>
<span id="current-directory"></span>
<mdui-button-icon id="upload" icon="upload"></mdui-button-icon>
</div>
<mdui-list id="file-list" class="file-list"></mdui-list>
<mdui-dialog id="error-dialog" headline="Error"
description="{{tr "FMError"}}"></mdui-dialog>
<mdui-dialog id="upd-modal" class="modal">
<mdui-linear-progress id="upd-progress"></mdui-linear-progress>
</mdui-dialog>
<mdui-dialog id="goto-dialog" headline="{{tr "GotoHeadline"}}" close-on-overlay-click>
<mdui-text-field id="goto-text" variant="outlined" value=""></mdui-text-field>
<mdui-button id="goto-go" slot="action" variant="text">{{tr "GotoGo"}}</mdui-button>
<mdui-button id="goto-close" slot="action" variant="tonal">{{tr "GotoClose"}}</mdui-button>
</mdui-dialog>
<script>
let currentPath = '/opt/nezha/';
let fileName = '';
let receivedBuffer = []; // 用于缓存数据块
let expectedLength = 0;
let receivedLength = 0;
let isFirstChunk = true;
let isUpCompleted = false;
let handleReady = false;
let worker;
function updateDirectoryTitle() {
const directoryTitle = document.getElementById('current-directory');
directoryTitle.textContent = `${currentPath}`;
}
function updateFileList(items) {
const fileListElement = document.getElementById('file-list');
fileListElement.innerHTML = '';
if (currentPath !== '/') {
const upItem = document.createElement('mdui-list-item');
upItem.className = 'file-item up-directory';
upItem.setAttribute('icon', 'arrow_back');
upItem.textContent = "..";
upItem.onclick = function () {
const lastSlashIndex = currentPath.lastIndexOf('/', currentPath.length - 2);
currentPath = currentPath.substring(0, lastSlashIndex + 1) || '/';
listFile();
};
fileListElement.appendChild(upItem);
}
items.sort((a, b) => {
if (a.fileType === 'dir' && b.fileType !== 'dir') {
return -1;
}
if (a.fileType !== 'dir' && b.fileType === 'dir') {
return 1;
}
return a.name.localeCompare(b.name);
});
items.forEach(item => {
const listItem = document.createElement('mdui-list-item');
listItem.className = `file-item ${item.fileType.toLowerCase()}`;
listItem.setAttribute('nonclickable', 'true');
listItem.setAttribute('icon', 'insert_drive_file');
listItem.textContent = `${item.name}`;
if (item.fileType === 'dir') {
listItem.setAttribute('nonclickable', 'false');
listItem.setAttribute('icon', 'folder');
listItem.style.cursor = 'pointer';
listItem.onclick = function () {
currentPath += `${item.name}/`;
listFile();
};
} else {
const downloadButton = document.createElement('mdui-button-icon');
downloadButton.setAttribute('slot', 'end-icon');
downloadButton.setAttribute('icon', 'download');
downloadButton.onclick = function () {
const filePath = currentPath + item.name;
fileName = item.name;
downloadFile(filePath);
};
listItem.appendChild(downloadButton);
}
fileListElement.appendChild(listItem);
});
updateDirectoryTitle();
}
function resetUpdState() {
receivedBuffer = [];
expectedLength = 0;
receivedLength = 0;
isFirstChunk = true;
}
function downloadFile(filePath) {
showUpdModal('d');
const prefix = new Int8Array([1]); // Request download
const filePathMessage = new TextEncoder().encode(filePath);
const msg = new Int8Array(prefix.length + filePathMessage.length);
msg.set(prefix);
msg.set(filePathMessage, prefix.length);
socket.send(msg);
}
function listFile() {
const prefix = new Int8Array([0]);
const resizeMessage = new TextEncoder().encode(currentPath);
const msg = new Int8Array(prefix.length + resizeMessage.length);
msg.set(prefix);
msg.set(resizeMessage, prefix.length);
socket.send(msg);
}
async function uploadFile(file) {
showUpdModal('u');
const chunkSize = 1048576; // 1MB chunk
let offset = 0;
const filePath = `${currentPath}${file.name}`;
const fileSize = file.size;
const messageType = 2;
// Build header (type + file size + path)
const filePathBytes = new TextEncoder().encode(filePath);
const header = new ArrayBuffer(1 + 8 + filePathBytes.length);
const headerView = new DataView(header);
headerView.setUint8(0, messageType);
headerView.setBigUint64(1, BigInt(fileSize), false);
new Uint8Array(header, 9).set(filePathBytes);
// Send header
socket.send(header);
// Send data chunks
while (offset < fileSize) {
const chunk = file.slice(offset, offset + chunkSize);
const arrayBuffer = await readFileAsArrayBuffer(chunk);
socket.send(arrayBuffer);
offset += chunkSize;
}
const checkCompletion = setInterval(() => {
if (isUpCompleted) {
clearInterval(checkCompletion);
hideUpdModal();
resetUpdState();
listFile();
}
}, 100);
}
async function parseFileList(arrayBuffer) {
const dataView = new DataView(arrayBuffer);
const items = [];
let offset = 4;
const pathLength = dataView.getUint32(offset);
offset += 4;
const pathArray = new Uint8Array(arrayBuffer, offset, pathLength);
currentPath = new TextDecoder('utf-8').decode(pathArray);
offset += pathLength;
while (offset < dataView.byteLength) {
const fileType = dataView.getUint8(offset);
offset += 1;
const nameLength = dataView.getUint8(offset);
offset += 1;
if (offset + nameLength > dataView.byteLength) {
console.error('Error: Name length exceeds buffer size');
break;
}
const nameArray = new Uint8Array(arrayBuffer, offset, nameLength);
const name = new TextDecoder('utf-8').decode(nameArray);
offset += nameLength;
items.push({
fileType: fileType === 0x01 ? 'dir' : 'f',
name: name,
});
}
return { items };
}
function readFileAsArrayBuffer(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(blob);
});
}
function concatenateArrayBuffers(buffers) {
let totalLength = 0;
buffers.forEach(buf => totalLength += buf.byteLength);
const result = new Uint8Array(totalLength);
let offset = 0;
buffers.forEach(buf => {
result.set(new Uint8Array(buf), offset);
offset += buf.byteLength;
});
return result.buffer;
}
function arraysEqual(a, b) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
async function copyTextToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error('Failed to copy text to clipboard: ', err);
}
}
async function handleError(errMsg) {
try {
console.error('Received error: ', errMsg);
hideUpdModal();
const errorDialog = document.getElementById('error-dialog');
errorDialog.open = true;
if (socket.readyState === WebSocket.OPEN) {
socket.close(1000, 'Closing due to error');
}
} catch (error) {
console.error('Error while handling error and closing WebSocket:', error);
}
}
function showUpdModal(operation) {
const modal = document.getElementById('upd-modal');
modal.open = true;
if (operation === 'd') {
modal.setAttribute('headline', 'Downloading...');
} else if (operation === 'u') {
modal.setAttribute('headline', 'Uploading...');
}
}
function hideUpdModal() {
const modal = document.getElementById('upd-modal');
modal.open = false;
}
function waitForHandleReady() {
return new Promise(resolve => {
const checkReady = () => {
if (handleReady) {
resolve();
} else {
setTimeout(checkReady, 10);
}
};
checkReady();
});
}
const socket = new WebSocket((window.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/file/' + '{{.SessionID}}');
socket.binaryType = 'arraybuffer';
socket.onmessage = async function (event) {
try {
const arrayBuffer = event.data;
if (isFirstChunk) {
const identifier = new Uint8Array(arrayBuffer, 0, 4);
const fileIdentifier = new Uint8Array([0x4E, 0x5A, 0x54, 0x44]); // NZTD
const fileNameIdentifier = new Uint8Array([0x4E, 0x5A, 0x46, 0x4E]); // NZFN
const errorIdentifier = new Uint8Array([0x4E, 0x45, 0x52, 0x52]); // NERR
const completeIdentifier = new Uint8Array([0x4E, 0x5A, 0x55, 0x50]); // NZUP
if (arraysEqual(identifier, fileIdentifier)) {
worker = new Worker('/static/file.js');
worker.onmessage = async function (event) {
switch (event.data.type) {
case 'error':
console.error('Error from worker:', event.data.error);
break;
case 'progress':
handleReady = true;
break;
case 'result':
handleReady = false;
const url = URL.createObjectURL(event.data.blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = event.data.fileName;
anchor.click();
URL.revokeObjectURL(url);
// Delete the file in OPFS
window.addEventListener('beforeunload', async () => {
await worker.postMessage({ operation: 3, arrayBuffer: null, fileName: event.data.fileName });
});
hideUpdModal();
resetUpdState();
break;
}
};
await worker.postMessage({ operation: 1, arrayBuffer: arrayBuffer, fileName: fileName });
isFirstChunk = false;
} else if (arraysEqual(identifier, fileNameIdentifier)) {
// List files
const { items } = await parseFileList(arrayBuffer);
updateFileList(items);
return;
} else if (arraysEqual(identifier, errorIdentifier)) {
// Handle error
const errBytes = arrayBuffer.slice(4);
const errMsg = new TextDecoder('utf-8').decode(errBytes);
await handleError(errMsg);
return;
} else if (arraysEqual(identifier, completeIdentifier)) {
// Upload is completed
isUpCompleted = true;
return;
} else {
console.log('Unknown identifier');
return;
}
} else {
await waitForHandleReady();
await worker.postMessage({ operation: 2, arrayBuffer: arrayBuffer, fileName: fileName });
}
} catch (error) {
console.error('Error processing received data:', error);
}
};
socket.onopen = function (event) {
listFile();
};
socket.onerror = function (event) {
console.error('WebSocket error:', event);
};
socket.onclose = function (event) {
console.log('WebSocket connection closed:', event);
};
document.getElementById('refresh').addEventListener('click', listFile);
document.getElementById('copy').addEventListener('click', async () => {
await copyTextToClipboard(currentPath);
});
document.getElementById('goto').addEventListener('click', function () {
const dialog = document.getElementById('goto-dialog');
const textField = document.getElementById('goto-text');
const goButton = document.getElementById('goto-go');
const closeButton = document.getElementById('goto-close');
dialog.open = true;
// Ensure the path ends with a separator
const updateText = function (event) {
let text = event.target.value;
if (!text.endsWith('/')) {
text += '/';
}
return text;
};
const handleGoClick = function () {
let text = updateText({ target: textField });
currentPath = text;
listFile();
dialog.open = false;
};
textField.removeEventListener('change', updateText);
textField.addEventListener('change', updateText);
goButton.removeEventListener('click', handleGoClick);
goButton.addEventListener('click', handleGoClick);
closeButton.addEventListener("click", () => dialog.open = false);
});
document.getElementById('upload').addEventListener('click', async function () {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.style.display = 'none';
fileInput.addEventListener('change', async function (event) {
const file = event.target.files[0];
if (file) {
await uploadFile(file);
isUpCompleted = false;
}
});
document.body.appendChild(fileInput);
fileInput.click();
document.body.removeChild(fileInput);
});
</script>
</body>
</html>
{{end}}
+1 -1
View File
@@ -9,7 +9,7 @@
</button>
</div>
</div>
<table class="ui very basic table">
<table class="ui basic table">
<thead>
<tr>
<th>ID</th>
+54
View File
@@ -0,0 +1,54 @@
{{define "dashboard-default/nat"}}
{{template "common/header" .}}
{{template "common/menu" .}}
<div class="nb-container">
<div class="ui container">
<div class="ui grid">
<div class="right floated right aligned twelve wide column">
<button class="ui right labeled nezha-primary-btn icon button" onclick="addOrEditNAT()"><i
class="add icon"></i> Add
</button>
</div>
</div>
<table class="ui basic table">
<thead>
<tr>
<th>ID</th>
<th>{{tr "Name"}}</th>
<th>Agent ID</th>
<th>{{tr "LocalService"}}</th>
<th>{{tr "BindHostname"}}</th>
<th>{{tr "Administration"}}</th>
</tr>
</thead>
<tbody>
{{range $item := .NAT}}
<tr>
<td>{{$item.ID}}</td>
<td>{{$item.Name}}</td>
<td>{{$item.ServerID}}</td>
<td>{{$item.Host}}</td>
<td>{{$item.Domain}}</td>
<td>
<div class="ui mini icon buttons">
<button class="ui button" onclick="addOrEditNAT({{$item}})">
<i class="edit icon"></i>
</button>
<button class="ui button"
onclick="showConfirm('确定删除NAT隧道?','确认删除',deleteRequest,'/api/nat/'+{{$item.ID}})">
<i class="trash alternate outline icon"></i>
</button>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{template "component/nat"}}
{{template "common/footer" .}}
<script>
$('.checkbox').checkbox()
</script>
{{end}}
+2 -2
View File
@@ -10,7 +10,7 @@
</button>
</div>
</div>
<table class="ui very basic table">
<table class="ui basic table">
<thead>
<tr>
<th>ID</th>
@@ -52,7 +52,7 @@
</button>
</div>
</div>
<table class="ui very basic table">
<table class="ui basic table">
<thead>
<tr>
<th>ID</th>
-3
View File
@@ -1,18 +1,15 @@
{{define "dashboard-default/redirect"}}
<!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.0">
<title>Redirecting..</title>
</head>
<body>
<p>If you are not redirected, please click <a href="{{.URL}}">here</a>.</p>
<script>window.location.href = "{{.URL}}"</script>
</body>
</html>
{{end}}
+3 -3
View File
@@ -19,7 +19,7 @@
</button>
</div>
</div>
<table class="ui very basic table">
<table class="ui basic table">
<thead>
<tr>
<th><button onclick="checkAllServer()" class="ui mini nezha-primary-btn button">{{tr "SelectAll"}}</button></th>
@@ -60,7 +60,7 @@
</td>
<td>
<button class="ui icon green mini button"
data-clipboard-text="{{if $.Conf.GRPCHost}}{{if eq $.Conf.Language "zh-CN"}}curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install.sh{{else}}curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install_en.sh{{end}} -o nezha.sh && chmod +x nezha.sh && sudo ./nezha.sh install_agent {{$.Conf.GRPCHost}} {{if $.Conf.ProxyGRPCPort}}{{$.Conf.ProxyGRPCPort}}{{else}}{{$.Conf.GRPCPort}}{{end}} {{$server.Secret}}{{if $.Conf.TLS}} --tls{{end}}{{else}}{{tr "NoDomainAlert"}}{{end}}"
data-clipboard-text="{{if $.Conf.GRPCHost}}{{if eq $.Conf.Language "zh-CN"}}curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install.sh{{else}}curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install_en.sh{{end}} -o nezha.sh && chmod +x nezha.sh && ./nezha.sh install_agent {{$.Conf.GRPCHost}} {{if $.Conf.ProxyGRPCPort}}{{$.Conf.ProxyGRPCPort}}{{else}}{{$.Conf.GRPCPort}}{{end}} {{$server.Secret}}{{if $.Conf.TLS}} --tls{{end}}{{else}}{{tr "NoDomainAlert"}}{{end}}"
data-tooltip="{{tr "ClickToCopy"}}">
<i class="linux icon"></i>
</button>
@@ -98,7 +98,7 @@
</div>
{{template "component/server" .}}
{{template "common/footer" .}}
<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 src="https://unpkg.com/clipboard@2.0.11/dist/clipboard.min.js"></script>
<script>
var clipboard = new ClipboardJS('.ui.icon.green.mini.button');
const checkBoxList = document.querySelectorAll('tbody > tr > td > input.nezha-servers[type=checkbox]')
+7 -3
View File
@@ -41,6 +41,10 @@
<label>{{tr "CustomCodes"}}</label>
<textarea name="CustomCode">{{.Conf.Site.CustomCode}}</textarea>
</div>
<div class="field">
<label>{{tr "CustomCodesDashboard"}}</label>
<textarea name="CustomCodeDashboard">{{.Conf.Site.CustomCodeDashboard}}</textarea>
</div>
<div class="field">
<label>{{tr "AccessPassword"}}</label>
<input type="text" name="ViewPassword" placeholder="" value="{{.Conf.Site.ViewPassword}}">
@@ -128,13 +132,13 @@
})
$('.checkbox').checkbox()
$('#settingForm').find("select[name=Cover]").val({{.Conf.Cover }});
{{if .Conf.EnableIPChangeNotification}}
{{ if .Conf.EnableIPChangeNotification}}
$('.checkbox.ip-change').checkbox('set checked')
{{ end }}
{{if .Conf.EnablePlainIPInNotification}}
{{ if .Conf.EnablePlainIPInNotification}}
$('.checkbox.plain-ip').checkbox('set checked')
{{ end }}
{{if .Conf.DisableSwitchTemplateInFrontend }}
{{ if .Conf.DisableSwitchTemplateInFrontend }}
$('.checkbox.disable-switch-template').checkbox('set checked')
{{ end }}
</script>
+64 -6
View File
@@ -8,9 +8,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>tty@{{.ServerName}} - {{.Title}}</title>
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/xterm.css" type="text/css" rel="stylesheet"/>
<link type="text/css" rel="stylesheet" href="https://unpkg.com/[email protected]/css/xterm.css" />
</head>
<style>
html,
body,
@@ -24,18 +23,53 @@
body {
background-color: black;
}
#file-list-iframe {
position: absolute;
top: 0;
right: 0;
width: 30%;
height: 100%;
border: none;
background-color: white;
display: none;
z-index: 10;
}
#folder-button {
position: absolute;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
background-color: #007bff;
color: white;
border: none;
border-radius: 25px;
font-size: 24px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
z-index: 20;
}
</style>
<body onresize="onResize()">
<div id="terminal-container"></div>
<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://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/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/fit/xterm-addon-fit.js"></script>
<iframe id="file-list-iframe" src=""></iframe>
<button id="folder-button">📁</button>
<script src="https://unpkg.com/[email protected]/lib/xterm.js"></script>
<script src="https://unpkg.com/@xterm/[email protected]/lib/addon-fit.js"></script>
<script src="https://unpkg.com/@xterm/[email protected]/lib/addon-web-links.js"></script>
<script src="https://unpkg.com/@xterm/[email protected]/lib/addon-attach.js"></script>
<script>
let sendResizing = false;
function doResize() {
fitAddon.fit()
const w = fitAddon.proposeDimensions();
const prefix = new Int8Array([1]);
const resizeMessage = new TextEncoder().encode(JSON.stringify({
@@ -73,10 +107,13 @@
cursorBlink: true,
});
const socket = new WebSocket((window.location.protocol == 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/terminal/' + '{{.SessionID}}');
const attachAddon = new AttachAddon.AttachAddon(socket);
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(attachAddon);
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('terminal-container'));
socket.onopen = () => {
@@ -91,6 +128,27 @@
socket.onerror = () => {
alert('{{tr "TerminalConnectionFailed"}}')
}
// 处理文件夹按钮点击事件
const folderButton = document.getElementById('folder-button');
const fileListIframe = document.getElementById('file-list-iframe');
let fileListVisible = false;
folderButton.addEventListener('click', () => {
if (!fileListVisible) {
// 显示文件列表
const params = new URLSearchParams({
id: "{{.ServerID}}"
}).toString();
fileListIframe.src = `/file?${params}`;
fileListIframe.style.display = 'block';
fileListVisible = true;
} else {
// 隐藏文件列表
fileListIframe.style.display = 'none';
fileListVisible = false;
}
});
</script>
</body>
+17 -16
View File
@@ -25,7 +25,7 @@
v-if="server.Host.Virtualization">@#server.Host.Virtualization#@:</span>@#server.Host.Arch#@]<br />
CPU: @#server.Host.CPU#@<br />
<template v-if="server.Host.GPU">
GPU: @#server.Host.GPU#@
GPU: @#server.Host.GPU#@<br />
</template>
{{tr "DiskUsed"}}:
@#formatByteSize(server.State.DiskUsed)#@/@#formatByteSize(server.Host.DiskTotal)#@<br />
@@ -261,22 +261,23 @@
const x = readableBytes(bs)
return x != "NaN undefined" ? x : '0B'
},
getCoreAndGHz(str){
if((str || []).hasOwnProperty(0) === false){
return '';
getCoreAndGHz(arr) {
if ((arr || []).length === 0) {
return '';
}
str = str[0];
let GHz = str.match(/(\d|\.)+GHz/g);
let Core = str.match(/(\d|\.)+ Physical/g);
GHz = GHz!==null?GHz.hasOwnProperty(0)===false?'':GHz[0]:''
Core = Core!==null?Core.hasOwnProperty(0)===false?'?':Core[0]:'?'
if(Core === '?'){
let Core = str.match(/(\d|\.)+ Virtual/g);
Core = Core!==null?Core.hasOwnProperty(0)===false?'?':Core[0]:'?'
return Core.replace('Virtual','Core')
}
return Core.replace('Physical','Core');
let totalCores = 0;
arr.forEach(str => {
let coreMatch = str.match(/(\d+(\.\d+)?) Physical/g);
let coreCount = 0;
if (coreMatch) {
coreCount = parseFloat(coreMatch[0]);
} else {
let coreMatch = str.match(/(\d+(\.\d+)?) Virtual/g);
coreCount = coreMatch ? parseFloat(coreMatch[0]) : 0;
}
totalCores += coreCount;
});
return `${totalCores} Cores`;
},
getByteToGB(bs){
return (bs/1024/1024/1024).toFixed(2) + 'GB'
+2 -4
View File
@@ -10,14 +10,12 @@
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
<script>
function showSwitchTemplate(list, currentBackendTheme) {
console.log(list, currentBackendTheme);
console.log("currentBackendTheme:",currentBackendTheme);
// console.log(list, currentBackendTheme);
// console.log("currentBackendTheme:",currentBackendTheme);
}
showSwitchTemplate({{ .Themes }}, {{ .Conf.Site.Theme }})
</script>
{{ end }}
<script>
</script>
</body>
</html>
{{end}}
+22 -8
View File
@@ -1,26 +1,40 @@
{{define "theme-default/header"}}
<!DOCTYPE html>
<html lang="{{.Conf.Language}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Title }}</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta content="telephone=no" name="format-detection">
<title>{{.Title}}</title>
<link rel="shortcut icon" type="image/png" href="/static/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#000000" />
{{ if eq .Conf.Language "zh-CN" }}
<link rel="manifest" href="/static/manifest-zh-CN.json?v20240905" />
{{ else }}
<link rel="manifest" href="/static/manifest-en-US.json?v20240905" />
{{ end }}
<link rel="apple-touch-startup-image" href="/static/logo.svg" />
<link rel="shortcut icon" type="image/png" href="/static/logo.svg">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/semantic.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/assets/font-logos.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/css/flag-icons.min.css">
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
<link rel="stylesheet" type="text/css" href="/static/theme-default/css/main.css?v20240616">
<link rel="stylesheet" type="text/css" href="/static/theme-default/css/main.css?v20240816">
<link rel="stylesheet" type="text/css" href="/static/darkmode.css?v20240816">
<script src="https://unpkg.com/[email protected]/dist/jquery.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/semantic.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script>
<script src="/static/theme-default/js/mixin.js?v20240302"></script>
<script src="/static/theme-default/js/mixin.js?v20240911"></script>
<script>
document.documentElement.setAttribute('nz-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
</script>
</head>
<body>
{{end}}
{{end}}
+18 -19
View File
@@ -194,7 +194,7 @@
]
},
mixins: [mixinsVue],
created() {
created() {
this.servers = JSON.parse('{{.Servers}}').servers;
this.group()
},
@@ -240,12 +240,12 @@
const itemHeight = isMobile ? 10 : 10;
const gridLeft = 25;
const gridRight = 12;
const fontColor = "rgba(0, 0, 0, 0.68)";
const backgroundColor = '';
const borderColor = "#ffffff";
const chartData = this.chartDataList[id - 1];
const chartContainer = this.$refs[`chart${id}`][0];
const chart = echarts.init(chartContainer, null, {
const chartTheme = $('html').attr('nz-theme') == "dark" ? "dark" : "";
const chart = echarts.init(chartContainer, chartTheme, {
renderer: 'canvas',
useDirtyRect: false,
width: 'auto',
@@ -283,7 +283,6 @@
trigger: 'axis',
textStyle: {
fontSize: fontSize,
color: fontColor
}
},
legend: {
@@ -292,7 +291,6 @@
show: true,
textStyle: {
fontSize: fontSize,
color: fontColor
},
lineStyle: {
cap: 'butt'
@@ -332,7 +330,6 @@
series: seriesData,
textStyle: {
fontSize: fontSize,
color: fontColor
},
grid: {
top: '30',
@@ -477,21 +474,23 @@
const sizes = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"];
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + sizes[i];
},
getCoreAndGHz(str){
if ((str || []).hasOwnProperty(0) === false) {
getCoreAndGHz(arr) {
if ((arr || []).length === 0) {
return '';
}
str = str[0];
let GHz = str.match(/(\d|\.)+GHz/g);
let Core = str.match(/(\d|\.)+ Physical/g);
GHz = GHz !== null ? GHz.hasOwnProperty(0) === false ? '' : GHz[0] : ''
Core = Core !== null ? Core.hasOwnProperty(0) === false ? '?' : Core[0] : '?'
if (Core === '?') {
let Core = str.match(/(\d|\.)+ Virtual/g);
Core = Core !== null ? Core.hasOwnProperty(0) === false ? '?' : Core[0] : '?'
return Core.replace('Virtual', 'Core')
}
return Core.replace('Physical', 'Core');
let totalCores = 0;
arr.forEach(str => {
let coreMatch = str.match(/(\d+(\.\d+)?) Physical/g);
let coreCount = 0;
if (coreMatch) {
coreCount = parseFloat(coreMatch[0]);
} else {
let coreMatch = str.match(/(\d+(\.\d+)?) Virtual/g);
coreCount = coreMatch ? parseFloat(coreMatch[0]) : 0;
}
totalCores += coreCount;
});
return `${totalCores} Cores`;
},
getK2Gb(bs){
bs = bs / 1024 / 1024 / 1024;
+2 -2
View File
@@ -7,7 +7,7 @@
</a>
<a class='item' href="/"><i class="home icon"></i>{{tr "Home"}}</a>
<template v-if="isMobile">
<div class="item ui simple dropdown">
<div class="item ui dropdown" :class="{ simple: !isMobile }">
<div class="text"><i class="bi bi-gear-wide-connected icon" style="margin-right:3px;"></i>{{tr "Feature" }}<i class="dropdown icon" style="margin-right:0px;"></i></div>
<div class="menu">
<a href="/service" class="item"><i class="rss icon"></i>{{tr "Services" }}</a>
@@ -20,7 +20,7 @@
<a href="/network" class="item"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a>
</template>
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
<div class="item ui simple dropdown">
<div class="item ui dropdown" :class="{ simple: !isMobile }">
<div class="text"><i class="bi bi-incognito icon" style="margin-right:3px;"></i>{{tr "Template" }}<i class="dropdown icon" style="margin-right:0px;"></i></div>
<div class="menu">
<a v-for="(item, index) in adaptedTemplates" :key="index" @click="toggleTemplate(item.key)" class="item">
+27 -18
View File
@@ -18,7 +18,7 @@
</div>
</div>
<div class="ui container">
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
<div ref="chartDom" style="margin-top: 15px;height: auto;overflow: hidden"></div>
</div>
</div>
@@ -36,7 +36,11 @@
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}},
servers: initData,
option: {
option: {}
},
mixins: [mixinsVue],
created() {
this.option = {
tooltip: {
trigger: 'axis',
position: function (pt) {
@@ -65,21 +69,13 @@
}
},
grid: {
left: '8%',
right: '8%',
},
backgroundColor: 'rgba(255, 255, 255, 0.8)',
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
left: this.isMobile ? '8%' : '3.8%',
right: this.isMobile ? '8%' : '3.8%',
},
backgroundColor: '',
dataZoom: [
{
type: 'slider',
start: 0,
end: 100
}
@@ -93,10 +89,8 @@
boundaryGap: false
},
series: [],
},
chartOnOff: true,
}
},
mixins: [mixinsVue],
mounted() {
this.renderChart();
this.parseMonitorInfo(monitorInfo);
@@ -236,6 +230,20 @@
this.option.title.text = monitorInfo.result[0].server_name;
this.option.series = tSeries;
this.option.legend.data = tLegendData;
const maxLegendsPerRowMobile = localStorage.getItem("maxLegendsPerRowMobile") ? localStorage.getItem("maxLegendsPerRowMobile") : 2;
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6;
const autoIncrement = Math.floor((tLegendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 28 : 34);
const height = 520 + autoIncrement;
const gridTop = 60 + autoIncrement;
this.option.grid = {
left: this.isMobile ? '8%' : '3.8%',
right: this.isMobile ? '8%' : '3.8%',
top: gridTop
};
this.myChart.resize({
width: 'auto',
height: height
});
this.myChart.clear();
this.myChart.setOption(this.option);
},
@@ -243,7 +251,8 @@
return str.includes('Windows')
},
renderChart() {
this.myChart = echarts.init(this.$refs.chartDom);
const chartTheme = $('html').attr('nz-theme') == "dark" ? "dark" : "";
this.myChart = echarts.init(this.$refs.chartDom,chartTheme);
this.myChart.setOption(this.option);
},
resizeHandle () {
@@ -1,23 +0,0 @@
{{define "theme-server-status/content-footer"}}
<footer class="container" style="padding-bottom: 2rem;">
<p style="text-align: center; font-size: 10px;">
{{ .Conf.Site.Brand }} | Theme ServerStatus | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}
</p>
</footer>
<aside class="toolbox">
<span v-if="page=='index' && countryMapChartData.length!=0" class="showMapChart">
<i @click="showMapChart" data-toggle="modal" data-target="#mapChartBox" class="bi bi-geo-alt"></i>
</span>
<span class="toggleView">
<i v-if="showGroup" @click="toggleShowGroup" class="show-nogroup bi bi-justify"></i>
<i v-else @click="toggleShowGroup" class="show-group bi bi-view-stacked"></i>
</span>
<span class="setTheme">
<i v-if="theme === 'light'" @click="setTheme('dark')" class="setTheme-dark bi bi-moon-fill"></i>
<i v-else @click="setTheme('light')" class="setTheme-light bi bi-brightness-high-fill"></i>
</span>
<span v-if="showGoTop" class="showGoTop">
<i @click="goTop" class="goTop bi bi-arrow-up"></i>
</span>
</aside>
{{end}}
+34
View File
@@ -1,4 +1,38 @@
{{define "theme-server-status/footer"}}
</section>
<footer class="container-fluid">
<p>{{ .Conf.Site.Brand }} | Theme ServerStatus | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}</p>
</footer>
<aside class="toolbox">
<template v-if="showTools">
<span v-if="page=='index' && countryMapChartData.length!=0" class="showMapChart">
<i @click="showMapChart" data-toggle="modal" data-target="#mapChartBox" class="bi bi-geo-alt"></i>
</span>
<span class="toggleView">
<i v-if="showGroup" @click="toggleShowGroup" class="show-nogroup bi bi-justify"></i>
<i v-else @click="toggleShowGroup" class="show-group bi bi-view-stacked"></i>
</span>
<span class="toggleSemiTransparent" @click="toggleSemiTransparent">
<i class="bi" :class="semiTransparent ? 'bi-droplet' : 'bi-droplet-half'"></i>
</span>
</template>
<span v-if="!showTools">
<i @click="toggleShowTools" class="bi bi-three-dots"></i>
</span>
<span class="setTheme">
<i v-if="theme === 'light'" @click="setTheme('dark')" class="setTheme-dark bi bi-moon-fill"></i>
<i v-else @click="setTheme('light')" class="setTheme-light bi bi-brightness-high-fill"></i>
</span>
<span v-if="showGoTop" class="showGoTop">
<i @click="goTop" class="goTop bi bi-arrow-up"></i>
</span>
</aside>
<template v-if="semiTransparent">
<link rel="stylesheet" href="/static/theme-server-status/css/light.plus.css?v20240807">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.plus.css?v20240807">
</template>
</div>
{{if ts .CustomCode}}{{.CustomCode|safe}}{{end}}
</body>
</html>
{{end}}
+18 -16
View File
@@ -2,16 +2,22 @@
<!DOCTYPE html>
<html lang="{{.Conf.Language}}">
<head>
<title>{{ .Title }}</title>
<title>{{.Title}}</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta content="telephone=no" name="format-detection">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" type="image/png" href="/static/logo.svg" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#000000" />
{{if eq .Conf.Language "zh-CN"}}
<link rel="manifest" href="/static/manifest-zh-CN.json?v20240905" />
{{else}}
<link rel="manifest" href="/static/manifest-en-US.json?v20240905" />
{{end}}
<link rel="apple-touch-startup-image" href="/static/logo.svg" />
<link rel="shortcut icon" type="image/png" href="/static/logo.svg">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20240616">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v20240407">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240407">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/assets/font-logos.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/css/flag-icons.min.css">
@@ -20,17 +26,13 @@
<script src="https://unpkg.com/[email protected]/dist/js/bootstrap.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
<script src="/static/theme-server-status/js/mixin.js?v20240707"></script>
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="/static/theme-server-status/js/html5shiv.js"></script>
<script src="/static/theme-server-status/js/respond.min.js"></script>
<![endif]-->
{{if ts .CustomCode}}
{{.CustomCode|safe}}
{{end}}
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20240909">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v202408011">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240811">
<script src="/static/theme-server-status/js/mixin.js?v20240907"></script>
</head>
<body>
<div id="app">
{{end}}
@@ -18,7 +18,7 @@
<tbody id="servers">
<template v-for="(node,index) in nodesNoTag">
<tr :id="'r'+node.ID" data-toggle="collapse" :data-target="'#rt'+node.ID" class="accordion-toggle" :class="index % 2 === 0 ? 'odd': 'even'"
aria-expanded="false" @click="showCharts($event, node.ID)">
aria-expanded="false" @click="showCharts(node.ID)">
<td class="node-cell status center">
<div class="status-container">
<div v-if="node.online" class="status-icon online"></div>
@@ -143,7 +143,7 @@
@#node.host.Version#@
</span>
<span class="node-echarts-expand">
<div class="chartbox" chartbox-show="0" :key="node.ID" :ref="`chart${node.ID}`" style="width: 100%; height: auto;"></div>
<div class="chartbox" :id="`chart-${node.ID}`" chartbox-show="0" :key="node.ID" style="width: 100%; height: auto;"></div>
</span>
</div>
</div>
+2 -2
View File
@@ -21,7 +21,7 @@
<tbody id="servers">
<template v-for="(node,index) in group.data">
<tr :id="'r'+node.ID" data-toggle="collapse" :data-target="'#rt'+node.ID" class="accordion-toggle"
:class="index % 2 === 0 ? 'odd': 'even'" aria-expanded="false" @click="showCharts($event, node.ID)">
:class="index % 2 === 0 ? 'odd': 'even'" aria-expanded="false" @click="showCharts(node.ID)">
<td class="node-cell status center">
<div class="status-container">
<div v-if="node.online" class="status-icon online"></div>
@@ -146,7 +146,7 @@
@#node.host.Version#@
</span>
<span class="node-echarts-expand">
<div class="chartbox" chartbox-show="0" :key="node.ID" :ref="`chart${node.ID}`" style="width: 100%; height: auto;"></div>
<div class="chartbox" :id="`chart-${node.ID}`" chartbox-show="0" :key="node.ID" style="width: 100%; height: auto;"></div>
</span>
</div>
</div>
+205 -125
View File
@@ -1,34 +1,35 @@
{{define "theme-server-status/home"}}
{{template "theme-server-status/header" .}}
<div id="app">
{{template "theme-server-status/content-nav" .}}
<!-- showGroup true -->
<template v-if="showGroup">
<section class="container table-responsive content" style="max-width: 95vw" v-for="group in nodesTag">
{{template "theme-server-status/home-group-true" .}}
</section>
</template>
<!-- showGroup false -->
<template v-else>
<section class="container table-responsive content" style="max-width: 95vw">
{{template "theme-server-status/home-group-false" .}}
</section>
</template>
<div class="modal fade" id="mapChartBox" tabindex="-1" role="dialog" aria-labelledby="mapChartTitle" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="mapChartTitle">{{tr "ServersOnWorldMap"}}</h4>
<i class="bi bi-x" data-dismiss="modal" aria-label="Close"></i>
</div>
<div class="modal-body">
<div id="mapChart" style="width:100%;height:auto;"></div>
</div>
{{template "theme-server-status/menu" .}}
<!-- showGroup true -->
<template v-if="showGroup">
<div class="container-fluid table-responsive content" v-for="group in nodesTag">
{{template "theme-server-status/home-group-true" .}}
</div>
</template>
<!-- showGroup false -->
<template v-else>
<div class="container-fluid table-responsive content">
{{template "theme-server-status/home-group-false" .}}
</div>
</template>
<div class="modal fade" id="mapChartBox" tabindex="-1" role="dialog" aria-labelledby="mapChartTitle" aria-hidden="true" style="padding-left:0">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="mapChartTitle">
<i class="bi bi-geo-alt"></i>
<span>{{tr "ServersOnWorldMap"}}</span>
</h4>
<i class="bi bi-x xclose" data-dismiss="modal" aria-label="Close"></i>
</div>
<div class="modal-body">
<div id="mapChart"></div>
</div>
</div>
</div>
{{template "theme-server-status/content-footer" .}}
</div>
{{template "theme-server-status/footer" .}}
<script>
new Vue({
el: '#app',
@@ -154,15 +155,17 @@
return;
}
const unit = this.language=='zh-CN' ? '台' : 'servers';
const isMobile = this.checkIsMobile();
const width = isMobile ? 338 : 1102;
const height = isMobile ? 200 : 500;
const backgroundColor = this.theme == "dark" ? '#1C1D26' : '#ffffff';
const width = window.innerWidth;
const height = 0.95 * window.innerHeight;
const backgroundColor = this.theme == "dark" ? '' : '';
const inRangeColor = this.theme == "dark" ? '#D2B206' : '#FFDF32';
const tooltipBackgroundColor = this.theme == "dark" ? "#ffffff" : '#ffffff';
const tooltipBorderColor = this.theme == "dark" ? "#ffffff" : "#ffffff";
const fontSize = isMobile ? 10 : 12;
const fontColor = this.theme == "dark" ? "#000000" : "#000000";
const fontSize = this.isMobile ? 10 : 12;
const fontColor = this.theme == "dark" ? "#000000" : "#000000";
const showLoadingMaskColor = this.theme == "dark" ? 'rgba(28, 29, 38, 1)' : 'rgba(249, 249, 249, 1)';
const showLoadingTextColor = this.theme == "dark" ? 'rgba(241, 241, 241, 1)' : 'rgba(0, 0, 0, 1)';
const showLoadingColor = inRangeColor;
const chartContainer = document.getElementById('mapChart');
const mapChart = echarts.init(chartContainer, '', { // init图表
renderer: 'canvas',
@@ -170,6 +173,13 @@
width: width,
height: height,
});
mapChart.showLoading({
text: 'loading',
textColor: showLoadingTextColor,
color: showLoadingColor,
maskColor: showLoadingMaskColor,
zlevel: 2
});
fetch(this.staticUrl + '/maps/' + this.countryMapGeoFile)
.then(response => response.json())
.then(worldMap => {
@@ -224,7 +234,10 @@
}
]
};
mapChart.setOption(option);
setTimeout(() => {
mapChart.hideLoading();
mapChart.setOption(option);
}, 1000);
});
},
isWindowsPlatform(str) {
@@ -470,24 +483,27 @@
// 如果所有元素的 Temperature 都为 0,则返回一个默认值 0
return 0;
},
showCharts(event, id) {
const chartContainer = this.$refs[`chart${id}`][0];
const chartboxShow = chartContainer.getAttribute('chartbox-show');
chartContainer.setAttribute('chartbox-show', chartboxShow === '0' ? '1' : '0');
const isAriaExpandedFalse = event.currentTarget.getAttribute('aria-expanded') === 'false';
if (!isAriaExpandedFalse) return;
showCharts(id,changeChartboxShow=true) {
const chartContainer = document.getElementById(`chart-${id}`);
if(changeChartboxShow){
const chartboxShow = chartContainer.getAttribute('chartbox-show');
chartContainer.setAttribute('chartbox-show', chartboxShow === '0' ? '1' : '0');
const collapseContainer = document.getElementById(`r${id}`);
const isAriaExpandedFalse = collapseContainer.getAttribute('aria-expanded') === 'false';
if (!isAriaExpandedFalse) return;
}
// 发起数据请求
const url = `/api/v1/monitor/${id}`;
fetch(url)
.then(response => response.json())
.then(data => {
if (data.result) { // 数据请求成功,更新数据并渲染图表
this.chartDataList[id - 1] = data.result;
this.chartDataList[id] = data.result;
this.$nextTick(() => {
this.renderCharts(id);
});
} else {
console.log('this agent (id:'+ id + ') has no monitor.');
console.log('this server (id:'+ id + ') has no monitor.');
}
})
.catch(error => {
@@ -495,90 +511,160 @@
});
},
renderCharts(id, reload = false) {
if (!this.chartDataList[id - 1]) return;
const chartData = this.chartDataList[id - 1];
const chartContainer = this.$refs[`chart${id}`][0];
if (reload) { //点击切换亮色/暗色风格模式时,重新载入echarts图表的逻辑,
// 第一步,查找已经渲染出的图表容器,并销毁它
if (!this.chartDataList[id]) return;
const chartData = this.chartDataList[id];
const chartContainer = document.getElementById(`chart-${id}`);
if (reload) {
const existingChart = echarts.getInstanceByDom(chartContainer);
if (existingChart) existingChart.dispose();
// 第二步,如果图表容器处于不可见状态chartboxShow=0,不重新渲染出新的图表,
// 如果图表容器处于可见状态chartboxShow=1,重新渲染出新的图表
const chartboxShow = chartContainer.getAttribute('chartbox-show');
if ( chartboxShow === '0' ) return;
}
// 定义图表参数值
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
const isMobile = this.checkIsMobile();
const fontSize = isMobile ? 10 : 14;
const gridLeft = isMobile ? 25 : 36;
const gridRight = isMobile ? 5 : 20;
const legendLeft = isMobile ? 'center' : 'center';
const legendTop = isMobile ? 5 : 5;
const legendPadding= isMobile ? [5,0,5,0] : [5,0,5,0];
const fontSize = this.isMobile ? 10 : 14;
const gridLeft = (MaxTCPPingValue > 500) ? (this.isMobile ? 36 : 42) : (this.isMobile ? 25 : 36);
const gridRight = this.isMobile ? 5 : 20;
const legendLeft = this.isMobile ? 'center' : 'center';
const legendTop = this.isMobile ? 5 : 5;
const legendPadding= this.isMobile ? [5,0,5,0] : [5,0,5,0];
const systemDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const theme = localStorage.getItem("theme") ? localStorage.getItem("theme") : systemDarkMode;
const chartTheme = theme == "dark" ? "dark" : "default";
const fontColor = theme == "dark" ? "#f1f1f1" : "#000000";
const backgroundColor = theme == "dark" ? "#1C1D26" : '';
const tooltipBackgroundColor = theme == "dark" ? "#1C1D26" : '#ffffff';
const tooltipBorderColor = theme == "dark" ? "#31363B" : "#ffffff";
// 渲染图表
const chart = echarts.init(chartContainer, chartTheme, {
const chartTheme = this.theme == "dark" ? "dark" : "default";
const fontColor = this.theme == "dark" ? "#f1f1f1" : "#000000";
const backgroundColor = theme == "dark" ? '' : '';
const tooltipBackgroundColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.85)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.85)" : "rgba(255,255,255,1)");
const tooltipBorderColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.9)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.9)" : "rgba(255,255,255,1)");
const lineStyleWidth = this.isMobile ? 1 : 2;
const splitLineWidth = this.isMobile ? 0.5 : 1;
const markLineItemStyleOpacity = this.semiTransparent ? 1 : 0.75;
const markLineLineStyleWidth = this.isMobile ? 0.15 : 0.3;
const chart = echarts.init(chartContainer, chartTheme, { // init图表
renderer: 'canvas',
useDirtyRect: false,
width: 'auto',
height: 300,
height: 300
});
const xAxisData = chartData[0].created_at.map(time => new Date(time).toLocaleString());
const seriesData = chartData.map(item => {
let legendData = [];
let seriesData = [];
chartData.forEach((item,key)=> {
let loss = 0;
const data = item.avg_delay.map((avgDelay, index) => {
if(avgDelay > 0 && avgDelay < MaxTCPPingValue){
loss += avgDelay > 0.9 * MaxTCPPingValue ? 1 : 0;
return [item.created_at[index], avgDelay.toFixed(2)];
}else{
let totalLossRate = 0;
let legendName = '';
let data = { main: [], markLine: []};
item.avg_delay.forEach((avgDelay, index) => {
const threshold = 0.9 * MaxTCPPingValue; // 定义阀值,用于判断是否丢包
// 定义丢包 1. avgDelay==0 2. avgDelay>=MaxTCPPingValue 3. avgDelay>=threshold
if(avgDelay == 0 || avgDelay >= MaxTCPPingValue){ //绝对丢包
loss += 1;
const lossrate = 100 * loss / (index + 1);
if(lossrate != 100) {
data['markLine'].push({
xAxis: item.created_at[index],
label: { show: false },
emphasis: { disabled: true },
lineStyle: { type: "solid" }
});
}
} else if (avgDelay >= threshold && avgDelay < MaxTCPPingValue){ // 相对丢包
loss += 1;
const lossrate = 100 * loss / (index + 1);
if(lossrate != 100) {
data['main'].push(
[item.created_at[index], avgDelay, lossrate]
);
data['markLine'].push({
xAxis: item.created_at[index],
label: { show: false },
emphasis: { disabled: true },
lineStyle: { type: "solid" }
});
}
} else { // 未丢包
const lossrate = 100 * loss / (index + 1);
data['main'].push(
[item.created_at[index], avgDelay, lossrate]
);
}
});
const lossRate = ((loss / item.created_at.length) * 100).toFixed(1);
item.monitor_name = item.monitor_name.includes("%") ? item.monitor_name : `${item.monitor_name} ${lossRate}%`;
return {
name: item.monitor_name,
type: 'line',
smooth: true,
symbol: 'none',
data: data,
connectNulls: true
};
totalLossRate = ((loss / item.created_at.length) * 100).toFixed(1);
legendName = `${item.monitor_name} ${totalLossRate}%`;
legendData.push(legendName);
seriesData.push(
{
name: legendName,
type: 'line',
smooth: true,
symbol: 'none',
connectNulls: true,
legendHoverLink: false,
emphasis: {
disabled: true
},
lineStyle: {
width: lineStyleWidth
},
data: data['main'],
markLine: {
symbol: "none",
symbolSize :0,
data: data['markLine'],
itemStyle: {
opacity: markLineItemStyleOpacity
},
lineStyle:{
width: markLineLineStyleWidth
}
}
}
);
});
const legendData = chartData.map(item => item.monitor_name);
const maxLegendsPerRowMobile = localStorage.getItem("maxLegendsPerRowMobile") ? localStorage.getItem("maxLegendsPerRowMobile") : 3;
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6;
const autoIncrement = Math.floor((legendData.length - 1) / (isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (isMobile ? 20 : 28)
const autoIncrement = Math.floor((legendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 20 : 28);
const height = 300 + autoIncrement;
const gridTop = 40 + autoIncrement;
const legendIcon = isMobile ? 'rect' : "";
const itemWidth = isMobile ? 10 : 25;
const itemHeight = isMobile ? 10 : 14;
const legendIcon = this.isMobile ? 'rect' : "";
const itemWidth = this.isMobile ? 10 : 25;
const itemHeight = this.isMobile ? 10 : 14;
chart.resize({
width: 'auto',
height: height
});
const option = {
color: this.colors,
backgroundColor: backgroundColor,
title: {
show: false
textStyle: {
fontSize: fontSize,
color: fontColor
},
tooltip: {
trigger: 'axis',
backgroundColor: tooltipBackgroundColor,
borderColor: tooltipBorderColor,
textStyle: {
fontSize: fontSize,
color: fontColor
grid: {
top: gridTop,
left: gridLeft,
right: gridRight,
},
title: {
show: false,
},
series: seriesData.flat(),
xAxis: {
type: 'time',
axisLabel: {
textStyle: {
fontSize: fontSize
}
}
},
yAxis: {
type: 'value',
axisLabel: {
textStyle: {
fontSize: fontSize
}
},
splitLine: {
lineStyle: {
width: splitLineWidth
}
}
},
legend: {
@@ -596,21 +682,26 @@
itemWidth: itemWidth,
itemHeight: itemHeight,
},
xAxis: {
type: 'time',
data: xAxisData,
axisLabel: {
textStyle: {
fontSize: fontSize
}
}
},
yAxis: {
type: 'value',
axisLabel: {
textStyle: {
fontSize: fontSize
}
tooltip: {
trigger: 'axis',
backgroundColor: tooltipBackgroundColor,
borderColor: tooltipBorderColor,
textStyle: {
fontSize: fontSize,
color: fontColor
},
formatter: function (params) {
let tooltipContent = '';
const formattedTime = new Date(params[0].value[0]).toLocaleString();
tooltipContent += `<span style="line-height:2em">${formattedTime}</span><br>`;
params.forEach(param => {
const formattedTime = new Date(param.value[0]).toLocaleString();
if (!param.seriesName.includes('stack')) {
const name = param.seriesName.replace(/\s\d+(\.\d+)?%$/, '');
tooltipContent += `<span style="line-height:2em">${param.marker} ${name} ${param.value[2].toFixed(1)}% ${param.value[1].toFixed(2)}</span><br>`;
}
});
return tooltipContent;
}
},
dataZoom: [
@@ -619,24 +710,14 @@
start: 0,
end: 100
}
],
series: seriesData,
textStyle: {
fontSize: fontSize,
color: fontColor
},
grid: {
top: gridTop,
left: gridLeft,
right: gridRight
}
]
};
chart.setOption(option);
},
reloadCharts() { // 重新加载所有图表
reloadCharts() {
this.servers.forEach(node => {
const id = node.ID;
const chartData = this.chartDataList[id - 1];
const chartData = this.chartDataList[id];
if (chartData) {
this.renderCharts(id,true);
}
@@ -645,5 +726,4 @@
}
})
</script>
{{template "theme-server-status/footer" .}}
{{end}}
@@ -1,6 +1,6 @@
{{define "theme-server-status/content-nav"}}
<header role="navigation" class="navbar navbar-inverse navbar-fixed-top" style="z-index:99999999;">
<div class="container">
{{define "theme-server-status/menu"}}
<header role="navigation" class="navbar navbar-inverse navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button data-target=".navbar-collapse" data-toggle="collapse" class="navbar-toggle" type="button">
<span class="sr-only">Toggle navigation</span>
@@ -64,5 +64,6 @@
</nav>
</div>
</header>
<section class="nezha">
{{end}}

Some files were not shown because too many files have changed in this diff Show More