Browse Source

Merge branch 'dev' into feat/cli

Hintay 4 months ago
parent
commit
8a7ed08619
69 changed files with 1223 additions and 613 deletions
  1. 1 1
      .air.toml
  2. 49 0
      .devcontainer/Dockerfile
  3. 44 0
      .devcontainer/devcontainer.json
  4. 57 0
      .devcontainer/docker-compose.yml
  5. 6 0
      .devcontainer/init-nginx.sh
  6. 87 0
      .devcontainer/node-supervisor.sh
  7. 25 0
      .devcontainer/pebble-test/certs/README.md
  8. 5 0
      .devcontainer/pebble-test/certs/localhost/README.md
  9. 19 0
      .devcontainer/pebble-test/certs/localhost/cert.pem
  10. 27 0
      .devcontainer/pebble-test/certs/localhost/key.pem
  11. 27 0
      .devcontainer/pebble-test/certs/pebble.minica.key.pem
  12. 19 0
      .devcontainer/pebble-test/certs/pebble.minica.pem
  13. 26 0
      .devcontainer/pebble-test/config/load-generator-config.json
  14. 22 0
      .devcontainer/pebble-test/config/pebble-config-external-account-bindings.json
  15. 27 0
      .devcontainer/pebble-test/config/pebble-config.json
  16. 22 0
      .devcontainer/start.sh
  17. 3 0
      .dockerignore
  18. 3 1
      .gitignore
  19. 12 2
      .idea/nginx-ui.iml
  20. 48 0
      .vscode/tasks.json
  21. 3 3
      README-es.md
  22. 4 4
      README-vi_VN.md
  23. 3 3
      README-zh_CN.md
  24. 3 3
      README-zh_TW.md
  25. 8 4
      README.md
  26. 8 20
      api/cluster/environment.go
  27. 1 1
      api/system/router.go
  28. 1 1
      app/.env
  29. 2 0
      app/app.go
  30. 8 0
      app/app_unembed.go
  31. 5 5
      app/package.json
  32. 218 228
      app/pnpm-lock.yaml
  33. 0 1
      app/src/App.vue
  34. 1 1
      app/src/components/StdDesign/StdDataDisplay/StdCurd.vue
  35. 1 1
      app/src/version.json
  36. 2 1
      app/src/views/dashboard/ServerAnalytic.vue
  37. 14 1
      app/src/views/environment/Environment.vue
  38. 4 4
      app/src/views/environment/envColumns.tsx
  39. 2 0
      app/src/views/site/cert/Cert.vue
  40. 9 9
      app/src/views/site/ngx_conf/NgxServer.vue
  41. 17 0
      app/vite.config.ts
  42. 31 30
      docs/.vitepress/config/en.ts
  43. 9 3
      docs/.vitepress/config/shared.ts
  44. 31 30
      docs/.vitepress/config/zh_CN.ts
  45. 31 30
      docs/.vitepress/config/zh_TW.ts
  46. 5 1
      docs/guide/about.md
  47. 50 0
      docs/guide/devcontainer.md
  48. 4 4
      docs/guide/install-script-linux.md
  49. 3 3
      docs/package.json
  50. 13 13
      docs/pnpm-lock.yaml
  51. 5 1
      docs/zh_CN/guide/about.md
  52. 48 0
      docs/zh_CN/guide/devcontainer.md
  53. 4 4
      docs/zh_CN/guide/install-script-linux.md
  54. 7 3
      docs/zh_TW/guide/about.md
  55. 48 0
      docs/zh_TW/guide/devcontainer.md
  56. 4 4
      docs/zh_TW/guide/install-script-linux.md
  57. 2 2
      go.mod
  58. 15 3
      internal/cert/cert_info.go
  59. 7 6
      internal/cert/write_file.go
  60. 0 27
      internal/logger/color.go
  61. 0 103
      internal/logger/logger.go
  62. 29 0
      internal/middleware/embed.go
  63. 4 22
      internal/middleware/middleware.go
  64. 1 1
      model/environment.go
  65. 0 13
      resources/development/entrypoint.sh
  66. 0 9
      resources/development/sources.list
  67. 4 7
      router/routers.go
  68. 17 0
      router/routers_embed.go
  69. 8 0
      router/routers_unembed.go

+ 1 - 1
.air.toml

@@ -7,7 +7,7 @@ tmp_dir = "tmp"
 
 [build]
 # Just plain old shell command. You could use `make` as well.
-cmd = "CGO_ENABLED=1 go build -tags=jsoniter -ldflags=\"-X 'github.com/0xJacky/Nginx-UI/settings.buildTime=$(date +%s)'\" -v -o ./tmp/main ."
+cmd = "CGO_ENABLED=1 go build -tags=jsoniter,unembed -ldflags=\"-X 'github.com/0xJacky/Nginx-UI/settings.buildTime=$(date +%s)'\" -v -o ./tmp/main ."
 # Binary file yields from `cmd`.
 bin = "tmp/main"
 # Customize binary.

+ 49 - 0
.devcontainer/Dockerfile

