Kaynağa Gözat

feat(nginx_log): add advanced indexing settings

0xJacky 7 ay önce
ebeveyn
işleme
9aa5a260b6
67 değiştirilmiş dosya ile 6108 ekleme ve 2499 silme
  1. 1 15
      README.md
  2. 66 11
      api/event/websocket.go
  3. 104 0
      api/license/license.go
  4. 48 0
      api/nginx_log/advanced_indexing_settings.go
  5. 3 0
      api/nginx_log/router.go
  6. 3 0
      app.example.ini
  7. 3 0
      app/components.d.ts
  8. 37 0
      app/src/api/license.ts
  9. 13 0
      app/src/api/nginx_log.ts
  10. 5 0
      app/src/api/settings.ts
  11. 30 13
      app/src/components/TabFilter/TabFilter.vue
  12. 0 12
      app/src/constants/errors/backup.ts
  13. 3 0
      app/src/constants/errors/nginx_log.ts
  14. 270 157
      app/src/language/ar/app.po
  15. 255 163
      app/src/language/de_DE/app.po
  16. 268 97
      app/src/language/en/app.po
  17. 262 151
      app/src/language/es/app.po
  18. 258 161
      app/src/language/fr_FR/app.po
  19. 248 200
      app/src/language/ja_JP/app.po
  20. 251 179
      app/src/language/ko_KR/app.po
  21. 266 96
      app/src/language/messages.pot
  22. 252 153
      app/src/language/pt_PT/app.po
  23. 256 152
      app/src/language/ru_RU/app.po
  24. 257 160
      app/src/language/tr_TR/app.po
  25. 267 166
      app/src/language/uk_UA/app.po
  26. 259 149
      app/src/language/vi_VN/app.po
  27. 252 161
      app/src/language/zh_CN/app.po
  28. 253 163
      app/src/language/zh_TW/app.po
  29. 8 0
      app/src/routes/modules/system.ts
  30. 85 6
      app/src/views/nginx_log/NginxLogList.vue
  31. 274 0
      app/src/views/nginx_log/components/IndexingSettingsModal.vue
  32. 3 3
      app/src/views/preference/components/ExternalNotify/ntfy.ts
  33. 3 0
      app/src/views/preference/store/index.ts
  34. 9 8
      app/src/views/system/About.vue
  35. 292 0
      app/src/views/system/Licenses.vue
  36. 718 0
      cmd/generate_licenses/main.go
  37. 0 0
      cmd/map_generator/main.go
  38. 1 0
      docs/.vitepress/config/en.ts
  39. 1 0
      docs/.vitepress/config/zh_CN.ts
  40. 1 0
      docs/.vitepress/config/zh_TW.ts
  41. 0 13
      docs/guide/about.md
  42. 73 0
      docs/guide/config-nginx-log.md
  43. 5 0
      docs/guide/env.md
  44. 0 13
      docs/zh_CN/guide/about.md
  45. 73 0
      docs/zh_CN/guide/config-nginx-log.md
  46. 6 0
      docs/zh_CN/guide/env.md
  47. 73 0
      docs/zh_TW/guide/config-nginx-log.md
  48. 6 0
      docs/zh_TW/guide/env.md
  49. 52 0
      internal/event/websocket.go
  50. 1 0
      internal/kernel/boot.go
  51. 95 0
      internal/license/license.go
  52. BIN
      internal/license/licenses.xz
  53. 8 3
      internal/nginx_log/indexer/parallel_indexer.go
  54. 5 0
      internal/nginx_log/indexer/rebuild_test.go
  55. 1 0
      internal/nginx_log/indexer/types.go
  56. 73 7
      internal/nginx_log/task_recovery.go
  57. 21 1
      query/nginx_log_indices.gen.go
  58. 3 0
      resources/demo/app.ini
  59. 1 15
      resources/readme/README-es.md
  60. 1 15
      resources/readme/README-ja_JP.md
  61. 1 15
      resources/readme/README-vi_VN.md
  62. 1 14
      resources/readme/README-zh_CN.md
  63. 1 15
      resources/readme/README-zh_TW.md
  64. 2 0
      router/routers.go
  65. 12 12
      settings/nginx.go
  66. 7 0
      settings/nginx_log.go
  67. 2 0
      settings/settings.go

+ 1 - 15
README.md

