Browse Source

Merge branch 'dev' into v2.1

Jacky 1 month ago
parent
commit
d98b8d9c97

+ 2 - 0
api/system/self_check.go

@@ -9,6 +9,7 @@ import (
 
 	"time"
 
+	"github.com/0xJacky/Nginx-UI/api"
 	"github.com/0xJacky/Nginx-UI/internal/self_check"
 	"github.com/gin-gonic/gin"
 )
@@ -45,6 +46,7 @@ func CheckWebSocket(c *gin.Context) {
 }
 
 func CheckSSE(c *gin.Context) {
+	api.SetSSEHeaders(c)
 	notify := c.Writer.CloseNotify()
 	for i := 0; i < 10; i++ {
 		select {

+ 11 - 0
app/src/api/analytic.ts

@@ -59,12 +59,23 @@ export interface MemStat {
   pressure: number
 }
 
+export interface PartitionStat {
+  mountpoint: string
+  device: string
+  fstype: string
+  total: string
+  used: string
+  free: string
+  percentage: number
+}
+
 export interface DiskStat {
   total: string
   used: string
   percentage: number
   writes: Usage
   reads: Usage
+  partitions: PartitionStat[]
 }
 
 export interface LoadStat {

+ 4 - 6
app/src/composables/useSSE.ts

@@ -28,10 +28,6 @@ export function useSSE() {
    * Connect to SSE service
    */
   function connect(options: SSEOptions) {
-    if (!token.value) {
-      return
-    }
-
     const {
       url,
       onMessage,
@@ -42,8 +38,10 @@ export function useSSE() {
 
     const fullUrl = urlJoin(window.location.pathname, url)
 
-    const headers = {
-      Authorization: token.value,
+    const headers: Record<string, string> = {}
+
+    if (token.value) {
+      headers.Authorization = token.value
     }
 
     if (settings.environment.id) {

+ 39 - 39
app/src/language/generate.ts

@@ -1,52 +1,52 @@
 // This file is auto-generated. DO NOT EDIT MANUALLY.
 
 export const msg = [
-  $gettext('[Nginx UI] Issued certificate successfully'),
-  $gettext('[Nginx UI] Preparing lego configurations'),
-  $gettext('[Nginx UI] Writing certificate private key to disk'),
+  $gettext('Certificate not found: %{error}'),
+  $gettext('Certificate revoked successfully'),
+  $gettext('Check if /var/run/docker.sock exists. If you are using Nginx UI Official Docker Image, please make sure the docker socket is mounted like this: `-v /var/run/docker.sock:/var/run/docker.sock`. Nginx UI official image uses /var/run/docker.sock to communicate with the host Docker Engine via Docker Client API. This feature is used to control Nginx in another container and perform container replacement rather than binary replacement during OTA upgrades of Nginx UI to ensure container dependencies are also upgraded. If you don\'t need this feature, please add the environment variable NGINX_UI_IGNORE_DOCKER_SOCKET=true to the container.'),
+  $gettext('Check if the nginx PID path exists. By default, this path is obtained from \'nginx -V\'. If it cannot be obtained, an error will be reported. In this case, you need to modify the configuration file to specify the Nginx PID path.Refer to the docs for more details: https://nginxui.com/zh_CN/guide/config-nginx.html#pidpath'),
+  $gettext('Check if the nginx access log path exists. By default, this path is obtained from \'nginx -V\'. If it cannot be obtained or the obtained path does not point to a valid, existing file, an error will be reported. In this case, you need to modify the configuration file to specify the access log path.Refer to the docs for more details: https://nginxui.com/zh_CN/guide/config-nginx.html#accesslogpath'),
+  $gettext('Check if the nginx configuration directory exists'),
   $gettext('Check if the nginx configuration entry file exists'),
+  $gettext('Check if the nginx error log path exists. By default, this path is obtained from \'nginx -V\'. If it cannot be obtained or the obtained path does not point to a valid, existing file, an error will be reported. In this case, you need to modify the configuration file to specify the error log path.Refer to the docs for more details: https://nginxui.com/zh_CN/guide/config-nginx.html#errorlogpath'),
+  $gettext('Check if the nginx.conf includes the conf.d directory'),
+  $gettext('Check if the nginx.conf includes the sites-enabled directory'),
+  $gettext('Check if the nginx.conf includes the streams-enabled directory'),
+  $gettext('Check if the sites-available and sites-enabled directories are under the nginx configuration directory'),
   $gettext('Check if the streams-available and streams-enabled directories are under the nginx configuration directory'),
   $gettext('Docker socket exists'),
-  $gettext('Check if /var/run/docker.sock exists. If you are using Nginx UI Official Docker Image, please make sure the docker socket is mounted like this: `-v /var/run/docker.sock:/var/run/docker.sock`. Nginx UI official image uses /var/run/docker.sock to communicate with the host Docker Engine via Docker Client API. This feature is used to control Nginx in another container and perform container replacement rather than binary replacement during OTA upgrades of Nginx UI to ensure container dependencies are also upgraded. If you don\'t need this feature, please add the environment variable NGINX_UI_IGNORE_DOCKER_SOCKET=true to the container.'),
-  $gettext('[Nginx UI] Obtaining certificate'),
-  $gettext('[Nginx UI] Writing certificate to disk'),
-  $gettext('Sites directory exists'),
-  $gettext('Nginx.conf includes conf.d directory'),
-  $gettext('Log file %{log_path} is not a regular file. If you are using nginx-ui in docker container, please refer to https://nginxui.com/zh_CN/guide/config-nginx-log.html for more information.'),
-  $gettext('[Nginx UI] Creating client facilitates communication with the CA server'),
-  $gettext('[Nginx UI] Reloading nginx'),
-  $gettext('Streams directory exists'),
-  $gettext('Nginx.conf includes streams-enabled directory'),
+  $gettext('Failed to delete certificate from database: %{error}'),
   $gettext('Failed to revoke certificate: %{error}'),
-  $gettext('[Nginx UI] Setting DNS01 challenge provider'),
-  $gettext('[Nginx UI] Environment variables cleaned'),
-  $gettext('[Nginx UI] Backing up current certificate for later revocation'),
-  $gettext('[Nginx UI] Finished'),
-  $gettext('Check if the sites-available and sites-enabled directories are under the nginx configuration directory'),
-  $gettext('Nginx configuration directory exists'),
-  $gettext('Check if the nginx configuration directory exists'),
-  $gettext('[Nginx UI] ACME User: %{name}, Email: %{email}, CA Dir: %{caDir}'),
-  $gettext('[Nginx UI] Setting environment variables'),
-  $gettext('[Nginx UI] Certificate was used for server, reloading server TLS certificate'),
+  $gettext('Log file %{log_path} is not a regular file. If you are using nginx-ui in docker container, please refer to https://nginxui.com/zh_CN/guide/config-nginx-log.html for more information.'),
+  $gettext('Nginx PID path exists'),
   $gettext('Nginx access log path exists'),
-  $gettext('Check if the nginx access log path exists. By default, this path is obtained from \'nginx -V\'. If it cannot be obtained or the obtained path does not point to a valid, existing file, an error will be reported. In this case, you need to modify the configuration file to specify the access log path.Refer to the docs for more details: https://nginxui.com/zh_CN/guide/config-nginx.html#accesslogpath'),
+  $gettext('Nginx configuration directory exists'),
+  $gettext('Nginx configuration entry file exists'),
   $gettext('Nginx error log path exists'),
-  $gettext('Check if the nginx.conf includes the streams-enabled directory'),
-  $gettext('Failed to delete certificate from database: %{error}'),
-  $gettext('[Nginx UI] Setting HTTP01 challenge provider'),
-  $gettext('[Nginx UI] Revoking old certificate'),
-  $gettext('[Nginx UI] Preparing for certificate revocation'),
-  $gettext('[Nginx UI] Revoking certificate'),
-  $gettext('[Nginx UI] Certificate successfully revoked'),
+  $gettext('Nginx.conf includes conf.d directory'),
   $gettext('Nginx.conf includes sites-enabled directory'),
-  $gettext('Check if the nginx.conf includes the conf.d directory'),
-  $gettext('Certificate not found: %{error}'),
-  $gettext('Certificate revoked successfully'),
+  $gettext('Nginx.conf includes streams-enabled directory'),
+  $gettext('Sites directory exists'),
+  $gettext('Streams directory exists'),
+  $gettext('[Nginx UI] ACME User: %{name}, Email: %{email}, CA Dir: %{caDir}'),
+  $gettext('[Nginx UI] Backing up current certificate for later revocation'),
   $gettext('[Nginx UI] Certificate renewed successfully'),
+  $gettext('[Nginx UI] Certificate successfully revoked'),
+  $gettext('[Nginx UI] Certificate was used for server, reloading server TLS certificate'),
+  $gettext('[Nginx UI] Creating client facilitates communication with the CA server'),
+  $gettext('[Nginx UI] Environment variables cleaned'),
+  $gettext('[Nginx UI] Finished'),
+  $gettext('[Nginx UI] Issued certificate successfully'),
+  $gettext('[Nginx UI] Obtaining certificate'),
+  $gettext('[Nginx UI] Preparing for certificate revocation'),
+  $gettext('[Nginx UI] Preparing lego configurations'),
+  $gettext('[Nginx UI] Reloading nginx'),
   $gettext('[Nginx UI] Revocation completed'),
-  $gettext('Nginx configuration entry file exists'),
-  $gettext('Nginx PID path exists'),
-  $gettext('Check if the nginx PID path exists. By default, this path is obtained from \'nginx -V\'. If it cannot be obtained, an error will be reported. In this case, you need to modify the configuration file to specify the Nginx PID path.Refer to the docs for more details: https://nginxui.com/zh_CN/guide/config-nginx.html#pidpath'),
-  $gettext('Check if the nginx error log path exists. By default, this path is obtained from \'nginx -V\'. If it cannot be obtained or the obtained path does not point to a valid, existing file, an error will be reported. In this case, you need to modify the configuration file to specify the error log path.Refer to the docs for more details: https://nginxui.com/zh_CN/guide/config-nginx.html#errorlogpath'),
-  $gettext('Check if the nginx.conf includes the sites-enabled directory'),
+  $gettext('[Nginx UI] Revoking certificate'),
+  $gettext('[Nginx UI] Revoking old certificate'),
+  $gettext('[Nginx UI] Setting DNS01 challenge provider'),
+  $gettext('[Nginx UI] Setting HTTP01 challenge provider'),
+  $gettext('[Nginx UI] Setting environment variables'),
+  $gettext('[Nginx UI] Writing certificate private key to disk'),
+  $gettext('[Nginx UI] Writing certificate to disk'),
 ]

+ 2 - 2
app/src/views/install/components/InstallView.vue

@@ -12,7 +12,7 @@ const installTimeout = ref(false)
 const activeTab = ref('1')
 const step = ref(1)
 const selfCheckStore = useSelfCheckStore()
-const { hasError } = storeToRefs(selfCheckStore)
+const { hasError, loading } = storeToRefs(selfCheckStore)
 
 const router = useRouter()
 
@@ -51,7 +51,7 @@ function handleRestoreSuccess(options: { restoreNginx: boolean, restoreNginxUI:
 }
 
 const canProceed = computed(() => {
-  return !installTimeout.value && !hasError.value
+  return !installTimeout.value && !hasError.value && !loading.value
 })
 
 const steps = computed(() => {

+ 9 - 1
cmd/translation/gettext.go

@@ -8,6 +8,7 @@ import (
 	"path/filepath"
 	"regexp"
 	"runtime"
+	"sort"
 	"strings"
 
 	"github.com/uozi-tech/cosy/logger"
@@ -241,8 +242,15 @@ func generateSingleTSFile(root string, calls map[string]bool) {
 	writer.WriteString("// This file is auto-generated. DO NOT EDIT MANUALLY.\n\n")
 	writer.WriteString("export const msg = [\n")
 
-	// Write each translation message
+	// Extract and sort the translation messages to ensure stable output
+	var messages []string
 	for message := range calls {
+		messages = append(messages, message)
+	}
+	sort.Strings(messages)
+
+	// Write each translation message in sorted order
+	for _, message := range messages {
 		// Escape single quotes and handle newlines in the message for JavaScript
 		escapedMessage := strings.ReplaceAll(message, "'", "\\'")
 		// Replace newlines with space to ensure proper formatting in the generated TS file

+ 24 - 1
docs/guide/install-script-linux.md

@@ -22,14 +22,37 @@ install.sh install [OPTIONS]
 | `-l, --local <file>`  | Install Nginx UI from a local file (`string`)                                                                   |
 | `-p, --proxy <url>`   | Download through a proxy server (`string`)<br/>e.g., `-p http://127.0.0.1:8118` or `-p socks5://127.0.0.1:1080` |
 | `-r, --reverse-proxy` | Download through a reverse proxy server (`string`)<br/>e.g., `-r https://cloud.nginxui.com/`                          |
+| `-c, --channel <channel>` | Specify the version channel (`string`)<br/>Available channels: `stable` (default), `prerelease`, `dev`
 
+#### Version Channels
+
+| Channel      | Description                                                                                          |
+|--------------|------------------------------------------------------------------------------------------------------|
+| `stable`     | Latest stable release (default) - Recommended for production use                                     |
+| `prerelease` | Latest prerelease version - Contains new features that are being tested before stable release       |
+| `dev`        | Latest development build from dev branch - Contains the newest features but may be unstable         |
 
 ### Quick Usage
 
-```shell
+::: code-group
+
+```shell [Stable (Default)]
+# Install the latest stable version
 bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install
 ```
 
+```shell [Prerelease]
+# Install the latest prerelease version
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install --channel prerelease
+```
+
+```shell [Development]
+# Install the latest development build
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install --channel dev
+```
+
+:::
+
 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,
 then use `systemctl restart nginx-ui` to restart the Nginx UI service.

+ 25 - 1
docs/zh_CN/guide/install-script-linux.md

@@ -21,13 +21,37 @@ install.sh install [OPTIONS]
 | `-l, --local <file>`  | 从本地文件安装 Nginx UI (`string`)                                                           |
 | `-p, --proxy <url>`   | 通过代理服务器下载 (`string`)<br/>例如:`-p http://127.0.0.1:8118` 或 `-p socks5://127.0.0.1:1080` |
 | `-r, --reverse-proxy` | 通过反向代理服务器下载 (`string`)<br/>例如:`-r https://cloud.nginxui.com/`                               |
+| `-c, --channel <channel>` | 指定版本通道 (`string`)<br/>可用通道:`stable`(默认)、`prerelease`、`dev`
+
+#### 版本通道
+
+| 通道         | 描述                                                      |
+|------------|-----------------------------------------------------------|
+| `stable`   | 最新稳定版本(默认) - 推荐用于生产环境                                |
+| `prerelease` | 最新预发布版本 - 包含正在测试的新功能,将在稳定版本发布前进行验证                |
+| `dev`      | 来自 dev 分支的最新开发构建 - 包含最新功能但可能不稳定                   |
 
 ### 快速使用
 
-```shell
+::: code-group
+
+```shell [稳定版(默认)]
+# 安装最新稳定版本
 bash -c "$(curl -L https://cloud.nginxui.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install -r https://cloud.nginxui.com/
 ```
 
+```shell [预发布版]
+# 安装最新预发布版本
+bash -c "$(curl -L https://cloud.nginxui.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install --channel prerelease -r https://cloud.nginxui.com/
+```
+
+```shell [开发版]
+# 安装最新开发构建
+bash -c "$(curl -L https://cloud.nginxui.com/https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install --channel dev -r https://cloud.nginxui.com/
+```
+
+:::
+
 一键安装脚本默认设置的监听端口为 `9000`,HTTP Challenge 端口默认为 `9180`。如果有端口冲突,请手动修改 `/usr/local/etc/nginx-ui/app.ini`,
 并使用 `systemctl restart nginx-ui` 重启 Nginx UI 服务。更多有关信息,请查看 [配置参考](./config-server)。
 

+ 25 - 2
docs/zh_TW/guide/install-script-linux.md

@@ -21,14 +21,37 @@ install.sh install [OPTIONS]
 | `-l, --local <file>`  | 從本機檔案安裝 Nginx UI (`string`)                                                           |
 | `-p, --proxy <url>`   | 透過代理伺服器下載 (`string`)<br/>例如:`-p http://127.0.0.1:8118` 或 `-p socks5://127.0.0.1:1080` |
 | `-r, --reverse-proxy` | 透過反向代理伺服器下載 (`string`)<br/>例如:`-r https://cloud.nginxui.com/`                               |
+| `-c, --channel <channel>` | 指定版本通道 (`string`)<br/>可用通道:`stable`(預設)、`prerelease`、`dev`
 
+#### 版本通道
+
+| 通道         | 說明                                                      |
+|------------|-----------------------------------------------------------|
+| `stable`   | 最新穩定版本(預設) - 建議用於正式環境                                |
+| `prerelease` | 最新預發布版本 - 包含正在測試的新功能,將在穩定版本發布前進行驗證                |
+| `dev`      | 來自 dev 分支的最新開發構建 - 包含最新功能但可能不穩定                   |
 
 ### 快速使用
 
-```shell
+::: code-group
+
+```shell [穩定版(預設)]
+# 安裝最新穩定版本
 bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install
 ```
 
+```shell [預發布版]
+# 安裝最新預發布版本
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install --channel prerelease
+```
+
+```shell [開發版]
+# 安裝最新開發構建
+bash -c "$(curl -L https://raw.githubusercontent.com/0xJacky/nginx-ui/main/install.sh)" @ install --channel dev
+```
+
+:::
+
 安裝指令碼預設的監聽連接埠為 `9000`,HTTP Challenge 連接埠預設為 `9180`。如果出現連接埠衝突請修改 `/usr/local/etc/nginx-ui/app.ini`,
 並使用 `systemctl restart nginx-ui` 重啟 Nginx UI 守護行程。更多有關資訊,請檢視 [設定參考](./config-server)。
 
@@ -166,4 +189,4 @@ rc-update add nginx-ui default
 /etc/init.d/nginx-ui status
 ```
 
-:::
+:::

+ 149 - 30
install.sh

@@ -17,6 +17,9 @@ SERVICE_TYPE=''
 # Latest release version
 RELEASE_LATEST=''
 
+# Version channel (stable, prerelease, dev)
+VERSION_CHANNEL='stable'
+
 # install
 INSTALL='0'
 
@@ -92,6 +95,18 @@ judgment_parameters() {
             PROXY="$2"
             shift
             ;;
+        '-c' | '--channel')
+            if [[ -z "$2" ]]; then
+                echo -e "${FontRed}error: Please specify the version channel (stable, prerelease, dev).${FontSuffix}"
+                exit 1
+            fi
+            if [[ "$2" != "stable" && "$2" != "prerelease" && "$2" != "dev" ]]; then
+                echo -e "${FontRed}error: Invalid channel. Must be one of: stable, prerelease, dev.${FontSuffix}"
+                exit 1
+            fi
+            VERSION_CHANNEL="$2"
+            shift
+            ;;
         '--purge')
             PURGE='1'
             ;;
@@ -231,12 +246,43 @@ install_software() {
 get_latest_version() {
     # Get latest release version number
     local latest_release
-    if ! latest_release=$(curl_with_retry -sS -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/0xJacky/nginx-ui/releases/latest"); then
-        echo -e "${FontRed}error: Failed to get release list, please check your network.${FontSuffix}"
-        exit 1
+    if [[ "$VERSION_CHANNEL" == "stable" ]]; then
+        if ! latest_release=$(curl_with_retry -sS -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/0xJacky/nginx-ui/releases/latest"); then
+            echo -e "${FontRed}error: Failed to get release list, please check your network.${FontSuffix}"
+            exit 1
+        fi
+        RELEASE_LATEST="$(echo "$latest_release" | sed 'y/,/\n/' | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')"
+    elif [[ "$VERSION_CHANNEL" == "prerelease" ]]; then
+        if ! latest_release=$(curl_with_retry -sS -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/0xJacky/nginx-ui/releases"); then
+            echo -e "${FontRed}error: Failed to get release list, please check your network.${FontSuffix}"
+            exit 1
+        fi
+        # Find the latest prerelease version
+        RELEASE_LATEST="$(echo "$latest_release" | sed 'y/,/\n/' | grep -B5 -A5 '"prerelease": true' | grep '"tag_name":' | head -1 | sed -E 's/.*"([^"]+)".*/\1/')"
+        if [[ -z "$RELEASE_LATEST" ]]; then
+            echo -e "${FontYellow}warning: No prerelease version found, falling back to stable version.${FontSuffix}"
+            # Fallback to stable release
+            if ! latest_release=$(curl_with_retry -sS -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/0xJacky/nginx-ui/releases/latest"); then
+                echo -e "${FontRed}error: Failed to get release list, please check your network.${FontSuffix}"
+                exit 1
+            fi
+            RELEASE_LATEST="$(echo "$latest_release" | sed 'y/,/\n/' | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')"
+        fi
+    elif [[ "$VERSION_CHANNEL" == "dev" ]]; then
+        # Get latest dev commit info
+        local dev_commit
+        if ! dev_commit=$(curl_with_retry -sS -H "Accept: application/vnd.github.v3+json" "${RPROXY}https://api.github.com/repos/0xJacky/nginx-ui/commits/dev?per_page=1"); then
+            echo -e "${FontRed}error: Failed to get dev commit info, please check your network.${FontSuffix}"
+            exit 1
+        fi
+        local commit_sha="$(echo "$dev_commit" | sed 'y/,/\n/' | grep '"sha":' | head -1 | sed -E 's/.*"([^"]+)".*/\1/')"
+        if [[ -z "$commit_sha" ]]; then
+            echo -e "${FontRed}error: Failed to get dev commit SHA.${FontSuffix}"
+            exit 1
+        fi
+        RELEASE_LATEST="sha-${commit_sha:0:7}"
     fi
 
-    RELEASE_LATEST="$(echo "$latest_release" | sed 'y/,/\n/' | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')"
     if [[ -z "$RELEASE_LATEST" ]]; then
         if echo "$latest_release" | grep -q "API rate limit exceeded"; then
             echo -e "${FontRed}error: github API rate limit exceeded${FontSuffix}"
@@ -251,7 +297,13 @@ get_latest_version() {
 
 download_nginx_ui() {
     local download_link
-    download_link="${RPROXY}https://github.com/0xJacky/nginx-ui/releases/download/$RELEASE_LATEST/nginx-ui-linux-$MACHINE.tar.gz"
+    if [[ "$VERSION_CHANNEL" == "dev" ]]; then
+        # For dev builds, use the CloudflareWorkerAPI dev-builds endpoint
+        download_link="${RPROXY}https://cloud.nginxui.com/dev-builds/nginx-ui-linux-$MACHINE.tar.gz"
+    else
+        # For stable and prerelease versions
+        download_link="${RPROXY}https://github.com/0xJacky/nginx-ui/releases/download/$RELEASE_LATEST/nginx-ui-linux-$MACHINE.tar.gz"
+    fi
 
     echo "Downloading Nginx UI archive: $download_link"
     if ! curl_with_retry -R -H 'Cache-Control: no-cache' -L -o "$TAR_FILE" "$download_link"; then
@@ -415,6 +467,73 @@ start_nginx_ui() {
     fi
 }
 
+check_nginx_ui_status() {
+    if [[ "$SERVICE_TYPE" == "systemd" ]]; then
+        if systemctl list-unit-files | grep -qw 'nginx-ui'; then
+            if systemctl -q is-active nginx-ui; then
+                return 0  # running
+            else
+                return 1  # not running
+            fi
+        else
+            return 2  # not installed
+        fi
+    elif [[ "$SERVICE_TYPE" == "openrc" ]]; then
+        if [[ -f "$OpenRCPath" ]]; then
+            if rc-service nginx-ui status | grep -q "started"; then
+                return 0  # running
+            else
+                return 1  # not running
+            fi
+        else
+            return 2  # not installed
+        fi
+    else
+        # init.d
+        if [[ -f "$InitPath" ]]; then
+            if $InitPath status >/dev/null 2>&1; then
+                return 0  # running
+            else
+                return 1  # not running
+            fi
+        else
+            return 2  # not installed
+        fi
+    fi
+}
+
+restart_nginx_ui() {
+    if [[ "$SERVICE_TYPE" == "systemd" ]]; then
+        systemctl restart nginx-ui
+        sleep 1s
+        if systemctl -q is-active nginx-ui; then
+            echo 'info: Restart the Nginx UI service.'
+        else
+            echo -e "${FontRed}error: Failed to restart the Nginx UI service.${FontSuffix}"
+            exit 1
+        fi
+    elif [[ "$SERVICE_TYPE" == "openrc" ]]; then
+        rc-service nginx-ui restart
+        sleep 1s
+        if rc-service nginx-ui status | grep -q "started"; then
+            echo 'info: Restart the Nginx UI service.'
+        else
+            echo -e "${FontRed}error: Failed to restart the Nginx UI service.${FontSuffix}"
+            exit 1
+        fi
+    else
+        # init.d
+        $InitPath restart
+        sleep 1s
+        if $InitPath status >/dev/null 2>&1; then
+            echo 'info: Restart the Nginx UI service.'
+        else
+            echo -e "${FontRed}error: Failed to restart the Nginx UI service.${FontSuffix}"
+            exit 1
+        fi
+    fi
+}
+
 stop_nginx_ui() {
     if [[ "$SERVICE_TYPE" == "systemd" ]]; then
         if ! systemctl stop nginx-ui; then
@@ -544,6 +663,10 @@ show_help() {
     echo '    -l, --local               Install Nginx UI from a local file'
     echo '    -p, --proxy               Download through a proxy server, e.g., -p http://127.0.0.1:8118 or -p socks5://127.0.0.1:1080'
     echo '    -r, --reverse-proxy       Download through a reverse proxy server, e.g., -r https://cloud.nginxui.com/'
+    echo '    -c, --channel             Specify the version channel (stable, prerelease, dev)'
+    echo '                              stable: Latest stable release (default)'
+    echo '                              prerelease: Latest prerelease version'
+    echo '                              dev: Latest development build from dev branch'
     echo '  remove:'
     echo '    --purge                   Remove all the Nginx UI files, include logs, configs, etc'
     exit 0
@@ -574,7 +697,7 @@ main() {
         decompression "$LOCAL_FILE"
     else
         get_latest_version
-        echo "info: Installing Nginx UI $RELEASE_LATEST for $(uname -m)"
+        echo "info: Installing Nginx UI $RELEASE_LATEST ($VERSION_CHANNEL channel) for $(uname -m)"
         if ! download_nginx_ui; then
             "rm" -r "$TMP_DIRECTORY"
             echo "removed: $TMP_DIRECTORY"
@@ -583,25 +706,6 @@ main() {
         decompression "$TAR_FILE"
     fi
 
-    # Determine if nginx-ui is running
-    NGINX_UI_RUNNING='0'
-    if [[ "$SERVICE_TYPE" == "systemd" && $(systemctl list-unit-files | grep -qw 'nginx-ui') ]]; then
-        if [[ -n "$(pidof nginx-ui)" ]]; then
-            stop_nginx_ui
-            NGINX_UI_RUNNING='1'
-        fi
-    elif [[ "$SERVICE_TYPE" == "openrc" && -f "$OpenRCPath" ]]; then
-        if rc-service nginx-ui status | grep -q "started"; then
-            stop_nginx_ui
-            NGINX_UI_RUNNING='1'
-        fi
-    elif [[ "$SERVICE_TYPE" == "initd" && -f "$InitPath" ]]; then
-        if [[ -n "$(pidof nginx-ui)" ]]; then
-            stop_nginx_ui
-            NGINX_UI_RUNNING='1'
-        fi
-    fi
-
     install_bin
     echo 'installed: /usr/local/bin/nginx-ui'
 
@@ -620,14 +724,31 @@ main() {
 
     install_config
 
-    if [[ "$NGINX_UI_RUNNING" -eq '1' ]]; then
+    # Check nginx-ui service status and decide whether to start or restart
+    check_nginx_ui_status
+    service_status=$?
+    
+    if [[ $service_status -eq 0 ]]; then
+        # Service is running, restart it
+        echo "info: Nginx UI service is running, restarting..."
+        restart_nginx_ui
+    elif [[ $service_status -eq 1 ]]; then
+        # Service is installed but not running, start it
+        echo "info: Nginx UI service is not running, starting..."
         start_nginx_ui
+        # Enable service for auto-start
+        if [[ "$SERVICE_TYPE" == "systemd" ]]; then
+            systemctl enable nginx-ui
+        elif [[ "$SERVICE_TYPE" == "openrc" ]]; then
+            rc-update add nginx-ui default
+        fi
     else
+        # Service is not installed, start it and enable
+        echo "info: Installing and starting Nginx UI service..."
         if [[ "$SERVICE_TYPE" == "systemd" ]]; then
             systemctl start nginx-ui
             systemctl enable nginx-ui
             sleep 1s
-
             if systemctl -q is-active nginx-ui; then
                 echo "info: Start and enable the Nginx UI service."
             else
@@ -637,8 +758,7 @@ main() {
             rc-service nginx-ui start
             rc-update add nginx-ui default
             sleep 1s
-
-            if rc-service nginx-ui status | grep -q "running"; then
+            if rc-service nginx-ui status | grep -q "started"; then
                 echo "info: Started and added the Nginx UI service to default runlevel."
             else
                 echo -e "${FontYellow}warning: Failed to start the Nginx UI service.${FontSuffix}"
@@ -646,7 +766,6 @@ main() {
         elif [[ "$SERVICE_TYPE" == "initd" ]]; then
             $InitPath start
             sleep 1s
-
             if $InitPath status >/dev/null 2>&1; then
                 echo "info: Started the Nginx UI service."
             else

+ 94 - 12
internal/analytic/stat.go

@@ -2,12 +2,13 @@ package analytic
 
 import (
 	"fmt"
+	"math"
+
 	"github.com/dustin/go-humanize"
 	"github.com/pkg/errors"
 	"github.com/shirou/gopsutil/v4/disk"
 	"github.com/shirou/gopsutil/v4/mem"
 	"github.com/spf13/cast"
-	"math"
 )
 
 type MemStat struct {
@@ -22,12 +23,23 @@ type MemStat struct {
 	Pressure    float64 `json:"pressure"`
 }
 
+type PartitionStat struct {
+	Mountpoint string  `json:"mountpoint"`
+	Device     string  `json:"device"`
+	Fstype     string  `json:"fstype"`
+	Total      string  `json:"total"`
+	Used       string  `json:"used"`
+	Free       string  `json:"free"`
+	Percentage float64 `json:"percentage"`
+}
+
 type DiskStat struct {
-	Total      string        `json:"total"`
-	Used       string        `json:"used"`
-	Percentage float64       `json:"percentage"`
-	Writes     Usage[uint64] `json:"writes"`
-	Reads      Usage[uint64] `json:"reads"`
+	Total      string          `json:"total"`
+	Used       string          `json:"used"`
+	Percentage float64         `json:"percentage"`
+	Writes     Usage[uint64]   `json:"writes"`
+	Reads      Usage[uint64]   `json:"reads"`
+	Partitions []PartitionStat `json:"partitions"`
 }
 
 func GetMemoryStat() (MemStat, error) {
@@ -50,17 +62,87 @@ func GetMemoryStat() (MemStat, error) {
 }
 
 func GetDiskStat() (DiskStat, error) {
-	diskUsage, err := disk.Usage(".")
-
+	// Get all partitions
+	partitions, err := disk.Partitions(false)
 	if err != nil {
-		return DiskStat{}, errors.Wrap(err, "error analytic getDiskStat")
+		return DiskStat{}, errors.Wrap(err, "error analytic getDiskStat - getting partitions")
+	}
+
+	var totalSize uint64
+	var totalUsed uint64
+	var partitionStats []PartitionStat
+
+	// Get usage for each partition
+	for _, partition := range partitions {
+		usage, err := disk.Usage(partition.Mountpoint)
+		if err != nil {
+			// Skip partitions that can't be accessed
+			continue
+		}
+
+		// Skip virtual filesystems and special filesystems
+		if isVirtualFilesystem(partition.Fstype) {
+			continue
+		}
+
+		partitionStat := PartitionStat{
+			Mountpoint: partition.Mountpoint,
+			Device:     partition.Device,
+			Fstype:     partition.Fstype,
+			Total:      humanize.Bytes(usage.Total),
+			Used:       humanize.Bytes(usage.Used),
+			Free:       humanize.Bytes(usage.Free),
+			Percentage: cast.ToFloat64(fmt.Sprintf("%.2f", usage.UsedPercent)),
+		}
+
+		partitionStats = append(partitionStats, partitionStat)
+		totalSize += usage.Total
+		totalUsed += usage.Used
+	}
+
+	// Calculate overall percentage
+	var overallPercentage float64
+	if totalSize > 0 {
+		overallPercentage = cast.ToFloat64(fmt.Sprintf("%.2f", float64(totalUsed)/float64(totalSize)*100))
 	}
 
 	return DiskStat{
-		Used:       humanize.Bytes(diskUsage.Used),
-		Total:      humanize.Bytes(diskUsage.Total),
-		Percentage: cast.ToFloat64(fmt.Sprintf("%.2f", diskUsage.UsedPercent)),
+		Used:       humanize.Bytes(totalUsed),
+		Total:      humanize.Bytes(totalSize),
+		Percentage: overallPercentage,
 		Writes:     DiskWriteRecord[len(DiskWriteRecord)-1],
 		Reads:      DiskReadRecord[len(DiskReadRecord)-1],
+		Partitions: partitionStats,
 	}, nil
 }
+
+// isVirtualFilesystem checks if the filesystem type is virtual
+func isVirtualFilesystem(fstype string) bool {
+	virtualFSTypes := map[string]bool{
+		"proc":        true,
+		"sysfs":       true,
+		"devfs":       true,
+		"devpts":      true,
+		"tmpfs":       true,
+		"debugfs":     true,
+		"securityfs":  true,
+		"cgroup":      true,
+		"cgroup2":     true,
+		"pstore":      true,
+		"bpf":         true,
+		"tracefs":     true,
+		"hugetlbfs":   true,
+		"mqueue":      true,
+		"overlay":     true,
+		"autofs":      true,
+		"binfmt_misc": true,
+		"configfs":    true,
+		"fusectl":     true,
+		"rpc_pipefs":  true,
+		"selinuxfs":   true,
+		"systemd-1":   true,
+		"none":        true,
+	}
+
+	return virtualFSTypes[fstype]
+}

+ 49 - 0
internal/analytic/stat_test.go

@@ -0,0 +1,49 @@
+package analytic
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetDiskStat(t *testing.T) {
+	diskStat, err := GetDiskStat()
+
+	// Test that the function doesn't return an error
+	assert.NoError(t, err)
+
+	// Test that partitions are populated
+	assert.NotEmpty(t, diskStat.Partitions)
+
+	// Test that overall stats are calculated
+	assert.NotEmpty(t, diskStat.Total)
+	assert.NotEmpty(t, diskStat.Used)
+	assert.GreaterOrEqual(t, diskStat.Percentage, 0.0)
+	assert.LessOrEqual(t, diskStat.Percentage, 100.0)
+
+	// Test each partition has required fields
+	for _, partition := range diskStat.Partitions {
+		assert.NotEmpty(t, partition.Mountpoint)
+		assert.NotEmpty(t, partition.Device)
+		assert.NotEmpty(t, partition.Fstype)
+		assert.NotEmpty(t, partition.Total)
+		assert.NotEmpty(t, partition.Used)
+		assert.NotEmpty(t, partition.Free)
+		assert.GreaterOrEqual(t, partition.Percentage, 0.0)
+		assert.LessOrEqual(t, partition.Percentage, 100.0)
+	}
+}
+
+func TestIsVirtualFilesystem(t *testing.T) {
+	// Test virtual filesystems
+	assert.True(t, isVirtualFilesystem("proc"))
+	assert.True(t, isVirtualFilesystem("sysfs"))
+	assert.True(t, isVirtualFilesystem("tmpfs"))
+	assert.True(t, isVirtualFilesystem("devpts"))
+
+	// Test real filesystems
+	assert.False(t, isVirtualFilesystem("ext4"))
+	assert.False(t, isVirtualFilesystem("xfs"))
+	assert.False(t, isVirtualFilesystem("ntfs"))
+	assert.False(t, isVirtualFilesystem("fat32"))
+}