@@ -0,0 +1,49 @@
+FROM mcr.microsoft.com/devcontainers/base:jammy
+
+# Combine installation steps for Nginx and Go to avoid repetitive update/cleanup commands
+RUN apt-get update && \
+    apt-get install -y --no-install-recommends curl gnupg2 ca-certificates lsb-release ubuntu-keyring jq && \
+    \
+    # Configure the Nginx repository
+    curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor > /usr/share/keyrings/nginx-archive-keyring.gpg && \
+    echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/mainline/ubuntu $(lsb_release -cs) nginx" \
+        > /etc/apt/sources.list.d/nginx.list && \
+    printf "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" \
+        > /etc/apt/preferences.d/99nginx && \
+    \
+    # Update package information and install Nginx
+    apt-get update && \
+    apt-get install -y --no-install-recommends nginx inotify-tools file && \
+    \
+    # Automatically retrieve the latest stable Go version and install it,
+    # download the appropriate binary based on system architecture (amd64 or arm64)
+    GO_VERSION=$(curl -sSL "https://golang.org/dl/?mode=json" | \
+        jq -r 'map(select(.stable)) | .[0].version' | sed 's/^go//') && \
+    ARCH=$(dpkg --print-architecture) && \
+    if [ "$ARCH" = "arm64" ]; then \
+      GO_ARCH=linux-arm64; \
+    else \
+      GO_ARCH=linux-amd64; \
+    fi && \
+    echo "Installing Go version: ${GO_VERSION} for architecture: ${GO_ARCH}" && \
+    curl -sSL "https://golang.org/dl/go${GO_VERSION}.${GO_ARCH}.tar.gz" -o go.tar.gz && \
+    rm -rf /usr/local/go && \
+    tar -C /usr/local -xzf go.tar.gz && \
+    rm go.tar.gz && \
+    \
+    # Remove jq and clean up to reduce image size
+    apt-get remove -y jq && \
+    apt-get autoremove -y && \
+    apt-get clean && \
+    rm -rf /var/lib/apt/lists/*
+
+RUN cp -rp /etc/nginx /etc/nginx.orig
+
+# Set PATH to include Go installation and default go install binary location
+ENV PATH="/usr/local/go/bin:/root/go/bin:${PATH}"
+
+# Install air with go install (requires Go 1.23 or higher)
+RUN go install github.com/air-verse/air@latest
+
+# set zsh as default shell
+RUN chsh -s $(which zsh)

+ 44 - 0
.devcontainer/devcontainer.json

@@ -0,0 +1,44 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu
+{
+  "name": "Ubuntu",
+  "dockerComposeFile": "docker-compose.yml",
+  "service": "nginx-ui",
+  "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
+  "shutdownAction": "stopCompose",
+  // Features to add to the dev container. More info: https://containers.dev/features.
+  "features": {
+    "ghcr.io/devcontainers/features/common-utils:2": {
+      "installOhMyZsh": true
+    },
+    "ghcr.io/devcontainers/features/node:1.6.1": {}
+  },
+
+  // Use 'forwardPorts' to make a list of ports inside the container available locally.
+  // "forwardPorts": [],
+
+  // Use 'postCreateCommand' to run commands after the container is created.
+  // "postCreateCommand": "",
+
+  // Configure tool-specific properties.
+  "customizations": {
+    "vscode": {
+      "extensions": [
+        "antfu.iconify",
+        "antfu.unocss",
+        "github.copilot",
+        "golang.go",
+        "vue.volar",
+        "ms-azuretools.vscode-docker"
+      ]
+    }
+  },
+
+  // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
+  "remoteUser": "root",
+  "overrideCommand": false,
+  "postStartCommand": "./.devcontainer/start.sh",
+  "mounts": [
+    "source=${localEnv:HOME}/.ssh,target=/root/.ssh,type=bind,consistency=cached"
+  ]
+}

+ 57 - 0
.devcontainer/docker-compose.yml

@@ -0,0 +1,57 @@
+services:
+  nginx-ui:
+    build: .
+    image: nginx-ui-dev
+    container_name: nginx-ui
+    volumes:
+      - ../..:/workspaces:cached
+      - ./go-path:/root/go
+      - ./data/nginx:/etc/nginx
+    command: sleep infinity
+    environment:
+      - NGINX_UI_CERT_CA_DIR=https://pebble:14000/dir
+    networks:
+      nginxui:
+  nginx-ui-2:
+    image: nginx-ui-dev
+    container_name: nginx-ui-2
+    volumes:
+      - ../..:/workspaces:cached
+      - ./data/nginx-ui-2/nginx:/etc/nginx
+      - ./data/nginx-ui-2/nginx-ui:/etc/nginx-ui
+    working_dir: /workspaces/nginx-ui
+    command: ./.devcontainer/node-supervisor.sh
+    depends_on:
+      - nginx-ui
+    networks:
+      nginxui:
+
+  pebble:
+    image: ghcr.io/letsencrypt/pebble:latest
+    volumes:
+      - ./pebble-test:/test
+    command: -config /test/config/pebble-config.json -strict -dnsserver challtestsrv:8053
+    ports:
+        - 14000:14000 # HTTPS ACME API
+        - 15000:15000 # HTTPS Management API
+    environment:
+      - PEBBLE_VA_NOSLEEP=1
+      - PEBBLE_VA_ALWAYS_VALID=1
+    networks:
+      nginxui:
+  challtestsrv:
+    image: ghcr.io/letsencrypt/pebble-challtestsrv:latest
+    command: -defaultIPv6 "" -defaultIPv4 challtestsrv
+    ports:
+      - 8055:8055 # HTTP Management API
+    networks:
+      nginxui:
+  casdoor:
+    image: casbin/casdoor-all-in-one
+    ports:
+      - 8001:8000
+    networks:
+      - nginxui
+
+networks:
+  nginxui:

+ 6 - 0
.devcontainer/init-nginx.sh

@@ -0,0 +1,6 @@
+# init nginx config dir
+if [ "$(ls -A /etc/nginx)" = "" ]; then
+    echo "Initialing Nginx config dir"
+    cp -rp /etc/nginx.orig/* /etc/nginx/
+    echo "Initialed Nginx config dir"
+fi

+ 87 - 0
.devcontainer/node-supervisor.sh

@@ -0,0 +1,87 @@
+#!/bin/bash
+
+# Configurable variables
+SOURCE_FILE=/workspaces/nginx-ui/tmp/main
+TARGET_PATH=/usr/local/bin/nginx-ui
+CONFIG_FILE=/etc/nginx-ui/app.ini
+
+# init nginx
+./.devcontainer/init-nginx.sh
+
+LOG_PREFIX="[Supervisor]"
+
+# Debug initial state
+echo "$LOG_PREFIX Starting supervisor with:"
+echo "$LOG_PREFIX SOURCE_FILE: $SOURCE_FILE"
+echo "$LOG_PREFIX TARGET_PATH: $TARGET_PATH"
+echo "$LOG_PREFIX CONFIG_FILE: $CONFIG_FILE"
+
+# Wait for initial file creation
+while [[ ! -f "$SOURCE_FILE" ]]; do
+    echo "$LOG_PREFIX Waiting for $SOURCE_FILE to be created..."
+    sleep 1
+done
+
+# Initial copy and start
+echo "$LOG_PREFIX Initial file detected, starting service..."
+cp -fv "$SOURCE_FILE" "$TARGET_PATH"
+chmod +x "$TARGET_PATH"
+pkill -x nginx-ui || echo "$LOG_PREFIX No existing process to kill"
+nohup "$TARGET_PATH" -config "$CONFIG_FILE" > /proc/1/fd/1 2>&1 &
+
+# Use proper field separation for inotify output
+inotifywait -m -e close_write,moved_to,create,delete \
+    --format "%T|%w%f|%e" \
+    --timefmt "%F-%H:%M:%S" \
+    "$(dirname "$SOURCE_FILE")" |
+while IFS='|' read -r TIME FILE EVENT; do
+    echo "$LOG_PREFIX [${TIME}] Event: ${EVENT} - ${FILE}"
+    
+    # Handle atomic save operations
+    if [[ "$FILE" =~ .*-tmp-umask$ ]] || [[ "$EVENT" == "DELETE" ]]; then
+        echo "$LOG_PREFIX Detected build intermediate file, checking main..."
+        sleep 0.3  # Allow atomic replace completion
+        
+        if [[ -f "$SOURCE_FILE" ]]; then
+            echo "$LOG_PREFIX Valid main file detected after build"
+            FILE="$SOURCE_FILE"
+        else
+            echo "$LOG_PREFIX Main file missing after build operation"
+            continue
+        fi
+    fi
+
+    if [[ "$FILE" == "$SOURCE_FILE" ]]; then
+        # Stability checks
+        echo "$LOG_PREFIX File metadata:"
+        ls -l "$FILE"
+        file "$FILE"
+        
+        # Wait for file stability with retries
+        retries=5
+        while ((retries-- > 0)); do
+            if file "$FILE" | grep -q "executable"; then
+                break
+            fi
+            echo "$LOG_PREFIX Waiting for valid executable (${retries} retries left)..."
+            sleep 1
+        done
+
+        if ((retries <= 0)); then
+            echo "$LOG_PREFIX ERROR: File validation failed after 5 retries"
+            continue
+        fi
+
+        # Copy and restart service
+        echo "$LOG_PREFIX Updating service..."
+        cp -fv "$FILE" "$TARGET_PATH"
+        chmod +x "$TARGET_PATH"
+        
+        echo "$LOG_PREFIX Killing existing process..."
+        pkill -x nginx-ui || echo "$LOG_PREFIX No process to kill"
+        
+        echo "$LOG_PREFIX Starting new process..."
+        nohup "$TARGET_PATH" -config "$CONFIG_FILE" > /proc/1/fd/1 2>&1 &
+        echo "$LOG_PREFIX Restart complete. New PID: $(pgrep nginx-ui)"
+    fi
+done

+ 25 - 0
.devcontainer/pebble-test/certs/README.md

@@ -0,0 +1,25 @@
+# certs/
+
+This directory contains a CA certificate (`pebble.minica.pem`) and a private key
+(`pebble.minica.key.pem`) that are used to issue a end-entity certificate (See
+`certs/localhost`)  for the Pebble HTTPS server.
+
+To get your **testing code** to use Pebble without HTTPS errors you should
+configure your ACME client to trust the `pebble.minica.pem` CA certificate. Your
+ACME client should offer a runtime option to specify a list of root CAs that you
+can configure to include the `pebble.minica.pem` file.
+
+**Do not** add this CA certificate to the system trust store or in production
+code!!! The CA's private key is **public** and anyone can use it to issue
+certificates that will be trusted by a system with the Pebble CA in the trust
+store.
+
+To re-create all of the Pebble certificates run:
+
+    minica -ca-cert pebble.minica.pem \
+           -ca-key pebble.minica.key.pem \
+           -domains localhost,pebble \
+           -ip-addresses 127.0.0.1
+
+From the `test/certs/` directory after [installing
+MiniCA](https://github.com/jsha/minica#installation)

+ 5 - 0
.devcontainer/pebble-test/certs/localhost/README.md

@@ -0,0 +1,5 @@
+# certs/localhost
+
+This directory contains an end-entity (leaf) certificate (`cert.pem`) and
+a private key (`key.pem`) for the Pebble HTTPS server. It includes `127.0.0.1`
+as an IP address SAN, and `[localhost, pebble]` as DNS SANs.

+ 19 - 0
.devcontainer/pebble-test/certs/localhost/cert.pem

@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDGzCCAgOgAwIBAgIIbEfayDFsBtwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
+AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMDcx
+MjA2MTk0MjEwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQCbFMW3DXXdErvQf2lCZ0qz0DGEWadDoF0O2neM5mVa
+VQ7QGW0xc5Qwvn3Tl62C0JtwLpF0pG2BICIN+DHdVaIUwkf77iBS2doH1I3waE1I
+8GkV9JrYmFY+j0dA1SwBmqUZNXhLNwZGq1a91nFSI59DZNy/JciqxoPX2K++ojU2
+FPpuXe2t51NmXMsszpa+TDqF/IeskA9A/ws6UIh4Mzhghx7oay2/qqj2IIPjAmJj
+i73kdUvtEry3wmlkBvtVH50+FscS9WmPC5h3lDTk5nbzSAXKuFusotuqy3XTgY5B
+PiRAwkZbEY43JNfqenQPHo7mNTt29i+NVVrBsnAa5ovrAgMBAAGjYzBhMA4GA1Ud
+DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T
+AQH/BAIwADAiBgNVHREEGzAZgglsb2NhbGhvc3SCBnBlYmJsZYcEfwAAATANBgkq
+hkiG9w0BAQsFAAOCAQEAYIkXff8H28KS0KyLHtbbSOGU4sujHHVwiVXSATACsNAE
+D0Qa8hdtTQ6AUqA6/n8/u1tk0O4rPE/cTpsM3IJFX9S3rZMRsguBP7BSr1Lq/XAB
+7JP/CNHt+Z9aKCKcg11wIX9/B9F7pyKM3TdKgOpqXGV6TMuLjg5PlYWI/07lVGFW
+/mSJDRs8bSCFmbRtEqc4lpwlrpz+kTTnX6G7JDLfLWYw/xXVqwFfdengcDTHCc8K
+wtgGq/Gu6vcoBxIO3jaca+OIkMfxxXmGrcNdseuUCa3RMZ8Qy03DqGu6Y6XQyK4B
+W8zIG6H9SVKkAznM2yfYhW8v2ktcaZ95/OBHY97ZIw==
+-----END CERTIFICATE-----

+ 27 - 0
.devcontainer/pebble-test/certs/localhost/key.pem

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAmxTFtw113RK70H9pQmdKs9AxhFmnQ6BdDtp3jOZlWlUO0Blt
+MXOUML5905etgtCbcC6RdKRtgSAiDfgx3VWiFMJH++4gUtnaB9SN8GhNSPBpFfSa
+2JhWPo9HQNUsAZqlGTV4SzcGRqtWvdZxUiOfQ2TcvyXIqsaD19ivvqI1NhT6bl3t
+redTZlzLLM6Wvkw6hfyHrJAPQP8LOlCIeDM4YIce6Gstv6qo9iCD4wJiY4u95HVL
+7RK8t8JpZAb7VR+dPhbHEvVpjwuYd5Q05OZ280gFyrhbrKLbqst104GOQT4kQMJG
+WxGONyTX6np0Dx6O5jU7dvYvjVVawbJwGuaL6wIDAQABAoIBAGW9W/S6lO+DIcoo
+PHL+9sg+tq2gb5ZzN3nOI45BfI6lrMEjXTqLG9ZasovFP2TJ3J/dPTnrwZdr8Et/
+357YViwORVFnKLeSCnMGpFPq6YEHj7mCrq+YSURjlRhYgbVPsi52oMOfhrOIJrEG
+ZXPAwPRi0Ftqu1omQEqz8qA7JHOkjB2p0i2Xc/uOSJccCmUDMlksRYz8zFe8wHuD
+XvUL2k23n2pBZ6wiez6Xjr0wUQ4ESI02x7PmYgA3aqF2Q6ECDwHhjVeQmAuypMF6
+IaTjIJkWdZCW96pPaK1t+5nTNZ+Mg7tpJ/PRE4BkJvqcfHEOOl6wAE8gSk5uVApY
+ZRKGmGkCgYEAzF9iRXYo7A/UphL11bR0gqxB6qnQl54iLhqS/E6CVNcmwJ2d9pF8
+5HTfSo1/lOXT3hGV8gizN2S5RmWBrc9HBZ+dNrVo7FYeeBiHu+opbX1X/C1HC0m1
+wJNsyoXeqD1OFc1WbDpHz5iv4IOXzYdOdKiYEcTv5JkqE7jomqBLQk8CgYEAwkG/
+rnwr4ThUo/DG5oH+l0LVnHkrJY+BUSI33g3eQ3eM0MSbfJXGT7snh5puJW0oXP7Z
+Gw88nK3Vnz2nTPesiwtO2OkUVgrIgWryIvKHaqrYnapZHuM+io30jbZOVaVTMR9c
+X/7/d5/evwXuP7p2DIdZKQKKFgROm1XnhNqVgaUCgYBD/ogHbCR5RVsOVciMbRlG
+UGEt3YmUp/vfMuAsKUKbT2mJM+dWHVlb+LZBa4pC06QFgfxNJi/aAhzSGvtmBEww
+xsXbaceauZwxgJfIIUPfNZCMSdQVIVTi2Smcx6UofBz6i/Jw14MEwlvhamaa7qVf
+kqflYYwelga1wRNCPopLaQKBgQCWsZqZKQqBNMm0Q9yIhN+TR+2d7QFjqeePoRPl
+1qxNejhq25ojE607vNv1ff9kWUGuoqSZMUC76r6FQba/JoNbefI4otd7x/GzM9uS
+8MHMJazU4okwROkHYwgLxxkNp6rZuJJYheB4VDTfyyH/ng5lubmY7rdgTQcNyZ5I
+majRYQKBgAMKJ3RlII0qvAfNFZr4Y2bNIq+60Z+Qu2W5xokIHCFNly3W1XDDKGFe
+CCPHSvQljinke3P9gPt2HVdXxcnku9VkTti+JygxuLkVg7E0/SWwrWfGsaMJs+84
+fK+mTZay2d3v24r9WKEKwLykngYPyZw5+BdWU0E+xx5lGUd3U4gG
+-----END RSA PRIVATE KEY-----

+ 27 - 0
.devcontainer/pebble-test/certs/pebble.minica.key.pem

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAuVoGTaFSWp3Y+N5JC8lOdL8wmWpaM73UaNzhYiqA7ZqijzVk
+TTtoQvQFDcUwyXKOdWHONrv1ld3z224Us504jjlbZwI5uoquCOZ2WJbRhmXrRgzk
+Fq+/MtoFmPkhtO/DLjjtocgyIirVXN8Yl2APvB5brvRfCm6kktYeecsWfW/O3ikf
+gdM7tmocwQiBypiloHOjdd5e2g8cWNw+rqvILSUVNLaLpsi23cxnLqVb424wz9dZ
+5dO0REg1gSxtf4N5LSb6iGuAVoFNhzIeKzQ+svDg9x8tx/DGOghJS/jDgmxSY1qo
+bTsXhcmWVfat5GJ5PQgLkCSjBBrjeBlOrc4VtQIDAQABAoIBAQCAoRoou6C0ZEDU
+DScyN8TrvlcS0LzClaWYFFmRT5/jxOG1cr8l3elwNXpgYQ2Hb6mvim2ajHxVQg/e
+oxlYwO4jvWhSJzg63c0DPjS5LAlCNO6+0Wlk2RheSPGDhLlAoPeZ10YKdS1dis5B
+Qk4Fl1O0IHlOBCcEzV4GzPOfYDI+X6/f4xY7qz1s+CgoIxjIeiG+1/WpZQpYhobY
+7CfSDdYDKtksXi7iQkc5earUAHBqZ1gQTq6e5LVm9AjRzENhMctFgcPs5zOjp2ak
+PluixrA8LTAfu9wQzvxDkPl0UarZVxCerw6nlAziILpQ+U6PtoPZj49VpntTc+cq
+1qjzkbhBAoGBANElJmFWY2X6LgBpszeqt0ZOSbkFg2bC0wHCJrMlRzUMEn83w9e8
+Z2Fqml9eCC5qxJcyxWDVQeoAX6090m0qgP8xNmGdafcVic2cUlrqtkqhhst2OHCO
+MCQEB7cdsjiidNNrOgLbQ3i1bYID8BVLf/TDhEbRgvTewDaz6XPdoSIRAoGBAOLg
+RuOec5gn50SrVycx8BLFO8AXjXojpZb1Xg26V5miz1IavSfDcgae/699ppSz+UWi
+jGMFr/PokY2JxDVs3PyQLu7ahMzyFHr16Agvp5g5kq056XV+uI/HhqLHOWSQ09DS
+1Vrj7FOYpKRzge3/AC7ty9Vr35uMiebpm4/CLFVlAoGALnsIJZfSbWaFdLgJCXUa
+WDir77/G7T6dMIXanfPJ+IMfVUCqeLa5bxAHEOzP+qjl2giBjzy18nB00warTnGk
+y5I/WMBoPW5++sAkGWqSatGtKGi0sGcZUdfHcy3ZXvbT6eyprtrWCuyfUsbXQ5RM
+8rPFIQwNA6jBpSak2ohF+FECgYEAn+6IKncNd6pRfnfmdSvf1+uPxkcUJZCxb2xC
+xByjGhvKWE+fHkPJwt8c0SIbZuJEC5Gds0RUF/XPfV4roZm/Yo9ldl02lp7kTxXA
+XtzxIP8c5d5YM8qD4l8+Csu0Kq9pkeC+JFddxkRpc8A1TIehInPhZ+6mb6mvoMb3
+MW0pAX0CgYATT74RYuIYWZvx0TK4ZXIKTw2i6HObLF63Y6UwyPXXdEVie/ToYRNH
+JIxE1weVpHvnHZvVD6D3yGk39ZsCIt31VvKpatWXlWBm875MbBc6kuIGsYT+mSSj
+y9TXaE89E5zfL27nZe15QLJ+Xw8Io6PMLZ/jtC5TYoEixSZ9J8v6HA==
+-----END RSA PRIVATE KEY-----

+ 19 - 0
.devcontainer/pebble-test/certs/pebble.minica.pem

@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDCTCCAfGgAwIBAgIIJOLbes8sTr4wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
+AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMTcx
+MjA2MTk0MjEwWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAyNGUyZGIwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ
+alozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn
+Ajm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu
+9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0
+toumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3
+Hy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB
+AAGjRTBDMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
+BQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAF85v
+d40HK1ouDAtWeO1PbnWfGEmC5Xa478s9ddOd9Clvp2McYzNlAFfM7kdcj6xeiNhF
+WPIfaGAi/QdURSL/6C1KsVDqlFBlTs9zYfh2g0UXGvJtj1maeih7zxFLvet+fqll
+xseM4P9EVJaQxwuK/F78YBt0tCNfivC6JNZMgxKF59h0FBpH70ytUSHXdz7FKwix
+Mfn3qEb9BXSk0Q3prNV5sOV3vgjEtB4THfDxSz9z3+DepVnW3vbbqwEbkXdk3j82
+2muVldgOUgTwK8eT+XdofVdntzU/kzygSAtAQwLJfn51fS1GvEcYGBc1bDryIqmF
+p9BI7gVKtWSZYegicA==
+-----END CERTIFICATE-----

+ 26 - 0
.devcontainer/pebble-test/config/load-generator-config.json

@@ -0,0 +1,26 @@
+{
+    "plan": {
+        "actions": [
+            "newAccount",
+            "newOrder",
+            "fulfillOrder",
+            "finalizeOrder"
+        ],
+        "rate": 10,
+        "runtime": "10s",
+        "rateDelta": "1/10s"
+    },
+    "directoryURL": "https://localhost:14000/dir",
+    "domainBase": "com",
+    "challengeStrategy": "random",
+    "httpOneAddrs": [":5002"],
+    "tlsAlpnOneAddrs": [":5001"],
+    "dnsAddrs": [":8053"],
+    "fakeDNS": "127.0.0.1",
+    "regKeySize": 2048,
+    "certKeySize": 2048,
+    "regEmail": "loadtesting@letsencrypt.org",
+    "maxRegs": 20,
+    "maxNamesPerCert": 20,
+    "dontSaveState": true
+}

+ 22 - 0
.devcontainer/pebble-test/config/pebble-config-external-account-bindings.json

@@ -0,0 +1,22 @@
+{
+  "pebble": {
+    "listenAddress": "0.0.0.0:14000",
+    "managementListenAddress": "0.0.0.0:15000",
+    "certificate": "/test/certs/localhost/cert.pem",
+    "privateKey": "/test/certs/localhost/key.pem",
+    "httpPort": 5002,
+    "tlsPort": 5001,
+    "ocspResponderURL": "",
+    "retryAfter": {
+        "authz": 3,
+        "order": 5
+    },
+    "externalAccountBindingRequired": true,
+    "externalAccountMACKeys": {
+      "kid-1": "zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W",
+      "kid-2": "b10lLJs8l1GPIzsLP0s6pMt8O0XVGnfTaCeROxQM0BIt2XrJMDHJZBM5NuQmQJQH",
+      "kid-3": "HjudV5qnbreN-n9WyFSH-t4HXuEx_XFen45zuxY-G1h6fr74V3cUM_dVlwQZBWmc"
+    },
+    "certificateValidityPeriod": 157766400
+  }
+}

+ 27 - 0
.devcontainer/pebble-test/config/pebble-config.json

@@ -0,0 +1,27 @@
+{
+  "pebble": {
+    "listenAddress": "0.0.0.0:14000",
+    "managementListenAddress": "0.0.0.0:15000",
+    "certificate": "/test/certs/localhost/cert.pem",
+    "privateKey": "/test/certs/localhost/key.pem",
+    "httpPort": 5002,
+    "tlsPort": 5001,
+    "ocspResponderURL": "",
+    "externalAccountBindingRequired": false,
+    "domainBlocklist": ["blocked-domain.example"],
+    "retryAfter": {
+        "authz": 3,
+        "order": 5
+    },
+    "profiles": {
+      "default": {
+        "description": "The profile you know and love",
+        "validityPeriod": 7776000
+      },
+      "shortlived": {
+        "description": "A short-lived cert profile, without actual enforcement",
+        "validityPeriod": 518400
+      }
+    }
+  }
+}

+ 22 - 0
.devcontainer/start.sh

@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# install zsh-autosuggestions
+git clone https://github.com/zsh-users/zsh-autosuggestions ~/.oh-my-zsh/custom/plugins/zsh-autosuggestions
+
+if ! grep -q "zsh-autosuggestions" ~/.zshrc; then
+    # add zsh-autosuggestions to plugins list
+    sed -i "/^plugins=(/s/)/ zsh-autosuggestions)/" ~/.zshrc
+fi
+
+# init nginx config dir
+./.devcontainer/init-nginx.sh
+
+# install app dependencies
+echo "Installing app dependencies"
+cd app && pnpm install -f
+cd ..
+
+# install docs dependencies
+echo "Installing docs dependencies"
+cd docs && pnpm install -f
+cd ..

+ 3 - 0
.dockerignore

@@ -2,3 +2,6 @@
 app/node_modules
 .idea
 tmp
+
+.devcontainer
+.vscode

+ 3 - 1
.gitignore

@@ -12,7 +12,9 @@ nginx-ui
 resources/development/nginx
 app/.env
 app/.status_hash
-casdoor.pub
 .idea/deployment.xml
 .idea/webServers.xml
 *.gen.go
+.devcontainer/go-path
+.devcontainer/data
+.devcontainer/casdoor.pem

+ 12 - 2
.idea/nginx-ui.iml

@@ -1,8 +1,18 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <module type="WEB_MODULE" version="4">
-  <component name="Go" enabled="true" />
+  <component name="Go" enabled="true">
+    <buildTags>
+      <option name="customFlags">
+        <array>
+          <option value="unembed" />
+        </array>
+      </option>
+    </buildTags>
+  </component>
   <component name="NewModuleRootManager">
-    <content url="file://$MODULE_DIR$" />
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.devcontainer/go-path" />
+    </content>
     <orderEntry type="inheritedJdk" />
     <orderEntry type="sourceFolder" forTests="false" />
   </component>

+ 48 - 0
.vscode/tasks.json

@@ -0,0 +1,48 @@
+{
+  "version": "2.0.0",
+  "tasks": [
+    {
+      "label": "Start Backend",
+      "type": "shell",
+      "command": "air",
+      "isBackground": true,
+      "presentation": {
+        "panel": "new"
+      },
+      "problemMatcher": []
+    },
+    {
+      "label": "Start Frontend",
+      "type": "shell",
+      "command": "cd app && pnpm dev",
+      "isBackground": true,
+      "presentation": {
+        "panel": "new"
+      }
+    },
+    {
+      "label": "Start Documentation",
+      "type": "shell",
+      "command": "cd docs && pnpm docs:dev",
+      "isBackground": true,
+      "presentation": {
+        "panel": "new"
+      },
+      "problemMatcher": []
+    },
+    {
+      "label": "Start All Services",
+      "dependsOrder": "parallel",
+      "dependsOn": [
+        "Start Backend",
+        "Start Frontend",
+        "Start Documentation"
+      ],
+      "group": {
+        "kind": "build",
+        "isDefault": true
+      },
+      "problemMatcher": []
+    }
+  ]
+}

+ 3 - 3
README-es.md

@@ -246,7 +246,7 @@ go build -tags=jsoniter -ldflags "$LD_FLAGS -X 'github.com/0xJacky/Nginx-UI/sett
 **Instalar and Actualizar**
 
 ```shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) install
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install
 ```
 El puerto de escucha predeterminado es `9000` y el puerto de Desafío HTTP predeterminado es `9180`.
 Si hay un conflicto de puertos, modifique manualmente `/usr/local/etc/nginx-ui/app.ini`,
@@ -255,13 +255,13 @@ luego use `systemctl restart nginx-ui` para recargar el servicio de UI de Nginx.
 **Eliminar UI Nginx UI, excepto los archivos de configuración y la base de datos**
 
 ```shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) remove
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ remove
 ```
 
 ### Uso avanzado
 
 ````shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) help
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ help
 ````
 
 ## Ejemplo de configuración de proxy reverso de Nginx

+ 4 - 4
README-vi_VN.md

@@ -300,7 +300,7 @@ go build -tags=jsoniter -ldflags "$LD_FLAGS -X 'github.com/0xJacky/Nginx-UI/sett
 **Cài đặt và nâng cấp**
 
 ```shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) install
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install
 ```
 Port mặc định để truy cập UI là `9000`, port HTTP Challenge mặc định để xác thực SSL là `9180`.
 Nếu có xung đột port, vui lòng sửa đổi trong file `/usr/local/etc/nginx-ui/app.ini`,
@@ -309,19 +309,19 @@ hãy nhớ restart nginx-ui bằng lệnh `systemctl restart nginx-ui` mỗi khi
 **Gỡ bỏ Nginx UI nhưng giữ lại các tệp cấu hình và cơ sở dữ liệu**
 
 ```shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) remove
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ remove
 ```
 
 **Gỡ bỏ Nginx UI đồng thời xoá các tệp cấu hình, cơ sở dữ liệu**
 
 ```shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) remove --purge
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ remove --purge
 ```
 
 ### Trợ giúp
 
 ````shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) help
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ help
 ````
 
 ## Ví dụ về cấu hình Nginx Reverse Proxy

+ 3 - 3
README-zh_CN.md

@@ -240,20 +240,20 @@ go build -tags=jsoniter -ldflags "$LD_FLAGS -X 'github.com/0xJacky/Nginx-UI/sett
 **安装或升级**
 
 ```shell
-bash <(curl -L -s https://mirror.ghproxy.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) install -r https://mirror.ghproxy.com/
+bash -c "$(curl -L https://mirror.ghproxy.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install -r https://mirror.ghproxy.com/
 ```
 一键安装脚本默认设置的监听端口为 `9000`,HTTP Challenge 端口默认为 `9180`,如果出现端口冲突请进入 `/usr/local/etc/nginx-ui/app.ini` 修改,并使用 `systemctl restart nginx-ui` 重启 Nginx UI 服务。
 
 **卸载 Nginx UI 但保留配置和数据库文件**
 
 ```shell
-bash <(curl -L -s https://mirror.ghproxy.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) remove
+bash -c "$(curl -L https://mirror.ghproxy.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ remove
 ```
 
 ### 更多用法
 
 ````shell
-bash <(curl -L -s https://mirror.ghproxy.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) help
+bash -c "$(curl -L https://mirror.ghproxy.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ help
 ````
 
 ## Nginx 反向代理配置示例

+ 3 - 3
README-zh_TW.md

@@ -245,7 +245,7 @@ go build -tags=jsoniter -ldflags "$LD_FLAGS -X 'github.com/0xJacky/Nginx-UI/sett
 **安裝或升級**
 
 ```shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) install
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install
 ```
 
 一鍵安裝指令預設的監聽連接埠為 `9000`,HTTP Challenge 埠預設為 `9180`,如果出現連接埠衝突請修改 `/usr/local/etc/nginx-ui/app.ini`,並使用 `systemctl restart nginx-ui` 重啟 Nginx UI 守護行程。
@@ -253,13 +253,13 @@ bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/instal
 **解除安裝 Nginx UI 但保留設定和資料庫檔案**
 
 ```shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) remove
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ remove
 ```
 
 ### 更多用法
 
 ````shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) help
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ help
 ````
 
 ## Nginx 反向代理設定範例

+ 8 - 4
README.md

@@ -102,11 +102,15 @@ URL:[https://demo.nginxui.com](https://demo.nginxui.com)
 
 ### Internationalization
 
+We proudly offer official support for:
+
 - English
 - Simplified Chinese
 - Traditional Chinese
 
-We welcome translations into any language.
+As non-native English speakers, we strive for accuracy, but we know there’s always room for improvement. If you spot any issues, we’d love your feedback!
+
+Thanks to our amazing community, additional languages are also available! Explore and contribute to translations on [Weblate](https://weblate.nginxui.com).
 
 ### Built With
 
@@ -302,7 +306,7 @@ go build -tags=jsoniter -ldflags "$LD_FLAGS -X 'github.com/0xJacky/Nginx-UI/sett
 **Install and Upgrade**
 
 ```shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) install
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install
 ```
 The default listening port is `9000`, and the default HTTP Challenge port is `9180`.
 If there is a port conflict, please modify `/usr/local/etc/nginx-ui/app.ini` manually,
@@ -311,13 +315,13 @@ then use `systemctl restart nginx-ui` to reload the Nginx UI service.
 **Remove Nginx UI, except configuration and database files**
 
 ```shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) remove
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ remove
 ```
 
 ### More Usage
 
 ````shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) help
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ help
 ````
 
 ## Example of Nginx Reverse Proxy Configuration

+ 8 - 20
api/cluster/environment.go

@@ -4,6 +4,10 @@ import (
 	"crypto/sha256"
 	"encoding/hex"
 	"encoding/json"
+	"io"
+	"net/http"
+	"time"
+
 	"github.com/0xJacky/Nginx-UI/api"
 	"github.com/0xJacky/Nginx-UI/internal/analytic"
 	"github.com/0xJacky/Nginx-UI/internal/cluster"
@@ -14,9 +18,6 @@ import (
 	"github.com/spf13/cast"
 	"github.com/uozi-tech/cosy"
 	"gorm.io/gorm"
-	"io"
-	"net/http"
-	"time"
 )
 
 func GetEnvironment(c *gin.Context) {
@@ -151,23 +152,10 @@ func EditEnvironment(c *gin.Context) {
 }
 
 func DeleteEnvironment(c *gin.Context) {
-	id := cast.ToUint64(c.Param("id"))
-	envQuery := query.Environment
-
-	env, err := envQuery.FirstByID(id)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-	err = envQuery.DeleteByID(env.ID)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	go analytic.RestartRetrieveNodesStatus()
-
-	c.JSON(http.StatusNoContent, nil)
+	cosy.Core[model.Environment](c).
+		ExecutedHook(func(c *cosy.Ctx[model.Environment]) {
+			go analytic.RestartRetrieveNodesStatus()
+		}).Destroy()
 }
 
 func LoadEnvironmentFromSettings(c *gin.Context) {

+ 1 - 1
api/system/router.go

@@ -15,9 +15,9 @@ func InitPrivateRouter(r *gin.RouterGroup) {
 	r.GET("upgrade/current", GetCurrentVersion)
 	r.GET("self_check", SelfCheck)
 	r.POST("self_check/:name/fix", SelfCheckFix)
-	r.GET("self_check/websocket", CheckWebSocket)
 }
 
 func InitWebSocketRouter(r *gin.RouterGroup) {
 	r.GET("upgrade/perform", PerformCoreUpgrade)
+	r.GET("self_check/websocket", CheckWebSocket)
 }

+ 1 - 1
app/.env

@@ -1 +1 @@
-VITE_PROXY_TARGET=http://127.0.0.1:9001
+VITE_PROXY_TARGET=http://127.0.0.1:9000

+ 2 - 0
app/app.go

@@ -1,3 +1,5 @@
+//go:build !unembed
+
 package app
 
 import (

+ 8 - 0
app/app_unembed.go

@@ -0,0 +1,8 @@
+//go:build unembed
+
+package app
+
+import "embed"
+
+//go:embed i18n.json src/language/* src/language/*/*
+var DistFS embed.FS