@@ -45,7 +45,7 @@ Your support helps us:
 
 [![Stargazers over time](https://starchart.cc/0xJacky/nginx-ui.svg)](https://starchart.cc/0xJacky/nginx-ui)
 
-English | [Español](README-es.md) | [简体中文](README-zh_CN.md) | [繁體中文](README-zh_TW.md) | [Tiếng Việt](README-vi_VN.md) | [日本語](README-ja_JP.md)
+English | [Español](resources/readme/README-es.md) | [简体中文](resources/readme/README-zh_CN.md) | [繁體中文](resources/readme/README-zh_TW.md) | [Tiếng Việt](resources/readme/README-vi_VN.md) | [日本語](resources/readme/README-ja_JP.md)
 
 <details>
   <summary>Table of Contents</summary>
@@ -56,7 +56,6 @@ English | [Español](README-es.md) | [简体中文](README-zh_CN.md) | [繁體
         <li><a href="#demo">Demo</a></li>
         <li><a href="#features">Features</a></li>
         <li><a href="#internationalization">Internationalization</a></li>
-        <li><a href="#built-with">Built With</a></li>
       </ul>
     </li>
     <li>
@@ -133,19 +132,6 @@ As non-native English speakers, we strive for accuracy, but we know there's alwa
 
 Thanks to our amazing community, additional languages are also available! Explore and contribute to translations on [Weblate](https://weblate.nginxui.com).
 
-### Built With
-
-- [The Go Programming Language](https://go.dev)
-- [Gin Web Framework](https://gin-gonic.com)
-- [GORM](http://gorm.io)
-- [Vue 3](https://v3.vuejs.org)
-- [Vite](https://vitejs.dev)
-- [TypeScript](https://www.typescriptlang.org/)
-- [Ant Design Vue](https://antdv.com)
-- [vue3-gettext](https://github.com/jshmrtn/vue3-gettext)
-- [vue3-ace-editor](https://github.com/CarterLi/vue3-ace-editor)
-- [Gonginx](https://github.com/tufanbarisyildirim/gonginx)
-- [lego](https://github.com/go-acme/lego)
 
 ## Getting Started
 

+ 66 - 11
api/event/websocket.go

@@ -27,7 +27,6 @@ type Client struct {
 	send   chan WebSocketMessage
 	ctx    context.Context
 	cancel context.CancelFunc
-	mutex  sync.RWMutex
 }
 
 // Hub maintains the set of active clients and broadcasts messages to them
@@ -37,6 +36,7 @@ type Hub struct {
 	register   chan *Client
 	unregister chan *Client
 	mutex      sync.RWMutex
+	ctx        context.Context
 }
 
 var (
@@ -52,6 +52,7 @@ func GetHub() *Hub {
 			broadcast:  make(chan WebSocketMessage, 1024), // Increased buffer size
 			register:   make(chan *Client),
 			unregister: make(chan *Client),
+			ctx:        event.GetWebSocketContext(),
 		}
 		go hub.run()
 
@@ -106,6 +107,26 @@ func (h *Hub) run() {
 				}
 			}
 			h.mutex.RUnlock()
+
+		case <-h.ctx.Done():
+			logger.Info("Hub context cancelled, shutting down WebSocket hub")
+			h.mutex.Lock()
+			for client := range h.clients {
+				close(client.send)
+				delete(h.clients, client)
+			}
+			h.mutex.Unlock()
+			return
+
+		case <-kernel.Context.Done():
+			logger.Debug("Kernel context cancelled, closing WebSocket hub")
+			h.mutex.Lock()
+			for client := range h.clients {
+				close(client.send)
+				delete(h.clients, client)
+			}
+			h.mutex.Unlock()
+			return
 		}
 	}
 }
@@ -139,7 +160,20 @@ func Bus(c *gin.Context) {
 	}
 
 	hub := GetHub()
-	hub.register <- client
+	
+	// Safely register the client with timeout to prevent blocking
+	select {
+	case hub.register <- client:
+		// Successfully registered
+	case <-time.After(1 * time.Second):
+		// Timeout - hub might be shutting down
+		logger.Warn("Failed to register client - hub may be shutting down")
+		return
+	case <-kernel.Context.Done():
+		// Kernel context cancelled
+		logger.Debug("Kernel context cancelled during client registration")
+		return
+	}
 
 	// Broadcast current processing status to the new client
 	go func() {
@@ -196,8 +230,17 @@ func (c *Client) writePump() {
 // readPump pumps messages from the websocket connection to the hub
 func (c *Client) readPump() {
 	defer func() {
+		// Safely unregister the client with timeout to prevent blocking
 		hub := GetHub()
-		hub.unregister <- c
+		select {
+		case hub.unregister <- c:
+			// Successfully unregistered
+		case <-time.After(1 * time.Second):
+			// Timeout - hub might be shutting down
+			logger.Warn("Failed to unregister client - hub may be shutting down")
+		}
+		
+		// Always close the connection and cancel context
 		c.conn.Close()
 		c.cancel()
 	}()
@@ -210,15 +253,27 @@ func (c *Client) readPump() {
 	})
 
 	for {
-		var msg json.RawMessage
-		err := c.conn.ReadJSON(&msg)
-		if err != nil {
-			if helper.IsUnexpectedWebsocketError(err) {
-				logger.Error("Unexpected WebSocket error:", err)
+		select {
+		case <-c.ctx.Done():
+			// Context cancelled, exit gracefully
+			return
+		case <-kernel.Context.Done():
+			// Kernel context cancelled, exit gracefully
+			return
+		default:
+			// Set a short read deadline to check context regularly
+			c.conn.SetReadDeadline(time.Now().Add(5 * time.Second))
+			
+			var msg json.RawMessage
+			err := c.conn.ReadJSON(&msg)
+			if err != nil {
+				if helper.IsUnexpectedWebsocketError(err) {
+					logger.Error("Unexpected WebSocket error:", err)
+				}
+				return
 			}
-			break
+			// Handle incoming messages if needed
+			// For now, this is a one-way communication (server to client)
 		}
-		// Handle incoming messages if needed
-		// For now, this is a one-way communication (server to client)
 	}
 }

+ 104 - 0
api/license/license.go

@@ -0,0 +1,104 @@
+package license
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+	"github.com/uozi-tech/cosy"
+
+	"github.com/0xJacky/Nginx-UI/internal/license"
+)
+
+type Controller struct{}
+
+func InitRouter(r *gin.RouterGroup) {
+	c := NewController()
+
+	licenseGroup := r.Group("/licenses")
+	{
+		licenseGroup.GET("", c.GetLicenses)
+		licenseGroup.GET("/backend", c.GetBackendLicenses)
+		licenseGroup.GET("/frontend", c.GetFrontendLicenses)
+		licenseGroup.GET("/stats", c.GetLicenseStats)
+	}
+}
+
+func NewController() *Controller {
+	return &Controller{}
+}
+
+// GetLicenses godoc
+// @Summary Get all open source component licenses
+// @Description Returns license information for all backend and frontend components
+// @Tags License
+// @Accept json
+// @Produce json
+// @Success 200 {object} license.ComponentInfo "License information"
+// @Failure 500 {object} cosy.HTTPError "Internal Server Error"
+// @Router /api/licenses [get]
+func (c *Controller) GetLicenses(ctx *gin.Context) {
+	info, err := license.GetLicenseInfo()
+	if err != nil {
+		cosy.ErrHandler(ctx, err)
+		return
+	}
+
+	ctx.JSON(http.StatusOK, info)
+}
+
+// GetBackendLicenses godoc
+// @Summary Get backend component licenses
+// @Description Returns license information for backend Go modules
+// @Tags License
+// @Accept json
+// @Produce json
+// @Success 200 {array} license.License "Backend license information"
+// @Failure 500 {object} cosy.HTTPError "Internal Server Error"
+// @Router /api/licenses/backend [get]
+func (c *Controller) GetBackendLicenses(ctx *gin.Context) {
+	licenses, err := license.GetBackendLicenses()
+	if err != nil {
+		cosy.ErrHandler(ctx, err)
+		return
+	}
+
+	ctx.JSON(http.StatusOK, licenses)
+}
+
+// GetFrontendLicenses godoc
+// @Summary Get frontend component licenses
+// @Description Returns license information for frontend npm packages
+// @Tags License
+// @Accept json
+// @Produce json
+// @Success 200 {array} license.License "Frontend license information"
+// @Failure 500 {object} cosy.HTTPError "Internal Server Error"
+// @Router /api/licenses/frontend [get]
+func (c *Controller) GetFrontendLicenses(ctx *gin.Context) {
+	licenses, err := license.GetFrontendLicenses()
+	if err != nil {
+		cosy.ErrHandler(ctx, err)
+		return
+	}
+
+	ctx.JSON(http.StatusOK, licenses)
+}
+
+// GetLicenseStats godoc
+// @Summary Get license statistics
+// @Description Returns statistics about the distribution of licenses
+// @Tags License
+// @Accept json
+// @Produce json
+// @Success 200 {object} map[string]interface{} "License statistics"
+// @Failure 500 {object} cosy.HTTPError "Internal Server Error"
+// @Router /api/licenses/stats [get]
+func (c *Controller) GetLicenseStats(ctx *gin.Context) {
+	stats, err := license.GetLicenseStats()
+	if err != nil {
+		cosy.ErrHandler(ctx, err)
+		return
+	}
+
+	ctx.JSON(http.StatusOK, stats)
+}

+ 48 - 0
api/nginx_log/advanced_indexing_settings.go

@@ -0,0 +1,48 @@
+package nginx_log
+
+import (
+	"net/http"
+
+	"github.com/0xJacky/Nginx-UI/settings"
+	"github.com/gin-gonic/gin"
+	"github.com/uozi-tech/cosy"
+)
+
+// EnableAdvancedIndexing enables advanced indexing for nginx logs
+func EnableAdvancedIndexing(c *gin.Context) {
+	settings.NginxLogSettings.AdvancedIndexingEnabled = true
+
+	err := settings.Save()
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "Advanced indexing enabled successfully",
+	})
+}
+
+// DisableAdvancedIndexing disables advanced indexing for nginx logs
+func DisableAdvancedIndexing(c *gin.Context) {
+	settings.NginxLogSettings.AdvancedIndexingEnabled = false
+
+	err := settings.Save()
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "Advanced indexing disabled successfully",
+	})
+}
+
+// GetAdvancedIndexingStatus returns the current status of advanced indexing
+func GetAdvancedIndexingStatus(c *gin.Context) {
+	enabled := settings.NginxLogSettings.AdvancedIndexingEnabled
+
+	c.JSON(http.StatusOK, gin.H{
+		"enabled": enabled,
+	})
+}

+ 3 - 0
api/nginx_log/router.go

@@ -16,4 +16,7 @@ func InitRouter(r *gin.RouterGroup) {
 	r.POST("nginx_log/geo/china", GetChinaMapData)
 	r.POST("nginx_log/geo/stats", GetGeoStats)
 	r.POST("nginx_log/index/rebuild", RebuildIndex)
+	r.POST("nginx_log/settings/advanced_indexing/enable", EnableAdvancedIndexing)
+	r.POST("nginx_log/settings/advanced_indexing/disable", DisableAdvancedIndexing)
+	r.GET("nginx_log/settings/advanced_indexing/status", GetAdvancedIndexingStatus)
 }

+ 3 - 0
app.example.ini

@@ -62,6 +62,9 @@ TestConfigCmd   =
 ReloadCmd       = nginx -s reload
 RestartCmd      = start-stop-daemon --start --quiet --pidfile /var/run/nginx.pid --exec /usr/sbin/nginx
 
+[nginx_log]
+AdvancedIndexingEnabled = false
+
 [node]
 Name             = Local
 Secret           =

+ 3 - 0
app/components.d.ts

@@ -44,6 +44,7 @@ declare module 'vue' {
     AMenu: typeof import('ant-design-vue/es')['Menu']
     AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
     AModal: typeof import('ant-design-vue/es')['Modal']
+    APageHeader: typeof import('ant-design-vue/es')['PageHeader']
     APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
     APopover: typeof import('ant-design-vue/es')['Popover']
     AProgress: typeof import('ant-design-vue/es')['Progress']
@@ -69,6 +70,8 @@ declare module 'vue' {
     ATag: typeof import('ant-design-vue/es')['Tag']
     ATextarea: typeof import('ant-design-vue/es')['Textarea']
     ATooltip: typeof import('ant-design-vue/es')['Tooltip']
+    ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
+    ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
     AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
     AutoCertFormAutoCertForm: typeof import('./src/components/AutoCertForm/AutoCertForm.vue')['default']
     AutoCertFormDNSChallenge: typeof import('./src/components/AutoCertForm/DNSChallenge.vue')['default']

+ 37 - 0
app/src/api/license.ts

@@ -0,0 +1,37 @@
+import { http } from '@uozi-admin/request'
+
+export interface License {
+  name: string
+  license: string
+  url: string
+  version: string
+}
+
+export interface ComponentInfo {
+  backend: License[]
+  frontend: License[]
+}
+
+export interface LicenseStats {
+  total_backend: number
+  total_frontend: number
+  total: number
+  license_distribution: Record<string, number>
+}
+
+const license = {
+  getAll(): Promise<ComponentInfo> {
+    return http.get('/licenses')
+  },
+  getBackend(): Promise<License[]> {
+    return http.get('/licenses/backend')
+  },
+  getFrontend(): Promise<License[]> {
+    return http.get('/licenses/frontend')
+  },
+  getStats(): Promise<LicenseStats> {
+    return http.get('/licenses/stats')
+  },
+}
+
+export default license

+ 13 - 0
app/src/api/nginx_log.ts

@@ -349,6 +349,19 @@ const nginx_log = extendCurdApi(useCurdApi('/nginx_logs'), {
   getGeoStats(data: AnalyticsRequest): Promise<{ stats: GeoStats[] }> {
     return http.post('/nginx_log/geo/stats', data)
   },
+
+  // Advanced indexing settings APIs
+  enableAdvancedIndexing(): Promise<{ message: string }> {
+    return http.post('/nginx_log/settings/advanced_indexing/enable')
+  },
+
+  disableAdvancedIndexing(): Promise<{ message: string }> {
+    return http.post('/nginx_log/settings/advanced_indexing/disable')
+  },
+
+  getAdvancedIndexingStatus(): Promise<{ enabled: boolean }> {
+    return http.get('/nginx_log/settings/advanced_indexing/status')
+  },
 })
 
 export default nginx_log

+ 5 - 0
app/src/api/settings.ts

@@ -70,6 +70,10 @@ export interface NginxSettings {
   container_name: string
 }
 
+export interface NginxLogSettings {
+  advanced_indexing_enabled: boolean
+}
+
 export interface NodeSettings {
   name: string
   secret: string
@@ -115,6 +119,7 @@ export interface Settings {
   http: HTTPSettings
   logrotate: LogrotateSettings
   nginx: NginxSettings
+  nginx_log: NginxLogSettings
   node: NodeSettings
   openai: OpenaiSettings
   terminal: TerminalSettings

+ 30 - 13
app/src/components/TabFilter/TabFilter.vue

@@ -1,5 +1,7 @@
 <script setup lang="ts">
 import type { Key } from 'ant-design-vue/es/_util/type'
+import { storeToRefs } from 'pinia'
+import { useSettingsStore } from '@/pinia'
 
 export interface TabOption {
   key: string
@@ -29,6 +31,11 @@ const props = withDefaults(defineProps<Props>(), {
 
 const emit = defineEmits<Emits>()
 
+const settings = useSettingsStore()
+const { theme } = storeToRefs(settings)
+
+const isDarkMode = computed(() => theme.value === 'dark')
+
 const currentActiveKey = computed({
   get: () => props.activeKey,
   set: value => emit('update:activeKey', value),
@@ -44,7 +51,7 @@ function handleTabChange(key: Key) {
 <template>
   <ATabs
     :active-key="currentActiveKey"
-    class="tab-filter mb-4"
+    class="tab-filter mb-4" :class="[{ 'tab-filter-dark': isDarkMode }]"
     :size="size"
     @change="handleTabChange"
   >
@@ -123,7 +130,7 @@ function handleTabChange(key: Key) {
 
 /* Active Tab State */
 .tab-filter :deep(.ant-tabs-tab.ant-tabs-tab-active) {
-  background: var(--white);
+  background: transparent;
   border-bottom: 2px solid var(--primary-color);
 }
 
@@ -177,19 +184,29 @@ function handleTabChange(key: Key) {
 }
 
 /* Dark Mode Support */
-@media (prefers-color-scheme: dark) {
-  .tab-filter {
-    --border-color: #303030;
-    --white: #1f1f1f;
-  }
+.tab-filter-dark {
+  --border-color: #303030;
+  --white: #1f1f1f;
+}
 
-  .tab-filter :deep(.ant-tabs-nav) {
-    border-bottom-color: var(--border-color);
-  }
+.tab-filter-dark :deep(.ant-tabs-nav) {
+  border-bottom-color: var(--border-color);
+}
 
-  .tab-filter :deep(.ant-tabs-tab.ant-tabs-tab-active) {
-    background: var(--white);
-  }
+.tab-filter-dark :deep(.ant-tabs-tab.ant-tabs-tab-active) {
+  background: transparent;
+}
+
+.tab-filter-dark :deep(.ant-tabs-tab) .tab-content {
+  color: #ffffff;
+}
+
+.tab-filter-dark :deep(.ant-tabs-tab.ant-tabs-tab-active) .tab-content {
+  color: #ffffff;
+}
+
+.tab-filter-dark :deep(.ant-tabs-tab.ant-tabs-tab-disabled) .tab-content {
+  color: #666666;
 }
 
 /* Responsive Design */

+ 0 - 12
app/src/constants/errors/backup.ts

@@ -13,7 +13,6 @@ export default {
   4014: () => $gettext('Failed to cleanup temporary directory: {0}'),
   4101: () => $gettext('Config path is empty'),
   4102: () => $gettext('Failed to copy config file: {0}'),
-  4103: () => $gettext('Failed to copy database directory: {0}'),
   4104: () => $gettext('Failed to copy database file: {0}'),
   4105: () => $gettext('Failed to calculate hash: {0}'),
   4106: () => $gettext('Nginx config directory is not set'),
@@ -30,14 +29,12 @@ export default {
   4303: () => $gettext('Failed to open source file: {0}'),
   4304: () => $gettext('Failed to create zip header: {0}'),
   4305: () => $gettext('Failed to copy file content: {0}'),
-  4306: () => $gettext('Failed to write to zip buffer: {0}'),
   4501: () => $gettext('Failed to create restore directory: {0}'),
   4505: () => $gettext('Failed to extract archive: {0}'),
   4506: () => $gettext('Failed to decrypt Nginx UI directory: {0}'),
   4507: () => $gettext('Failed to decrypt Nginx directory: {0}'),
   4508: () => $gettext('Failed to verify hashes: {0}'),
   4509: () => $gettext('Failed to restore Nginx configs: {0}'),
-  4510: () => $gettext('Failed to restore Nginx UI files: {0}'),
   4511: () => $gettext('Backup file not found: {0}'),
   4512: () => $gettext('Invalid security token format'),
   4513: () => $gettext('Invalid AES key format: {0}'),
@@ -49,27 +46,18 @@ export default {
   4605: () => $gettext('Failed to open zip entry: {0}'),
   4606: () => $gettext('Failed to create symbolic link: {0}'),
   4607: () => $gettext('Invalid file path: {0}'),
-  4608: () => $gettext('Failed to evaluate symbolic links: {0}'),
   4701: () => $gettext('Failed to read encrypted file: {0}'),
   4702: () => $gettext('Failed to decrypt file: {0}'),
   4703: () => $gettext('Failed to write decrypted file: {0}'),
   4801: () => $gettext('Failed to read hash info file: {0}'),
   4802: () => $gettext('Failed to calculate Nginx UI hash: {0}'),
   4803: () => $gettext('Failed to calculate Nginx hash: {0}'),
-  4804: () => $gettext('Hash verification failed: file integrity compromised'),
-  4901: () => $gettext('Backup path not in granted access paths: {0}'),
-  4902: () => $gettext('Storage path not in granted access paths: {0}'),
   4903: () => $gettext('Backup path is required for custom directory backup'),
   4904: () => $gettext('S3 configuration is incomplete: missing {0}'),
   4905: () => $gettext('Unsupported backup type: {0}'),
-  4906: () => $gettext('Failed to create backup directory: {0}'),
   4907: () => $gettext('Failed to write backup file: {0}'),
   4908: () => $gettext('Failed to write security key file: {0}'),
   4909: () => $gettext('S3 upload failed: {0}'),
-  4920: () => $gettext('S3 connection test failed: {0}'),
-  4921: () => $gettext('S3 bucket access denied: {0}'),
-  4922: () => $gettext('S3 credentials are invalid: {0}'),
-  4923: () => $gettext('S3 endpoint is invalid: {0}'),
   4910: () => $gettext('Invalid path: {0}'),
   4911: () => $gettext('Path not in granted access paths: {0}'),
   4912: () => $gettext('Backup path does not exist: {0}'),

+ 3 - 0
app/src/constants/errors/nginx_log.ts

@@ -24,4 +24,7 @@ export default {
   50023: () => $gettext('Failed to get persistence stats'),
   50024: () => $gettext('Log file is not a regular file'),
   50025: () => $gettext('Invalid websocket message type'),
+  50026: () => $gettext('Modern searcher service not available'),
+  50027: () => $gettext('Modern analytics service not available'),
+  50028: () => $gettext('Modern indexer service not available'),
 }

Dosya farkı çok büyük olduğundan ihmal edildi
+ 270 - 157
app/src/language/ar/app.po


Dosya farkı çok büyük olduğundan ihmal edildi
+ 255 - 163
app/src/language/de_DE/app.po


Dosya farkı çok büyük olduğundan ihmal edildi
+ 268 - 97
app/src/language/en/app.po


Dosya farkı çok büyük olduğundan ihmal edildi
+ 262 - 151
app/src/language/es/app.po


Dosya farkı çok büyük olduğundan ihmal edildi
+ 258 - 161
app/src/language/fr_FR/app.po


Dosya farkı çok büyük olduğundan ihmal edildi
+ 248 - 200
app/src/language/ja_JP/app.po


Dosya farkı çok büyük olduğundan ihmal edildi
+ 251 - 179
app/src/language/ko_KR/app.po


Dosya farkı çok büyük olduğundan ihmal edildi
+ 266 - 96
app/src/language/messages.pot


Dosya farkı çok büyük olduğundan ihmal edildi
+ 252 - 153
app/src/language/pt_PT/app.po


Dosya farkı çok büyük olduğundan ihmal edildi
+ 256 - 152
app/src/language/ru_RU/app.po


Dosya farkı çok büyük olduğundan ihmal edildi
+ 257 - 160
app/src/language/tr_TR/app.po


Dosya farkı çok büyük olduğundan ihmal edildi
+ 267 - 166
app/src/language/uk_UA/app.po


Dosya farkı çok büyük olduğundan ihmal edildi
+ 259 - 149
app/src/language/vi_VN/app.po


Dosya farkı çok büyük olduğundan ihmal edildi
+ 252 - 161
app/src/language/zh_CN/app.po


Dosya farkı çok büyük olduğundan ihmal edildi
+ 253 - 163
app/src/language/zh_TW/app.po


+ 8 - 0
app/src/routes/modules/system.ts

@@ -37,6 +37,14 @@ export const systemRoutes: RouteRecordRaw[] = [
       meta: {
         name: () => $gettext('About'),
       },
+    }, {
+      path: 'licenses',
+      name: 'Licenses',
+      component: () => import('@/views/system/Licenses.vue'),
+      meta: {
+        name: () => $gettext('Third-party Components'),
+        hiddenInSidebar: true,
+      },
     }],
   },
 ]

+ 85 - 6
app/src/views/nginx_log/NginxLogList.vue

@@ -5,12 +5,13 @@ import type { TabOption } from '@/components/TabFilter'
 import { CheckCircleOutlined, ExclamationCircleOutlined, SyncOutlined } from '@ant-design/icons-vue'
 import { StdCurd } from '@uozi-admin/curd'
 import { useRouteQuery } from '@vueuse/router'
-import { Badge, Tag, Tooltip } from 'ant-design-vue'
+import { Badge, message, Tag, Tooltip } from 'ant-design-vue'
 import dayjs from 'dayjs'
 import nginxLog from '@/api/nginx_log'
 import { TabFilter } from '@/components/TabFilter'
 import { useWebSocketEventBus } from '@/composables/useWebSocketEventBus'
 import { useGlobalStore } from '@/pinia'
+import IndexingSettingsModal from './components/IndexingSettingsModal.vue'
 import { useIndexProgress } from './composables/useIndexProgress'
 import IndexProgressBar from './indexing/components/IndexProgressBar.vue'
 import IndexManagement from './indexing/IndexManagement.vue'
@@ -18,6 +19,9 @@ import IndexManagement from './indexing/IndexManagement.vue'
 const router = useRouter()
 const stdCurdRef = ref()
 const indexManagementRef = ref()
+const indexingSettingsModalVisible = ref(false)
+const advancedIndexingEnabled = ref(false)
+const enableIndexingLoading = ref(false)
 
 // WebSocket event bus and global store
 const { subscribe } = useWebSocketEventBus()
@@ -68,8 +72,22 @@ function stopAutoRefresh() {
   }
 }
 
+// Check advanced indexing status on mount
+async function checkAdvancedIndexingStatus() {
+  try {
+    const response = await nginxLog.getAdvancedIndexingStatus()
+    advancedIndexingEnabled.value = response.enabled
+  }
+  catch (error) {
+    console.error('Failed to check advanced indexing status:', error)
+  }
+}
+
 // Subscribe to events
-onMounted(() => {
+onMounted(async () => {
+  // Check advanced indexing status
+  await checkAdvancedIndexingStatus()
+
   // Subscribe to processing status events
   subscribe('processing_status', data => {
     const wasIndexing = processingStatus.value?.nginx_log_indexing
@@ -345,6 +363,48 @@ function rebuildFileIndex(record: NginxLogData) {
 async function refreshTable() {
   stdCurdRef.value.refresh()
 }
+
+function showIndexingSettingsModal() {
+  indexingSettingsModalVisible.value = true
+}
+
+async function enableAdvancedIndexing() {
+  enableIndexingLoading.value = true
+  try {
+    await nginxLog.enableAdvancedIndexing()
+    advancedIndexingEnabled.value = true
+    indexingSettingsModalVisible.value = false
+
+    // Show success message
+    message.success($gettext('Advanced indexing enabled successfully'))
+
+    // Start rebuild all indexes
+    try {
+      await nginxLog.rebuildIndex()
+      message.success($gettext('Index rebuild initiated'))
+    }
+    catch (rebuildError) {
+      console.error('Failed to rebuild index:', rebuildError)
+      message.warning($gettext('Advanced indexing enabled but failed to start rebuild'))
+    }
+
+    // Refresh table to show updated indexing status
+    setTimeout(() => {
+      refreshTable()
+    }, 500)
+  }
+  catch (error) {
+    console.error('Failed to enable advanced indexing:', error)
+    message.error($gettext('Failed to enable advanced indexing'))
+  }
+  finally {
+    enableIndexingLoading.value = false
+  }
+}
+
+function cancelIndexingSettings() {
+  indexingSettingsModalVisible.value = false
+}
 </script>
 
 <template>
@@ -381,9 +441,20 @@ async function refreshTable() {
           </div>
         </div>
 
-        <!-- Index Management - only for Access logs -->
+        <!-- Advanced Indexing Toggle - only for Access logs -->
+        <div v-if="activeLogType === 'access' && !advancedIndexingEnabled" class="flex items-center">
+          <AButton
+            type="link"
+            size="small"
+            @click="showIndexingSettingsModal"
+          >
+            {{ $gettext('Enable Advanced Indexing') }}
+          </AButton>
+        </div>
+
+        <!-- Index Management - only for Access logs when advanced indexing is enabled -->
         <IndexManagement
-          v-if="activeLogType === 'access'"
+          v-if="activeLogType === 'access' && advancedIndexingEnabled"
           ref="indexManagementRef"
           :disabled="processingStatus.nginx_log_indexing"
           :indexing="isGlobalIndexing || processingStatus.nginx_log_indexing"
@@ -396,9 +467,9 @@ async function refreshTable() {
         {{ $gettext('View') }}
       </AButton>
 
-      <!-- Rebuild File Index Action - only for Access logs -->
+      <!-- Rebuild File Index Action - only for Access logs with advanced indexing enabled -->
       <AButton
-        v-if="record.type === 'access'"
+        v-if="record.type === 'access' && advancedIndexingEnabled"
         type="link"
         size="small"
         :disabled="processingStatus.nginx_log_indexing"
@@ -408,6 +479,14 @@ async function refreshTable() {
       </AButton>
     </template>
   </StdCurd>
+
+  <!-- Advanced Indexing Settings Modal -->
+  <IndexingSettingsModal
+    v-model:visible="indexingSettingsModalVisible"
+    :loading="enableIndexingLoading"
+    @confirm="enableAdvancedIndexing"
+    @cancel="cancelIndexingSettings"
+  />
 </template>
 
 <style scoped lang="less">

+ 274 - 0
app/src/views/nginx_log/components/IndexingSettingsModal.vue

@@ -0,0 +1,274 @@
+<script setup lang="tsx">
+import { CheckCircleOutlined, CloseCircleOutlined, HeartOutlined, InfoCircleOutlined, MailOutlined, ThunderboltOutlined, WarningOutlined } from '@ant-design/icons-vue'
+
+interface Props {
+  loading?: boolean
+}
+
+interface Emits {
+  (e: 'confirm'): void
+  (e: 'cancel'): void
+}
+
+withDefaults(defineProps<Props>(), {
+  loading: false,
+})
+
+const emit = defineEmits<Emits>()
+
+const visible = defineModel<boolean>('visible', { required: true })
+
+const systemRequirements = [
+  {
+    title: $gettext('CPU'),
+    requirement: $gettext('1 core minimum'),
+    recommended: $gettext('2+ cores recommended'),
+    icon: <CheckCircleOutlined class="text-green-500" />,
+  },
+  {
+    title: $gettext('Memory'),
+    requirement: $gettext('2GB RAM minimum'),
+    recommended: $gettext('4GB+ RAM recommended'),
+    icon: <CheckCircleOutlined class="text-green-500" />,
+  },
+  {
+    title: $gettext('Storage'),
+    requirement: $gettext('At least 20GB available disk space'),
+    recommended: $gettext('SSD storage for better I/O performance'),
+    icon: <CheckCircleOutlined class="text-green-500" />,
+  },
+]
+
+const performanceStats = [
+  {
+    metric: $gettext('Indexing Throughput'),
+    value: '3,860it/s',
+    description: $gettext('Based on M2 Pro (12 cores) testing'),
+  },
+  {
+    metric: $gettext('CPU Utilization'),
+    value: '90%+',
+    description: $gettext('Optimized multi-core processing'),
+  },
+  {
+    metric: $gettext('Memory Efficiency'),
+    value: '600MB/1Mit',
+    description: $gettext('Zero-allocation pipeline optimization'),
+  },
+]
+
+function handleConfirm() {
+  emit('confirm')
+}
+
+function handleCancel() {
+  visible.value = false
+  emit('cancel')
+}
+</script>
+
+<template>
+  <AModal
+    :open="visible"
+    :title="$gettext('Enable Advanced Log Indexing')"
+    :confirm-loading="loading"
+    :ok-text="$gettext('Enable Indexing')"
+    :cancel-text="$gettext('Cancel')"
+    width="720px"
+    @ok="handleConfirm"
+    @cancel="handleCancel"
+  >
+    <div class="space-y-6">
+      <!-- Warning Alert -->
+      <AAlert
+        :message="$gettext('Performance Impact Notice')"
+        :description="$gettext('Enabling advanced indexing will consume system resources during log processing. Please review the requirements below.')"
+        type="warning"
+        show-icon
+        :icon="h(WarningOutlined)"
+      />
+
+      <!-- System Requirements -->
+      <div>
+        <ATypographyTitle :level="4" class="mb-3">
+          <InfoCircleOutlined class="mr-2" />
+          {{ $gettext('System Requirements') }}
+        </ATypographyTitle>
+
+        <AList
+          :data-source="systemRequirements"
+          item-layout="horizontal"
+        >
+          <template #renderItem="{ item }">
+            <AListItem>
+              <AListItemMeta>
+                <template #avatar>
+                  <component :is="item.icon" />
+                </template>
+                <template #title>
+                  <ATypographyText strong>
+                    {{ item.title }}
+                  </ATypographyText>
+                </template>
+                <template #description>
+                  <div class="space-y-1">
+                    <div>
+                      <ATypographyText>
+                        {{ $gettext('Minimum:') }}
+                      </ATypographyText>
+                      <ATypographyText type="secondary">
+                        {{ item.requirement }}
+                      </ATypographyText>
+                    </div>
+                    <div>
+                      <ATypographyText>
+                        {{ $gettext('Recommended:') }}
+                      </ATypographyText>
+                      <ATypographyText type="secondary">
+                        {{ item.recommended }}
+                      </ATypographyText>
+                    </div>
+                  </div>
+                </template>
+              </AListItemMeta>
+            </AListItem>
+          </template>
+        </AList>
+      </div>
+
+      <ADivider />
+
+      <!-- Performance Statistics -->
+      <div>
+        <ATypographyTitle :level="4" class="mb-3">
+          <CheckCircleOutlined class="mr-2 text-green-500" />
+          {{ $gettext('Expected Performance') }}
+        </ATypographyTitle>
+
+        <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
+          <div
+            v-for="stat in performanceStats"
+            :key="stat.metric"
+            class="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg"
+          >
+            <div class="text-xl font-bold text-blue-600 dark:text-blue-400 mb-1">
+              {{ stat.value }}
+            </div>
+            <div class="font-medium text-gray-900 dark:text-gray-100 mb-1 text-sm">
+              {{ stat.metric }}
+            </div>
+            <div class="text-sm text-gray-600 dark:text-gray-400">
+              {{ stat.description }}
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <ADivider />
+
+      <!-- Features -->
+      <div>
+        <ATypographyTitle :level="4" class="mb-3">
+          <ThunderboltOutlined class="mr-2 text-blue-500" />
+          {{ $gettext('Features') }}
+        </ATypographyTitle>
+
+        <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
+          <div class="flex items-center space-x-2">
+            <CheckCircleOutlined class="text-green-500" />
+            <ATypographyText>{{ $gettext('Zero-allocation pipeline') }}</ATypographyText>
+          </div>
+          <div class="flex items-center space-x-2">
+            <CheckCircleOutlined class="text-green-500" />
+            <ATypographyText>{{ $gettext('Dynamic shard management') }}</ATypographyText>
+          </div>
+          <div class="flex items-center space-x-2">
+            <CheckCircleOutlined class="text-green-500" />
+            <ATypographyText>{{ $gettext('Advanced search & filtering') }}</ATypographyText>
+          </div>
+          <div class="flex items-center space-x-2">
+            <CheckCircleOutlined class="text-green-500" />
+            <ATypographyText>{{ $gettext('Real-time analytics dashboard') }}</ATypographyText>
+          </div>
+          <div class="flex items-center space-x-2">
+            <CheckCircleOutlined class="text-green-500" />
+            <ATypographyText>{{ $gettext('Offline GeoIP analysis') }}</ATypographyText>
+          </div>
+          <div class="flex items-center space-x-2">
+            <CheckCircleOutlined class="text-green-500" />
+            <ATypographyText>{{ $gettext('Incremental index scanning') }}</ATypographyText>
+          </div>
+          <div class="flex items-center space-x-2">
+            <CheckCircleOutlined class="text-green-500" />
+            <ATypographyText>{{ $gettext('Full-text search with regex support') }}</ATypographyText>
+          </div>
+          <div class="flex items-center space-x-2">
+            <CheckCircleOutlined class="text-green-500" />
+            <ATypographyText>{{ $gettext('Automated log rotation detection') }}</ATypographyText>
+          </div>
+          <div class="flex items-center space-x-2">
+            <CheckCircleOutlined class="text-green-500" />
+            <ATypographyText>{{ $gettext('Cross-file timeline correlation') }}</ATypographyText>
+          </div>
+          <div class="flex items-center space-x-2">
+            <CheckCircleOutlined class="text-green-500" />
+            <ATypographyText>{{ $gettext('Compressed log file support') }}</ATypographyText>
+          </div>
+          <div class="flex items-center space-x-2">
+            <CheckCircleOutlined class="text-green-500" />
+            <ATypographyText>{{ $gettext('Error pattern recognition') }}</ATypographyText>
+          </div>
+          <div class="flex items-center space-x-2">
+            <CheckCircleOutlined class="text-green-500" />
+            <ATypographyText>{{ $gettext('Multi-dimensional data visualization') }}</ATypographyText>
+          </div>
+        </div>
+      </div>
+
+      <!-- License Notice -->
+      <div>
+        <ATypographyTitle :level="4" class="mb-3">
+          <HeartOutlined class="mr-2 text-red-500" />
+          {{ $gettext('Open Source Limitation') }}
+        </ATypographyTitle>
+
+        <div class="space-y-2">
+          <div class="flex items-center space-x-2">
+            <CheckCircleOutlined class="text-green-500" />
+            <ATypographyText class="text-sm">
+              {{ $gettext('Advanced log indexing features are free and open source for all users') }}
+            </ATypographyText>
+          </div>
+          <div class="flex items-center space-x-2">
+            <CloseCircleOutlined class="text-orange-500" />
+            <ATypographyText class="text-sm">
+              {{ $gettext('We do not accept any feature requests') }}
+            </ATypographyText>
+          </div>
+          <div class="flex items-center space-x-2">
+            <MailOutlined class="text-blue-500" />
+            <ATypographyText class="text-sm">
+              {{ $gettext('For commercial or professional use, contact') }}
+              <a href="mailto:business@uozi.com" class="text-blue-600 hover:text-blue-800">business@uozi.com</a>
+            </ATypographyText>
+          </div>
+        </div>
+      </div>
+
+      <!-- Final Warning -->
+      <AAlert
+        :message="$gettext('Confirmation Required')"
+        :description="$gettext('By enabling advanced indexing, you acknowledge that your system meets the requirements and understand the performance implications. This will start indexing existing log files immediately.')"
+        type="info"
+        show-icon
+        :icon="h(InfoCircleOutlined)"
+      />
+    </div>
+  </AModal>
+</template>
+
+<style scoped lang="less">
+:deep(.ant-list-item-meta-description) {
+  margin-top: 8px;
+}
+</style>

+ 3 - 3
app/src/views/preference/components/ExternalNotify/ntfy.ts

@@ -14,11 +14,11 @@ const NtfyConfig: ExternalNotifyConfig = {
     },
     {
       key: 'priority',
-      label: 'Priority(int, one of: 1, 2, 3, 4, 5)',
+      label: 'Priority',
     },
     {
       key: 'tags',
-      label: 'Tags(string array)',
+      label: 'Tags',
     },
     {
       key: 'click',
@@ -26,7 +26,7 @@ const NtfyConfig: ExternalNotifyConfig = {
     },
     {
       key: 'actions',
-      label: 'Actions(JSON array)',
+      label: 'Actions',
     },
     {
       key: 'username',

+ 3 - 0
app/src/views/preference/store/index.ts

@@ -66,6 +66,9 @@ const useSystemSettingsStore = defineStore('systemSettings', () => {
       stub_status_port: 51820,
       container_name: '',
     },
+    nginx_log: {
+      advanced_indexing_enabled: false,
+    },
     node: {
       name: '',
       secret: '',

+ 9 - 8
app/src/views/system/About.vue

@@ -71,17 +71,18 @@ const thisYear = new Date().getFullYear()
       {{ $gettext('Project Team') }}
     </h3>
     <p><a href="https://jackyu.cn/">@0xJacky</a> <a href="https://blog.kugeek.com/">@Hintay</a> <a href="https://github.com/akinoccc">@Akino</a></p>
-    <h3>
-      {{ $gettext('Build with') }}
-    </h3>
-    <p>❤️</p>
-    <p>Go</p>
-    <p>Gin</p>
-    <p>Vue3 + Vite + TypeScript</p>
-    <p>Websocket</p>
     <h3>
       {{ $gettext('License') }}
     </h3>
+    <div class="mb-3">
+      <AButton
+        type="link"
+        size="small"
+        @click="$router.push('/system/licenses')"
+      >
+        {{ $gettext('View Third-party Components') }}
+      </AButton>
+    </div>
     <p>GNU Affero General Public License v3.0</p>
     <p>Copyright © 2021 - {{ thisYear }} Nginx UI Team</p>
   </ACard>

+ 292 - 0
app/src/views/system/Licenses.vue

@@ -0,0 +1,292 @@
+<script setup lang="ts">
+import type { License, LicenseStats } from '@/api/license'
+import license from '@/api/license'
+
+const loading = ref(false)
+const activeTab = ref('all')
+const backendLicenses = ref<License[]>([])
+const frontendLicenses = ref<License[]>([])
+const stats = ref<LicenseStats>()
+
+const columns = [
+  {
+    title: $gettext('Name'),
+    dataIndex: 'name',
+    key: 'name',
+    sorter: (a: License, b: License) => a.name.localeCompare(b.name),
+    width: 300,
+    ellipsis: true,
+  },
+  {
+    title: $gettext('License'),
+    dataIndex: 'license',
+    key: 'license',
+    sorter: (a: License, b: License) => a.license.localeCompare(b.license),
+    width: 120,
+  },
+  {
+    title: $gettext('Version'),
+    dataIndex: 'version',
+    key: 'version',
+    width: 120,
+  },
+  {
+    title: $gettext('URL'),
+    dataIndex: 'url',
+    key: 'url',
+    width: 80,
+  },
+]
+
+async function fetchLicenses() {
+  loading.value = true
+  try {
+    const [backendRes, frontendRes, statsRes] = await Promise.all([
+      license.getBackend(),
+      license.getFrontend(),
+      license.getStats(),
+    ])
+
+    backendLicenses.value = backendRes
+    frontendLicenses.value = frontendRes
+    stats.value = statsRes
+  }
+  catch (error) {
+    console.error(error)
+  }
+  finally {
+    loading.value = false
+  }
+}
+
+function getAllLicenses() {
+  return [...backendLicenses.value, ...frontendLicenses.value]
+}
+
+function getCurrentLicenses() {
+  switch (activeTab.value) {
+    case 'backend':
+      return backendLicenses.value
+    case 'frontend':
+      return frontendLicenses.value
+    default:
+      return getAllLicenses()
+  }
+}
+
+onMounted(() => {
+  fetchLicenses()
+})
+</script>
+
+<script lang="ts">
+function getLicenseColor(license: string): string {
+  const colors: Record<string, string> = {
+    'MIT': 'green',
+    'Apache-2.0': 'blue',
+    'BSD-3-Clause': 'cyan',
+    'BSD-2-Clause': 'cyan',
+    'GPL-3.0': 'orange',
+    'AGPL-3.0': 'red',
+    'ISC': 'geekblue',
+    'Unknown': 'default',
+    'Custom': 'purple',
+  }
+  return colors[license] || 'default'
+}
+
+export { getLicenseColor }
+</script>
+
+<template>
+  <div>
+    <ACard v-if="stats" class="mb-4">
+      <ARow :gutter="[16, 16]">
+        <ACol :xs="12" :sm="12" :md="6" :lg="6" :xl="6">
+          <AStatistic
+            :title="$gettext('Total Components')"
+            :value="stats.total"
+          />
+        </ACol>
+        <ACol :xs="12" :sm="12" :md="6" :lg="6" :xl="6">
+          <AStatistic
+            :title="$gettext('Backend')"
+            :value="stats.total_backend"
+          />
+        </ACol>
+        <ACol :xs="12" :sm="12" :md="6" :lg="6" :xl="6">
+          <AStatistic
+            :title="$gettext('Frontend')"
+            :value="stats.total_frontend"
+          />
+        </ACol>
+        <ACol :xs="12" :sm="12" :md="6" :lg="6" :xl="6">
+          <AStatistic
+            :title="$gettext('License Types')"
+            :value="Object.keys(stats.license_distribution || {}).length"
+          />
+        </ACol>
+      </ARow>
+
+      <ADivider />
+
+      <h4>{{ $gettext('License Distribution') }}</h4>
+      <ARow :gutter="[16, 16]">
+        <ACol
+          v-for="[licenseName, count] in Object.entries(stats.license_distribution || {})"
+          :key="licenseName"
+          :xs="24" :sm="12" :md="8" :lg="6" :xl="6"
+        >
+          <div class="license-item">
+            <ATag :color="getLicenseColor(licenseName)">
+              {{ licenseName }}
+            </ATag>
+            <span class="ml-2">{{ count }} {{ $gettext('components') }}</span>
+          </div>
+        </ACol>
+      </ARow>
+    </ACard>
+
+    <ACard>
+      <ATabs v-model:active-key="activeTab">
+        <ATabPane key="all" :tab="$gettext('All Components')">
+          <ATable
+            :columns="columns"
+            :data-source="getCurrentLicenses()"
+            :loading="loading"
+            :pagination="{ pageSize: 20, showSizeChanger: true, showQuickJumper: true }"
+            :scroll="{ x: 800 }"
+            size="small"
+          >
+            <template #bodyCell="{ column, record }">
+              <template v-if="column.key === 'name'">
+                <ATypographyText code>
+                  {{ record.name }}
+                </ATypographyText>
+              </template>
+              <template v-else-if="column.key === 'license'">
+                <ATag :color="getLicenseColor(record.license)">
+                  {{ record.license }}
+                </ATag>
+              </template>
+              <template v-else-if="column.key === 'url'">
+                <AButton
+                  type="link"
+                  size="small"
+                  :href="record.url"
+                  target="_blank"
+                  rel="noopener noreferrer"
+                >
+                  {{ $gettext('View') }}
+                </AButton>
+              </template>
+            </template>
+          </ATable>
+        </ATabPane>
+
+        <ATabPane key="backend" :tab="$gettext('Backend')">
+          <ATable
+            :columns="columns"
+            :data-source="getCurrentLicenses()"
+            :loading="loading"
+            :pagination="{ pageSize: 20, showSizeChanger: true, showQuickJumper: true }"
+            :scroll="{ x: 800 }"
+            size="small"
+          >
+            <template #bodyCell="{ column, record }">
+              <template v-if="column.key === 'name'">
+                <ATypographyText code>
+                  {{ record.name }}
+                </ATypographyText>
+              </template>
+              <template v-else-if="column.key === 'license'">
+                <ATag :color="getLicenseColor(record.license)">
+                  {{ record.license }}
+                </ATag>
+              </template>
+              <template v-else-if="column.key === 'url'">
+                <AButton
+                  type="link"
+                  size="small"
+                  :href="record.url"
+                  target="_blank"
+                  rel="noopener noreferrer"
+                >
+                  {{ $gettext('View') }}
+                </AButton>
+              </template>
+            </template>
+          </ATable>
+        </ATabPane>
+
+        <ATabPane key="frontend" :tab="$gettext('Frontend')">
+          <ATable
+            :columns="columns"
+            :data-source="getCurrentLicenses()"
+            :loading="loading"
+            :pagination="{ pageSize: 20, showSizeChanger: true, showQuickJumper: true }"
+            :scroll="{ x: 800 }"
+            size="small"
+          >
+            <template #bodyCell="{ column, record }">
+              <template v-if="column.key === 'name'">
+                <ATypographyText code>
+                  {{ record.name }}
+                </ATypographyText>
+              </template>
+              <template v-else-if="column.key === 'license'">
+                <ATag :color="getLicenseColor(record.license)">
+                  {{ record.license }}
+                </ATag>
+              </template>
+              <template v-else-if="column.key === 'url'">
+                <AButton
+                  type="link"
+                  size="small"
+                  :href="record.url"
+                  target="_blank"
+                  rel="noopener noreferrer"
+                >
+                  {{ $gettext('View') }}
+                </AButton>
+              </template>
+            </template>
+          </ATable>
+        </ATabPane>
+      </ATabs>
+    </ACard>
+  </div>
+</template>
+
+<style lang="less" scoped>
+.license-item {
+  display: flex;
+  align-items: center;
+  padding: 4px 0;
+  flex-wrap: wrap;
+  gap: 8px;
+
+  @media (max-width: 576px) {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+}
+
+:deep(.ant-table-wrapper) {
+  @media (max-width: 768px) {
+    .ant-table-pagination {
+      .ant-pagination-options {
+        display: none;
+      }
+    }
+  }
+}
+
+:deep(.ant-statistic) {
+  text-align: center;
+
+  @media (max-width: 768px) {
+    margin-bottom: 16px;
+  }
+}
+</style>

+ 718 - 0
cmd/generate_licenses/main.go

@@ -0,0 +1,718 @@
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/ulikunitz/xz"
+)
+
+type License struct {
+	Name    string `json:"name"`
+	License string `json:"license"`
+	URL     string `json:"url"`
+	Version string `json:"version"`
+}
+
+type ComponentInfo struct {
+	Backend  []License `json:"backend"`
+	Frontend []License `json:"frontend"`
+}
+
+func main() {
+	log.Println("Generating license information...")
+
+	var info ComponentInfo
+
+	// Generate backend licenses
+	backendLicenses, err := generateBackendLicenses()
+	if err != nil {
+		log.Printf("Error generating backend licenses: %v", err)
+	} else {
+		info.Backend = backendLicenses
+		log.Printf("INFO: Backend license collection completed: %d components", len(backendLicenses))
+	}
+
+	// Generate frontend licenses  
+	frontendLicenses, err := generateFrontendLicenses()
+	if err != nil {
+		log.Printf("Error generating frontend licenses: %v", err)
+	} else {
+		info.Frontend = frontendLicenses
+		log.Printf("INFO: Frontend license collection completed: %d components", len(frontendLicenses))
+	}
+
+	log.Println("INFO: Serializing license data to JSON...")
+	// Marshal to JSON
+	jsonData, err := json.MarshalIndent(info, "", "  ")
+	if err != nil {
+		log.Fatalf("Error marshaling JSON: %v", err)
+	}
+	log.Printf("INFO: JSON size: %d bytes", len(jsonData))
+
+	log.Println("INFO: Compressing license data with xz...")
+	// Compress with xz
+	var compressed bytes.Buffer
+	writer, err := xz.NewWriter(&compressed)
+	if err != nil {
+		log.Fatalf("Error creating xz writer: %v", err)
+	}
+
+	_, err = writer.Write(jsonData)
+	if err != nil {
+		log.Fatalf("Error writing compressed data: %v", err)
+	}
+
+	err = writer.Close()
+	if err != nil {
+		log.Fatalf("Error closing xz writer: %v", err)
+	}
+	log.Printf("INFO: Compressed size: %d bytes (%.1f%% of original)", 
+		compressed.Len(), float64(compressed.Len())/float64(len(jsonData))*100)
+
+	// Write compressed data to file
+	outputPath := "internal/license/licenses.xz"
+	log.Printf("INFO: Writing compressed data to %s", outputPath)
+	err = os.MkdirAll(filepath.Dir(outputPath), 0755)
+	if err != nil {
+		log.Fatalf("Error creating output directory: %v", err)
+	}
+
+	err = os.WriteFile(outputPath, compressed.Bytes(), 0644)
+	if err != nil {
+		log.Fatalf("Error writing output file: %v", err)
+	}
+
+	log.Printf("SUCCESS: License data generated successfully!")
+	log.Printf("  - Backend components: %d", len(info.Backend))
+	log.Printf("  - Frontend components: %d", len(info.Frontend))
+	log.Printf("  - Total components: %d", len(info.Backend)+len(info.Frontend))
+	log.Printf("  - Compressed size: %d bytes", compressed.Len())
+	log.Printf("  - Output file: %s", outputPath)
+}
+
+func generateBackendLicenses() ([]License, error) {
+	var licenses []License
+
+	log.Println("INFO: Collecting backend Go modules...")
+	
+	// Get only direct and indirect dependencies, exclude workspace modules
+	cmd := exec.Command("go", "mod", "graph")
+	output, err := cmd.Output()
+	if err != nil {
+		return nil, fmt.Errorf("failed to run go mod graph: %v", err)
+	}
+
+	// Parse module graph to get unique dependencies
+	depMap := make(map[string]string) // path -> version
+	lines := strings.Split(string(output), "\n")
+	
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+		
+		parts := strings.Fields(line)
+		if len(parts) != 2 {
+			continue
+		}
+		
+		// Extract dependency info from "module@version dependency@version"
+		dep := parts[1]
+		if dep == "" || !strings.Contains(dep, "@") {
+			continue
+		}
+		
+		atIndex := strings.LastIndex(dep, "@")
+		if atIndex == -1 {
+			continue
+		}
+		
+		path := dep[:atIndex]
+		version := dep[atIndex+1:]
+		
+		// Skip our own module and workspace modules
+		if path == "" || 
+		   strings.HasPrefix(path, "github.com/0xJacky/Nginx-UI") ||
+		   strings.Contains(path, "git.uozi.org") ||
+		   strings.Contains(path, "apple-store-helper") {
+			continue
+		}
+		
+		// Only keep the first version we see (go mod graph shows all versions)
+		if _, exists := depMap[path]; !exists {
+			depMap[path] = version
+		}
+	}
+
+	// Convert map to slice
+	var allMods []struct {
+		Path    string `json:"Path"`
+		Version string `json:"Version"`
+	}
+	
+	for path, version := range depMap {
+		allMods = append(allMods, struct {
+			Path    string `json:"Path"`
+			Version string `json:"Version"`
+		}{
+			Path:    path,
+			Version: version,
+		})
+	}
+
+	// Add Go language itself
+	goLicense := License{
+		Name:    "Go Programming Language",
+		Version: getGoVersion(),
+		URL:     "https://golang.org",
+		License: "BSD-3-Clause",
+	}
+	licenses = append(licenses, goLicense)
+
+	log.Printf("INFO: Found %d backend dependencies (+ Go language)", len(allMods))
+
+	// Process modules in parallel
+	const maxWorkers = 64
+	jobs := make(chan struct {
+		Path    string
+		Version string
+		Index   int
+	}, len(allMods))
+	
+	results := make(chan License, len(allMods))
+	
+	// Progress tracking
+	var processed int32
+	var mu sync.Mutex
+	
+	// Start workers
+	var wg sync.WaitGroup
+	for i := 0; i < maxWorkers; i++ {
+		wg.Add(1)
+		go func(workerID int) {
+			defer wg.Done()
+			for job := range jobs {
+				license := License{
+					Name:    job.Path,
+					Version: job.Version,
+					URL:     fmt.Sprintf("https://%s", job.Path),
+				}
+
+				// Try to get license info from various sources
+				licenseText := tryGetLicenseFromGit(job.Path)
+				if licenseText == "" {
+					licenseText = detectCommonLicense(job.Path)
+				}
+				license.License = licenseText
+
+				mu.Lock()
+				processed++
+				currentCount := processed
+				mu.Unlock()
+				
+				log.Printf("INFO: [%d/%d] Backend: %s -> %s", currentCount, len(allMods), job.Path, licenseText)
+				results <- license
+			}
+		}(i)
+	}
+
+	// Send jobs
+	go func() {
+		for i, mod := range allMods {
+			jobs <- struct {
+				Path    string
+				Version string
+				Index   int
+			}{
+				Path:    mod.Path,
+				Version: mod.Version,
+				Index:   i,
+			}
+		}
+		close(jobs)
+	}()
+
+	// Wait for workers and close results
+	go func() {
+		wg.Wait()
+		close(results)
+	}()
+
+	// Collect results
+	for license := range results {
+		licenses = append(licenses, license)
+	}
+
+	return licenses, nil
+}
+
+func generateFrontendLicenses() ([]License, error) {
+	var licenses []License
+
+	log.Println("INFO: Collecting frontend npm packages...")
+
+	// Read package.json
+	packagePath := "app/package.json"
+	if _, err := os.Stat(packagePath); os.IsNotExist(err) {
+		return nil, fmt.Errorf("package.json not found at %s", packagePath)
+	}
+
+	data, err := os.ReadFile(packagePath)
+	if err != nil {
+		return nil, err
+	}
+
+	var pkg struct {
+		Dependencies    map[string]string `json:"dependencies"`
+		DevDependencies map[string]string `json:"devDependencies"`
+	}
+
+	if err := json.Unmarshal(data, &pkg); err != nil {
+		return nil, err
+	}
+
+	log.Printf("INFO: Found %d frontend dependencies", len(pkg.Dependencies))
+
+	// Convert map to slice for easier parallel processing
+	var packages []struct {
+		Name    string
+		Version string
+		Index   int
+	}
+	
+	i := 0
+	for name, version := range pkg.Dependencies {
+		packages = append(packages, struct {
+			Name    string
+			Version string
+			Index   int
+		}{
+			Name:    name,
+			Version: version,
+			Index:   i,
+		})
+		i++
+	}
+
+	// Process packages in parallel
+	const maxWorkers = 64
+	jobs := make(chan struct {
+		Name    string
+		Version string
+		Index   int
+	}, len(packages))
+	
+	results := make(chan License, len(packages))
+	
+	// Progress tracking
+	var processed int32
+	var mu sync.Mutex
+	
+	// Start workers
+	var wg sync.WaitGroup
+	for i := 0; i < maxWorkers; i++ {
+		wg.Add(1)
+		go func(workerID int) {
+			defer wg.Done()
+			for job := range jobs {
+				license := License{
+					Name:    job.Name,
+					Version: job.Version,
+					URL:     fmt.Sprintf("https://www.npmjs.com/package/%s", job.Name),
+				}
+
+				// Try to get license info
+				licenseText := tryGetNpmLicense(job.Name)
+				if licenseText == "" {
+					licenseText = "Unknown"
+				}
+				license.License = licenseText
+
+				mu.Lock()
+				processed++
+				currentCount := processed
+				mu.Unlock()
+				
+				log.Printf("INFO: [%d/%d] Frontend: %s -> %s", currentCount, len(packages), job.Name, licenseText)
+				results <- license
+			}
+		}(i)
+	}
+
+	// Send jobs
+	go func() {
+		for _, pkg := range packages {
+			jobs <- pkg
+		}
+		close(jobs)
+	}()
+
+	// Wait for workers and close results
+	go func() {
+		wg.Wait()
+		close(results)
+	}()
+
+	// Collect results
+	for license := range results {
+		licenses = append(licenses, license)
+	}
+
+	return licenses, nil
+}
+
+func tryGetLicenseFromGitHub(modulePath string) string {
+	// Extract GitHub info from module path
+	if !strings.HasPrefix(modulePath, "github.com/") {
+		return ""
+	}
+
+	parts := strings.Split(modulePath, "/")
+	if len(parts) < 3 {
+		return ""
+	}
+
+	owner := parts[1]
+	repo := parts[2]
+	
+	// Try common license files and branches
+	licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING", "COPYING.txt", "License", "license"}
+	branches := []string{"master", "main", "HEAD"}
+	
+	for _, branch := range branches {
+		for _, file := range licenseFiles {
+			url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s", owner, repo, branch, file)
+			
+			client := &http.Client{Timeout: 10 * time.Second}
+			resp, err := client.Get(url)
+			if err != nil {
+				continue
+			}
+			defer resp.Body.Close()
+
+			if resp.StatusCode == 200 {
+				body, err := io.ReadAll(resp.Body)
+				if err != nil {
+					continue
+				}
+				
+				// Extract license type from content
+				content := string(body)
+				licenseType := extractLicenseType(content)
+				if licenseType != "Custom" {
+					return licenseType
+				}
+			}
+		}
+	}
+
+	return ""
+}
+
+func tryGetLicenseFromGit(modulePath string) string {
+	// Try different Git hosting platforms
+	if strings.HasPrefix(modulePath, "github.com/") {
+		return tryGetLicenseFromGitHub(modulePath)
+	}
+	
+	if strings.HasPrefix(modulePath, "gitlab.com/") {
+		return tryGetLicenseFromGitLab(modulePath)
+	}
+	
+	if strings.HasPrefix(modulePath, "gitee.com/") {
+		return tryGetLicenseFromGitee(modulePath)
+	}
+	
+	if strings.HasPrefix(modulePath, "bitbucket.org/") {
+		return tryGetLicenseFromBitbucket(modulePath)
+	}
+	
+	// Try to use go.mod info or pkg.go.dev API
+	return tryGetLicenseFromPkgGoDev(modulePath)
+}
+
+func tryGetLicenseFromGitLab(modulePath string) string {
+	parts := strings.Split(modulePath, "/")
+	if len(parts) < 3 {
+		return ""
+	}
+	
+	owner := parts[1]
+	repo := parts[2]
+	
+	// GitLab raw file URL format
+	licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING", "License"}
+	branches := []string{"master", "main"}
+	
+	for _, branch := range branches {
+		for _, file := range licenseFiles {
+			url := fmt.Sprintf("https://gitlab.com/%s/%s/-/raw/%s/%s", owner, repo, branch, file)
+			
+			client := &http.Client{Timeout: 10 * time.Second}
+			resp, err := client.Get(url)
+			if err != nil {
+				continue
+			}
+			defer resp.Body.Close()
+
+			if resp.StatusCode == 200 {
+				body, err := io.ReadAll(resp.Body)
+				if err != nil {
+					continue
+				}
+				
+				content := string(body)
+				licenseType := extractLicenseType(content)
+				if licenseType != "Custom" {
+					return licenseType
+				}
+			}
+		}
+	}
+	
+	return ""
+}
+
+func tryGetLicenseFromGitee(modulePath string) string {
+	parts := strings.Split(modulePath, "/")
+	if len(parts) < 3 {
+		return ""
+	}
+	
+	owner := parts[1]
+	repo := parts[2]
+	
+	// Gitee raw file URL format
+	licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING"}
+	branches := []string{"master", "main"}
+	
+	for _, branch := range branches {
+		for _, file := range licenseFiles {
+			url := fmt.Sprintf("https://gitee.com/%s/%s/raw/%s/%s", owner, repo, branch, file)
+			
+			client := &http.Client{Timeout: 10 * time.Second}
+			resp, err := client.Get(url)
+			if err != nil {
+				continue
+			}
+			defer resp.Body.Close()
+
+			if resp.StatusCode == 200 {
+				body, err := io.ReadAll(resp.Body)
+				if err != nil {
+					continue
+				}
+				
+				content := string(body)
+				licenseType := extractLicenseType(content)
+				if licenseType != "Custom" {
+					return licenseType
+				}
+			}
+		}
+	}
+	
+	return ""
+}
+
+func tryGetLicenseFromBitbucket(modulePath string) string {
+	parts := strings.Split(modulePath, "/")
+	if len(parts) < 3 {
+		return ""
+	}
+	
+	owner := parts[1]
+	repo := parts[2]
+	
+	// Bitbucket raw file URL format  
+	licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING"}
+	branches := []string{"master", "main"}
+	
+	for _, branch := range branches {
+		for _, file := range licenseFiles {
+			url := fmt.Sprintf("https://bitbucket.org/%s/%s/raw/%s/%s", owner, repo, branch, file)
+			
+			client := &http.Client{Timeout: 10 * time.Second}
+			resp, err := client.Get(url)
+			if err != nil {
+				continue
+			}
+			defer resp.Body.Close()
+
+			if resp.StatusCode == 200 {
+				body, err := io.ReadAll(resp.Body)
+				if err != nil {
+					continue
+				}
+				
+				content := string(body)
+				licenseType := extractLicenseType(content)
+				if licenseType != "Custom" {
+					return licenseType
+				}
+			}
+		}
+	}
+	
+	return ""
+}
+
+func tryGetLicenseFromPkgGoDev(modulePath string) string {
+	// Try to get license info from pkg.go.dev API
+	url := fmt.Sprintf("https://api.deps.dev/v3alpha/systems/go/packages/%s", modulePath)
+	
+	client := &http.Client{Timeout: 10 * time.Second}
+	resp, err := client.Get(url)
+	if err != nil {
+		return ""
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		return ""
+	}
+
+	var apiResponse struct {
+		Package struct {
+			License []struct {
+				Type string `json:"type"`
+			} `json:"license"`
+		} `json:"package"`
+	}
+
+	if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
+		return ""
+	}
+
+	if len(apiResponse.Package.License) > 0 {
+		return apiResponse.Package.License[0].Type
+	}
+
+	return ""
+}
+
+func tryGetNpmLicense(packageName string) string {
+	// Try to get license from npm registry
+	url := fmt.Sprintf("https://registry.npmjs.org/%s/latest", packageName)
+	
+	client := &http.Client{Timeout: 10 * time.Second}
+	resp, err := client.Get(url)
+	if err != nil {
+		return ""
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		return ""
+	}
+
+	var pkg struct {
+		License interface{} `json:"license"`
+	}
+
+	if err := json.NewDecoder(resp.Body).Decode(&pkg); err != nil {
+		return ""
+	}
+
+	switch v := pkg.License.(type) {
+	case string:
+		return v
+	case map[string]interface{}:
+		if t, ok := v["type"].(string); ok {
+			return t
+		}
+	}
+
+	return ""
+}
+
+func extractLicenseType(content string) string {
+	content = strings.ToUpper(content)
+	
+	licensePatterns := map[string]*regexp.Regexp{
+		"MIT":         regexp.MustCompile(`MIT\s+LICENSE`),
+		"Apache-2.0":  regexp.MustCompile(`APACHE\s+LICENSE.*VERSION\s+2\.0`),
+		"GPL-3.0":     regexp.MustCompile(`GNU\s+GENERAL\s+PUBLIC\s+LICENSE.*VERSION\s+3`),
+		"BSD-3":       regexp.MustCompile(`BSD\s+3-CLAUSE`),
+		"BSD-2":       regexp.MustCompile(`BSD\s+2-CLAUSE`),
+		"ISC":         regexp.MustCompile(`ISC\s+LICENSE`),
+		"AGPL-3.0":    regexp.MustCompile(`GNU\s+AFFERO\s+GENERAL\s+PUBLIC\s+LICENSE`),
+	}
+
+	for license, pattern := range licensePatterns {
+		if pattern.MatchString(content) {
+			return license
+		}
+	}
+
+	return "Custom"
+}
+
+func detectCommonLicense(modulePath string) string {
+	// Common patterns for detecting license types based on module paths
+	commonLicenses := map[string]string{
+		"golang.org/x":            "BSD-3-Clause",
+		"google.golang.org":       "Apache-2.0",
+		"gopkg.in":               "Various",
+		"go.uber.org":            "MIT",
+		"go.etcd.io":             "Apache-2.0",
+		"go.mongodb.org":         "Apache-2.0",
+		"go.opentelemetry.io":    "Apache-2.0",
+		"k8s.io":                 "Apache-2.0",
+		"sigs.k8s.io":            "Apache-2.0",
+		"cloud.google.com":       "Apache-2.0",
+		"go.opencensus.io":       "Apache-2.0",
+		"contrib.go.opencensus.io": "Apache-2.0",
+		"github.com/golang/":     "BSD-3-Clause",
+		"github.com/google/":     "Apache-2.0",
+		"github.com/grpc-ecosystem/": "Apache-2.0",
+		"github.com/prometheus/":    "Apache-2.0",
+		"github.com/coreos/":        "Apache-2.0",
+		"github.com/etcd-io/":       "Apache-2.0",
+		"github.com/go-kit/":        "MIT",
+		"github.com/sirupsen/":      "MIT",
+		"github.com/stretchr/":      "MIT",
+		"github.com/spf13/":         "Apache-2.0",
+		"github.com/gorilla/":       "BSD-3-Clause",
+		"github.com/gin-gonic/":     "MIT",
+		"github.com/labstack/":      "MIT",
+		"github.com/julienschmidt/": "BSD-2-Clause",
+	}
+
+	for prefix, license := range commonLicenses {
+		if strings.HasPrefix(modulePath, prefix) {
+			return license
+		}
+	}
+
+	return "Unknown"
+}
+
+func getGoVersion() string {
+	cmd := exec.Command("go", "version")
+	output, err := cmd.Output()
+	if err != nil {
+		return "Unknown"
+	}
+	
+	// Parse "go version go1.21.5 darwin/amd64" to extract "go1.21.5"
+	parts := strings.Fields(string(output))
+	if len(parts) >= 3 {
+		return parts[2] // "go1.21.5"
+	}
+	
+	return "Unknown"
+}

+ 0 - 0
cmd/map-generator/main.go → cmd/map_generator/main.go


+ 1 - 0
docs/.vitepress/config/en.ts

@@ -58,6 +58,7 @@ export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
             { text: 'Http', link: '/guide/config-http' },
             { text: 'Logrotate', link: '/guide/config-logrotate' },
             { text: 'Nginx', link: '/guide/config-nginx' },
+            { text: 'Nginx Log', link: '/guide/config-nginx-log' },
             { text: 'Node', link: '/guide/config-node' },
             { text: 'Open AI', link: '/guide/config-openai' },
             { text: 'Server', link: '/guide/config-server' },

+ 1 - 0
docs/.vitepress/config/zh_CN.ts

@@ -63,6 +63,7 @@ export const zhCNConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
             { 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: 'Nginx Log', link: '/zh_CN/guide/config-nginx-log' },
             { text: 'Node', link: '/zh_CN/guide/config-node' },
             { text: 'Open AI', link: '/zh_CN/guide/config-openai' },
             { text: 'Server', link: '/zh_CN/guide/config-server' },

+ 1 - 0
docs/.vitepress/config/zh_TW.ts

@@ -63,6 +63,7 @@ export const zhTWConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
             { 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: 'Nginx Log', link: '/zh_TW/guide/config-nginx-log' },
             { text: 'Node', link: '/zh_TW/guide/config-node' },
             { text: 'Open AI', link: '/zh_TW/guide/config-openai' },
             { text: 'Server', link: '/zh_TW/guide/config-server' },

+ 0 - 13
docs/guide/about.md

@@ -106,16 +106,3 @@ As non-native English speakers, we strive for accuracy, but we know there's alwa
 
 Thanks to our amazing community, additional languages are also available! Explore and contribute to translations on [Weblate](https://weblate.nginxui.com).
 
-## Built With
-
-- [The Go Programming Language](https://go.dev)
-- [Gin Web Framework](https://gin-gonic.com)
-- [GORM](http://gorm.io)
-- [Vue 3](https://v3.vuejs.org)
-- [Vite](https://vitejs.dev)
-- [TypeScript](https://www.typescriptlang.org/)
-- [Ant Design Vue](https://antdv.com)
-- [vue3-gettext](https://github.com/jshmrtn/vue3-gettext)
-- [vue3-ace-editor](https://github.com/CarterLi/vue3-ace-editor)
-- [Gonginx](https://github.com/tufanbarisyildirim/gonginx)
-- [lego](https://github.com/go-acme/lego)

+ 73 - 0
docs/guide/config-nginx-log.md

@@ -0,0 +1,73 @@
+# Nginx Log
+
+This section covers configuration options for Nginx log processing and analysis features in Nginx UI.
+
+## Advanced Indexing
+
+### AdvancedIndexingEnabled
+
+- Type: `boolean`
+- Default: `false`
+- Environment Variable: `NGINX_UI_NGINX_LOG_ADVANCED_INDEXING_ENABLED`
+
+This option enables advanced indexing for Nginx logs, which provides high-performance log search and analysis capabilities.
+
+### System Requirements
+
+#### Minimum Requirements
+- **CPU**: 1 core minimum
+- **Memory**: 2GB RAM minimum
+- **Storage**: At least 20GB available disk space
+
+#### Recommended Configuration
+- **CPU**: 2+ cores recommended
+- **Memory**: 4GB+ RAM recommended
+- **Storage**: SSD storage for better I/O performance
+
+### Performance Metrics
+
+Based on testing with M2 Pro (12 cores):
+
+| Metric | Value | Description |
+|--------|-------|-------------|
+| **Indexing Throughput** | 3,860it/s | Based on M2 Pro (12 cores) testing |
+| **CPU Utilization** | 90%+ | Optimized multi-core processing |
+| **Memory Efficiency** | 600MB/1Mit | Zero-allocation pipeline optimization |
+
+### Features
+
+When advanced indexing is enabled, you get access to the following features:
+
+#### Core Capabilities
+- **Zero-allocation pipeline** - Optimized memory usage for high-performance processing
+- **Dynamic shard management** - Intelligent distribution of log data across shards
+- **Incremental index scanning** - Only indexes new log entries for efficiency
+- **Automated log rotation detection** - Seamlessly handles rotated log files
+
+#### Search & Analysis
+- **Advanced search & filtering** - Complex queries with multiple criteria
+- **Full-text search with regex support** - Powerful pattern matching capabilities
+- **Cross-file timeline correlation** - Analyze events across multiple log files
+- **Error pattern recognition** - Automatic detection of error patterns
+
+#### Data Processing
+- **Compressed log file support** - Works with gzipped and other compressed formats
+- **Offline GeoIP analysis** - Location-based analytics without external services
+- **Real-time analytics dashboard** - Live monitoring and statistics
+- **Multi-dimensional data visualization** - Advanced charts and graphs
+
+### Usage Considerations
+
+::: tip Performance Impact Notice
+Enabling advanced indexing will consume system resources during log processing. The feature is designed to maximize CPU utilization for optimal indexing performance.
+:::
+
+::: info Open Source Limitation
+- Advanced log indexing features are free and open source for all users
+- We do not accept feature requests for this functionality
+- For commercial or professional use, contact business@uozi.com
+:::
+
+::: warning Initial Indexing
+When you enable advanced indexing, the system will immediately start indexing existing log files. This initial indexing process may temporarily impact system performance.
+:::

+ 5 - 0
docs/guide/env.md

@@ -89,6 +89,11 @@ Applicable for version v2.0.0-beta.37 and above.
 | StubStatusPort        | NGINX_UI_NGINX_STUB_STATUS_PORT   |
 | ContainerName         | NGINX_UI_NGINX_CONTAINER_NAME     |
 
+## Nginx Log
+| Configuration Setting  | Environment Variable                   |
+|------------------------|---------------------------------------|
+| AdvancedIndexingEnabled | NGINX_UI_NGINX_LOG_ADVANCED_INDEXING_ENABLED |
+
 ## Node
 | Configuration Setting | Environment Variable            |
 |-----------------------|---------------------------------|

+ 0 - 13
docs/zh_CN/guide/about.md

@@ -103,16 +103,3 @@ Nginx UI 可在以下平台中使用:
 
 此外,感谢我们优秀的社区提供了更多语言,欢迎访问 [Weblate](https://weblate.nginxui.com) 进行查看和贡献翻译。
 
-## 构建基于
-
-- [The Go Programming Language](https://go.dev)
-- [Gin Web Framework](https://gin-gonic.com)
-- [GORM](http://gorm.io)
-- [Vue 3](https://v3.vuejs.org)
-- [Vite](https://vitejs.dev)
-- [TypeScript](https://www.typescriptlang.org/)
-- [Ant Design Vue](https://antdv.com)
-- [vue3-gettext](https://github.com/jshmrtn/vue3-gettext)
-- [vue3-ace-editor](https://github.com/CarterLi/vue3-ace-editor)
-- [Gonginx](https://github.com/tufanbarisyildirim/gonginx)
-- [lego](https://github.com/go-acme/lego)

+ 73 - 0
docs/zh_CN/guide/config-nginx-log.md

@@ -0,0 +1,73 @@
+# Nginx Log
+
+本节介绍 Nginx UI 中 Nginx 日志处理和分析功能的配置选项。
+
+## 高级索引
+
+### AdvancedIndexingEnabled
+
+- 类型: `boolean`
+- 默认值: `false`
+- 环境变量: `NGINX_UI_NGINX_LOG_ADVANCED_INDEXING_ENABLED`
+
+此选项启用 Nginx 日志的高级索引功能,提供高性能的日志搜索和分析能力。
+
+### 系统要求
+
+#### 最低要求
+- **CPU**: 最少 1 核心
+- **内存**: 最少 2GB RAM
+- **存储**: 至少 20GB 可用磁盘空间
+
+#### 推荐配置
+- **CPU**: 建议 2 核心或以上
+- **内存**: 建议 4GB RAM 或以上
+- **存储**: 建议使用 SSD 以获得更好的 I/O 性能
+
+### 性能指标
+
+基于 M2 Pro(12 核心)的测试结果:
+
+| 指标 | 数值 | 说明 |
+|------|------|------|
+| **索引吞吐量** | 3,860it/s | 基于 M2 Pro(12 核心)测试 |
+| **CPU 利用率** | 90%+ | 优化的多核处理 |
+| **内存效率** | 600MB/1Mit | 零分配管道优化 |
+
+### 功能特性
+
+启用高级索引后,您将获得以下功能:
+
+#### 核心能力
+- **零分配管道** - 优化内存使用以实现高性能处理
+- **动态分片管理** - 智能分布日志数据到各个分片
+- **增量索引扫描** - 仅索引新的日志条目以提高效率
+- **自动日志轮转检测** - 无缝处理轮转的日志文件
+
+#### 搜索与分析
+- **高级搜索和过滤** - 支持多条件的复杂查询
+- **支持正则表达式的全文搜索** - 强大的模式匹配能力
+- **跨文件时间线关联** - 分析多个日志文件中的事件
+- **错误模式识别** - 自动检测错误模式
+
+#### 数据处理
+- **压缩日志文件支持** - 支持 gzip 和其他压缩格式
+- **离线 GeoIP 分析** - 无需外部服务的位置分析
+- **实时分析仪表板** - 实时监控和统计
+- **多维数据可视化** - 高级图表和图形
+
+### 使用注意事项
+
+::: tip 性能影响提示
+启用高级索引将在日志处理期间消耗系统资源。该功能设计为最大化 CPU 利用率以获得最佳索引性能。
+:::
+
+::: info 开源限制
+- 高级日志索引功能对所有用户免费开源
+- 我们不接受该功能的功能请求
+- 如需商业或专业使用,请联系 business@uozi.com
+:::
+
+::: warning 初始索引
+当您启用高级索引时,系统将立即开始索引现有日志文件。此初始索引过程可能会暂时影响系统性能。
+:::

+ 6 - 0
docs/zh_CN/guide/env.md

@@ -99,6 +99,12 @@
 | StubStatusPort  | NGINX_UI_NGINX_STUB_STATUS_PORT   |
 | ContainerName   | NGINX_UI_NGINX_CONTAINER_NAME     |
 
+## Nginx Log
+
+| 配置                     | 环境变量                                       |
+|-------------------------|-----------------------------------------------|
+| AdvancedIndexingEnabled | NGINX_UI_NGINX_LOG_ADVANCED_INDEXING_ENABLED |
+
 ## Node
 
 | 配置               | 环境变量                            |

+ 73 - 0
docs/zh_TW/guide/config-nginx-log.md

@@ -0,0 +1,73 @@
+# Nginx Log
+
+本節介紹 Nginx UI 中 Nginx 日誌處理和分析功能的設定選項。
+
+## 進階索引
+
+### AdvancedIndexingEnabled
+
+- 類型: `boolean`
+- 預設值: `false`
+- 環境變數: `NGINX_UI_NGINX_LOG_ADVANCED_INDEXING_ENABLED`
+
+此選項啟用 Nginx 日誌的進階索引功能,提供高效能的日誌搜尋和分析能力。
+
+### 系統需求
+
+#### 最低需求
+- **CPU**: 最少 1 核心
+- **記憶體**: 最少 2GB RAM
+- **儲存**: 至少 20GB 可用磁碟空間
+
+#### 建議配置
+- **CPU**: 建議 2 核心或以上
+- **記憶體**: 建議 4GB RAM 或以上
+- **儲存**: 建議使用 SSD 以獲得更好的 I/O 效能
+
+### 效能指標
+
+基於 M2 Pro(12 核心)的測試結果:
+
+| 指標 | 數值 | 說明 |
+|------|------|------|
+| **索引吞吐量** | 3,860it/s | 基於 M2 Pro(12 核心)測試 |
+| **CPU 使用率** | 90%+ | 最佳化的多核處理 |
+| **記憶體效率** | 600MB/1Mit | 零分配管道最佳化 |
+
+### 功能特性
+
+啟用進階索引後,您將獲得以下功能:
+
+#### 核心能力
+- **零分配管道** - 最佳化記憶體使用以實現高效能處理
+- **動態分片管理** - 智慧分布日誌資料到各個分片
+- **增量索引掃描** - 僅索引新的日誌條目以提高效率
+- **自動日誌輪轉偵測** - 無縫處理輪轉的日誌檔案
+
+#### 搜尋與分析
+- **進階搜尋和過濾** - 支援多條件的複雜查詢
+- **支援正規表示式的全文搜尋** - 強大的模式比對能力
+- **跨檔案時間線關聯** - 分析多個日誌檔案中的事件
+- **錯誤模式識別** - 自動偵測錯誤模式
+
+#### 資料處理
+- **壓縮日誌檔案支援** - 支援 gzip 和其他壓縮格式
+- **離線 GeoIP 分析** - 無需外部服務的位置分析
+- **即時分析儀表板** - 即時監控和統計
+- **多維資料視覺化** - 進階圖表和圖形
+
+### 使用注意事項
+
+::: tip 效能影響提示
+啟用進階索引將在日誌處理期間消耗系統資源。該功能設計為最大化 CPU 使用率以獲得最佳索引效能。
+:::
+
+::: info 開源限制
+- 進階日誌索引功能對所有使用者免費開源
+- 我們不接受該功能的功能請求
+- 如需商業或專業使用,請聯絡 business@uozi.com
+:::
+
+::: warning 初始索引
+當您啟用進階索引時,系統將立即開始索引現有日誌檔案。此初始索引過程可能會暫時影響系統效能。
+:::

+ 6 - 0
docs/zh_TW/guide/env.md

@@ -99,6 +99,12 @@
 | StubStatusPort  | NGINX_UI_NGINX_STUB_STATUS_PORT   |
 | ContainerName   | NGINX_UI_NGINX_CONTAINER_NAME     |
 
+## Nginx Log
+
+| 設定                     | 環境變數                                       |
+|-------------------------|-----------------------------------------------|
+| AdvancedIndexingEnabled | NGINX_UI_NGINX_LOG_ADVANCED_INDEXING_ENABLED |
+
 ## Node
 
 | 設定               | 環境變數                            |

+ 52 - 0
internal/event/websocket.go

@@ -0,0 +1,52 @@
+package event
+
+import (
+	"context"
+
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// WebSocketHubManager manages WebSocket hub initialization and context handling
+type WebSocketHubManager struct {
+	ctx    context.Context
+	cancel context.CancelFunc
+}
+
+var (
+	wsHubManager *WebSocketHubManager
+)
+
+// InitWebSocketHub initializes the WebSocket hub with proper context handling
+func InitWebSocketHub(ctx context.Context) {
+	logger.Info("Initializing WebSocket hub...")
+	
+	hubCtx, cancel := context.WithCancel(ctx)
+	wsHubManager = &WebSocketHubManager{
+		ctx:    hubCtx,
+		cancel: cancel,
+	}
+
+	logger.Info("WebSocket hub initialized successfully")
+
+	// Wait for context cancellation
+	go func() {
+		<-hubCtx.Done()
+		logger.Info("WebSocket hub context cancelled")
+	}()
+}
+
+// GetWebSocketContext returns the WebSocket hub context
+func GetWebSocketContext() context.Context {
+	if wsHubManager == nil {
+		return context.Background()
+	}
+	return wsHubManager.ctx
+}
+
+// ShutdownWebSocketHub gracefully shuts down the WebSocket hub
+func ShutdownWebSocketHub() {
+	if wsHubManager != nil {
+		wsHubManager.cancel()
+		logger.Info("WebSocket hub shutdown completed")
+	}
+}

+ 1 - 0
internal/kernel/boot.go

@@ -59,6 +59,7 @@ func Boot(ctx context.Context) {
 	syncs := []func(ctx context.Context){
 		analytic.RecordServerAnalytic,
 		event.InitEventSystem,
+		event.InitWebSocketHub,
 	}
 
 	for _, v := range async {

+ 95 - 0
internal/license/license.go

@@ -0,0 +1,95 @@
+package license
+
+import (
+	"bytes"
+	_ "embed"
+	"encoding/json"
+	"fmt"
+
+	"github.com/ulikunitz/xz"
+)
+
+//go:embed licenses.xz
+var compressedLicenses []byte
+
+type License struct {
+	Name    string `json:"name"`
+	License string `json:"license"`
+	URL     string `json:"url"`
+	Version string `json:"version"`
+}
+
+type ComponentInfo struct {
+	Backend  []License `json:"backend"`
+	Frontend []License `json:"frontend"`
+}
+
+// GetLicenseInfo returns the license information for all components
+func GetLicenseInfo() (*ComponentInfo, error) {
+	if len(compressedLicenses) == 0 {
+		return nil, fmt.Errorf("no license data available, run go generate to collect licenses")
+	}
+
+	// Decompress the xz data
+	reader, err := xz.NewReader(bytes.NewReader(compressedLicenses))
+	if err != nil {
+		return nil, fmt.Errorf("failed to create xz reader: %v", err)
+	}
+
+	var decompressed bytes.Buffer
+	_, err = decompressed.ReadFrom(reader)
+	if err != nil {
+		return nil, fmt.Errorf("failed to decompress license data: %v", err)
+	}
+
+	// Parse JSON
+	var info ComponentInfo
+	if err := json.Unmarshal(decompressed.Bytes(), &info); err != nil {
+		return nil, fmt.Errorf("failed to parse license data: %v", err)
+	}
+
+	return &info, nil
+}
+
+// GetBackendLicenses returns only backend license information
+func GetBackendLicenses() ([]License, error) {
+	info, err := GetLicenseInfo()
+	if err != nil {
+		return nil, err
+	}
+	return info.Backend, nil
+}
+
+// GetFrontendLicenses returns only frontend license information
+func GetFrontendLicenses() ([]License, error) {
+	info, err := GetLicenseInfo()
+	if err != nil {
+		return nil, err
+	}
+	return info.Frontend, nil
+}
+
+// GetLicenseStats returns statistics about the licenses
+func GetLicenseStats() (map[string]interface{}, error) {
+	info, err := GetLicenseInfo()
+	if err != nil {
+		return nil, err
+	}
+
+	stats := make(map[string]interface{})
+	stats["total_backend"] = len(info.Backend)
+	stats["total_frontend"] = len(info.Frontend)
+	stats["total"] = len(info.Backend) + len(info.Frontend)
+
+	// Count license types
+	licenseCount := make(map[string]int)
+	for _, license := range info.Backend {
+		licenseCount[license.License]++
+	}
+	for _, license := range info.Frontend {
+		licenseCount[license.License]++
+	}
+
+	stats["license_distribution"] = licenseCount
+	return stats, nil
+}

BIN
internal/license/licenses.xz


+ 8 - 3
internal/nginx_log/indexer/parallel_indexer.go

@@ -223,9 +223,14 @@ func (pi *ParallelIndexer) Stop() error {
 		// Skip flush during stop - shards may already be closed by searcher
 		// FlushAll should be called before Stop() if needed
 
-		// Close the shard manager - this will close all shards
-		// But we don't do this here because the shards might be in use by the searcher
-		// The shards will be closed when the searcher is stopped
+		// Close the shard manager - this will close all shards and stop Bleve worker goroutines
+		// This is critical to prevent goroutine leaks from Bleve's internal workers
+		if pi.shardManager != nil {
+			if err := pi.shardManager.Close(); err != nil {
+				logger.Errorf("Failed to close shard manager: %v", err)
+				stopErr = err
+			}
+		}
 	})
 
 	return stopErr

+ 5 - 0
internal/nginx_log/indexer/rebuild_test.go

@@ -64,6 +64,11 @@ func (m *mockShardManagerForRebuild) HealthCheck() error {
 	return nil
 }
 
+func (m *mockShardManagerForRebuild) Close() error {
+	atomic.StoreInt32(&m.closeCalled, 1)
+	return nil
+}
+
 type mockShard struct {
 	closed bool
 	mu     sync.Mutex

+ 1 - 0
internal/nginx_log/indexer/types.go

@@ -251,6 +251,7 @@ type ShardManager interface {
 	CloseShard(id int) error
 	OptimizeShard(id int) error
 	HealthCheck() error
+	Close() error // Close all shards and cleanup resources
 }
 
 // MetricsCollector collects and reports indexing metrics

+ 73 - 7
internal/nginx_log/task_recovery.go

@@ -3,6 +3,7 @@ package nginx_log
 import (
 	"context"
 	"fmt"
+	"sync"
 	"sync/atomic"
 	"time"
 
@@ -16,13 +17,19 @@ type TaskRecovery struct {
 	logFileManager *indexer.LogFileManager
 	modernIndexer  *indexer.ParallelIndexer
 	activeTasks    int32 // Counter for active recovery tasks
+	ctx            context.Context
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
 }
 
 // NewTaskRecovery creates a new task recovery manager
-func NewTaskRecovery() *TaskRecovery {
+func NewTaskRecovery(parentCtx context.Context) *TaskRecovery {
+	ctx, cancel := context.WithCancel(parentCtx)
 	return &TaskRecovery{
 		logFileManager: GetLogFileManager(),
 		modernIndexer:  GetModernIndexer(),
+		ctx:            ctx,
+		cancel:         cancel,
 	}
 }
 
@@ -103,16 +110,32 @@ func (tr *TaskRecovery) recoverTask(ctx context.Context, logPath string, queuePo
 		return err
 	}
 	
-	// Queue the recovery task asynchronously
-	go tr.executeRecoveredTask(ctx, logPath)
+	// Queue the recovery task asynchronously with proper context and WaitGroup
+	tr.wg.Add(1)
+	go tr.executeRecoveredTask(tr.ctx, logPath)
 	
 	return nil
 }
 
 // executeRecoveredTask executes a recovered indexing task with proper global state and progress tracking
 func (tr *TaskRecovery) executeRecoveredTask(ctx context.Context, logPath string) {
-	// Add a small delay to stagger recovery tasks
-	time.Sleep(time.Second * 2)
+	defer tr.wg.Done() // Always decrement WaitGroup
+	
+	// Check context before starting
+	select {
+	case <-ctx.Done():
+		logger.Infof("Context cancelled, skipping recovery task for %s", logPath)
+		return
+	default:
+	}
+	
+	// Add a small delay to stagger recovery tasks, but check context
+	select {
+	case <-time.After(time.Second * 2):
+	case <-ctx.Done():
+		logger.Infof("Context cancelled during delay, skipping recovery task for %s", logPath)
+		return
+	}
 	
 	logger.Infof("Executing recovered indexing task: %s", logPath)
 	
@@ -199,6 +222,14 @@ func (tr *TaskRecovery) executeRecoveredTask(ctx context.Context, logPath string
 		},
 	}
 	
+	// Check context before starting indexing
+	select {
+	case <-ctx.Done():
+		logger.Infof("Context cancelled before indexing, stopping recovery task for %s", logPath)
+		return
+	default:
+	}
+	
 	// Execute the indexing with progress tracking
 	startTime := time.Now()
 	docsCountMap, minTime, maxTime, err := tr.modernIndexer.IndexLogGroupWithProgress(logPath, progressConfig)
@@ -247,6 +278,33 @@ func (tr *TaskRecovery) setTaskStatus(logPath, status string, queuePosition int)
 	return persistence.SetIndexStatus(logPath, status, queuePosition, "")
 }
 
+// Shutdown gracefully stops all recovery tasks
+func (tr *TaskRecovery) Shutdown() {
+	logger.Info("Shutting down task recovery system...")
+	
+	// Cancel all active tasks
+	tr.cancel()
+	
+	// Wait for all tasks to complete with timeout
+	done := make(chan struct{})
+	go func() {
+		tr.wg.Wait()
+		close(done)
+	}()
+	
+	select {
+	case <-done:
+		logger.Info("All recovery tasks completed successfully")
+	case <-time.After(30 * time.Second):
+		logger.Warn("Timeout waiting for recovery tasks to complete")
+	}
+	
+	logger.Info("Task recovery system shutdown completed")
+}
+
+// Global task recovery manager
+var globalTaskRecovery *TaskRecovery
+
 // InitTaskRecovery initializes the task recovery system - called during application startup
 func InitTaskRecovery(ctx context.Context) {
 	logger.Info("Initializing task recovery system")
@@ -254,8 +312,16 @@ func InitTaskRecovery(ctx context.Context) {
 	// Wait a bit for services to fully initialize
 	time.Sleep(3 * time.Second)
 	
-	recoveryManager := NewTaskRecovery()
-	if err := recoveryManager.RecoverUnfinishedTasks(ctx); err != nil {
+	globalTaskRecovery = NewTaskRecovery(ctx)
+	if err := globalTaskRecovery.RecoverUnfinishedTasks(ctx); err != nil {
 		logger.Errorf("Failed to recover unfinished tasks: %v", err)
 	}
+	
+	// Monitor context for shutdown
+	go func() {
+		<-ctx.Done()
+		if globalTaskRecovery != nil {
+			globalTaskRecovery.Shutdown()
+		}
+	}()
 }

+ 21 - 1
query/nginx_log_indices.gen.go

@@ -43,6 +43,11 @@ func newNginxLogIndex(db *gorm.DB, opts ...gen.DOOption) nginxLogIndex {
 	_nginxLogIndex.TimeRangeEnd = field.NewTime(tableName, "time_range_end")
 	_nginxLogIndex.DocumentCount = field.NewUint64(tableName, "document_count")
 	_nginxLogIndex.Enabled = field.NewBool(tableName, "enabled")
+	_nginxLogIndex.IndexStatus = field.NewString(tableName, "index_status")
+	_nginxLogIndex.ErrorMessage = field.NewString(tableName, "error_message")
+	_nginxLogIndex.ErrorTime = field.NewTime(tableName, "error_time")
+	_nginxLogIndex.RetryCount = field.NewInt(tableName, "retry_count")
+	_nginxLogIndex.QueuePosition = field.NewInt(tableName, "queue_position")
 
 	_nginxLogIndex.fillFieldMap()
 
@@ -68,6 +73,11 @@ type nginxLogIndex struct {
 	TimeRangeEnd   field.Time
 	DocumentCount  field.Uint64
 	Enabled        field.Bool
+	IndexStatus    field.String
+	ErrorMessage   field.String
+	ErrorTime      field.Time
+	RetryCount     field.Int
+	QueuePosition  field.Int
 
 	fieldMap map[string]field.Expr
 }
@@ -99,6 +109,11 @@ func (n *nginxLogIndex) updateTableName(table string) *nginxLogIndex {
 	n.TimeRangeEnd = field.NewTime(table, "time_range_end")
 	n.DocumentCount = field.NewUint64(table, "document_count")
 	n.Enabled = field.NewBool(table, "enabled")
+	n.IndexStatus = field.NewString(table, "index_status")
+	n.ErrorMessage = field.NewString(table, "error_message")
+	n.ErrorTime = field.NewTime(table, "error_time")
+	n.RetryCount = field.NewInt(table, "retry_count")
+	n.QueuePosition = field.NewInt(table, "queue_position")
 
 	n.fillFieldMap()
 
@@ -115,7 +130,7 @@ func (n *nginxLogIndex) GetFieldByName(fieldName string) (field.OrderExpr, bool)
 }
 
 func (n *nginxLogIndex) fillFieldMap() {
-	n.fieldMap = make(map[string]field.Expr, 15)
+	n.fieldMap = make(map[string]field.Expr, 20)
 	n.fieldMap["id"] = n.ID
 	n.fieldMap["created_at"] = n.CreatedAt
 	n.fieldMap["updated_at"] = n.UpdatedAt
@@ -131,6 +146,11 @@ func (n *nginxLogIndex) fillFieldMap() {
 	n.fieldMap["time_range_end"] = n.TimeRangeEnd
 	n.fieldMap["document_count"] = n.DocumentCount
 	n.fieldMap["enabled"] = n.Enabled
+	n.fieldMap["index_status"] = n.IndexStatus
+	n.fieldMap["error_message"] = n.ErrorMessage
+	n.fieldMap["error_time"] = n.ErrorTime
+	n.fieldMap["retry_count"] = n.RetryCount
+	n.fieldMap["queue_position"] = n.QueuePosition
 }
 
 func (n nginxLogIndex) clone(db *gorm.DB) nginxLogIndex {

+ 3 - 0
resources/demo/app.ini

@@ -56,6 +56,9 @@ TestConfigCmd   =
 ReloadCmd       =
 RestartCmd      =
 
+[nginx_log]
+AdvancedIndexingEnabled = true
+
 [node]
 Name             =
 Secret           = 57D079F2-CA8B-412A-B5C0-FDA291C13391

+ 1 - 15
README-es.md → resources/readme/README-es.md

@@ -16,7 +16,7 @@ Para consultar la documentación, visite [nginxui.com](https://nginxui.com).
 [![Stargazers over time](https://starchart.cc/0xJacky/nginx-ui.svg)](https://starchart.cc/0xJacky/nginx-ui)
 
 
-[English](README.md) | Español | [简体中文](README-zh_CN.md) | [繁體中文](README-zh_TW.md)
+[English](../../README.md) | Español | [简体中文](README-zh_CN.md) | [繁體中文](README-zh_TW.md)
 
 <details>
   <summary>Tabla de Contenidos</summary>
@@ -27,7 +27,6 @@ Para consultar la documentación, visite [nginxui.com](https://nginxui.com).
         <li><a href="#demo">Demostración</a></li>
         <li><a href="#features">Características</a></li>
         <li><a href="#internationalization">Internacionalización</a></li>
-        <li><a href="#built-with">Desarrollado con</a></li>
       </ul>
     </li>
     <li>
@@ -97,19 +96,6 @@ URL:[https://demo.nginxui.com](https://demo.nginxui.com)
 
 Aceptamos traducciones a cualquier idioma.
 
-### Desarrollado con
-
-- [El lenguaje de programación Go](https://go.dev)
-- [Gin Web Framework](https://gin-gonic.com)
-- [GORM](http://gorm.io)
-- [Vue 3](https://v3.vuejs.org)
-- [Vite](https://vitejs.dev)
-- [TypeScript](https://www.typescriptlang.org/)
-- [Ant Design Vue](https://antdv.com)
-- [vue3-gettext](https://github.com/jshmrtn/vue3-gettext)
-- [vue3-ace-editor](https://github.com/CarterLi/vue3-ace-editor)
-- [Gonginx](https://github.com/tufanbarisyildirim/gonginx)
-- [lego](https://github.com/go-acme/lego)
 
 ## Cómo empezar
 

+ 1 - 15
README-ja_JP.md → resources/readme/README-ja_JP.md

@@ -30,7 +30,7 @@
 
 [![Stargazers over time](https://starchart.cc/0xJacky/nginx-ui.svg)](https://starchart.cc/0xJacky/nginx-ui)
 
-English | [Español](README-es.md) | [简体中文](README-zh_CN.md) | [繁體中文](README-zh_TW.md) | [Tiếng Việt](README-vi_VN.md) | [日本語](README-ja_JP.md)
+[English](../../README.md) | [Español](README-es.md) | [简体中文](README-zh_CN.md) | [繁體中文](README-zh_TW.md) | [Tiếng Việt](README-vi_VN.md) | [日本語](README-ja_JP.md)
 
 <details>
   <summary>目次</summary>
@@ -41,7 +41,6 @@ English | [Español](README-es.md) | [简体中文](README-zh_CN.md) | [繁體
         <li><a href="#demo">デモ</a></li>
         <li><a href="#features">機能</a></li>
         <li><a href="#internationalization">多言語化</a></li>
-        <li><a href="#built-with">主要技術</a></li>
       </ul>
     </li>
     <li>
@@ -118,19 +117,6 @@ URL:[https://demo.nginxui.com](https://demo.nginxui.com)
 
 コミュニティのおかげで他の言語もいろいろ揃っとるで。翻訳に参加したい人は [Weblate](https://weblate.nginxui.com) 見てみてな。
 
-### 主要技術
-
-- [Go言語](https://go.dev)
-- [Gin Web Framework](https://gin-gonic.com)
-- [GORM](http://gorm.io)
-- [Vue 3](https://v3.vuejs.org)
-- [Vite](https://vitejs.dev)
-- [TypeScript](https://www.typescriptlang.org/)
-- [Ant Design Vue](https://antdv.com)
-- [vue3-gettext](https://github.com/jshmrtn/vue3-gettext)
-- [vue3-ace-editor](https://github.com/CarterLi/vue3-ace-editor)
-- [Gonginx](https://github.com/tufanbarisyildirim/gonginx)
-- [lego](https://github.com/go-acme/lego)
 
 ## はじめに
 

+ 1 - 15
README-vi_VN.md → resources/readme/README-vi_VN.md

@@ -25,7 +25,7 @@ Yet another Nginx Web UI, được phát triển bởi [0xJacky](https://jackyu.
 
 [![Stargazers over time](https://starchart.cc/0xJacky/nginx-ui.svg)](https://starchart.cc/0xJacky/nginx-ui)
 
-English | [Español](README-es.md) | [简体中文](README-zh_CN.md) | [繁體中文](README-zh_TW.md) | [Tiếng Việt](README-vi_VN.md)
+[English](../../README.md) | [Español](README-es.md) | [简体中文](README-zh_CN.md) | [繁體中文](README-zh_TW.md) | [Tiếng Việt](README-vi_VN.md)
 
 <details>
   <summary>Mục lục</summary>
@@ -36,7 +36,6 @@ English | [Español](README-es.md) | [简体中文](README-zh_CN.md) | [繁體
         <li><a href="#demo">Demo</a></li>
         <li><a href="#features">Tính năng</a></li>
         <li><a href="#internationalization">Ngôn ngữ hiển thị</a></li>
-        <li><a href="#built-with">Được xây dựng với</a></li>
       </ul>
     </li>
     <li>
@@ -109,19 +108,6 @@ URL:[https://demo.nginxui.com](https://demo.nginxui.com)
 
 Chúng tôi hoan nghênh bản dịch sang bất kỳ ngôn ngữ nào.
 
-### Được xây dựng với
-
-- [The Go Programming Language](https://go.dev)
-- [Gin Web Framework](https://gin-gonic.com)
-- [GORM](http://gorm.io)
-- [Vue 3](https://v3.vuejs.org)
-- [Vite](https://vitejs.dev)
-- [TypeScript](https://www.typescriptlang.org/)
-- [Ant Design Vue](https://antdv.com)
-- [vue3-gettext](https://github.com/jshmrtn/vue3-gettext)
-- [vue3-ace-editor](https://github.com/CarterLi/vue3-ace-editor)
-- [Gonginx](https://github.com/tufanbarisyildirim/gonginx)
-- [lego](https://github.com/go-acme/lego)
 
 ## Bắt đầu
 

+ 1 - 14
README-zh_CN.md → resources/readme/README-zh_CN.md

@@ -10,7 +10,7 @@ Nginx 网络管理界面,由 [0xJacky](https://jackyu.cn/)、[Hintay](https://
 
 [![Build and Publish](https://github.com/0xJacky/nginx-ui/actions/workflows/build.yml/badge.svg)](https://github.com/0xJacky/nginx-ui/actions/workflows/build.yml)
 
-[English](README.md) | [Español](README-es.md) | 简体中文 | [繁體中文](README-zh_TW.md)
+[English](../../README.md) | [Español](README-es.md) | 简体中文 | [繁體中文](README-zh_TW.md)
 
 <details>
   <summary>目录</summary>
@@ -21,7 +21,6 @@ Nginx 网络管理界面,由 [0xJacky](https://jackyu.cn/)、[Hintay](https://
         <li><a href="#在线预览">在线预览</a></li>
         <li><a href="#特色">特色</a></li>
         <li><a href="#国际化">国际化</a></li>
-        <li><a href="#构建基于">构建基于</a></li>
       </ul>
     </li>
     <li>
@@ -95,18 +94,6 @@ Nginx 网络管理界面,由 [0xJacky](https://jackyu.cn/)、[Hintay](https://
 
 我们欢迎您将项目翻译成任何语言。
 
-### 构建基于
-- [The Go Programming Language](https://go.dev)
-- [Gin Web Framework](https://gin-gonic.com)
-- [GORM](http://gorm.io)
-- [Vue 3](https://v3.vuejs.org)
-- [Vite](https://vitejs.dev)
-- [TypeScript](https://www.typescriptlang.org/)
-- [Ant Design Vue](https://antdv.com)
-- [vue3-gettext](https://github.com/jshmrtn/vue3-gettext)
-- [vue3-ace-editor](https://github.com/CarterLi/vue3-ace-editor)
-- [Gonginx](https://github.com/tufanbarisyildirim/gonginx)
-- [lego](https://github.com/go-acme/lego)
 
 ## 入门指南
 

+ 1 - 15
README-zh_TW.md → resources/readme/README-zh_TW.md

@@ -10,7 +10,7 @@ Nginx 網路管理介面,由 [0xJacky](https://jackyu.cn/)、[Hintay](https://
 
 [![Build and Publish](https://github.com/0xJacky/nginx-ui/actions/workflows/build.yml/badge.svg)](https://github.com/0xJacky/nginx-ui/actions/workflows/build.yml)
 
-[English](README.md) | [Español](README-es.md) | [简体中文](README-zh_CN.md) | 繁體中文
+[English](../../README.md) | [Español](README-es.md) | [简体中文](README-zh_CN.md) | 繁體中文
 
 <details>
   <summary>目錄</summary>
@@ -21,7 +21,6 @@ Nginx 網路管理介面,由 [0xJacky](https://jackyu.cn/)、[Hintay](https://
         <li><a href="#線上預覽">線上預覽</a></li>
         <li><a href="#特色">特色</a></li>
         <li><a href="#國際化">國際化</a></li>
-        <li><a href="#建置基於">建置基於</a></li>
       </ul>
     </li>
     <li>
@@ -97,19 +96,6 @@ Nginx 網路管理介面,由 [0xJacky](https://jackyu.cn/)、[Hintay](https://
 
 我們歡迎您將專案翻譯成任何語言。
 
-### 建置基於
-
-- [The Go Programming Language](https://go.dev)
-- [Gin Web Framework](https://gin-gonic.com)
-- [GORM](http://gorm.io)
-- [Vue 3](https://v3.vuejs.org)
-- [Vite](https://vitejs.dev)
-- [TypeScript](https://www.typescriptlang.org/)
-- [Ant Design Vue](https://antdv.com)
-- [vue3-gettext](https://github.com/jshmrtn/vue3-gettext)
-- [vue3-ace-editor](https://github.com/CarterLi/vue3-ace-editor)
-- [Gonginx](https://github.com/tufanbarisyildirim/gonginx)
-- [lego](https://github.com/go-acme/lego)
 
 ## 入門指南
 

+ 2 - 0
router/routers.go

@@ -13,6 +13,7 @@ import (
 	"github.com/0xJacky/Nginx-UI/api/crypto"
 	"github.com/0xJacky/Nginx-UI/api/event"
 	"github.com/0xJacky/Nginx-UI/api/external_notify"
+	"github.com/0xJacky/Nginx-UI/api/license"
 	"github.com/0xJacky/Nginx-UI/api/nginx"
 	nginxLog "github.com/0xJacky/Nginx-UI/api/nginx_log"
 	"github.com/0xJacky/Nginx-UI/api/notification"
@@ -59,6 +60,7 @@ func InitRouter() {
 		public.InitRouter(root)
 		crypto.InitPublicRouter(root)
 		user.InitAuthRouter(root)
+		license.InitRouter(root)
 
 		system.InitPublicRouter(root)
 		system.InitSelfCheckRouter(root)

+ 12 - 12
settings/nginx.go

@@ -1,18 +1,18 @@
 package settings
 
 type Nginx struct {
-	AccessLogPath   string   `json:"access_log_path" protected:"true"`
-	ErrorLogPath    string   `json:"error_log_path" protected:"true"`
-	LogDirWhiteList []string `json:"log_dir_white_list" protected:"true"`
-	ConfigDir       string   `json:"config_dir" protected:"true"`
-	ConfigPath      string   `json:"config_path" protected:"true"`
-	PIDPath         string   `json:"pid_path" protected:"true"`
-	SbinPath        string   `json:"sbin_path" protected:"true"`
-	TestConfigCmd   string   `json:"test_config_cmd" protected:"true"`
-	ReloadCmd       string   `json:"reload_cmd" protected:"true"`
-	RestartCmd      string   `json:"restart_cmd" protected:"true"`
-	StubStatusPort  uint     `json:"stub_status_port" binding:"omitempty,min=1,max=65535"`
-	ContainerName   string   `json:"container_name" protected:"true"`
+	AccessLogPath            string   `json:"access_log_path" protected:"true"`
+	ErrorLogPath             string   `json:"error_log_path" protected:"true"`
+	LogDirWhiteList          []string `json:"log_dir_white_list" protected:"true"`
+	ConfigDir                string   `json:"config_dir" protected:"true"`
+	ConfigPath               string   `json:"config_path" protected:"true"`
+	PIDPath                  string   `json:"pid_path" protected:"true"`
+	SbinPath                 string   `json:"sbin_path" protected:"true"`
+	TestConfigCmd            string   `json:"test_config_cmd" protected:"true"`
+	ReloadCmd                string   `json:"reload_cmd" protected:"true"`
+	RestartCmd               string   `json:"restart_cmd" protected:"true"`
+	StubStatusPort           uint     `json:"stub_status_port" binding:"omitempty,min=1,max=65535"`
+	ContainerName            string   `json:"container_name" protected:"true"`
 }
 
 var NginxSettings = &Nginx{}

+ 7 - 0
settings/nginx_log.go

@@ -0,0 +1,7 @@
+package settings
+
+type NginxLog struct {
+	AdvancedIndexingEnabled bool `json:"advanced_indexing_enabled"`
+}
+
+var NginxLogSettings = &NginxLog{}

+ 2 - 0
settings/settings.go

@@ -34,6 +34,7 @@ var envPrefixMap = map[string]interface{}{
 	"HTTP":      HTTPSettings,
 	"LOGROTATE": LogrotateSettings,
 	"NGINX":     NginxSettings,
+	"NGINX_LOG": NginxLogSettings,
 	"NODE":      NodeSettings,
 	"OPENAI":    OpenAISettings,
 	"TERMINAL":  TerminalSettings,
@@ -55,6 +56,7 @@ func init() {
 	sections.Set("http", HTTPSettings)
 	sections.Set("logrotate", LogrotateSettings)
 	sections.Set("nginx", NginxSettings)
+	sections.Set("nginx_log", NginxLogSettings)
 	sections.Set("node", NodeSettings)
 	sections.Set("openai", OpenAISettings)
 	sections.Set("terminal", TerminalSettings)

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor