Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96c3fd433f | ||
|
|
45b0f1edfc | ||
|
|
61fdcd2a9e | ||
|
|
a474b8484b | ||
|
|
67b003a27d | ||
|
|
99aeda7939 | ||
|
|
d5744d0c73 | ||
|
|
e7837d8eaf | ||
|
|
aa81fc02ab | ||
|
|
f6531a52bd | ||
|
|
c58c4f866a | ||
|
|
fc9f1b6bcc | ||
|
|
be7b6e9c5e | ||
|
|
a503f0cf40 | ||
|
|
0b7f43b149 | ||
|
|
55f5c89c1c | ||
|
|
937696c26d | ||
|
|
08aaea8244 | ||
|
|
b886f70e48 | ||
|
|
12c6b8a6f6 | ||
|
|
ebcc8a6efb | ||
|
|
c4d8b63d32 | ||
|
|
318da135b9 | ||
|
|
da74bf4749 | ||
|
|
3250283ef9 | ||
|
|
2b40040ec5 | ||
|
|
60ada33ee9 | ||
|
|
7940e010e0 | ||
|
|
2df57e6249 | ||
|
|
106d58575b | ||
|
|
bdf36276da | ||
|
|
f32f127dfc | ||
|
|
f78ba281fb | ||
|
|
7f9f9a9129 | ||
|
|
b701efd9b5 | ||
|
|
511f43784e | ||
|
|
3f6112a919 | ||
|
|
23751440c6 | ||
|
|
989c9f9740 | ||
|
|
71508f8724 | ||
|
|
eb6dd2855e | ||
|
|
64da3c7438 | ||
|
|
fd85f599ba | ||
|
|
cae443d5c8 | ||
|
|
0ec1bb2c54 | ||
|
|
47f8447a22 | ||
|
|
9c986d06cb | ||
|
|
e9428d5757 | ||
|
|
daab64d232 | ||
|
|
da8fb57268 | ||
|
|
fda0dad5f1 | ||
|
|
c6fb3196ea | ||
|
|
711d162a46 | ||
|
|
d667ce8afe |
+16
-12
@@ -97,20 +97,24 @@ def sync_to_gitee(tag: str, body: str, files: slice):
|
|||||||
asset_api_uri = f"{release_api_uri}/{release_id}/attach_files"
|
asset_api_uri = f"{release_api_uri}/{release_id}/attach_files"
|
||||||
|
|
||||||
for file_path in files:
|
for file_path in files:
|
||||||
files = {
|
success = False
|
||||||
'file': open(file_path, 'rb')
|
|
||||||
}
|
|
||||||
|
|
||||||
asset_api_response = requests.post(
|
while not success:
|
||||||
asset_api_uri, params={'access_token': access_token}, files=files)
|
files = {
|
||||||
|
'file': open(file_path, 'rb')
|
||||||
|
}
|
||||||
|
|
||||||
if asset_api_response.status_code == 201:
|
asset_api_response = requests.post(
|
||||||
asset_info = asset_api_response.json()
|
asset_api_uri, params={'access_token': access_token}, files=files)
|
||||||
asset_name = asset_info.get('name')
|
|
||||||
print(f"Successfully uploaded {asset_name}!")
|
if asset_api_response.status_code == 201:
|
||||||
else:
|
asset_info = asset_api_response.json()
|
||||||
print(
|
asset_name = asset_info.get('name')
|
||||||
f"Request failed with status code {asset_api_response.status_code}")
|
print(f"Successfully uploaded {asset_name}!")
|
||||||
|
success = True
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"Request failed with status code {asset_api_response.status_code}")
|
||||||
|
|
||||||
# 仅保留最新 Release 以防超出 Gitee 仓库配额
|
# 仅保留最新 Release 以防超出 Gitee 仓库配额
|
||||||
try:
|
try:
|
||||||
|
|||||||
+119
-73
@@ -4,54 +4,139 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
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
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: goreleaser/goreleaser-cross:v1.23
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
GOARM: ${{ matrix.goarm }}
|
||||||
|
outputs:
|
||||||
|
tag_name: ${{ steps.extract_branch.outputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
|
- run: git config --global --add safe.directory /__w/nezha/nezha
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Fetch IPInfo GeoIP Database
|
- name: Fetch IPInfo GeoIP Database
|
||||||
env:
|
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
|
||||||
|
env:
|
||||||
IPINFO_TOKEN: ${{ secrets.IPINFO_TOKEN }}
|
IPINFO_TOKEN: ${{ secrets.IPINFO_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
rm pkg/geoip/geoip.db
|
rm pkg/geoip/geoip.db
|
||||||
wget -O pkg/geoip/geoip.db https://ipinfo.io/data/free/country.mmdb?token=${IPINFO_TOKEN}
|
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.23.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"
|
||||||
|
export fileWithoutExt=${file%.*}
|
||||||
|
zip -jr "$fileWithoutExt.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
|
- name: Extract branch name
|
||||||
run: |
|
run: |
|
||||||
export TAG_NAME=$(echo ${GITHUB_REF#refs/tags/})
|
export TAG_NAME=$(echo ${GITHUB_REF#refs/tags/})
|
||||||
echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT
|
echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT
|
||||||
id: extract_branch
|
id: extract_branch
|
||||||
|
|
||||||
- name: xgo build
|
- name: Log into GHCR
|
||||||
uses: crazy-max/ghaction-xgo@v2
|
|
||||||
with:
|
|
||||||
xgo_version: latest
|
|
||||||
go_version: 1.21.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: |
|
|
||||||
ls -al dist/
|
|
||||||
mv dist/dashboard-linux-arm-7 dist/dashboard-linux-arm
|
|
||||||
|
|
||||||
- name: Log in to the GHCR
|
|
||||||
uses: docker/login-action@master
|
uses: docker/login-action@master
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ github.token }}
|
||||||
|
|
||||||
- name: Log in to the AliyunCS
|
- name: Log in to the AliyunCS
|
||||||
uses: docker/login-action@master
|
uses: docker/login-action@master
|
||||||
@@ -68,13 +153,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up image name
|
- name: Set up image name
|
||||||
run: |
|
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" ]
|
if [ ${{ github.repository_owner }} = "naiba" ]
|
||||||
then ALI_IMAGE_NAME=$(echo "registry.cn-shanghai.aliyuncs.com/naibahq/nezha-dashboard")
|
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:]')
|
else ALI_IMAGE_NAME=$(echo "registry.cn-shanghai.aliyuncs.com/${{ github.repository_owner }}/nezha-dashboard" | tr '[:upper:]' '[:lower:]')
|
||||||
fi
|
fi
|
||||||
echo "::set-output name=GHRC_IMAGE_NAME::$GHRC_IMAGE_NAME"
|
echo "GHCR_IMAGE_NAME=$GHCR_IMAGE_NAME" >> $GITHUB_OUTPUT
|
||||||
echo "::set-output name=ALI_IMAGE_NAME::$ALI_IMAGE_NAME"
|
echo "ALI_IMAGE_NAME=$ALI_IMAGE_NAME" >> $GITHUB_OUTPUT
|
||||||
id: image-name
|
id: image-name
|
||||||
|
|
||||||
- name: Build dasbboard image And Push
|
- name: Build dasbboard image And Push
|
||||||
@@ -82,49 +167,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm,linux/s390x,linux/riscv64 # linux/386,
|
platforms: linux/amd64,linux/arm64,linux/s390x
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ steps.image-name.outputs.GHRC_IMAGE_NAME }}:latest
|
${{ steps.image-name.outputs.GHCR_IMAGE_NAME }}:latest
|
||||||
${{ steps.image-name.outputs.GHRC_IMAGE_NAME }}:${{ steps.extract_branch.outputs.tag }}
|
${{ 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 }}:latest
|
||||||
${{ steps.image-name.outputs.ALI_IMAGE_NAME }}:${{ steps.extract_branch.outputs.tag }}
|
${{ 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
|
|
||||||
|
|
||||||
- name: Trigger sync
|
|
||||||
if: ${{ env.SYNCED == 0 }}
|
|
||||||
uses: actions/github-script@v6
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
await github.rest.actions.createWorkflowDispatch({
|
|
||||||
owner: 'naiba',
|
|
||||||
repo: 'nezha',
|
|
||||||
workflow_id: 'sync-release.yml',
|
|
||||||
ref: 'master'
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,43 +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.21.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
|
|
||||||
- name: fix dist
|
|
||||||
run: |
|
|
||||||
ls -al dist/
|
|
||||||
mv dist/dashboard-linux-arm-7 dist/dashboard-linux-arm
|
|
||||||
|
|
||||||
+19
-23
@@ -10,40 +10,36 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
- "resource/**"
|
- "resource/**"
|
||||||
- ".github/workflows/test.yml"
|
- ".github/workflows/test.yml"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu, windows, macos]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
env:
|
env:
|
||||||
GO111MODULE: on
|
GO111MODULE: on
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v4
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "^1.21.3"
|
go-version: "1.23.x"
|
||||||
|
|
||||||
- name: Unit test
|
- name: Unit test
|
||||||
run: |
|
run: |
|
||||||
go test -v ./...
|
go test -v ./...
|
||||||
|
|
||||||
|
- name: Build test
|
||||||
|
run: go build -v ./cmd/dashboard
|
||||||
|
|
||||||
- name: Run Gosec Security Scanner
|
- name: Run Gosec Security Scanner
|
||||||
|
if: runner.os == 'Linux'
|
||||||
uses: securego/gosec@master
|
uses: securego/gosec@master
|
||||||
with:
|
with:
|
||||||
args: --exclude=G104,G402 ./...
|
args: --exclude=G104,G402,G115,G203 ./...
|
||||||
- name: xgo build
|
|
||||||
uses: crazy-max/ghaction-xgo@v2
|
|
||||||
with:
|
|
||||||
xgo_version: latest
|
|
||||||
go_version: 1.21.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
|
|
||||||
- name: fix dist
|
|
||||||
run: |
|
|
||||||
ls -al dist/
|
|
||||||
mv dist/dashboard-linux-arm-7 dist/dashboard-linux-arm
|
|
||||||
|
|||||||
@@ -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
@@ -1,16 +1,12 @@
|
|||||||
# latest 镜像还没有 riscv64 https://hub.docker.com/_/alpine/tags
|
FROM alpine AS certs
|
||||||
FROM alpine:edge
|
RUN apk update && apk add ca-certificates
|
||||||
|
|
||||||
|
FROM busybox:stable-musl
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
RUN apk update && \
|
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
|
||||||
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 ./script/entrypoint.sh /entrypoint.sh
|
COPY ./script/entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
@@ -19,4 +15,6 @@ COPY dist/dashboard-${TARGETOS}-${TARGETARCH} ./app
|
|||||||
|
|
||||||
VOLUME ["/dashboard/data"]
|
VOLUME ["/dashboard/data"]
|
||||||
EXPOSE 80 5555
|
EXPOSE 80 5555
|
||||||
|
ARG TZ=Asia/Shanghai
|
||||||
|
ENV TZ=$TZ
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<br>
|
<br>
|
||||||
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
|
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
|
||||||
<br><br>
|
<br><br>
|
||||||
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard"> <img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.18.3-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"> <img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.20.2-brightgreen?style=for-the-badge&logo=linux">
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
|
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
|
||||||
@@ -44,8 +44,8 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
|||||||
<a href="https://github.com/AkkiaS7" title="Akkia"><img src="https://avatars.githubusercontent.com/u/68485070?v=4" width="50;" alt="Akkia"/></a>
|
<a href="https://github.com/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/Erope" title="卖女孩的小火柴"><img src="https://avatars.githubusercontent.com/u/44471469?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/uubulb" title="UUBulb"><img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/></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/nap0o" title="nap0o"><img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/></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/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/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/JackieSung4ev" title="JackieSung4ev"><img src="https://avatars.githubusercontent.com/u/24974735?v=4" width="50;" alt="JackieSung4ev"/></a>
|
||||||
@@ -59,6 +59,7 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
|||||||
<a href="https://github.com/1ridic" title="1ridic"><img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/></a>
|
<a href="https://github.com/1ridic" title="1ridic"><img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/></a>
|
||||||
<a href="https://github.com/Mmx233" title="Mmx"><img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/></a>
|
<a href="https://github.com/Mmx233" title="Mmx"><img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/></a>
|
||||||
<a href="https://github.com/rootmelo92118" title="rootmelo92118"><img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/></a>
|
<a href="https://github.com/rootmelo92118" title="rootmelo92118"><img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/></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/iilemon" title="Sean"><img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/></a>
|
<a href="https://github.com/iilemon" title="Sean"><img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/></a>
|
||||||
<a href="https://github.com/fscarmen" title="fscarmen"><img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/></a>
|
<a href="https://github.com/fscarmen" title="fscarmen"><img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/></a>
|
||||||
<a href="https://github.com/ch8o" title="no-name-now"><img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/></a>
|
<a href="https://github.com/ch8o" title="no-name-now"><img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/></a>
|
||||||
@@ -72,16 +73,18 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
|||||||
<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/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/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/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/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/lvyaoting" title="lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50;" alt="lvyaoting"/></a>
|
||||||
|
<a href="https://github.com/lyj0309" title="lyj"><img src="https://avatars.githubusercontent.com/u/50474995?v=4" width="50;" alt="lyj"/></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/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/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/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/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/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/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/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/Moraxyc" title="Moraxyc"><img src="https://avatars.githubusercontent.com/u/69713071?v=4" width="50;" alt="Moraxyc"/></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/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/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/xrgzs" title="MadDogOwner"><img src="https://avatars.githubusercontent.com/u/26499123?v=4" width="50;" alt="MadDogOwner"/></a>
|
||||||
@@ -90,7 +93,7 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
|||||||
<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/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/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/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-->
|
<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
|
## Special Thanks
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ func (cp *commonPage) serve() {
|
|||||||
cr.GET("/network", cp.network)
|
cr.GET("/network", cp.network)
|
||||||
cr.GET("/ws", cp.ws)
|
cr.GET("/ws", cp.ws)
|
||||||
cr.POST("/terminal", cp.createTerminal)
|
cr.POST("/terminal", cp.createTerminal)
|
||||||
|
cr.GET("/file", cp.createFM)
|
||||||
|
cr.GET("/file/:id", cp.fm)
|
||||||
}
|
}
|
||||||
|
|
||||||
type viewPasswordForm struct {
|
type viewPasswordForm struct {
|
||||||
@@ -99,7 +101,6 @@ func (p *commonPage) service(c *gin.Context) {
|
|||||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesStatus"}),
|
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesStatus"}),
|
||||||
"Services": res.([]interface{})[0],
|
"Services": res.([]interface{})[0],
|
||||||
"CycleTransferStats": res.([]interface{})[1],
|
"CycleTransferStats": res.([]interface{})[1],
|
||||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,12 +205,11 @@ func (cp *commonPage) network(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/network"), mygin.CommonEnvironment(c, gin.H{
|
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/network"), mygin.CommonEnvironment(c, gin.H{
|
||||||
"Servers": string(serversBytes),
|
"Servers": string(serversBytes),
|
||||||
"MonitorInfos": string(monitorInfos),
|
"MonitorInfos": string(monitorInfos),
|
||||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
|
||||||
"MaxTCPPingValue": singleton.Conf.MaxTCPPingValue,
|
"MaxTCPPingValue": singleton.Conf.MaxTCPPingValue,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
|
func (cp *commonPage) getServerStat(c *gin.Context, withPublicNote bool) ([]byte, error) {
|
||||||
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||||
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
|
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
|
||||||
authorized := isMember || isViewPasswordVerfied
|
authorized := isMember || isViewPasswordVerfied
|
||||||
@@ -217,18 +217,20 @@ func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
|
|||||||
singleton.SortedServerLock.RLock()
|
singleton.SortedServerLock.RLock()
|
||||||
defer singleton.SortedServerLock.RUnlock()
|
defer singleton.SortedServerLock.RUnlock()
|
||||||
|
|
||||||
var servers []*model.Server
|
var serverList []*model.Server
|
||||||
|
|
||||||
if authorized {
|
if authorized {
|
||||||
servers = singleton.SortedServerList
|
serverList = singleton.SortedServerList
|
||||||
} else {
|
} else {
|
||||||
filteredServers := make([]*model.Server, len(singleton.SortedServerListForGuest))
|
serverList = singleton.SortedServerListForGuest
|
||||||
for i, server := range singleton.SortedServerListForGuest {
|
}
|
||||||
filteredServer := *server
|
|
||||||
filteredServer.DDNSDomain = "redacted"
|
var servers []*model.Server
|
||||||
filteredServers[i] = &filteredServer
|
for _, server := range serverList {
|
||||||
|
item := *server
|
||||||
|
if !withPublicNote {
|
||||||
|
item.PublicNote = ""
|
||||||
}
|
}
|
||||||
servers = filteredServers
|
servers = append(servers, &item)
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.Json.Marshal(Data{
|
return utils.Json.Marshal(Data{
|
||||||
@@ -240,7 +242,7 @@ func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cp *commonPage) home(c *gin.Context) {
|
func (cp *commonPage) home(c *gin.Context) {
|
||||||
stat, err := cp.getServerStat(c)
|
stat, err := cp.getServerStat(c, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
@@ -254,14 +256,13 @@ func (cp *commonPage) home(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/home"), mygin.CommonEnvironment(c, gin.H{
|
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/home"), mygin.CommonEnvironment(c, gin.H{
|
||||||
"Servers": string(stat),
|
"Servers": string(stat),
|
||||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
var upgrader = websocket.Upgrader{
|
||||||
ReadBufferSize: 10240,
|
ReadBufferSize: 32768,
|
||||||
WriteBufferSize: 10240,
|
WriteBufferSize: 32768,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Data struct {
|
type Data struct {
|
||||||
@@ -286,7 +287,7 @@ func (cp *commonPage) ws(c *gin.Context) {
|
|||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
count := 0
|
count := 0
|
||||||
for {
|
for {
|
||||||
stat, err := cp.getServerStat(c)
|
stat, err := cp.getServerStat(c, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -430,5 +431,130 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/terminal", mygin.CommonEnvironment(c, gin.H{
|
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/terminal", mygin.CommonEnvironment(c, gin.H{
|
||||||
"SessionID": streamId,
|
"SessionID": streamId,
|
||||||
"ServerName": server.Name,
|
"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,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -44,12 +43,7 @@ func ServeWeb(port uint) *http.Server {
|
|||||||
tmpl = loadThirdPartyTemplates(tmpl)
|
tmpl = loadThirdPartyTemplates(tmpl)
|
||||||
r.SetHTMLTemplate(tmpl)
|
r.SetHTMLTemplate(tmpl)
|
||||||
r.Use(mygin.RecordPath)
|
r.Use(mygin.RecordPath)
|
||||||
staticFs, err := fs.Sub(resource.StaticFS, "static")
|
r.StaticFS("/static", http.FS(resource.StaticFS))
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
r.StaticFS("/static", http.FS(staticFs))
|
|
||||||
r.Static("/static-custom", "resource/static/custom")
|
|
||||||
routers(r)
|
routers(r)
|
||||||
page404 := func(c *gin.Context) {
|
page404 := func(c *gin.Context) {
|
||||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||||
@@ -90,7 +84,7 @@ func routers(r *gin.Engine) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
|
func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
|
||||||
var ret = tmpl
|
ret := tmpl
|
||||||
themes, err := os.ReadDir("resource/template")
|
themes, err := os.ReadDir("resource/template")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("NEZHA>> Error reading themes folder: %v", err)
|
log.Printf("NEZHA>> Error reading themes folder: %v", err)
|
||||||
@@ -100,17 +94,61 @@ func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
|
|||||||
if !theme.IsDir() {
|
if !theme.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// load templates
|
|
||||||
t, err := ret.ParseGlob(fmt.Sprintf("resource/template/%s/*.html", theme.Name()))
|
themeDir := theme.Name()
|
||||||
if err != nil {
|
if themeDir == "theme-custom" {
|
||||||
log.Printf("NEZHA>> Error parsing templates %s error: %v", theme.Name(), err)
|
// for backward compatibility
|
||||||
|
// note: will remove this in future versions
|
||||||
|
ret = loadTemplates(ret, themeDir)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ret = t
|
|
||||||
|
if strings.HasPrefix(themeDir, "dashboard-") {
|
||||||
|
// load dashboard templates, ignore desc file
|
||||||
|
ret = loadTemplates(ret, themeDir)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(themeDir, "theme-") {
|
||||||
|
log.Printf("NEZHA>> Invalid theme name: %s", themeDir)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
descPath := filepath.Join("resource", "template", themeDir, "theme.json")
|
||||||
|
desc, err := os.ReadFile(filepath.Clean(descPath))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("NEZHA>> Error opening %s config: %v", themeDir, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
themeName, err := utils.GjsonGet(desc, "name")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("NEZHA>> Error opening %s config: not a valid description file", theme.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// load templates
|
||||||
|
ret = loadTemplates(ret, themeDir)
|
||||||
|
|
||||||
|
themeKey := strings.TrimPrefix(themeDir, "theme-")
|
||||||
|
model.Themes[themeKey] = themeName.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadTemplates(tmpl *template.Template, themeDir string) *template.Template {
|
||||||
|
// load templates
|
||||||
|
templatePath := filepath.Join("resource", "template", themeDir, "*.html")
|
||||||
|
t, err := tmpl.ParseGlob(templatePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("NEZHA>> Error parsing templates %s: %v", themeDir, err)
|
||||||
|
return tmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
var funcMap = template.FuncMap{
|
var funcMap = template.FuncMap{
|
||||||
"tr": func(id string, dataAndCount ...interface{}) string {
|
"tr": func(id string, dataAndCount ...interface{}) string {
|
||||||
conf := i18n.LocalizeConfig{
|
conf := i18n.LocalizeConfig{
|
||||||
@@ -270,7 +308,7 @@ func natGateway(c *gin.Context) {
|
|||||||
rpc.NezhaHandlerSingleton.CreateStream(streamId)
|
rpc.NezhaHandlerSingleton.CreateStream(streamId)
|
||||||
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
|
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
|
||||||
|
|
||||||
taskData, err := json.Marshal(model.TaskNAT{
|
taskData, err := utils.Json.Marshal(model.TaskNAT{
|
||||||
StreamID: streamId,
|
StreamID: streamId,
|
||||||
Host: natConfig.Host,
|
Host: natConfig.Host,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
|
"golang.org/x/net/idna"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/naiba/nezha/model"
|
"github.com/naiba/nezha/model"
|
||||||
@@ -38,6 +39,7 @@ func (ma *memberAPI) serve() {
|
|||||||
|
|
||||||
mr.GET("/search-server", ma.searchServer)
|
mr.GET("/search-server", ma.searchServer)
|
||||||
mr.GET("/search-tasks", ma.searchTask)
|
mr.GET("/search-tasks", ma.searchTask)
|
||||||
|
mr.GET("/search-ddns", ma.searchDDNS)
|
||||||
mr.POST("/server", ma.addOrEditServer)
|
mr.POST("/server", ma.addOrEditServer)
|
||||||
mr.POST("/monitor", ma.addOrEditMonitor)
|
mr.POST("/monitor", ma.addOrEditMonitor)
|
||||||
mr.POST("/cron", ma.addOrEditCron)
|
mr.POST("/cron", ma.addOrEditCron)
|
||||||
@@ -46,6 +48,7 @@ func (ma *memberAPI) serve() {
|
|||||||
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
|
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
|
||||||
mr.POST("/batch-delete-server", ma.batchDeleteServer)
|
mr.POST("/batch-delete-server", ma.batchDeleteServer)
|
||||||
mr.POST("/notification", ma.addOrEditNotification)
|
mr.POST("/notification", ma.addOrEditNotification)
|
||||||
|
mr.POST("/ddns", ma.addOrEditDDNS)
|
||||||
mr.POST("/nat", ma.addOrEditNAT)
|
mr.POST("/nat", ma.addOrEditNAT)
|
||||||
mr.POST("/alert-rule", ma.addOrEditAlertRule)
|
mr.POST("/alert-rule", ma.addOrEditAlertRule)
|
||||||
mr.POST("/setting", ma.updateSetting)
|
mr.POST("/setting", ma.updateSetting)
|
||||||
@@ -211,6 +214,11 @@ func (ma *memberAPI) delete(c *gin.Context) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
singleton.OnDeleteNotification(id)
|
singleton.OnDeleteNotification(id)
|
||||||
}
|
}
|
||||||
|
case "ddns":
|
||||||
|
err = singleton.DB.Unscoped().Delete(&model.DDNSProfile{}, "id = ?", id).Error
|
||||||
|
if err == nil {
|
||||||
|
singleton.OnDDNSUpdate()
|
||||||
|
}
|
||||||
case "nat":
|
case "nat":
|
||||||
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
|
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -299,19 +307,38 @@ func (ma *memberAPI) searchTask(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ma *memberAPI) searchDDNS(c *gin.Context) {
|
||||||
|
var ddns []model.DDNSProfile
|
||||||
|
likeWord := "%" + c.Query("word") + "%"
|
||||||
|
singleton.DB.Select("id,name").Where("id = ? OR name LIKE ?",
|
||||||
|
c.Query("word"), likeWord).Find(&ddns)
|
||||||
|
|
||||||
|
var resp []searchResult
|
||||||
|
for i := 0; i < len(ddns); i++ {
|
||||||
|
resp = append(resp, searchResult{
|
||||||
|
Value: ddns[i].ID,
|
||||||
|
Name: ddns[i].Name,
|
||||||
|
Text: ddns[i].Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"results": resp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type serverForm struct {
|
type serverForm struct {
|
||||||
ID uint64
|
ID uint64
|
||||||
Name string `binding:"required"`
|
Name string `binding:"required"`
|
||||||
DisplayIndex int
|
DisplayIndex int
|
||||||
Secret string
|
Secret string
|
||||||
Tag string
|
Tag string
|
||||||
Note string
|
Note string
|
||||||
HideForGuest string
|
PublicNote string
|
||||||
EnableDDNS string
|
HideForGuest string
|
||||||
EnableIPv4 string
|
EnableDDNS string
|
||||||
EnableIpv6 string
|
DDNSProfilesRaw string
|
||||||
DDNSDomain string
|
|
||||||
DDNSProfile string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
||||||
@@ -326,20 +353,21 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
|||||||
s.ID = sf.ID
|
s.ID = sf.ID
|
||||||
s.Tag = sf.Tag
|
s.Tag = sf.Tag
|
||||||
s.Note = sf.Note
|
s.Note = sf.Note
|
||||||
|
s.PublicNote = sf.PublicNote
|
||||||
s.HideForGuest = sf.HideForGuest == "on"
|
s.HideForGuest = sf.HideForGuest == "on"
|
||||||
s.EnableDDNS = sf.EnableDDNS == "on"
|
s.EnableDDNS = sf.EnableDDNS == "on"
|
||||||
s.EnableIPv4 = sf.EnableIPv4 == "on"
|
s.DDNSProfilesRaw = sf.DDNSProfilesRaw
|
||||||
s.EnableIpv6 = sf.EnableIpv6 == "on"
|
err = utils.Json.Unmarshal([]byte(sf.DDNSProfilesRaw), &s.DDNSProfiles)
|
||||||
s.DDNSDomain = sf.DDNSDomain
|
if err == nil {
|
||||||
s.DDNSProfile = sf.DDNSProfile
|
if s.ID == 0 {
|
||||||
if s.ID == 0 {
|
s.Secret, err = utils.GenerateRandomString(18)
|
||||||
s.Secret, err = utils.GenerateRandomString(18)
|
if err == nil {
|
||||||
if err == nil {
|
err = singleton.DB.Create(&s).Error
|
||||||
err = singleton.DB.Create(&s).Error
|
}
|
||||||
|
} else {
|
||||||
|
isEdit = true
|
||||||
|
err = singleton.DB.Save(&s).Error
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
isEdit = true
|
|
||||||
err = singleton.DB.Save(&s).Error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -741,6 +769,81 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ddnsForm struct {
|
||||||
|
ID uint64
|
||||||
|
MaxRetries uint64
|
||||||
|
EnableIPv4 string
|
||||||
|
EnableIPv6 string
|
||||||
|
Name string
|
||||||
|
Provider uint8
|
||||||
|
DomainsRaw string
|
||||||
|
AccessID string
|
||||||
|
AccessSecret string
|
||||||
|
WebhookURL string
|
||||||
|
WebhookMethod uint8
|
||||||
|
WebhookRequestType uint8
|
||||||
|
WebhookRequestBody string
|
||||||
|
WebhookHeaders string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ma *memberAPI) addOrEditDDNS(c *gin.Context) {
|
||||||
|
var df ddnsForm
|
||||||
|
var p model.DDNSProfile
|
||||||
|
err := c.ShouldBindJSON(&df)
|
||||||
|
if err == nil {
|
||||||
|
if df.MaxRetries < 1 || df.MaxRetries > 10 {
|
||||||
|
err = errors.New("重试次数必须为大于 1 且不超过 10 的整数")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
p.Name = df.Name
|
||||||
|
p.ID = df.ID
|
||||||
|
enableIPv4 := df.EnableIPv4 == "on"
|
||||||
|
enableIPv6 := df.EnableIPv6 == "on"
|
||||||
|
p.EnableIPv4 = &enableIPv4
|
||||||
|
p.EnableIPv6 = &enableIPv6
|
||||||
|
p.MaxRetries = df.MaxRetries
|
||||||
|
p.Provider = df.Provider
|
||||||
|
p.DomainsRaw = df.DomainsRaw
|
||||||
|
p.Domains = strings.Split(p.DomainsRaw, ",")
|
||||||
|
p.AccessID = df.AccessID
|
||||||
|
p.AccessSecret = df.AccessSecret
|
||||||
|
p.WebhookURL = df.WebhookURL
|
||||||
|
p.WebhookMethod = df.WebhookMethod
|
||||||
|
p.WebhookRequestType = df.WebhookRequestType
|
||||||
|
p.WebhookRequestBody = df.WebhookRequestBody
|
||||||
|
p.WebhookHeaders = df.WebhookHeaders
|
||||||
|
|
||||||
|
for n, domain := range p.Domains {
|
||||||
|
// IDN to ASCII
|
||||||
|
domainValid, domainErr := idna.Lookup.ToASCII(domain)
|
||||||
|
if domainErr != nil {
|
||||||
|
err = fmt.Errorf("域名 %s 解析错误: %v", domain, domainErr)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p.Domains[n] = domainValid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
if p.ID == 0 {
|
||||||
|
err = singleton.DB.Create(&p).Error
|
||||||
|
} else {
|
||||||
|
err = singleton.DB.Save(&p).Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, model.Response{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
Message: fmt.Sprintf("请求错误:%s", err),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
singleton.OnDDNSUpdate()
|
||||||
|
c.JSON(http.StatusOK, model.Response{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type natForm struct {
|
type natForm struct {
|
||||||
ID uint64
|
ID uint64
|
||||||
Name string
|
Name string
|
||||||
@@ -908,6 +1011,8 @@ type settingForm struct {
|
|||||||
Theme string
|
Theme string
|
||||||
DashboardTheme string
|
DashboardTheme string
|
||||||
CustomCode string
|
CustomCode string
|
||||||
|
CustomCodeDashboard string
|
||||||
|
CustomNameservers string
|
||||||
ViewPassword string
|
ViewPassword string
|
||||||
IgnoredIPNotification string
|
IgnoredIPNotification string
|
||||||
IPChangeNotificationTag string // IP变更提醒的通知组
|
IPChangeNotificationTag string // IP变更提醒的通知组
|
||||||
@@ -937,7 +1042,7 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, yes := model.Themes[sf.DashboardTheme]; !yes {
|
if _, yes := model.DashboardThemes[sf.DashboardTheme]; !yes {
|
||||||
c.JSON(http.StatusOK, model.Response{
|
c.JSON(http.StatusOK, model.Response{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Message: fmt.Sprintf("后台主题不存在:%s", sf.DashboardTheme),
|
Message: fmt.Sprintf("后台主题不存在:%s", sf.DashboardTheme),
|
||||||
@@ -973,6 +1078,8 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
|
|||||||
singleton.Conf.Site.Theme = sf.Theme
|
singleton.Conf.Site.Theme = sf.Theme
|
||||||
singleton.Conf.Site.DashboardTheme = sf.DashboardTheme
|
singleton.Conf.Site.DashboardTheme = sf.DashboardTheme
|
||||||
singleton.Conf.Site.CustomCode = sf.CustomCode
|
singleton.Conf.Site.CustomCode = sf.CustomCode
|
||||||
|
singleton.Conf.Site.CustomCodeDashboard = sf.CustomCodeDashboard
|
||||||
|
singleton.Conf.DNSServers = sf.CustomNameservers
|
||||||
singleton.Conf.Site.ViewPassword = sf.ViewPassword
|
singleton.Conf.Site.ViewPassword = sf.ViewPassword
|
||||||
singleton.Conf.Oauth2.Admin = sf.Admin
|
singleton.Conf.Oauth2.Admin = sf.Admin
|
||||||
// 保证NotificationTag不为空
|
// 保证NotificationTag不为空
|
||||||
@@ -988,6 +1095,8 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
// 更新系统语言
|
// 更新系统语言
|
||||||
singleton.InitLocalizer()
|
singleton.InitLocalizer()
|
||||||
|
// 更新DNS服务器
|
||||||
|
singleton.OnNameserverUpdate()
|
||||||
c.JSON(http.StatusOK, model.Response{
|
c.JSON(http.StatusOK, model.Response{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func (mp *memberPage) serve() {
|
|||||||
mr.GET("/monitor", mp.monitor)
|
mr.GET("/monitor", mp.monitor)
|
||||||
mr.GET("/cron", mp.cron)
|
mr.GET("/cron", mp.cron)
|
||||||
mr.GET("/notification", mp.notification)
|
mr.GET("/notification", mp.notification)
|
||||||
|
mr.GET("/ddns", mp.ddns)
|
||||||
mr.GET("/nat", mp.nat)
|
mr.GET("/nat", mp.nat)
|
||||||
mr.GET("/setting", mp.setting)
|
mr.GET("/setting", mp.setting)
|
||||||
mr.GET("/api", mp.api)
|
mr.GET("/api", mp.api)
|
||||||
@@ -78,6 +79,17 @@ func (mp *memberPage) notification(c *gin.Context) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mp *memberPage) ddns(c *gin.Context) {
|
||||||
|
var data []model.DDNSProfile
|
||||||
|
singleton.DB.Find(&data)
|
||||||
|
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/ddns", mygin.CommonEnvironment(c, gin.H{
|
||||||
|
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "DDNS"}),
|
||||||
|
"DDNS": data,
|
||||||
|
"ProviderMap": model.ProviderMap,
|
||||||
|
"ProviderList": model.ProviderList,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
func (mp *memberPage) nat(c *gin.Context) {
|
func (mp *memberPage) nat(c *gin.Context) {
|
||||||
var data []model.NAT
|
var data []model.NAT
|
||||||
singleton.DB.Find(&data)
|
singleton.DB.Find(&data)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -199,7 +198,7 @@ func (oa *oauth2controller) callback(c *gin.Context) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
var cloudflareUserInfo *cloudflare.UserInfo
|
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()
|
user = cloudflareUserInfo.MapToNezhaUser()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-8
@@ -1,10 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
_ "time/tzdata"
|
||||||
|
|
||||||
"github.com/naiba/nezha/cmd/dashboard/controller"
|
"github.com/naiba/nezha/cmd/dashboard/controller"
|
||||||
"github.com/naiba/nezha/cmd/dashboard/rpc"
|
"github.com/naiba/nezha/cmd/dashboard/rpc"
|
||||||
@@ -31,13 +33,6 @@ func init() {
|
|||||||
flag.StringVarP(&dashboardCliParam.ConfigFile, "config", "c", "data/config.yaml", "配置文件路径")
|
flag.StringVarP(&dashboardCliParam.ConfigFile, "config", "c", "data/config.yaml", "配置文件路径")
|
||||||
flag.StringVar(&dashboardCliParam.DatebaseLocation, "db", "data/sqlite.db", "Sqlite3数据库文件路径")
|
flag.StringVar(&dashboardCliParam.DatebaseLocation, "db", "data/sqlite.db", "Sqlite3数据库文件路径")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// 初始化 dao 包
|
|
||||||
singleton.InitConfigFromPath(dashboardCliParam.ConfigFile)
|
|
||||||
singleton.InitTimezoneAndCache()
|
|
||||||
singleton.InitDBFromPath(dashboardCliParam.DatebaseLocation)
|
|
||||||
singleton.InitLocalizer()
|
|
||||||
initSystem()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initSystem() {
|
func initSystem() {
|
||||||
@@ -58,9 +53,16 @@ func initSystem() {
|
|||||||
func main() {
|
func main() {
|
||||||
if dashboardCliParam.Version {
|
if dashboardCliParam.Version {
|
||||||
fmt.Println(singleton.Version)
|
fmt.Println(singleton.Version)
|
||||||
return
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化 dao 包
|
||||||
|
singleton.InitConfigFromPath(dashboardCliParam.ConfigFile)
|
||||||
|
singleton.InitTimezoneAndCache()
|
||||||
|
singleton.InitDBFromPath(dashboardCliParam.DatebaseLocation)
|
||||||
|
singleton.InitLocalizer()
|
||||||
|
initSystem()
|
||||||
|
|
||||||
// TODO 使用 cmux 在同一端口服务 HTTP 和 gRPC
|
// TODO 使用 cmux 在同一端口服务 HTTP 和 gRPC
|
||||||
singleton.CleanMonitorHistory()
|
singleton.CleanMonitorHistory()
|
||||||
go rpc.ServeRPC(singleton.Conf.GRPCPort)
|
go rpc.ServeRPC(singleton.Conf.GRPCPort)
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ require (
|
|||||||
github.com/hashicorp/go-uuid v1.0.3
|
github.com/hashicorp/go-uuid v1.0.3
|
||||||
github.com/jinzhu/copier v0.4.0
|
github.com/jinzhu/copier v0.4.0
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
|
github.com/libdns/cloudflare v0.1.1
|
||||||
|
github.com/libdns/libdns v0.2.2
|
||||||
|
github.com/miekg/dns v1.1.62
|
||||||
|
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||||
github.com/ory/graceful v0.1.3
|
github.com/ory/graceful v0.1.3
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1
|
github.com/oschwald/maxminddb-golang v1.13.1
|
||||||
@@ -21,6 +25,7 @@ require (
|
|||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
|
github.com/tidwall/gjson v1.18.0
|
||||||
github.com/xanzy/go-gitlab v0.103.0
|
github.com/xanzy/go-gitlab v0.103.0
|
||||||
golang.org/x/crypto v0.25.0
|
golang.org/x/crypto v0.25.0
|
||||||
golang.org/x/net v0.27.0
|
golang.org/x/net v0.27.0
|
||||||
@@ -70,14 +75,19 @@ require (
|
|||||||
github.com/spf13/afero v1.11.0 // indirect
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
|
golang.org/x/mod v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
|
golang.org/x/tools v0.22.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
|
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
|
||||||
|
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
|
||||||
|
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||||
|
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
@@ -116,6 +120,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||||
|
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -123,6 +129,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff h1:3WDsbsg3dmsRENYLUPGPTkEcWWmTvk41i+rM91AIIbY=
|
||||||
|
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff/go.mod h1:k+cDtTbUY+UV56Aqv4Ahmc9bWmhpga9JJXZlAIPKBEc=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
|
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/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 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
|
||||||
@@ -180,6 +188,15 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||||
@@ -203,6 +220,8 @@ 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/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 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
|
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||||
|
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
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.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
@@ -232,8 +251,8 @@ 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 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
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.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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 h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
|
||||||
|
|||||||
+8
-32
@@ -80,12 +80,13 @@ type Config struct {
|
|||||||
Debug bool // debug模式开关
|
Debug bool // debug模式开关
|
||||||
Language string // 系统语言,默认 zh-CN
|
Language string // 系统语言,默认 zh-CN
|
||||||
Site struct {
|
Site struct {
|
||||||
Brand string // 站点名称
|
Brand string // 站点名称
|
||||||
CookieName string // 浏览器 Cookie 名称
|
CookieName string // 浏览器 Cookie 名称
|
||||||
Theme string
|
Theme string
|
||||||
DashboardTheme string
|
DashboardTheme string
|
||||||
CustomCode string
|
CustomCode string
|
||||||
ViewPassword string // 前台查看密码
|
CustomCodeDashboard string
|
||||||
|
ViewPassword string // 前台查看密码
|
||||||
}
|
}
|
||||||
Oauth2 struct {
|
Oauth2 struct {
|
||||||
Type string
|
Type string
|
||||||
@@ -126,29 +127,7 @@ type Config struct {
|
|||||||
MaxTCPPingValue int32
|
MaxTCPPingValue int32
|
||||||
AvgPingCount int
|
AvgPingCount int
|
||||||
|
|
||||||
// 动态域名解析更新
|
DNSServers string
|
||||||
DDNS struct {
|
|
||||||
Enable bool
|
|
||||||
Provider string
|
|
||||||
AccessID string
|
|
||||||
AccessSecret string
|
|
||||||
WebhookURL string
|
|
||||||
WebhookMethod string
|
|
||||||
WebhookRequestBody string
|
|
||||||
WebhookHeaders string
|
|
||||||
MaxRetries uint32
|
|
||||||
Profiles map[string]DDNSProfile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type DDNSProfile struct {
|
|
||||||
Provider string
|
|
||||||
AccessID string
|
|
||||||
AccessSecret string
|
|
||||||
WebhookURL string
|
|
||||||
WebhookMethod string
|
|
||||||
WebhookRequestBody string
|
|
||||||
WebhookHeaders string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read 读取配置文件并应用
|
// Read 读取配置文件并应用
|
||||||
@@ -189,9 +168,6 @@ func (c *Config) Read(path string) error {
|
|||||||
if c.AvgPingCount == 0 {
|
if c.AvgPingCount == 0 {
|
||||||
c.AvgPingCount = 2
|
c.AvgPingCount = 2
|
||||||
}
|
}
|
||||||
if c.DDNS.MaxRetries == 0 {
|
|
||||||
c.DDNS.MaxRetries = 3
|
|
||||||
}
|
|
||||||
if c.Oauth2.OidcScopes == "" {
|
if c.Oauth2.OidcScopes == "" {
|
||||||
c.Oauth2.OidcScopes = "openid,profile,email"
|
c.Oauth2.OidcScopes = "openid,profile,email"
|
||||||
}
|
}
|
||||||
|
|||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProviderDummy = iota
|
||||||
|
ProviderWebHook
|
||||||
|
ProviderCloudflare
|
||||||
|
ProviderTencentCloud
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_Dummy = "dummy"
|
||||||
|
_WebHook = "webhook"
|
||||||
|
_Cloudflare = "cloudflare"
|
||||||
|
_TencentCloud = "tencentcloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ProviderMap = map[uint8]string{
|
||||||
|
ProviderDummy: _Dummy,
|
||||||
|
ProviderWebHook: _WebHook,
|
||||||
|
ProviderCloudflare: _Cloudflare,
|
||||||
|
ProviderTencentCloud: _TencentCloud,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProviderList = []DDNSProvider{
|
||||||
|
{
|
||||||
|
Name: _Dummy,
|
||||||
|
ID: ProviderDummy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: _Cloudflare,
|
||||||
|
ID: ProviderCloudflare,
|
||||||
|
AccessSecret: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: _TencentCloud,
|
||||||
|
ID: ProviderTencentCloud,
|
||||||
|
AccessID: true,
|
||||||
|
AccessSecret: true,
|
||||||
|
},
|
||||||
|
// Least frequently used, always place this at the end
|
||||||
|
{
|
||||||
|
Name: _WebHook,
|
||||||
|
ID: ProviderWebHook,
|
||||||
|
AccessID: true,
|
||||||
|
AccessSecret: true,
|
||||||
|
WebhookURL: true,
|
||||||
|
WebhookMethod: true,
|
||||||
|
WebhookRequestType: true,
|
||||||
|
WebhookRequestBody: true,
|
||||||
|
WebhookHeaders: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type DDNSProfile struct {
|
||||||
|
Common
|
||||||
|
EnableIPv4 *bool
|
||||||
|
EnableIPv6 *bool
|
||||||
|
MaxRetries uint64
|
||||||
|
Name string
|
||||||
|
Provider uint8
|
||||||
|
AccessID string
|
||||||
|
AccessSecret string
|
||||||
|
WebhookURL string
|
||||||
|
WebhookMethod uint8
|
||||||
|
WebhookRequestType uint8
|
||||||
|
WebhookRequestBody string
|
||||||
|
WebhookHeaders string
|
||||||
|
|
||||||
|
Domains []string `gorm:"-"`
|
||||||
|
DomainsRaw string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DDNSProfile) TableName() string {
|
||||||
|
return "ddns"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DDNSProfile) AfterFind(tx *gorm.DB) error {
|
||||||
|
if d.DomainsRaw != "" {
|
||||||
|
d.Domains = strings.Split(d.DomainsRaw, ",")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DDNSProvider struct {
|
||||||
|
Name string
|
||||||
|
ID uint8
|
||||||
|
AccessID bool
|
||||||
|
AccessSecret bool
|
||||||
|
WebhookURL bool
|
||||||
|
WebhookMethod bool
|
||||||
|
WebhookRequestType bool
|
||||||
|
WebhookRequestBody bool
|
||||||
|
WebhookHeaders bool
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ const (
|
|||||||
TaskTypeTerminalGRPC
|
TaskTypeTerminalGRPC
|
||||||
TaskTypeNAT
|
TaskTypeNAT
|
||||||
TaskTypeReportHostInfo
|
TaskTypeReportHostInfo
|
||||||
|
TaskTypeFM
|
||||||
)
|
)
|
||||||
|
|
||||||
type TerminalTask struct {
|
type TerminalTask struct {
|
||||||
@@ -34,6 +35,10 @@ type TaskNAT struct {
|
|||||||
Host string
|
Host string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TaskFM struct {
|
||||||
|
StreamID string
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MonitorCoverAll = iota
|
MonitorCoverAll = iota
|
||||||
MonitorCoverIgnoreAll
|
MonitorCoverIgnoreAll
|
||||||
|
|||||||
+13
-4
@@ -71,8 +71,8 @@ func (ns *NotificationServerBundle) reqBody(message string) (string, error) {
|
|||||||
return string(msgBytes)[1 : len(msgBytes)-1]
|
return string(msgBytes)[1 : len(msgBytes)-1]
|
||||||
}), nil
|
}), nil
|
||||||
case NotificationRequestTypeForm:
|
case NotificationRequestTypeForm:
|
||||||
var data map[string]string
|
data, err := utils.GjsonParseStringMap(n.RequestBody)
|
||||||
if err := utils.Json.Unmarshal([]byte(n.RequestBody), &data); err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
@@ -99,8 +99,8 @@ func (n *Notification) setRequestHeader(req *http.Request) error {
|
|||||||
if n.RequestHeader == "" {
|
if n.RequestHeader == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var m map[string]string
|
m, err := utils.GjsonParseStringMap(n.RequestHeader)
|
||||||
if err := utils.Json.Unmarshal([]byte(n.RequestHeader), &m); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
@@ -170,14 +170,23 @@ func (ns *NotificationServerBundle) replaceParamsInString(str string, message st
|
|||||||
|
|
||||||
if ns.Server != nil {
|
if ns.Server != nil {
|
||||||
str = strings.ReplaceAll(str, "#SERVER.NAME#", mod(ns.Server.Name))
|
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.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.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.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.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.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.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.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.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.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.LOAD5#", mod(fmt.Sprintf("%f", ns.Server.State.Load5)))
|
||||||
str = strings.ReplaceAll(str, "#SERVER.LOAD15#", mod(fmt.Sprintf("%f", ns.Server.State.Load15)))
|
str = strings.ReplaceAll(str, "#SERVER.LOAD15#", mod(fmt.Sprintf("%f", ns.Server.State.Load15)))
|
||||||
|
|||||||
+2
-14
@@ -1,6 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -44,19 +45,6 @@ func percentage(used, total uint64) float64 {
|
|||||||
return float64(used) * 100 / float64(total)
|
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
|
// Snapshot 未通过规则返回 struct{}{}, 通过返回 nil
|
||||||
func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) interface{} {
|
func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) interface{} {
|
||||||
// 监控全部但是排除了此服务器
|
// 监控全部但是排除了此服务器
|
||||||
@@ -145,7 +133,7 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
|
|||||||
temp = append(temp, tempStat.Temperature)
|
temp = append(temp, tempStat.Temperature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
src = maxSliceValue(temp)
|
src = slices.Max(temp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-14
@@ -3,26 +3,28 @@ package model
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/naiba/nezha/pkg/utils"
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
pb "github.com/naiba/nezha/proto"
|
pb "github.com/naiba/nezha/proto"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
Common
|
Common
|
||||||
Name string
|
Name string
|
||||||
Tag string // 分组名
|
Tag string // 分组名
|
||||||
Secret string `gorm:"uniqueIndex" json:"-"`
|
Secret string `gorm:"uniqueIndex" json:"-"`
|
||||||
Note string `json:"-"` // 管理员可见备注
|
Note string `json:"-"` // 管理员可见备注
|
||||||
DisplayIndex int // 展示排序,越大越靠前
|
PublicNote string `json:"PublicNote,omitempty"` // 公开备注
|
||||||
HideForGuest bool // 对游客隐藏
|
DisplayIndex int // 展示排序,越大越靠前
|
||||||
EnableDDNS bool `json:"-"` // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效
|
HideForGuest bool // 对游客隐藏
|
||||||
EnableIPv4 bool `json:"-"` // 是否启用DDNS IPv4
|
EnableDDNS bool // 启用DDNS
|
||||||
EnableIpv6 bool `json:"-"` // 是否启用DDNS IPv6
|
DDNSProfiles []uint64 `gorm:"-" json:"-"` // DDNS配置
|
||||||
DDNSDomain string `json:"-"` // DDNS中的前缀 如基础域名为abc.oracle DDNSName为mjj 就会把mjj.abc.oracle解析服务器IP 为空则停用
|
|
||||||
DDNSProfile string `json:"-"` // DDNS配置
|
DDNSProfilesRaw string `gorm:"default:'[]';column:ddns_profiles_raw" json:"-"`
|
||||||
|
|
||||||
Host *Host `gorm:"-"`
|
Host *Host `gorm:"-"`
|
||||||
State *HostState `gorm:"-"`
|
State *HostState `gorm:"-"`
|
||||||
@@ -47,6 +49,16 @@ func (s *Server) CopyFromRunningServer(old *Server) {
|
|||||||
s.PrevTransferOutSnapshot = old.PrevTransferOutSnapshot
|
s.PrevTransferOutSnapshot = old.PrevTransferOutSnapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) AfterFind(tx *gorm.DB) error {
|
||||||
|
if s.DDNSProfilesRaw != "" {
|
||||||
|
if err := utils.Json.Unmarshal([]byte(s.DDNSProfilesRaw), &s.DDNSProfiles); err != nil {
|
||||||
|
log.Println("NEZHA>> Server.AfterFind:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func boolToString(b bool) string {
|
func boolToString(b bool) string {
|
||||||
if b {
|
if b {
|
||||||
return "true"
|
return "true"
|
||||||
@@ -54,12 +66,12 @@ func boolToString(b bool) string {
|
|||||||
return "false"
|
return "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Server) Marshal() template.JS {
|
func (s Server) MarshalForDashboard() template.JS {
|
||||||
name, _ := utils.Json.Marshal(s.Name)
|
name, _ := utils.Json.Marshal(s.Name)
|
||||||
tag, _ := utils.Json.Marshal(s.Tag)
|
tag, _ := utils.Json.Marshal(s.Tag)
|
||||||
note, _ := utils.Json.Marshal(s.Note)
|
note, _ := utils.Json.Marshal(s.Note)
|
||||||
secret, _ := utils.Json.Marshal(s.Secret)
|
secret, _ := utils.Json.Marshal(s.Secret)
|
||||||
ddnsDomain, _ := utils.Json.Marshal(s.DDNSDomain)
|
ddnsProfilesRaw, _ := utils.Json.Marshal(s.DDNSProfilesRaw)
|
||||||
ddnsProfile, _ := utils.Json.Marshal(s.DDNSProfile)
|
publicNote, _ := utils.Json.Marshal(s.PublicNote)
|
||||||
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"EnableIPv4": %s,"EnableIpv6": %s,"DDNSDomain": %s,"DDNSProfile": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), boolToString(s.EnableIPv4), boolToString(s.EnableIpv6), ddnsDomain, ddnsProfile)) // #nosec
|
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"DDNSProfilesRaw": %s,"PublicNote": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), ddnsProfilesRaw, publicNote))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func TestServerMarshal(t *testing.T) {
|
|||||||
Name: patterns[i],
|
Name: patterns[i],
|
||||||
Tag: patterns[i],
|
Tag: patterns[i],
|
||||||
}
|
}
|
||||||
serverStr := string(server.Marshal())
|
serverStr := string(server.MarshalForDashboard())
|
||||||
var serverRestore Server
|
var serverRestore Server
|
||||||
if utils.Json.Unmarshal([]byte(serverStr), &serverRestore) != nil {
|
if utils.Json.Unmarshal([]byte(serverStr), &serverRestore) != nil {
|
||||||
t.Fatalf("Error: %s", serverStr)
|
t.Fatalf("Error: %s", serverStr)
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
package ddns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProviderCloudflare struct {
|
|
||||||
Secret string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool {
|
|
||||||
if domainConfig == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
zoneID, err := provider.getZoneID(domainConfig.FullDomain)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("无法获取 zone ID: %s\n", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当IPv4和IPv6同时成功才算作成功
|
|
||||||
var resultV4 = true
|
|
||||||
var resultV6 = true
|
|
||||||
if domainConfig.EnableIPv4 {
|
|
||||||
if !provider.addDomainRecord(zoneID, domainConfig, true) {
|
|
||||||
resultV4 = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if domainConfig.EnableIpv6 {
|
|
||||||
if !provider.addDomainRecord(zoneID, domainConfig, false) {
|
|
||||||
resultV6 = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultV4 && resultV6
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *DomainConfig, isIpv4 bool) bool {
|
|
||||||
record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain, isIpv4)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("查找 DNS 记录时出错: %s\n", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if record == nil {
|
|
||||||
// 添加 DNS 记录
|
|
||||||
return provider.createDNSRecord(zoneID, domainConfig, isIpv4)
|
|
||||||
} else {
|
|
||||||
// 更新 DNS 记录
|
|
||||||
return provider.updateDNSRecord(zoneID, record["id"].(string), domainConfig, 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)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := res["result"].([]interface{})
|
|
||||||
if len(result) > 0 {
|
|
||||||
zoneID := result[0].(map[string]interface{})["id"].(string)
|
|
||||||
return zoneID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("找不到 Zone ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) {
|
|
||||||
var ipType = "A"
|
|
||||||
if !isIPv4 {
|
|
||||||
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)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := res["result"].([]interface{})
|
|
||||||
if len(result) > 0 {
|
|
||||||
return result[0].(map[string]interface{}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil // 没有找到 DNS 记录
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
|
||||||
var ipType = "A"
|
|
||||||
var ipAddr = domainConfig.Ipv4Addr
|
|
||||||
if !isIPv4 {
|
|
||||||
ipType = "AAAA"
|
|
||||||
ipAddr = 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,
|
|
||||||
}
|
|
||||||
jsonData, _ := json.Marshal(data)
|
|
||||||
_, err := provider.sendRequest("POST", url, jsonData)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
|
||||||
var ipType = "A"
|
|
||||||
var ipAddr = domainConfig.Ipv4Addr
|
|
||||||
if !isIPv4 {
|
|
||||||
ipType = "AAAA"
|
|
||||||
ipAddr = 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,
|
|
||||||
}
|
|
||||||
jsonData, _ := json.Marshal(data)
|
|
||||||
_, err := provider.sendRequest("PATCH", url, jsonData)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 以下为辅助方法,如发送 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("Content-Type", "application/json")
|
|
||||||
|
|
||||||
resp, err := client.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())
|
|
||||||
}
|
|
||||||
}(resp.Body)
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
+136
-9
@@ -1,14 +1,141 @@
|
|||||||
package ddns
|
package ddns
|
||||||
|
|
||||||
type DomainConfig struct {
|
import (
|
||||||
EnableIPv4 bool
|
"context"
|
||||||
EnableIpv6 bool
|
"fmt"
|
||||||
FullDomain string
|
"log"
|
||||||
Ipv4Addr string
|
"strings"
|
||||||
Ipv6Addr string
|
"time"
|
||||||
|
|
||||||
|
"github.com/libdns/libdns"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
|
"github.com/naiba/nezha/model"
|
||||||
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dnsTimeOut = 10 * time.Second
|
||||||
|
customDNSServers []string
|
||||||
|
)
|
||||||
|
|
||||||
|
type IP struct {
|
||||||
|
Ipv4Addr string
|
||||||
|
Ipv6Addr string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Provider interface {
|
type Provider struct {
|
||||||
// UpdateDomain Return is updated
|
ctx context.Context
|
||||||
UpdateDomain(domainConfig *DomainConfig) bool
|
ipAddr string
|
||||||
|
recordType string
|
||||||
|
domain string
|
||||||
|
prefix string
|
||||||
|
zone string
|
||||||
|
|
||||||
|
DDNSProfile *model.DDNSProfile
|
||||||
|
IPAddrs *IP
|
||||||
|
Setter libdns.RecordSetter
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitDNSServers(s string) {
|
||||||
|
if s != "" {
|
||||||
|
customDNSServers = strings.Split(s, ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) UpdateDomain(ctx context.Context) {
|
||||||
|
provider.ctx = ctx
|
||||||
|
for _, domain := range provider.DDNSProfile.Domains {
|
||||||
|
for retries := 0; retries < int(provider.DDNSProfile.MaxRetries); retries++ {
|
||||||
|
provider.domain = domain
|
||||||
|
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)", provider.domain, retries+1, provider.DDNSProfile.MaxRetries)
|
||||||
|
if err := provider.updateDomain(); err != nil {
|
||||||
|
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败: %v", provider.domain, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功", provider.domain)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) updateDomain() error {
|
||||||
|
var err error
|
||||||
|
provider.prefix, provider.zone, err = splitDomainSOA(provider.domain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当IPv4和IPv6同时成功才算作成功
|
||||||
|
if *provider.DDNSProfile.EnableIPv4 {
|
||||||
|
provider.recordType = getRecordString(true)
|
||||||
|
provider.ipAddr = provider.IPAddrs.Ipv4Addr
|
||||||
|
if err = provider.addDomainRecord(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *provider.DDNSProfile.EnableIPv6 {
|
||||||
|
provider.recordType = getRecordString(false)
|
||||||
|
provider.ipAddr = provider.IPAddrs.Ipv6Addr
|
||||||
|
if err = provider.addDomainRecord(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) addDomainRecord() error {
|
||||||
|
_, err := provider.Setter.SetRecords(provider.ctx, provider.zone,
|
||||||
|
[]libdns.Record{
|
||||||
|
{
|
||||||
|
Type: provider.recordType,
|
||||||
|
Name: provider.prefix,
|
||||||
|
Value: provider.ipAddr,
|
||||||
|
TTL: time.Minute,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitDomainSOA(domain string) (prefix string, zone string, err error) {
|
||||||
|
c := &dns.Client{Timeout: dnsTimeOut}
|
||||||
|
|
||||||
|
domain += "."
|
||||||
|
indexes := dns.Split(domain)
|
||||||
|
|
||||||
|
servers := utils.DNSServers
|
||||||
|
if len(customDNSServers) > 0 {
|
||||||
|
servers = customDNSServers
|
||||||
|
}
|
||||||
|
|
||||||
|
var r *dns.Msg
|
||||||
|
for _, idx := range indexes {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(domain[idx:], dns.TypeSOA)
|
||||||
|
|
||||||
|
for _, server := range servers {
|
||||||
|
r, _, err = c.Exchange(m, server)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(r.Answer) > 0 {
|
||||||
|
if soa, ok := r.Answer[0].(*dns.SOA); ok {
|
||||||
|
zone = soa.Hdr.Name
|
||||||
|
prefix = libdns.RelativeName(domain, zone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", fmt.Errorf("SOA record not found for domain: %s", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRecordString(isIpv4 bool) string {
|
||||||
|
if isIpv4 {
|
||||||
|
return "A"
|
||||||
|
}
|
||||||
|
return "AAAA"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package ddns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testSt struct {
|
||||||
|
domain string
|
||||||
|
zone string
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitDomainSOA(t *testing.T) {
|
||||||
|
if ci := os.Getenv("CI"); ci != "" { // skip if test on CI
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []testSt{
|
||||||
|
{
|
||||||
|
domain: "www.example.co.uk",
|
||||||
|
zone: "example.co.uk.",
|
||||||
|
prefix: "www",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: "abc.example.com",
|
||||||
|
zone: "example.com.",
|
||||||
|
prefix: "abc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: "example.com",
|
||||||
|
zone: "example.com.",
|
||||||
|
prefix: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
prefix, zone, err := splitDomainSOA(c.domain)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %s", err)
|
||||||
|
}
|
||||||
|
if prefix != c.prefix {
|
||||||
|
t.Fatalf("Expected prefix %s, but got %s", c.prefix, prefix)
|
||||||
|
}
|
||||||
|
if zone != c.zone {
|
||||||
|
t.Fatalf("Expected zone %s, but got %s", c.zone, zone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package ddns
|
|
||||||
|
|
||||||
type ProviderDummy struct{}
|
|
||||||
|
|
||||||
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package dummy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/libdns/libdns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Internal use
|
||||||
|
type Provider struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) SetRecords(ctx context.Context, zone string,
|
||||||
|
recs []libdns.Record) ([]libdns.Record, error) {
|
||||||
|
return recs, nil
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
package ddns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
url = "https://dnspod.tencentcloudapi.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProviderTencentCloud struct {
|
|
||||||
SecretID string
|
|
||||||
SecretKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bool {
|
|
||||||
if domainConfig == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当IPv4和IPv6同时成功才算作成功
|
|
||||||
var resultV4 = true
|
|
||||||
var resultV6 = true
|
|
||||||
if domainConfig.EnableIPv4 {
|
|
||||||
if !provider.addDomainRecord(domainConfig, true) {
|
|
||||||
resultV4 = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if domainConfig.EnableIpv6 {
|
|
||||||
if !provider.addDomainRecord(domainConfig, false) {
|
|
||||||
resultV6 = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultV4 && resultV6
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig, isIpv4 bool) bool {
|
|
||||||
record, err := provider.findDNSRecord(domainConfig.FullDomain, isIpv4)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("查找 DNS 记录时出错: %s\n", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认情况下更新 DNS 记录
|
|
||||||
return provider.updateDNSRecord(domainConfig.FullDomain, record["RecordList"].([]interface{})[0].(map[string]interface{})["RecordId"].(float64), domainConfig, isIpv4)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (map[string]interface{}, error) {
|
|
||||||
var ipType = "A"
|
|
||||||
if !isIPv4 {
|
|
||||||
ipType = "AAAA"
|
|
||||||
}
|
|
||||||
_, realDomain := SplitDomain(domain)
|
|
||||||
prefix, _ := SplitDomain(domain)
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"RecordType": ipType,
|
|
||||||
"Domain": realDomain,
|
|
||||||
"RecordLine": "默认",
|
|
||||||
"Subdomain": prefix,
|
|
||||||
}
|
|
||||||
jsonData, _ := json.Marshal(data)
|
|
||||||
body, err := provider.sendRequest("DescribeRecordList", jsonData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := res["Response"].(map[string]interface{})
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) createDNSRecord(domain string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
|
||||||
var ipType = "A"
|
|
||||||
var ipAddr = domainConfig.Ipv4Addr
|
|
||||||
if !isIPv4 {
|
|
||||||
ipType = "AAAA"
|
|
||||||
ipAddr = domainConfig.Ipv6Addr
|
|
||||||
}
|
|
||||||
_, realDomain := SplitDomain(domain)
|
|
||||||
prefix, _ := SplitDomain(domain)
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"RecordType": ipType,
|
|
||||||
"RecordLine": "默认",
|
|
||||||
"Domain": realDomain,
|
|
||||||
"SubDomain": prefix,
|
|
||||||
"Value": ipAddr,
|
|
||||||
"TTL": 600,
|
|
||||||
}
|
|
||||||
jsonData, _ := json.Marshal(data)
|
|
||||||
_, err := provider.sendRequest("CreateRecord", jsonData)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) updateDNSRecord(domain string, recordID float64, domainConfig *DomainConfig, isIPv4 bool) bool {
|
|
||||||
var ipType = "A"
|
|
||||||
var ipAddr = domainConfig.Ipv4Addr
|
|
||||||
if !isIPv4 {
|
|
||||||
ipType = "AAAA"
|
|
||||||
ipAddr = 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,
|
|
||||||
}
|
|
||||||
jsonData, _ := json.Marshal(data)
|
|
||||||
_, err := provider.sendRequest("ModifyRecord", jsonData)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 以下为辅助方法,如发送 HTTP 请求等
|
|
||||||
func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
|
|
||||||
client := &http.Client{}
|
|
||||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}(resp.Body)
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/jeessy2/ddns-go/blob/master/util/tencent_cloud_signer.go
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) sha256hex(s string) string {
|
|
||||||
b := sha256.Sum256([]byte(s))
|
|
||||||
return hex.EncodeToString(b[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) hmacsha256(s, key string) string {
|
|
||||||
hashed := hmac.New(sha256.New, []byte(key))
|
|
||||||
hashed.Write([]byte(s))
|
|
||||||
return string(hashed.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) WriteString(strs ...string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
for _, str := range strs {
|
|
||||||
b.WriteString(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) signRequest(secretId string, secretKey string, r *http.Request, action string, payload string) {
|
|
||||||
algorithm := "TC3-HMAC-SHA256"
|
|
||||||
service := "dnspod"
|
|
||||||
host := provider.WriteString(service, ".tencentcloudapi.com")
|
|
||||||
timestamp := time.Now().Unix()
|
|
||||||
timestampStr := strconv.FormatInt(timestamp, 10)
|
|
||||||
|
|
||||||
// 步骤 1:拼接规范请求串
|
|
||||||
canonicalHeaders := provider.WriteString("content-type:application/json\nhost:", host, "\nx-tc-action:", strings.ToLower(action), "\n")
|
|
||||||
signedHeaders := "content-type;host;x-tc-action"
|
|
||||||
hashedRequestPayload := provider.sha256hex(payload)
|
|
||||||
canonicalRequest := provider.WriteString("POST\n/\n\n", canonicalHeaders, "\n", signedHeaders, "\n", hashedRequestPayload)
|
|
||||||
|
|
||||||
// 步骤 2:拼接待签名字符串
|
|
||||||
date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
|
|
||||||
credentialScope := provider.WriteString(date, "/", service, "/tc3_request")
|
|
||||||
hashedCanonicalRequest := provider.sha256hex(canonicalRequest)
|
|
||||||
string2sign := provider.WriteString(algorithm, "\n", timestampStr, "\n", credentialScope, "\n", hashedCanonicalRequest)
|
|
||||||
|
|
||||||
// 步骤 3:计算签名
|
|
||||||
secretDate := provider.hmacsha256(date, provider.WriteString("TC3", secretKey))
|
|
||||||
secretService := provider.hmacsha256(service, secretDate)
|
|
||||||
secretSigning := provider.hmacsha256("tc3_request", secretService)
|
|
||||||
signature := hex.EncodeToString([]byte(provider.hmacsha256(string2sign, secretSigning)))
|
|
||||||
|
|
||||||
// 步骤 4:拼接 Authorization
|
|
||||||
authorization := provider.WriteString(algorithm, " Credential=", secretId, "/", credentialScope, ", SignedHeaders=", signedHeaders, ", Signature=", signature)
|
|
||||||
|
|
||||||
r.Header.Add("Authorization", authorization)
|
|
||||||
r.Header.Set("Host", host)
|
|
||||||
r.Header.Set("X-TC-Action", action)
|
|
||||||
r.Header.Add("X-TC-Timestamp", timestampStr)
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package ddns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProviderWebHook struct {
|
|
||||||
URL string
|
|
||||||
RequestMethod string
|
|
||||||
RequestBody string
|
|
||||||
RequestHeader string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool {
|
|
||||||
if domainConfig == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updated
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/libdns/libdns"
|
||||||
|
"github.com/naiba/nezha/model"
|
||||||
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ = iota
|
||||||
|
methodGET
|
||||||
|
methodPOST
|
||||||
|
methodPATCH
|
||||||
|
methodDELETE
|
||||||
|
methodPUT
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ = iota
|
||||||
|
requestTypeJSON
|
||||||
|
requestTypeForm
|
||||||
|
)
|
||||||
|
|
||||||
|
var requestTypes = map[uint8]string{
|
||||||
|
methodGET: "GET",
|
||||||
|
methodPOST: "POST",
|
||||||
|
methodPATCH: "PATCH",
|
||||||
|
methodDELETE: "DELETE",
|
||||||
|
methodPUT: "PUT",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal use
|
||||||
|
type Provider struct {
|
||||||
|
ipAddr string
|
||||||
|
ipType string
|
||||||
|
recordType string
|
||||||
|
domain string
|
||||||
|
|
||||||
|
DDNSProfile *model.DDNSProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) SetRecords(ctx context.Context, zone string,
|
||||||
|
recs []libdns.Record) ([]libdns.Record, error) {
|
||||||
|
for _, rec := range recs {
|
||||||
|
provider.recordType = rec.Type
|
||||||
|
provider.ipType = recordToIPType(provider.recordType)
|
||||||
|
provider.ipAddr = rec.Value
|
||||||
|
provider.domain = fmt.Sprintf("%s.%s", rec.Name, strings.TrimSuffix(zone, "."))
|
||||||
|
|
||||||
|
req, err := provider.prepareRequest(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domain, err)
|
||||||
|
}
|
||||||
|
if _, err := utils.HttpClient.Do(req); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) prepareRequest(ctx context.Context) (*http.Request, error) {
|
||||||
|
u, err := provider.reqUrl()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := provider.reqBody()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
headers, err := utils.GjsonParseStringMap(
|
||||||
|
provider.formatWebhookString(provider.DDNSProfile.WebhookHeaders))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, requestTypes[provider.DDNSProfile.WebhookMethod], u.String(), strings.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.setContentType(req)
|
||||||
|
|
||||||
|
for k, v := range headers {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) setContentType(req *http.Request) {
|
||||||
|
if provider.DDNSProfile.WebhookMethod == methodGET {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if provider.DDNSProfile.WebhookRequestType == requestTypeForm {
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
} else {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) reqUrl() (*url.URL, error) {
|
||||||
|
formattedUrl := strings.ReplaceAll(provider.DDNSProfile.WebhookURL, "#", "%23")
|
||||||
|
|
||||||
|
u, err := url.Parse(formattedUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only handle queries here
|
||||||
|
q := u.Query()
|
||||||
|
for p, vals := range q {
|
||||||
|
for n, v := range vals {
|
||||||
|
vals[n] = provider.formatWebhookString(v)
|
||||||
|
}
|
||||||
|
q[p] = vals
|
||||||
|
}
|
||||||
|
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) reqBody() (string, error) {
|
||||||
|
if provider.DDNSProfile.WebhookMethod == methodGET ||
|
||||||
|
provider.DDNSProfile.WebhookMethod == methodDELETE {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch provider.DDNSProfile.WebhookRequestType {
|
||||||
|
case requestTypeJSON:
|
||||||
|
return provider.formatWebhookString(provider.DDNSProfile.WebhookRequestBody), nil
|
||||||
|
case requestTypeForm:
|
||||||
|
data, err := utils.GjsonParseStringMap(provider.DDNSProfile.WebhookRequestBody)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
params := url.Values{}
|
||||||
|
for k, v := range data {
|
||||||
|
params.Add(k, provider.formatWebhookString(v))
|
||||||
|
}
|
||||||
|
return params.Encode(), nil
|
||||||
|
default:
|
||||||
|
return "", errors.New("request type not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) formatWebhookString(s string) string {
|
||||||
|
r := strings.NewReplacer(
|
||||||
|
"#ip#", provider.ipAddr,
|
||||||
|
"#domain#", provider.domain,
|
||||||
|
"#type#", provider.ipType,
|
||||||
|
"#record#", provider.recordType,
|
||||||
|
"#access_id#", provider.DDNSProfile.AccessID,
|
||||||
|
"#access_secret#", provider.DDNSProfile.AccessSecret,
|
||||||
|
"\r", "",
|
||||||
|
)
|
||||||
|
|
||||||
|
result := r.Replace(strings.TrimSpace(s))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordToIPType(record string) string {
|
||||||
|
switch record {
|
||||||
|
case "A":
|
||||||
|
return "ipv4"
|
||||||
|
case "AAAA":
|
||||||
|
return "ipv6"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/naiba/nezha/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
reqTypeForm = "application/x-www-form-urlencoded"
|
||||||
|
reqTypeJSON = "application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testSt struct {
|
||||||
|
profile model.DDNSProfile
|
||||||
|
expectURL string
|
||||||
|
expectBody string
|
||||||
|
expectContentType string
|
||||||
|
expectHeader map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func execCase(t *testing.T, item testSt) {
|
||||||
|
pw := Provider{DDNSProfile: &item.profile}
|
||||||
|
pw.ipAddr = "1.1.1.1"
|
||||||
|
pw.domain = item.profile.Domains[0]
|
||||||
|
pw.ipType = "ipv4"
|
||||||
|
pw.recordType = "A"
|
||||||
|
pw.DDNSProfile = &item.profile
|
||||||
|
|
||||||
|
reqUrl, err := pw.reqUrl()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %s", err)
|
||||||
|
}
|
||||||
|
if item.expectURL != reqUrl.String() {
|
||||||
|
t.Fatalf("Expected %s, but got %s", item.expectURL, reqUrl.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody, err := pw.reqBody()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %s", err)
|
||||||
|
}
|
||||||
|
if item.expectBody != reqBody {
|
||||||
|
t.Fatalf("Expected %s, but got %s", item.expectBody, reqBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := pw.prepareRequest(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.expectContentType != req.Header.Get("Content-Type") {
|
||||||
|
t.Fatalf("Expected %s, but got %s", item.expectContentType, req.Header.Get("Content-Type"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range item.expectHeader {
|
||||||
|
if v != req.Header.Get(k) {
|
||||||
|
t.Fatalf("Expected %s, but got %s", v, req.Header.Get(k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebhookRequest(t *testing.T) {
|
||||||
|
ipv4 := true
|
||||||
|
|
||||||
|
cases := []testSt{
|
||||||
|
{
|
||||||
|
profile: model.DDNSProfile{
|
||||||
|
Domains: []string{"www.example.com"},
|
||||||
|
MaxRetries: 1,
|
||||||
|
EnableIPv4: &ipv4,
|
||||||
|
WebhookURL: "http://ddns.example.com/?ip=#ip#",
|
||||||
|
WebhookMethod: methodGET,
|
||||||
|
WebhookHeaders: `{"ip":"#ip#","record":"#record#"}`,
|
||||||
|
},
|
||||||
|
expectURL: "http://ddns.example.com/?ip=1.1.1.1",
|
||||||
|
expectContentType: "",
|
||||||
|
expectHeader: map[string]string{
|
||||||
|
"ip": "1.1.1.1",
|
||||||
|
"record": "A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
profile: model.DDNSProfile{
|
||||||
|
Domains: []string{"www.example.com"},
|
||||||
|
MaxRetries: 1,
|
||||||
|
EnableIPv4: &ipv4,
|
||||||
|
WebhookURL: "http://ddns.example.com/api",
|
||||||
|
WebhookMethod: methodPOST,
|
||||||
|
WebhookRequestType: requestTypeJSON,
|
||||||
|
WebhookRequestBody: `{"ip":"#ip#","record":"#record#"}`,
|
||||||
|
},
|
||||||
|
expectURL: "http://ddns.example.com/api",
|
||||||
|
expectContentType: reqTypeJSON,
|
||||||
|
expectBody: `{"ip":"1.1.1.1","record":"A"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
profile: model.DDNSProfile{
|
||||||
|
Domains: []string{"www.example.com"},
|
||||||
|
MaxRetries: 1,
|
||||||
|
EnableIPv4: &ipv4,
|
||||||
|
WebhookURL: "http://ddns.example.com/api",
|
||||||
|
WebhookMethod: methodPOST,
|
||||||
|
WebhookRequestType: requestTypeForm,
|
||||||
|
WebhookRequestBody: `{"ip":"#ip#","record":"#record#"}`,
|
||||||
|
},
|
||||||
|
expectURL: "http://ddns.example.com/api",
|
||||||
|
expectContentType: reqTypeForm,
|
||||||
|
expectBody: "ip=1.1.1.1&record=A",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
execCase(t, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ var adminPage = map[string]bool{
|
|||||||
"/monitor": true,
|
"/monitor": true,
|
||||||
"/setting": true,
|
"/setting": true,
|
||||||
"/notification": true,
|
"/notification": true,
|
||||||
|
"/ddns": true,
|
||||||
"/nat": true,
|
"/nat": true,
|
||||||
"/cron": true,
|
"/cron": true,
|
||||||
"/api": true,
|
"/api": true,
|
||||||
@@ -26,6 +27,8 @@ func CommonEnvironment(c *gin.Context, data map[string]interface{}) gin.H {
|
|||||||
data["Version"] = singleton.Version
|
data["Version"] = singleton.Version
|
||||||
data["Conf"] = singleton.Conf
|
data["Conf"] = singleton.Conf
|
||||||
data["Themes"] = model.Themes
|
data["Themes"] = model.Themes
|
||||||
|
data["CustomCode"] = singleton.Conf.Site.CustomCode
|
||||||
|
data["CustomCodeDashboard"] = singleton.Conf.Site.CustomCodeDashboard
|
||||||
// 是否是管理页面
|
// 是否是管理页面
|
||||||
data["IsAdminPage"] = adminPage[data["MatchedPath"].(string)]
|
data["IsAdminPage"] = adminPage[data["MatchedPath"].(string)]
|
||||||
// 站点标题
|
// 站点标题
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ func ValidateViewPassword(opt ValidateViewPasswordOption) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
if opt.IsPage {
|
if opt.IsPage {
|
||||||
c.HTML(http.StatusOK, GetPreferredTheme(c, "/viewpassword"), CommonEnvironment(c, gin.H{
|
c.HTML(http.StatusOK, GetPreferredTheme(c, "/viewpassword"), CommonEnvironment(c, gin.H{
|
||||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
|
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
|
||||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrGjsonNotFound = errors.New("specified path does not exist")
|
||||||
|
ErrGjsonWrongType = errors.New("wrong type")
|
||||||
|
)
|
||||||
|
|
||||||
|
func GjsonGet(json []byte, path string) (gjson.Result, error) {
|
||||||
|
result := gjson.GetBytes(json, path)
|
||||||
|
if !result.Exists() {
|
||||||
|
return result, ErrGjsonNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GjsonParseStringMap(jsonObject string) (map[string]string, error) {
|
||||||
|
if jsonObject == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := gjson.Parse(jsonObject)
|
||||||
|
if !result.IsObject() {
|
||||||
|
return nil, ErrGjsonWrongType
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make(map[string]string)
|
||||||
|
result.ForEach(func(key, value gjson.Result) bool {
|
||||||
|
ret[key.String()] = value.String()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HybridFS combines embed.FS and os.DirFS.
|
||||||
|
type HybridFS struct {
|
||||||
|
embedFS, dir fs.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHybridFS(embed embed.FS, subDir string, localDir string) (*HybridFS, error) {
|
||||||
|
subFS, err := fs.Sub(embed, subDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HybridFS{
|
||||||
|
embedFS: subFS,
|
||||||
|
dir: os.DirFS(localDir),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hfs *HybridFS) Open(name string) (fs.File, error) {
|
||||||
|
// Ensure embed files are not replaced
|
||||||
|
if file, err := hfs.embedFS.Open(name); err == nil {
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return hfs.dir.Open(name)
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ func init() {
|
|||||||
SkipVerifySSL: false,
|
SkipVerifySSL: false,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
http.DefaultClient.Timeout = time.Minute * 10
|
||||||
}
|
}
|
||||||
|
|
||||||
type _httpTransport struct {
|
type _httpTransport struct {
|
||||||
|
|||||||
+5
-1
@@ -10,7 +10,11 @@ import (
|
|||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Json = jsoniter.ConfigCompatibleWithStandardLibrary
|
var (
|
||||||
|
Json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
|
||||||
|
DNSServers = []string{"1.1.1.1:53", "223.5.5.5:53"}
|
||||||
|
)
|
||||||
|
|
||||||
func IsWindows() bool {
|
func IsWindows() bool {
|
||||||
return os.PathSeparator == '\\' && os.PathListSeparator == ';'
|
return os.PathSeparator == '\\' && os.PathListSeparator == ';'
|
||||||
|
|||||||
Vendored
+139
-40
@@ -23,19 +23,19 @@ other = "Home"
|
|||||||
other = "Back to Homepage"
|
other = "Back to Homepage"
|
||||||
|
|
||||||
[AdminPanel]
|
[AdminPanel]
|
||||||
other = "Admin Panel"
|
other = "Dashboard"
|
||||||
|
|
||||||
[Logout]
|
[Logout]
|
||||||
other = "Logout"
|
other = "Log out"
|
||||||
|
|
||||||
[Login]
|
[Login]
|
||||||
other = "Login"
|
other = "Log in"
|
||||||
|
|
||||||
[ConfirmLogout]
|
[ConfirmLogout]
|
||||||
other = "Do you want to Logout?"
|
other = "Confirm Logging Out?"
|
||||||
|
|
||||||
[AfterLoggingOutYouHaveToLoginAgain]
|
[AfterLoggingOutYouHaveToLoginAgain]
|
||||||
other = "You need to Login again to continue"
|
other = "You will need to log in again to continue"
|
||||||
|
|
||||||
[Cancel]
|
[Cancel]
|
||||||
other = "Cancel"
|
other = "Cancel"
|
||||||
@@ -56,7 +56,7 @@ other = "Cron Expression"
|
|||||||
other = "Backup"
|
other = "Backup"
|
||||||
|
|
||||||
[3amDaily]
|
[3amDaily]
|
||||||
other = "(Daily at 3 AM)"
|
other = "(At 3 AM)"
|
||||||
|
|
||||||
[Command]
|
[Command]
|
||||||
other = "Command"
|
other = "Command"
|
||||||
@@ -71,7 +71,7 @@ other = "Ignore All, Execute Only on Specific Servers"
|
|||||||
other = "Cover All, Except Specific Servers"
|
other = "Cover All, Except Specific Servers"
|
||||||
|
|
||||||
[ExecuteByTriggerServer]
|
[ExecuteByTriggerServer]
|
||||||
other = "Execute on Trigger Server"
|
other = "Execute on Alarmed Servers"
|
||||||
|
|
||||||
[SpecificServers]
|
[SpecificServers]
|
||||||
other = "Specific Servers"
|
other = "Specific Servers"
|
||||||
@@ -95,7 +95,7 @@ other = "Cron Task"
|
|||||||
other = "Trigger Task"
|
other = "Trigger Task"
|
||||||
|
|
||||||
[TheFormaOfTheScheduleIs]
|
[TheFormaOfTheScheduleIs]
|
||||||
other = "The Cron Expression is like:"
|
other = "The Cron Expression format is:"
|
||||||
|
|
||||||
[SecondsMinutesHoursDaysMonthsWeeksSeeDetails]
|
[SecondsMinutesHoursDaysMonthsWeeksSeeDetails]
|
||||||
other = "Sec Min Hour Day Month Week, see details in "
|
other = "Sec Min Hour Day Month Week, see details in "
|
||||||
@@ -104,7 +104,7 @@ other = "Sec Min Hour Day Month Week, see details in "
|
|||||||
other = "CRON Expression Format"
|
other = "CRON Expression Format"
|
||||||
|
|
||||||
[IntroductionOfCommands]
|
[IntroductionOfCommands]
|
||||||
other = "Introduction of Commands: Writing commands is similar to shell/bat scripts. It's advised not to break lines, and multiple commands can be connected with <code>&&</code> or <code>&</code>. If a command cannot be found, it might be due to <code>PATH</code> environment variable configuration issues. On <code>Linux</code> servers, you can add <code>source ~/.bashrc</code> at the beginning of the command, or use the command's absolute path."
|
other = "Note on writing commands: It is similar to shell/bat scripts. but it's advised not to start a newline, and connect multiple commands with <code>&&</code> or <code>;</code>. If a command cannot be found, it is possibly due to <code>PATH</code> environment variable issues. On <code>Linux</code> servers, you can add <code>source ~/.bashrc</code> at the beginning of the command, or just use the absolute path."
|
||||||
|
|
||||||
[AddMonitor]
|
[AddMonitor]
|
||||||
other = "Add Service Monitor"
|
other = "Add Service Monitor"
|
||||||
@@ -119,7 +119,7 @@ other = "Target"
|
|||||||
other = "Type"
|
other = "Type"
|
||||||
|
|
||||||
[SslExpirationOrChange]
|
[SslExpirationOrChange]
|
||||||
other = "(SSL Expiration or Change)"
|
other = "(Certificate Expiration and Changes)"
|
||||||
|
|
||||||
[Duration]
|
[Duration]
|
||||||
other = "Interval"
|
other = "Interval"
|
||||||
@@ -134,10 +134,10 @@ other = "Enable Failure Notification"
|
|||||||
other = "Failure Notification"
|
other = "Failure Notification"
|
||||||
|
|
||||||
[MaxLatency]
|
[MaxLatency]
|
||||||
other = "Max Latency (ms)"
|
other = "Maximum Latency (ms)"
|
||||||
|
|
||||||
[MinLatency]
|
[MinLatency]
|
||||||
other = "Min Latency (ms)"
|
other = "Minimum Latency (ms)"
|
||||||
|
|
||||||
[EnableLatencyNotification]
|
[EnableLatencyNotification]
|
||||||
other = "Enable Latency Notification"
|
other = "Enable Latency Notification"
|
||||||
@@ -147,7 +147,7 @@ other = "Latency Notification"
|
|||||||
|
|
||||||
[IntroductionOfMonitor]
|
[IntroductionOfMonitor]
|
||||||
other = """
|
other = """
|
||||||
For type <b>HTTP-GET</b>, enter URL (with http/https, HTTPS protocol will also monitor SSL certificate);<br>
|
For type <b>HTTP-GET</b>, enter URL (with http/https scheme, HTTPS protocol will also monitor SSL certificate);<br>
|
||||||
For type <b>ICMP-Ping</b>, enter domain/IP without port: example.com;<br>
|
For type <b>ICMP-Ping</b>, enter domain/IP without port: example.com;<br>
|
||||||
For type <b>TCP-Ping</b>, enter domain/IP + port number: example.com:22"""
|
For type <b>TCP-Ping</b>, enter domain/IP + port number: example.com:22"""
|
||||||
|
|
||||||
@@ -182,16 +182,16 @@ other = "Notification Trigger Mode"
|
|||||||
other = "Always Trigger"
|
other = "Always Trigger"
|
||||||
|
|
||||||
[ModeOnetimeTrigger]
|
[ModeOnetimeTrigger]
|
||||||
other = "Always Trigger"
|
other = "Trigger Once"
|
||||||
|
|
||||||
[EnableTriggerTask]
|
[EnableTriggerTask]
|
||||||
other = "Enable Trigger Task"
|
other = "Enable Trigger Task"
|
||||||
|
|
||||||
[FailTriggerTasks]
|
[FailTriggerTasks]
|
||||||
other = "Tasks triggered during an alarm"
|
other = "Tasks to trigger on an alarm"
|
||||||
|
|
||||||
[RecoverTriggerTasks]
|
[RecoverTriggerTasks]
|
||||||
other = "Tasks triggered during recovery"
|
other = "Tasks to trigger after recovery"
|
||||||
|
|
||||||
[Enable]
|
[Enable]
|
||||||
other = "Enable"
|
other = "Enable"
|
||||||
@@ -218,7 +218,7 @@ other = "Einstein Lightspeed 1"
|
|||||||
other = "Display Index"
|
other = "Display Index"
|
||||||
|
|
||||||
[TheLargerTheNumberTheHigherThePriority]
|
[TheLargerTheNumberTheHigherThePriority]
|
||||||
other = "Higher Number, Higher Priority"
|
other = "Greater Number, Higher Priority"
|
||||||
|
|
||||||
[Secret]
|
[Secret]
|
||||||
other = "Secret"
|
other = "Secret"
|
||||||
@@ -226,11 +226,14 @@ other = "Secret"
|
|||||||
[Note]
|
[Note]
|
||||||
other = "Note"
|
other = "Note"
|
||||||
|
|
||||||
|
[PublicNote]
|
||||||
|
other = "Public Note"
|
||||||
|
|
||||||
[LinuxOneKeyInstall]
|
[LinuxOneKeyInstall]
|
||||||
other = "Linux One-Command Install"
|
other = "Linux Installation Command"
|
||||||
|
|
||||||
[NoDomainAlert]
|
[NoDomainAlert]
|
||||||
other = "Set CDN bypassed Domain/IP in setting before using this function"
|
other = "Please set the IP (or a domain pointed to it) of the original server in settings"
|
||||||
|
|
||||||
[PushSuccessfully]
|
[PushSuccessfully]
|
||||||
other = "Send Success Notification"
|
other = "Send Success Notification"
|
||||||
@@ -251,13 +254,13 @@ other = "Cover All"
|
|||||||
other = "Ignore All"
|
other = "Ignore All"
|
||||||
|
|
||||||
[ByTrigger]
|
[ByTrigger]
|
||||||
other = "Execute by Trigger"
|
other = "Alarmed Servers"
|
||||||
|
|
||||||
[DeleteScheduledTask]
|
[DeleteScheduledTask]
|
||||||
other = "Delete Scheduled Task"
|
other = "Delete Scheduled Task"
|
||||||
|
|
||||||
[ConfirmToDeleteThisScheduledTask]
|
[ConfirmToDeleteThisScheduledTask]
|
||||||
other = "Are you sure you want to Delete?"
|
other = "Confirm deletion?"
|
||||||
|
|
||||||
[AccessDenied]
|
[AccessDenied]
|
||||||
other = "Access Denied"
|
other = "Access Denied"
|
||||||
@@ -278,22 +281,22 @@ other = "Don't Have an Account?"
|
|||||||
other = "HTTP(S)/SSL Certificate"
|
other = "HTTP(S)/SSL Certificate"
|
||||||
|
|
||||||
[TCPPort]
|
[TCPPort]
|
||||||
other = "TCP Port"
|
other = "TCPing"
|
||||||
|
|
||||||
[DeleteService]
|
[DeleteService]
|
||||||
other = "Remove Service Monitor"
|
other = "Remove Service Monitor"
|
||||||
|
|
||||||
[ConfirmToDeleteThisService]
|
[ConfirmToDeleteThisService]
|
||||||
other = "Confirm to Delete This Service?"
|
other = "Confirm Deleting This Service?"
|
||||||
|
|
||||||
[DeleteNotificationMethod]
|
[DeleteNotificationMethod]
|
||||||
other = "Delete Notification Method"
|
other = "Delete Notification Method"
|
||||||
|
|
||||||
[ConfirmToDeleteThisNotificationMethod]
|
[ConfirmToDeleteThisNotificationMethod]
|
||||||
other = "Confirm to Delete This Notification Method?"
|
other = "Confirm Deleting This Notification Method?"
|
||||||
|
|
||||||
[ForceUpdate]
|
[ForceUpdate]
|
||||||
other = "Force to Update"
|
other = "Trigger Agent Update"
|
||||||
|
|
||||||
[SelectAll]
|
[SelectAll]
|
||||||
other = "Select All"
|
other = "Select All"
|
||||||
@@ -302,7 +305,7 @@ other = "Select All"
|
|||||||
other = "Version"
|
other = "Version"
|
||||||
|
|
||||||
[OneKeyInstall]
|
[OneKeyInstall]
|
||||||
other = "One-Command Install"
|
other = "Installation commands"
|
||||||
|
|
||||||
[ClickToCopy]
|
[ClickToCopy]
|
||||||
other = "Click To Copy"
|
other = "Click To Copy"
|
||||||
@@ -311,7 +314,7 @@ other = "Click To Copy"
|
|||||||
other = "Delete Server"
|
other = "Delete Server"
|
||||||
|
|
||||||
[ConfirmToDeleteServer]
|
[ConfirmToDeleteServer]
|
||||||
other = "Confirm to Delete Server?"
|
other = "Confirm Deleting This Server?"
|
||||||
|
|
||||||
[NoServerSelected]
|
[NoServerSelected]
|
||||||
other = "No Server Selected"
|
other = "No Server Selected"
|
||||||
@@ -331,6 +334,9 @@ other = "Frontend Theme"
|
|||||||
[CustomCodes]
|
[CustomCodes]
|
||||||
other = "Custom Codes (Style and Script)"
|
other = "Custom Codes (Style and Script)"
|
||||||
|
|
||||||
|
[CustomCodesDashboard]
|
||||||
|
other = "Custom Codes for Dashboard"
|
||||||
|
|
||||||
[AccessPassword]
|
[AccessPassword]
|
||||||
other = "Frontend Access Password"
|
other = "Frontend Access Password"
|
||||||
|
|
||||||
@@ -539,7 +545,7 @@ other = "Scheduled Tasks"
|
|||||||
other = "API Management"
|
other = "API Management"
|
||||||
|
|
||||||
[IssueNewApiToken]
|
[IssueNewApiToken]
|
||||||
other = "Issue New API Token"
|
other = "Generate New API Token"
|
||||||
|
|
||||||
[Token]
|
[Token]
|
||||||
other = "Token"
|
other = "Token"
|
||||||
@@ -548,10 +554,10 @@ other = "Token"
|
|||||||
other = "Delete Token"
|
other = "Delete Token"
|
||||||
|
|
||||||
[ConfirmToDeleteThisToken]
|
[ConfirmToDeleteThisToken]
|
||||||
other = "Confirm to Delete This Token?"
|
other = "Confirm Deleting This Token?"
|
||||||
|
|
||||||
[YouAreNotAuthorized]
|
[YouAreNotAuthorized]
|
||||||
other = "Not Authorized for This Page"
|
other = "This Page needs Authorization"
|
||||||
|
|
||||||
[WrongAccessPassword]
|
[WrongAccessPassword]
|
||||||
other = "Wrong Access Password"
|
other = "Wrong Access Password"
|
||||||
@@ -599,13 +605,13 @@ other = "IP Changed"
|
|||||||
other = "Remaining Data"
|
other = "Remaining Data"
|
||||||
|
|
||||||
[DashboardTheme]
|
[DashboardTheme]
|
||||||
other = "Admin Panel Theme"
|
other = "Dashboard Theme"
|
||||||
|
|
||||||
[Info]
|
[Info]
|
||||||
other = "Info"
|
other = "Info"
|
||||||
|
|
||||||
[HideForGuest]
|
[HideForGuest]
|
||||||
other = "Hide for Guest"
|
other = "Hide from Guest"
|
||||||
|
|
||||||
[Menu]
|
[Menu]
|
||||||
other = "Menu"
|
other = "Menu"
|
||||||
@@ -616,20 +622,62 @@ other = "Network"
|
|||||||
[EnableShowInService]
|
[EnableShowInService]
|
||||||
other = "Enable Show in Service"
|
other = "Enable Show in Service"
|
||||||
|
|
||||||
|
[DDNS]
|
||||||
|
other = "Dynamic DNS"
|
||||||
|
|
||||||
|
[DDNSProfiles]
|
||||||
|
other = "DDNS Profiles"
|
||||||
|
|
||||||
|
[AddDDNSProfile]
|
||||||
|
other = "New Profile"
|
||||||
|
|
||||||
[EnableDDNS]
|
[EnableDDNS]
|
||||||
other = "Enable DDNS"
|
other = "Enable DDNS"
|
||||||
|
|
||||||
[EnableIPv4]
|
[EnableIPv4]
|
||||||
other = "Enable DDNS IPv4"
|
other = "IPv4 Enabled"
|
||||||
|
|
||||||
[EnableIpv6]
|
[EnableIPv6]
|
||||||
other = "Enable DDNS IPv6"
|
other = "IPv6 Enabled"
|
||||||
|
|
||||||
[DDNSDomain]
|
[DDNSDomain]
|
||||||
other = "DDNS Domain"
|
other = "Domains"
|
||||||
|
|
||||||
[DDNSProfile]
|
[DDNSDomains]
|
||||||
other = "DDNS Profile Name"
|
other = "Domains (separate with comma)"
|
||||||
|
|
||||||
|
[DDNSProvider]
|
||||||
|
other = "DDNS Provider"
|
||||||
|
|
||||||
|
[MaxRetries]
|
||||||
|
other = "Maximum retry attempts"
|
||||||
|
|
||||||
|
[DDNSAccessID]
|
||||||
|
other = "Credential 1"
|
||||||
|
|
||||||
|
[DDNSAccessSecret]
|
||||||
|
other = "Credential 2"
|
||||||
|
|
||||||
|
[DDNSTokenID]
|
||||||
|
other = "Token ID"
|
||||||
|
|
||||||
|
[DDNSTokenSecret]
|
||||||
|
other = "Token Secret"
|
||||||
|
|
||||||
|
[WebhookURL]
|
||||||
|
other = "Webhook URL"
|
||||||
|
|
||||||
|
[WebhookMethod]
|
||||||
|
other = "Webhook Request Method"
|
||||||
|
|
||||||
|
[WebhookRequestType]
|
||||||
|
other = "Webhook Request Type"
|
||||||
|
|
||||||
|
[WebhookHeaders]
|
||||||
|
other = "Webhook Request Headers"
|
||||||
|
|
||||||
|
[WebhookRequestBody]
|
||||||
|
other = "Webhook Request Body"
|
||||||
|
|
||||||
[Feature]
|
[Feature]
|
||||||
other = "Feature"
|
other = "Feature"
|
||||||
@@ -650,7 +698,58 @@ other = "Disable Switch Template in Frontend"
|
|||||||
other = "Servers On World Map"
|
other = "Servers On World Map"
|
||||||
|
|
||||||
[NAT]
|
[NAT]
|
||||||
other = "NAT"
|
other = "NAT Traversal"
|
||||||
|
|
||||||
|
[LocalService]
|
||||||
|
other = "Local service"
|
||||||
|
|
||||||
|
[LocalServicePlaceholder]
|
||||||
|
other = "192.168.1.1:80 (with port)"
|
||||||
|
|
||||||
|
[BindHostname]
|
||||||
|
other = "Bind hostname"
|
||||||
|
|
||||||
[NetworkSpiterList]
|
[NetworkSpiterList]
|
||||||
other = "Network Monitor"
|
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."
|
||||||
|
|
||||||
|
[Remaining]
|
||||||
|
other = "Remaining"
|
||||||
|
|
||||||
|
[Lifetime]
|
||||||
|
other = "Lifetime"
|
||||||
|
|
||||||
|
[Price]
|
||||||
|
other = "Price"
|
||||||
|
|
||||||
|
[Expired]
|
||||||
|
other = "Expired"
|
||||||
|
|
||||||
|
[Days]
|
||||||
|
other = "d"
|
||||||
|
|
||||||
|
[CustomNameservers]
|
||||||
|
other = "Custom Public DNS Nameservers for DDNS (separate with comma)"
|
||||||
|
|
||||||
|
[Plan]
|
||||||
|
other = "Plan"
|
||||||
|
|||||||
Vendored
+111
-12
@@ -71,7 +71,7 @@ other = "Ignorar todo, ejecutar solo a través de servidores específicos"
|
|||||||
other = "Incluir todo, excepto servidores específicos"
|
other = "Incluir todo, excepto servidores específicos"
|
||||||
|
|
||||||
[ExecuteByTriggerServer]
|
[ExecuteByTriggerServer]
|
||||||
other = "Ejecutar por servidor disparador"
|
other = "Ejecutar en servidores con alarma"
|
||||||
|
|
||||||
[SpecificServers]
|
[SpecificServers]
|
||||||
other = "Servidores Específicos"
|
other = "Servidores Específicos"
|
||||||
@@ -104,7 +104,7 @@ other = "Segundos, Minutos, Horas, Días, Meses, Semanas - Ver detalles a"
|
|||||||
other = "Formato de Expresión de Cron"
|
other = "Formato de Expresión de Cron"
|
||||||
|
|
||||||
[IntroductionOfCommands]
|
[IntroductionOfCommands]
|
||||||
other = "Introducción de Comandos: Escriba comandos como en scripts shell/bat. Evite saltos de línea; conecte múltiples comandos con <code>&&</code> o <code>&</code>. Si no se encuentra un comando, verifique la variable de entorno <code>PATH</code>. En servidores <code>Linux</code>, antepóngale <code>source ~/.bashrc</code> a los comandos, o use rutas absolutas."
|
other = "Introducción de Comandos: Escriba comandos como en scripts shell/bat. Evite saltos de línea; conecte múltiples comandos con <code>&&</code> o <code>;</code>. Si no se encuentra un comando, verifique la variable de entorno <code>PATH</code>. En servidores <code>Linux</code>, antepóngale <code>source ~/.bashrc</code> a los comandos, o use rutas absolutas."
|
||||||
|
|
||||||
[AddMonitor]
|
[AddMonitor]
|
||||||
other = "Agregar Monitor de Servicio"
|
other = "Agregar Monitor de Servicio"
|
||||||
@@ -226,6 +226,9 @@ other = "Secreto"
|
|||||||
[Note]
|
[Note]
|
||||||
other = "Nota"
|
other = "Nota"
|
||||||
|
|
||||||
|
[PublicNote]
|
||||||
|
other = "Nota Pública"
|
||||||
|
|
||||||
[LinuxOneKeyInstall]
|
[LinuxOneKeyInstall]
|
||||||
other = "Instalación Linux con Un Solo Clic"
|
other = "Instalación Linux con Un Solo Clic"
|
||||||
|
|
||||||
@@ -251,7 +254,7 @@ other = "Cubrir Todo"
|
|||||||
other = "Ignorar Todo"
|
other = "Ignorar Todo"
|
||||||
|
|
||||||
[ByTrigger]
|
[ByTrigger]
|
||||||
other = "Ejecutar por Disparador"
|
other = "Servidores con alarma"
|
||||||
|
|
||||||
[DeleteScheduledTask]
|
[DeleteScheduledTask]
|
||||||
other = "Eliminar Tarea Programada"
|
other = "Eliminar Tarea Programada"
|
||||||
@@ -278,7 +281,7 @@ other = "¿No tiene una cuenta?"
|
|||||||
other = "Certificados HTTP(S)/SSL"
|
other = "Certificados HTTP(S)/SSL"
|
||||||
|
|
||||||
[TCPPort]
|
[TCPPort]
|
||||||
other = "Puerto TCP"
|
other = "TCPing"
|
||||||
|
|
||||||
[DeleteService]
|
[DeleteService]
|
||||||
other = "Eliminar Servicio"
|
other = "Eliminar Servicio"
|
||||||
@@ -331,6 +334,9 @@ other = "Tema de Frontend"
|
|||||||
[CustomCodes]
|
[CustomCodes]
|
||||||
other = "Códigos Personalizados (Incluye style y script)"
|
other = "Códigos Personalizados (Incluye style y script)"
|
||||||
|
|
||||||
|
[CustomCodesDashboard]
|
||||||
|
other = "Custom Codes for Dashboard"
|
||||||
|
|
||||||
[AccessPassword]
|
[AccessPassword]
|
||||||
other = "Contraseña de Acceso al Frontend"
|
other = "Contraseña de Acceso al Frontend"
|
||||||
|
|
||||||
@@ -616,20 +622,62 @@ other = "Red"
|
|||||||
[EnableShowInService]
|
[EnableShowInService]
|
||||||
other = "Mostrar en servicio"
|
other = "Mostrar en servicio"
|
||||||
|
|
||||||
|
[DDNS]
|
||||||
|
other = "DNS Dinámico"
|
||||||
|
|
||||||
|
[DDNSProfiles]
|
||||||
|
other = "Perfiles DDNS"
|
||||||
|
|
||||||
|
[AddDDNSProfile]
|
||||||
|
other = "Nuevo Perfil"
|
||||||
|
|
||||||
[EnableDDNS]
|
[EnableDDNS]
|
||||||
other = "Habilitar DDNS"
|
other = "Habilitar DDNS"
|
||||||
|
|
||||||
[EnableIPv4]
|
[EnableIPv4]
|
||||||
other = "Habilitar DDNS IPv4"
|
other = "IPv4 Activado"
|
||||||
|
|
||||||
[EnableIpv6]
|
[EnableIPv6]
|
||||||
other = "Habilitar DDNS IPv6"
|
other = "IPv6 Activado"
|
||||||
|
|
||||||
[DDNSDomain]
|
[DDNSDomain]
|
||||||
other = "Dominio DDNS"
|
other = "Dominios"
|
||||||
|
|
||||||
[DDNSProfile]
|
[DDNSDomains]
|
||||||
other = "Nombre del perfil de DDNS"
|
other = "Dominios (separados por comas)"
|
||||||
|
|
||||||
|
[DDNSProvider]
|
||||||
|
other = "Proveedor DDNS"
|
||||||
|
|
||||||
|
[MaxRetries]
|
||||||
|
other = "Número máximo de intentos de reintento"
|
||||||
|
|
||||||
|
[DDNSAccessID]
|
||||||
|
other = "Credencial 1"
|
||||||
|
|
||||||
|
[DDNSAccessSecret]
|
||||||
|
other = "Credencial 2"
|
||||||
|
|
||||||
|
[DDNSTokenID]
|
||||||
|
other = "ID del Token"
|
||||||
|
|
||||||
|
[DDNSTokenSecret]
|
||||||
|
other = "Secreto del Token"
|
||||||
|
|
||||||
|
[WebhookURL]
|
||||||
|
other = "URL del Webhook"
|
||||||
|
|
||||||
|
[WebhookMethod]
|
||||||
|
other = "Método de Solicitud del Webhook"
|
||||||
|
|
||||||
|
[WebhookRequestType]
|
||||||
|
other = "Tipo de solicitud del Webhook"
|
||||||
|
|
||||||
|
[WebhookHeaders]
|
||||||
|
other = "Encabezados de Solicitud del Webhook"
|
||||||
|
|
||||||
|
[WebhookRequestBody]
|
||||||
|
other = "Cuerpo de Solicitud del Webhook"
|
||||||
|
|
||||||
[Feature]
|
[Feature]
|
||||||
other = "Característica"
|
other = "Característica"
|
||||||
@@ -650,7 +698,58 @@ other = "Deshabilitar Cambio de Plantilla en Frontend"
|
|||||||
other = "Servidores en el mapa mundial"
|
other = "Servidores en el mapa mundial"
|
||||||
|
|
||||||
[NAT]
|
[NAT]
|
||||||
other = "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]
|
[NetworkSpiterList]
|
||||||
other = "Red Monitor"
|
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."
|
||||||
|
|
||||||
|
[Remaining]
|
||||||
|
other = "Remaining"
|
||||||
|
|
||||||
|
[Lifetime]
|
||||||
|
other = "Lifetime"
|
||||||
|
|
||||||
|
[Price]
|
||||||
|
other = "Price"
|
||||||
|
|
||||||
|
[Expired]
|
||||||
|
other = "Expired"
|
||||||
|
|
||||||
|
[Days]
|
||||||
|
other = "d"
|
||||||
|
|
||||||
|
[CustomNameservers]
|
||||||
|
other = "Servidores DNS públicos personalizados para DDNS (separar con coma)"
|
||||||
|
|
||||||
|
[Plan]
|
||||||
|
other = "Plan"
|
||||||
|
|||||||
Vendored
+111
-12
@@ -71,7 +71,7 @@ other = "忽略所有,仅通过特定服务器执行"
|
|||||||
other = "覆盖所有,仅特定服务器不执行"
|
other = "覆盖所有,仅特定服务器不执行"
|
||||||
|
|
||||||
[ExecuteByTriggerServer]
|
[ExecuteByTriggerServer]
|
||||||
other = "由触发的服务器执行"
|
other = "由触发告警的服务器执行"
|
||||||
|
|
||||||
[SpecificServers]
|
[SpecificServers]
|
||||||
other = "特定服务器"
|
other = "特定服务器"
|
||||||
@@ -104,7 +104,7 @@ other = "秒 分 时 天 月 星期,详情见"
|
|||||||
other = "计划表达式格式"
|
other = "计划表达式格式"
|
||||||
|
|
||||||
[IntroductionOfCommands]
|
[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]
|
[AddMonitor]
|
||||||
other = "新增监控"
|
other = "新增监控"
|
||||||
@@ -226,6 +226,9 @@ other = "密钥"
|
|||||||
[Note]
|
[Note]
|
||||||
other = "备注"
|
other = "备注"
|
||||||
|
|
||||||
|
[PublicNote]
|
||||||
|
other = "公开备注"
|
||||||
|
|
||||||
[LinuxOneKeyInstall]
|
[LinuxOneKeyInstall]
|
||||||
other = "Linux 一键安装"
|
other = "Linux 一键安装"
|
||||||
|
|
||||||
@@ -251,7 +254,7 @@ other = "覆盖所有"
|
|||||||
other = "忽略所有"
|
other = "忽略所有"
|
||||||
|
|
||||||
[ByTrigger]
|
[ByTrigger]
|
||||||
other = "触发执行"
|
other = "触发告警的服务器"
|
||||||
|
|
||||||
[DeleteScheduledTask]
|
[DeleteScheduledTask]
|
||||||
other = "删除计划任务"
|
other = "删除计划任务"
|
||||||
@@ -278,7 +281,7 @@ other = "没有账号?"
|
|||||||
other = "HTTP(S)/SSL证书"
|
other = "HTTP(S)/SSL证书"
|
||||||
|
|
||||||
[TCPPort]
|
[TCPPort]
|
||||||
other = "TCP 端口"
|
other = "TCPing"
|
||||||
|
|
||||||
[DeleteService]
|
[DeleteService]
|
||||||
other = "移除服务"
|
other = "移除服务"
|
||||||
@@ -331,6 +334,9 @@ other = "前台界面主题"
|
|||||||
[CustomCodes]
|
[CustomCodes]
|
||||||
other = "自定义代码(包括 style 和 script)"
|
other = "自定义代码(包括 style 和 script)"
|
||||||
|
|
||||||
|
[CustomCodesDashboard]
|
||||||
|
other = "Custom Codes for Dashboard"
|
||||||
|
|
||||||
[AccessPassword]
|
[AccessPassword]
|
||||||
other = "前台访问密码"
|
other = "前台访问密码"
|
||||||
|
|
||||||
@@ -419,7 +425,7 @@ other = "活动"
|
|||||||
other = "版本"
|
other = "版本"
|
||||||
|
|
||||||
[NetSpeed]
|
[NetSpeed]
|
||||||
other = "网络"
|
other = "网速"
|
||||||
|
|
||||||
[Uptime]
|
[Uptime]
|
||||||
other = "在线"
|
other = "在线"
|
||||||
@@ -616,20 +622,62 @@ other = "网络"
|
|||||||
[EnableShowInService]
|
[EnableShowInService]
|
||||||
other = "在服务中显示"
|
other = "在服务中显示"
|
||||||
|
|
||||||
|
[DDNS]
|
||||||
|
other = "动态 DNS"
|
||||||
|
|
||||||
|
[DDNSProfiles]
|
||||||
|
other = "DDNS配置"
|
||||||
|
|
||||||
|
[AddDDNSProfile]
|
||||||
|
other = "新配置"
|
||||||
|
|
||||||
[EnableDDNS]
|
[EnableDDNS]
|
||||||
other = "启用DDNS"
|
other = "启用 DDNS"
|
||||||
|
|
||||||
[EnableIPv4]
|
[EnableIPv4]
|
||||||
other = "启用DDNS IPv4"
|
other = "启用 DDNS IPv4"
|
||||||
|
|
||||||
[EnableIpv6]
|
[EnableIPv6]
|
||||||
other = "启用DDNS IPv6"
|
other = "启用 DDNS IPv6"
|
||||||
|
|
||||||
[DDNSDomain]
|
[DDNSDomain]
|
||||||
other = "DDNS域名"
|
other = "DDNS 域名"
|
||||||
|
|
||||||
[DDNSProfile]
|
[DDNSDomains]
|
||||||
other = "DDNS配置名"
|
other = "域名(逗号分隔)"
|
||||||
|
|
||||||
|
[DDNSProvider]
|
||||||
|
other = "DDNS 供应商"
|
||||||
|
|
||||||
|
[MaxRetries]
|
||||||
|
other = "最大重试次数"
|
||||||
|
|
||||||
|
[DDNSAccessID]
|
||||||
|
other = "DDNS 凭据 1"
|
||||||
|
|
||||||
|
[DDNSAccessSecret]
|
||||||
|
other = "DDNS 凭据 2"
|
||||||
|
|
||||||
|
[DDNSTokenID]
|
||||||
|
other = "令牌 ID"
|
||||||
|
|
||||||
|
[DDNSTokenSecret]
|
||||||
|
other = "令牌 Secret"
|
||||||
|
|
||||||
|
[WebhookURL]
|
||||||
|
other = "Webhook 地址"
|
||||||
|
|
||||||
|
[WebhookMethod]
|
||||||
|
other = "Webhook 请求方式"
|
||||||
|
|
||||||
|
[WebhookRequestType]
|
||||||
|
other = "Webhook 请求类型"
|
||||||
|
|
||||||
|
[WebhookHeaders]
|
||||||
|
other = "Webhook 请求头"
|
||||||
|
|
||||||
|
[WebhookRequestBody]
|
||||||
|
other = "Webhook 请求体"
|
||||||
|
|
||||||
[Feature]
|
[Feature]
|
||||||
other = "功能"
|
other = "功能"
|
||||||
@@ -652,5 +700,56 @@ other = "服务器世界分布图"
|
|||||||
[NAT]
|
[NAT]
|
||||||
other = "内网穿透"
|
other = "内网穿透"
|
||||||
|
|
||||||
|
[LocalService]
|
||||||
|
other = "内网服务"
|
||||||
|
|
||||||
|
[LocalServicePlaceholder]
|
||||||
|
other = "192.168.1.1:80(带端口)"
|
||||||
|
|
||||||
|
[BindHostname]
|
||||||
|
other = "绑定域名"
|
||||||
|
|
||||||
[NetworkSpiterList]
|
[NetworkSpiterList]
|
||||||
other = "网络监控"
|
other = "网络监控"
|
||||||
|
|
||||||
|
[Refresh]
|
||||||
|
other = "刷新"
|
||||||
|
|
||||||
|
[CopyPath]
|
||||||
|
other = "复制路径"
|
||||||
|
|
||||||
|
[Goto]
|
||||||
|
other = "跳往"
|
||||||
|
|
||||||
|
[GotoHeadline]
|
||||||
|
other = "跳往文件夹"
|
||||||
|
|
||||||
|
[GotoGo]
|
||||||
|
other = "确认"
|
||||||
|
|
||||||
|
[GotoClose]
|
||||||
|
other = "取消"
|
||||||
|
|
||||||
|
[FMError]
|
||||||
|
other = "Agent 返回了错误,请查看控制台获取详细信息。要建立新连接,请重新打开 FM。"
|
||||||
|
|
||||||
|
[Remaining]
|
||||||
|
other = "剩余"
|
||||||
|
|
||||||
|
[Lifetime]
|
||||||
|
other = "永续"
|
||||||
|
|
||||||
|
[Price]
|
||||||
|
other = "价格"
|
||||||
|
|
||||||
|
[Expired]
|
||||||
|
other = "已到期"
|
||||||
|
|
||||||
|
[Days]
|
||||||
|
other = "天"
|
||||||
|
|
||||||
|
[CustomNameservers]
|
||||||
|
other = "自定义DDNS使用的公共DNS服务器(逗号分隔)"
|
||||||
|
|
||||||
|
[Plan]
|
||||||
|
other = "套餐"
|
||||||
|
|||||||
Vendored
+129
-30
@@ -50,7 +50,7 @@ other = "新增計劃任務"
|
|||||||
other = "名稱"
|
other = "名稱"
|
||||||
|
|
||||||
[Scheduler]
|
[Scheduler]
|
||||||
other = "計劃"
|
other = "排程"
|
||||||
|
|
||||||
[BackUp]
|
[BackUp]
|
||||||
other = "備份"
|
other = "備份"
|
||||||
@@ -71,7 +71,7 @@ other = "忽略所有,僅通過特定伺服器執行"
|
|||||||
other = "覆蓋所有,僅特定伺服器不執行"
|
other = "覆蓋所有,僅特定伺服器不執行"
|
||||||
|
|
||||||
[ExecuteByTriggerServer]
|
[ExecuteByTriggerServer]
|
||||||
other = "由觸發的伺服器執行"
|
other = "由觸發告警的伺服器執行"
|
||||||
|
|
||||||
[SpecificServers]
|
[SpecificServers]
|
||||||
other = "特定伺服器"
|
other = "特定伺服器"
|
||||||
@@ -80,37 +80,37 @@ other = "特定伺服器"
|
|||||||
other = "輸入ID/名稱以搜尋"
|
other = "輸入ID/名稱以搜尋"
|
||||||
|
|
||||||
[NotificationMethodGroup]
|
[NotificationMethodGroup]
|
||||||
other = "通知方式組"
|
other = "通知群組"
|
||||||
|
|
||||||
[PushSuccessMessages]
|
[PushSuccessMessages]
|
||||||
other = "推送成功的消息"
|
other = "推送成功的訊息"
|
||||||
|
|
||||||
[TaskType]
|
[TaskType]
|
||||||
other = "任務類型"
|
other = "任務類型"
|
||||||
|
|
||||||
[CronTask]
|
[CronTask]
|
||||||
other = "計劃任務"
|
other = "排程任務"
|
||||||
|
|
||||||
[TriggerTask]
|
[TriggerTask]
|
||||||
other = "觸發任務"
|
other = "觸發任務"
|
||||||
|
|
||||||
[TheFormaOfTheScheduleIs]
|
[TheFormaOfTheScheduleIs]
|
||||||
other = "計劃的格式為:"
|
other = "排程的格式為:"
|
||||||
|
|
||||||
[SecondsMinutesHoursDaysMonthsWeeksSeeDetails]
|
[SecondsMinutesHoursDaysMonthsWeeksSeeDetails]
|
||||||
other = "秒 分 時 天 月 星期,詳情見"
|
other = "秒 分 時 天 月 星期,詳情見"
|
||||||
|
|
||||||
[ScheduleExpressionFormat]
|
[ScheduleExpressionFormat]
|
||||||
other = "計劃表達式格式"
|
other = "排程表達式格式"
|
||||||
|
|
||||||
[IntroductionOfCommands]
|
[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]
|
[AddMonitor]
|
||||||
other = "新增監控"
|
other = "新增監控"
|
||||||
|
|
||||||
[Blog]
|
[Blog]
|
||||||
other = "博客"
|
other = "部落格"
|
||||||
|
|
||||||
[Target]
|
[Target]
|
||||||
other = "目標"
|
other = "目標"
|
||||||
@@ -158,7 +158,7 @@ other = "新增通知方式"
|
|||||||
other = "分組"
|
other = "分組"
|
||||||
|
|
||||||
[DoNotSendTestMessages]
|
[DoNotSendTestMessages]
|
||||||
other = "不發送測試信息"
|
other = "不發送測試訊息"
|
||||||
|
|
||||||
[RequestMethod]
|
[RequestMethod]
|
||||||
other = "請求方式"
|
other = "請求方式"
|
||||||
@@ -221,11 +221,14 @@ other = "排序"
|
|||||||
other = "越大越靠前"
|
other = "越大越靠前"
|
||||||
|
|
||||||
[Secret]
|
[Secret]
|
||||||
other = "密鑰"
|
other = "金鑰"
|
||||||
|
|
||||||
[Note]
|
[Note]
|
||||||
other = "備註"
|
other = "備註"
|
||||||
|
|
||||||
|
[PublicNote]
|
||||||
|
other = "公開備註"
|
||||||
|
|
||||||
[LinuxOneKeyInstall]
|
[LinuxOneKeyInstall]
|
||||||
other = "Linux 一鍵安裝"
|
other = "Linux 一鍵安裝"
|
||||||
|
|
||||||
@@ -251,13 +254,13 @@ other = "覆蓋所有"
|
|||||||
other = "忽略所有"
|
other = "忽略所有"
|
||||||
|
|
||||||
[ByTrigger]
|
[ByTrigger]
|
||||||
other = "觸發執行"
|
other = "觸發告警的伺服器"
|
||||||
|
|
||||||
[DeleteScheduledTask]
|
[DeleteScheduledTask]
|
||||||
other = "刪除計劃任務"
|
other = "刪除排程任務"
|
||||||
|
|
||||||
[ConfirmToDeleteThisScheduledTask]
|
[ConfirmToDeleteThisScheduledTask]
|
||||||
other = "確認刪除此計劃任務?"
|
other = "確認刪除此排程任務?"
|
||||||
|
|
||||||
[AccessDenied]
|
[AccessDenied]
|
||||||
other = "訪問被拒絕"
|
other = "訪問被拒絕"
|
||||||
@@ -278,7 +281,7 @@ other = "沒有帳號?"
|
|||||||
other = "HTTP(S)/SSL證書"
|
other = "HTTP(S)/SSL證書"
|
||||||
|
|
||||||
[TCPPort]
|
[TCPPort]
|
||||||
other = "TCP 端口"
|
other = "TCPing"
|
||||||
|
|
||||||
[DeleteService]
|
[DeleteService]
|
||||||
other = "移除服務"
|
other = "移除服務"
|
||||||
@@ -331,6 +334,9 @@ other = "前台界面主題"
|
|||||||
[CustomCodes]
|
[CustomCodes]
|
||||||
other = "自定義代碼(包括 style 和 script)"
|
other = "自定義代碼(包括 style 和 script)"
|
||||||
|
|
||||||
|
[CustomCodesDashboard]
|
||||||
|
other = "Custom Codes for Dashboard"
|
||||||
|
|
||||||
[AccessPassword]
|
[AccessPassword]
|
||||||
other = "前台訪問密碼"
|
other = "前台訪問密碼"
|
||||||
|
|
||||||
@@ -404,7 +410,7 @@ other = "流量"
|
|||||||
other = "負載"
|
other = "負載"
|
||||||
|
|
||||||
[ProcessCount]
|
[ProcessCount]
|
||||||
other = "進程數"
|
other = "行程數"
|
||||||
|
|
||||||
[ConnCount]
|
[ConnCount]
|
||||||
other = "連接數"
|
other = "連接數"
|
||||||
@@ -419,7 +425,7 @@ other = "活動"
|
|||||||
other = "版本"
|
other = "版本"
|
||||||
|
|
||||||
[NetSpeed]
|
[NetSpeed]
|
||||||
other = "網絡"
|
other = "網速"
|
||||||
|
|
||||||
[Uptime]
|
[Uptime]
|
||||||
other = "在線"
|
other = "在線"
|
||||||
@@ -455,7 +461,7 @@ other = "狀態"
|
|||||||
other = "可用性"
|
other = "可用性"
|
||||||
|
|
||||||
[AverageLatency]
|
[AverageLatency]
|
||||||
other = "平均響應時間"
|
other = "平均回應時間"
|
||||||
|
|
||||||
[CycleTransferStats]
|
[CycleTransferStats]
|
||||||
other = "周期性流量統計"
|
other = "周期性流量統計"
|
||||||
@@ -521,7 +527,7 @@ other = "發生錯誤"
|
|||||||
other = "系統錯誤"
|
other = "系統錯誤"
|
||||||
|
|
||||||
[NetworkError]
|
[NetworkError]
|
||||||
other = "網絡錯誤"
|
other = "網路錯誤"
|
||||||
|
|
||||||
[ServicesStatus]
|
[ServicesStatus]
|
||||||
other = "服務狀態"
|
other = "服務狀態"
|
||||||
@@ -533,7 +539,7 @@ other = "伺服器管理"
|
|||||||
other = "服務監控"
|
other = "服務監控"
|
||||||
|
|
||||||
[ScheduledTasks]
|
[ScheduledTasks]
|
||||||
other = "計劃任務"
|
other = "排程任務"
|
||||||
|
|
||||||
[ApiManagement]
|
[ApiManagement]
|
||||||
other = "API 管理"
|
other = "API 管理"
|
||||||
@@ -611,25 +617,67 @@ other = "對遊客隱藏"
|
|||||||
other = "菜單"
|
other = "菜單"
|
||||||
|
|
||||||
[NetworkSpiter]
|
[NetworkSpiter]
|
||||||
other = "網絡"
|
other = "網路"
|
||||||
|
|
||||||
[EnableShowInService]
|
[EnableShowInService]
|
||||||
other = "在服務中顯示"
|
other = "在服務中顯示"
|
||||||
|
|
||||||
|
[DDNS]
|
||||||
|
other = "動態 DNS"
|
||||||
|
|
||||||
|
[DDNSProfiles]
|
||||||
|
other = "DDNS配置"
|
||||||
|
|
||||||
|
[AddDDNSProfile]
|
||||||
|
other = "新增配置"
|
||||||
|
|
||||||
[EnableDDNS]
|
[EnableDDNS]
|
||||||
other = "啟用DDNS"
|
other = "啟用 DDNS"
|
||||||
|
|
||||||
[EnableIPv4]
|
[EnableIPv4]
|
||||||
other = "啟用DDNS IPv4"
|
other = "啟用 DDNS IPv4"
|
||||||
|
|
||||||
[EnableIpv6]
|
[EnableIPv6]
|
||||||
other = "啟用DDNS IPv6"
|
other = "啟用 DDNS IPv6"
|
||||||
|
|
||||||
[DDNSDomain]
|
[DDNSDomain]
|
||||||
other = "DDNS網域"
|
other = "DDNS 域名"
|
||||||
|
|
||||||
[DDNSProfile]
|
[DDNSDomains]
|
||||||
other = "DDNS設定名"
|
other = "域名(逗號分隔)"
|
||||||
|
|
||||||
|
[DDNSProvider]
|
||||||
|
other = "DDN S供應商"
|
||||||
|
|
||||||
|
[MaxRetries]
|
||||||
|
other = "最大重試次數"
|
||||||
|
|
||||||
|
[DDNSAccessID]
|
||||||
|
other = "DDNS 憑據 1"
|
||||||
|
|
||||||
|
[DDNSAccessSecret]
|
||||||
|
other = "DDNS 憑據 2"
|
||||||
|
|
||||||
|
[DDNSTokenID]
|
||||||
|
other = "令牌 ID"
|
||||||
|
|
||||||
|
[DDNSTokenSecret]
|
||||||
|
other = "令牌 Secret"
|
||||||
|
|
||||||
|
[WebhookURL]
|
||||||
|
other = "Webhook 地址"
|
||||||
|
|
||||||
|
[WebhookMethod]
|
||||||
|
other = "Webhook 請求方式"
|
||||||
|
|
||||||
|
[WebhookRequestType]
|
||||||
|
other = "Webhook 請求類型"
|
||||||
|
|
||||||
|
[WebhookHeaders]
|
||||||
|
other = "Webhook 請求頭"
|
||||||
|
|
||||||
|
[WebhookRequestBody]
|
||||||
|
other = "Webhook 請求體"
|
||||||
|
|
||||||
[Feature]
|
[Feature]
|
||||||
other = "功能"
|
other = "功能"
|
||||||
@@ -650,7 +698,58 @@ other = "禁止前台切換主題"
|
|||||||
other = "伺服器世界分布圖"
|
other = "伺服器世界分布圖"
|
||||||
|
|
||||||
[NAT]
|
[NAT]
|
||||||
other = "NAT"
|
other = "NAT穿透"
|
||||||
|
|
||||||
|
[LocalService]
|
||||||
|
other = "內網服務"
|
||||||
|
|
||||||
|
[LocalServicePlaceholder]
|
||||||
|
other = "192.168.1.1:80(帶埠號)"
|
||||||
|
|
||||||
|
[BindHostname]
|
||||||
|
other = "綁定網域"
|
||||||
|
|
||||||
[NetworkSpiterList]
|
[NetworkSpiterList]
|
||||||
other = "網絡監控"
|
other = "網路監控"
|
||||||
|
|
||||||
|
[Refresh]
|
||||||
|
other = "重新整理"
|
||||||
|
|
||||||
|
[CopyPath]
|
||||||
|
other = "複製路徑"
|
||||||
|
|
||||||
|
[Goto]
|
||||||
|
other = "跳至"
|
||||||
|
|
||||||
|
[GotoHeadline]
|
||||||
|
other = "跳至資料夾"
|
||||||
|
|
||||||
|
[GotoGo]
|
||||||
|
other = "確定"
|
||||||
|
|
||||||
|
[GotoClose]
|
||||||
|
other = "取消"
|
||||||
|
|
||||||
|
[FMError]
|
||||||
|
other = "Agent 回傳了錯誤,請查看主控台獲取詳細資訊。要建立新連線,請重新開啟 FM。"
|
||||||
|
|
||||||
|
[Remaining]
|
||||||
|
other = "剩餘"
|
||||||
|
|
||||||
|
[Lifetime]
|
||||||
|
other = "永續"
|
||||||
|
|
||||||
|
[Price]
|
||||||
|
other = "價格"
|
||||||
|
|
||||||
|
[Expired]
|
||||||
|
other = "已到期"
|
||||||
|
|
||||||
|
[Days]
|
||||||
|
other = "天"
|
||||||
|
|
||||||
|
[CustomNameservers]
|
||||||
|
other = "自訂DDNS使用的公共DNS伺服器(逗號分隔)"
|
||||||
|
|
||||||
|
[Plan]
|
||||||
|
other = "套餐"
|
||||||
|
|||||||
Vendored
+13
-1
@@ -2,10 +2,14 @@ package resource
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
|
||||||
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var StaticFS *utils.HybridFS
|
||||||
|
|
||||||
//go:embed static
|
//go:embed static
|
||||||
var StaticFS embed.FS
|
var staticFS embed.FS
|
||||||
|
|
||||||
//go:embed template
|
//go:embed template
|
||||||
var TemplateFS embed.FS
|
var TemplateFS embed.FS
|
||||||
@@ -13,6 +17,14 @@ var TemplateFS embed.FS
|
|||||||
//go:embed l10n
|
//go:embed l10n
|
||||||
var I18nFS embed.FS
|
var I18nFS embed.FS
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
StaticFS, err = utils.NewHybridFS(staticFS, "static", "resource/static/custom")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func IsTemplateFileExist(name string) bool {
|
func IsTemplateFileExist(name string) bool {
|
||||||
_, err := TemplateFS.Open(name)
|
_, err := TemplateFS.Open(name)
|
||||||
return err == nil
|
return err == nil
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -9,6 +9,11 @@ td {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.container{
|
||||||
|
width: 95vw !important;
|
||||||
|
max-width: 1680px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.nb-container {
|
.nb-container {
|
||||||
padding-top: 75px;
|
padding-top: 75px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|||||||
+111
-16
@@ -99,7 +99,11 @@ function showFormModal(modelSelector, formID, URL, getData) {
|
|||||||
item.name === "DisplayIndex" ||
|
item.name === "DisplayIndex" ||
|
||||||
item.name === "Type" ||
|
item.name === "Type" ||
|
||||||
item.name === "Cover" ||
|
item.name === "Cover" ||
|
||||||
item.name === "Duration"
|
item.name === "Duration" ||
|
||||||
|
item.name === "MaxRetries" ||
|
||||||
|
item.name === "Provider" ||
|
||||||
|
item.name === "WebhookMethod" ||
|
||||||
|
item.name === "WebhookRequestType"
|
||||||
) {
|
) {
|
||||||
obj[item.name] = parseInt(item.value);
|
obj[item.name] = parseInt(item.value);
|
||||||
} else if (item.name.endsWith("Latency")) {
|
} else if (item.name.endsWith("Latency")) {
|
||||||
@@ -128,6 +132,16 @@ function showFormModal(modelSelector, formID, URL, getData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.name.endsWith("DDNSProfilesRaw")) {
|
||||||
|
if (item.value.length > 2) {
|
||||||
|
obj[item.name] = JSON.stringify(
|
||||||
|
[...item.value.matchAll(/\d+/gm)].map((k) =>
|
||||||
|
parseInt(k[0])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
$.post(URL, JSON.stringify(data))
|
$.post(URL, JSON.stringify(data))
|
||||||
@@ -207,6 +221,7 @@ function addOrEditAlertRule(rule) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 需要在 showFormModal 进一步拼接数组
|
||||||
modal
|
modal
|
||||||
.find("input[name=FailTriggerTasksRaw]")
|
.find("input[name=FailTriggerTasksRaw]")
|
||||||
.val(rule ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
|
.val(rule ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
|
||||||
@@ -256,6 +271,55 @@ function addOrEditNotification(notification) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addOrEditDDNS(ddns) {
|
||||||
|
const modal = $(".ddns.modal");
|
||||||
|
modal.children(".header").text((ddns ? LANG.Edit : LANG.Add));
|
||||||
|
modal
|
||||||
|
.find(".nezha-primary-btn.button")
|
||||||
|
.html(
|
||||||
|
ddns
|
||||||
|
? LANG.Edit + '<i class="edit icon"></i>'
|
||||||
|
: LANG.Add + '<i class="add icon"></i>'
|
||||||
|
);
|
||||||
|
modal.find("input[name=ID]").val(ddns ? ddns.ID : null);
|
||||||
|
modal.find("input[name=Name]").val(ddns ? ddns.Name : null);
|
||||||
|
modal.find("input[name=DomainsRaw]").val(ddns ? ddns.DomainsRaw : null);
|
||||||
|
modal.find("input[name=AccessID]").val(ddns ? ddns.AccessID : null);
|
||||||
|
modal.find("input[name=AccessSecret]").val(ddns ? ddns.AccessSecret : null);
|
||||||
|
modal.find("input[name=MaxRetries]").val(ddns ? ddns.MaxRetries : 3);
|
||||||
|
modal.find("input[name=WebhookURL]").val(ddns ? ddns.WebhookURL : null);
|
||||||
|
modal
|
||||||
|
.find("textarea[name=WebhookHeaders]")
|
||||||
|
.val(ddns ? ddns.WebhookHeaders : null);
|
||||||
|
modal
|
||||||
|
.find("textarea[name=WebhookRequestBody]")
|
||||||
|
.val(ddns ? ddns.WebhookRequestBody : null);
|
||||||
|
modal
|
||||||
|
.find("select[name=Provider]")
|
||||||
|
.val(ddns ? ddns.Provider : 0);
|
||||||
|
modal
|
||||||
|
.find("select[name=WebhookMethod]")
|
||||||
|
.val(ddns ? ddns.WebhookMethod : 1);
|
||||||
|
modal
|
||||||
|
.find("select[name=WebhookRequestType]")
|
||||||
|
.val(ddns ? ddns.WebhookRequestType : 1);
|
||||||
|
if (ddns && ddns.EnableIPv4) {
|
||||||
|
modal.find(".ui.enableipv4.checkbox").checkbox("set checked");
|
||||||
|
} else {
|
||||||
|
modal.find(".ui.enableipv4.checkbox").checkbox("set unchecked");
|
||||||
|
}
|
||||||
|
if (ddns && ddns.EnableIPv6) {
|
||||||
|
modal.find(".ui.enableipv6.checkbox").checkbox("set checked");
|
||||||
|
} else {
|
||||||
|
modal.find(".ui.enableipv6.checkbox").checkbox("set unchecked");
|
||||||
|
}
|
||||||
|
showFormModal(
|
||||||
|
".ddns.modal",
|
||||||
|
"#ddnsForm",
|
||||||
|
"/api/ddns"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function addOrEditNAT(nat) {
|
function addOrEditNAT(nat) {
|
||||||
const modal = $(".nat.modal");
|
const modal = $(".nat.modal");
|
||||||
modal.children(".header").text((nat ? LANG.Edit : LANG.Add));
|
modal.children(".header").text((nat ? LANG.Edit : LANG.Add));
|
||||||
@@ -325,12 +389,38 @@ function addOrEditServer(server, conf) {
|
|||||||
modal.find("input[name=id]").val(server ? server.ID : null);
|
modal.find("input[name=id]").val(server ? server.ID : null);
|
||||||
modal.find("input[name=name]").val(server ? server.Name : null);
|
modal.find("input[name=name]").val(server ? server.Name : null);
|
||||||
modal.find("input[name=Tag]").val(server ? server.Tag : null);
|
modal.find("input[name=Tag]").val(server ? server.Tag : null);
|
||||||
modal.find("input[name=DDNSDomain]").val(server ? server.DDNSDomain : null);
|
modal.find("a.ui.label.visible").each((i, el) => {
|
||||||
modal.find("input[name=DDNSProfile]").val(server ? server.DDNSProfile : null);
|
el.remove();
|
||||||
|
});
|
||||||
|
var ddns;
|
||||||
|
if (server) {
|
||||||
|
ddns = server.DDNSProfilesRaw;
|
||||||
|
let serverList;
|
||||||
|
try {
|
||||||
|
serverList = JSON.parse(ddns);
|
||||||
|
} catch (error) {
|
||||||
|
serverList = "[]";
|
||||||
|
}
|
||||||
|
const node = modal.find("i.dropdown.icon.ddnsProfiles");
|
||||||
|
for (let i = 0; i < serverList.length; i++) {
|
||||||
|
node.after(
|
||||||
|
'<a class="ui label transition visible" data-value="' +
|
||||||
|
serverList[i] +
|
||||||
|
'" style="display: inline-block !important;">ID:' +
|
||||||
|
serverList[i] +
|
||||||
|
'<i class="delete icon"></i></a>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 需要在 showFormModal 进一步拼接数组
|
||||||
|
modal
|
||||||
|
.find("input[name=DDNSProfilesRaw]")
|
||||||
|
.val(server ? "[]," + ddns.substr(1, ddns.length - 2) : "[]");
|
||||||
modal
|
modal
|
||||||
.find("input[name=DisplayIndex]")
|
.find("input[name=DisplayIndex]")
|
||||||
.val(server ? server.DisplayIndex : null);
|
.val(server ? server.DisplayIndex : null);
|
||||||
modal.find("textarea[name=Note]").val(server ? server.Note : null);
|
modal.find("textarea[name=Note]").val(server ? server.Note : null);
|
||||||
|
modal.find("textarea[name=PublicNote]").val(server ? server.PublicNote : null);
|
||||||
if (server) {
|
if (server) {
|
||||||
modal.find(".secret.field").attr("style", "");
|
modal.find(".secret.field").attr("style", "");
|
||||||
modal.find(".command.field").attr("style", "");
|
modal.find(".command.field").attr("style", "");
|
||||||
@@ -341,26 +431,17 @@ function addOrEditServer(server, conf) {
|
|||||||
modal.find(".command.field").attr("style", "display:none");
|
modal.find(".command.field").attr("style", "display:none");
|
||||||
modal.find("input[name=secret]").val("");
|
modal.find("input[name=secret]").val("");
|
||||||
}
|
}
|
||||||
if (server && server.HideForGuest) {
|
|
||||||
modal.find(".ui.hideforguest.checkbox").checkbox("set checked");
|
|
||||||
} else {
|
|
||||||
modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
|
|
||||||
}
|
|
||||||
if (server && server.EnableDDNS) {
|
if (server && server.EnableDDNS) {
|
||||||
modal.find(".ui.enableddns.checkbox").checkbox("set checked");
|
modal.find(".ui.enableddns.checkbox").checkbox("set checked");
|
||||||
} else {
|
} else {
|
||||||
modal.find(".ui.enableddns.checkbox").checkbox("set unchecked");
|
modal.find(".ui.enableddns.checkbox").checkbox("set unchecked");
|
||||||
}
|
}
|
||||||
if (server && server.EnableIPv4) {
|
if (server && server.HideForGuest) {
|
||||||
modal.find(".ui.enableipv4.checkbox").checkbox("set checked");
|
modal.find(".ui.hideforguest.checkbox").checkbox("set checked");
|
||||||
} else {
|
} else {
|
||||||
modal.find(".ui.enableipv4.checkbox").checkbox("set unchecked");
|
modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
|
||||||
}
|
|
||||||
if (server && server.EnableIpv6) {
|
|
||||||
modal.find(".ui.enableipv6.checkbox").checkbox("set checked");
|
|
||||||
} else {
|
|
||||||
modal.find(".ui.enableipv6.checkbox").checkbox("set unchecked");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showFormModal(".server.modal", "#serverForm", "/api/server");
|
showFormModal(".server.modal", "#serverForm", "/api/server");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +527,7 @@ function addOrEditMonitor(monitor) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 需要在 showFormModal 进一步拼接数组
|
||||||
modal
|
modal
|
||||||
.find("input[name=FailTriggerTasksRaw]")
|
.find("input[name=FailTriggerTasksRaw]")
|
||||||
.val(monitor ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
|
.val(monitor ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
|
||||||
@@ -491,6 +573,7 @@ function addOrEditCron(cron) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 需要在 showFormModal 进一步拼接数组
|
||||||
modal
|
modal
|
||||||
.find("input[name=ServersRaw]")
|
.find("input[name=ServersRaw]")
|
||||||
.val(cron ? "[]," + servers.substr(1, servers.length - 2) : "[]");
|
.val(cron ? "[]," + servers.substr(1, servers.length - 2) : "[]");
|
||||||
@@ -620,3 +703,15 @@ $(document).ready(() => {
|
|||||||
});
|
});
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).ready(() => {
|
||||||
|
try {
|
||||||
|
$(".ui.ddns.search.dropdown").dropdown({
|
||||||
|
clearable: true,
|
||||||
|
apiSettings: {
|
||||||
|
url: "/api/search-ddns?word={query}",
|
||||||
|
cache: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
@@ -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) {
|
@media only screen and (max-width:767px) {
|
||||||
.ui.card>.content>.header:not(.ui),.ui.cards>.card>.content>.header:not(.ui) {
|
.ui.card>.content>.header:not(.ui),
|
||||||
margin-top:0.4em !important;
|
.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;
|
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;
|
padding: 0.9rem 0.55rem;
|
||||||
}
|
}
|
||||||
.ui.large.menu{
|
|
||||||
|
.ui.large.menu {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i.icon {
|
i.icon {
|
||||||
color:#000;
|
color: #000;
|
||||||
width:1.2em !important;
|
width: 1.2em !important;
|
||||||
}
|
|
||||||
i.fi {
|
|
||||||
width:0.9em;
|
|
||||||
margin:0px 6px 0px 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
i.fi {
|
||||||
content:" " !important;
|
width: 0.9em;
|
||||||
background:fixed !important;
|
margin: 0px 6px 0px 2px;
|
||||||
z-index:-1 !important;
|
|
||||||
top:0 !important;
|
|
||||||
right:0 !important;
|
|
||||||
bottom:0 !important;
|
|
||||||
left:0 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
@@ -55,6 +44,11 @@ td {
|
|||||||
margin-bottom: -47px;
|
margin-bottom: -47px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.container {
|
||||||
|
width: 95%;
|
||||||
|
max-width: 1680px !important;
|
||||||
|
}
|
||||||
|
|
||||||
#app .ui.fluid.accordion {
|
#app .ui.fluid.accordion {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
@@ -65,79 +59,80 @@ td {
|
|||||||
padding-top: unset;
|
padding-top: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login.nb-container > .grid {
|
.login.nb-container>.grid {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login.nb-container > .grid .column {
|
.login.nb-container>.grid .column {
|
||||||
max-width: 450px;
|
max-width: 450px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.menu .item-right:before{
|
.ui.menu .item-right:before {
|
||||||
width:0px;
|
width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .flag {
|
.status.cards .flag {
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .header > .info.icon {
|
.status.cards .header>.info.icon {
|
||||||
float: right;
|
float: right;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.grid {
|
.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;
|
line-height: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .wide.column {
|
.status.cards .wide.column {
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
height:2.3rem !important;
|
height: 2.3rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .wide.column:nth-child(1) {
|
.status.cards .wide.column:nth-child(1) {
|
||||||
margin-top:1.2rem !important;
|
margin-top: 1.2rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .wide.column:nth-child(2) {
|
.status.cards .wide.column:nth-child(2) {
|
||||||
margin-top:1.2rem !important;
|
margin-top: 1.2rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .three.wide.column {
|
.status.cards .three.wide.column {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 22%!important;
|
width: 22% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .thirteen.wide.column{
|
.status.cards .thirteen.wide.column {
|
||||||
width: 78%!important;
|
width: 78% !important;
|
||||||
padding-left:0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .description {
|
.status.cards .description {
|
||||||
padding-bottom:0 !important;
|
padding-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .flag {
|
.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;
|
float: right;
|
||||||
margin-right:0 !important;
|
margin-right: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.popup:before {
|
.ui.popup:before {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.closePopup{
|
.closePopup {
|
||||||
color:rgb(10, 148, 242) !important;
|
color: rgb(10, 148, 242) !important;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 7px;
|
top: 7px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
@@ -150,26 +145,26 @@ td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ui.content {
|
.ui.content {
|
||||||
margin:0 !important;
|
margin: 0 !important;
|
||||||
padding:1em !important;
|
padding: 1em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .ui.content.popup {
|
.status.cards .ui.content.popup {
|
||||||
min-width:calc(100%)!important;
|
min-width: calc(100%) !important;
|
||||||
line-height:2rem !important;
|
line-height: 2rem !important;
|
||||||
border-radius:5px !important;
|
border-radius: 5px !important;
|
||||||
border:1px solid transparent !important;
|
border: 1px solid transparent !important;
|
||||||
font-family:Arial,Helvetica,sans-serif !important;
|
font-family: Arial, Helvetica, sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .outline.icon {
|
.status.cards .outline.icon {
|
||||||
margin-right:1px !important;
|
margin-right: 1px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.progress .bar {
|
.ui.progress .bar {
|
||||||
min-width:1.8em !important;
|
min-width: 1.8em !important;
|
||||||
border-radius:5px !important;
|
border-radius: 5px !important;
|
||||||
line-height:1.65em !important;
|
line-height: 1.65em !important;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding-right: 0.4em;
|
padding-right: 0.4em;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
@@ -182,7 +177,7 @@ td {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-status .delay-today > i {
|
.service-status .delay-today>i {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1.2em;
|
width: 1.2em;
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
|
|||||||
+11
@@ -13,7 +13,18 @@ const mixinsVue = {
|
|||||||
this.isMobile = this.checkIsMobile();
|
this.isMobile = this.checkIsMobile();
|
||||||
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
|
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initDropdown();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
initDropdown() {
|
||||||
|
if(this.isMobile) $('.ui.dropdown').dropdown({
|
||||||
|
action: 'hide',
|
||||||
|
on: 'click',
|
||||||
|
duration: 100,
|
||||||
|
direction: 'direction'
|
||||||
|
});
|
||||||
|
},
|
||||||
toggleTemplate(template) {
|
toggleTemplate(template) {
|
||||||
if( template != this.preferredTemplate){
|
if( template != this.preferredTemplate){
|
||||||
this.preferredTemplate = template;
|
this.preferredTemplate = template;
|
||||||
|
|||||||
+24
-14
@@ -8,6 +8,11 @@ body[theme="dark"] .navbar .navbar-brand {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[theme="dark"] .navbar .navbar-nav li.pc-active a,
|
||||||
|
body[theme="dark"] .navbar .navbar-nav li.m-active a {
|
||||||
|
color: rgba(73, 146, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
body[theme="dark"] .navbar .dropdown-menu {
|
body[theme="dark"] .navbar .dropdown-menu {
|
||||||
background-color: rgba(23, 26, 30, 1);
|
background-color: rgba(23, 26, 30, 1);
|
||||||
border-color: rgba(49, 54, 59, 1);
|
border-color: rgba(49, 54, 59, 1);
|
||||||
@@ -27,7 +32,7 @@ body[theme="dark"] .navbar .dropdown-menu > li > a:hover {
|
|||||||
body[theme="dark"] .navbar .navbar-nav > .open > a:focus,
|
body[theme="dark"] .navbar .navbar-nav > .open > a:focus,
|
||||||
body[theme="dark"] .navbar .navbar-nav > .open > a:hover,
|
body[theme="dark"] .navbar .navbar-nav > .open > a:hover,
|
||||||
body[theme="dark"] .navbar .navbar-nav > .active > a,
|
body[theme="dark"] .navbar .navbar-nav > .active > a,
|
||||||
body[theme="dark"] .navbar .navbar-nav > .open > a{
|
body[theme="dark"] .navbar .navbar-nav > .open > a {
|
||||||
background-image: none;
|
background-image: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -53,7 +58,7 @@ body[theme="dark"] .table {
|
|||||||
body[theme="dark"] .table > tbody > tr > td:before,
|
body[theme="dark"] .table > tbody > tr > td:before,
|
||||||
body[theme="dark"] .table > tfoot > 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 > td:before,
|
||||||
body[theme="dark"] .table > thead > tr.node-group-cell > th:before{
|
body[theme="dark"] .table > thead > tr.node-group-cell > th:before {
|
||||||
background-color: rgba(58, 62, 65, 1);
|
background-color: rgba(58, 62, 65, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +74,7 @@ body[theme="dark"] .table-striped tbody > tr.odd > th {
|
|||||||
background-color: rgba(34, 35, 46, 1);
|
background-color: rgba(34, 35, 46, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .table-hover > tbody > tr:hover{
|
body[theme="dark"] .table-hover > tbody > tr:hover {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,18 +83,23 @@ body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* expandRow展开部分样式 */
|
/* expandRow展开部分样式 */
|
||||||
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before{
|
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before {
|
||||||
background-color: rgba(34, 35, 46, 1);
|
background-color: rgba(34, 35, 46, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before{
|
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before {
|
||||||
background-color: rgba(28, 29, 38, 1);
|
background-color: rgba(28, 29, 38, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[theme="dark"] .plan {
|
||||||
|
background-image: none;
|
||||||
|
background-color: rgba(255, 255, 255, 0.075);
|
||||||
|
}
|
||||||
/* expandRow展开部分样式结束 */
|
/* expandRow展开部分样式结束 */
|
||||||
|
|
||||||
body[theme="dark"] .progress {
|
body[theme="dark"] .progress {
|
||||||
background-image: linear-gradient(#2c2c2c 0,rgba(28, 29, 38, 1) 100%);
|
background-image: none;
|
||||||
background-color: rgba(28, 29, 38, 1);
|
background-color: rgba(255, 255, 255, 0.075);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .progress-bar {
|
body[theme="dark"] .progress-bar {
|
||||||
@@ -150,29 +160,29 @@ body[theme="dark"] footer p a:hover {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .modal-content{
|
body[theme="dark"] .modal-content {
|
||||||
background-color: rgba(28, 29, 38, 1);
|
background-color: rgba(28, 29, 38, 1);
|
||||||
color: #f1f1f1;
|
color: #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] #earthChartBox .modal-content{
|
body[theme="dark"] #earthChartBox .modal-content {
|
||||||
background-color: rgba(0, 0, 0, 1);
|
background-color: rgba(0, 0, 0, 1);
|
||||||
color: #f1f1f1;
|
color: #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .modal-header{
|
body[theme="dark"] .modal-header {
|
||||||
border-bottom-color:#080808;
|
border-bottom-color:#080808;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .modal-header i.xclose{
|
body[theme="dark"] .modal-header i.xclose {
|
||||||
color: #f1f1f1;
|
color: #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .toolbox i{
|
body[theme="dark"] .toolbox i {
|
||||||
color: rgba(241, 241, 241, 1);
|
color: rgba(241, 241, 241, 1);
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .network-box .network-box-header{
|
body[theme="dark"] .network-box .network-box-header {
|
||||||
border-bottom: 1px solid rgba(110, 112, 121, 0.25);
|
border-bottom-color: rgba(110, 112, 121, 0.25);
|
||||||
}
|
}
|
||||||
+3
-3
@@ -86,7 +86,7 @@ body[theme="dark"] .table > thead > tr.node-group-tag > th:before {
|
|||||||
body[theme="dark"] .table > tbody > tr > td:before,
|
body[theme="dark"] .table > tbody > tr > td:before,
|
||||||
body[theme="dark"] .table > tfoot > 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 > td:before,
|
||||||
body[theme="dark"] .table > thead > tr.node-group-cell > th:before{
|
body[theme="dark"] .table > thead > tr.node-group-cell > th:before {
|
||||||
/** border-bottom 颜色 **/
|
/** border-bottom 颜色 **/
|
||||||
background-color: rgba(155, 155, 155, 0.1);
|
background-color: rgba(155, 155, 155, 0.1);
|
||||||
}
|
}
|
||||||
@@ -96,11 +96,11 @@ body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* expandRow展开部分样式 */
|
/* expandRow展开部分样式 */
|
||||||
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before{
|
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before{
|
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
}
|
}
|
||||||
/* expandRow展开部分样式结束 */
|
/* expandRow展开部分样式结束 */
|
||||||
|
|||||||
+26
-14
@@ -16,10 +16,15 @@ body[theme="light"] .navbar .navbar-brand {
|
|||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-nav > li > a{
|
body[theme="light"] .navbar .navbar-nav > li > a {
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[theme="light"] .navbar .navbar-nav li.pc-active a,
|
||||||
|
body[theme="light"] .navbar .navbar-nav li.m-active a {
|
||||||
|
color: rgba(73, 146, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-nav > .open > a:focus,
|
body[theme="light"] .navbar .navbar-nav > .open > a:focus,
|
||||||
body[theme="light"] .navbar .navbar-nav > .open > a:hover,
|
body[theme="light"] .navbar .navbar-nav > .open > a:hover,
|
||||||
body[theme="light"] .navbar .navbar-nav > .active > a,
|
body[theme="light"] .navbar .navbar-nav > .active > a,
|
||||||
@@ -42,16 +47,16 @@ body[theme="light"] .navbar .navbar-nav .open .dropdown-menu > li > a {
|
|||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-toggle{
|
body[theme="light"] .navbar .navbar-toggle {
|
||||||
border:none;
|
border:none;
|
||||||
padding:9px 12px;
|
padding:9px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-collapse{
|
body[theme="light"] .navbar .navbar-collapse {
|
||||||
border-color:rgba(0, 0, 0, 0.085);
|
border-color:rgba(0, 0, 0, 0.085);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-toggle .icon-bar{
|
body[theme="light"] .navbar .navbar-toggle .icon-bar {
|
||||||
background-color: rgba(0, 0, 0, 0.87);
|
background-color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,12 +95,12 @@ body[theme="light"] .table-striped tbody > tr.odd > th {
|
|||||||
body[theme="light"] .table > tbody > tr > td:before,
|
body[theme="light"] .table > tbody > tr > td:before,
|
||||||
body[theme="light"] .table > tfoot > 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 > td:before,
|
||||||
body[theme="light"] .table > thead > tr.node-group-cell > th:before{
|
body[theme="light"] .table > thead > tr.node-group-cell > th:before {
|
||||||
background-color: rgba(221, 221, 221, 1);
|
background-color: rgba(221, 221, 221, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .table > tbody > tr.expandRow > td:before,
|
body[theme="light"] .table > tbody > tr.expandRow > td:before,
|
||||||
body[theme="light"] .table > thead > tr.node-group-tag > th:before{
|
body[theme="light"] .table > thead > tr.node-group-tag > th:before {
|
||||||
background-color: rgba(255, 255, 255, 1);
|
background-color: rgba(255, 255, 255, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +116,13 @@ body[theme="light"] tr.odd.expandRow > :hover {
|
|||||||
background: #ffffff !important;
|
background: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[theme="light"] .plan {
|
||||||
|
color: #000000;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
|
||||||
|
}
|
||||||
|
|
||||||
body[theme="light"] .progress-bar {
|
body[theme="light"] .progress-bar {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
}
|
}
|
||||||
@@ -169,36 +181,36 @@ body[theme="light"] footer p a:hover {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .toolbox i{
|
body[theme="light"] .toolbox i {
|
||||||
color: rgba(94, 114, 228, .9);
|
color: rgba(94, 114, 228, .9);
|
||||||
background-color: rgba(255, 255, 255, .5);
|
background-color: rgba(255, 255, 255, .5);
|
||||||
box-shadow:0 .125rem .25rem rgba(0, 0, 0, .15) !important;
|
box-shadow:0 .125rem .25rem rgba(0, 0, 0, .15) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .toolbox .mytoggle .tooltip-inner{
|
body[theme="light"] .toolbox .mytoggle .tooltip-inner {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .15) !important;
|
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .15) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .toolbox .mytoggle li:hover{
|
body[theme="light"] .toolbox .mytoggle li:hover {
|
||||||
background-color:#e6e6e6;
|
background-color:#e6e6e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .modal-content{
|
body[theme="light"] .modal-content {
|
||||||
background-color: rgba(249, 249, 249, 1);
|
background-color: rgba(249, 249, 249, 1);
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .modal-header{
|
body[theme="light"] .modal-header {
|
||||||
border-bottom-color:#f1f1f1;
|
border-bottom-color:#f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .modal-header i.xclose{
|
body[theme="light"] .modal-header i.xclose {
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .network-box .network-box-header{
|
body[theme="light"] .network-box .network-box-header {
|
||||||
border-bottom: 1px solid rgba(224, 230, 241, 0.6);
|
border-bottom-color: rgba(224, 230, 241, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 767px) {
|
@media only screen and (max-width: 767px) {
|
||||||
|
|||||||
+12
-7
@@ -36,16 +36,16 @@ body[theme="light"] .navbar .dropdown-menu {
|
|||||||
|
|
||||||
|
|
||||||
body[theme="light"] .navbar .dropdown-menu > li > a:focus,
|
body[theme="light"] .navbar .dropdown-menu > li > a:focus,
|
||||||
body[theme="light"] .navbar .dropdown-menu > li > a:hover{
|
body[theme="light"] .navbar .dropdown-menu > li > a:hover {
|
||||||
/** 二级导航鼠标悬停选中背景 **/
|
/** 二级导航鼠标悬停选中背景 **/
|
||||||
background-color: rgba(0, 0, 0, .05);
|
background-color: rgba(0, 0, 0, .05);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-collapse{
|
body[theme="light"] .navbar .navbar-collapse {
|
||||||
border-color:rgba(0, 0, 0, 0.085);
|
border-color:rgba(0, 0, 0, 0.085);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-toggle .icon-bar{
|
body[theme="light"] .navbar .navbar-toggle .icon-bar {
|
||||||
background-color: rgba(0, 0, 0, 0.9);
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ body[theme="light"] .table > thead > tr.node-group-tag > th:before {
|
|||||||
body[theme="light"] .table > tbody > tr > td:before,
|
body[theme="light"] .table > tbody > tr > td:before,
|
||||||
body[theme="light"] .table > tfoot > 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 > td:before,
|
||||||
body[theme="light"] .table > thead > tr.node-group-cell > th:before{
|
body[theme="light"] .table > thead > tr.node-group-cell > th:before {
|
||||||
/** border-bottom 颜色 **/
|
/** border-bottom 颜色 **/
|
||||||
background-color: rgba(0, 0, 0, 0.075);
|
background-color: rgba(0, 0, 0, 0.075);
|
||||||
}
|
}
|
||||||
@@ -97,13 +97,18 @@ body[theme="light"] .table-hover > tbody > tr:not(.expandRow):hover > td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* expandRow展开部分样式 */
|
/* expandRow展开部分样式 */
|
||||||
body[theme="light"] .table > tbody > tr.expandRow.odd > td:before{
|
body[theme="light"] .table > tbody > tr.expandRow.odd > td:before {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .table > tbody > tr.expandRow.even > td:before{
|
body[theme="light"] .table > tbody > tr.expandRow.even > td:before {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[theme="light"] .plan {
|
||||||
|
background-image: none;
|
||||||
|
background-color: rgba(0, 0, 0, 0.015);
|
||||||
|
}
|
||||||
/* expandRow展开部分样式结束 */
|
/* expandRow展开部分样式结束 */
|
||||||
|
|
||||||
body[theme="light"] .progress {
|
body[theme="light"] .progress {
|
||||||
@@ -123,7 +128,7 @@ 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%);
|
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{
|
body[theme="light"] .toolbox i {
|
||||||
background-color: rgba(255, 255, 255, .55);
|
background-color: rgba(255, 255, 255, .55);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+95
-38
@@ -9,6 +9,10 @@ body {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container-fluid::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.nezha {
|
.nezha {
|
||||||
min-height: calc(100vh - 100px);
|
min-height: calc(100vh - 100px);
|
||||||
}
|
}
|
||||||
@@ -246,6 +250,40 @@ tr.accordion-toggle{
|
|||||||
max-width: 50px;
|
max-width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-cell.remaining {
|
||||||
|
min-width: 65px;
|
||||||
|
max-width: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-cell.remaining .additional {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-cell.remaining .additional small{
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 85%;
|
||||||
|
margin-right: 2px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-route, .extra {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.temp-detail {
|
.temp-detail {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -318,15 +356,17 @@ td.ping-network-quality {
|
|||||||
/* 服务页 正文结束 */
|
/* 服务页 正文结束 */
|
||||||
|
|
||||||
/* 网络页 正文*/
|
/* 网络页 正文*/
|
||||||
.network-box .btn-group.open .dropdown-toggle{
|
.network-box .btn-group.open .dropdown-toggle {
|
||||||
box-shadow:unset;
|
box-shadow:unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-box .network-box-header{
|
.network-box .network-box-header {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
padding: 5px 0px 15px 5px;
|
padding: 5px 0px 15px 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-box .network-box-header .dropdown-menu {
|
.network-box .network-box-header .dropdown-menu {
|
||||||
@@ -341,12 +381,12 @@ td.ping-network-quality {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-box .network-box-header .dropdown-menu li{
|
.network-box .network-box-header .dropdown-menu li {
|
||||||
height: 2em;
|
height: 2em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-box .network-box-header .dropdown-menu li.input-group{
|
.network-box .network-box-header .dropdown-menu li.input-group {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@@ -354,7 +394,7 @@ td.ping-network-quality {
|
|||||||
z-index: 99999997;
|
z-index: 99999997;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-box .network-box-header .dropdown-menu li.input-group input{
|
.network-box .network-box-header .dropdown-menu li.input-group input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@@ -377,12 +417,13 @@ td.ping-network-quality {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.network-box .chartTitle {
|
.network-box .chartTitle {
|
||||||
|
cursor: pointer;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
margin: 18px 0px 15px 0px;
|
margin: 18px 0px 15px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-box .chartTitle i.chartCountryCode{
|
.network-box .chartTitle i.chartCountryCode {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border-radius: 12.5%;
|
border-radius: 12.5%;
|
||||||
}
|
}
|
||||||
@@ -390,17 +431,17 @@ td.ping-network-quality {
|
|||||||
/* 网络页 正文结束 */
|
/* 网络页 正文结束 */
|
||||||
|
|
||||||
/* 地图版服务器分布图 */
|
/* 地图版服务器分布图 */
|
||||||
#mapChartBox{
|
#mapChartBox {
|
||||||
z-index: 999999999;
|
z-index: 999999999;
|
||||||
padding-left: 0 !important;
|
padding-left: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-open .modal{
|
.modal-open .modal {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-dialog-centered{
|
.modal-dialog-centered {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -410,7 +451,7 @@ td.ping-network-quality {
|
|||||||
min-height: calc(100%) /* 确保模态框占据整个视口高度 */
|
min-height: calc(100%) /* 确保模态框占据整个视口高度 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content{
|
.modal-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
@@ -418,15 +459,15 @@ td.ping-network-quality {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header{
|
.modal-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header h4{
|
.modal-header h4 {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header i.xclose{
|
.modal-header i.xclose {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
@@ -434,32 +475,32 @@ td.ping-network-quality {
|
|||||||
right: 10px;
|
right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body{
|
.modal-body {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.template-item, a.cdn-item{
|
a.template-item, a.cdn-item {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer{
|
footer {
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer p{
|
footer p {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 1200px) {
|
@media only screen and (max-width: 1200px) {
|
||||||
.accordian-body{
|
.accordian-body {
|
||||||
margin: 5px 0px 5px 10px;
|
margin: 5px 0px 5px 10px;
|
||||||
}
|
}
|
||||||
table.monitor-detail{
|
table.monitor-detail {
|
||||||
margin:0px 0px 2px 0px;
|
margin:0px 0px 2px 0px;
|
||||||
}
|
}
|
||||||
.table .network-thead-tr th,
|
.table .network-thead-tr th,
|
||||||
.table .node-group-tag th{
|
.table .node-group-tag th {
|
||||||
font-size:16px;
|
font-size:16px;
|
||||||
padding-bottom:6px;
|
padding-bottom:6px;
|
||||||
}
|
}
|
||||||
@@ -470,23 +511,27 @@ footer p{
|
|||||||
background-image: url(/static/theme-server-status/img/rb.png);
|
background-image: url(/static/theme-server-status/img/rb.png);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media only screen and (max-width: 767px) {
|
@media only screen and (max-width: 767px) {
|
||||||
body {
|
body {
|
||||||
font-size: 10px !important;
|
font-size: 10px !important;
|
||||||
padding-top:60px !important;
|
padding-top:60px !important;
|
||||||
}
|
}
|
||||||
.nezha {
|
.nezha {
|
||||||
min-height: calc(100vh - 80px);
|
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 {
|
.content {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
footer{
|
footer {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
.cycle-transfer-stats .dataTables_wrapper{
|
.cycle-transfer-stats .dataTables_wrapper {
|
||||||
margin-bottom: -10px;
|
margin-bottom: -10px;
|
||||||
}
|
}
|
||||||
.navbar .navbar-nav .open .dropdown-menu>li>a {
|
.navbar .navbar-nav .open .dropdown-menu>li>a {
|
||||||
@@ -508,50 +553,61 @@ footer p{
|
|||||||
}
|
}
|
||||||
.node-cell.os,
|
.node-cell.os,
|
||||||
.node-cell.uptime,
|
.node-cell.uptime,
|
||||||
.node-cell.traffic{
|
.node-cell.traffic {
|
||||||
display: none;
|
display: none;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
.node-cell.location{
|
.node-cell.location {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
.node-cell-os-text,.node-cell-location-text{
|
.node-cell-os-text,.node-cell-location-text {
|
||||||
display: none;
|
display: none;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
.accordian-body{
|
.node-cell.network {
|
||||||
|
min-width: 95px;
|
||||||
|
max-width: 95px;
|
||||||
|
}
|
||||||
|
.node-cell.remaining {
|
||||||
|
min-width: 75px;
|
||||||
|
max-width: 75px;
|
||||||
|
}
|
||||||
|
.plan {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.accordian-body {
|
||||||
margin: 5px 0px 5px 10px;
|
margin: 5px 0px 5px 10px;
|
||||||
}
|
}
|
||||||
table.monitor-detail{
|
table.monitor-detail {
|
||||||
margin:0px 0px 2px 0px;
|
margin:0px 0px 2px 0px;
|
||||||
}
|
}
|
||||||
.table .network-thead-tr th,
|
.table .network-thead-tr th,
|
||||||
.table .node-group-tag th{
|
.table .node-group-tag th {
|
||||||
font-size:14px;
|
font-size:14px;
|
||||||
padding-bottom:6px;
|
padding-bottom:6px;
|
||||||
}
|
}
|
||||||
.table tr.network td{
|
.table tr.network td {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.service-status .service-status-th{
|
.service-status .service-status-th {
|
||||||
min-width:30px;
|
min-width:30px;
|
||||||
}
|
}
|
||||||
.service-status .delay-today{
|
.service-status .delay-today {
|
||||||
margin-top:4px;
|
margin-top:4px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.service-status .delay-today i{
|
.service-status .delay-today i {
|
||||||
margin-right:0px;
|
margin-right:0px;
|
||||||
}
|
}
|
||||||
.service-status .delay-today-text{
|
.service-status .delay-today-text {
|
||||||
display: none;
|
display: none;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
.service-status .service-averagelatency-th{
|
.service-status .service-averagelatency-th {
|
||||||
min-width:70px;
|
min-width:70px;
|
||||||
}
|
}
|
||||||
.service-status .service-30daysonline-th{
|
.service-status .service-30daysonline-th {
|
||||||
min-width:75px;
|
min-width:75px;
|
||||||
}
|
}
|
||||||
.toolbox {
|
.toolbox {
|
||||||
@@ -563,12 +619,13 @@ footer p{
|
|||||||
.network-box .network-box-header {
|
.network-box .network-box-header {
|
||||||
margin: 8px 0px 0px 8px;
|
margin: 8px 0px 0px 8px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
.network-box .chartTitle {
|
.network-box .chartTitle {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 10px 0px 10px 0px;
|
margin: 10px 0px 10px 0px;
|
||||||
}
|
}
|
||||||
.network-box .chartTitle i.chartCountryCode{
|
.network-box .chartTitle i.chartCountryCode {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
border-radius: 12.5%;
|
border-radius: 12.5%;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -26,7 +26,9 @@ const mixinsVue = {
|
|||||||
this.semiTransparent = this.initSemiTransparent();
|
this.semiTransparent = this.initSemiTransparent();
|
||||||
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
|
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
|
||||||
this.colors = this.theme == "dark" ? this.colorsDark : this.colorsLight;
|
this.colors = this.theme == "dark" ? this.colorsDark : this.colorsLight;
|
||||||
|
this.setBenchmarkHeight();
|
||||||
window.addEventListener('scroll', this.handleScroll);
|
window.addEventListener('scroll', this.handleScroll);
|
||||||
|
window.addEventListener('resize', this.setBenchmarkHeight());
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
window.removeEventListener('scroll', this.handleScroll);
|
window.removeEventListener('scroll', this.handleScroll);
|
||||||
@@ -152,6 +154,15 @@ const mixinsVue = {
|
|||||||
},
|
},
|
||||||
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
|
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
|
||||||
return window.innerWidth <= 768;
|
return window.innerWidth <= 768;
|
||||||
|
},
|
||||||
|
isMenuActive(page){
|
||||||
|
if(page == this.$root.page) {
|
||||||
|
return this.isMobile ? 'm-active' : 'pc-active';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setBenchmarkHeight() {
|
||||||
|
let vh = window.innerHeight * 0.01;
|
||||||
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
+2
-1
@@ -10,12 +10,13 @@
|
|||||||
<script src="https://unpkg.com/[email protected]/dist/semantic.min.js"></script>
|
<script src="https://unpkg.com/[email protected]/dist/semantic.min.js"></script>
|
||||||
<script src="/static/semantic-ui-alerts.min.js"></script>
|
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||||
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
|
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
|
||||||
<script src="/static/main.js?v20240714"></script>
|
<script src="/static/main.js?v20241011"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
updateLang({{.LANG }});
|
updateLang({{.LANG }});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
Vendored
+6
-1
@@ -11,7 +11,12 @@
|
|||||||
<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]/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="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/semantic-ui-alerts.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314">
|
<link rel="stylesheet" type="text/css" href="/static/main.css?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>
|
</head>
|
||||||
|
{{if ts .CustomCodeDashboard}} {{.CustomCodeDashboard|safe}} {{end}}
|
||||||
<body>
|
<body>
|
||||||
{{end}}
|
{{end}}
|
||||||
Vendored
+1
@@ -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 "/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 "/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 "/notification"}} active{{end}}' href="/notification"><i class="bell icon"></i>{{tr "Notification"}}</a>
|
||||||
|
<a class='item{{if eq .MatchedPath "/ddns"}} active{{end}}' href="/ddns"><i class="globe icon"></i>{{tr "DDNS"}}</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 "/nat"}} active{{end}}' href="/nat"><i class="exchange icon"></i>{{tr "NAT"}}</a>
|
||||||
<a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting">
|
<a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting">
|
||||||
<i class="settings icon"></i>{{tr "Settings"}}
|
<i class="settings icon"></i>{{tr "Settings"}}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class=" actions">
|
<div class="actions">
|
||||||
<div class="ui negative button">{{tr "Cancel"}}</div>
|
<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 class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+1
-1
@@ -60,7 +60,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class=" actions">
|
<div class="actions">
|
||||||
<div class="ui negative button">{{tr "Cancel"}}</div>
|
<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 class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
{{define "component/ddns"}}
|
||||||
|
<div class="ui tiny ddns modal transition hidden">
|
||||||
|
<div class="header">Add</div>
|
||||||
|
<div class="content">
|
||||||
|
<form id="ddnsForm" 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>{{tr "DDNSProvider"}}</label>
|
||||||
|
<select name="Provider" class="ui fluid dropdown" id="providerSelect" onchange="toggleFields()">
|
||||||
|
{{ range $provider := .ProviderList }}
|
||||||
|
<option value="{{ $provider.ID }}">
|
||||||
|
{{ $provider.Name }}
|
||||||
|
</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "DDNSDomains"}}</label>
|
||||||
|
<input type="text" name="DomainsRaw" placeholder="www.example.com">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "DDNSAccessID"}}</label>
|
||||||
|
<input type="text" name="AccessID" placeholder="{{tr "DDNSTokenID"}}">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "DDNSAccessSecret"}}</label>
|
||||||
|
<input type="text" name="AccessSecret" placeholder="{{tr "DDNSTokenSecret"}}">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "MaxRetries"}}</label>
|
||||||
|
<input type="number" name="MaxRetries" placeholder="3">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "WebhookURL"}}</label>
|
||||||
|
<input type="text" name="WebhookURL" placeholder="https://ddns.example.com/?record=#record#">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "WebhookMethod"}}</label>
|
||||||
|
<select name="WebhookMethod" class="ui fluid dropdown">
|
||||||
|
<option value="1">GET</option>
|
||||||
|
<option value="2">POST</option>
|
||||||
|
<option value="3">PATCH</option>
|
||||||
|
<option value="4">DELETE</option>
|
||||||
|
<option value="5">PUT</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "WebhookRequestType"}}</label>
|
||||||
|
<select name="WebhookRequestType" class="ui fluid dropdown">
|
||||||
|
<option value="1">JSON</option>
|
||||||
|
<option value="2">Form</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "WebhookHeaders"}}</label>
|
||||||
|
<textarea name="WebhookHeaders" placeholder='{"User-Agent":"Nezha-Agent"}'></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "WebhookRequestBody"}}</label>
|
||||||
|
<textarea name="WebhookRequestBody" placeholder='{ "ip": #ip#, "domain": "#domain#" }'></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui enableipv4 checkbox">
|
||||||
|
<input name="EnableIPv4" type="checkbox" tabindex="0" class="hidden">
|
||||||
|
<label>{{tr "EnableIPv4"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui enableipv6 checkbox">
|
||||||
|
<input name="EnableIPv6" type="checkbox" tabindex="0" class="hidden">
|
||||||
|
<label>{{tr "EnableIPv6"}}</label>
|
||||||
|
</div>
|
||||||
|
</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}}
|
||||||
Vendored
+3
-3
@@ -13,11 +13,11 @@
|
|||||||
<input type="number" name="ServerID" placeholder="1">
|
<input type="number" name="ServerID" placeholder="1">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>内网服务</label>
|
<label>{{tr "LocalService"}}</label>
|
||||||
<input type="text" name="Host" placeholder="192.168.1.1:80(带端口)">
|
<input type="text" name="Host" placeholder="{{tr "LocalServicePlaceholder"}}">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>绑定域名</label>
|
<label>{{tr "BindHostname"}}</label>
|
||||||
<input type="text" name="Domain" placeholder="router.app.yourdomain.com">
|
<input type="text" name="Domain" placeholder="router.app.yourdomain.com">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+20
-27
@@ -20,42 +20,35 @@
|
|||||||
<label>{{tr "Secret"}}</label>
|
<label>{{tr "Secret"}}</label>
|
||||||
<input type="text" name="secret">
|
<input type="text" name="secret">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "DDNSProfiles"}}</label>
|
||||||
|
<div class="ui fluid multiple ddns search selection dropdown">
|
||||||
|
<input type="hidden" name="DDNSProfilesRaw">
|
||||||
|
<i class="dropdown icon ddnsProfiles"></i>
|
||||||
|
<div class="default text">{{tr "EnterIdAndNameToSearch"}}</div>
|
||||||
|
<div class="menu"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui enableddns checkbox">
|
||||||
|
<input name="EnableDDNS" type="checkbox" tabindex="0" class="hidden" />
|
||||||
|
<label>{{tr "EnableDDNS"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui hideforguest checkbox">
|
<div class="ui hideforguest checkbox">
|
||||||
<input name="HideForGuest" type="checkbox" tabindex="0" class="hidden" />
|
<input name="HideForGuest" type="checkbox" tabindex="0" class="hidden" />
|
||||||
<label>{{tr "HideForGuest"}}</label>
|
<label>{{tr "HideForGuest"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<div class="ui enableddns checkbox">
|
|
||||||
<input name="EnableDDNS" type="checkbox" tabindex="0" />
|
|
||||||
<label>{{tr "EnableDDNS"}}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui enableipv4 checkbox">
|
|
||||||
<input name="EnableIPv4" type="checkbox" tabindex="0" />
|
|
||||||
<label>{{tr "EnableIPv4"}}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui enableipv6 checkbox">
|
|
||||||
<input name="EnableIpv6" type="checkbox" tabindex="0" />
|
|
||||||
<label>{{tr "EnableIpv6"}}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>{{tr "DDNSDomain"}}</label>
|
|
||||||
<input type="text" name="DDNSDomain" placeholder="{{tr "DDNSDomain"}}">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>{{tr "DDNSProfile"}}</label>
|
|
||||||
<input type="text" name="DDNSProfile" placeholder="{{tr "DDNSProfile"}}">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{tr "Note"}}</label>
|
<label>{{tr "Note"}}</label>
|
||||||
<textarea name="Note"></textarea>
|
<textarea name="Note"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "PublicNote"}}</label>
|
||||||
|
<textarea name="PublicNote"></textarea>
|
||||||
|
</div>
|
||||||
<div class="command field">
|
<div class="command field">
|
||||||
<label>{{tr "LinuxOneKeyInstall"}}</label>
|
<label>{{tr "LinuxOneKeyInstall"}}</label>
|
||||||
<div class="ui message">
|
<div class="ui message">
|
||||||
@@ -71,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class=" actions">
|
<div class="actions">
|
||||||
<div class="ui negative button">{{tr "Cancel"}}</div>
|
<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 class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="ui very basic table">
|
<table class="ui basic table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{tr "Token"}}</th>
|
<th>{{tr "Token"}}</th>
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="ui very basic table">
|
<table class="ui basic table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
|
|||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
{{define "dashboard-default/ddns"}}
|
||||||
|
{{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="addOrEditDDNS()"><i
|
||||||
|
class="add icon"></i> {{tr "AddDDNSProfile"}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class="ui basic table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>{{tr "Name"}}</th>
|
||||||
|
<th>{{tr "EnableIPv4"}}</th>
|
||||||
|
<th>{{tr "EnableIPv6"}}</th>
|
||||||
|
<th>{{tr "DDNSProvider"}}</th>
|
||||||
|
<th>{{tr "DDNSDomain"}}</th>
|
||||||
|
<th>{{tr "MaxRetries"}}</th>
|
||||||
|
<th>{{tr "Administration"}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range $item := .DDNS}}
|
||||||
|
<tr>
|
||||||
|
<td>{{$item.ID}}</td>
|
||||||
|
<td>{{$item.Name}}</td>
|
||||||
|
<td>{{$item.EnableIPv4}}</td>
|
||||||
|
<td>{{$item.EnableIPv6}}</td>
|
||||||
|
<td>{{index $.ProviderMap $item.Provider}}</td>
|
||||||
|
<td>{{$item.DomainsRaw}}</td>
|
||||||
|
<td>{{$item.MaxRetries}}</td>
|
||||||
|
<td>
|
||||||
|
<div class="ui mini icon buttons">
|
||||||
|
<button class="ui button" onclick="addOrEditDDNS({{$item}})">
|
||||||
|
<i class="edit icon"></i>
|
||||||
|
</button>
|
||||||
|
<button class="ui button"
|
||||||
|
onclick="showConfirm('确定删除DDNS配置?','确认删除',deleteRequest,'/api/ddns/'+{{$item.ID}})">
|
||||||
|
<i class="trash alternate outline icon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "component/ddns" .}}
|
||||||
|
{{template "common/footer" .}}
|
||||||
|
<script>
|
||||||
|
$('.checkbox').checkbox()
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
+495
@@ -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
@@ -9,7 +9,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="ui very basic table">
|
<table class="ui basic table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
|
|||||||
+3
-3
@@ -10,14 +10,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="ui very basic table">
|
<table class="ui basic table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>{{tr "Name"}}</th>
|
<th>{{tr "Name"}}</th>
|
||||||
<th>Agent ID</th>
|
<th>Agent ID</th>
|
||||||
<th>内网服务</th>
|
<th>{{tr "LocalService"}}</th>
|
||||||
<th>绑定域名</th>
|
<th>{{tr "BindHostname"}}</th>
|
||||||
<th>{{tr "Administration"}}</th>
|
<th>{{tr "Administration"}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="ui very basic table">
|
<table class="ui basic table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="ui very basic table">
|
<table class="ui basic table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
|
|||||||
+17
-13
@@ -19,7 +19,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="ui very basic table">
|
<table class="ui basic table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><button onclick="checkAllServer()" class="ui mini nezha-primary-btn button">{{tr "SelectAll"}}</button></th>
|
<th><button onclick="checkAllServer()" class="ui mini nezha-primary-btn button">{{tr "SelectAll"}}</button></th>
|
||||||
@@ -28,14 +28,12 @@
|
|||||||
<th>{{tr "ServerGroup"}}</th>
|
<th>{{tr "ServerGroup"}}</th>
|
||||||
<th>IP</th>
|
<th>IP</th>
|
||||||
<th>{{tr "VersionNumber"}}</th>
|
<th>{{tr "VersionNumber"}}</th>
|
||||||
<th>{{tr "HideForGuest"}}</th>
|
|
||||||
<th>{{tr "EnableDDNS"}}</th>
|
<th>{{tr "EnableDDNS"}}</th>
|
||||||
<th>{{tr "EnableIPv4"}}</th>
|
<th>{{tr "HideForGuest"}}</th>
|
||||||
<th>{{tr "EnableIpv6"}}</th>
|
|
||||||
<th>{{tr "DDNSDomain"}}</th>
|
|
||||||
<th>{{tr "Secret"}}</th>
|
<th>{{tr "Secret"}}</th>
|
||||||
<th>{{tr "OneKeyInstall"}}</th>
|
<th>{{tr "OneKeyInstall"}}</th>
|
||||||
<th>{{tr "Note"}}</th>
|
<th>{{tr "Note"}}</th>
|
||||||
|
<th>{{tr "PublicNote"}}</th>
|
||||||
<th>{{tr "Administration"}}</th>
|
<th>{{tr "Administration"}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -48,11 +46,8 @@
|
|||||||
<td>{{$server.Tag}}</td>
|
<td>{{$server.Tag}}</td>
|
||||||
<td>{{$server.Host.IP}}</td>
|
<td>{{$server.Host.IP}}</td>
|
||||||
<td>{{$server.Host.Version}}</td>
|
<td>{{$server.Host.Version}}</td>
|
||||||
<td>{{$server.HideForGuest}}</td>
|
|
||||||
<td>{{$server.EnableDDNS}}</td>
|
<td>{{$server.EnableDDNS}}</td>
|
||||||
<td>{{$server.EnableIPv4}}</td>
|
<td>{{$server.HideForGuest}}</td>
|
||||||
<td>{{$server.EnableIpv6}}</td>
|
|
||||||
<td>{{$server.DDNSDomain}}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
|
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
|
||||||
<i class="copy icon"></i>
|
<i class="copy icon"></i>
|
||||||
@@ -75,13 +70,22 @@
|
|||||||
<i class="apple icon"></i>
|
<i class="apple icon"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td style="word-break: break-word;white-space: pre-wrap;">{{$server.Note}}</td>
|
<td>
|
||||||
|
{{if $server.Note}}
|
||||||
|
<button class="ui icon green mini button" title="{{ $server.Note }}" onclick="addOrEditServer({{$server.MarshalForDashboard}})"><i class="sticky note icon"></i></button>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if $server.PublicNote}}
|
||||||
|
<button class="ui icon green mini button" title="{{$server.PublicNote}}" onclick="addOrEditServer({{$server.MarshalForDashboard}})"><i class="sticky clipboard icon"></i></button>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="ui mini icon buttons">
|
<div class="ui mini icon buttons">
|
||||||
<button class="ui button" onclick="connectToServer({{$server.ID}})">
|
<button class="ui button" onclick="connectToServer({{$server.ID}})">
|
||||||
<i class="terminal icon"></i>
|
<i class="terminal icon"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="ui button" onclick="addOrEditServer({{$server.Marshal}})">
|
<button class="ui button" onclick="addOrEditServer({{$server.MarshalForDashboard}})">
|
||||||
<i class="edit icon"></i>
|
<i class="edit icon"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="ui button"
|
<button class="ui button"
|
||||||
@@ -155,8 +159,8 @@
|
|||||||
}
|
}
|
||||||
function batchEditServerGroup() {
|
function batchEditServerGroup() {
|
||||||
let groupName = prompt('{{tr "InputServerGroupName"}}')
|
let groupName = prompt('{{tr "InputServerGroupName"}}')
|
||||||
if (!groupName) {
|
if (groupName === null) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const servers = []
|
const servers = []
|
||||||
checkBoxList.forEach(cb => {
|
checkBoxList.forEach(cb => {
|
||||||
|
|||||||
+11
-3
@@ -41,6 +41,10 @@
|
|||||||
<label>{{tr "CustomCodes"}}</label>
|
<label>{{tr "CustomCodes"}}</label>
|
||||||
<textarea name="CustomCode">{{.Conf.Site.CustomCode}}</textarea>
|
<textarea name="CustomCode">{{.Conf.Site.CustomCode}}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "CustomCodesDashboard"}}</label>
|
||||||
|
<textarea name="CustomCodeDashboard">{{.Conf.Site.CustomCodeDashboard}}</textarea>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{tr "AccessPassword"}}</label>
|
<label>{{tr "AccessPassword"}}</label>
|
||||||
<input type="text" name="ViewPassword" placeholder="" value="{{.Conf.Site.ViewPassword}}">
|
<input type="text" name="ViewPassword" placeholder="" value="{{.Conf.Site.ViewPassword}}">
|
||||||
@@ -49,6 +53,10 @@
|
|||||||
<label>{{tr "PanelServerDomainAndIP"}}</label>
|
<label>{{tr "PanelServerDomainAndIP"}}</label>
|
||||||
<input type="text" name="GRPCHost" placeholder="" value="{{.Conf.GRPCHost}}">
|
<input type="text" name="GRPCHost" placeholder="" value="{{.Conf.GRPCHost}}">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "CustomNameservers"}}</label>
|
||||||
|
<input type="text" name="CustomNameservers" placeholder="" value="{{.Conf.DNSServers}}">
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{tr "IPChangeAlert"}}</label>
|
<label>{{tr "IPChangeAlert"}}</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,13 +136,13 @@
|
|||||||
})
|
})
|
||||||
$('.checkbox').checkbox()
|
$('.checkbox').checkbox()
|
||||||
$('#settingForm').find("select[name=Cover]").val({{.Conf.Cover }});
|
$('#settingForm').find("select[name=Cover]").val({{.Conf.Cover }});
|
||||||
{{if .Conf.EnableIPChangeNotification}}
|
{{ if .Conf.EnableIPChangeNotification}}
|
||||||
$('.checkbox.ip-change').checkbox('set checked')
|
$('.checkbox.ip-change').checkbox('set checked')
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{if .Conf.EnablePlainIPInNotification}}
|
{{ if .Conf.EnablePlainIPInNotification}}
|
||||||
$('.checkbox.plain-ip').checkbox('set checked')
|
$('.checkbox.plain-ip').checkbox('set checked')
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{if .Conf.DisableSwitchTemplateInFrontend }}
|
{{ if .Conf.DisableSwitchTemplateInFrontend }}
|
||||||
$('.checkbox.disable-switch-template').checkbox('set checked')
|
$('.checkbox.disable-switch-template').checkbox('set checked')
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+64
-2
@@ -1,6 +1,7 @@
|
|||||||
{{define "dashboard-default/terminal"}}
|
{{define "dashboard-default/terminal"}}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{.Conf.Language}}">
|
<html lang="{{.Conf.Language}}">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
@@ -22,17 +23,53 @@
|
|||||||
body {
|
body {
|
||||||
background-color: black;
|
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>
|
</style>
|
||||||
|
|
||||||
<body onresize="onResize()">
|
<body onresize="onResize()">
|
||||||
<div id="terminal-container"></div>
|
<div id="terminal-container"></div>
|
||||||
|
<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/[email protected]/lib/xterm.js"></script>
|
||||||
<script src="https://unpkg.com/@xterm/[email protected]/lib/addon-attach.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-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>
|
<script>
|
||||||
let sendResizing = false;
|
let sendResizing = false;
|
||||||
|
|
||||||
function doResize() {
|
function doResize() {
|
||||||
fitAddon.fit()
|
fitAddon.fit()
|
||||||
|
|
||||||
const w = fitAddon.proposeDimensions();
|
const w = fitAddon.proposeDimensions();
|
||||||
const prefix = new Int8Array([1]);
|
const prefix = new Int8Array([1]);
|
||||||
const resizeMessage = new TextEncoder().encode(JSON.stringify({
|
const resizeMessage = new TextEncoder().encode(JSON.stringify({
|
||||||
@@ -70,10 +107,13 @@
|
|||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
});
|
});
|
||||||
const socket = new WebSocket((window.location.protocol == 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/terminal/' + '{{.SessionID}}');
|
const socket = new WebSocket((window.location.protocol == 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/terminal/' + '{{.SessionID}}');
|
||||||
|
|
||||||
const attachAddon = new AttachAddon.AttachAddon(socket);
|
const attachAddon = new AttachAddon.AttachAddon(socket);
|
||||||
const fitAddon = new FitAddon.FitAddon();
|
|
||||||
term.loadAddon(attachAddon);
|
term.loadAddon(attachAddon);
|
||||||
|
|
||||||
|
const fitAddon = new FitAddon.FitAddon();
|
||||||
term.loadAddon(fitAddon);
|
term.loadAddon(fitAddon);
|
||||||
|
|
||||||
term.open(document.getElementById('terminal-container'));
|
term.open(document.getElementById('terminal-container'));
|
||||||
|
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
@@ -88,7 +128,29 @@
|
|||||||
socket.onerror = () => {
|
socket.onerror = () => {
|
||||||
alert('{{tr "TerminalConnectionFailed"}}')
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
|
||||||
<script src="/static/semantic-ui-alerts.min.js"></script>
|
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||||
<script src="/static/main.js?v20240330"></script>
|
<script src="/static/main.js?v20241011"></script>
|
||||||
<script src="/static/theme-default/js/mixin.js?v20240302"></script>
|
<script src="/static/theme-default/js/mixin.js?v20240302"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
+16
-15
@@ -261,22 +261,23 @@
|
|||||||
const x = readableBytes(bs)
|
const x = readableBytes(bs)
|
||||||
return x != "NaN undefined" ? x : '0B'
|
return x != "NaN undefined" ? x : '0B'
|
||||||
},
|
},
|
||||||
getCoreAndGHz(str){
|
getCoreAndGHz(arr) {
|
||||||
if((str || []).hasOwnProperty(0) === false){
|
if ((arr || []).length === 0) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
str = str[0];
|
let totalCores = 0;
|
||||||
let GHz = str.match(/(\d|\.)+GHz/g);
|
arr.forEach(str => {
|
||||||
let Core = str.match(/(\d|\.)+ Physical/g);
|
let coreMatch = str.match(/(\d+(\.\d+)?) Physical/g);
|
||||||
GHz = GHz!==null?GHz.hasOwnProperty(0)===false?'':GHz[0]:''
|
let coreCount = 0;
|
||||||
Core = Core!==null?Core.hasOwnProperty(0)===false?'?':Core[0]:'?'
|
if (coreMatch) {
|
||||||
if(Core === '?'){
|
coreCount = parseFloat(coreMatch[0]);
|
||||||
let Core = str.match(/(\d|\.)+ Virtual/g);
|
} else {
|
||||||
Core = Core!==null?Core.hasOwnProperty(0)===false?'?':Core[0]:'?'
|
let coreMatch = str.match(/(\d+(\.\d+)?) Virtual/g);
|
||||||
return Core.replace('Virtual','Core')
|
coreCount = coreMatch ? parseFloat(coreMatch[0]) : 0;
|
||||||
}
|
}
|
||||||
return Core.replace('Physical','Core');
|
totalCores += coreCount;
|
||||||
|
});
|
||||||
|
return `${totalCores} Cores`;
|
||||||
},
|
},
|
||||||
getByteToGB(bs){
|
getByteToGB(bs){
|
||||||
return (bs/1024/1024/1024).toFixed(2) + 'GB'
|
return (bs/1024/1024/1024).toFixed(2) + 'GB'
|
||||||
|
|||||||
+2
-4
@@ -10,14 +10,12 @@
|
|||||||
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
|
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
|
||||||
<script>
|
<script>
|
||||||
function showSwitchTemplate(list, currentBackendTheme) {
|
function showSwitchTemplate(list, currentBackendTheme) {
|
||||||
console.log(list, currentBackendTheme);
|
// console.log(list, currentBackendTheme);
|
||||||
console.log("currentBackendTheme:",currentBackendTheme);
|
// console.log("currentBackendTheme:",currentBackendTheme);
|
||||||
}
|
}
|
||||||
showSwitchTemplate({{ .Themes }}, {{ .Conf.Site.Theme }})
|
showSwitchTemplate({{ .Themes }}, {{ .Conf.Site.Theme }})
|
||||||
</script>
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<script>
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
+22
-8
@@ -1,26 +1,40 @@
|
|||||||
{{define "theme-default/header"}}
|
{{define "theme-default/header"}}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{.Conf.Language}}">
|
<html lang="{{.Conf.Language}}">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<title>{{ .Title }}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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 content="telephone=no" name="format-detection">
|
||||||
<title>{{.Title}}</title>
|
<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/semantic.min.css">
|
<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]/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]/font/bootstrap-icons.min.css">
|
||||||
<link rel="stylesheet" href="https://unpkg.com/[email protected]/css/flag-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/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/jquery.min.js"></script>
|
||||||
<script src="https://unpkg.com/[email protected]/dist/semantic.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/vue.min.js"></script>
|
||||||
<script src="https://unpkg.com/[email protected]/dist/echarts.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/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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
+18
-19
@@ -194,7 +194,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
mixins: [mixinsVue],
|
mixins: [mixinsVue],
|
||||||
created() {
|
created() {
|
||||||
this.servers = JSON.parse('{{.Servers}}').servers;
|
this.servers = JSON.parse('{{.Servers}}').servers;
|
||||||
this.group()
|
this.group()
|
||||||
},
|
},
|
||||||
@@ -240,12 +240,12 @@
|
|||||||
const itemHeight = isMobile ? 10 : 10;
|
const itemHeight = isMobile ? 10 : 10;
|
||||||
const gridLeft = 25;
|
const gridLeft = 25;
|
||||||
const gridRight = 12;
|
const gridRight = 12;
|
||||||
const fontColor = "rgba(0, 0, 0, 0.68)";
|
|
||||||
const backgroundColor = '';
|
const backgroundColor = '';
|
||||||
const borderColor = "#ffffff";
|
const borderColor = "#ffffff";
|
||||||
const chartData = this.chartDataList[id - 1];
|
const chartData = this.chartDataList[id - 1];
|
||||||
const chartContainer = this.$refs[`chart${id}`][0];
|
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',
|
renderer: 'canvas',
|
||||||
useDirtyRect: false,
|
useDirtyRect: false,
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
@@ -283,7 +283,6 @@
|
|||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
color: fontColor
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
@@ -292,7 +291,6 @@
|
|||||||
show: true,
|
show: true,
|
||||||
textStyle: {
|
textStyle: {
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
color: fontColor
|
|
||||||
},
|
},
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
cap: 'butt'
|
cap: 'butt'
|
||||||
@@ -332,7 +330,6 @@
|
|||||||
series: seriesData,
|
series: seriesData,
|
||||||
textStyle: {
|
textStyle: {
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
color: fontColor
|
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
top: '30',
|
top: '30',
|
||||||
@@ -477,21 +474,23 @@
|
|||||||
const sizes = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"];
|
const sizes = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"];
|
||||||
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + sizes[i];
|
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + sizes[i];
|
||||||
},
|
},
|
||||||
getCoreAndGHz(str){
|
getCoreAndGHz(arr) {
|
||||||
if ((str || []).hasOwnProperty(0) === false) {
|
if ((arr || []).length === 0) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
str = str[0];
|
let totalCores = 0;
|
||||||
let GHz = str.match(/(\d|\.)+GHz/g);
|
arr.forEach(str => {
|
||||||
let Core = str.match(/(\d|\.)+ Physical/g);
|
let coreMatch = str.match(/(\d+(\.\d+)?) Physical/g);
|
||||||
GHz = GHz !== null ? GHz.hasOwnProperty(0) === false ? '' : GHz[0] : ''
|
let coreCount = 0;
|
||||||
Core = Core !== null ? Core.hasOwnProperty(0) === false ? '?' : Core[0] : '?'
|
if (coreMatch) {
|
||||||
if (Core === '?') {
|
coreCount = parseFloat(coreMatch[0]);
|
||||||
let Core = str.match(/(\d|\.)+ Virtual/g);
|
} else {
|
||||||
Core = Core !== null ? Core.hasOwnProperty(0) === false ? '?' : Core[0] : '?'
|
let coreMatch = str.match(/(\d+(\.\d+)?) Virtual/g);
|
||||||
return Core.replace('Virtual', 'Core')
|
coreCount = coreMatch ? parseFloat(coreMatch[0]) : 0;
|
||||||
}
|
}
|
||||||
return Core.replace('Physical', 'Core');
|
totalCores += coreCount;
|
||||||
|
});
|
||||||
|
return `${totalCores} Cores`;
|
||||||
},
|
},
|
||||||
getK2Gb(bs){
|
getK2Gb(bs){
|
||||||
bs = bs / 1024 / 1024 / 1024;
|
bs = bs / 1024 / 1024 / 1024;
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a class='item' href="/"><i class="home icon"></i>{{tr "Home"}}</a>
|
<a class='item' href="/"><i class="home icon"></i>{{tr "Home"}}</a>
|
||||||
<template v-if="isMobile">
|
<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="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">
|
<div class="menu">
|
||||||
<a href="/service" class="item"><i class="rss icon"></i>{{tr "Services" }}</a>
|
<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>
|
<a href="/network" class="item"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a>
|
||||||
</template>
|
</template>
|
||||||
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
|
{{ 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="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">
|
<div class="menu">
|
||||||
<a v-for="(item, index) in adaptedTemplates" :key="index" @click="toggleTemplate(item.key)" class="item">
|
<a v-for="(item, index) in adaptedTemplates" :key="index" @click="toggleTemplate(item.key)" class="item">
|
||||||
|
|||||||
+27
-18
@@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui container">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -36,7 +36,11 @@
|
|||||||
defaultTemplate: {{.Conf.Site.Theme}},
|
defaultTemplate: {{.Conf.Site.Theme}},
|
||||||
templates: {{.Themes}},
|
templates: {{.Themes}},
|
||||||
servers: initData,
|
servers: initData,
|
||||||
option: {
|
option: {}
|
||||||
|
},
|
||||||
|
mixins: [mixinsVue],
|
||||||
|
created() {
|
||||||
|
this.option = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
position: function (pt) {
|
position: function (pt) {
|
||||||
@@ -65,21 +69,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: '8%',
|
left: this.isMobile ? '8%' : '3.8%',
|
||||||
right: '8%',
|
right: this.isMobile ? '8%' : '3.8%',
|
||||||
},
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
|
||||||
toolbox: {
|
|
||||||
feature: {
|
|
||||||
dataZoom: {
|
|
||||||
yAxisIndex: 'none'
|
|
||||||
},
|
|
||||||
restore: {},
|
|
||||||
saveAsImage: {}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
backgroundColor: '',
|
||||||
dataZoom: [
|
dataZoom: [
|
||||||
{
|
{
|
||||||
|
type: 'slider',
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 100
|
end: 100
|
||||||
}
|
}
|
||||||
@@ -93,10 +89,8 @@
|
|||||||
boundaryGap: false
|
boundaryGap: false
|
||||||
},
|
},
|
||||||
series: [],
|
series: [],
|
||||||
},
|
}
|
||||||
chartOnOff: true,
|
|
||||||
},
|
},
|
||||||
mixins: [mixinsVue],
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.renderChart();
|
this.renderChart();
|
||||||
this.parseMonitorInfo(monitorInfo);
|
this.parseMonitorInfo(monitorInfo);
|
||||||
@@ -236,6 +230,20 @@
|
|||||||
this.option.title.text = monitorInfo.result[0].server_name;
|
this.option.title.text = monitorInfo.result[0].server_name;
|
||||||
this.option.series = tSeries;
|
this.option.series = tSeries;
|
||||||
this.option.legend.data = tLegendData;
|
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.clear();
|
||||||
this.myChart.setOption(this.option);
|
this.myChart.setOption(this.option);
|
||||||
},
|
},
|
||||||
@@ -243,7 +251,8 @@
|
|||||||
return str.includes('Windows')
|
return str.includes('Windows')
|
||||||
},
|
},
|
||||||
renderChart() {
|
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);
|
this.myChart.setOption(this.option);
|
||||||
},
|
},
|
||||||
resizeHandle () {
|
resizeHandle () {
|
||||||
|
|||||||
+32
-32
@@ -1,36 +1,36 @@
|
|||||||
{{define "theme-server-status/footer"}}
|
{{define "theme-server-status/footer"}}
|
||||||
</div>
|
</section>
|
||||||
<footer class="container-fluid">
|
<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>
|
<p>{{ .Conf.Site.Brand }} | Theme ServerStatus | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}</p>
|
||||||
</footer>
|
</footer>
|
||||||
<aside class="toolbox">
|
<aside class="toolbox">
|
||||||
<template v-if="showTools">
|
<template v-if="showTools">
|
||||||
<span v-if="page=='index' && countryMapChartData.length!=0" class="showMapChart">
|
<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>
|
<i @click="showMapChart" data-toggle="modal" data-target="#mapChartBox" class="bi bi-geo-alt"></i>
|
||||||
</span>
|
</span>
|
||||||
<span class="toggleView">
|
<span class="toggleView">
|
||||||
<i v-if="showGroup" @click="toggleShowGroup" class="show-nogroup bi bi-justify"></i>
|
<i v-if="showGroup" @click="toggleShowGroup" class="show-nogroup bi bi-justify"></i>
|
||||||
<i v-else @click="toggleShowGroup" class="show-group bi bi-view-stacked"></i>
|
<i v-else @click="toggleShowGroup" class="show-group bi bi-view-stacked"></i>
|
||||||
</span>
|
</span>
|
||||||
<span class="toggleSemiTransparent" @click="toggleSemiTransparent">
|
<span class="toggleSemiTransparent" @click="toggleSemiTransparent">
|
||||||
<i class="bi" :class="semiTransparent ? 'bi-droplet' : 'bi-droplet-half'"></i>
|
<i class="bi" :class="semiTransparent ? 'bi-droplet' : 'bi-droplet-half'"></i>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<span v-if="!showTools">
|
<span v-if="!showTools">
|
||||||
<i @click="toggleShowTools" class="bi bi-three-dots"></i>
|
<i @click="toggleShowTools" class="bi bi-three-dots"></i>
|
||||||
</span>
|
</span>
|
||||||
<span class="setTheme">
|
<span class="setTheme">
|
||||||
<i v-if="theme === 'light'" @click="setTheme('dark')" class="setTheme-dark bi bi-moon-fill"></i>
|
<i v-if="theme === 'light'" @click="setTheme('dark')" class="setTheme-dark bi bi-moon-fill"></i>
|
||||||
<i v-else @click="setTheme('light')" class="setTheme-light bi bi-brightness-high-fill"></i>
|
<i v-else @click="setTheme('light')" class="setTheme-light bi bi-brightness-high-fill"></i>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="showGoTop" class="showGoTop">
|
<span v-if="showGoTop" class="showGoTop">
|
||||||
<i @click="goTop" class="goTop bi bi-arrow-up"></i>
|
<i @click="goTop" class="goTop bi bi-arrow-up"></i>
|
||||||
</span>
|
</span>
|
||||||
</aside>
|
</aside>
|
||||||
<template v-if="semiTransparent">
|
<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/light.plus.css?v20241008">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/dark.plus.css?v20240807">
|
<link rel="stylesheet" href="/static/theme-server-status/css/dark.plus.css?v20241008">
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
{{if ts .CustomCode}}{{.CustomCode|safe}}{{end}}
|
{{if ts .CustomCode}}{{.CustomCode|safe}}{{end}}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+17
-7
@@ -2,11 +2,21 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{.Conf.Language}}">
|
<html lang="{{.Conf.Language}}">
|
||||||
<head>
|
<head>
|
||||||
<title>{{ .Title }}</title>
|
<title>{{.Title}}</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<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">
|
<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.min.css">
|
||||||
<link rel="stylesheet" href="https://unpkg.com/[email protected]/font/bootstrap-icons.min.css">
|
<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]/assets/font-logos.css">
|
||||||
@@ -16,10 +26,10 @@
|
|||||||
<script src="https://unpkg.com/[email protected]/dist/js/bootstrap.min.js"></script>
|
<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/vue.min.js"></script>
|
||||||
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
|
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v202408011">
|
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20241029">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v202408011">
|
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v20241029">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240811">
|
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20241029">
|
||||||
<script src="/static/theme-server-status/js/mixin.js?v20240811"></script>
|
<script src="/static/theme-server-status/js/mixin.js?v20240915"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
|||||||
+71
-8
@@ -6,6 +6,7 @@
|
|||||||
<th class="node-cell name center">{{tr "Name"}}</th>
|
<th class="node-cell name center">{{tr "Name"}}</th>
|
||||||
<th class="node-cell os center">{{tr "Platform"}}</th>
|
<th class="node-cell os center">{{tr "Platform"}}</th>
|
||||||
<th class="node-cell location center">{{tr "Location"}}</th>
|
<th class="node-cell location center">{{tr "Location"}}</th>
|
||||||
|
<th v-if="nodesNoTag.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">{{tr "Price"}}</th>
|
||||||
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
|
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
|
||||||
<th class="node-cell load center">{{tr "Load"}}</th>
|
<th class="node-cell load center">{{tr "Load"}}</th>
|
||||||
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
|
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
|
||||||
@@ -13,12 +14,13 @@
|
|||||||
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
|
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
|
||||||
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
|
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
|
||||||
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
|
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
|
||||||
|
<th v-if="nodesNoTag.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining center">{{tr "Remaining"}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="servers">
|
<tbody id="servers">
|
||||||
<template v-for="(node,index) in nodesNoTag">
|
<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'"
|
<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">
|
<td class="node-cell status center">
|
||||||
<div class="status-container">
|
<div class="status-container">
|
||||||
<div v-if="node.online" class="status-icon online"></div>
|
<div v-if="node.online" class="status-icon online"></div>
|
||||||
@@ -30,11 +32,18 @@
|
|||||||
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
|
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
|
||||||
<i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
|
<i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
|
||||||
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
|
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
|
||||||
<span class="node-cell-os-text">@#getPlatformName(node.os)#@</span>
|
<span class="node-cell-os-text">@#getPlatformName(node.os) === '' && node.stateuptime > 0 ? 'linux' : getPlatformName(node.os)#@</span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;" class="node-cell location">
|
<td class="node-cell location center">
|
||||||
<i :class="'fi fi-' + node.location"></i>
|
<i :class="'fi fi-' + (node.stateuptime > 0 ? (node.location || 'un') : '')"></i>
|
||||||
<span class="node-cell-location-text text-uppercase">@#node.location#@</span>
|
<span class="node-cell-location-text text-uppercase">@#node.stateuptime > 0 ? (node.location || 'UN') : ''#@</span>
|
||||||
|
</td>
|
||||||
|
<td v-if="nodesNoTag.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">
|
||||||
|
<template v-if="node.additional && node.additional.price">
|
||||||
|
<span v-if="node.additional.price.amount == 0" class="node-cell-price-text">FREE</span>
|
||||||
|
<span v-else-if="node.additional.price.amount == -1" class="node-cell-price-text">PAYG</span>
|
||||||
|
<span v-else class="node-cell-price-text">@#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
|
||||||
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
|
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
|
||||||
<td style="text-align: center;" class="node-cell load">@#node.load#@</td>
|
<td style="text-align: center;" class="node-cell load">@#node.load#@</td>
|
||||||
@@ -59,15 +68,69 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td v-if="nodesNoTag.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining">
|
||||||
|
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
|
||||||
|
<template v-if="node.additional && node.additional.remaining.format">
|
||||||
|
<div :style="node.additional.remaining.format.style" :class="node.additional.remaining.days == 'lifetime' ? '' : node.additional.remaining.format.class"></div>
|
||||||
|
<div class="additional">
|
||||||
|
<small v-if="node.additional.remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
|
||||||
|
<small v-else-if="node.additional.remaining.days < 0">{{tr "Expired"}}</small>
|
||||||
|
<small v-else>
|
||||||
|
<span class="node-cell-remaining-days">@#node.additional.remaining.days#@{{tr "Days"}}</span>
|
||||||
|
<span class="node-cell-remaining-percent">@#node.additional.remaining.percent#@%</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
|
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
|
||||||
<td colspan="16">
|
<td colspan="16">
|
||||||
<div class="accordian-body collapse" :id="'rt'+node.ID">
|
<div class="accordian-body collapse" :id="'rt'+node.ID">
|
||||||
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
|
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
|
||||||
|
<span v-if="node.additional && Object.keys(node.additional.plan).length > 0" class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "Plan"}}:</span>
|
||||||
|
<span v-if="node.additional && Object.keys(node.additional.price).length > 0" class="plan price">
|
||||||
|
<span v-if="node.additional.price.amount == 0">FREE</span>
|
||||||
|
<span v-else-if="node.additional.price.amount == -1">PAYG</span>
|
||||||
|
<span v-else><i class="bi bi-cash-stack"></i> @#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="node.additional && node.additional.remaining.endDate" class="plan enddate">
|
||||||
|
<i class="bi bi-clock-history"></i>
|
||||||
|
<span v-if="node.additional.remaining.days == 'lifetime'">{{tr "Lifetime"}}</span>
|
||||||
|
<span v-else-if="node.additional.remaining.days < 0">{{tr "Expired"}}</span>
|
||||||
|
<span v-else>@#node.additional.remaining.endDate.toISOString().split('T')[0]#@</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="node.additional && node.additional.plan.bandwidth" class="plan bandwidth">
|
||||||
|
<i class="bi bi-speedometer2"></i>
|
||||||
|
<span>@#node.additional.plan.bandwidth#@</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="node.additional && node.additional.plan.trafficVol" class="plan traffic">
|
||||||
|
<i v-if="node.additional && node.additional.plan.trafficType == 1" class="bi bi-arrow-up"></i>
|
||||||
|
<i v-else class="bi bi-arrow-down-up"></i>
|
||||||
|
<span>@#node.additional.plan.trafficVol#@</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="node.additional && node.additional.plan.ipv4" class="plan ipv4">
|
||||||
|
<span>IPv4</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="node.additional && node.additional.plan.ipv6" class="plan ipv6">
|
||||||
|
<span>IPv6</span>
|
||||||
|
</span>
|
||||||
|
<template v-if="node.additional && node.additional.plan.networkRoute.length>0" v-for="(item, index) in node.additional.plan.networkRoute" :key="index">
|
||||||
|
<span class="plan network-route" :class="{ last: index === node.additional.plan.networkRoute.length - 1 }">
|
||||||
|
<span>@#item#@</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-if="node.additional && node.additional.plan.extra.length>0" v-for="(item, index) in node.additional.plan.extra" :key="index">
|
||||||
|
<span class="plan extra" :class="{ last: index === node.additional.plan.extra.length - 1 }">
|
||||||
|
<span>@#item#@</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
<span class="node-cell-expand">
|
<span class="node-cell-expand">
|
||||||
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
|
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
|
||||||
@#node.host.Platform#@-@#node.host.PlatformVersion#@
|
<span v-if="node.host.Platform">@#node.host.Platform#@@#node.host.PlatformVersion ? '-' + node.host.PlatformVersion : ''#@</span>
|
||||||
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@]
|
<span v-if="node.host.Arch">[@#node.host.Virtualization ? node.host.Virtualization + ':' : ''#@@#node.host.Arch#@]</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="node-cell-expand" v-if="node.host.CPU">
|
<span class="node-cell-expand" v-if="node.host.CPU">
|
||||||
<span class="node-cell-expand-label">CPU:</span>
|
<span class="node-cell-expand-label">CPU:</span>
|
||||||
@@ -143,7 +206,7 @@
|
|||||||
@#node.host.Version#@
|
@#node.host.Version#@
|
||||||
</span>
|
</span>
|
||||||
<span class="node-echarts-expand">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+71
-8
@@ -9,6 +9,7 @@
|
|||||||
<th class="node-cell name center">{{tr "Name"}}</th>
|
<th class="node-cell name center">{{tr "Name"}}</th>
|
||||||
<th class="node-cell os center">{{tr "Platform"}}</th>
|
<th class="node-cell os center">{{tr "Platform"}}</th>
|
||||||
<th class="node-cell location center">{{tr "Location"}}</th>
|
<th class="node-cell location center">{{tr "Location"}}</th>
|
||||||
|
<th v-if="group.data.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">{{tr "Price"}}</th>
|
||||||
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
|
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
|
||||||
<th class="node-cell load center">{{tr "Load"}}</th>
|
<th class="node-cell load center">{{tr "Load"}}</th>
|
||||||
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
|
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
|
||||||
@@ -16,12 +17,13 @@
|
|||||||
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
|
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
|
||||||
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
|
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
|
||||||
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
|
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
|
||||||
|
<th v-if="group.data.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining center">{{tr "Remaining"}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="servers">
|
<tbody id="servers">
|
||||||
<template v-for="(node,index) in group.data">
|
<template v-for="(node,index) in group.data">
|
||||||
<tr :id="'r'+node.ID" data-toggle="collapse" :data-target="'#rt'+node.ID" class="accordion-toggle"
|
<tr :id="'r'+node.ID" data-toggle="collapse" :data-target="'#rt'+node.ID" class="accordion-toggle"
|
||||||
:class="index % 2 === 0 ? 'odd': 'even'" 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">
|
<td class="node-cell status center">
|
||||||
<div class="status-container">
|
<div class="status-container">
|
||||||
<div v-if="node.online" class="status-icon online"></div>
|
<div v-if="node.online" class="status-icon online"></div>
|
||||||
@@ -33,11 +35,18 @@
|
|||||||
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
|
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
|
||||||
<i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
|
<i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
|
||||||
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
|
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
|
||||||
<span class="node-cell-os-text">@#getPlatformName(node.os)#@</span>
|
<span class="node-cell-os-text">@#getPlatformName(node.os) === '' && node.stateuptime > 0 ? 'linux' : getPlatformName(node.os)#@</span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;" class="node-cell location">
|
<td class="node-cell location center">
|
||||||
<i :class="'fi fi-' + node.location"></i>
|
<i :class="'fi fi-' + (node.stateuptime > 0 ? (node.location || 'un') : '')"></i>
|
||||||
<span class="node-cell-location-text text-uppercase"> @#node.location#@</span>
|
<span class="node-cell-location-text text-uppercase">@#node.stateuptime > 0 ? (node.location || 'UN') : ''#@</span>
|
||||||
|
</td>
|
||||||
|
<td v-if="group.data.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">
|
||||||
|
<template v-if="node.additional && node.additional.price">
|
||||||
|
<span v-if="node.additional.price.amount == 0" class="node-cell-price-text">FREE</span>
|
||||||
|
<span v-else-if="node.additional.price.amount == -1" class="node-cell-price-text">PAYG</span>
|
||||||
|
<span v-else class="node-cell-price-text">@#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
|
||||||
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
|
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
|
||||||
<td style="text-align: center;" class="node-cell load">@#node.load#@</td>
|
<td style="text-align: center;" class="node-cell load">@#node.load#@</td>
|
||||||
@@ -62,15 +71,69 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td v-if="group.data.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining">
|
||||||
|
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
|
||||||
|
<template v-if="node.additional && node.additional.remaining.format">
|
||||||
|
<div :style="node.additional.remaining.format.style" :class="node.additional.remaining.days == 'lifetime' ? '' : node.additional.remaining.format.class"></div>
|
||||||
|
<div class="additional">
|
||||||
|
<small v-if="node.additional.remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
|
||||||
|
<small v-else-if="node.additional.remaining.days < 0">{{tr "Expired"}}</small>
|
||||||
|
<small v-else>
|
||||||
|
<span class="node-cell-remaining-days">@#node.additional.remaining.days#@{{tr "Days"}}</span>
|
||||||
|
<span class="node-cell-remaining-percent">@#node.additional.remaining.percent#@%</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
|
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
|
||||||
<td colspan="16">
|
<td colspan="16">
|
||||||
<div class="accordian-body collapse" :id="'rt'+node.ID">
|
<div class="accordian-body collapse" :id="'rt'+node.ID">
|
||||||
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
|
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
|
||||||
|
<span v-if="node.additional && Object.keys(node.additional.plan).length > 0" class="node-cell-expand">
|
||||||
|
<span class="node-cell-expand-label">{{tr "Plan"}}:</span>
|
||||||
|
<span v-if="node.additional && Object.keys(node.additional.price).length > 0" class="plan price">
|
||||||
|
<span v-if="node.additional.price.amount == 0">FREE</span>
|
||||||
|
<span v-else-if="node.additional.price.amount == -1">PAYG</span>
|
||||||
|
<span v-else><i class="bi bi-cash-stack"></i> @#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="node.additional && node.additional.remaining.endDate" class="plan enddate">
|
||||||
|
<i class="bi bi-clock-history"></i>
|
||||||
|
<span v-if="node.additional.remaining.days == 'lifetime'">{{tr "Lifetime"}}</span>
|
||||||
|
<span v-else-if="node.additional.remaining.days < 0">{{tr "Expired"}}</span>
|
||||||
|
<span v-else>@#node.additional.remaining.endDate.toISOString().split('T')[0]#@</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="node.additional && node.additional.plan.bandwidth" class="plan bandwidth">
|
||||||
|
<i class="bi bi-speedometer2"></i>
|
||||||
|
<span>@#node.additional.plan.bandwidth#@</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="node.additional && node.additional.plan.trafficVol" class="plan traffic">
|
||||||
|
<i v-if="node.additional && node.additional.plan.trafficType == 1" class="bi bi-arrow-up"></i>
|
||||||
|
<i v-else class="bi bi-arrow-down-up"></i>
|
||||||
|
<span>@#node.additional.plan.trafficVol#@</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="node.additional && node.additional.plan.ipv4" class="plan ipv4">
|
||||||
|
<span>IPv4</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="node.additional && node.additional.plan.ipv6" class="plan ipv6">
|
||||||
|
<span>IPv6</span>
|
||||||
|
</span>
|
||||||
|
<template v-if="node.additional && node.additional.plan.networkRoute.length>0" v-for="(item, index) in node.additional.plan.networkRoute" :key="index">
|
||||||
|
<span class="plan network-route" :class="{ last: index === node.additional.plan.networkRoute.length - 1 }">
|
||||||
|
<span>@#item#@</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-if="node.additional && node.additional.plan.extra.length>0" v-for="(item, index) in node.additional.plan.extra" :key="index">
|
||||||
|
<span class="plan extra" :class="{ last: index === node.additional.plan.extra.length - 1 }">
|
||||||
|
<span>@#item#@</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
<span class="node-cell-expand">
|
<span class="node-cell-expand">
|
||||||
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
|
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
|
||||||
@#node.host.Platform#@-@#node.host.PlatformVersion#@
|
<span v-if="node.host.Platform">@#node.host.Platform#@@#node.host.PlatformVersion ? '-' + node.host.PlatformVersion : ''#@</span>
|
||||||
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@]
|
<span v-if="node.host.Arch">[@#node.host.Virtualization ? node.host.Virtualization + ':' : ''#@@#node.host.Arch#@]</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="node-cell-expand" v-if="node.host.CPU">
|
<span class="node-cell-expand" v-if="node.host.CPU">
|
||||||
<span class="node-cell-expand-label">CPU:</span>
|
<span class="node-cell-expand-label">CPU:</span>
|
||||||
@@ -146,7 +209,7 @@
|
|||||||
@#node.host.Version#@
|
@#node.host.Version#@
|
||||||
</span>
|
</span>
|
||||||
<span class="node-echarts-expand">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+602
-93
@@ -3,15 +3,15 @@
|
|||||||
{{template "theme-server-status/menu" .}}
|
{{template "theme-server-status/menu" .}}
|
||||||
<!-- showGroup true -->
|
<!-- showGroup true -->
|
||||||
<template v-if="showGroup">
|
<template v-if="showGroup">
|
||||||
<section class="container-fluid table-responsive content" v-for="group in nodesTag">
|
<div class="container-fluid table-responsive content" v-for="group in nodesTag">
|
||||||
{{template "theme-server-status/home-group-true" .}}
|
{{template "theme-server-status/home-group-true" .}}
|
||||||
</section>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- showGroup false -->
|
<!-- showGroup false -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<section class="container-fluid table-responsive content">
|
<div class="container-fluid table-responsive content">
|
||||||
{{template "theme-server-status/home-group-false" .}}
|
{{template "theme-server-status/home-group-false" .}}
|
||||||
</section>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="modal fade" id="mapChartBox" tabindex="-1" role="dialog" aria-labelledby="mapChartTitle" aria-hidden="true" style="padding-left:0">
|
<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-dialog modal-lg modal-dialog-centered">
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
servers: [],
|
servers: [],
|
||||||
nodesTag: [],
|
nodesTag: [],
|
||||||
nodesNoTag: [],
|
nodesNoTag: [],
|
||||||
|
additional: {},
|
||||||
chartDataList: [],
|
chartDataList: [],
|
||||||
ws: null,
|
ws: null,
|
||||||
language: {{.Conf.Language}},
|
language: {{.Conf.Language}},
|
||||||
@@ -71,17 +72,32 @@
|
|||||||
mixins: [mixinsVue],
|
mixins: [mixinsVue],
|
||||||
created() {
|
created() {
|
||||||
this.servers = JSON.parse('{{.Servers}}').servers;
|
this.servers = JSON.parse('{{.Servers}}').servers;
|
||||||
|
this.additional = this.initAdditional(this.servers);
|
||||||
if(this.showGroup) {
|
if(this.showGroup) {
|
||||||
this.nodesTag = this.groupingData(this.handleNodes(this.servers),"Tag");
|
this.nodesTag = this.groupingData(this.handleNodes(this.servers),"Tag");
|
||||||
|
this.nodesTag.forEach(group => {
|
||||||
|
group.data.forEach(item => {
|
||||||
|
const additionalData = this.additional[item.ID];
|
||||||
|
if (additionalData) {
|
||||||
|
item.additional = additionalData; // 添加 additional 属性
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.nodesNoTag = this.handleNodes(this.servers);
|
this.nodesNoTag = this.handleNodes(this.servers);
|
||||||
|
this.nodesNoTag.forEach(item => {
|
||||||
|
const additionalData = this.additional[item.ID];
|
||||||
|
if (additionalData) {
|
||||||
|
item.additional = additionalData; // 添加 additional 属性
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.initCountryMap().then(countryMap => {
|
this.initCountryMap().then(countryMap => {
|
||||||
this.countryMap = countryMap;
|
this.countryMap = countryMap;
|
||||||
this.countryNameMap = this.initCountryNameMap();
|
this.countryNameMap = this.initCountryNameMap();
|
||||||
this.countryServer = this.initCountryServer();
|
this.countryServer = this.initCountryServer();
|
||||||
this.countryMapChartData = this.initCountryMapChartData();
|
this.countryMapChartData = this.initCountryMapChartData();
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
// 初始化时建立WebSocket连接
|
// 初始化时建立WebSocket连接
|
||||||
@@ -98,6 +114,60 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
initAdditional(servers) {
|
||||||
|
let nodes = {};
|
||||||
|
servers?.forEach(server => {
|
||||||
|
//处理异常
|
||||||
|
if (!server.PublicNote) return;
|
||||||
|
|
||||||
|
// 初始化节点
|
||||||
|
nodes[server.ID] = {
|
||||||
|
"remaining": {},
|
||||||
|
"price": {},
|
||||||
|
"plan": {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 billingDataMod 的 remaining 配置
|
||||||
|
const remainingEndDate = this.getRemainingDays(this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"), server.PublicNote, 1);
|
||||||
|
const remainingFormat = this.getRemainingFormat(server.live, server.PublicNote);
|
||||||
|
const remainingDays = this.getRemainingDays(this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"), server.PublicNote);
|
||||||
|
const remainingPercent = this.getRemainingPercent(
|
||||||
|
this.getNoteElementValue(server.PublicNote, "billingDataMod", "startDate"),
|
||||||
|
this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"),
|
||||||
|
server.PublicNote
|
||||||
|
);
|
||||||
|
// 设置 remaining 属性
|
||||||
|
if (remainingEndDate) nodes[server.ID].remaining.endDate = remainingEndDate;
|
||||||
|
if (remainingFormat) nodes[server.ID].remaining.format = remainingFormat;
|
||||||
|
if (remainingDays) nodes[server.ID].remaining.days = remainingDays;
|
||||||
|
if (remainingPercent) nodes[server.ID].remaining.percent = this.toFixed2(100 - remainingPercent);
|
||||||
|
|
||||||
|
// 处理 billingDataMod 的 price 配置
|
||||||
|
const priceAmount = this.getNoteElementValue(server.PublicNote, "billingDataMod", "amount");
|
||||||
|
const priceCycle = this.getNoteElementValue(server.PublicNote, "billingDataMod", "cycle");
|
||||||
|
// 设置 price 属性
|
||||||
|
if (priceAmount) nodes[server.ID].price.amount = priceAmount;
|
||||||
|
if (priceCycle && priceAmount) nodes[server.ID].price.cycle = priceCycle;
|
||||||
|
|
||||||
|
// 处理 planDataMod 配置
|
||||||
|
const planBandwidth = this.getNoteElementValue(server.PublicNote, "planDataMod", "bandwidth");
|
||||||
|
const planTrafficVol = this.getNoteElementValue(server.PublicNote, "planDataMod", "trafficVol");
|
||||||
|
const planTrafficType = this.getNoteElementValue(server.PublicNote, "planDataMod", "trafficType");
|
||||||
|
const planIPv4 = this.getNoteElementValue(server.PublicNote, "planDataMod", "IPv4");
|
||||||
|
const planIPv6 = this.getNoteElementValue(server.PublicNote, "planDataMod", "IPv6");
|
||||||
|
const planNetworkRoute = this.getNoteElementValue(server.PublicNote, "planDataMod", "networkRoute");
|
||||||
|
const planExtra = this.getNoteElementValue(server.PublicNote, "planDataMod", "extra");
|
||||||
|
// 设置 plan 属性
|
||||||
|
if (planBandwidth) nodes[server.ID].plan.bandwidth = planBandwidth;
|
||||||
|
if (planTrafficVol) nodes[server.ID].plan.trafficVol = planTrafficVol;
|
||||||
|
if (planTrafficType) nodes[server.ID].plan.trafficType = planTrafficType;
|
||||||
|
if (planIPv4) nodes[server.ID].plan.ipv4 = planIPv4 >= 1;
|
||||||
|
if (planIPv6) nodes[server.ID].plan.ipv6 = planIPv6 >= 1;
|
||||||
|
if (planNetworkRoute) nodes[server.ID].plan.networkRoute = planNetworkRoute.split(',');
|
||||||
|
if (planExtra) nodes[server.ID].plan.extra = planExtra.split(',');
|
||||||
|
});
|
||||||
|
return nodes;
|
||||||
|
},
|
||||||
initCountryMap() {
|
initCountryMap() {
|
||||||
return fetch(this.staticUrl + '/maps/nezha.countrymap.json')
|
return fetch(this.staticUrl + '/maps/nezha.countrymap.json')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
@@ -162,7 +232,10 @@
|
|||||||
const tooltipBackgroundColor = this.theme == "dark" ? "#ffffff" : '#ffffff';
|
const tooltipBackgroundColor = this.theme == "dark" ? "#ffffff" : '#ffffff';
|
||||||
const tooltipBorderColor = this.theme == "dark" ? "#ffffff" : "#ffffff";
|
const tooltipBorderColor = this.theme == "dark" ? "#ffffff" : "#ffffff";
|
||||||
const fontSize = this.isMobile ? 10 : 12;
|
const fontSize = this.isMobile ? 10 : 12;
|
||||||
const fontColor = this.theme == "dark" ? "#000000" : "#000000";
|
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 chartContainer = document.getElementById('mapChart');
|
||||||
const mapChart = echarts.init(chartContainer, '', { // init图表
|
const mapChart = echarts.init(chartContainer, '', { // init图表
|
||||||
renderer: 'canvas',
|
renderer: 'canvas',
|
||||||
@@ -170,6 +243,13 @@
|
|||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
});
|
});
|
||||||
|
mapChart.showLoading({
|
||||||
|
text: 'loading',
|
||||||
|
textColor: showLoadingTextColor,
|
||||||
|
color: showLoadingColor,
|
||||||
|
maskColor: showLoadingMaskColor,
|
||||||
|
zlevel: 2
|
||||||
|
});
|
||||||
fetch(this.staticUrl + '/maps/' + this.countryMapGeoFile)
|
fetch(this.staticUrl + '/maps/' + this.countryMapGeoFile)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(worldMap => {
|
.then(worldMap => {
|
||||||
@@ -224,7 +304,10 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
mapChart.setOption(option);
|
setTimeout(() => {
|
||||||
|
mapChart.hideLoading();
|
||||||
|
mapChart.setOption(option);
|
||||||
|
}, 1000);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
isWindowsPlatform(str) {
|
isWindowsPlatform(str) {
|
||||||
@@ -382,8 +465,22 @@
|
|||||||
}
|
}
|
||||||
if(this.showGroup) {
|
if(this.showGroup) {
|
||||||
this.nodesTag = this.groupingData(this.handleNodes(data.servers),"Tag");
|
this.nodesTag = this.groupingData(this.handleNodes(data.servers),"Tag");
|
||||||
|
this.nodesTag.forEach(group => {
|
||||||
|
group.data.forEach(item => {
|
||||||
|
const additionalData = this.additional[item.ID];
|
||||||
|
if (additionalData) {
|
||||||
|
item.additional = additionalData; // 添加 additional 属性
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.nodesNoTag = this.handleNodes(data.servers);
|
this.nodesNoTag = this.handleNodes(data.servers);
|
||||||
|
this.nodesNoTag.forEach(item => {
|
||||||
|
const additionalData = this.additional[item.ID];
|
||||||
|
if (additionalData) {
|
||||||
|
item.additional = additionalData; // 添加 additional 属性
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.ws.onclose = () => {
|
this.ws.onclose = () => {
|
||||||
@@ -470,97 +567,144 @@
|
|||||||
// 如果所有元素的 Temperature 都为 0,则返回一个默认值 0
|
// 如果所有元素的 Temperature 都为 0,则返回一个默认值 0
|
||||||
return 0;
|
return 0;
|
||||||
},
|
},
|
||||||
showCharts(event, id) {
|
showCharts(id,changeChartboxShow=true) {
|
||||||
const chartContainer = this.$refs[`chart${id}`][0];
|
const chartContainer = document.getElementById(`chart-${id}`);
|
||||||
const chartboxShow = chartContainer.getAttribute('chartbox-show');
|
if(changeChartboxShow){
|
||||||
chartContainer.setAttribute('chartbox-show', chartboxShow === '0' ? '1' : '0');
|
const chartboxShow = chartContainer.getAttribute('chartbox-show');
|
||||||
const isAriaExpandedFalse = event.currentTarget.getAttribute('aria-expanded') === 'false';
|
chartContainer.setAttribute('chartbox-show', chartboxShow === '0' ? '1' : '0');
|
||||||
if (!isAriaExpandedFalse) return;
|
const collapseContainer = document.getElementById(`r${id}`);
|
||||||
|
const isAriaExpandedFalse = collapseContainer.getAttribute('aria-expanded') === 'false';
|
||||||
|
if (!isAriaExpandedFalse) return;
|
||||||
|
}
|
||||||
// 发起数据请求
|
// 发起数据请求
|
||||||
const url = `/api/v1/monitor/${id}`;
|
const url = `/api/v1/monitor/${id}`;
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.result) { // 数据请求成功,更新数据并渲染图表
|
if (data.result) { // 数据请求成功,更新数据并渲染图表
|
||||||
this.chartDataList[id - 1] = data.result;
|
this.chartDataList[id] = data.result;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.renderCharts(id);
|
this.renderCharts(id);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('this agent (id:'+ id + ') has no monitor.');
|
console.log('this server (id:'+ id + ') has no monitor.');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error fetching data:', error);
|
console.log('Error fetching data:', error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
renderCharts(id, reload = false) {
|
renderCharts(id, reload = false) {
|
||||||
if (!this.chartDataList[id - 1]) return;
|
if (!this.chartDataList[id]) return;
|
||||||
const chartData = this.chartDataList[id - 1];
|
const chartData = this.chartDataList[id];
|
||||||
const chartContainer = this.$refs[`chart${id}`][0];
|
const chartContainer = document.getElementById(`chart-${id}`);
|
||||||
if (reload) { //点击切换亮色/暗色风格模式时,重新载入echarts图表的逻辑,
|
if (reload) {
|
||||||
// 第一步,查找已经渲染出的图表容器,并销毁它
|
|
||||||
const existingChart = echarts.getInstanceByDom(chartContainer);
|
const existingChart = echarts.getInstanceByDom(chartContainer);
|
||||||
if (existingChart) existingChart.dispose();
|
if (existingChart) existingChart.dispose();
|
||||||
// 第二步,如果图表容器处于不可见状态chartboxShow=0,不重新渲染出新的图表,
|
|
||||||
// 如果图表容器处于可见状态chartboxShow=1,重新渲染出新的图表
|
|
||||||
const chartboxShow = chartContainer.getAttribute('chartbox-show');
|
const chartboxShow = chartContainer.getAttribute('chartbox-show');
|
||||||
if ( chartboxShow === '0' ) return;
|
if ( chartboxShow === '0' ) return;
|
||||||
}
|
}
|
||||||
// 定义图表参数值
|
|
||||||
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
|
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
|
||||||
const fontSize = this.isMobile ? 10 : 14;
|
const fontSize = this.isMobile ? 10 : 14;
|
||||||
const gridLeft = this.isMobile ? 25 : 36;
|
const gridLeft = (MaxTCPPingValue > 500) ? (this.isMobile ? 36 : 42) : (this.isMobile ? 25 : 36);
|
||||||
const gridRight = this.isMobile ? 5 : 20;
|
const gridRight = this.isMobile ? 5 : 20;
|
||||||
const legendLeft = this.isMobile ? 'center' : 'center';
|
const legendLeft = this.isMobile ? 'center' : 'center';
|
||||||
const legendTop = this.isMobile ? 5 : 5;
|
const legendTop = this.isMobile ? 5 : 5;
|
||||||
const legendPadding= this.isMobile ? [5,0,5,0] : [5,0,5,0];
|
const legendPadding= this.isMobile ? [5,0,5,0] : [5,0,5,0];
|
||||||
const systemDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
const systemDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
const theme = localStorage.getItem("theme") ? localStorage.getItem("theme") : systemDarkMode;
|
const theme = localStorage.getItem("theme") ? localStorage.getItem("theme") : systemDarkMode;
|
||||||
const chartTheme = theme == "dark" ? "dark" : "default";
|
const chartTheme = this.theme == "dark" ? "dark" : "default";
|
||||||
const fontColor = theme == "dark" ? "#f1f1f1" : "#000000";
|
const fontColor = this.theme == "dark" ? "#f1f1f1" : "#000000";
|
||||||
const backgroundColor = theme == "dark" ? '' : '';
|
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 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 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 lineStyleWidth = this.isMobile ? 1 : 2;
|
||||||
const splitLineWidth = this.isMobile ? 0.5 : 1;
|
const splitLineWidth = this.isMobile ? 0.5 : 1;
|
||||||
// 渲染图表
|
const markLineItemStyleOpacity = this.semiTransparent ? 1 : 0.75;
|
||||||
const chart = echarts.init(chartContainer, chartTheme, {
|
const markLineLineStyleWidth = this.isMobile ? 0.15 : 0.3;
|
||||||
|
const chart = echarts.init(chartContainer, chartTheme, { // init图表
|
||||||
renderer: 'canvas',
|
renderer: 'canvas',
|
||||||
useDirtyRect: false,
|
useDirtyRect: false,
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
height: 300,
|
height: 300
|
||||||
});
|
});
|
||||||
const xAxisData = chartData[0].created_at.map(time => new Date(time).toLocaleString());
|
let legendData = [];
|
||||||
const seriesData = chartData.map(item => {
|
let seriesData = [];
|
||||||
|
chartData.forEach((item,key)=> {
|
||||||
let loss = 0;
|
let loss = 0;
|
||||||
const data = item.avg_delay.map((avgDelay, index) => {
|
let totalLossRate = 0;
|
||||||
if(avgDelay > 0 && avgDelay < MaxTCPPingValue){
|
let legendName = '';
|
||||||
loss += avgDelay > 0.9 * MaxTCPPingValue ? 1 : 0;
|
let data = { main: [], markLine: []};
|
||||||
return [item.created_at[index], avgDelay.toFixed(2)];
|
item.avg_delay.forEach((avgDelay, index) => {
|
||||||
}else{
|
const threshold = 0.9 * MaxTCPPingValue; // 定义阀值,用于判断是否丢包
|
||||||
|
// 定义丢包 1. avgDelay==0 2. avgDelay>=MaxTCPPingValue 3. avgDelay>=threshold
|
||||||
|
if(avgDelay == 0 || avgDelay >= MaxTCPPingValue){ //绝对丢包
|
||||||
loss += 1;
|
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);
|
totalLossRate = ((loss / item.created_at.length) * 100).toFixed(1);
|
||||||
item.monitor_name = item.monitor_name.includes("%") ? item.monitor_name : `${item.monitor_name} ${lossRate}%`;
|
legendName = `${item.monitor_name} ${totalLossRate}%`;
|
||||||
return {
|
legendData.push(legendName);
|
||||||
name: item.monitor_name,
|
seriesData.push(
|
||||||
type: 'line',
|
{
|
||||||
smooth: true,
|
name: legendName,
|
||||||
symbol: 'none',
|
type: 'line',
|
||||||
data: data,
|
smooth: true,
|
||||||
connectNulls: true,
|
symbol: 'none',
|
||||||
lineStyle: {
|
connectNulls: true,
|
||||||
width: lineStyleWidth
|
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 maxLegendsPerRowMobile = localStorage.getItem("maxLegendsPerRowMobile") ? localStorage.getItem("maxLegendsPerRowMobile") : 3;
|
||||||
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6;
|
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6;
|
||||||
const autoIncrement = Math.floor((legendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 20 : 28)
|
const autoIncrement = Math.floor((legendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 20 : 28);
|
||||||
const height = 300 + autoIncrement;
|
const height = 300 + autoIncrement;
|
||||||
const gridTop = 40 + autoIncrement;
|
const gridTop = 40 + autoIncrement;
|
||||||
const legendIcon = this.isMobile ? 'rect' : "";
|
const legendIcon = this.isMobile ? 'rect' : "";
|
||||||
@@ -570,39 +714,24 @@
|
|||||||
width: 'auto',
|
width: 'auto',
|
||||||
height: height
|
height: height
|
||||||
});
|
});
|
||||||
|
|
||||||
const option = {
|
const option = {
|
||||||
|
color: this.colors,
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: fontColor
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: gridTop,
|
||||||
|
left: gridLeft,
|
||||||
|
right: gridRight,
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
show: false
|
show: false,
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
backgroundColor: tooltipBackgroundColor,
|
|
||||||
borderColor: tooltipBorderColor,
|
|
||||||
textStyle: {
|
|
||||||
fontSize: fontSize,
|
|
||||||
color: fontColor
|
|
||||||
}
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
data: legendData,
|
|
||||||
show: true,
|
|
||||||
icon: legendIcon,
|
|
||||||
textStyle: {
|
|
||||||
fontSize: fontSize,
|
|
||||||
color: fontColor
|
|
||||||
},
|
|
||||||
top: legendTop,
|
|
||||||
bottom: 0,
|
|
||||||
left: legendLeft,
|
|
||||||
padding: legendPadding,
|
|
||||||
itemWidth: itemWidth,
|
|
||||||
itemHeight: itemHeight,
|
|
||||||
},
|
},
|
||||||
|
series: seriesData.flat(),
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'time',
|
type: 'time',
|
||||||
data: xAxisData,
|
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
textStyle: {
|
textStyle: {
|
||||||
fontSize: fontSize
|
fontSize: fontSize
|
||||||
@@ -622,34 +751,414 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
legend: {
|
||||||
|
data: legendData,
|
||||||
|
show: true,
|
||||||
|
icon: legendIcon,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: fontColor
|
||||||
|
},
|
||||||
|
top: legendTop,
|
||||||
|
bottom: 0,
|
||||||
|
left: legendLeft,
|
||||||
|
padding: legendPadding,
|
||||||
|
itemWidth: itemWidth,
|
||||||
|
itemHeight: itemHeight,
|
||||||
|
},
|
||||||
|
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: [
|
dataZoom: [
|
||||||
{
|
{
|
||||||
type: 'slider',
|
type: 'slider',
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 100
|
end: 100
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
series: seriesData,
|
|
||||||
textStyle: {
|
|
||||||
fontSize: fontSize,
|
|
||||||
color: fontColor
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
top: gridTop,
|
|
||||||
left: gridLeft,
|
|
||||||
right: gridRight
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
chart.setOption(option);
|
chart.setOption(option);
|
||||||
},
|
},
|
||||||
reloadCharts() { // 重新加载所有图表
|
reloadCharts() {
|
||||||
this.servers.forEach(node => {
|
this.servers.forEach(node => {
|
||||||
const id = node.ID;
|
const id = node.ID;
|
||||||
const chartData = this.chartDataList[id - 1];
|
const chartData = this.chartDataList[id];
|
||||||
if (chartData) {
|
if (chartData) {
|
||||||
this.renderCharts(id,true);
|
this.renderCharts(id,true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
getNoteElementValue(string, elementName, childElementName) {
|
||||||
|
let obj;
|
||||||
|
|
||||||
|
try {
|
||||||
|
obj = JSON.parse(string);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!elementName) return null;
|
||||||
|
|
||||||
|
if (childElementName) {
|
||||||
|
return obj[elementName] && childElementName in obj[elementName] ? obj[elementName][childElementName] : null;
|
||||||
|
} else {
|
||||||
|
return elementName in obj ? obj[elementName] : null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getBillingCycle(billingCycle) {
|
||||||
|
// 统一转换为小写进行比较
|
||||||
|
const cycle = billingCycle.toLowerCase();
|
||||||
|
|
||||||
|
switch (cycle) {
|
||||||
|
case '月':
|
||||||
|
case 'mo':
|
||||||
|
case 'month':
|
||||||
|
case 'monthly':
|
||||||
|
case 'm':
|
||||||
|
return "M";
|
||||||
|
case '季':
|
||||||
|
case 'quarterly':
|
||||||
|
case 'q':
|
||||||
|
return "Q";
|
||||||
|
case '半':
|
||||||
|
case '半年':
|
||||||
|
case 'half':
|
||||||
|
case 'semi-annually':
|
||||||
|
case 'h':
|
||||||
|
return "H";
|
||||||
|
case '年':
|
||||||
|
case 'yr':
|
||||||
|
case 'year':
|
||||||
|
case 'annually':
|
||||||
|
case 'y':
|
||||||
|
return "Y";
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getAdjustTimezone(reference, target) {
|
||||||
|
// 获取时区
|
||||||
|
const referenceTimezoneOffset = reference.getTimezoneOffset();
|
||||||
|
const targetTimezoneOffset = target.getTimezoneOffset();
|
||||||
|
|
||||||
|
// 计算时区差异
|
||||||
|
const timezoneDifference = (referenceTimezoneOffset - targetTimezoneOffset) * 60 * 1000;
|
||||||
|
|
||||||
|
// 将 target 日期调整到 reference 时区
|
||||||
|
return new Date(target.getTime() + timezoneDifference);
|
||||||
|
},
|
||||||
|
getAutoRenewalEndDate(endDate, billingCycle) {
|
||||||
|
const expiration = new Date(endDate);
|
||||||
|
const current = this.getAdjustTimezone(new Date(endDate), new Date());
|
||||||
|
|
||||||
|
// 如果 expiration 无效,记录日志
|
||||||
|
if (isNaN(expiration.getTime())) {
|
||||||
|
console.log("getAutoRenewalEndDate: Invalid expiration format");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
flag: 1, // 1表示需要更新
|
||||||
|
check: 0, // 判断逻辑标记
|
||||||
|
count: 0 // 周期累计计数
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果当前时间还没到到期时间,直接返回到期时间
|
||||||
|
if (current < expiration) {
|
||||||
|
result.flag = 0;
|
||||||
|
result.date = expiration;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextExpiration = new Date(expiration); // 初始化为原到期时间
|
||||||
|
let newExpirationMonth = expiration.getMonth(); // 获取expiration初始月份
|
||||||
|
|
||||||
|
switch (billingCycle.toUpperCase()) {
|
||||||
|
case 'M': // 月度
|
||||||
|
nextExpiration.setFullYear(current.getFullYear());
|
||||||
|
|
||||||
|
const monthCheck = current < new Date(current.getFullYear(), current.getMonth(), expiration.getDate());
|
||||||
|
|
||||||
|
// 检查当前月是否在有效期内
|
||||||
|
if (monthCheck) {
|
||||||
|
nextExpiration.setMonth(current.getMonth());
|
||||||
|
result.check = 1;
|
||||||
|
} else {
|
||||||
|
nextExpiration.setMonth(current.getMonth() + 1);
|
||||||
|
result.check = 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Q': // 季度
|
||||||
|
nextExpiration.setFullYear(current.getFullYear());
|
||||||
|
|
||||||
|
// 每次增加 3 个月,直到新的月份大于当前月份
|
||||||
|
while (newExpirationMonth < current.getMonth()) {
|
||||||
|
newExpirationMonth += 3;
|
||||||
|
result.count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextExpiration设置获取到的新月份
|
||||||
|
nextExpiration.setMonth(newExpirationMonth);
|
||||||
|
|
||||||
|
const quarterlyCheck = current < nextExpiration;
|
||||||
|
|
||||||
|
// 检查新月份是否在有效期内
|
||||||
|
if (quarterlyCheck) {
|
||||||
|
result.check = 3;
|
||||||
|
} else {
|
||||||
|
nextExpiration.setMonth(current.getMonth() + 3);
|
||||||
|
result.check = 4;
|
||||||
|
result.count += 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'H': // 半年
|
||||||
|
nextExpiration.setFullYear(current.getFullYear());
|
||||||
|
|
||||||
|
// 每次增加 6 个月,直到新的月份大于当前月份
|
||||||
|
while (newExpirationMonth < current.getMonth()) {
|
||||||
|
newExpirationMonth += 6;
|
||||||
|
result.count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextExpiration设置获取到的新月份
|
||||||
|
nextExpiration.setMonth(newExpirationMonth);
|
||||||
|
|
||||||
|
const halfCheck = current < nextExpiration;
|
||||||
|
|
||||||
|
// 检查新月份是否在有效期内
|
||||||
|
if (halfCheck) {
|
||||||
|
result.check = 5;
|
||||||
|
} else {
|
||||||
|
nextExpiration.setMonth(current.getMonth() + 6);
|
||||||
|
result.check = 6;
|
||||||
|
result.count += 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Y': // 年度
|
||||||
|
const yearCheck = current < new Date(current.getFullYear(), expiration.getMonth(), expiration.getDate());
|
||||||
|
|
||||||
|
// 如果当前时间比这一年有效期早,则到期为本年
|
||||||
|
if (yearCheck) {
|
||||||
|
nextExpiration.setFullYear(current.getFullYear());
|
||||||
|
result.check = 7;
|
||||||
|
} else {
|
||||||
|
// 否则推到下一年
|
||||||
|
nextExpiration.setFullYear(current.getFullYear() + 1);
|
||||||
|
result.check = 8;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid billing cycle");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保持原到期时间的时分秒
|
||||||
|
nextExpiration.setHours(expiration.getHours());
|
||||||
|
nextExpiration.setMinutes(expiration.getMinutes());
|
||||||
|
nextExpiration.setSeconds(expiration.getSeconds());
|
||||||
|
|
||||||
|
result.date = nextExpiration;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
getAutoRenewalStartDate(flag, startDate, check, count) {
|
||||||
|
//1.判断什么时候改变 2.如何改变
|
||||||
|
const start = new Date(startDate);
|
||||||
|
const current = this.getAdjustTimezone(start, new Date());
|
||||||
|
|
||||||
|
// 检查 startDate 格式是否有效,若无效返回 null
|
||||||
|
if (isNaN(start.getTime())) {
|
||||||
|
console.log("getAutoRenewalStartDate: Invalid startDate format");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 flag 为 0,直接返回开始日期
|
||||||
|
if (flag === 0) {
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化新的开始日期
|
||||||
|
const newStart = new Date(start);
|
||||||
|
|
||||||
|
switch (check) {
|
||||||
|
case 1: // 处理月份:设置为上个月
|
||||||
|
newStart.setFullYear(current.getFullYear());
|
||||||
|
newStart.setMonth(current.getMonth() - 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2: // 处理月份:设置为当前月
|
||||||
|
newStart.setFullYear(current.getFullYear());
|
||||||
|
newStart.setMonth(current.getMonth());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
case 4: // 处理季度
|
||||||
|
newStart.setFullYear(current.getFullYear());
|
||||||
|
newStart.setMonth(start.getMonth() + 3 * count);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
case 6: // 处理半年
|
||||||
|
newStart.setFullYear(current.getFullYear());
|
||||||
|
newStart.setMonth(start.getMonth() + 6 * count);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 7: // 处理年份:设置为上一年
|
||||||
|
newStart.setFullYear(current.getFullYear() - 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 8: // 处理年份:保持当前年份
|
||||||
|
newStart.setFullYear(current.getFullYear());
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // 默认处理:直接返回
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newStart;
|
||||||
|
},
|
||||||
|
getRemainingFormat(online, note) {
|
||||||
|
if (!note) return null;
|
||||||
|
const startDate = this.getNoteElementValue(note, "billingDataMod", "startDate");
|
||||||
|
const endDate = this.getNoteElementValue(note, "billingDataMod", "endDate");
|
||||||
|
|
||||||
|
// 检查 startDate 和 endDate 是否有效
|
||||||
|
if (!startDate || !endDate || typeof startDate !== 'string' || typeof endDate !== 'string') {
|
||||||
|
return null; // 如果无效,返回 null 或其他错误处理逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理特殊时间格式
|
||||||
|
if (startDate.includes('0000-00-00') || endDate.includes("0000-00-00")) {
|
||||||
|
return this.formatPercents(online, this.toFixed2(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 startDate 和 endDate 是否为合法的Date
|
||||||
|
if (isNaN(new Date(startDate).getTime()) || isNaN(new Date(endDate).getTime())) {
|
||||||
|
return {
|
||||||
|
"class": "",
|
||||||
|
"style": "width: 0%",
|
||||||
|
"percent": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//处理特殊时间格式
|
||||||
|
const percent = this.getRemainingPercent(startDate, endDate, note);
|
||||||
|
return this.formatPercents(online, this.toFixed2(percent));
|
||||||
|
|
||||||
|
},
|
||||||
|
getRemainingDays(endDate, note, type) {
|
||||||
|
// 检查 endDate 是否有效
|
||||||
|
if (!endDate || typeof endDate !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理特殊时间格式
|
||||||
|
if (endDate.includes("0000-00-00")) {
|
||||||
|
return "lifetime";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 endDate 是否为合法的Date
|
||||||
|
if (isNaN(new Date(endDate).getTime())) {
|
||||||
|
return type === 1 ? null : "NaN";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前时间,并调整时区
|
||||||
|
const currentTime = this.getAdjustTimezone(new Date(endDate), new Date());
|
||||||
|
|
||||||
|
// 获取计费周期和自动续订日期
|
||||||
|
const billingCycle = this.getNoteElementValue(note, "billingDataMod", "cycle") || "月";
|
||||||
|
const autoEndDate = this.getAutoRenewalEndDate(
|
||||||
|
this.getNoteElementValue(note, "billingDataMod", "endDate"),
|
||||||
|
this.getBillingCycle(billingCycle)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 检查 autoRenewal 状态
|
||||||
|
const autoRenewal = this.getNoteElementValue(note, "billingDataMod", "autoRenewal") == 1;
|
||||||
|
|
||||||
|
// 确定到期时间
|
||||||
|
const end = autoRenewal ? autoEndDate.date : new Date(endDate);
|
||||||
|
|
||||||
|
// 直接返回处理后的到期时间
|
||||||
|
if (type === 1) return end;
|
||||||
|
|
||||||
|
// 计算剩余天数
|
||||||
|
const timeDiff = end - currentTime;
|
||||||
|
const daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
return daysDiff;
|
||||||
|
},
|
||||||
|
getRemainingPercent(startDate, endDate, note) {
|
||||||
|
// 检查 startDate 和 endDate 是否为有效字符串并处理特殊格式
|
||||||
|
if (!startDate || !endDate || typeof startDate !== 'string' || typeof endDate !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理特殊时间格式
|
||||||
|
if (startDate.includes("0000-00-00") || endDate.includes("0000-00-00")) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 startDate 和 endDate 是否为合法的Date
|
||||||
|
if (isNaN(new Date(startDate).getTime()) || isNaN(new Date(endDate).getTime())) {
|
||||||
|
return "NaN";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前时间并调整时区
|
||||||
|
const now = this.getAdjustTimezone(new Date(endDate), new Date());
|
||||||
|
|
||||||
|
// 获取计费周期
|
||||||
|
const billingCycle = this.getNoteElementValue(note, "billingDataMod", "cycle") || "月";
|
||||||
|
|
||||||
|
// 自动获取结束日期
|
||||||
|
const autoEndDate = this.getAutoRenewalEndDate(
|
||||||
|
this.getNoteElementValue(note, "billingDataMod", "endDate"),
|
||||||
|
this.getBillingCycle(billingCycle)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 自动获取开始日期
|
||||||
|
const autoStartDate = autoEndDate.flag == 1
|
||||||
|
? this.getAutoRenewalStartDate(autoEndDate.flag, this.getNoteElementValue(note, "billingDataMod", "startDate"), autoEndDate.check, autoEndDate.count)
|
||||||
|
: new Date(startDate);
|
||||||
|
|
||||||
|
// 计算开始和结束时间
|
||||||
|
const autoRenewal = this.getNoteElementValue(note, "billingDataMod", "autoRenewal") == 1;
|
||||||
|
const start = autoRenewal ? autoStartDate : new Date(startDate);
|
||||||
|
const end = autoRenewal ? autoEndDate.date : new Date(endDate);
|
||||||
|
|
||||||
|
// 计算剩余百分比
|
||||||
|
if (now < start) {
|
||||||
|
return 0.01; //避免当条件判断!percent被过滤
|
||||||
|
}
|
||||||
|
if (now >= end) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalDuration = end - start;
|
||||||
|
const elapsedDuration = now - start;
|
||||||
|
const percent = (elapsedDuration / totalDuration) * 100;
|
||||||
|
|
||||||
|
// 确保百分比在 0-100 之间
|
||||||
|
return Math.min(Math.max(percent, 0), 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+4
-15
@@ -15,20 +15,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<nav id="navbar" class="navbar-collapse collapse">
|
<nav id="navbar" class="navbar-collapse collapse">
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
<li><a href="/"><i class="home icon"></i>{{tr "Home" }}</a></li>
|
<li :class="isMenuActive('index')"><a href="/"><i class="home icon"></i>{{tr "Home" }}</a></li>
|
||||||
<template v-if="isMobile">
|
<li :class="isMenuActive('service')"><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
|
||||||
<li class="dropdown">
|
<li :class="isMenuActive('network')"><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
|
||||||
<a data-toggle="dropdown"><i class="bi bi-gear-wide-connected" style="position:relative;top:1px;margin-right:3px;font-size:1.1rem;"></i>{{tr "Feature" }}<b class="caret"></b></a>
|
|
||||||
<ul class="dropdown-menu" style="min-width:100px;">
|
|
||||||
<li><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
|
|
||||||
<li><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<li><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
|
|
||||||
<li><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
|
|
||||||
</template>
|
|
||||||
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
|
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a data-toggle="dropdown"><i class="bi bi-incognito" style="position:relative;top:1px;margin-right:3px;font-size:1.2rem;vertical-align:top;"></i>{{tr "Template" }}<b class="caret"></b></a>
|
<a data-toggle="dropdown"><i class="bi bi-incognito" style="position:relative;top:1px;margin-right:3px;font-size:1.2rem;vertical-align:top;"></i>{{tr "Template" }}<b class="caret"></b></a>
|
||||||
@@ -64,6 +53,6 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="nezha">
|
<section class="nezha">
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
+82
-81
@@ -11,11 +11,11 @@
|
|||||||
<input type="text" id="dropdown-search" class="form-control" placeholder="Search...">
|
<input type="text" id="dropdown-search" class="form-control" placeholder="Search...">
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown-item" v-for="server in servers" @click="showCharts(server.ID)">
|
<li class="dropdown-item" v-for="server in servers" @click="showCharts(server.ID)">
|
||||||
<a><i :class="'fi fi-' + (server.Host.CountryCode || 'rb')"></i> @#server.Name#@ <i v-if="server.ID == currentServerId" class="check icon"></i></a>
|
<a><i :class="'fi fi-' + (server.Host.CountryCode || 'un')"></i> @#server.Name#@ <i v-if="server.ID == currentServerId" class="check icon"></i></a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="chartTitle"><i class="chartCountryCode" :class="'fi fi-' + chartCountryCode"></i> @#chartTitle#@</div>
|
<div class="chartTitle" @click="showCharts(nextServerId)"><i class="chartCountryCode" :class="'fi fi-' + chartCountryCode"></i> @#chartTitle#@</div>
|
||||||
<div id="chartbox" style="width:100%;height:auto;"></div>
|
<div id="chartbox" style="width:100%;height:auto;"></div>
|
||||||
</div>
|
</div>
|
||||||
{{template "theme-server-status/footer" .}}
|
{{template "theme-server-status/footer" .}}
|
||||||
@@ -32,7 +32,8 @@
|
|||||||
chartTitle: '',
|
chartTitle: '',
|
||||||
chartCountryCode: '',
|
chartCountryCode: '',
|
||||||
chart: null,
|
chart: null,
|
||||||
currentServerId: ''
|
currentServerId: '',
|
||||||
|
nextServerId: '',
|
||||||
},
|
},
|
||||||
mixins: [mixinsVue],
|
mixins: [mixinsVue],
|
||||||
created() {
|
created() {
|
||||||
@@ -44,7 +45,6 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showCharts(id) {
|
showCharts(id) {
|
||||||
const chartContainer = document.getElementById('chartbox');
|
|
||||||
// 发起数据请求
|
// 发起数据请求
|
||||||
const url = `/api/v1/monitor/${id}`;
|
const url = `/api/v1/monitor/${id}`;
|
||||||
fetch(url)
|
fetch(url)
|
||||||
@@ -64,44 +64,50 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
renderCharts(id, reload = false) {
|
renderCharts(id, reload = false) {
|
||||||
if (!this.chartDataList[id]) return;
|
if(!this.chartDataList[id]) return;
|
||||||
this.disposeCharts();
|
if(this.chart) this.disposeCharts(this.chart);
|
||||||
this.currentServerId = id;
|
this.currentServerId = id;
|
||||||
|
this.nextServerId = this.getNextServerId(id);
|
||||||
this.chartCountryCode = this.getServerCountryCode(id);
|
this.chartCountryCode = this.getServerCountryCode(id);
|
||||||
|
this.chartTitle = this.chartDataList[id][0].server_name;
|
||||||
const chartData = this.chartDataList[id];
|
const chartData = this.chartDataList[id];
|
||||||
const chartContainer = document.getElementById('chartbox');
|
const chartContainer = document.getElementById('chartbox');
|
||||||
this.chartTitle = chartData[0].server_name;
|
|
||||||
if (reload) {
|
|
||||||
const existingChart = echarts.getInstanceByDom(chartContainer);
|
|
||||||
if (existingChart) existingChart.dispose();
|
|
||||||
}
|
|
||||||
// 定义图表参数值
|
|
||||||
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
|
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
|
||||||
const autoheight = this.isMobile ? (window.innerHeight - 200) : (window.innerHeight - 250);
|
const autoheight = this.isMobile ? (window.innerHeight - 180) : (window.innerHeight - 250);
|
||||||
const fontSize = this.isMobile ? 10 : 14;
|
const fontSize = this.isMobile ? 10 : 14;
|
||||||
const gridLeft = this.isMobile ? 30 : 36;
|
const gridLeft = (MaxTCPPingValue > 500) ? (this.isMobile ? 36 : 42) : (this.isMobile ? 30 : 36);
|
||||||
const gridRight = this.isMobile ? 12 : 20;
|
const gridRight = this.isMobile ? 12 : 20;
|
||||||
const legendLeft = this.isMobile ? 'center' : 'center';
|
const legendLeft = this.isMobile ? 'center' : 'center';
|
||||||
const legendTop = this.isMobile ? 5 : 5;
|
const legendTop = this.isMobile ? 5 : 5;
|
||||||
const legendPadding= this.isMobile ? [5,0,5,0] : [5,0,5,0];
|
const legendPadding= this.isMobile ? [5,0,5,0] : [5,0,5,0];
|
||||||
const fontColor = this.theme == "dark" ? "#f1f1f1" : "#000000";
|
const fontColor = this.theme == "dark" ? "#f1f1f1" : "#000000";
|
||||||
|
const chartTheme = this.theme == "dark" ? "dark" : "default";
|
||||||
const backgroundColor = this.theme == "dark" ? '' : '';
|
const backgroundColor = this.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 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 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 lineStyleWidth = this.isMobile ? 1 : 2;
|
||||||
const splitLineWidth = this.isMobile ? 0.5 : 1;
|
const splitLineWidth = this.isMobile ? 0.5 : 1;
|
||||||
const splitLineColor = this.theme == "dark" ? "rgba(110, 112, 121, 0.95)" : "rgba(224, 230, 241, 0.95)";
|
const markPointSymbolSize = this.isMobile ? 36 : 42;
|
||||||
const markPointLabelColor = this.theme == "dark" ? "#111111" : "#000000";
|
const markPointItemStyleOpacity = this.semiTransparent ? 1 : 1;
|
||||||
const markPointItemStyleOpacity = this.semiTransparent ? 1 : 0.75;
|
const markPointFontSize = this.isMobile ? 8 : 10;
|
||||||
const markLineItemStyleOpacity = this.semiTransparent ? 1 : 0.75;
|
const markLineItemStyleOpacity = this.semiTransparent ? 1 : 0.75;
|
||||||
const markLineLineStyleWidth = this.isMobile ? 0.15 : 0.3;
|
const markLineLineStyleWidth = this.isMobile ? 0.15 : 0.3;
|
||||||
this.chart = echarts.init(chartContainer, '', { // init图表
|
const showLoadingMaskColor = this.theme == "dark" ? 'rgba(0, 0, 0, 0)' : 'rgba(255, 255, 255, 0)';
|
||||||
|
const showLoadingTextColor = this.theme == "dark" ? 'rgba(241, 241, 241, 1)' : 'rgba(0, 0, 0, 1)';
|
||||||
|
const showLoadingColor = this.theme == "dark" ? '#D2B206' : '#FFDF32';
|
||||||
|
this.chart = echarts.init(chartContainer, chartTheme, { // init图表
|
||||||
renderer: 'canvas',
|
renderer: 'canvas',
|
||||||
useDirtyRect: false,
|
useDirtyRect: false,
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
height: autoheight,
|
height: autoheight,
|
||||||
});
|
});
|
||||||
// 获取图表数据
|
this.chart.showLoading({
|
||||||
|
text: 'loading',
|
||||||
|
textColor: showLoadingTextColor,
|
||||||
|
color: showLoadingColor,
|
||||||
|
maskColor: showLoadingMaskColor,
|
||||||
|
zlevel: 2
|
||||||
|
});
|
||||||
let legendData = [];
|
let legendData = [];
|
||||||
let seriesData = [];
|
let seriesData = [];
|
||||||
chartData.forEach((item,key)=> {
|
chartData.forEach((item,key)=> {
|
||||||
@@ -111,34 +117,32 @@
|
|||||||
let data = { main: [], markLine: []};
|
let data = { main: [], markLine: []};
|
||||||
item.avg_delay.forEach((avgDelay, index) => {
|
item.avg_delay.forEach((avgDelay, index) => {
|
||||||
const threshold = 0.9 * MaxTCPPingValue; // 定义阀值,用于判断是否丢包
|
const threshold = 0.9 * MaxTCPPingValue; // 定义阀值,用于判断是否丢包
|
||||||
const filterAvgDelay = item.avg_delay.filter(value => value !== 0 && value !== MaxTCPPingValue);
|
|
||||||
const max = Math.max(...filterAvgDelay).toFixed(1);
|
|
||||||
const autoAvgDelay = 1.05 * max > 0.91 * MaxTCPPingValue ? 1.05 * max : 0.91 * MaxTCPPingValue;
|
|
||||||
// 定义丢包 1. avgDelay==0 2. avgDelay>=MaxTCPPingValue 3. avgDelay>=threshold
|
// 定义丢包 1. avgDelay==0 2. avgDelay>=MaxTCPPingValue 3. avgDelay>=threshold
|
||||||
if(avgDelay == 0 || avgDelay >= MaxTCPPingValue){ //绝对丢包
|
if(avgDelay == 0 || avgDelay >= MaxTCPPingValue){ //绝对丢包
|
||||||
loss += 1;
|
loss += 1;
|
||||||
const lossrate = 100 * loss / (index + 1);
|
const lossrate = 100 * loss / (index + 1);
|
||||||
data['main'].push(
|
if(lossrate != 100) {
|
||||||
[item.created_at[index], autoAvgDelay, lossrate]
|
data['markLine'].push({
|
||||||
);
|
xAxis: item.created_at[index],
|
||||||
data['markLine'].push({
|
label: { show: false },
|
||||||
xAxis: item.created_at[index],
|
emphasis: { disabled: true },
|
||||||
label: { show: false },
|
lineStyle: { type: "solid" }
|
||||||
emphasis: { disabled: true },
|
});
|
||||||
lineStyle: { type: "solid" }
|
}
|
||||||
});
|
|
||||||
} else if (avgDelay >= threshold && avgDelay < MaxTCPPingValue){ // 相对丢包
|
} else if (avgDelay >= threshold && avgDelay < MaxTCPPingValue){ // 相对丢包
|
||||||
loss += 1;
|
loss += 1;
|
||||||
const lossrate = 100 * loss / (index + 1);
|
const lossrate = 100 * loss / (index + 1);
|
||||||
data['main'].push(
|
if(lossrate != 100) {
|
||||||
[item.created_at[index], avgDelay, lossrate]
|
data['main'].push(
|
||||||
);
|
[item.created_at[index], avgDelay, lossrate]
|
||||||
data['markLine'].push({
|
);
|
||||||
xAxis: item.created_at[index],
|
data['markLine'].push({
|
||||||
label: { show: false },
|
xAxis: item.created_at[index],
|
||||||
emphasis: { disabled: true },
|
label: { show: false },
|
||||||
lineStyle: { type: "solid" }
|
emphasis: { disabled: true },
|
||||||
});
|
lineStyle: { type: "solid" }
|
||||||
|
});
|
||||||
|
}
|
||||||
} else { // 未丢包
|
} else { // 未丢包
|
||||||
const lossrate = 100 * loss / (index + 1);
|
const lossrate = 100 * loss / (index + 1);
|
||||||
data['main'].push(
|
data['main'].push(
|
||||||
@@ -146,11 +150,9 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 处理legendData
|
|
||||||
totalLossRate = ((loss / item.created_at.length) * 100).toFixed(1);
|
totalLossRate = ((loss / item.created_at.length) * 100).toFixed(1);
|
||||||
legendName = `${item.monitor_name} ${totalLossRate}%`;
|
legendName = `${item.monitor_name} ${totalLossRate}%`;
|
||||||
legendData.push(legendName);
|
legendData.push(legendName);
|
||||||
// 处理seriesData
|
|
||||||
seriesData.push(
|
seriesData.push(
|
||||||
{
|
{
|
||||||
name: legendName,
|
name: legendName,
|
||||||
@@ -158,6 +160,10 @@
|
|||||||
smooth: true,
|
smooth: true,
|
||||||
symbol: 'none',
|
symbol: 'none',
|
||||||
connectNulls: true,
|
connectNulls: true,
|
||||||
|
legendHoverLink: false,
|
||||||
|
emphasis: {
|
||||||
|
disabled: true
|
||||||
|
},
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: lineStyleWidth
|
width: lineStyleWidth
|
||||||
},
|
},
|
||||||
@@ -182,10 +188,9 @@
|
|||||||
itemStyle: {
|
itemStyle: {
|
||||||
opacity: markPointItemStyleOpacity
|
opacity: markPointItemStyleOpacity
|
||||||
},
|
},
|
||||||
symbolSize: 30,
|
symbolSize: markPointSymbolSize,
|
||||||
label: {
|
label: {
|
||||||
fontSize: 8,
|
fontSize: markPointFontSize,
|
||||||
color: markPointLabelColor,
|
|
||||||
formatter: function (params) {
|
formatter: function (params) {
|
||||||
return Math.round(params.value);
|
return Math.round(params.value);
|
||||||
}
|
}
|
||||||
@@ -198,10 +203,9 @@
|
|||||||
itemStyle: {
|
itemStyle: {
|
||||||
opacity: markPointItemStyleOpacity
|
opacity: markPointItemStyleOpacity
|
||||||
},
|
},
|
||||||
symbolSize: 30,
|
symbolSize: markPointSymbolSize,
|
||||||
label: {
|
label: {
|
||||||
fontSize: 8,
|
fontSize: markPointFontSize,
|
||||||
color: markPointLabelColor,
|
|
||||||
offset: [0, 8],
|
offset: [0, 8],
|
||||||
formatter: function (params) {
|
formatter: function (params) {
|
||||||
return Math.round(params.value);
|
return Math.round(params.value);
|
||||||
@@ -216,9 +220,10 @@
|
|||||||
});
|
});
|
||||||
const maxLegendsPerRowMobile = localStorage.getItem("maxLegendsPerRowMobile") ? localStorage.getItem("maxLegendsPerRowMobile") : 3;
|
const maxLegendsPerRowMobile = localStorage.getItem("maxLegendsPerRowMobile") ? localStorage.getItem("maxLegendsPerRowMobile") : 3;
|
||||||
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6;
|
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6;
|
||||||
const autoIncrement = Math.floor((legendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 20 : 28)
|
const autoIncrement = Math.floor((legendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 20 : 28);
|
||||||
const height = autoheight + autoIncrement;
|
const height = autoheight + autoIncrement;
|
||||||
const gridTop = 40 + autoIncrement;
|
const gridTop = this.isMobile ? ( 60 + autoIncrement) : (80 + autoIncrement);
|
||||||
|
const gridBottom = this.isMobile ? 70 : 90;
|
||||||
const legendIcon = this.isMobile ? 'rect' : "";
|
const legendIcon = this.isMobile ? 'rect' : "";
|
||||||
const itemWidth = this.isMobile ? 10 : 25;
|
const itemWidth = this.isMobile ? 10 : 25;
|
||||||
const itemHeight = this.isMobile ? 10 : 14;
|
const itemHeight = this.isMobile ? 10 : 14;
|
||||||
@@ -228,28 +233,22 @@
|
|||||||
});
|
});
|
||||||
// 设置图表配置项
|
// 设置图表配置项
|
||||||
const option = {
|
const option = {
|
||||||
// 全局调色盘
|
|
||||||
color: this.colors,
|
color: this.colors,
|
||||||
// 背景颜色
|
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
// 文字样式
|
|
||||||
textStyle: {
|
textStyle: {
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
color: fontColor
|
color: fontColor
|
||||||
},
|
},
|
||||||
// 图表网格设置
|
|
||||||
grid: {
|
grid: {
|
||||||
top: gridTop,
|
top: gridTop,
|
||||||
left: gridLeft,
|
left: gridLeft,
|
||||||
right: gridRight
|
right: gridRight,
|
||||||
|
bottom: gridBottom
|
||||||
},
|
},
|
||||||
// 图表标题设置
|
|
||||||
title: {
|
title: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
// 图表系列数据设置
|
|
||||||
series: seriesData.flat(),
|
series: seriesData.flat(),
|
||||||
// X轴设置
|
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'time',
|
type: 'time',
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
@@ -258,7 +257,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Y轴设置
|
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
@@ -268,12 +266,10 @@
|
|||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: splitLineWidth,
|
width: splitLineWidth
|
||||||
color: splitLineColor
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 图例设置
|
|
||||||
legend: {
|
legend: {
|
||||||
data: legendData,
|
data: legendData,
|
||||||
show: true,
|
show: true,
|
||||||
@@ -289,7 +285,6 @@
|
|||||||
itemWidth: itemWidth,
|
itemWidth: itemWidth,
|
||||||
itemHeight: itemHeight,
|
itemHeight: itemHeight,
|
||||||
},
|
},
|
||||||
// 提示框设置
|
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
backgroundColor: tooltipBackgroundColor,
|
backgroundColor: tooltipBackgroundColor,
|
||||||
@@ -312,7 +307,6 @@
|
|||||||
return tooltipContent;
|
return tooltipContent;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 数据缩放设置
|
|
||||||
dataZoom: [
|
dataZoom: [
|
||||||
{
|
{
|
||||||
type: 'slider',
|
type: 'slider',
|
||||||
@@ -321,27 +315,34 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
// 设置图表的配置选项
|
setTimeout(() => {
|
||||||
this.chart.setOption(option);
|
this.chart.hideLoading();
|
||||||
|
this.chart.setOption(option);
|
||||||
|
}, 1000);
|
||||||
},
|
},
|
||||||
reloadCharts() { // 重新加载所有图表
|
reloadCharts() {
|
||||||
this.servers.forEach(node => {
|
const chartData = this.chartDataList[this.currentServerId];
|
||||||
const id = node.ID;
|
if (chartData) {
|
||||||
const chartData = this.chartDataList[id];
|
this.renderCharts(this.currentServerId,true);
|
||||||
if (chartData) {
|
|
||||||
this.renderCharts(id,true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
disposeCharts(){
|
|
||||||
if(this.chart) {
|
|
||||||
this.chart.dispose();
|
|
||||||
this.chart = null;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
disposeCharts(chart){
|
||||||
|
chart.dispose();
|
||||||
|
chart = null;
|
||||||
|
},
|
||||||
getServerCountryCode(id){
|
getServerCountryCode(id){
|
||||||
const result = this.servers.find(item => item.ID == id);
|
const result = this.servers.find(item => item.ID == id);
|
||||||
return result.Host.CountryCode ? result.Host.CountryCode : 'rb';
|
return result.Host.CountryCode ? result.Host.CountryCode : 'un';
|
||||||
|
},
|
||||||
|
getNextServerId(id) {
|
||||||
|
const currentIndex = this.servers.findIndex(item => item.ID === id);
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
return this.servers[0].ID;
|
||||||
|
}
|
||||||
|
// 判断是否有下一个元素
|
||||||
|
const nextIndex = currentIndex + 1;
|
||||||
|
// 如果有下一个元素,返回下一个元素的 ID;否则返回第一个元素的 ID
|
||||||
|
return nextIndex < this.servers.length ? this.servers[nextIndex].ID : this.servers[0].ID;
|
||||||
},
|
},
|
||||||
initSearch() {
|
initSearch() {
|
||||||
$('#dropdown-search').on('keyup', function() {
|
$('#dropdown-search').on('keyup', function() {
|
||||||
|
|||||||
+10
-10
@@ -3,24 +3,24 @@
|
|||||||
{{template "theme-server-status/menu" .}}
|
{{template "theme-server-status/menu" .}}
|
||||||
<!-- showGroup true -->
|
<!-- showGroup true -->
|
||||||
<template v-if="showGroup">
|
<template v-if="showGroup">
|
||||||
<section v-if="servicesTag.length === 0" class="container-fluid content" style="min-height: .01%;overflow-x: auto;">
|
<div v-if="servicesTag.length === 0" class="container-fluid content" style="min-height: .01%;overflow-x: auto;">
|
||||||
<p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p>
|
<p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p>
|
||||||
</section>
|
</div>
|
||||||
<section v-else class="container-fluid content" style="min-height: .01%;overflow-x: auto;" v-for="group in servicesTag">
|
<div v-else class="container-fluid content" style="min-height: .01%;overflow-x: auto;" v-for="group in servicesTag">
|
||||||
{{template "theme-server-status/service-group-true" .}}
|
{{template "theme-server-status/service-group-true" .}}
|
||||||
</section>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- showGroup false -->
|
<!-- showGroup false -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<section v-if="servicesNoTag.length === 0" class="container-fluid content" style="min-height: .01%;overflow-x: auto;">
|
<div v-if="servicesNoTag.length === 0" class="container-fluid content" style="min-height: .01%;overflow-x: auto;">
|
||||||
<p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p>
|
<p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p>
|
||||||
</section>
|
</div>
|
||||||
<section v-else class="container-fluid content" style="min-height: .01%;overflow-x: auto;">
|
<div v-else class="container-fluid content" style="min-height: .01%;overflow-x: auto;">
|
||||||
{{template "theme-server-status/service-group-false" .}}
|
{{template "theme-server-status/service-group-false" .}}
|
||||||
</section>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
{{if .CycleTransferStats}}
|
{{if .CycleTransferStats}}
|
||||||
<section class="container-fluid content table-responsive">
|
<div class="container-fluid content table-responsive">
|
||||||
<table class="table table-striped table-condensed table-hover">
|
<table class="table table-striped table-condensed table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="node-group-tag">
|
<tr class="node-group-tag">
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "theme-server-status/footer" .}}
|
{{template "theme-server-status/footer" .}}
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -12,22 +12,3 @@ site:
|
|||||||
brand: "nz_site_title"
|
brand: "nz_site_title"
|
||||||
cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改
|
cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改
|
||||||
theme: "default"
|
theme: "default"
|
||||||
ddns:
|
|
||||||
enable: false
|
|
||||||
provider: "webhook" # 如需使用多配置功能,请把此项留空
|
|
||||||
accessid: ""
|
|
||||||
accesssecret: ""
|
|
||||||
webhookmethod: ""
|
|
||||||
webhookurl: ""
|
|
||||||
webhookrequestbody: ""
|
|
||||||
webhookheaders: ""
|
|
||||||
maxretries: 3
|
|
||||||
profiles:
|
|
||||||
example:
|
|
||||||
provider: ""
|
|
||||||
accessid: ""
|
|
||||||
accesssecret: ""
|
|
||||||
webhookmethod: ""
|
|
||||||
webhookurl: ""
|
|
||||||
webhookrequestbody: ""
|
|
||||||
webhookheaders: ""
|
|
||||||
+1
-1
@@ -51,7 +51,7 @@ if ([string]::IsNullOrWhiteSpace($agenttag)) {
|
|||||||
#Region判断
|
#Region判断
|
||||||
$ipapi = ""
|
$ipapi = ""
|
||||||
$region = "Unknown"
|
$region = "Unknown"
|
||||||
foreach ($url in ("https://dash.cloudflare.com/cdn-cgi/trace","https://cf-ns.com/cdn-cgi/trace","https://1.0.0.1/cdn-cgi/trace")) {
|
foreach ($url in ("https://dash.cloudflare.com/cdn-cgi/trace","https://developers.cloudflare.com/cdn-cgi/trace","https://1.0.0.1/cdn-cgi/trace")) {
|
||||||
try {
|
try {
|
||||||
$ipapi = Invoke-RestMethod -Uri $url -TimeoutSec 5 -UseBasicParsing
|
$ipapi = Invoke-RestMethod -Uri $url -TimeoutSec 5 -UseBasicParsing
|
||||||
if ($ipapi -match "loc=(\w+)" ) {
|
if ($ipapi -match "loc=(\w+)" ) {
|
||||||
|
|||||||
+112
-96
@@ -12,13 +12,13 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
|
|||||||
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
|
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
|
||||||
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
||||||
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
||||||
NZ_VERSION="v0.18.3"
|
NZ_VERSION="v0.20.2"
|
||||||
|
|
||||||
red='\033[0;31m'
|
red='\033[0;31m'
|
||||||
green='\033[0;32m'
|
green='\033[0;32m'
|
||||||
yellow='\033[0;33m'
|
yellow='\033[0;33m'
|
||||||
plain='\033[0m'
|
plain='\033[0m'
|
||||||
export PATH=$PATH:/usr/local/bin
|
export PATH="$PATH:/usr/local/bin"
|
||||||
|
|
||||||
os_arch=""
|
os_arch=""
|
||||||
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
|
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
|
||||||
@@ -45,26 +45,36 @@ check_systemd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err() {
|
err() {
|
||||||
printf "${red}$*${plain}\n" >&2
|
printf "${red}%s${plain}\n" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
printf "${green}%s${plain}\n" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
printf "${yellow}%s${plain}\n" "$*"
|
||||||
}
|
}
|
||||||
|
|
||||||
geo_check() {
|
geo_check() {
|
||||||
api_list="https://blog.cloudflare.com/cdn-cgi/trace https://dash.cloudflare.com/cdn-cgi/trace https://cf-ns.com/cdn-cgi/trace"
|
api_list="https://blog.cloudflare.com/cdn-cgi/trace https://dash.cloudflare.com/cdn-cgi/trace https://developers.cloudflare.com/cdn-cgi/trace"
|
||||||
ua="Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
|
ua="Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
|
||||||
set -- $api_list
|
set -- "$api_list"
|
||||||
for url in $api_list; do
|
for url in $api_list; do
|
||||||
text="$(curl -A "$ua" -m 10 -s $url)"
|
text="$(curl -A "$ua" -m 10 -s "$url")"
|
||||||
endpoint="$(echo $text | sed -n 's/.*h=\([^ ]*\).*/\1/p')"
|
endpoint="$(echo "$text" | sed -n 's/.*h=\([^ ]*\).*/\1/p')"
|
||||||
if echo $text | grep -qw 'CN'; then
|
if echo "$text" | grep -qw 'CN'; then
|
||||||
isCN=true
|
isCN=true
|
||||||
break
|
break
|
||||||
elif echo $url | grep -q $endpoint; then
|
elif echo "$url" | grep -q "$endpoint"; then
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
pre_check() {
|
pre_check() {
|
||||||
|
umask 077
|
||||||
|
|
||||||
## os_arch
|
## os_arch
|
||||||
if uname -m | grep -q 'x86_64'; then
|
if uname -m | grep -q 'x86_64'; then
|
||||||
os_arch="amd64"
|
os_arch="amd64"
|
||||||
@@ -83,9 +93,9 @@ pre_check() {
|
|||||||
## China_IP
|
## China_IP
|
||||||
if [ -z "$CN" ]; then
|
if [ -z "$CN" ]; then
|
||||||
geo_check
|
geo_check
|
||||||
if [ ! -z "$isCN" ]; then
|
if [ -n "$isCN" ]; then
|
||||||
echo "根据geoip api提供的信息,当前IP可能在中国"
|
echo "根据geoip api提供的信息,当前IP可能在中国"
|
||||||
printf "是否选用中国镜像完成安装? [Y/n] (自定义镜像输入 3):"
|
printf "是否选用中国镜像完成安装? [Y/n] (自定义镜像输入 3):"
|
||||||
read -r input
|
read -r input
|
||||||
case $input in
|
case $input in
|
||||||
[yY][eE][sS] | [yY])
|
[yY][eE][sS] | [yY])
|
||||||
@@ -178,7 +188,9 @@ installation_check() {
|
|||||||
|
|
||||||
select_version() {
|
select_version() {
|
||||||
if [ -z "$IS_DOCKER_NEZHA" ]; then
|
if [ -z "$IS_DOCKER_NEZHA" ]; then
|
||||||
printf "${yellow}请自行选择您的安装方式(如果你是安装Agent,输入哪个都是一样的):\n1. Docker\n2. 独立安装${plain}\n"
|
info "请自行选择您的安装方式(如果你是安装Agent,输入哪个都是一样的):"
|
||||||
|
info "1. Docker"
|
||||||
|
info "2. 独立安装"
|
||||||
while true; do
|
while true; do
|
||||||
printf "请输入选择 [1-2]:"
|
printf "请输入选择 [1-2]:"
|
||||||
read -r option
|
read -r option
|
||||||
@@ -204,7 +216,7 @@ update_script() {
|
|||||||
|
|
||||||
curl -sL https://${GITHUB_RAW_URL}/script/install.sh -o /tmp/nezha.sh
|
curl -sL https://${GITHUB_RAW_URL}/script/install.sh -o /tmp/nezha.sh
|
||||||
new_version=$(grep "NZ_VERSION" /tmp/nezha.sh | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
new_version=$(grep "NZ_VERSION" /tmp/nezha.sh | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
||||||
if [ ! -n "$new_version" ]; then
|
if [ -z "$new_version" ]; then
|
||||||
echo "脚本获取失败,请检查本机能否链接 https://${GITHUB_RAW_URL}/script/install.sh"
|
echo "脚本获取失败,请检查本机能否链接 https://${GITHUB_RAW_URL}/script/install.sh"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -219,7 +231,7 @@ update_script() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
before_show_menu() {
|
before_show_menu() {
|
||||||
echo && printf "${yellow}* 按回车返回主菜单 *${plain}" && read temp
|
echo && info "* 按回车返回主菜单 *" && read temp
|
||||||
show_menu
|
show_menu
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +241,7 @@ install_base() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
install_arch() {
|
install_arch() {
|
||||||
printf "${green}提示: ${plain} Arch安装libselinux需添加nezha-agent用户,安装完会自动删除,建议手动检查一次\n"
|
info "提示:Arch安装libselinux需添加nezha-agent用户,安装完会自动删除,建议手动检查一次"
|
||||||
read -r -p "是否安装libselinux? [Y/n] " input
|
read -r -p "是否安装libselinux? [Y/n] " input
|
||||||
case $input in
|
case $input in
|
||||||
[yY][eE][sS] | [yY])
|
[yY][eE][sS] | [yY])
|
||||||
@@ -253,11 +265,11 @@ install_arch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
install_soft() {
|
install_soft() {
|
||||||
(command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install $* selinux-policy -y) ||
|
(command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install "$@" selinux-policy -y) ||
|
||||||
(command -v apt >/dev/null 2>&1 && sudo apt update && sudo apt install $* selinux-utils -y) ||
|
(command -v apt >/dev/null 2>&1 && sudo apt update && sudo apt install "$@" selinux-utils -y) ||
|
||||||
(command -v pacman >/dev/null 2>&1 && sudo pacman -Syu $* base-devel --noconfirm && install_arch) ||
|
(command -v pacman >/dev/null 2>&1 && sudo pacman -Syu "$@" base-devel --noconfirm && install_arch) ||
|
||||||
(command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install $* selinux-utils -y) ||
|
(command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install "$@" selinux-utils -y) ||
|
||||||
(command -v apk >/dev/null 2>&1 && sudo apk update && sudo apk add $* -f)
|
(command -v apk >/dev/null 2>&1 && sudo apk update && sudo apk add "$@" -f)
|
||||||
}
|
}
|
||||||
|
|
||||||
install_dashboard() {
|
install_dashboard() {
|
||||||
@@ -288,8 +300,6 @@ install_dashboard() {
|
|||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sudo chmod -R 700 $NZ_DASHBOARD_PATH
|
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
install_dashboard_docker
|
install_dashboard_docker
|
||||||
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
||||||
@@ -305,12 +315,10 @@ install_dashboard() {
|
|||||||
|
|
||||||
install_dashboard_docker() {
|
install_dashboard_docker() {
|
||||||
if [ ! "$FRESH_INSTALL" = 0 ]; then
|
if [ ! "$FRESH_INSTALL" = 0 ]; then
|
||||||
command -v docker >/dev/null 2>&1
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
if [ $? != 0 ]; then
|
|
||||||
echo "正在安装 Docker"
|
echo "正在安装 Docker"
|
||||||
if [ "$os_alpine" != 1 ]; then
|
if [ "$os_alpine" != 1 ]; then
|
||||||
curl -sL https://${Get_Docker_URL} | sudo bash -s ${Get_Docker_Argu}
|
if ! curl -sL https://${Get_Docker_URL} | sudo bash -s "${Get_Docker_Argu}"; then
|
||||||
if [ $? != 0 ]; then
|
|
||||||
err "下载脚本失败,请检查本机能否连接 ${Get_Docker_URL}"
|
err "下载脚本失败,请检查本机能否连接 ${Get_Docker_URL}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -321,7 +329,7 @@ install_dashboard_docker() {
|
|||||||
sudo rc-update add docker
|
sudo rc-update add docker
|
||||||
sudo rc-service docker start
|
sudo rc-service docker start
|
||||||
fi
|
fi
|
||||||
printf "${green}Docker${plain} 安装成功\n"
|
success "Docker 安装成功"
|
||||||
installation_check
|
installation_check
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -334,13 +342,11 @@ install_dashboard_standalone() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selinux() {
|
selinux() {
|
||||||
#判断当前的状态
|
#Check SELinux
|
||||||
command -v getenforce >/dev/null 2>&1
|
if command -v getenforce >/dev/null 2>&1; then
|
||||||
if [ $? -eq 0 ]; then
|
if getenforce | grep '[Ee]nfor'; then
|
||||||
getenforce | grep '[Ee]nfor'
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "SELinux是开启状态,正在关闭!"
|
echo "SELinux是开启状态,正在关闭!"
|
||||||
sudo setenforce 0 &>/dev/null
|
sudo setenforce 0 >/dev/null 2>&1
|
||||||
find_key="SELINUX="
|
find_key="SELINUX="
|
||||||
sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config
|
sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config
|
||||||
fi
|
fi
|
||||||
@@ -355,36 +361,37 @@ install_agent() {
|
|||||||
|
|
||||||
echo "正在获取监控Agent版本号"
|
echo "正在获取监控Agent版本号"
|
||||||
|
|
||||||
local version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
|
||||||
if [ ! -n "$version" ]; then
|
_version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
||||||
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
|
if [ -z "$_version" ]; then
|
||||||
|
_version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
|
||||||
fi
|
fi
|
||||||
if [ ! -n "$version" ]; then
|
if [ -z "$_version" ]; then
|
||||||
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
|
_version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
|
||||||
fi
|
fi
|
||||||
if [ ! -n "$version" ]; then
|
if [ -z "$_version" ]; then
|
||||||
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
|
_version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -n "$version" ]; then
|
if [ -z "$_version" ]; then
|
||||||
err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/nezhahq/agent/releases/latest"
|
err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/nezhahq/agent/releases/latest"
|
||||||
return 1
|
return 1
|
||||||
else
|
else
|
||||||
echo "当前最新版本为: ${version}"
|
echo "当前最新版本为: ${_version}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 哪吒监控文件夹
|
# 哪吒监控文件夹
|
||||||
sudo mkdir -p $NZ_AGENT_PATH
|
sudo mkdir -p $NZ_AGENT_PATH
|
||||||
sudo chmod -R 700 $NZ_AGENT_PATH
|
|
||||||
|
|
||||||
echo "正在下载监控端"
|
echo "正在下载监控端"
|
||||||
if [ -z "$CN" ]; then
|
if [ -z "$CN" ]; then
|
||||||
NZ_AGENT_URL="https://${GITHUB_URL}/nezhahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip"
|
NZ_AGENT_URL="https://${GITHUB_URL}/nezhahq/agent/releases/download/${_version}/nezha-agent_linux_${os_arch}.zip"
|
||||||
else
|
else
|
||||||
NZ_AGENT_URL="https://${GITHUB_URL}/naibahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip"
|
NZ_AGENT_URL="https://${GITHUB_URL}/naibahq/agent/releases/download/${_version}/nezha-agent_linux_${os_arch}.zip"
|
||||||
fi
|
fi
|
||||||
wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip $NZ_AGENT_URL >/dev/null 2>&1
|
|
||||||
if [ $? != 0 ]; then
|
_cmd="wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip $NZ_AGENT_URL >/dev/null 2>&1"
|
||||||
|
if ! eval "$_cmd"; then
|
||||||
err "Release 下载失败,请检查本机能否连接 ${GITHUB_URL}"
|
err "Release 下载失败,请检查本机能否连接 ${GITHUB_URL}"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -436,14 +443,14 @@ modify_agent_config() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sudo ${NZ_AGENT_PATH}/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p $nz_client_secret $args >/dev/null 2>&1
|
_cmd="sudo ${NZ_AGENT_PATH}/nezha-agent service install -s $nz_grpc_host:$nz_grpc_port -p $nz_client_secret $args >/dev/null 2>&1"
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if ! eval "$_cmd"; then
|
||||||
sudo ${NZ_AGENT_PATH}/nezha-agent service uninstall >/dev/null 2>&1
|
sudo "${NZ_AGENT_PATH}"/nezha-agent service uninstall >/dev/null 2>&1
|
||||||
sudo ${NZ_AGENT_PATH}/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p $nz_client_secret $args >/dev/null 2>&1
|
sudo "${NZ_AGENT_PATH}"/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p "$nz_client_secret" "$args" >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf "Agent配置 ${green}修改成功,请稍等重启生效${plain}\n"
|
success "Agent配置 修改成功,请稍等重启生效"
|
||||||
|
|
||||||
#if [[ $# == 0 ]]; then
|
#if [[ $# == 0 ]]; then
|
||||||
# before_show_menu
|
# before_show_menu
|
||||||
@@ -454,16 +461,21 @@ modify_dashboard_config() {
|
|||||||
echo "> 修改面板配置"
|
echo "> 修改面板配置"
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
echo "正在下载 Docker 脚本"
|
if [ -n "$DOCKER_COMPOSE_COMMAND" ]; then
|
||||||
wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1
|
echo "正在下载 Docker 脚本"
|
||||||
if [ $? != 0 ]; then
|
_cmd="wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1"
|
||||||
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
|
if ! eval "$_cmd"; then
|
||||||
return 0
|
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
err "请手动安装 docker-compose。https://docs.docker.com/compose/install/linux/"
|
||||||
|
before_show_menu
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1
|
_cmd="wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1"
|
||||||
if [ $? != 0 ]; then
|
if ! eval "$_cmd"; then
|
||||||
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
|
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -525,18 +537,22 @@ modify_dashboard_config() {
|
|||||||
if [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
||||||
echo "正在下载服务文件"
|
echo "正在下载服务文件"
|
||||||
if [ "$os_alpine" != 1 ]; then
|
if [ "$os_alpine" != 1 ]; then
|
||||||
sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1
|
_download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1"
|
||||||
else
|
if ! eval "$_download"; then
|
||||||
sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1
|
|
||||||
sudo chmod +x $NZ_DASHBOARD_SERVICERC
|
|
||||||
if [ $? != 0 ]; then
|
|
||||||
err "文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
|
err "文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
_download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1"
|
||||||
|
if ! eval "$_download"; then
|
||||||
|
err "文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sudo chmod +x $NZ_DASHBOARD_SERVICERC
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf "面板配置 ${green}修改成功,请稍等重启生效${plain}\n"
|
success "面板配置 修改成功,请稍等重启生效"
|
||||||
|
|
||||||
restart_and_update
|
restart_and_update
|
||||||
|
|
||||||
@@ -549,14 +565,14 @@ restart_and_update() {
|
|||||||
echo "> 重启并更新面板"
|
echo "> 重启并更新面板"
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
restart_and_update_docker
|
_cmd="restart_and_update_docker"
|
||||||
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
||||||
restart_and_update_standalone
|
_cmd="restart_and_update_standalone"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $? = 0 ]; then
|
if eval "$_cmd"; then
|
||||||
printf "${green}哪吒监控 重启成功${plain}\n"
|
success "哪吒监控 重启成功"
|
||||||
printf "默认管理面板地址:${yellow}域名:站点访问端口${plain}\n"
|
info "默认管理面板地址:域名:站点访问端口"
|
||||||
else
|
else
|
||||||
err "重启失败,可能是因为启动时间超过了两秒,请稍后查看日志信息"
|
err "重启失败,可能是因为启动时间超过了两秒,请稍后查看日志信息"
|
||||||
fi
|
fi
|
||||||
@@ -573,15 +589,22 @@ restart_and_update_docker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
restart_and_update_standalone() {
|
restart_and_update_standalone() {
|
||||||
local version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
_version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
||||||
if [ ! -n "$version" ]; then
|
if [ -z "$_version" ]; then
|
||||||
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
|
_version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
|
||||||
fi
|
fi
|
||||||
if [ ! -n "$version" ]; then
|
if [ -z "$_version" ]; then
|
||||||
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
|
_version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
|
||||||
fi
|
fi
|
||||||
if [ ! -n "$version" ]; then
|
if [ -z "$_version" ]; then
|
||||||
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
|
_version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$_version" ]; then
|
||||||
|
err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/naiba/nezha/releases/latest"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
echo "当前最新版本为: ${_version}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$os_alpine" != 1 ]; then
|
if [ "$os_alpine" != 1 ]; then
|
||||||
@@ -591,20 +614,13 @@ restart_and_update_standalone() {
|
|||||||
sudo rc-service nezha-dashboard stop
|
sudo rc-service nezha-dashboard stop
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -n "$version" ]; then
|
|
||||||
err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/naiba/nezha/releases/latest"
|
|
||||||
return 1
|
|
||||||
else
|
|
||||||
echo "当前最新版本为: ${version}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$CN" ]; then
|
if [ -z "$CN" ]; then
|
||||||
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
|
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/${_version}/dashboard-linux-${os_arch}.zip"
|
||||||
else
|
else
|
||||||
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
|
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/${_version}/dashboard-linux-${os_arch}.zip"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_URL >/dev/null 2>&1 && sudo unzip -qq $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dist/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm -r $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_PATH/dist
|
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip "$NZ_DASHBOARD_URL" >/dev/null 2>&1 && sudo unzip -qq -o $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm $NZ_DASHBOARD_PATH/app.zip
|
||||||
|
|
||||||
if [ "$os_alpine" != 1 ]; then
|
if [ "$os_alpine" != 1 ]; then
|
||||||
sudo systemctl enable nezha-dashboard
|
sudo systemctl enable nezha-dashboard
|
||||||
@@ -619,13 +635,13 @@ start_dashboard() {
|
|||||||
echo "> 启动面板"
|
echo "> 启动面板"
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
start_dashboard_docker
|
_cmd="start_dashboard_docker"
|
||||||
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
||||||
start_dashboard_standalone
|
_cmd="start_dashboard_standalone"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $? = 0 ]; then
|
if eval "$_cmd"; then
|
||||||
printf "${green}哪吒监控 启动成功${plain}\n"
|
success "哪吒监控 启动成功"
|
||||||
else
|
else
|
||||||
err "启动失败,请稍后查看日志信息"
|
err "启动失败,请稍后查看日志信息"
|
||||||
fi
|
fi
|
||||||
@@ -651,13 +667,13 @@ stop_dashboard() {
|
|||||||
echo "> 停止面板"
|
echo "> 停止面板"
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
stop_dashboard_docker
|
_cmd="stop_dashboard_docker"
|
||||||
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
||||||
stop_dashboard_standalone
|
_cmd="stop_dashboard_standalone"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $? = 0 ]; then
|
if eval "$_cmd"; then
|
||||||
printf "${green}哪吒监控 停止成功${plain}\n"
|
success "哪吒监控 停止成功"
|
||||||
else
|
else
|
||||||
err "停止失败,请稍后查看日志信息"
|
err "停止失败,请稍后查看日志信息"
|
||||||
fi
|
fi
|
||||||
@@ -936,4 +952,4 @@ if [ $# -gt 0 ]; then
|
|||||||
else
|
else
|
||||||
select_version
|
select_version
|
||||||
show_menu
|
show_menu
|
||||||
fi
|
fi
|
||||||
|
|||||||
+132
-111
@@ -12,13 +12,13 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
|
|||||||
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
|
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
|
||||||
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
||||||
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
||||||
NZ_VERSION="v0.18.3"
|
NZ_VERSION="v0.20.2"
|
||||||
|
|
||||||
red='\033[0;31m'
|
red='\033[0;31m'
|
||||||
green='\033[0;32m'
|
green='\033[0;32m'
|
||||||
yellow='\033[0;33m'
|
yellow='\033[0;33m'
|
||||||
plain='\033[0m'
|
plain='\033[0m'
|
||||||
export PATH=$PATH:/usr/local/bin
|
export PATH="$PATH:/usr/local/bin"
|
||||||
|
|
||||||
os_arch=""
|
os_arch=""
|
||||||
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
|
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
|
||||||
@@ -45,26 +45,36 @@ check_systemd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err() {
|
err() {
|
||||||
printf "${red}$*${plain}\n" >&2
|
printf "${red}%s${plain}\n" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
printf "${green}%s${plain}\n" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
printf "${yellow}%s${plain}\n" "$*"
|
||||||
}
|
}
|
||||||
|
|
||||||
geo_check() {
|
geo_check() {
|
||||||
api_list="https://blog.cloudflare.com/cdn-cgi/trace https://dash.cloudflare.com/cdn-cgi/trace https://cf-ns.com/cdn-cgi/trace"
|
api_list="https://blog.cloudflare.com/cdn-cgi/trace https://dash.cloudflare.com/cdn-cgi/trace https://developers.cloudflare.com/cdn-cgi/trace"
|
||||||
ua="Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
|
ua="Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
|
||||||
set -- $api_list
|
set -- "$api_list"
|
||||||
for url in $api_list; do
|
for url in $api_list; do
|
||||||
text="$(curl -A "$ua" -m 10 -s $url)"
|
text="$(curl -A "$ua" -m 10 -s "$url")"
|
||||||
endpoint="$(echo $text | sed -n 's/.*h=\([^ ]*\).*/\1/p')"
|
endpoint="$(echo "$text" | sed -n 's/.*h=\([^ ]*\).*/\1/p')"
|
||||||
if echo $text | grep -qw 'CN'; then
|
if echo "$text" | grep -qw 'CN'; then
|
||||||
isCN=true
|
isCN=true
|
||||||
break
|
break
|
||||||
elif echo $url | grep -q $endpoint; then
|
elif echo "$url" | grep -q "$endpoint"; then
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
pre_check() {
|
pre_check() {
|
||||||
|
umask 077
|
||||||
|
|
||||||
## os_arch
|
## os_arch
|
||||||
if uname -m | grep -q 'x86_64'; then
|
if uname -m | grep -q 'x86_64'; then
|
||||||
os_arch="amd64"
|
os_arch="amd64"
|
||||||
@@ -83,7 +93,7 @@ pre_check() {
|
|||||||
## China_IP
|
## China_IP
|
||||||
if [ -z "$CN" ]; then
|
if [ -z "$CN" ]; then
|
||||||
geo_check
|
geo_check
|
||||||
if [ ! -z "$isCN" ]; then
|
if [ -n "$isCN" ]; then
|
||||||
echo "According to the information provided by various geoip api, the current IP may be in China"
|
echo "According to the information provided by various geoip api, the current IP may be in China"
|
||||||
printf "Will the installation be done with a Chinese Mirror? [Y/n] (Custom Mirror Input 3): "
|
printf "Will the installation be done with a Chinese Mirror? [Y/n] (Custom Mirror Input 3): "
|
||||||
read -r input
|
read -r input
|
||||||
@@ -177,7 +187,9 @@ installation_check() {
|
|||||||
|
|
||||||
select_version() {
|
select_version() {
|
||||||
if [ -z "$IS_DOCKER_NEZHA" ]; then
|
if [ -z "$IS_DOCKER_NEZHA" ]; then
|
||||||
printf "${yellow}Select your installation method(Input anything is ok if you are installing agent):\n1. Docker\n2. Standalone${plain}\n"
|
info "Select your installation method(Input anything is ok if you are installing agent):"
|
||||||
|
info "1. Docker"
|
||||||
|
info "2. Standalone"
|
||||||
while true; do
|
while true; do
|
||||||
printf "Please enter [1-2]: "
|
printf "Please enter [1-2]: "
|
||||||
read -r option
|
read -r option
|
||||||
@@ -203,7 +215,7 @@ update_script() {
|
|||||||
|
|
||||||
curl -sL https://${GITHUB_RAW_URL}/script/install_en.sh -o /tmp/nezha.sh
|
curl -sL https://${GITHUB_RAW_URL}/script/install_en.sh -o /tmp/nezha.sh
|
||||||
new_version=$(grep "NZ_VERSION" /tmp/nezha.sh | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
new_version=$(grep "NZ_VERSION" /tmp/nezha.sh | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
||||||
if [ ! -n "$new_version" ]; then
|
if [ -z "$new_version" ]; then
|
||||||
echo "Script failed to get, please check if the network can link https://${GITHUB_RAW_URL}/script/install.sh"
|
echo "Script failed to get, please check if the network can link https://${GITHUB_RAW_URL}/script/install.sh"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -218,7 +230,7 @@ update_script() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
before_show_menu() {
|
before_show_menu() {
|
||||||
echo && printf "${yellow}* Press Enter to return to the main menu *${plain}" && read temp
|
echo && info "* Press Enter to return to the main menu *" && read temp
|
||||||
show_menu
|
show_menu
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +240,7 @@ install_base() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
install_arch() {
|
install_arch() {
|
||||||
printf "${green}Info: ${plain} Archlinux needs to add nezha-agent user to install libselinux. It will be deleted automatically after installation. It is recommended to check manually\n"
|
info "Archlinux needs to add nezha-agent user to install libselinux. It will be deleted automatically after installation. It is recommended to check manually"
|
||||||
read -r -p "Do you need to install libselinux? [Y/n] " input
|
read -r -p "Do you need to install libselinux? [Y/n] " input
|
||||||
case $input in
|
case $input in
|
||||||
[yY][eE][sS] | [yY])
|
[yY][eE][sS] | [yY])
|
||||||
@@ -252,11 +264,11 @@ install_arch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
install_soft() {
|
install_soft() {
|
||||||
(command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install $* selinux-policy -y) ||
|
(command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install "$@" selinux-policy -y) ||
|
||||||
(command -v apt >/dev/null 2>&1 && sudo apt update && sudo apt install $* selinux-utils -y) ||
|
(command -v apt >/dev/null 2>&1 && sudo apt update && sudo apt install "$@" selinux-utils -y) ||
|
||||||
(command -v pacman >/dev/null 2>&1 && sudo pacman -Syu $* base-devel --noconfirm && install_arch) ||
|
(command -v pacman >/dev/null 2>&1 && sudo pacman -Syu "$@" base-devel --noconfirm && install_arch) ||
|
||||||
(command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install $* selinux-utils -y) ||
|
(command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install "$@" selinux-utils -y) ||
|
||||||
(command -v apk >/dev/null 2>&1 && sudo apk update && sudo apk add $* -f)
|
(command -v apk >/dev/null 2>&1 && sudo apk update && sudo apk add "$@" -f)
|
||||||
}
|
}
|
||||||
|
|
||||||
install_dashboard() {
|
install_dashboard() {
|
||||||
@@ -287,8 +299,6 @@ install_dashboard() {
|
|||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sudo chmod -R 700 $NZ_DASHBOARD_PATH
|
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
install_dashboard_docker
|
install_dashboard_docker
|
||||||
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
||||||
@@ -304,12 +314,10 @@ install_dashboard() {
|
|||||||
|
|
||||||
install_dashboard_docker() {
|
install_dashboard_docker() {
|
||||||
if [ ! "$FRESH_INSTALL" = 0 ]; then
|
if [ ! "$FRESH_INSTALL" = 0 ]; then
|
||||||
command -v docker >/dev/null 2>&1
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
if [ $? != 0 ]; then
|
|
||||||
echo "Installing Docker"
|
echo "Installing Docker"
|
||||||
if [ "$os_alpine" != 1 ]; then
|
if [ "$os_alpine" != 1 ]; then
|
||||||
curl -sL https://${Get_Docker_URL} | sudo bash -s ${Get_Docker_Argu}
|
if ! curl -sL https://${Get_Docker_URL} | sudo bash -s "${Get_Docker_Argu}"; then
|
||||||
if [ $? != 0 ]; then
|
|
||||||
err "Script failed to get, please check if the network can link ${Get_Docker_URL}"
|
err "Script failed to get, please check if the network can link ${Get_Docker_URL}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -320,7 +328,7 @@ install_dashboard_docker() {
|
|||||||
sudo rc-update add docker
|
sudo rc-update add docker
|
||||||
sudo rc-service docker start
|
sudo rc-service docker start
|
||||||
fi
|
fi
|
||||||
printf "${green}Docker${plain} installed successfully\n"
|
success "Docker installed successfully"
|
||||||
installation_check
|
installation_check
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -334,12 +342,10 @@ install_dashboard_standalone() {
|
|||||||
|
|
||||||
selinux() {
|
selinux() {
|
||||||
#Check SELinux
|
#Check SELinux
|
||||||
command -v getenforce >/dev/null 2>&1
|
if command -v getenforce >/dev/null 2>&1; then
|
||||||
if [ $? -eq 0 ]; then
|
if getenforce | grep '[Ee]nfor'; then
|
||||||
getenforce | grep '[Ee]nfor'
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "SELinux running, closing now!"
|
echo "SELinux running, closing now!"
|
||||||
sudo setenforce 0 &>/dev/null
|
sudo setenforce 0 >/dev/null 2>&1
|
||||||
find_key="SELINUX="
|
find_key="SELINUX="
|
||||||
sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config
|
sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config
|
||||||
fi
|
fi
|
||||||
@@ -354,31 +360,37 @@ install_agent() {
|
|||||||
|
|
||||||
echo "Obtaining Agent version number"
|
echo "Obtaining Agent version number"
|
||||||
|
|
||||||
local version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
|
||||||
if [ ! -n "$version" ]; then
|
_version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
||||||
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
|
if [ -z "$_version" ]; then
|
||||||
|
_version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
|
||||||
fi
|
fi
|
||||||
if [ ! -n "$version" ]; then
|
if [ -z "$_version" ]; then
|
||||||
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
|
_version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
|
||||||
fi
|
fi
|
||||||
if [ ! -n "$version" ]; then
|
if [ -z "$_version" ]; then
|
||||||
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
|
_version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -n "$version" ]; then
|
if [ -z "$_version" ]; then
|
||||||
err "Fail to obtaine agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest"
|
err "Fail to obtain agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest"
|
||||||
return 1
|
return 1
|
||||||
else
|
else
|
||||||
echo "The current latest version is: ${version}"
|
echo "The current latest version is: ${_version}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Nezha Monitoring Folder
|
# Nezha Monitoring Folder
|
||||||
sudo mkdir -p $NZ_AGENT_PATH
|
sudo mkdir -p $NZ_AGENT_PATH
|
||||||
sudo chmod -R 700 $NZ_AGENT_PATH
|
|
||||||
|
|
||||||
echo "Downloading Agent"
|
echo "Downloading Agent"
|
||||||
wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip https://${GITHUB_URL}/nezhahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip >/dev/null 2>&1
|
if [ -z "$CN" ]; then
|
||||||
if [ $? != 0 ]; then
|
NZ_AGENT_URL="https://${GITHUB_URL}/nezhahq/agent/releases/download/${_version}/nezha-agent_linux_${os_arch}.zip"
|
||||||
|
else
|
||||||
|
NZ_AGENT_URL="https://${GITHUB_URL}/naibahq/agent/releases/download/${_version}/nezha-agent_linux_${os_arch}.zip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_cmd="wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip $NZ_AGENT_URL >/dev/null 2>&1"
|
||||||
|
if ! eval "$_cmd"; then
|
||||||
err "Fail to download agent, please check if the network can link ${GITHUB_URL}"
|
err "Fail to download agent, please check if the network can link ${GITHUB_URL}"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -402,10 +414,10 @@ modify_agent_config() {
|
|||||||
echo "> Modify Agent Configuration"
|
echo "> Modify Agent Configuration"
|
||||||
|
|
||||||
if [ $# -lt 3 ]; then
|
if [ $# -lt 3 ]; then
|
||||||
echo "Please add Agent in the admin panel first, record the secret"
|
echo "Please add Agent in the Dashboard first, record the secret"
|
||||||
printf "Please enter a domain that resolves to the IP where the panel is located (no CDN): "
|
printf "Please enter a domain that resolves to the IP where Dashboard is located (no CDN): "
|
||||||
read -r nz_grpc_host
|
read -r nz_grpc_host
|
||||||
printf "Please enter the panel RPC port (default 5555): "
|
printf "Please enter Dashboard RPC port (default 5555): "
|
||||||
read -r nz_grpc_port
|
read -r nz_grpc_port
|
||||||
printf "Please enter the Agent secret: "
|
printf "Please enter the Agent secret: "
|
||||||
read -r nz_client_secret
|
read -r nz_client_secret
|
||||||
@@ -430,14 +442,14 @@ modify_agent_config() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sudo ${NZ_AGENT_PATH}/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p $nz_client_secret $args >/dev/null 2>&1
|
_cmd="sudo ${NZ_AGENT_PATH}/nezha-agent service install -s $nz_grpc_host:$nz_grpc_port -p $nz_client_secret $args >/dev/null 2>&1"
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if ! eval "$_cmd"; then
|
||||||
sudo ${NZ_AGENT_PATH}/nezha-agent service uninstall >/dev/null 2>&1
|
sudo "${NZ_AGENT_PATH}"/nezha-agent service uninstall >/dev/null 2>&1
|
||||||
sudo ${NZ_AGENT_PATH}/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p $nz_client_secret $args >/dev/null 2>&1
|
sudo "${NZ_AGENT_PATH}"/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p "$nz_client_secret" "$args" >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf "Agent configuration ${green} modified successfully, please wait for agent self-restart to take effect${plain}\n"
|
success "Agent configuration modified successfully, please wait for agent self-restart to take effect"
|
||||||
|
|
||||||
#if [[ $# == 0 ]]; then
|
#if [[ $# == 0 ]]; then
|
||||||
# before_show_menu
|
# before_show_menu
|
||||||
@@ -448,16 +460,21 @@ modify_dashboard_config() {
|
|||||||
echo "> Modify Dashboard Configuration"
|
echo "> Modify Dashboard Configuration"
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
echo "Download Docker Script"
|
if [ -n "$DOCKER_COMPOSE_COMMAND" ]; then
|
||||||
wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1
|
echo "Download Docker Script"
|
||||||
if [ $? != 0 ]; then
|
_cmd="wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1"
|
||||||
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
|
if ! eval "$_cmd"; then
|
||||||
return 0
|
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
err "Please install docker-compose manually. https://docs.docker.com/compose/install/linux/"
|
||||||
|
before_show_menu
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1
|
_cmd="wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1"
|
||||||
if [ $? != 0 ]; then
|
if ! eval "$_cmd"; then
|
||||||
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
|
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -519,18 +536,22 @@ modify_dashboard_config() {
|
|||||||
if [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
||||||
echo "Downloading service file"
|
echo "Downloading service file"
|
||||||
if [ "$os_alpine" != 1 ]; then
|
if [ "$os_alpine" != 1 ]; then
|
||||||
sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1
|
_download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1"
|
||||||
else
|
if ! eval "$_download"; then
|
||||||
sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1
|
|
||||||
sudo chmod +x $NZ_DASHBOARD_SERVICERC
|
|
||||||
if [ $? != 0 ]; then
|
|
||||||
err "File failed to get, please check if the network can link ${GITHUB_RAW_URL}"
|
err "File failed to get, please check if the network can link ${GITHUB_RAW_URL}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
_download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1"
|
||||||
|
if ! eval "$_download"; then
|
||||||
|
err "File failed to get, please check if the network can link ${GITHUB_RAW_URL}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sudo chmod +x $NZ_DASHBOARD_SERVICERC
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf "Dashboard configuration ${green} modified successfully, please wait for Dashboard self-restart to take effect${plain}\n"
|
success "Dashboard configuration modified successfully, please wait for Dashboard self-restart to take effect"
|
||||||
|
|
||||||
restart_and_update
|
restart_and_update
|
||||||
|
|
||||||
@@ -540,17 +561,17 @@ modify_dashboard_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
restart_and_update() {
|
restart_and_update() {
|
||||||
echo "> Restart and Update the Panel"
|
echo "> Restart and Update Dashboard"
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
restart_and_update_docker
|
_cmd="restart_and_update_docker"
|
||||||
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
||||||
restart_and_update_standalone
|
_cmd="restart_and_update_standalone"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $? = 0 ]; then
|
if eval "$_cmd"; then
|
||||||
printf "${green}Nezha Monitoring Restart Successful${plain}\n"
|
success "Nezha Monitoring Restart Successful"
|
||||||
printf "Default panel address: ${yellow}domain:Site_access_port${plain}\n"
|
info "Default Dashboard address: domain:site_access_port"
|
||||||
else
|
else
|
||||||
err "The restart failed, probably because the boot time exceeded two seconds, please check the log information later"
|
err "The restart failed, probably because the boot time exceeded two seconds, please check the log information later"
|
||||||
fi
|
fi
|
||||||
@@ -567,22 +588,22 @@ restart_and_update_docker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
restart_and_update_standalone() {
|
restart_and_update_standalone() {
|
||||||
local version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
_version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
|
||||||
if [ ! -n "$version" ]; then
|
if [ -z "$_version" ]; then
|
||||||
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
|
_version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
|
||||||
fi
|
fi
|
||||||
if [ ! -n "$version" ]; then
|
if [ -z "$_version" ]; then
|
||||||
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
|
_version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
|
||||||
fi
|
fi
|
||||||
if [ ! -n "$version" ]; then
|
if [ -z "$_version" ]; then
|
||||||
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
|
_version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -n "$version" ]; then
|
if [ -z "$_version" ]; then
|
||||||
err "Fail to obtaine agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest"
|
err "Fail to obtain agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest"
|
||||||
return 1
|
return 1
|
||||||
else
|
else
|
||||||
echo "The current latest version is: ${version}"
|
echo "The current latest version is: ${_version}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$os_alpine" != 1 ]; then
|
if [ "$os_alpine" != 1 ]; then
|
||||||
@@ -593,12 +614,12 @@ restart_and_update_standalone() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$CN" ]; then
|
if [ -z "$CN" ]; then
|
||||||
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
|
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/${_version}/dashboard-linux-${os_arch}.zip"
|
||||||
else
|
else
|
||||||
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
|
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/${_version}/dashboard-linux-${os_arch}.zip"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_URL >/dev/null 2>&1 && sudo unzip -qq $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dist/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm -r $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_PATH/dist
|
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip "$NZ_DASHBOARD_URL" >/dev/null 2>&1 && sudo unzip -qq -o $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm $NZ_DASHBOARD_PATH/app.zip
|
||||||
|
|
||||||
if [ "$os_alpine" != 1 ]; then
|
if [ "$os_alpine" != 1 ]; then
|
||||||
sudo systemctl enable nezha-dashboard
|
sudo systemctl enable nezha-dashboard
|
||||||
@@ -610,16 +631,16 @@ restart_and_update_standalone() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start_dashboard() {
|
start_dashboard() {
|
||||||
echo "> Start Panel"
|
echo "> Start Dashboard"
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
start_dashboard_docker
|
_cmd="start_dashboard_docker"
|
||||||
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
||||||
start_dashboard_standalone
|
_cmd="start_dashboard_standalone"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $? = 0 ]; then
|
if eval "$_cmd"; then
|
||||||
printf "${green}Nezha Monitoring Start Successful${plain}\n"
|
success "Nezha Monitoring Start Successful"
|
||||||
else
|
else
|
||||||
err "Failed to start, please check the log message later"
|
err "Failed to start, please check the log message later"
|
||||||
fi
|
fi
|
||||||
@@ -642,16 +663,16 @@ start_dashboard_standalone() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop_dashboard() {
|
stop_dashboard() {
|
||||||
echo "> Stop Panel"
|
echo "> Stop Dashboard"
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
stop_dashboard_docker
|
_cmd="stop_dashboard_docker"
|
||||||
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
||||||
stop_dashboard_standalone
|
_cmd="stop_dashboard_standalone"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $? = 0 ]; then
|
if eval "$_cmd"; then
|
||||||
printf "${green}Nezha Monitoring Stop Successful${plain}\n"
|
success "Nezha Monitoring Stop Successful"
|
||||||
else
|
else
|
||||||
err "Failed to stop, please check the log message later"
|
err "Failed to stop, please check the log message later"
|
||||||
fi
|
fi
|
||||||
@@ -674,7 +695,7 @@ stop_dashboard_standalone() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show_dashboard_log() {
|
show_dashboard_log() {
|
||||||
echo "> View Panel Log"
|
echo "> View Dashboard Log"
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
show_dashboard_log_docker
|
show_dashboard_log_docker
|
||||||
@@ -700,7 +721,7 @@ show_dashboard_log_standalone() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uninstall_dashboard() {
|
uninstall_dashboard() {
|
||||||
echo "> Uninstall Panel"
|
echo "> Uninstall Dashboard"
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
uninstall_dashboard_docker
|
uninstall_dashboard_docker
|
||||||
@@ -787,13 +808,13 @@ show_usage() {
|
|||||||
echo "Nezha Monitor Management Script Usage: "
|
echo "Nezha Monitor Management Script Usage: "
|
||||||
echo "--------------------------------------------------------"
|
echo "--------------------------------------------------------"
|
||||||
echo "./nezha.sh - Show Menu"
|
echo "./nezha.sh - Show Menu"
|
||||||
echo "./nezha.sh install_dashboard - Install Panel"
|
echo "./nezha.sh install_dashboard - Install Dashboard"
|
||||||
echo "./nezha.sh modify_dashboard_config - Modify Panel Configuration"
|
echo "./nezha.sh modify_dashboard_config - Modify Dashboard Configuration"
|
||||||
echo "./nezha.sh start_dashboard - Start Panel"
|
echo "./nezha.sh start_dashboard - Start Dashboard"
|
||||||
echo "./nezha.sh stop_dashboard - Stop Panel"
|
echo "./nezha.sh stop_dashboard - Stop Dashboard"
|
||||||
echo "./nezha.sh restart_and_update - Restart and Update the Panel"
|
echo "./nezha.sh restart_and_update - Restart and Update the Dashboard"
|
||||||
echo "./nezha.sh show_dashboard_log - View Panel Log"
|
echo "./nezha.sh show_dashboard_log - View Dashboard Log"
|
||||||
echo "./nezha.sh uninstall_dashboard - Uninstall Panel"
|
echo "./nezha.sh uninstall_dashboard - Uninstall Dashboard"
|
||||||
echo "--------------------------------------------------------"
|
echo "--------------------------------------------------------"
|
||||||
echo "./nezha.sh install_agent - Install Agent"
|
echo "./nezha.sh install_agent - Install Agent"
|
||||||
echo "./nezha.sh modify_agent_config - Modify Agent Configuration"
|
echo "./nezha.sh modify_agent_config - Modify Agent Configuration"
|
||||||
@@ -808,13 +829,13 @@ show_menu() {
|
|||||||
printf "
|
printf "
|
||||||
${green}Nezha Monitor Management Script${plain} ${red}${NZ_VERSION}${plain}
|
${green}Nezha Monitor Management Script${plain} ${red}${NZ_VERSION}${plain}
|
||||||
--- https://github.com/naiba/nezha ---
|
--- https://github.com/naiba/nezha ---
|
||||||
${green}1.${plain} Install Panel
|
${green}1.${plain} Install Dashboard
|
||||||
${green}2.${plain} Modify Panel Configuration
|
${green}2.${plain} Modify Dashbaord Configuration
|
||||||
${green}3.${plain} Start Panel
|
${green}3.${plain} Start Dashboard
|
||||||
${green}4.${plain} Stop Panel
|
${green}4.${plain} Stop Dashboard
|
||||||
${green}5.${plain} Restart and Update the Panel
|
${green}5.${plain} Restart and Update Dashboard
|
||||||
${green}6.${plain} View Panel Log
|
${green}6.${plain} View Dashboard Log
|
||||||
${green}7.${plain} Uninstall Panel
|
${green}7.${plain} Uninstall Dashboard
|
||||||
————————————————-
|
————————————————-
|
||||||
${green}8.${plain} Install Agent
|
${green}8.${plain} Install Agent
|
||||||
${green}9.${plain} Modify Agent Configuration
|
${green}9.${plain} Modify Agent Configuration
|
||||||
@@ -930,4 +951,4 @@ if [ $# -gt 0 ]; then
|
|||||||
else
|
else
|
||||||
select_version
|
select_version
|
||||||
show_menu
|
show_menu
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package rpc
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -14,6 +15,18 @@ type ioStreamContext struct {
|
|||||||
agentIoConnectCh chan struct{}
|
agentIoConnectCh chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type bp struct {
|
||||||
|
buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var bufPool = sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
return &bp{
|
||||||
|
buf: make([]byte, 1024*1024),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func (s *NezhaHandler) CreateStream(streamId string) {
|
func (s *NezhaHandler) CreateStream(streamId string) {
|
||||||
s.ioStreamMutex.Lock()
|
s.ioStreamMutex.Lock()
|
||||||
defer s.ioStreamMutex.Unlock()
|
defer s.ioStreamMutex.Unlock()
|
||||||
@@ -117,7 +130,9 @@ LOOP:
|
|||||||
endCh := make(chan struct{})
|
endCh := make(chan struct{})
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
_, innerErr := io.Copy(stream.userIo, stream.agentIo)
|
bp := bufPool.Get().(*bp)
|
||||||
|
defer bufPool.Put(bp)
|
||||||
|
_, innerErr := io.CopyBuffer(stream.userIo, stream.agentIo, bp.buf)
|
||||||
if innerErr != nil {
|
if innerErr != nil {
|
||||||
err = innerErr
|
err = innerErr
|
||||||
}
|
}
|
||||||
@@ -126,7 +141,9 @@ LOOP:
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
_, innerErr := io.Copy(stream.agentIo, stream.userIo)
|
bp := bufPool.Get().(*bp)
|
||||||
|
defer bufPool.Put(bp)
|
||||||
|
_, innerErr := io.CopyBuffer(stream.agentIo, stream.userIo, bp.buf)
|
||||||
if innerErr != nil {
|
if innerErr != nil {
|
||||||
err = innerErr
|
err = innerErr
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-24
@@ -125,7 +125,6 @@ func (s *NezhaHandler) ReportSystemState(c context.Context, r *pb.State) (*pb.Re
|
|||||||
|
|
||||||
func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Receipt, error) {
|
func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Receipt, error) {
|
||||||
var clientID uint64
|
var clientID uint64
|
||||||
var provider ddns.Provider
|
|
||||||
var err error
|
var err error
|
||||||
if clientID, err = s.Auth.Check(c); err != nil {
|
if clientID, err = s.Auth.Check(c); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -135,33 +134,19 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
|
|||||||
defer singleton.ServerLock.RUnlock()
|
defer singleton.ServerLock.RUnlock()
|
||||||
|
|
||||||
// 检查并更新DDNS
|
// 检查并更新DDNS
|
||||||
if singleton.Conf.DDNS.Enable &&
|
if singleton.ServerList[clientID].EnableDDNS && host.IP != "" &&
|
||||||
singleton.ServerList[clientID].EnableDDNS &&
|
|
||||||
host.IP != "" &&
|
|
||||||
(singleton.ServerList[clientID].Host == nil || singleton.ServerList[clientID].Host.IP != host.IP) {
|
(singleton.ServerList[clientID].Host == nil || singleton.ServerList[clientID].Host.IP != host.IP) {
|
||||||
serverDomain := singleton.ServerList[clientID].DDNSDomain
|
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
|
||||||
if singleton.Conf.DDNS.Provider == "" {
|
providers, err := singleton.GetDDNSProvidersFromProfiles(singleton.ServerList[clientID].DDNSProfiles, &ddns.IP{Ipv4Addr: ipv4, Ipv6Addr: ipv6})
|
||||||
provider, err = singleton.GetDDNSProviderFromProfile(singleton.ServerList[clientID].DDNSProfile)
|
if err == nil {
|
||||||
} else {
|
for _, provider := range providers {
|
||||||
provider, err = singleton.GetDDNSProviderFromString(singleton.Conf.DDNS.Provider)
|
go func(provider *ddns.Provider) {
|
||||||
}
|
provider.UpdateDomain(context.Background())
|
||||||
if err == nil && serverDomain != "" {
|
}(provider)
|
||||||
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
|
|
||||||
maxRetries := int(singleton.Conf.DDNS.MaxRetries)
|
|
||||||
config := &ddns.DomainConfig{
|
|
||||||
EnableIPv4: singleton.ServerList[clientID].EnableIPv4,
|
|
||||||
EnableIpv6: singleton.ServerList[clientID].EnableIpv6,
|
|
||||||
FullDomain: serverDomain,
|
|
||||||
Ipv4Addr: ipv4,
|
|
||||||
Ipv6Addr: ipv6,
|
|
||||||
}
|
}
|
||||||
go singleton.RetryableUpdateDomain(provider, config, maxRetries)
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// 虽然会在启动时panic, 可以断言不会走这个分支, 但是考虑到动态加载配置或者其它情况, 这里输出一下方便检查奇奇怪怪的BUG
|
log.Printf("NEZHA>> 获取DDNS配置时发生错误: %v", err)
|
||||||
log.Printf("NEZHA>> 未找到对应的DDNS配置(%s), 或者是provider填写不正确, 请前往config.yml检查你的设置\n", singleton.ServerList[clientID].DDNSProfile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送IP变动通知
|
// 发送IP变动通知
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type CommonServerInfo struct {
|
|||||||
IPV6 string `json:"ipv6"`
|
IPV6 string `json:"ipv6"`
|
||||||
ValidIP string `json:"valid_ip"`
|
ValidIP string `json:"valid_ip"`
|
||||||
DisplayIndex int `json:"display_index"`
|
DisplayIndex int `json:"display_index"`
|
||||||
|
HideForGuest bool `json:"hide_for_guest"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusResponse 服务器状态子结构 包含服务器信息与状态信息
|
// StatusResponse 服务器状态子结构 包含服务器信息与状态信息
|
||||||
@@ -150,6 +151,7 @@ func (s *ServerAPIService) GetAllStatus() *ServerStatusResponse {
|
|||||||
IPV6: ipv6,
|
IPV6: ipv6,
|
||||||
ValidIP: validIP,
|
ValidIP: validIP,
|
||||||
DisplayIndex: v.DisplayIndex,
|
DisplayIndex: v.DisplayIndex,
|
||||||
|
HideForGuest: v.HideForGuest,
|
||||||
}
|
}
|
||||||
res.Result = append(res.Result, &StatusResponse{
|
res.Result = append(res.Result, &StatusResponse{
|
||||||
CommonServerInfo: info,
|
CommonServerInfo: info,
|
||||||
|
|||||||
+60
-73
@@ -2,87 +2,74 @@ package singleton
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/libdns/cloudflare"
|
||||||
|
tencentcloud "github.com/nezhahq/libdns-tencentcloud"
|
||||||
|
|
||||||
|
"github.com/naiba/nezha/model"
|
||||||
ddns2 "github.com/naiba/nezha/pkg/ddns"
|
ddns2 "github.com/naiba/nezha/pkg/ddns"
|
||||||
|
"github.com/naiba/nezha/pkg/ddns/dummy"
|
||||||
|
"github.com/naiba/nezha/pkg/ddns/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RetryableUpdateDomain(provider ddns2.Provider, config *ddns2.DomainConfig, maxRetries int) bool {
|
var (
|
||||||
if nil == config {
|
ddnsCache map[uint64]*model.DDNSProfile
|
||||||
return false
|
ddnsCacheLock sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func initDDNS() {
|
||||||
|
OnDDNSUpdate()
|
||||||
|
OnNameserverUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnDDNSUpdate() {
|
||||||
|
var ddns []*model.DDNSProfile
|
||||||
|
DB.Find(&ddns)
|
||||||
|
ddnsCacheLock.Lock()
|
||||||
|
defer ddnsCacheLock.Unlock()
|
||||||
|
ddnsCache = make(map[uint64]*model.DDNSProfile)
|
||||||
|
for i := 0; i < len(ddns); i++ {
|
||||||
|
ddnsCache[ddns[i].ID] = ddns[i]
|
||||||
}
|
}
|
||||||
for retries := 0; retries < maxRetries; retries++ {
|
}
|
||||||
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)\n", config.FullDomain, retries+1, maxRetries)
|
|
||||||
if provider.UpdateDomain(config) {
|
func OnNameserverUpdate() {
|
||||||
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功\n", config.FullDomain)
|
ddns2.InitDNSServers(Conf.DNSServers)
|
||||||
return true
|
}
|
||||||
|
|
||||||
|
func GetDDNSProvidersFromProfiles(profileId []uint64, ip *ddns2.IP) ([]*ddns2.Provider, error) {
|
||||||
|
profiles := make([]*model.DDNSProfile, 0, len(profileId))
|
||||||
|
ddnsCacheLock.RLock()
|
||||||
|
for _, id := range profileId {
|
||||||
|
if profile, ok := ddnsCache[id]; ok {
|
||||||
|
profiles = append(profiles, profile)
|
||||||
|
} else {
|
||||||
|
ddnsCacheLock.RUnlock()
|
||||||
|
return nil, fmt.Errorf("无法找到DDNS配置 ID %d", id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败\n", config.FullDomain)
|
ddnsCacheLock.RUnlock()
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) {
|
providers := make([]*ddns2.Provider, 0, len(profiles))
|
||||||
switch provider {
|
for _, profile := range profiles {
|
||||||
case "webhook":
|
provider := &ddns2.Provider{DDNSProfile: profile, IPAddrs: ip}
|
||||||
return &ddns2.ProviderWebHook{
|
switch profile.Provider {
|
||||||
URL: Conf.DDNS.WebhookURL,
|
case model.ProviderDummy:
|
||||||
RequestMethod: Conf.DDNS.WebhookMethod,
|
provider.Setter = &dummy.Provider{}
|
||||||
RequestBody: Conf.DDNS.WebhookRequestBody,
|
providers = append(providers, provider)
|
||||||
RequestHeader: Conf.DDNS.WebhookHeaders,
|
case model.ProviderWebHook:
|
||||||
}, nil
|
provider.Setter = &webhook.Provider{DDNSProfile: profile}
|
||||||
case "dummy":
|
providers = append(providers, provider)
|
||||||
return &ddns2.ProviderDummy{}, nil
|
case model.ProviderCloudflare:
|
||||||
case "cloudflare":
|
provider.Setter = &cloudflare.Provider{APIToken: profile.AccessSecret}
|
||||||
return &ddns2.ProviderCloudflare{
|
providers = append(providers, provider)
|
||||||
Secret: Conf.DDNS.AccessSecret,
|
case model.ProviderTencentCloud:
|
||||||
}, nil
|
provider.Setter = &tencentcloud.Provider{SecretId: profile.AccessID, SecretKey: profile.AccessSecret}
|
||||||
case "tencentcloud":
|
providers = append(providers, provider)
|
||||||
return &ddns2.ProviderTencentCloud{
|
default:
|
||||||
SecretID: Conf.DDNS.AccessID,
|
return nil, fmt.Errorf("无法找到配置的DDNS提供者ID %d", profile.Provider)
|
||||||
SecretKey: Conf.DDNS.AccessSecret,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return &ddns2.ProviderDummy{}, fmt.Errorf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDDNSProviderFromProfile(profileName string) (ddns2.Provider, error) {
|
|
||||||
profile, ok := Conf.DDNS.Profiles[profileName]
|
|
||||||
if !ok {
|
|
||||||
return &ddns2.ProviderDummy{}, fmt.Errorf("未找到配置项 %s", profileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch profile.Provider {
|
|
||||||
case "webhook":
|
|
||||||
return &ddns2.ProviderWebHook{
|
|
||||||
URL: profile.WebhookURL,
|
|
||||||
RequestMethod: profile.WebhookMethod,
|
|
||||||
RequestBody: profile.WebhookRequestBody,
|
|
||||||
RequestHeader: profile.WebhookHeaders,
|
|
||||||
}, nil
|
|
||||||
case "dummy":
|
|
||||||
return &ddns2.ProviderDummy{}, nil
|
|
||||||
case "cloudflare":
|
|
||||||
return &ddns2.ProviderCloudflare{
|
|
||||||
Secret: profile.AccessSecret,
|
|
||||||
}, nil
|
|
||||||
case "tencentcloud":
|
|
||||||
return &ddns2.ProviderTencentCloud{
|
|
||||||
SecretID: profile.AccessID,
|
|
||||||
SecretKey: profile.AccessSecret,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return &ddns2.ProviderDummy{}, fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateDDNSProvidersFromProfiles() error {
|
|
||||||
validProviders := map[string]bool{"webhook": true, "dummy": true, "cloudflare": true, "tencentcloud": true}
|
|
||||||
providers := make(map[string]string)
|
|
||||||
for profileName, profile := range Conf.DDNS.Profiles {
|
|
||||||
if _, ok := validProviders[profile.Provider]; !ok {
|
|
||||||
return fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
|
|
||||||
}
|
}
|
||||||
providers[profileName] = profile.Provider
|
|
||||||
}
|
}
|
||||||
return nil
|
return providers, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user