+ 5 - 5
app/package.json

@@ -2,7 +2,7 @@
   "name": "nginx-ui-app-next",
   "type": "module",
   "version": "2.0.0-rc.1",
-  "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0",
+  "packageManager": "pnpm@10.2.0+sha512.0d27364e0139c6aadeed65ada153135e0ca96c8da42123bd50047f961339dc7a758fc2e944b428f52be570d1bd3372455c1c65fa2e7aa0bfbf931190f9552001",
   "scripts": {
     "dev": "vite --host",
     "typecheck": "vue-tsc --noEmit",
@@ -52,14 +52,14 @@
     "vuedraggable": "^4.1.0"
   },
   "devDependencies": {
-    "@antfu/eslint-config": "^4.1.0",
+    "@antfu/eslint-config": "^4.1.1",
     "@iconify-json/fa": "1.2.1",
-    "@iconify-json/tabler": "^1.2.14",
+    "@iconify-json/tabler": "^1.2.15",
     "@iconify/tools": "^4.1.1",
     "@iconify/types": "^2.0.0",
     "@iconify/utils": "^2.2.1",
     "@iconify/vue": "^4.3.0",
-    "@types/lodash": "^4.17.14",
+    "@types/lodash": "^4.17.15",
     "@types/nprogress": "^0.2.3",
     "@types/sortablejs": "^1.15.8",
     "@vitejs/plugin-vue": "^5.2.1",
@@ -68,7 +68,7 @@
     "@vue/tsconfig": "^0.7.0",
     "ace-builds": "^1.37.5",
     "autoprefixer": "^10.4.20",
-    "eslint": "9.18.0",
+    "eslint": "9.19.0",
     "eslint-plugin-sonarjs": "^3.0.1",
     "less": "^4.2.2",
     "postcss": "^8.5.1",

+ 218 - 228
app/pnpm-lock.yaml

@@ -121,14 +121,14 @@ importers:
         version: 4.1.0(vue@3.5.13(typescript@5.7.3))
     devDependencies:
       '@antfu/eslint-config':
-        specifier: ^4.1.0
-        version: 4.1.0(@typescript-eslint/utils@8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3))(@vue/compiler-sfc@3.5.13)(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
+        specifier: ^4.1.1
+        version: 4.1.1(@typescript-eslint/utils@8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3))(@vue/compiler-sfc@3.5.13)(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
       '@iconify-json/fa':
         specifier: 1.2.1
         version: 1.2.1
       '@iconify-json/tabler':
-        specifier: ^1.2.14
-        version: 1.2.14
+        specifier: ^1.2.15
+        version: 1.2.15
       '@iconify/tools':
         specifier: ^4.1.1
         version: 4.1.1
@@ -142,8 +142,8 @@ importers:
         specifier: ^4.3.0
         version: 4.3.0(vue@3.5.13(typescript@5.7.3))
       '@types/lodash':
-        specifier: ^4.17.14
-        version: 4.17.14
+        specifier: ^4.17.15
+        version: 4.17.15
       '@types/nprogress':
         specifier: ^0.2.3
         version: 0.2.3
@@ -169,11 +169,11 @@ importers:
         specifier: ^10.4.20
         version: 10.4.20(postcss@8.5.1)
       eslint:
-        specifier: 9.18.0
-        version: 9.18.0(jiti@2.4.1)
+        specifier: 9.19.0
+        version: 9.19.0(jiti@2.4.1)
       eslint-plugin-sonarjs:
         specifier: ^3.0.1
-        version: 3.0.1(eslint@9.18.0(jiti@2.4.1))
+        version: 3.0.1(eslint@9.19.0(jiti@2.4.1))
       less:
         specifier: ^4.2.2
         version: 4.2.2
@@ -222,8 +222,8 @@ packages:
     peerDependencies:
       vue: '>=3.0.3'
 
-  '@antfu/eslint-config@4.1.0':
-    resolution: {integrity: sha512-2yainF3mBykqzsxXbHYGuLwm60sRRzQqJdLJd2IfESIGkkIkQfUI3IEGTANGpdWSmiC9jhICP7Y8yY+TSKGUQg==}
+  '@antfu/eslint-config@4.1.1':
+    resolution: {integrity: sha512-5UVRu8uC6Q9e+o49ppafvIfOT3geqo74bZNAZ1Rvx10OF8gkUh7gT6b5yEJkUeej3WHRyVw3kTTgK52To1E+VQ==}
     hasBin: true
     peerDependencies:
       '@eslint-react/eslint-plugin': ^1.19.0
@@ -1197,8 +1197,8 @@ packages:
     resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
     engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
 
-  '@eslint/compat@1.2.4':
-    resolution: {integrity: sha512-S8ZdQj/N69YAtuqFt7653jwcvuUj131+6qGLUyDqfDg1OIoBQ66OCuXC473YQfO2AaxITTutiRQiDwoo7ZLYyg==}
+  '@eslint/compat@1.2.6':
+    resolution: {integrity: sha512-k7HNCqApoDHM6XzT30zGoETj+D+uUcZUb+IVAJmar3u6bvHf7hhHJcWx09QHj4/a2qrKZMWU0E16tvkiAdv06Q==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: ^9.10.0
@@ -1218,8 +1218,8 @@ packages:
     resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/js@9.18.0':
-    resolution: {integrity: sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==}
+  '@eslint/js@9.19.0':
+    resolution: {integrity: sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   '@eslint/markdown@6.2.2':
@@ -1260,8 +1260,8 @@ packages:
   '@iconify-json/fa@1.2.1':
     resolution: {integrity: sha512-aY2+tQNWq5ch+ShtAz3KKbNrFfwf4BPrXvyN7S4/lcf6Wms+kIxsd7C7KortzHZhoBnbhVN+qo+YUWLW7rLs9Q==}
 
-  '@iconify-json/tabler@1.2.14':
-    resolution: {integrity: sha512-X5Li79KW5KilHIaNVFKMuPr0WOA4B/Y/EqkrrZiEihs3Wcq/eIDMDDqGBkmei/8naYyfpkj0LWH7osr+vJYFEA==}
+  '@iconify-json/tabler@1.2.15':
+    resolution: {integrity: sha512-EMEOt1PubLxcbwHMO9XmHT601A/2fCNd7fK50p5Qh42xvVxCvO3YhhGuGMAk6t6VkdSZYLQ14Y2bYPcom1aFeQ==}
 
   '@iconify/tools@4.1.1':
     resolution: {integrity: sha512-Hybu/HGhv6T8nLQhiG9rKx+ekF7NNpPOEQAy7JRSKht3s3dcFSsPccYzk24Znc9MTxrR6Gak3cg6CPP5dyvS2Q==}
@@ -1447,8 +1447,8 @@ packages:
     resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
     engines: {node: '>=18'}
 
-  '@stylistic/eslint-plugin@3.0.0':
-    resolution: {integrity: sha512-9GJI6iBtGjOqSsyCKUvE6Vn7qDT52hbQaoq/SwxH6A1bciymZfvBfHIIrD3E7Koi2sjzOa/MNQ2XOguHtVJOyw==}
+  '@stylistic/eslint-plugin@3.0.1':
+    resolution: {integrity: sha512-rQ3tcT5N2cynofJfbjUsnL4seoewTaOVBLyUEwtNldo7iNMPo3h/GUQk+Cl3iHEWwRxjq2wuH6q0FufQrbVL1A==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: '>=8.40.0'
@@ -1503,8 +1503,8 @@ packages:
   '@types/json-schema@7.0.15':
     resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
 
-  '@types/lodash@4.17.14':
-    resolution: {integrity: sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==}
+  '@types/lodash@4.17.15':
+    resolution: {integrity: sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==}
 
   '@types/mdast@4.0.4':
     resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -1545,52 +1545,43 @@ packages:
   '@types/yauzl@2.10.3':
     resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
 
-  '@typescript-eslint/eslint-plugin@8.21.0':
-    resolution: {integrity: sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==}
+  '@typescript-eslint/eslint-plugin@8.23.0':
+    resolution: {integrity: sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
       eslint: ^8.57.0 || ^9.0.0
       typescript: '>=4.8.4 <5.8.0'
 
-  '@typescript-eslint/parser@8.21.0':
-    resolution: {integrity: sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA==}
+  '@typescript-eslint/parser@8.23.0':
+    resolution: {integrity: sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: ^8.57.0 || ^9.0.0
       typescript: '>=4.8.4 <5.8.0'
 
-  '@typescript-eslint/scope-manager@8.13.0':
-    resolution: {integrity: sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==}
-    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
   '@typescript-eslint/scope-manager@8.21.0':
     resolution: {integrity: sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript-eslint/type-utils@8.21.0':
-    resolution: {integrity: sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ==}
+  '@typescript-eslint/scope-manager@8.23.0':
+    resolution: {integrity: sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/type-utils@8.23.0':
+    resolution: {integrity: sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: ^8.57.0 || ^9.0.0
       typescript: '>=4.8.4 <5.8.0'
 
-  '@typescript-eslint/types@8.13.0':
-    resolution: {integrity: sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==}
-    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
   '@typescript-eslint/types@8.21.0':
     resolution: {integrity: sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript-eslint/typescript-estree@8.13.0':
-    resolution: {integrity: sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==}
+  '@typescript-eslint/types@8.23.0':
+    resolution: {integrity: sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-    peerDependencies:
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
 
   '@typescript-eslint/typescript-estree@8.21.0':
     resolution: {integrity: sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==}
@@ -1598,11 +1589,11 @@ packages:
     peerDependencies:
       typescript: '>=4.8.4 <5.8.0'
 
-  '@typescript-eslint/utils@8.13.0':
-    resolution: {integrity: sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==}
+  '@typescript-eslint/typescript-estree@8.23.0':
+    resolution: {integrity: sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
-      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <5.8.0'
 
   '@typescript-eslint/utils@8.21.0':
     resolution: {integrity: sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw==}
@@ -1611,14 +1602,21 @@ packages:
       eslint: ^8.57.0 || ^9.0.0
       typescript: '>=4.8.4 <5.8.0'
 
-  '@typescript-eslint/visitor-keys@8.13.0':
-    resolution: {integrity: sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==}
+  '@typescript-eslint/utils@8.23.0':
+    resolution: {integrity: sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <5.8.0'
 
   '@typescript-eslint/visitor-keys@8.21.0':
     resolution: {integrity: sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
+  '@typescript-eslint/visitor-keys@8.23.0':
+    resolution: {integrity: sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
   '@unocss/astro@65.4.3':
     resolution: {integrity: sha512-yhPKH4CT2CFjvKR8lL6oS/7jarMWp4iSnYcNlTlZLmvTIS3dGxyhAsVy/xkdzdJ6sM+6FS0hUuQNv+NYvArRNg==}
     peerDependencies:
@@ -2467,13 +2465,13 @@ packages:
     peerDependencies:
       eslint: '>=6.0.0'
 
-  eslint-config-flat-gitignore@1.0.0:
-    resolution: {integrity: sha512-EWpSLrAP80IdcYK5sIhq/qAY0pmUdBnbzqzpE3QAn6H6wLBN26cMRoMNU9Di8upTzUSL6TXeYRxWhTYuz8+UQA==}
+  eslint-config-flat-gitignore@2.0.0:
+    resolution: {integrity: sha512-9iH+DZO94uxsw5iFjzqa9GfahA5oK3nA1GoJK/6u8evAtooYJMwuSWiLcGDfrdLoqdQ5/kqFJKKuMY/+SAasvg==}
     peerDependencies:
       eslint: ^9.5.0
 
-  eslint-flat-config-utils@2.0.0:
-    resolution: {integrity: sha512-AbpYwI9FBmjF6BQ8UcaDCrM750DWEB6UJzEjQEg+iWFP6UX9rGsUGJlMf7sWbW3dOA0klUEwmWGZa5FoynXU/w==}
+  eslint-flat-config-utils@2.0.1:
+    resolution: {integrity: sha512-brf0eAgQ6JlKj3bKfOTuuI7VcCZvi8ZCD1MMTVoEvS/d38j8cByZViLFALH/36+eqB17ukmfmKq3bWzGvizejA==}
 
   eslint-import-resolver-node@0.3.9:
     resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
@@ -2618,8 +2616,8 @@ packages:
     resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  eslint@9.18.0:
-    resolution: {integrity: sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==}
+  eslint@9.19.0:
+    resolution: {integrity: sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     hasBin: true
     peerDependencies:
@@ -2710,10 +2708,6 @@ packages:
     resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==}
     engines: {node: '>=4.0.0'}
 
-  find-up-simple@1.0.0:
-    resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==}
-    engines: {node: '>=18'}
-
   find-up@4.1.0:
     resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
     engines: {node: '>=8'}
@@ -4124,18 +4118,18 @@ packages:
     resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
     engines: {node: '>=6'}
 
-  ts-api-utils@1.4.3:
-    resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
-    engines: {node: '>=16'}
-    peerDependencies:
-      typescript: '>=4.2.0'
-
   ts-api-utils@2.0.0:
     resolution: {integrity: sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==}
     engines: {node: '>=18.12'}
     peerDependencies:
       typescript: '>=4.8.4'
 
+  ts-api-utils@2.0.1:
+    resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==}
+    engines: {node: '>=18.12'}
+    peerDependencies:
+      typescript: '>=4.8.4'
+
   tslib@2.8.1:
     resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
 
@@ -4564,42 +4558,42 @@ snapshots:
       '@ant-design/icons-svg': 4.4.2
       vue: 3.5.13(typescript@5.7.3)
 
-  '@antfu/eslint-config@4.1.0(@typescript-eslint/utils@8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3))(@vue/compiler-sfc@3.5.13)(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)':
+  '@antfu/eslint-config@4.1.1(@typescript-eslint/utils@8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3))(@vue/compiler-sfc@3.5.13)(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)':
     dependencies:
       '@antfu/install-pkg': 1.0.0
       '@clack/prompts': 0.9.1
-      '@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.18.0(jiti@2.4.1))
+      '@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.19.0(jiti@2.4.1))
       '@eslint/markdown': 6.2.2
-      '@stylistic/eslint-plugin': 3.0.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
-      '@typescript-eslint/eslint-plugin': 8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
-      '@typescript-eslint/parser': 8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
-      '@vitest/eslint-plugin': 1.1.25(@typescript-eslint/utils@8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
-      eslint: 9.18.0(jiti@2.4.1)
-      eslint-config-flat-gitignore: 1.0.0(eslint@9.18.0(jiti@2.4.1))
-      eslint-flat-config-utils: 2.0.0
-      eslint-merge-processors: 1.0.0(eslint@9.18.0(jiti@2.4.1))
-      eslint-plugin-antfu: 3.0.0(eslint@9.18.0(jiti@2.4.1))
-      eslint-plugin-command: 3.0.0(eslint@9.18.0(jiti@2.4.1))
-      eslint-plugin-import-x: 4.6.1(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
-      eslint-plugin-jsdoc: 50.6.3(eslint@9.18.0(jiti@2.4.1))
-      eslint-plugin-jsonc: 2.19.1(eslint@9.18.0(jiti@2.4.1))
-      eslint-plugin-n: 17.15.1(eslint@9.18.0(jiti@2.4.1))
+      '@stylistic/eslint-plugin': 3.0.1(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
+      '@typescript-eslint/eslint-plugin': 8.23.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
+      '@typescript-eslint/parser': 8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
+      '@vitest/eslint-plugin': 1.1.25(@typescript-eslint/utils@8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
+      eslint: 9.19.0(jiti@2.4.1)
+      eslint-config-flat-gitignore: 2.0.0(eslint@9.19.0(jiti@2.4.1))
+      eslint-flat-config-utils: 2.0.1
+      eslint-merge-processors: 1.0.0(eslint@9.19.0(jiti@2.4.1))
+      eslint-plugin-antfu: 3.0.0(eslint@9.19.0(jiti@2.4.1))
+      eslint-plugin-command: 3.0.0(eslint@9.19.0(jiti@2.4.1))
+      eslint-plugin-import-x: 4.6.1(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
+      eslint-plugin-jsdoc: 50.6.3(eslint@9.19.0(jiti@2.4.1))
+      eslint-plugin-jsonc: 2.19.1(eslint@9.19.0(jiti@2.4.1))
+      eslint-plugin-n: 17.15.1(eslint@9.19.0(jiti@2.4.1))
       eslint-plugin-no-only-tests: 3.3.0
-      eslint-plugin-perfectionist: 4.7.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
-      eslint-plugin-regexp: 2.7.0(eslint@9.18.0(jiti@2.4.1))
-      eslint-plugin-toml: 0.12.0(eslint@9.18.0(jiti@2.4.1))
-      eslint-plugin-unicorn: 56.0.1(eslint@9.18.0(jiti@2.4.1))
-      eslint-plugin-unused-imports: 4.1.4(@typescript-eslint/eslint-plugin@8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.18.0(jiti@2.4.1))
-      eslint-plugin-vue: 9.32.0(eslint@9.18.0(jiti@2.4.1))
-      eslint-plugin-yml: 1.16.0(eslint@9.18.0(jiti@2.4.1))
-      eslint-processor-vue-blocks: 1.0.0(@vue/compiler-sfc@3.5.13)(eslint@9.18.0(jiti@2.4.1))
+      eslint-plugin-perfectionist: 4.7.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
+      eslint-plugin-regexp: 2.7.0(eslint@9.19.0(jiti@2.4.1))
+      eslint-plugin-toml: 0.12.0(eslint@9.19.0(jiti@2.4.1))
+      eslint-plugin-unicorn: 56.0.1(eslint@9.19.0(jiti@2.4.1))
+      eslint-plugin-unused-imports: 4.1.4(@typescript-eslint/eslint-plugin@8.23.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.1))
+      eslint-plugin-vue: 9.32.0(eslint@9.19.0(jiti@2.4.1))
+      eslint-plugin-yml: 1.16.0(eslint@9.19.0(jiti@2.4.1))
+      eslint-processor-vue-blocks: 1.0.0(@vue/compiler-sfc@3.5.13)(eslint@9.19.0(jiti@2.4.1))
       globals: 15.14.0
       jsonc-eslint-parser: 2.4.0
       local-pkg: 1.0.0
       parse-gitignore: 2.0.0
       picocolors: 1.1.1
       toml-eslint-parser: 0.10.0
-      vue-eslint-parser: 9.4.3(eslint@9.18.0(jiti@2.4.1))
+      vue-eslint-parser: 9.4.3(eslint@9.19.0(jiti@2.4.1))
       yaml-eslint-parser: 1.2.3
       yargs: 17.7.2
     transitivePeerDependencies:
@@ -4652,11 +4646,11 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/eslint-parser@7.25.9(@babel/core@7.26.0)(eslint@9.18.0(jiti@2.4.1))':
+  '@babel/eslint-parser@7.25.9(@babel/core@7.26.0)(eslint@9.19.0(jiti@2.4.1))':
     dependencies:
       '@babel/core': 7.26.0
       '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       eslint-visitor-keys: 2.1.0
       semver: 6.3.1
 
@@ -5555,22 +5549,22 @@ snapshots:
   '@esbuild/win32-x64@0.24.2':
     optional: true
 
-  '@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.18.0(jiti@2.4.1))':
+  '@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.19.0(jiti@2.4.1))':
     dependencies:
       escape-string-regexp: 4.0.0
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       ignore: 5.3.2
 
-  '@eslint-community/eslint-utils@4.4.1(eslint@9.18.0(jiti@2.4.1))':
+  '@eslint-community/eslint-utils@4.4.1(eslint@9.19.0(jiti@2.4.1))':
     dependencies:
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       eslint-visitor-keys: 3.4.3
 
   '@eslint-community/regexpp@4.12.1': {}
 
-  '@eslint/compat@1.2.4(eslint@9.18.0(jiti@2.4.1))':
+  '@eslint/compat@1.2.6(eslint@9.19.0(jiti@2.4.1))':
     optionalDependencies:
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
 
   '@eslint/config-array@0.19.1':
     dependencies:
@@ -5598,7 +5592,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@eslint/js@9.18.0': {}
+  '@eslint/js@9.19.0': {}
 
   '@eslint/markdown@6.2.2':
     dependencies:
@@ -5636,7 +5630,7 @@ snapshots:
     dependencies:
       '@iconify/types': 2.0.0
 
-  '@iconify-json/tabler@1.2.14':
+  '@iconify-json/tabler@1.2.15':
     dependencies:
       '@iconify/types': 2.0.0
 
@@ -5847,10 +5841,10 @@ snapshots:
 
   '@sindresorhus/merge-streams@2.3.0': {}
 
-  '@stylistic/eslint-plugin@3.0.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)':
+  '@stylistic/eslint-plugin@3.0.1(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)':
     dependencies:
-      '@typescript-eslint/utils': 8.13.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
-      eslint: 9.18.0(jiti@2.4.1)
+      '@typescript-eslint/utils': 8.21.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
+      eslint: 9.19.0(jiti@2.4.1)
       eslint-visitor-keys: 4.2.0
       espree: 10.3.0
       estraverse: 5.3.0
@@ -5902,7 +5896,7 @@ snapshots:
 
   '@types/json-schema@7.0.15': {}
 
-  '@types/lodash@4.17.14': {}
+  '@types/lodash@4.17.15': {}
 
   '@types/mdast@4.0.4':
     dependencies:
@@ -5941,121 +5935,120 @@ snapshots:
       '@types/node': 22.10.2
     optional: true
 
-  '@typescript-eslint/eslint-plugin@8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)':
+  '@typescript-eslint/eslint-plugin@8.23.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)':
     dependencies:
       '@eslint-community/regexpp': 4.12.1
-      '@typescript-eslint/parser': 8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
-      '@typescript-eslint/scope-manager': 8.21.0
-      '@typescript-eslint/type-utils': 8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
-      '@typescript-eslint/utils': 8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
-      '@typescript-eslint/visitor-keys': 8.21.0
-      eslint: 9.18.0(jiti@2.4.1)
+      '@typescript-eslint/parser': 8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
+      '@typescript-eslint/scope-manager': 8.23.0
+      '@typescript-eslint/type-utils': 8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
+      '@typescript-eslint/utils': 8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
+      '@typescript-eslint/visitor-keys': 8.23.0
+      eslint: 9.19.0(jiti@2.4.1)
       graphemer: 1.4.0
       ignore: 5.3.2
       natural-compare: 1.4.0
-      ts-api-utils: 2.0.0(typescript@5.7.3)
+      ts-api-utils: 2.0.1(typescript@5.7.3)
       typescript: 5.7.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)':
+  '@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)':
     dependencies:
-      '@typescript-eslint/scope-manager': 8.21.0
-      '@typescript-eslint/types': 8.21.0
-      '@typescript-eslint/typescript-estree': 8.21.0(typescript@5.7.3)
-      '@typescript-eslint/visitor-keys': 8.21.0
+      '@typescript-eslint/scope-manager': 8.23.0
+      '@typescript-eslint/types': 8.23.0
+      '@typescript-eslint/typescript-estree': 8.23.0(typescript@5.7.3)
+      '@typescript-eslint/visitor-keys': 8.23.0
       debug: 4.4.0
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       typescript: 5.7.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/scope-manager@8.13.0':
-    dependencies:
-      '@typescript-eslint/types': 8.13.0
-      '@typescript-eslint/visitor-keys': 8.13.0
-
   '@typescript-eslint/scope-manager@8.21.0':
     dependencies:
       '@typescript-eslint/types': 8.21.0
       '@typescript-eslint/visitor-keys': 8.21.0
 
-  '@typescript-eslint/type-utils@8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)':
+  '@typescript-eslint/scope-manager@8.23.0':
     dependencies:
-      '@typescript-eslint/typescript-estree': 8.21.0(typescript@5.7.3)
-      '@typescript-eslint/utils': 8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
+      '@typescript-eslint/types': 8.23.0
+      '@typescript-eslint/visitor-keys': 8.23.0
+
+  '@typescript-eslint/type-utils@8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)':
+    dependencies:
+      '@typescript-eslint/typescript-estree': 8.23.0(typescript@5.7.3)
+      '@typescript-eslint/utils': 8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
       debug: 4.4.0
-      eslint: 9.18.0(jiti@2.4.1)
-      ts-api-utils: 2.0.0(typescript@5.7.3)
+      eslint: 9.19.0(jiti@2.4.1)
+      ts-api-utils: 2.0.1(typescript@5.7.3)
       typescript: 5.7.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/types@8.13.0': {}
-
   '@typescript-eslint/types@8.21.0': {}
 
-  '@typescript-eslint/typescript-estree@8.13.0(typescript@5.7.3)':
+  '@typescript-eslint/types@8.23.0': {}
+
+  '@typescript-eslint/typescript-estree@8.21.0(typescript@5.7.3)':
     dependencies:
-      '@typescript-eslint/types': 8.13.0
-      '@typescript-eslint/visitor-keys': 8.13.0
+      '@typescript-eslint/types': 8.21.0
+      '@typescript-eslint/visitor-keys': 8.21.0
       debug: 4.4.0
       fast-glob: 3.3.3
       is-glob: 4.0.3
       minimatch: 9.0.5
       semver: 7.6.3
-      ts-api-utils: 1.4.3(typescript@5.7.3)
-    optionalDependencies:
+      ts-api-utils: 2.0.0(typescript@5.7.3)
       typescript: 5.7.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/typescript-estree@8.21.0(typescript@5.7.3)':
+  '@typescript-eslint/typescript-estree@8.23.0(typescript@5.7.3)':
     dependencies:
-      '@typescript-eslint/types': 8.21.0
-      '@typescript-eslint/visitor-keys': 8.21.0
+      '@typescript-eslint/types': 8.23.0
+      '@typescript-eslint/visitor-keys': 8.23.0
       debug: 4.4.0
       fast-glob: 3.3.3
       is-glob: 4.0.3
       minimatch: 9.0.5
       semver: 7.6.3
-      ts-api-utils: 2.0.0(typescript@5.7.3)
+      ts-api-utils: 2.0.1(typescript@5.7.3)
       typescript: 5.7.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/utils@8.13.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)':
+  '@typescript-eslint/utils@8.21.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0(jiti@2.4.1))
-      '@typescript-eslint/scope-manager': 8.13.0
-      '@typescript-eslint/types': 8.13.0
-      '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.7.3)
-      eslint: 9.18.0(jiti@2.4.1)
-    transitivePeerDependencies:
-      - supports-color
-      - typescript
-
-  '@typescript-eslint/utils@8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)':
-    dependencies:
-      '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0(jiti@2.4.1))
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.1))
       '@typescript-eslint/scope-manager': 8.21.0
       '@typescript-eslint/types': 8.21.0
       '@typescript-eslint/typescript-estree': 8.21.0(typescript@5.7.3)
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       typescript: 5.7.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/visitor-keys@8.13.0':
+  '@typescript-eslint/utils@8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)':
     dependencies:
-      '@typescript-eslint/types': 8.13.0
-      eslint-visitor-keys: 3.4.3
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.1))
+      '@typescript-eslint/scope-manager': 8.23.0
+      '@typescript-eslint/types': 8.23.0
+      '@typescript-eslint/typescript-estree': 8.23.0(typescript@5.7.3)
+      eslint: 9.19.0(jiti@2.4.1)
+      typescript: 5.7.3
+    transitivePeerDependencies:
+      - supports-color
 
   '@typescript-eslint/visitor-keys@8.21.0':
     dependencies:
       '@typescript-eslint/types': 8.21.0
       eslint-visitor-keys: 4.2.0
 
+  '@typescript-eslint/visitor-keys@8.23.0':
+    dependencies:
+      '@typescript-eslint/types': 8.23.0
+      eslint-visitor-keys: 4.2.0
+
   '@unocss/astro@65.4.3(rollup@4.28.1)(vite@6.0.11(@types/node@22.10.2)(jiti@2.4.1)(less@4.2.2)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.13(typescript@5.7.3))':
     dependencies:
       '@unocss/core': 65.4.3
@@ -6223,10 +6216,10 @@ snapshots:
       vite: 6.0.11(@types/node@22.10.2)(jiti@2.4.1)(less@4.2.2)(tsx@4.19.2)(yaml@2.6.1)
       vue: 3.5.13(typescript@5.7.3)
 
-  '@vitest/eslint-plugin@1.1.25(@typescript-eslint/utils@8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)':
+  '@vitest/eslint-plugin@1.1.25(@typescript-eslint/utils@8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)':
     dependencies:
-      '@typescript-eslint/utils': 8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
-      eslint: 9.18.0(jiti@2.4.1)
+      '@typescript-eslint/utils': 8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
+      eslint: 9.19.0(jiti@2.4.1)
     optionalDependencies:
       typescript: 5.7.3
 
@@ -7114,23 +7107,22 @@ snapshots:
 
   escape-string-regexp@5.0.0: {}
 
-  eslint-compat-utils@0.5.1(eslint@9.18.0(jiti@2.4.1)):
+  eslint-compat-utils@0.5.1(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       semver: 7.6.3
 
-  eslint-compat-utils@0.6.4(eslint@9.18.0(jiti@2.4.1)):
+  eslint-compat-utils@0.6.4(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       semver: 7.6.3
 
-  eslint-config-flat-gitignore@1.0.0(eslint@9.18.0(jiti@2.4.1)):
+  eslint-config-flat-gitignore@2.0.0(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
-      '@eslint/compat': 1.2.4(eslint@9.18.0(jiti@2.4.1))
-      eslint: 9.18.0(jiti@2.4.1)
-      find-up-simple: 1.0.0
+      '@eslint/compat': 1.2.6(eslint@9.19.0(jiti@2.4.1))
+      eslint: 9.19.0(jiti@2.4.1)
 
-  eslint-flat-config-utils@2.0.0:
+  eslint-flat-config-utils@2.0.1:
     dependencies:
       pathe: 2.0.2
 
@@ -7142,41 +7134,41 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-json-compat-utils@0.2.1(eslint@9.18.0(jiti@2.4.1))(jsonc-eslint-parser@2.4.0):
+  eslint-json-compat-utils@0.2.1(eslint@9.19.0(jiti@2.4.1))(jsonc-eslint-parser@2.4.0):
     dependencies:
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       esquery: 1.6.0
       jsonc-eslint-parser: 2.4.0
 
-  eslint-merge-processors@1.0.0(eslint@9.18.0(jiti@2.4.1)):
+  eslint-merge-processors@1.0.0(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
 
-  eslint-plugin-antfu@3.0.0(eslint@9.18.0(jiti@2.4.1)):
+  eslint-plugin-antfu@3.0.0(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
 
-  eslint-plugin-command@3.0.0(eslint@9.18.0(jiti@2.4.1)):
+  eslint-plugin-command@3.0.0(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
       '@es-joy/jsdoccomment': 0.50.0
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
 
-  eslint-plugin-es-x@7.8.0(eslint@9.18.0(jiti@2.4.1)):
+  eslint-plugin-es-x@7.8.0(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0(jiti@2.4.1))
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.1))
       '@eslint-community/regexpp': 4.12.1
-      eslint: 9.18.0(jiti@2.4.1)
-      eslint-compat-utils: 0.5.1(eslint@9.18.0(jiti@2.4.1))
+      eslint: 9.19.0(jiti@2.4.1)
+      eslint-compat-utils: 0.5.1(eslint@9.19.0(jiti@2.4.1))
 
-  eslint-plugin-import-x@4.6.1(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3):
+  eslint-plugin-import-x@4.6.1(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3):
     dependencies:
       '@types/doctrine': 0.0.9
       '@typescript-eslint/scope-manager': 8.21.0
-      '@typescript-eslint/utils': 8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
+      '@typescript-eslint/utils': 8.21.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
       debug: 4.4.0
       doctrine: 3.0.0
       enhanced-resolve: 5.17.1
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       eslint-import-resolver-node: 0.3.9
       get-tsconfig: 4.8.1
       is-glob: 4.0.3
@@ -7188,14 +7180,14 @@ snapshots:
       - supports-color
       - typescript
 
-  eslint-plugin-jsdoc@50.6.3(eslint@9.18.0(jiti@2.4.1)):
+  eslint-plugin-jsdoc@50.6.3(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
       '@es-joy/jsdoccomment': 0.49.0
       are-docs-informative: 0.0.2
       comment-parser: 1.4.1
       debug: 4.4.0
       escape-string-regexp: 4.0.0
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       espree: 10.3.0
       esquery: 1.6.0
       parse-imports: 2.2.1
@@ -7205,12 +7197,12 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-plugin-jsonc@2.19.1(eslint@9.18.0(jiti@2.4.1)):
+  eslint-plugin-jsonc@2.19.1(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0(jiti@2.4.1))
-      eslint: 9.18.0(jiti@2.4.1)
-      eslint-compat-utils: 0.6.4(eslint@9.18.0(jiti@2.4.1))
-      eslint-json-compat-utils: 0.2.1(eslint@9.18.0(jiti@2.4.1))(jsonc-eslint-parser@2.4.0)
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.1))
+      eslint: 9.19.0(jiti@2.4.1)
+      eslint-compat-utils: 0.6.4(eslint@9.19.0(jiti@2.4.1))
+      eslint-json-compat-utils: 0.2.1(eslint@9.19.0(jiti@2.4.1))(jsonc-eslint-parser@2.4.0)
       espree: 9.6.1
       graphemer: 1.4.0
       jsonc-eslint-parser: 2.4.0
@@ -7219,12 +7211,12 @@ snapshots:
     transitivePeerDependencies:
       - '@eslint/json'
 
-  eslint-plugin-n@17.15.1(eslint@9.18.0(jiti@2.4.1)):
+  eslint-plugin-n@17.15.1(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0(jiti@2.4.1))
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.1))
       enhanced-resolve: 5.17.1
-      eslint: 9.18.0(jiti@2.4.1)
-      eslint-plugin-es-x: 7.8.0(eslint@9.18.0(jiti@2.4.1))
+      eslint: 9.19.0(jiti@2.4.1)
+      eslint-plugin-es-x: 7.8.0(eslint@9.19.0(jiti@2.4.1))
       get-tsconfig: 4.8.1
       globals: 15.14.0
       ignore: 5.3.2
@@ -7233,31 +7225,31 @@ snapshots:
 
   eslint-plugin-no-only-tests@3.3.0: {}
 
-  eslint-plugin-perfectionist@4.7.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3):
+  eslint-plugin-perfectionist@4.7.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3):
     dependencies:
       '@typescript-eslint/types': 8.21.0
-      '@typescript-eslint/utils': 8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
-      eslint: 9.18.0(jiti@2.4.1)
+      '@typescript-eslint/utils': 8.21.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
+      eslint: 9.19.0(jiti@2.4.1)
       natural-orderby: 5.0.0
     transitivePeerDependencies:
       - supports-color
       - typescript
 
-  eslint-plugin-regexp@2.7.0(eslint@9.18.0(jiti@2.4.1)):
+  eslint-plugin-regexp@2.7.0(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0(jiti@2.4.1))
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.1))
       '@eslint-community/regexpp': 4.12.1
       comment-parser: 1.4.1
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       jsdoc-type-pratt-parser: 4.1.0
       refa: 0.12.1
       regexp-ast-analysis: 0.7.1
       scslre: 0.3.0
 
-  eslint-plugin-sonarjs@3.0.1(eslint@9.18.0(jiti@2.4.1)):
+  eslint-plugin-sonarjs@3.0.1(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
       '@babel/core': 7.26.0
-      '@babel/eslint-parser': 7.25.9(@babel/core@7.26.0)(eslint@9.18.0(jiti@2.4.1))
+      '@babel/eslint-parser': 7.25.9(@babel/core@7.26.0)(eslint@9.19.0(jiti@2.4.1))
       '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.26.0)
       '@babel/preset-env': 7.26.0(@babel/core@7.26.0)
       '@babel/preset-flow': 7.25.9(@babel/core@7.26.0)
@@ -7265,7 +7257,7 @@ snapshots:
       '@eslint-community/regexpp': 4.12.1
       builtin-modules: 3.3.0
       bytes: 3.1.2
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       functional-red-black-tree: 1.0.1
       jsx-ast-utils: 3.3.5
       minimatch: 9.0.5
@@ -7275,24 +7267,24 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-plugin-toml@0.12.0(eslint@9.18.0(jiti@2.4.1)):
+  eslint-plugin-toml@0.12.0(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
       debug: 4.4.0
-      eslint: 9.18.0(jiti@2.4.1)
-      eslint-compat-utils: 0.6.4(eslint@9.18.0(jiti@2.4.1))
+      eslint: 9.19.0(jiti@2.4.1)
+      eslint-compat-utils: 0.6.4(eslint@9.19.0(jiti@2.4.1))
       lodash: 4.17.21
       toml-eslint-parser: 0.10.0
     transitivePeerDependencies:
       - supports-color
 
-  eslint-plugin-unicorn@56.0.1(eslint@9.18.0(jiti@2.4.1)):
+  eslint-plugin-unicorn@56.0.1(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
       '@babel/helper-validator-identifier': 7.25.9
-      '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0(jiti@2.4.1))
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.1))
       ci-info: 4.1.0
       clean-regexp: 1.0.0
       core-js-compat: 3.39.0
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       esquery: 1.6.0
       globals: 15.14.0
       indent-string: 4.0.0
@@ -7305,41 +7297,41 @@ snapshots:
       semver: 7.6.3
       strip-indent: 3.0.0
 
-  eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.18.0(jiti@2.4.1)):
+  eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.23.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
     optionalDependencies:
-      '@typescript-eslint/eslint-plugin': 8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.18.0(jiti@2.4.1))(typescript@5.7.3)
+      '@typescript-eslint/eslint-plugin': 8.23.0(@typescript-eslint/parser@8.23.0(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.1))(typescript@5.7.3)
 
-  eslint-plugin-vue@9.32.0(eslint@9.18.0(jiti@2.4.1)):
+  eslint-plugin-vue@9.32.0(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0(jiti@2.4.1))
-      eslint: 9.18.0(jiti@2.4.1)
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.1))
+      eslint: 9.19.0(jiti@2.4.1)
       globals: 13.24.0
       natural-compare: 1.4.0
       nth-check: 2.1.1
       postcss-selector-parser: 6.1.2
       semver: 7.6.3
-      vue-eslint-parser: 9.4.3(eslint@9.18.0(jiti@2.4.1))
+      vue-eslint-parser: 9.4.3(eslint@9.19.0(jiti@2.4.1))
       xml-name-validator: 4.0.0
     transitivePeerDependencies:
       - supports-color
 
-  eslint-plugin-yml@1.16.0(eslint@9.18.0(jiti@2.4.1)):
+  eslint-plugin-yml@1.16.0(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
       debug: 4.4.0
-      eslint: 9.18.0(jiti@2.4.1)
-      eslint-compat-utils: 0.6.4(eslint@9.18.0(jiti@2.4.1))
+      eslint: 9.19.0(jiti@2.4.1)
+      eslint-compat-utils: 0.6.4(eslint@9.19.0(jiti@2.4.1))
       lodash: 4.17.21
       natural-compare: 1.4.0
       yaml-eslint-parser: 1.2.3
     transitivePeerDependencies:
       - supports-color
 
-  eslint-processor-vue-blocks@1.0.0(@vue/compiler-sfc@3.5.13)(eslint@9.18.0(jiti@2.4.1)):
+  eslint-processor-vue-blocks@1.0.0(@vue/compiler-sfc@3.5.13)(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
       '@vue/compiler-sfc': 3.5.13
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
 
   eslint-scope@5.1.1:
     dependencies:
@@ -7362,14 +7354,14 @@ snapshots:
 
   eslint-visitor-keys@4.2.0: {}
 
-  eslint@9.18.0(jiti@2.4.1):
+  eslint@9.19.0(jiti@2.4.1):
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0(jiti@2.4.1))
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.1))
       '@eslint-community/regexpp': 4.12.1
       '@eslint/config-array': 0.19.1
       '@eslint/core': 0.10.0
       '@eslint/eslintrc': 3.2.0
-      '@eslint/js': 9.18.0
+      '@eslint/js': 9.19.0
       '@eslint/plugin-kit': 0.2.5
       '@humanfs/node': 0.16.6
       '@humanwhocodes/module-importer': 1.0.1
@@ -7495,8 +7487,6 @@ snapshots:
     dependencies:
       array-back: 3.1.0
 
-  find-up-simple@1.0.0: {}
-
   find-up@4.1.0:
     dependencies:
       locate-path: 5.0.0
@@ -9087,11 +9077,11 @@ snapshots:
 
   totalist@3.0.1: {}
 
-  ts-api-utils@1.4.3(typescript@5.7.3):
+  ts-api-utils@2.0.0(typescript@5.7.3):
     dependencies:
       typescript: 5.7.3
 
-  ts-api-utils@2.0.0(typescript@5.7.3):
+  ts-api-utils@2.0.1(typescript@5.7.3):
     dependencies:
       typescript: 5.7.3
 
@@ -9387,10 +9377,10 @@ snapshots:
       dompurify: 3.2.3
       vue: 3.5.13(typescript@5.7.3)
 
-  vue-eslint-parser@9.4.3(eslint@9.18.0(jiti@2.4.1)):
+  vue-eslint-parser@9.4.3(eslint@9.19.0(jiti@2.4.1)):
     dependencies:
       debug: 4.4.0
-      eslint: 9.18.0(jiti@2.4.1)
+      eslint: 9.19.0(jiti@2.4.1)
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3
       espree: 9.6.1

+ 0 - 1
app/src/App.vue

@@ -4,7 +4,6 @@ import gettext from '@/gettext'
 import { useSettingsStore } from '@/pinia'
 import { theme } from 'ant-design-vue'
 import en_US from 'ant-design-vue/es/locale/en_US'
-
 import zh_CN from 'ant-design-vue/es/locale/zh_CN'
 import zh_TW from 'ant-design-vue/es/locale/zh_TW'
 // This starter template is using Vue 3 <script setup> SFCs

+ 1 - 1
app/src/components/StdDesign/StdDataDisplay/StdCurd.vue

@@ -181,7 +181,7 @@ function handleBatchUpdated() {
             v-if="!disableAdd && !inTrash"
             type="link"
             size="small"
-            @click="add"
+            @click="add()"
           >
             {{ $gettext('Add') }}
           </AButton>

+ 1 - 1
app/src/version.json

@@ -1 +1 @@
-{"version":"2.0.0-rc.1","build_id":1,"total_build":375}
+{"version":"2.0.0-rc.1","build_id":7,"total_build":381}

+ 2 - 1
app/src/views/dashboard/ServerAnalytic.vue

@@ -185,7 +185,7 @@ function wsOnMessage(m: MessageEvent) {
           </p>
           <p>
             {{ $gettext('Load Average:') }}
-            <span class="load-avg-describe"> 1min:</span>{{ ` ${loadavg?.load1?.toFixed(2)}` }}
+            <span class="load-avg-describe"> 1min:</span>{{ loadavg?.load1?.toFixed(2) }}
             <span class="load-avg-describe"> | 5min:</span>{{ loadavg?.load5?.toFixed(2) }}
             <span class="load-avg-describe"> | 15min:</span>{{ loadavg?.load15?.toFixed(2) }}
           </p>
@@ -440,6 +440,7 @@ function wsOnMessage(m: MessageEvent) {
 }
 
 .load-avg-describe {
+  margin-right: 2px;
   @media (max-width: 1600px) and (min-width: 1200px) {
     display: none;
   }

+ 14 - 1
app/src/views/environment/Environment.vue

@@ -6,6 +6,7 @@ import BatchUpgrader from '@/views/environment/BatchUpgrader.vue'
 import envColumns from '@/views/environment/envColumns'
 import { message } from 'ant-design-vue'
 
+const route = useRoute()
 const curd = ref()
 function loadFromSettings() {
   environment.load_from_settings().then(() => {
@@ -20,6 +21,18 @@ const refUpgrader = ref()
 function batchUpgrade() {
   refUpgrader.value.open(selectedNodeIds, selectedNodes)
 }
+
+const inTrash = computed(() => {
+  return route.query.trash === 'true'
+})
+
+// const timer = setInterval(() => {
+//   curd.value.get_list()
+// }, 10000)
+
+// onUnmounted(() => {
+//   clearInterval(timer)
+// })
 </script>
 
 <template>
@@ -43,7 +56,7 @@ function batchUpgrade() {
 
     <BatchUpgrader ref="refUpgrader" />
 
-    <FooterToolBar>
+    <FooterToolBar v-if="!inTrash">
       <ATooltip
         :title="$gettext('Please select at least one node to upgrade')"
         placement="topLeft"

+ 4 - 4
app/src/views/environment/envColumns.tsx

@@ -26,12 +26,12 @@ const columns: Column[] = [{
       placeholder: () => 'https://10.0.0.1:9000',
     },
   },
-  width: 300,
+  width: 260,
 }, {
   title: () => $gettext('Version'),
   dataIndex: 'version',
   pithy: true,
-  width: 150,
+  width: 120,
 }, {
   title: () => 'NodeSecret',
   dataIndex: 'token',
@@ -65,7 +65,7 @@ const columns: Column[] = [{
   },
   sorter: true,
   pithy: true,
-  width: 200,
+  width: 120,
 }, {
   title: () => $gettext('Enabled'),
   dataIndex: 'enabled',
@@ -85,7 +85,7 @@ const columns: Column[] = [{
   },
   sorter: true,
   pithy: true,
-  width: 150,
+  width: 120,
 }, {
   title: () => $gettext('Updated at'),
   dataIndex: 'updated_at',

+ 2 - 0
app/src/views/site/cert/Cert.vue

@@ -8,6 +8,7 @@ const props = defineProps<{
   configName: string
   currentServerIndex: number
   certInfo?: CertificateInfo[]
+  siteEnabled: boolean
 }>()
 
 const enabled = defineModel<boolean>('enabled', {
@@ -68,6 +69,7 @@ function handleCertChange(certs: Cert[]) {
     <ChangeCert @change="handleCertChange" />
 
     <IssueCert
+      v-if="siteEnabled"
       v-model:enabled="enabled"
       :config-name="configName"
     />

+ 9 - 9
app/src/views/site/ngx_conf/NgxServer.vue

@@ -120,15 +120,15 @@ provide('ngx_directives', ngx_directives)
         />
 
         <div class="tab-content">
-          <template v-if="current_support_ssl && enabled">
-            <Cert
-              v-if="current_support_ssl"
-              v-model:enabled="autoCert"
-              :config-name="ngx_config.name"
-              :cert-info="certInfo?.[k]"
-              :current-server-index="current_server_index"
-            />
-          </template>
+          <Cert
+            v-if="current_support_ssl"
+            v-model:enabled="autoCert"
+            class="mb-4"
+            :site-enabled="enabled"
+            :config-name="ngx_config.name"
+            :cert-info="certInfo?.[k]"
+            :current-server-index="current_server_index"
+          />
 
           <template v-if="v.comments">
             <h3>{{ $gettext('Comments') }}</h3>

+ 17 - 0
app/vite.config.ts

@@ -1,3 +1,4 @@
+import { Agent } from 'node:http'
 import { fileURLToPath, URL } from 'node:url'
 import vue from '@vitejs/plugin-vue'
 import vueJsx from '@vitejs/plugin-vue-jsx'
@@ -81,6 +82,22 @@ export default defineConfig(({ mode }) => {
           changeOrigin: true,
           secure: false,
           ws: true,
+          timeout: 5000,
+          agent: new Agent({
+            keepAlive: false,
+          }),
+          onProxyReq(proxyReq, req) {
+            proxyReq.setHeader('Connection', 'keep-alive')
+            if (req.headers.accept === 'text/event-stream') {
+              proxyReq.setHeader('Cache-Control', 'no-cache')
+              proxyReq.setHeader('Content-Type', 'text/event-stream')
+            }
+          },
+          onProxyReqWs(proxyReq, req, socket) {
+            socket.on('close', () => {
+              proxyReq.destroy()
+            })
+          },
         },
       },
     },

+ 31 - 30
docs/.vitepress/config/en.ts

@@ -1,12 +1,12 @@
-import {LocaleSpecificConfig, DefaultTheme} from 'vitepress'
-import {demoUrl} from './common'
+import { LocaleSpecificConfig, DefaultTheme } from 'vitepress'
+import { demoUrl } from './common'
 
 export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
   themeConfig: {
     nav: [
-      {text: 'Home', link: '/'},
-      {text: 'Guide', link: '/guide/about'},
-      {text: 'Demo', link: demoUrl}
+      { text: 'Home', link: '/' },
+      { text: 'Guide', link: '/guide/about' },
+      { text: 'Demo', link: demoUrl }
     ],
 
     sidebar: {
@@ -15,55 +15,56 @@ export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
           text: 'Introduction',
           collapsed: false,
           items: [
-            {text: 'What is Nginx UI?', link: '/guide/about'},
-            {text: 'Getting Started', link: '/guide/getting-started'},
-            {text: 'Install Script', link: '/guide/install-script-linux'}
+            { text: 'What is Nginx UI?', link: '/guide/about' },
+            { text: 'Getting Started', link: '/guide/getting-started' },
+            { text: 'Install Script', link: '/guide/install-script-linux' }
           ]
         },
         {
           text: 'Development',
           collapsed: false,
           items: [
-            {text: 'Build', link: '/guide/build'},
-            {text: 'Project Structure', link: '/guide/project-structure'},
-            {text: 'Config Template', link: '/guide/nginx-ui-template'},
-            {text: 'Contributing', link: '/guide/contributing'}
+            { text: 'Devcontainer', link: '/guide/devcontainer' },
+            { text: 'Build', link: '/guide/build' },
+            { text: 'Project Structure', link: '/guide/project-structure' },
+            { text: 'Config Template', link: '/guide/nginx-ui-template' },
+            { text: 'Contributing', link: '/guide/contributing' }
           ]
         },
         {
           text: 'Configuration',
           collapsed: false,
           items: [
-            {text: 'App', link: '/guide/config-app'},
-            {text: 'Server', link: '/guide/config-server'},
-            {text: 'Database', link: '/guide/config-database'},
-            {text: 'Auth', link: '/guide/config-auth'},
-            {text: 'Casdoor', link: '/guide/config-casdoor'},
-            {text: 'Cert', link: '/guide/config-cert'},
-            {text: 'Cluster', link: '/guide/config-cluster'},
-            {text: 'Crypto', link: '/guide/config-crypto'},
-            {text: 'Http', link: '/guide/config-http'},
-            {text: 'Logrotate', link: '/guide/config-logrotate'},
-            {text: 'Nginx', link: '/guide/config-nginx'},
-            {text: 'Node', link: '/guide/config-node'},
-            {text: 'Open AI', link: '/guide/config-openai'},
-            {text: 'Terminal', link: '/guide/config-terminal'},
-            {text: 'Webauthn', link: '/guide/config-webauthn'}
+            { text: 'App', link: '/guide/config-app' },
+            { text: 'Server', link: '/guide/config-server' },
+            { text: 'Database', link: '/guide/config-database' },
+            { text: 'Auth', link: '/guide/config-auth' },
+            { text: 'Casdoor', link: '/guide/config-casdoor' },
+            { text: 'Cert', link: '/guide/config-cert' },
+            { text: 'Cluster', link: '/guide/config-cluster' },
+            { text: 'Crypto', link: '/guide/config-crypto' },
+            { text: 'Http', link: '/guide/config-http' },
+            { text: 'Logrotate', link: '/guide/config-logrotate' },
+            { text: 'Nginx', link: '/guide/config-nginx' },
+            { text: 'Node', link: '/guide/config-node' },
+            { text: 'Open AI', link: '/guide/config-openai' },
+            { text: 'Terminal', link: '/guide/config-terminal' },
+            { text: 'Webauthn', link: '/guide/config-webauthn' }
           ]
         },
         {
           text: 'Environment Variables',
           collapsed: false,
           items: [
-            {text: 'Reference', link: '/guide/env'},
+            { text: 'Reference', link: '/guide/env' },
           ]
         },
         {
           text: 'Appendix',
           collapsed: false,
           items: [
-            {text: 'Nginx Proxy Example', link: '/guide/nginx-proxy-example'},
-            {text: 'License', link: '/guide/license'}
+            { text: 'Nginx Proxy Example', link: '/guide/nginx-proxy-example' },
+            { text: 'License', link: '/guide/license' }
           ]
         }
       ]

+ 9 - 3
docs/.vitepress/config/shared.ts

@@ -1,8 +1,8 @@
 import { defineConfig } from 'vitepress'
-import {projectUrl, editLinkPattern} from './common'
+import { projectUrl, editLinkPattern } from './common'
 
 export const commitRef = process.env.COMMIT_REF ?
-    `<a href="${projectUrl}/commit/${process.env.COMMIT_REF}">` + process.env.COMMIT_REF.slice(0, 8) + '</a>':
+    `<a href="${projectUrl}/commit/${process.env.COMMIT_REF}">` + process.env.COMMIT_REF.slice(0, 8) + '</a>' :
     'dev'
 
 function thisYear() {
@@ -37,7 +37,13 @@ export const sharedConfig = defineConfig({
         },
 
         socialLinks: [
-            {icon: 'github', link: projectUrl}
+            { icon: 'github', link: projectUrl }
         ]
+    },
+
+    vite: {
+        server: {
+            port: Number.parseInt(process.env.VITE_PORT ?? '3003')
+        }
     }
 })

+ 31 - 30
docs/.vitepress/config/zh_CN.ts

@@ -1,12 +1,12 @@
-import {LocaleSpecificConfig, DefaultTheme} from 'vitepress'
-import {demoUrl, editLinkPattern} from './common'
+import { LocaleSpecificConfig, DefaultTheme } from 'vitepress'
+import { demoUrl, editLinkPattern } from './common'
 
 export const zhCNConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
   themeConfig: {
     nav: [
-      {text: '首页', link: '/zh_CN/'},
-      {text: '手册', link: '/zh_CN/guide/about'},
-      {text: '演示', link: demoUrl}
+      { text: '首页', link: '/zh_CN/' },
+      { text: '手册', link: '/zh_CN/guide/about' },
+      { text: '演示', link: demoUrl }
     ],
 
     editLink: {
@@ -20,55 +20,56 @@ export const zhCNConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
           text: '介绍',
           collapsed: false,
           items: [
-            {text: '何为 Nginx UI?', link: '/zh_CN/guide/about'},
-            {text: '即刻开始', link: '/zh_CN/guide/getting-started'},
-            {text: '安装脚本', link: '/zh_CN/guide/install-script-linux'}
+            { text: '何为 Nginx UI?', link: '/zh_CN/guide/about' },
+            { text: '即刻开始', link: '/zh_CN/guide/getting-started' },
+            { text: '安装脚本', link: '/zh_CN/guide/install-script-linux' }
           ]
         },
         {
           text: '开发',
           collapsed: false,
           items: [
-            {text: '构建', link: '/zh_CN/guide/build'},
-            {text: '项目结构', link: '/zh_CN/guide/project-structure'},
-            {text: '配置模板', link: '/zh_CN/guide/nginx-ui-template'},
-            {text: '贡献代码', link: '/zh_CN/guide/contributing'}
+            { text: '开发容器', link: '/zh_CN/guide/devcontainer' },
+            { text: '构建', link: '/zh_CN/guide/build' },
+            { text: '项目结构', link: '/zh_CN/guide/project-structure' },
+            { text: '配置模板', link: '/zh_CN/guide/nginx-ui-template' },
+            { text: '贡献代码', link: '/zh_CN/guide/contributing' }
           ]
         },
         {
           text: '配置',
           collapsed: false,
           items: [
-            {text: 'App', link: '/zh_CN/guide/config-app'},
-            {text: 'Server', link: '/zh_CN/guide/config-server'},
-            {text: 'Database', link: '/zh_CN/guide/config-database'},
-            {text: 'Auth', link: '/zh_CN/guide/config-auth'},
-            {text: 'Casdoor', link: '/zh_CN/guide/config-casdoor'},
-            {text: 'Cert', link: '/zh_CN/guide/config-cert'},
-            {text: 'Cluster', link: '/zh_CN/guide/config-cluster'},
-            {text: 'Crypto', link: '/zh_CN/guide/config-crypto'},
-            {text: 'Http', link: '/zh_CN/guide/config-http'},
-            {text: 'Logrotate', link: '/zh_CN/guide/config-logrotate'},
-            {text: 'Nginx', link: '/zh_CN/guide/config-nginx'},
-            {text: 'Node', link: '/zh_CN/guide/config-node'},
-            {text: 'Open AI', link: '/zh_CN/guide/config-openai'},
-            {text: 'Terminal', link: '/zh_CN/guide/config-terminal'},
-            {text: 'Webauthn', link: '/zh_CN/guide/config-webauthn'}
+            { text: 'App', link: '/zh_CN/guide/config-app' },
+            { text: 'Server', link: '/zh_CN/guide/config-server' },
+            { text: 'Database', link: '/zh_CN/guide/config-database' },
+            { text: 'Auth', link: '/zh_CN/guide/config-auth' },
+            { text: 'Casdoor', link: '/zh_CN/guide/config-casdoor' },
+            { text: 'Cert', link: '/zh_CN/guide/config-cert' },
+            { text: 'Cluster', link: '/zh_CN/guide/config-cluster' },
+            { text: 'Crypto', link: '/zh_CN/guide/config-crypto' },
+            { text: 'Http', link: '/zh_CN/guide/config-http' },
+            { text: 'Logrotate', link: '/zh_CN/guide/config-logrotate' },
+            { text: 'Nginx', link: '/zh_CN/guide/config-nginx' },
+            { text: 'Node', link: '/zh_CN/guide/config-node' },
+            { text: 'Open AI', link: '/zh_CN/guide/config-openai' },
+            { text: 'Terminal', link: '/zh_CN/guide/config-terminal' },
+            { text: 'Webauthn', link: '/zh_CN/guide/config-webauthn' }
           ]
         },
         {
           text: '环境变量',
           collapsed: false,
           items: [
-            {text: '参考手册', link: '/zh_CN/guide/env'},
+            { text: '参考手册', link: '/zh_CN/guide/env' },
           ]
         },
         {
           text: '附录',
           collapsed: false,
           items: [
-            {text: 'Nginx 代理示例', link: '/zh_CN/guide/nginx-proxy-example'},
-            {text: '开源协议', link: '/zh_CN/guide/license'}
+            { text: 'Nginx 代理示例', link: '/zh_CN/guide/nginx-proxy-example' },
+            { text: '开源协议', link: '/zh_CN/guide/license' }
           ]
         }
       ]

+ 31 - 30
docs/.vitepress/config/zh_TW.ts

@@ -1,12 +1,12 @@
-import {LocaleSpecificConfig, DefaultTheme} from 'vitepress'
-import {demoUrl, editLinkPattern} from './common'
+import { LocaleSpecificConfig, DefaultTheme } from 'vitepress'
+import { demoUrl, editLinkPattern } from './common'
 
 export const zhTWConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
   themeConfig: {
     nav: [
-      {text: '首頁', link: '/zh_TW/'},
-      {text: '手冊', link: '/zh_TW/guide/about'},
-      {text: '演示', link: demoUrl}
+      { text: '首頁', link: '/zh_TW/' },
+      { text: '手冊', link: '/zh_TW/guide/about' },
+      { text: '演示', link: demoUrl }
     ],
 
     editLink: {
@@ -20,55 +20,56 @@ export const zhTWConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
           text: '介紹',
           collapsed: false,
           items: [
-            {text: '何為 Nginx UI?', link: '/zh_TW/guide/about'},
-            {text: '即刻開始', link: '/zh_TW/guide/getting-started'},
-            {text: '安裝指令碼', link: '/zh_TW/guide/install-script-linux'}
+            { text: '何為 Nginx UI?', link: '/zh_TW/guide/about' },
+            { text: '即刻開始', link: '/zh_TW/guide/getting-started' },
+            { text: '安裝指令碼', link: '/zh_TW/guide/install-script-linux' }
           ]
         },
         {
           text: '開發',
           collapsed: false,
           items: [
-            {text: '構建', link: '/zh_TW/guide/build'},
-            {text: '專案結構', link: '/zh_TW/guide/project-structure'},
-            {text: '配置模板', link: '/zh_TW/guide/nginx-ui-template'},
-            {text: '貢獻程式碼', link: '/zh_TW/guide/contributing'}
+            { text: '開發容器', link: '/zh_TW/guide/devcontainer' },
+            { text: '構建', link: '/zh_TW/guide/build' },
+            { text: '專案結構', link: '/zh_TW/guide/project-structure' },
+            { text: '配置模板', link: '/zh_TW/guide/nginx-ui-template' },
+            { text: '貢獻程式碼', link: '/zh_TW/guide/contributing' }
           ]
         },
         {
           text: '配置',
           collapsed: false,
           items: [
-            {text: 'App', link: '/zh_TW/guide/config-app'},
-            {text: 'Server', link: '/zh_TW/guide/config-server'},
-            {text: 'Database', link: '/zh_TW/guide/config-database'},
-            {text: 'Auth', link: '/zh_TW/guide/config-auth'},
-            {text: 'Casdoor', link: '/zh_TW/guide/config-casdoor'},
-            {text: 'Cert', link: '/zh_TW/guide/config-cert'},
-            {text: 'Cluster', link: '/zh_TW/guide/config-cluster'},
-            {text: 'Crypto', link: '/zh_TW/guide/config-crypto'},
-            {text: 'Http', link: '/zh_TW/guide/config-http'},
-            {text: 'Logrotate', link: '/zh_TW/guide/config-logrotate'},
-            {text: 'Nginx', link: '/zh_TW/guide/config-nginx'},
-            {text: 'Node', link: '/zh_TW/guide/config-node'},
-            {text: 'Open AI', link: '/zh_TW/guide/config-openai'},
-            {text: 'Terminal', link: '/zh_TW/guide/config-terminal'},
-            {text: 'Webauthn', link: '/zh_TW/guide/config-webauthn'}
+            { text: 'App', link: '/zh_TW/guide/config-app' },
+            { text: 'Server', link: '/zh_TW/guide/config-server' },
+            { text: 'Database', link: '/zh_TW/guide/config-database' },
+            { text: 'Auth', link: '/zh_TW/guide/config-auth' },
+            { text: 'Casdoor', link: '/zh_TW/guide/config-casdoor' },
+            { text: 'Cert', link: '/zh_TW/guide/config-cert' },
+            { text: 'Cluster', link: '/zh_TW/guide/config-cluster' },
+            { text: 'Crypto', link: '/zh_TW/guide/config-crypto' },
+            { text: 'Http', link: '/zh_TW/guide/config-http' },
+            { text: 'Logrotate', link: '/zh_TW/guide/config-logrotate' },
+            { text: 'Nginx', link: '/zh_TW/guide/config-nginx' },
+            { text: 'Node', link: '/zh_TW/guide/config-node' },
+            { text: 'Open AI', link: '/zh_TW/guide/config-openai' },
+            { text: 'Terminal', link: '/zh_TW/guide/config-terminal' },
+            { text: 'Webauthn', link: '/zh_TW/guide/config-webauthn' }
           ]
         },
         {
           text: '環境變量',
           collapsed: false,
           items: [
-            {text: '參考手冊', link: '/zh_TW/guide/env'},
+            { text: '參考手冊', link: '/zh_TW/guide/env' },
           ]
         },
         {
           text: '附錄',
           collapsed: false,
           items: [
-            {text: 'Nginx 代理示例', link: '/zh_TW/guide/nginx-proxy-example'},
-            {text: '開源協議', link: '/zh_TW/guide/license'}
+            { text: 'Nginx 代理示例', link: '/zh_TW/guide/nginx-proxy-example' },
+            { text: '開源協議', link: '/zh_TW/guide/license' }
           ]
         }
       ]

+ 5 - 1
docs/guide/about.md

@@ -74,11 +74,15 @@ Nginx UI is available on the following platforms:
 
 ## Internationalization
 
+We proudly offer official support for:
+
 - English
 - Simplified Chinese
 - Traditional Chinese
 
-We welcome translations into any language.
+As non-native English speakers, we strive for accuracy, but we know there’s always room for improvement. If you spot any issues, we’d love your feedback!
+
+Thanks to our amazing community, additional languages are also available! Explore and contribute to translations on [Weblate](https://weblate.nginxui.com).
 
 ## Built With
 

+ 50 - 0
docs/guide/devcontainer.md

@@ -0,0 +1,50 @@
+# Devcontainer
+
+You'll need to set up a development environment if you want to develop on this project.
+
+## Prerequisites
+
+- Docker
+- VSCode (Cursor)
+- Git
+
+## Setup
+
+1. Open the Command Palette in VSCode (Cursor)
+  - Mac: `Cmd`+`Shift`+`P`
+  - Windows: `Ctrl`+`Shift`+`P`
+2. Search for `Dev Containers: Rebuild and Reopen in Container` and click on it
+3. Wait for the container to start
+4. Open the Command Palette in VSCode (Cursor)
+  - Mac: `Cmd`+`Shift`+`P`
+  - Windows: `Ctrl`+`Shift`+`P`
+5. Select Tasks: Run Task -> Start all services
+6. Wait for the services to start
+
+## Ports
+
+| Port  | Service          |
+|-------|------------------|
+| 3002  | App              |
+| 3003  | Documentation    |
+| 9000  | API Backend      |
+
+
+## Services
+
+- nginx-ui
+- nginx-ui-2
+- casdoor
+- chaltestsrv
+- pebble
+
+## Multi-node development
+
+Add the following enviroment in the main node:
+
+```
+name: nginx-ui-2
+url: http://nginx-ui-2
+token: nginx-ui-2
+```
+

+ 4 - 4
docs/guide/install-script-linux.md

@@ -27,7 +27,7 @@ install.sh install [OPTIONS]
 ### Quick Usage
 
 ```shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) install
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install
 ```
 
 The default listening port is `9000`, and the default HTTP Challenge port is `9180`.
@@ -60,12 +60,12 @@ install.sh remove [OPTIONS]
 
 ```shell [Remove]
 # Remove Nginx UI, except configuration and database files
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) remove
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ remove
 ```
 
 ```shell [Purge]
 # Remove all the Nginx UI file, include configuration and database files
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) remove --purge
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ remove --purge
 ```
 
 :::
@@ -85,7 +85,7 @@ install.sh help
 ### Quick Usage
 
 ```shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) help
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ help
 ```
 
 ## Control Service

+ 3 - 3
docs/package.json

@@ -2,7 +2,7 @@
   "name": "nginx-ui-docs",
   "type": "module",
   "scripts": {
-    "docs:dev": "vitepress dev",
+    "docs:dev": "vitepress dev --host",
     "docs:build": "vitepress build",
     "docs:preview": "vitepress preview"
   },
@@ -11,9 +11,9 @@
     "vue": "^3.5.13"
   },
   "devDependencies": {
-    "@types/node": "^22.10.8",
+    "@types/node": "^22.13.1",
     "less": "^4.2.2"
   },
   "license": "AGPL-3.0",
-  "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
+  "packageManager": "pnpm@10.2.0+sha512.0d27364e0139c6aadeed65ada153135e0ca96c8da42123bd50047f961339dc7a758fc2e944b428f52be570d1bd3372455c1c65fa2e7aa0bfbf931190f9552001"
 }

+ 13 - 13
docs/pnpm-lock.yaml

@@ -10,14 +10,14 @@ importers:
     dependencies:
       vitepress:
         specifier: ^1.6.3
-        version: 1.6.3(@algolia/client-search@5.15.0)(@types/node@22.10.8)(less@4.2.2)(postcss@8.4.49)(search-insights@2.13.0)
+        version: 1.6.3(@algolia/client-search@5.15.0)(@types/node@22.13.1)(less@4.2.2)(postcss@8.4.49)(search-insights@2.13.0)
       vue:
         specifier: ^3.5.13
         version: 3.5.13
     devDependencies:
       '@types/node':
-        specifier: ^22.10.8
-        version: 22.10.8
+        specifier: ^22.13.1
+        version: 22.13.1
       less:
         specifier: ^4.2.2
         version: 4.2.2
@@ -415,8 +415,8 @@ packages:
   '@types/mdurl@2.0.0':
     resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
 
-  '@types/node@22.10.8':
-    resolution: {integrity: sha512-rk+QvAEGsbX/ZPiiyel6hJHNUS9cnSbPWVaZLvE+Er3tLqQFzWMz9JOfWW7XUmKvRPfxJfbl3qYWve+RGXncFw==}
+  '@types/node@22.13.1':
+    resolution: {integrity: sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==}
 
   '@types/unist@3.0.3':
     resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
@@ -1176,7 +1176,7 @@ snapshots:
 
   '@types/mdurl@2.0.0': {}
 
-  '@types/node@22.10.8':
+  '@types/node@22.13.1':
     dependencies:
       undici-types: 6.20.0
 
@@ -1186,9 +1186,9 @@ snapshots:
 
   '@ungap/structured-clone@1.2.0': {}
 
-  '@vitejs/plugin-vue@5.2.1(vite@5.4.14(@types/node@22.10.8)(less@4.2.2))(vue@3.5.13)':
+  '@vitejs/plugin-vue@5.2.1(vite@5.4.14(@types/node@22.13.1)(less@4.2.2))(vue@3.5.13)':
     dependencies:
-      vite: 5.4.14(@types/node@22.10.8)(less@4.2.2)
+      vite: 5.4.14(@types/node@22.13.1)(less@4.2.2)
       vue: 3.5.13
 
   '@vue/compiler-core@3.5.13':
@@ -1629,17 +1629,17 @@ snapshots:
       '@types/unist': 3.0.3
       vfile-message: 4.0.2
 
-  vite@5.4.14(@types/node@22.10.8)(less@4.2.2):
+  vite@5.4.14(@types/node@22.13.1)(less@4.2.2):
     dependencies:
       esbuild: 0.21.5
       postcss: 8.4.49
       rollup: 4.27.4
     optionalDependencies:
-      '@types/node': 22.10.8
+      '@types/node': 22.13.1
       fsevents: 2.3.3
       less: 4.2.2
 
-  vitepress@1.6.3(@algolia/client-search@5.15.0)(@types/node@22.10.8)(less@4.2.2)(postcss@8.4.49)(search-insights@2.13.0):
+  vitepress@1.6.3(@algolia/client-search@5.15.0)(@types/node@22.13.1)(less@4.2.2)(postcss@8.4.49)(search-insights@2.13.0):
     dependencies:
       '@docsearch/css': 3.8.2
       '@docsearch/js': 3.8.2(@algolia/client-search@5.15.0)(search-insights@2.13.0)
@@ -1648,7 +1648,7 @@ snapshots:
       '@shikijs/transformers': 2.1.0
       '@shikijs/types': 2.1.0
       '@types/markdown-it': 14.1.2
-      '@vitejs/plugin-vue': 5.2.1(vite@5.4.14(@types/node@22.10.8)(less@4.2.2))(vue@3.5.13)
+      '@vitejs/plugin-vue': 5.2.1(vite@5.4.14(@types/node@22.13.1)(less@4.2.2))(vue@3.5.13)
       '@vue/devtools-api': 7.7.0
       '@vue/shared': 3.5.13
       '@vueuse/core': 12.5.0
@@ -1657,7 +1657,7 @@ snapshots:
       mark.js: 8.11.1
       minisearch: 7.1.1
       shiki: 2.1.0
-      vite: 5.4.14(@types/node@22.10.8)(less@4.2.2)
+      vite: 5.4.14(@types/node@22.13.1)(less@4.2.2)
       vue: 3.5.13
     optionalDependencies:
       postcss: 8.4.49

+ 5 - 1
docs/zh_CN/guide/about.md

@@ -71,11 +71,15 @@ Nginx UI 可在以下平台中使用:
 
 ## 国际化
 
+我们官方支持以下语言:
+
 - 英语
 - 简体中文
 - 繁体中文
 
-我们欢迎您将项目翻译成任何语言。
+由于我们并非英语母语者,尽管已尽力确保准确性,但仍可能存在改进空间。如果您发现任何问题,欢迎向我们反馈!
+
+此外,感谢我们优秀的社区提供了更多语言,欢迎访问 [Weblate](https://weblate.nginxui.com) 进行查看和贡献翻译。
 
 ## 构建基于
 

+ 48 - 0
docs/zh_CN/guide/devcontainer.md

@@ -0,0 +1,48 @@
+# 开发容器
+
+如果您想参与本项目开发,需要设置开发环境。
+
+## 前提条件
+
+- Docker
+- VSCode (Cursor)
+- Git
+
+## 设置步骤
+
+1. 在 VSCode (Cursor) 中打开命令面板
+  - Mac: `Cmd`+`Shift`+`P`
+  - Windows: `Ctrl`+`Shift`+`P`
+2. 搜索 `Dev Containers: 重新生成并重新打开容器` 并点击
+3. 等待容器启动
+4. 再次打开命令面板
+  - Mac: `Cmd`+`Shift`+`P`
+  - Windows: `Ctrl`+`Shift`+`P`
+5. 选择 任务: 运行任务 -> 启动所有服务
+6. 等待所有服务启动完成
+
+## 端口映射
+
+| 端口   | 服务              |
+|-------|-------------------|
+| 3002  | 主应用            |
+| 3003  | 文档              |
+| 9000  | API 后端          |
+
+## 服务列表
+
+- nginx-ui
+- nginx-ui-2
+- casdoor
+- chaltestsrv
+- pebble
+
+## 多节点开发
+
+在主节点中添加以下环境配置:
+
+```
+name: nginx-ui-2
+url: http://nginx-ui-2
+token: nginx-ui-2
+```

+ 4 - 4
docs/zh_CN/guide/install-script-linux.md

@@ -26,7 +26,7 @@ install.sh install [OPTIONS]
 ### 快速使用
 
 ```shell
-bash <(curl -L -s https://mirror.ghproxy.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) install -r https://mirror.ghproxy.com/
+bash -c "$(curl -L https://mirror.ghproxy.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install -r https://mirror.ghproxy.com/
 ```
 
 一键安装脚本默认设置的监听端口为 `9000`,HTTP Challenge 端口默认为 `9180`。如果有端口冲突,请手动修改 `/usr/local/etc/nginx-ui/app.ini`,
@@ -56,12 +56,12 @@ install.sh remove [OPTIONS]
 
 ```shell [移除]
 # 删除 Nginx UI,但不包括配置和数据库文件
-bash <(curl -L -s https://mirror.ghproxy.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) remove
+bash -c "$(curl -L https://mirror.ghproxy.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ remove
 ```
 
 ```shell [清除]
 # 删除所有 Nginx UI 文件,包括配置和数据库文件
-bash <(curl -L -s https://mirror.ghproxy.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) remove --purge
+bash -c "$(curl -L https://mirror.ghproxy.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ remove --purge
 ```
 
 :::
@@ -81,7 +81,7 @@ install.sh help
 ### 快速使用
 
 ```shell
-bash <(curl -L -s https://mirror.ghproxy.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) help
+bash -c "$(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ help
 ```
 
 ## 控制服务

+ 7 - 3
docs/zh_TW/guide/about.md

@@ -71,11 +71,15 @@ Nginx UI 可在以下作業系統中使用:
 
 ## 國際化
 
-- 英語
+我們目前官方支援以下語言:
+
+- 英文
 - 簡體中文
-- 繁體中文
+- 正体中文
+
+由於我們並非英文母語者,儘管已盡力確保準確性,仍可能有改進的空間。若您發現任何問題,歡迎提供回饋!
 
-我們歡迎您將專案翻譯成任何語言。
+此外,感謝熱心的社群貢獻更多語言支援,歡迎前往 [Weblate](https://weblate.nginxui.com) 瀏覽並參與翻譯,共同打造更完善的多語言體驗!
 
 ## 構建基於
 

+ 48 - 0
docs/zh_TW/guide/devcontainer.md

@@ -0,0 +1,48 @@
+# 開發容器
+
+如果您想參與本專案開發,需要設定開發環境。
+
+## 必要條件
+
+- Docker
+- VSCode (Cursor)
+- Git
+
+## 設定步驟
+
+1. 在 VSCode (Cursor) 中開啟指令面板
+  - Mac: `Cmd`+`Shift`+`P`
+  - Windows: `Ctrl`+`Shift`+`P`
+2. 搜尋 `Dev Containers: 重新產生並重新開啟容器` 並點擊
+3. 等待容器啟動
+4. 再次開啟指令面板
+  - Mac: `Cmd`+`Shift`+`P`
+  - Windows: `Ctrl`+`Shift`+`P`
+5. 選擇 任務: 執行任務 -> 啟動所有服務
+6. 等待所有服務啟動完成
+
+## 連接埠映射
+
+| 連接埠 | 服務              |
+|-------|-------------------|
+| 3002  | 主應用            |
+| 3003  | 文件              |
+| 9000  | API 後端          |
+
+## 服務清單
+
+- nginx-ui
+- nginx-ui-2
+- casdoor
+- chaltestsrv
+- pebble
+
+## 多節點開發
+
+在主節點中新增以下環境設定:
+
+```
+name: nginx-ui-2
+url: http://nginx-ui-2
+token: nginx-ui-2
+```

+ 4 - 4
docs/zh_TW/guide/install-script-linux.md

@@ -26,7 +26,7 @@ install.sh install [OPTIONS]
 ### 快速使用
 
 ```shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) install
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install
 ```
 
 安裝指令碼預設的監聽埠為 `9000`,HTTP Challenge 埠預設為 `9180`。如果出現埠衝突請修改 `/usr/local/etc/nginx-ui/app.ini`,
@@ -56,12 +56,12 @@ install.sh remove [OPTIONS]
 
 ```shell [移除]
 # 解除安裝 Nginx UI 但保留配置和資料庫檔案
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) remove
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ remove
 ```
 
 ```shell [清除]
 # 解除安裝並刪除所有 Nginx UI 檔案,包括配置和資料庫檔案
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) remove --purge
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ remove --purge
 ```
 
 :::
@@ -81,7 +81,7 @@ install.sh help
 ### 快速使用
 
 ```shell
-bash <(curl -L -s https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh) help
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ help
 ```
 
 ## 控制服務

+ 2 - 2
go.mod

@@ -11,7 +11,6 @@ require (
 	github.com/dgraph-io/ristretto/v2 v2.1.0
 	github.com/dustin/go-humanize v1.0.1
 	github.com/elliotchance/orderedmap/v3 v3.1.0
-	github.com/fatih/color v1.18.0
 	github.com/gin-contrib/static v1.1.3
 	github.com/gin-gonic/gin v1.10.0
 	github.com/go-acme/lego/v4 v4.21.0
@@ -38,7 +37,6 @@ require (
 	github.com/uozi-tech/cosy v1.14.3
 	github.com/uozi-tech/cosy-driver-sqlite v0.2.0
 	github.com/urfave/cli/v3 v3.0.0-beta1
-	go.uber.org/zap v1.27.0
 	golang.org/x/crypto v0.32.0
 	golang.org/x/net v0.34.0
 	gopkg.in/ini.v1 v1.67.0
@@ -108,6 +106,7 @@ require (
 	github.com/dnsimple/dnsimple-go v1.7.0 // indirect
 	github.com/ebitengine/purego v0.8.2 // indirect
 	github.com/exoscale/egoscale/v3 v3.1.9 // indirect
+	github.com/fatih/color v1.18.0 // indirect
 	github.com/fatih/structs v1.1.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.8.0 // indirect
@@ -254,6 +253,7 @@ require (
 	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/ratelimit v0.3.1 // indirect
+	go.uber.org/zap v1.27.0 // indirect
 	golang.org/x/arch v0.13.0 // indirect
 	golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
 	golang.org/x/mod v0.22.0 // indirect

+ 15 - 3
internal/cert/cert_info.go

@@ -3,10 +3,11 @@ package cert
 import (
 	"crypto/x509"
 	"encoding/pem"
-	"github.com/0xJacky/Nginx-UI/internal/helper"
-	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"os"
 	"time"
+
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
 )
 
 type Info struct {
@@ -39,8 +40,19 @@ func GetCertInfo(sslCertificatePath string) (info *Info, err error) {
 		return
 	}
 
+	// for wildcard certificate, the subject name is the first DNS name
+	subjectName := cert.Subject.CommonName
+	if subjectName == "" {
+		for _, name := range cert.DNSNames {
+			if name != "" {
+				subjectName = name
+				break
+			}
+		}
+	}
+
 	info = &Info{
-		SubjectName: cert.Subject.CommonName,
+		SubjectName: subjectName,
 		IssuerName:  cert.Issuer.CommonName,
 		NotAfter:    cert.NotAfter,
 		NotBefore:   cert.NotBefore,

+ 7 - 6
internal/cert/write_file.go

@@ -1,10 +1,11 @@
 package cert
 
 import (
-	"github.com/0xJacky/Nginx-UI/internal/helper"
-	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"os"
 	"path/filepath"
+
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
 )
 
 type Content struct {
@@ -33,25 +34,25 @@ func (c *Content) WriteFile() (err error) {
 	// The permission bits perm (before umask) are used for all directories that MkdirAll creates.
 	// If path is already a directory, MkdirAll does nothing and returns nil.
 
-	err = os.MkdirAll(filepath.Dir(c.SSLCertificatePath), 0644)
+	err = os.MkdirAll(filepath.Dir(c.SSLCertificatePath), 0755)
 	if err != nil {
 		return
 	}
 
-	err = os.MkdirAll(filepath.Dir(c.SSLCertificateKeyPath), 0644)
+	err = os.MkdirAll(filepath.Dir(c.SSLCertificateKeyPath), 0755)
 	if err != nil {
 		return
 	}
 
 	if c.SSLCertificate != "" {
-		err = os.WriteFile(c.SSLCertificatePath, []byte(c.SSLCertificate), 0644)
+		err = os.WriteFile(c.SSLCertificatePath, []byte(c.SSLCertificate), 0755)
 		if err != nil {
 			return
 		}
 	}
 
 	if c.SSLCertificateKey != "" {
-		err = os.WriteFile(c.SSLCertificateKeyPath, []byte(c.SSLCertificateKey), 0644)
+		err = os.WriteFile(c.SSLCertificateKeyPath, []byte(c.SSLCertificateKey), 0755)
 		if err != nil {
 			return
 		}

+ 0 - 27
internal/logger/color.go

@@ -1,27 +0,0 @@
-package logger
-
-import (
-	"github.com/fatih/color"
-	"go.uber.org/zap/zapcore"
-)
-
-func colorLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
-	colorLevel := color.New()
-
-	switch l {
-	case zapcore.DebugLevel:
-		colorLevel.Add(color.FgCyan)
-	case zapcore.InfoLevel:
-		colorLevel.Add(color.FgGreen)
-	case zapcore.WarnLevel:
-		colorLevel.Add(color.FgYellow)
-	case zapcore.ErrorLevel, zapcore.DPanicLevel:
-		colorLevel.Add(color.FgHiRed)
-	case zapcore.PanicLevel, zapcore.FatalLevel:
-		colorLevel.Add(color.FgRed)
-	default:
-		colorLevel.Add(color.Reset)
-	}
-
-	enc.AppendString(colorLevel.Sprint(l.CapitalString()))
-}

+ 0 - 103
internal/logger/logger.go

@@ -1,103 +0,0 @@
-package logger
-
-import (
-	"github.com/0xJacky/Nginx-UI/settings"
-	"github.com/gin-gonic/gin"
-	"go.uber.org/zap"
-	"go.uber.org/zap/zapcore"
-	"os"
-)
-
-var logger *zap.SugaredLogger
-
-func init() {
-	// First, define our level-handling logic.
-	highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
-		return lvl >= zapcore.ErrorLevel
-	})
-	lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
-		switch settings.ServerSettings.RunMode {
-		case gin.ReleaseMode:
-			return lvl >= zapcore.InfoLevel && lvl < zapcore.ErrorLevel
-		default:
-			fallthrough
-		case gin.DebugMode:
-			return lvl < zapcore.ErrorLevel
-		}
-	})
-
-	// Directly output to stdout and stderr, and add caller information.
-	consoleDebugging := zapcore.Lock(os.Stdout)
-	consoleErrors := zapcore.Lock(os.Stderr)
-	encoderConfig := zap.NewDevelopmentEncoderConfig()
-	encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05")
-	encoderConfig.ConsoleSeparator = "\t"
-	encoderConfig.EncodeLevel = colorLevelEncoder
-	consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig)
-
-	// Join the outputs, encoders, and level-handling functions into
-	// zapcore.Cores, then tee the two cores together.
-	core := zapcore.NewTee(
-		zapcore.NewCore(consoleEncoder, consoleErrors, highPriority),
-		zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority),
-	)
-
-	// From a zapcore.Core, it's easy to construct a Logger.
-	logger = zap.New(core, zap.AddCaller()).WithOptions(zap.AddCallerSkip(1)).Sugar()
-}
-
-func Sync() {
-	_ = logger.Sync()
-}
-
-func GetLogger() *zap.SugaredLogger {
-	return logger
-}
-
-func Info(args ...interface{}) {
-	logger.Infoln(args...)
-}
-
-func Error(args ...interface{}) {
-	logger.Errorln(args...)
-}
-
-func Fatal(args ...interface{}) {
-	logger.Fatalln(args...)
-}
-
-func Warn(args ...interface{}) {
-	logger.Warnln(args...)
-}
-
-func Debug(args ...interface{}) {
-	logger.Debugln(args...)
-}
-
-func DPanic(args ...interface{}) {
-	logger.DPanic(args...)
-}
-
-func Infof(format string, args ...interface{}) {
-	logger.Infof(format, args...)
-}
-
-func Errorf(format string, args ...interface{}) {
-	logger.Errorf(format, args...)
-}
-
-func Fatalf(format string, args ...interface{}) {
-	logger.Fatalf(format, args...)
-}
-
-func DPanicf(format string, args ...interface{}) {
-	logger.DPanicf(format, args...)
-}
-
-func Warnf(format string, args ...interface{}) {
-	logger.Warnf(format, args...)
-}
-
-func Debugf(format string, args ...interface{}) {
-	logger.Debugf(format, args...)
-}

+ 29 - 0
internal/middleware/embed.go

@@ -0,0 +1,29 @@
+//go:build !unembed
+
+package middleware
+
+import (
+	"io/fs"
+	"net/http"
+	"path"
+
+	"github.com/0xJacky/Nginx-UI/app"
+	"github.com/gin-contrib/static"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+func MustFs(dir string) (serverFileSystem static.ServeFileSystem) {
+
+	sub, err := fs.Sub(app.DistFS, path.Join("dist", dir))
+
+	if err != nil {
+		logger.Error(err)
+		return
+	}
+
+	serverFileSystem = ServerFileSystemType{
+		http.FS(sub),
+	}
+
+	return
+}

+ 4 - 22
internal/middleware/middleware.go

@@ -2,16 +2,14 @@ package middleware
 
 import (
 	"encoding/base64"
-	"github.com/0xJacky/Nginx-UI/app"
+	"net/http"
+	"path"
+	"strings"
+
 	"github.com/0xJacky/Nginx-UI/internal/user"
 	"github.com/0xJacky/Nginx-UI/settings"
-	"github.com/gin-contrib/static"
 	"github.com/gin-gonic/gin"
 	"github.com/uozi-tech/cosy/logger"
-	"io/fs"
-	"net/http"
-	"path"
-	"strings"
 )
 
 func AuthRequired() gin.HandlerFunc {
@@ -72,22 +70,6 @@ func (f ServerFileSystemType) Exists(prefix string, _path string) bool {
 	return err == nil
 }
 
-func MustFs(dir string) (serverFileSystem static.ServeFileSystem) {
-
-	sub, err := fs.Sub(app.DistFS, path.Join("dist", dir))
-
-	if err != nil {
-		logger.Error(err)
-		return
-	}
-
-	serverFileSystem = ServerFileSystemType{
-		http.FS(sub),
-	}
-
-	return
-}
-
 func CacheJs() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		if strings.Contains(c.Request.URL.String(), "js") {

+ 1 - 1
model/environment.go

@@ -10,7 +10,7 @@ type Environment struct {
 	Name    string `json:"name"`
 	URL     string `json:"url"`
 	Token   string `json:"token"`
-	Enabled bool   `json:"enabled" gorm:"default:true"`
+	Enabled bool   `json:"enabled" gorm:"default:false"`
 }
 
 func (e *Environment) GetUrl(uri string) (decodedUri string, err error) {

+ 0 - 13
resources/development/entrypoint.sh

@@ -1,13 +0,0 @@
-#!/bin/bash
-
-if [ "$(ls -A /etc/nginx)" = "" ]; then
-    echo "Initialing Nginx config dir"
-    cp -rp /usr/etc/nginx/* /etc/nginx/
-    echo "Initialed Nginx config dir"
-fi
-
-echo "export PATH=$PATH:/usr/local/go/bin:$(go env GOPATH)/bin" >> ~/.profile
-source ~/.profile
-
-nginx
-cd /app && air

+ 0 - 9
resources/development/sources.list

@@ -1,9 +0,0 @@
-# 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释
-deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted universe multiverse
-# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted universe multiverse
-deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main restricted universe multiverse
-# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main restricted universe multiverse
-deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-backports main restricted universe multiverse
-# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-backports main restricted universe multiverse
-deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-security main restricted universe multiverse
-# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-security main restricted universe multiverse

+ 4 - 7
router/routers.go

@@ -1,6 +1,8 @@
 package router
 
 import (
+	"net/http"
+
 	"github.com/0xJacky/Nginx-UI/api/analytic"
 	"github.com/0xJacky/Nginx-UI/api/certificate"
 	"github.com/0xJacky/Nginx-UI/api/cluster"
@@ -19,19 +21,14 @@ import (
 	"github.com/0xJacky/Nginx-UI/api/upstream"
 	"github.com/0xJacky/Nginx-UI/api/user"
 	"github.com/0xJacky/Nginx-UI/internal/middleware"
-	"github.com/gin-contrib/static"
 	"github.com/gin-gonic/gin"
 	"github.com/uozi-tech/cosy"
-	"net/http"
 )
 
 func InitRouter() {
 	r := cosy.GetEngine()
-	r.Use(
-		middleware.CacheJs(),
-		middleware.IPWhiteList(),
-		static.Serve("/", middleware.MustFs("")),
-	)
+
+	initEmbedRoute(r)
 
 	r.NoRoute(func(c *gin.Context) {
 		c.JSON(http.StatusNotFound, gin.H{

+ 17 - 0
router/routers_embed.go

@@ -0,0 +1,17 @@
+//go:build !unembed
+
+package router
+
+import (
+	"github.com/0xJacky/Nginx-UI/internal/middleware"
+	"github.com/gin-contrib/static"
+	"github.com/gin-gonic/gin"
+)
+
+func initEmbedRoute(r *gin.Engine) {
+	r.Use(
+		middleware.CacheJs(),
+		middleware.IPWhiteList(),
+		static.Serve("/", middleware.MustFs("")),
+	)
+}

+ 8 - 0
router/routers_unembed.go

@@ -0,0 +1,8 @@
+//go:build unembed
+
+package router
+
+import "github.com/gin-gonic/gin"
+
+func initEmbedRoute(r *gin.Engine) {
+}