feat: allow disabling proxy targets availability test #1327

This commit is contained in:
0xJacky
2025-10-03 13:51:12 +00:00
parent ccedb94880
commit de0467b9e7
46 changed files with 5483 additions and 3707 deletions
+3 -1
View File
@@ -15,7 +15,9 @@
"Bash(find:*)",
"Bash(sed:*)",
"Bash(cp:*)",
"mcp__eslint__lint-files"
"mcp__eslint__lint-files",
"Bash(go generate:*)",
"Bash(pnpm eslint:*)"
],
"deny": []
}
+35
View File
@@ -0,0 +1,35 @@
package upstream
import (
"github.com/0xJacky/Nginx-UI/internal/upstream"
"github.com/0xJacky/Nginx-UI/model"
"github.com/uozi-tech/cosy/logger"
)
func init() {
// Register the disabled sockets checker callback
service := upstream.GetUpstreamService()
service.SetDisabledSocketsChecker(getDisabledSockets)
}
// getDisabledSockets queries the database for disabled sockets
func getDisabledSockets() map[string]bool {
disabled := make(map[string]bool)
db := model.UseDB()
if db == nil {
return disabled
}
var configs []model.UpstreamConfig
if err := db.Where("enabled = ?", false).Find(&configs).Error; err != nil {
logger.Error("Failed to query disabled sockets:", err)
return disabled
}
for _, config := range configs {
disabled[config.Socket] = true
}
return disabled
}
+131
View File
@@ -0,0 +1,131 @@
package upstream
import (
"net/http"
"sort"
"github.com/0xJacky/Nginx-UI/internal/upstream"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger"
)
// UpstreamInfo represents an upstream with its configuration and health status
type UpstreamInfo struct {
Name string `json:"name"`
Servers []upstream.ProxyTarget `json:"servers"`
ConfigPath string `json:"config_path"`
LastSeen string `json:"last_seen"`
Status map[string]*upstream.Status `json:"status"`
Enabled bool `json:"enabled"`
}
// GetUpstreamList returns all upstreams with their configuration and health status
func GetUpstreamList(c *gin.Context) {
service := upstream.GetUpstreamService()
// Get all upstream definitions
upstreams := service.GetAllUpstreamDefinitions()
// Get availability map
availabilityMap := service.GetAvailabilityMap()
// Get all upstream configurations from database
u := query.UpstreamConfig
configs, err := u.Find()
if err != nil {
cosy.ErrHandler(c, err)
return
}
// Create a map for quick lookup of enabled status by upstream name
configMap := make(map[string]bool)
for _, config := range configs {
configMap[config.Socket] = config.Enabled
}
// Build response
result := make([]UpstreamInfo, 0, len(upstreams))
for name, def := range upstreams {
// Get enabled status from database, default to true if not found
enabled := true
if val, exists := configMap[name]; exists {
enabled = val
}
// Get status for each server in this upstream
serverStatus := make(map[string]*upstream.Status)
for _, server := range def.Servers {
key := formatSocketAddress(server.Host, server.Port)
if status, exists := availabilityMap[key]; exists {
serverStatus[key] = status
}
}
info := UpstreamInfo{
Name: name,
Servers: def.Servers,
ConfigPath: def.ConfigPath,
LastSeen: def.LastSeen.Format("2006-01-02 15:04:05"),
Status: serverStatus,
Enabled: enabled,
}
result = append(result, info)
}
// Sort by name for stable ordering
sort.Slice(result, func(i, j int) bool {
return result[i].Name < result[j].Name
})
c.JSON(http.StatusOK, gin.H{
"data": result,
})
}
// UpdateUpstreamConfigRequest represents the request body for updating upstream config
type UpdateUpstreamConfigRequest struct {
Enabled bool `json:"enabled"`
}
// UpdateUpstreamConfig updates the enabled status of an upstream
func UpdateUpstreamConfig(c *gin.Context) {
name := c.Param("name")
var req UpdateUpstreamConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
cosy.ErrHandler(c, err)
return
}
u := query.UpstreamConfig
// Check if config exists
config, err := u.Where(u.Socket.Eq(name)).First()
if err != nil {
// Create new config if not found
config = &model.UpstreamConfig{
Socket: name,
Enabled: req.Enabled,
}
if err := u.Create(config); err != nil {
logger.Error("Failed to create upstream config:", err)
cosy.ErrHandler(c, err)
return
}
} else {
// Update existing config
if _, err := u.Where(u.Socket.Eq(name)).Update(u.Enabled, req.Enabled); err != nil {
logger.Error("Failed to update upstream config:", err)
cosy.ErrHandler(c, err)
return
}
}
c.JSON(http.StatusOK, gin.H{
"message": "Upstream config updated successfully",
})
}
+2
View File
@@ -5,4 +5,6 @@ import "github.com/gin-gonic/gin"
func InitRouter(r *gin.RouterGroup) {
r.GET("/upstream/availability", GetAvailability)
r.GET("/upstream/availability_ws", AvailabilityWebSocket)
r.GET("/upstream/sockets", GetSocketList)
r.PUT("/upstream/socket/:socket", UpdateSocketConfig)
}
+155
View File
@@ -0,0 +1,155 @@
package upstream
import (
"net/http"
"sort"
"github.com/0xJacky/Nginx-UI/internal/upstream"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger"
)
// SocketInfo represents a socket with its configuration and health status
type SocketInfo struct {
Socket string `json:"socket"` // host:port
Host string `json:"host"` // hostname/IP
Port string `json:"port"` // port number
Type string `json:"type"` // proxy_pass, grpc_pass, or upstream
IsConsul bool `json:"is_consul"` // whether this is a consul service
UpstreamName string `json:"upstream_name"` // which upstream this belongs to (if any)
LastCheck string `json:"last_check"` // last time health check was performed
Status *upstream.Status `json:"status"` // health check status
Enabled bool `json:"enabled"` // whether health check is enabled
}
// GetSocketList returns all sockets with their configuration and health status
func GetSocketList(c *gin.Context) {
service := upstream.GetUpstreamService()
// Get all target infos
targets := service.GetTargetInfos()
// Get availability map
availabilityMap := service.GetAvailabilityMap()
// Get all socket configurations from database
u := query.UpstreamConfig
configs, err := u.Find()
if err != nil {
cosy.ErrHandler(c, err)
return
}
// Create a map for quick lookup of enabled status
configMap := make(map[string]bool)
for _, config := range configs {
configMap[config.Socket] = config.Enabled
}
// Build response
result := make([]SocketInfo, 0, len(targets))
for _, target := range targets {
socketAddr := formatSocketAddress(target.Host, target.Port)
// Get enabled status from database, default to true if not found
enabled := true
if val, exists := configMap[socketAddr]; exists {
enabled = val
}
// Get health status
var status *upstream.Status
if s, exists := availabilityMap[socketAddr]; exists {
status = s
}
// Find which upstream this belongs to
upstreamName := findUpstreamForSocket(service, target.ProxyTarget)
info := SocketInfo{
Socket: socketAddr,
Host: target.Host,
Port: target.Port,
Type: target.Type,
IsConsul: target.IsConsul,
UpstreamName: upstreamName,
LastCheck: target.LastSeen.Format("2006-01-02 15:04:05"),
Status: status,
Enabled: enabled,
}
result = append(result, info)
}
// Sort by socket address for stable ordering
sort.Slice(result, func(i, j int) bool {
return result[i].Socket < result[j].Socket
})
c.JSON(http.StatusOK, gin.H{
"data": result,
})
}
// UpdateSocketConfigRequest represents the request body for updating socket config
type UpdateSocketConfigRequest struct {
Enabled bool `json:"enabled"`
}
// UpdateSocketConfig updates the enabled status of a socket
func UpdateSocketConfig(c *gin.Context) {
socket := c.Param("socket")
var req UpdateSocketConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
cosy.ErrHandler(c, err)
return
}
u := query.UpstreamConfig
// Check if config exists
config, err := u.Where(u.Socket.Eq(socket)).First()
if err != nil {
// Create new config if not found
config = &model.UpstreamConfig{
Socket: socket,
Enabled: req.Enabled,
}
if err := u.Create(config); err != nil {
logger.Error("Failed to create socket config:", err)
cosy.ErrHandler(c, err)
return
}
} else {
// Update existing config
if _, err := u.Where(u.Socket.Eq(socket)).Update(u.Enabled, req.Enabled); err != nil {
logger.Error("Failed to update socket config:", err)
cosy.ErrHandler(c, err)
return
}
}
c.JSON(http.StatusOK, gin.H{
"message": "Socket config updated successfully",
})
}
// findUpstreamForSocket finds which upstream a socket belongs to
func findUpstreamForSocket(service *upstream.Service, target upstream.ProxyTarget) string {
socketAddr := formatSocketAddress(target.Host, target.Port)
upstreams := service.GetAllUpstreamDefinitions()
for name, upstream := range upstreams {
for _, server := range upstream.Servers {
serverAddr := formatSocketAddress(server.Host, server.Port)
if serverAddr == socketAddr {
return name
}
}
}
return ""
}
+21
View File
@@ -0,0 +1,21 @@
package upstream
// formatSocketAddress formats a host:port combination into a proper socket address
// For IPv6 addresses, it adds brackets around the host if they're not already present
func formatSocketAddress(host, port string) string {
// Reuse the logic from service package
if len(host) > 0 && host[0] != '[' && containsColon(host) {
return "[" + host + "]:" + port
}
return host + ":" + port
}
// containsColon checks if string contains a colon
func containsColon(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] == ':' {
return true
}
}
return false
}
-30
View File
@@ -10,29 +10,16 @@ declare module 'vue' {
export interface GlobalComponents {
AAlert: typeof import('ant-design-vue/es')['Alert']
AApp: typeof import('ant-design-vue/es')['App']
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge']
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col']
ACollapse: typeof import('ant-design-vue/es')['Collapse']
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input']
AInputGroup: typeof import('ant-design-vue/es')['InputGroup']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
ALayout: typeof import('ant-design-vue/es')['Layout']
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter']
@@ -43,34 +30,17 @@ declare module 'vue' {
AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
APopover: typeof import('ant-design-vue/es')['Popover']
AppProvider: typeof import('./src/components/AppProvider.vue')['default']
AProgress: typeof import('ant-design-vue/es')['Progress']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
AResult: typeof import('ant-design-vue/es')['Result']
ARow: typeof import('ant-design-vue/es')['Row']
ASegmented: typeof import('ant-design-vue/es')['Segmented']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic']
AStep: typeof import('ant-design-vue/es')['Step']
ASteps: typeof import('ant-design-vue/es')['Steps']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
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']
AutoCertFormAutoCertForm: typeof import('./src/components/AutoCertForm/AutoCertForm.vue')['default']
AutoCertFormDNSChallenge: typeof import('./src/components/AutoCertForm/DNSChallenge.vue')['default']
BaseEditorBaseEditor: typeof import('./src/components/BaseEditor/BaseEditor.vue')['default']
+30
View File
@@ -19,6 +19,26 @@ export interface UpstreamAvailabilityResponse {
target_count: number
}
export interface SocketInfo {
socket: string
host: string
port: string
type: string
is_consul: boolean
upstream_name: string
last_check: string
status: UpstreamStatus | null
enabled: boolean
}
export interface SocketListResponse {
data: SocketInfo[]
}
export interface UpdateSocketConfigRequest {
enabled: boolean
}
const upstream = {
// HTTP GET interface to get all upstream availability results
getAvailability(): Promise<UpstreamAvailabilityResponse> {
@@ -29,6 +49,16 @@ const upstream = {
availabilityWebSocket() {
return ws('/api/upstream/availability_ws')
},
// Get all sockets with their configuration and health status
getSocketList(): Promise<SocketListResponse> {
return http.get('/upstream/sockets')
},
// Update socket configuration
updateSocketConfig(socket: string, data: UpdateSocketConfigRequest) {
return http.put(`/upstream/socket/${encodeURIComponent(socket)}`, data)
},
}
export default upstream
+3
View File
@@ -0,0 +1,3 @@
export default {
54001: () => $gettext('Node analytics failed: {0}'),
}
+1
View File
@@ -22,4 +22,5 @@ export default {
50021: () => $gettext('Write private.key error: {0}'),
50022: () => $gettext('Obtain cert error: {0}'),
50023: () => $gettext('Revoke cert error: {0}'),
50031: () => $gettext('No certificate available'),
}
+2
View File
@@ -12,4 +12,6 @@ export default {
500011: () => $gettext('Failed to inspect current container: {0}'),
500012: () => $gettext('Failed to create temp container: {0}'),
500013: () => $gettext('Failed to start temp container: {0}'),
500014: () => $gettext('Could not find old container name'),
500015: () => $gettext('Could not find temp container'),
}
@@ -0,0 +1,3 @@
export default {
50201: () => $gettext('Log parser is not initialized; call indexer.InitLogParser() before use'),
}
@@ -0,0 +1,6 @@
export default {
50101: () => $gettext('Empty log line'),
50102: () => $gettext('Log line exceeds maximum length'),
50103: () => $gettext('Unsupported log format'),
50104: () => $gettext('Invalid timestamp format'),
}
-3
View File
@@ -6,9 +6,6 @@ export default {
50005: () => $gettext('Directive params is empty'),
50006: () => $gettext('Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://nginxui.com/guide/config-nginx.html for more information'),
50007: () => $gettext('Settings.NginxLogSettings.AccessLogPath is empty, refer to https://nginxui.com/guide/config-nginx.html for more information'),
50008: () => $gettext('Empty log line'),
50009: () => $gettext('Invalid timestamp format'),
50010: () => $gettext('Unsupported log format'),
50011: () => $gettext('Log indexer not available'),
50012: () => $gettext('Analytics service not available'),
50013: () => $gettext('Log file does not exist'),
+1
View File
@@ -3,4 +3,5 @@ export default {
400001: () => $gettext('Invalid notifier config'),
400002: () => $gettext('Invalid notification ID'),
404002: () => $gettext('External notification configuration not found'),
400003: () => $gettext('Invalid Telegram Chat ID: cannot be zero'),
}
+1
View File
@@ -6,4 +6,5 @@ export default {
51004: () => $gettext('Failed to execute template: {0}'),
51005: () => $gettext('Failed to parse nginx config: {0}'),
51006: () => $gettext('Failed to build nginx config: {0}'),
51007: () => $gettext('Failed to get nginx.conf path'),
}
+7
View File
@@ -0,0 +1,7 @@
export default {
52001: () => $gettext('Upgrader core downloadUrl is empty'),
52002: () => $gettext('Upgrader core digest is empty'),
52003: () => $gettext('Digest file content is empty'),
52004: () => $gettext('Executable binary file is empty'),
52005: () => $gettext('Update already in progress'),
}
+3
View File
@@ -12,4 +12,7 @@ export default {
40401: () => $gettext('Session not found'),
40402: () => $gettext('Token is empty'),
50005: () => $gettext('Invalid claims type'),
50006: () => $gettext('Config not found'),
50007: () => $gettext('Db file not found'),
50008: () => $gettext('Init user not exists'),
}
+4
View File
@@ -0,0 +1,4 @@
export default {
53001: () => $gettext('Invalid commit SHA'),
53002: () => $gettext('Release API request failed: {0}'),
}
+289 -209
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+125 -24
View File
@@ -297,7 +297,7 @@ msgid ""
"certificate application will fail."
msgstr ""
#: src/constants/errors/nginx_log.ts:13
#: src/constants/errors/nginx_log.ts:10
msgid "Analytics service not available"
msgstr ""
@@ -543,7 +543,7 @@ msgid ""
"ready."
msgstr ""
#: src/constants/errors/nginx_log.ts:17
#: src/constants/errors/nginx_log.ts:14
msgid "Background log service not available"
msgstr ""
@@ -748,7 +748,7 @@ msgstr ""
msgid "Cannot access backup path {0}: {1}"
msgstr ""
#: src/constants/errors/nginx_log.ts:16
#: src/constants/errors/nginx_log.ts:13
msgid "Cannot access log file"
msgstr ""
@@ -1197,6 +1197,10 @@ msgstr ""
msgid "Config entry file not exist"
msgstr ""
#: src/constants/errors/user.ts:15
msgid "Config not found"
msgstr ""
#: src/constants/errors/backup.ts:14
msgid "Config path is empty"
msgstr ""
@@ -1294,6 +1298,14 @@ msgstr ""
msgid "Core Upgrade"
msgstr ""
#: src/constants/errors/docker.ts:15
msgid "Could not find old container name"
msgstr ""
#: src/constants/errors/docker.ts:16
msgid "Could not find temp container"
msgstr ""
#: src/views/nginx_log/dashboard/components/BrowserStatsTable.vue:18
#: src/views/nginx_log/dashboard/components/DailyTrendsChart.vue:98
#: src/views/nginx_log/dashboard/components/DeviceStatsTable.vue:17
@@ -1480,6 +1492,10 @@ msgstr ""
msgid "Days"
msgstr ""
#: src/constants/errors/user.ts:16
msgid "Db file not found"
msgstr ""
#: src/constants/errors/middleware.ts:3
msgid "Decryption failed"
msgstr ""
@@ -1633,10 +1649,18 @@ msgstr ""
msgid "Device Type"
msgstr ""
#: src/constants/errors/upgrader.ts:4
msgid "Digest file content is empty"
msgstr ""
#: src/views/preference/components/ExternalNotify/dingtalk.ts:5
msgid "DingTalk"
msgstr ""
#: src/views/upstream/SocketList.vue:31
msgid "Direct"
msgstr ""
#: src/components/NgxConfigEditor/directive/DirectiveAdd.vue:72
msgid "Directive"
msgstr ""
@@ -1924,7 +1948,7 @@ msgstr ""
msgid "Email (*)"
msgstr ""
#: src/constants/errors/nginx_log.ts:9
#: src/constants/errors/nginx_log.parser.ts:2
msgid "Empty log line"
msgstr ""
@@ -2153,6 +2177,10 @@ msgstr ""
msgid "Error processing content"
msgstr ""
#: src/constants/errors/upgrader.ts:5
msgid "Executable binary file is empty"
msgstr ""
#: src/views/system/Upgrade.vue:195
msgid "Executable Path"
msgstr ""
@@ -2369,7 +2397,7 @@ msgstr ""
msgid "Failed to decrypt Nginx UI directory: {0}"
msgstr ""
#: src/constants/errors/nginx_log.ts:22
#: src/constants/errors/nginx_log.ts:19
msgid "Failed to delete all indexes"
msgstr ""
@@ -2381,7 +2409,7 @@ msgstr ""
msgid "Failed to delete certificate from database: %{error}"
msgstr ""
#: src/constants/errors/nginx_log.ts:21
#: src/constants/errors/nginx_log.ts:18
msgid "Failed to delete file index"
msgstr ""
@@ -2455,7 +2483,7 @@ msgstr ""
msgid "Failed to get container id: {0}"
msgstr ""
#: src/constants/errors/nginx_log.ts:23
#: src/constants/errors/nginx_log.ts:20
msgid "Failed to get index status"
msgstr ""
@@ -2463,11 +2491,15 @@ msgstr ""
msgid "Failed to get Nginx performance settings"
msgstr ""
#: src/constants/errors/performance.ts:9
msgid "Failed to get nginx.conf path"
msgstr ""
#: src/composables/useNginxPerformance.ts:49
msgid "Failed to get performance data"
msgstr ""
#: src/constants/errors/nginx_log.ts:24
#: src/constants/errors/nginx_log.ts:21
msgid "Failed to get persistence stats"
msgstr ""
@@ -2547,11 +2579,11 @@ msgstr ""
msgid "Failed to read symlink: {0}"
msgstr ""
#: src/constants/errors/nginx_log.ts:20
#: src/constants/errors/nginx_log.ts:17
msgid "Failed to rebuild file index"
msgstr ""
#: src/constants/errors/nginx_log.ts:19
#: src/constants/errors/nginx_log.ts:16
msgid "Failed to rebuild index"
msgstr ""
@@ -2651,7 +2683,7 @@ msgstr ""
msgid "File or directory not found: {0}"
msgstr ""
#: src/constants/errors/nginx_log.ts:18
#: src/constants/errors/nginx_log.ts:15
msgid "File path is required"
msgstr ""
@@ -2857,6 +2889,10 @@ msgstr ""
msgid "GZIP Min Length"
msgstr ""
#: src/views/upstream/SocketList.vue:61
msgid "Health Check"
msgstr ""
#: src/views/dashboard/components/SiteHealthCheckModal.vue:365
msgid "Health Check Configuration"
msgstr ""
@@ -2869,6 +2905,10 @@ msgstr ""
msgid "Health check configuration saved successfully"
msgstr ""
#: src/views/upstream/SocketList.vue:37
msgid "Health Status"
msgstr ""
#: src/components/SensitiveString/SensitiveString.vue:40
msgid "Hide"
msgstr ""
@@ -2893,7 +2933,7 @@ msgstr ""
msgid "History"
msgstr ""
#: src/routes/index.ts:50
#: src/routes/index.ts:52
msgid "Home"
msgstr ""
@@ -3032,6 +3072,10 @@ msgstr ""
msgid "Info"
msgstr ""
#: src/constants/errors/user.ts:17
msgid "Init user not exists"
msgstr ""
#: src/language/constants.ts:25
msgid "Initial core upgrader error"
msgstr ""
@@ -3102,6 +3146,10 @@ msgstr ""
msgid "Invalid claims type"
msgstr ""
#: src/constants/errors/version.ts:2
msgid "Invalid commit SHA"
msgstr ""
#: src/components/SystemRestore/SystemRestoreContent.vue:121
msgid "Invalid file object"
msgstr ""
@@ -3160,11 +3208,15 @@ msgstr ""
msgid "Invalid security token format"
msgstr ""
#: src/constants/errors/nginx_log.ts:10
#: src/constants/errors/notification.ts:6
msgid "Invalid Telegram Chat ID: cannot be zero"
msgstr ""
#: src/constants/errors/nginx_log.parser.ts:5
msgid "Invalid timestamp format"
msgstr ""
#: src/constants/errors/nginx_log.ts:26
#: src/constants/errors/nginx_log.ts:23
msgid "Invalid websocket message type"
msgstr ""
@@ -3279,6 +3331,10 @@ msgstr ""
msgid "Last Backup Time"
msgstr ""
#: src/views/upstream/SocketList.vue:52
msgid "Last Check"
msgstr ""
#: src/views/system/Upgrade.vue:197
msgid "Last checked at"
msgstr ""
@@ -3437,11 +3493,11 @@ msgid ""
"nginx-log.html for more information."
msgstr ""
#: src/constants/errors/nginx_log.ts:14
#: src/constants/errors/nginx_log.ts:11
msgid "Log file does not exist"
msgstr ""
#: src/constants/errors/nginx_log.ts:25
#: src/constants/errors/nginx_log.ts:22
msgid "Log file is not a regular file"
msgstr ""
@@ -3453,7 +3509,7 @@ msgstr ""
msgid "Log file not indexed yet"
msgstr ""
#: src/constants/errors/nginx_log.ts:12
#: src/constants/errors/nginx_log.ts:9
msgid "Log indexer not available"
msgstr ""
@@ -3461,11 +3517,19 @@ msgstr ""
msgid "Log indexing completed! Loading updated data..."
msgstr ""
#: src/constants/errors/nginx_log.parser.ts:3
msgid "Log line exceeds maximum length"
msgstr ""
#: src/routes/modules/nginx_log.ts:39 src/views/nginx_log/NginxLogList.vue:430
msgid "Log List"
msgstr ""
#: src/constants/errors/nginx_log.ts:15
#: src/constants/errors/nginx_log.indexer.ts:2
msgid "Log parser is not initialized; call indexer.InitLogParser() before use"
msgstr ""
#: src/constants/errors/nginx_log.ts:12
msgid "Log path is not under whitelist"
msgstr ""
@@ -3687,15 +3751,15 @@ msgstr ""
msgid "Model"
msgstr ""
#: src/constants/errors/nginx_log.ts:28
#: src/constants/errors/nginx_log.ts:25
msgid "Modern analytics service not available"
msgstr ""
#: src/constants/errors/nginx_log.ts:29
#: src/constants/errors/nginx_log.ts:26
msgid "Modern indexer service not available"
msgstr ""
#: src/constants/errors/nginx_log.ts:27
#: src/constants/errors/nginx_log.ts:24
msgid "Modern searcher service not available"
msgstr ""
@@ -4081,6 +4145,10 @@ msgstr ""
msgid "No Action"
msgstr ""
#: src/constants/errors/cert.ts:25
msgid "No certificate available"
msgstr ""
#: src/views/nginx_log/dashboard/components/ChinaMapChart/ChinaMapChart.vue:196
#: src/views/nginx_log/dashboard/components/ChinaMapChart/ChinaMapChart.vue:232
msgid "No China geographic data available"
@@ -4093,6 +4161,10 @@ msgstr ""
msgid "No data"
msgstr ""
#: src/views/upstream/SocketList.vue:42
msgid "No Data"
msgstr ""
#: src/views/nginx_log/structured/StructuredLogViewer.vue:820
msgid "No entries in current page"
msgstr ""
@@ -4140,6 +4212,10 @@ msgstr ""
msgid "Node"
msgstr ""
#: src/constants/errors/analytic.ts:2
msgid "Node analytics failed: {0}"
msgstr ""
#: src/views/preference/tabs/NodeSettings.vue:15
msgid "Node name"
msgstr ""
@@ -4969,6 +5045,10 @@ msgstr ""
msgid "Reinstall"
msgstr ""
#: src/constants/errors/version.ts:3
msgid "Release API request failed: {0}"
msgstr ""
#: src/views/system/Upgrade.vue:270
msgid "Release Note"
msgstr ""
@@ -5774,6 +5854,10 @@ msgstr ""
msgid "Sleep time between cache manager iterations"
msgstr ""
#: src/views/upstream/SocketList.vue:18
msgid "Socket"
msgstr ""
#: src/views/nginx_log/structured/StructuredLogViewer.vue:735
msgid "Sorted by"
msgstr ""
@@ -6614,10 +6698,14 @@ msgstr ""
msgid "Unsupported backup type: {0}"
msgstr ""
#: src/constants/errors/nginx_log.ts:11
#: src/constants/errors/nginx_log.parser.ts:4
msgid "Unsupported log format"
msgstr ""
#: src/constants/errors/upgrader.ts:6
msgid "Update already in progress"
msgstr ""
#: src/views/user/UserProfile.vue:218
msgid "Update Password"
msgstr ""
@@ -6657,6 +6745,14 @@ msgstr ""
msgid "Upgraded successfully"
msgstr ""
#: src/constants/errors/upgrader.ts:3
msgid "Upgrader core digest is empty"
msgstr ""
#: src/constants/errors/upgrader.ts:2
msgid "Upgrader core downloadUrl is empty"
msgstr ""
#: src/views/node/BatchUpgrader.vue:88 src/views/system/Upgrade.vue:80
msgid "Upgrading Nginx UI, please wait..."
msgstr ""
@@ -6673,7 +6769,8 @@ msgstr ""
msgid "Upload Folders"
msgstr ""
#: src/composables/useUpstreamStatus.ts:132
#: src/composables/useUpstreamStatus.ts:132 src/routes/modules/upstream.ts:10
#: src/views/upstream/SocketList.vue:25
msgid "Upstream"
msgstr ""
@@ -6681,6 +6778,10 @@ msgstr ""
msgid "Upstream Name"
msgstr ""
#: src/views/upstream/SocketList.vue:134
msgid "Upstream Sockets"
msgstr ""
#: src/views/namespace/columns.ts:59
msgid "Upstream Test Type"
msgstr ""
@@ -6935,7 +7036,7 @@ msgstr ""
msgid "Workers"
msgstr ""
#: src/layouts/HeaderLayout.vue:61 src/routes/index.ts:59
#: src/layouts/HeaderLayout.vue:61 src/routes/index.ts:61
#: src/views/workspace/WorkSpace.vue:51
msgid "Workspace"
msgstr ""
+334 -262
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+125 -23
View File
@@ -302,7 +302,7 @@ msgstr ""
msgid "All selected subdomains must belong to the same DNS Provider, otherwise the certificate application will fail."
msgstr ""
#: src/constants/errors/nginx_log.ts:13
#: src/constants/errors/nginx_log.ts:10
msgid "Analytics service not available"
msgstr ""
@@ -549,7 +549,7 @@ msgstr ""
msgid "Background indexing in progress. Data will be updated automatically when ready."
msgstr ""
#: src/constants/errors/nginx_log.ts:17
#: src/constants/errors/nginx_log.ts:14
msgid "Background log service not available"
msgstr ""
@@ -750,7 +750,7 @@ msgstr ""
msgid "Cannot access backup path {0}: {1}"
msgstr ""
#: src/constants/errors/nginx_log.ts:16
#: src/constants/errors/nginx_log.ts:13
msgid "Cannot access log file"
msgstr ""
@@ -1170,6 +1170,10 @@ msgstr ""
msgid "Config entry file not exist"
msgstr ""
#: src/constants/errors/user.ts:15
msgid "Config not found"
msgstr ""
#: src/constants/errors/backup.ts:14
msgid "Config path is empty"
msgstr ""
@@ -1267,6 +1271,14 @@ msgstr ""
msgid "Core Upgrade"
msgstr ""
#: src/constants/errors/docker.ts:15
msgid "Could not find old container name"
msgstr ""
#: src/constants/errors/docker.ts:16
msgid "Could not find temp container"
msgstr ""
#: src/views/nginx_log/dashboard/components/BrowserStatsTable.vue:18
#: src/views/nginx_log/dashboard/components/DailyTrendsChart.vue:98
#: src/views/nginx_log/dashboard/components/DeviceStatsTable.vue:17
@@ -1451,6 +1463,10 @@ msgstr ""
msgid "Days"
msgstr ""
#: src/constants/errors/user.ts:16
msgid "Db file not found"
msgstr ""
#: src/constants/errors/middleware.ts:3
msgid "Decryption failed"
msgstr ""
@@ -1608,10 +1624,18 @@ msgstr ""
msgid "Device Type"
msgstr ""
#: src/constants/errors/upgrader.ts:4
msgid "Digest file content is empty"
msgstr ""
#: src/views/preference/components/ExternalNotify/dingtalk.ts:5
msgid "DingTalk"
msgstr ""
#: src/views/upstream/SocketList.vue:31
msgid "Direct"
msgstr ""
#: src/components/NgxConfigEditor/directive/DirectiveAdd.vue:72
msgid "Directive"
msgstr ""
@@ -1900,7 +1924,7 @@ msgstr ""
msgid "Email (*)"
msgstr ""
#: src/constants/errors/nginx_log.ts:9
#: src/constants/errors/nginx_log.parser.ts:2
msgid "Empty log line"
msgstr ""
@@ -2128,6 +2152,10 @@ msgstr ""
msgid "Error processing content"
msgstr ""
#: src/constants/errors/upgrader.ts:5
msgid "Executable binary file is empty"
msgstr ""
#: src/views/system/Upgrade.vue:195
msgid "Executable Path"
msgstr ""
@@ -2340,7 +2368,7 @@ msgstr ""
msgid "Failed to decrypt Nginx UI directory: {0}"
msgstr ""
#: src/constants/errors/nginx_log.ts:22
#: src/constants/errors/nginx_log.ts:19
msgid "Failed to delete all indexes"
msgstr ""
@@ -2352,7 +2380,7 @@ msgstr ""
msgid "Failed to delete certificate from database: %{error}"
msgstr ""
#: src/constants/errors/nginx_log.ts:21
#: src/constants/errors/nginx_log.ts:18
msgid "Failed to delete file index"
msgstr ""
@@ -2426,7 +2454,7 @@ msgstr ""
msgid "Failed to get container id: {0}"
msgstr ""
#: src/constants/errors/nginx_log.ts:23
#: src/constants/errors/nginx_log.ts:20
msgid "Failed to get index status"
msgstr ""
@@ -2434,11 +2462,15 @@ msgstr ""
msgid "Failed to get Nginx performance settings"
msgstr ""
#: src/constants/errors/performance.ts:9
msgid "Failed to get nginx.conf path"
msgstr ""
#: src/composables/useNginxPerformance.ts:49
msgid "Failed to get performance data"
msgstr ""
#: src/constants/errors/nginx_log.ts:24
#: src/constants/errors/nginx_log.ts:21
msgid "Failed to get persistence stats"
msgstr ""
@@ -2518,11 +2550,11 @@ msgstr ""
msgid "Failed to read symlink: {0}"
msgstr ""
#: src/constants/errors/nginx_log.ts:20
#: src/constants/errors/nginx_log.ts:17
msgid "Failed to rebuild file index"
msgstr ""
#: src/constants/errors/nginx_log.ts:19
#: src/constants/errors/nginx_log.ts:16
msgid "Failed to rebuild index"
msgstr ""
@@ -2622,7 +2654,7 @@ msgstr ""
msgid "File or directory not found: {0}"
msgstr ""
#: src/constants/errors/nginx_log.ts:18
#: src/constants/errors/nginx_log.ts:15
msgid "File path is required"
msgstr ""
@@ -2820,6 +2852,10 @@ msgstr ""
msgid "GZIP Min Length"
msgstr ""
#: src/views/upstream/SocketList.vue:61
msgid "Health Check"
msgstr ""
#: src/views/dashboard/components/SiteHealthCheckModal.vue:365
msgid "Health Check Configuration"
msgstr ""
@@ -2832,6 +2868,10 @@ msgstr ""
msgid "Health check configuration saved successfully"
msgstr ""
#: src/views/upstream/SocketList.vue:37
msgid "Health Status"
msgstr ""
#: src/components/SensitiveString/SensitiveString.vue:40
msgid "Hide"
msgstr ""
@@ -2856,7 +2896,7 @@ msgstr ""
msgid "History"
msgstr ""
#: src/routes/index.ts:50
#: src/routes/index.ts:52
msgid "Home"
msgstr ""
@@ -2986,6 +3026,10 @@ msgstr ""
msgid "Info"
msgstr ""
#: src/constants/errors/user.ts:17
msgid "Init user not exists"
msgstr ""
#: src/language/constants.ts:25
msgid "Initial core upgrader error"
msgstr ""
@@ -3054,6 +3098,10 @@ msgstr ""
msgid "Invalid claims type"
msgstr ""
#: src/constants/errors/version.ts:2
msgid "Invalid commit SHA"
msgstr ""
#: src/components/SystemRestore/SystemRestoreContent.vue:121
msgid "Invalid file object"
msgstr ""
@@ -3112,11 +3160,15 @@ msgstr ""
msgid "Invalid security token format"
msgstr ""
#: src/constants/errors/nginx_log.ts:10
#: src/constants/errors/notification.ts:6
msgid "Invalid Telegram Chat ID: cannot be zero"
msgstr ""
#: src/constants/errors/nginx_log.parser.ts:5
msgid "Invalid timestamp format"
msgstr ""
#: src/constants/errors/nginx_log.ts:26
#: src/constants/errors/nginx_log.ts:23
msgid "Invalid websocket message type"
msgstr ""
@@ -3229,6 +3281,10 @@ msgstr ""
msgid "Last Backup Time"
msgstr ""
#: src/views/upstream/SocketList.vue:52
msgid "Last Check"
msgstr ""
#: src/views/system/Upgrade.vue:197
msgid "Last checked at"
msgstr ""
@@ -3387,11 +3443,11 @@ msgstr ""
msgid "Log file %{log_path} is not a regular file. If you are using nginx-ui in docker container, please refer to https://nginxui.com/zh_CN/guide/config-nginx-log.html for more information."
msgstr ""
#: src/constants/errors/nginx_log.ts:14
#: src/constants/errors/nginx_log.ts:11
msgid "Log file does not exist"
msgstr ""
#: src/constants/errors/nginx_log.ts:25
#: src/constants/errors/nginx_log.ts:22
msgid "Log file is not a regular file"
msgstr ""
@@ -3403,7 +3459,7 @@ msgstr ""
msgid "Log file not indexed yet"
msgstr ""
#: src/constants/errors/nginx_log.ts:12
#: src/constants/errors/nginx_log.ts:9
msgid "Log indexer not available"
msgstr ""
@@ -3411,12 +3467,20 @@ msgstr ""
msgid "Log indexing completed! Loading updated data..."
msgstr ""
#: src/constants/errors/nginx_log.parser.ts:3
msgid "Log line exceeds maximum length"
msgstr ""
#: src/routes/modules/nginx_log.ts:39
#: src/views/nginx_log/NginxLogList.vue:430
msgid "Log List"
msgstr ""
#: src/constants/errors/nginx_log.ts:15
#: src/constants/errors/nginx_log.indexer.ts:2
msgid "Log parser is not initialized; call indexer.InitLogParser() before use"
msgstr ""
#: src/constants/errors/nginx_log.ts:12
msgid "Log path is not under whitelist"
msgstr ""
@@ -3634,15 +3698,15 @@ msgstr ""
msgid "Model"
msgstr ""
#: src/constants/errors/nginx_log.ts:28
#: src/constants/errors/nginx_log.ts:25
msgid "Modern analytics service not available"
msgstr ""
#: src/constants/errors/nginx_log.ts:29
#: src/constants/errors/nginx_log.ts:26
msgid "Modern indexer service not available"
msgstr ""
#: src/constants/errors/nginx_log.ts:27
#: src/constants/errors/nginx_log.ts:24
msgid "Modern searcher service not available"
msgstr ""
@@ -4039,6 +4103,10 @@ msgstr ""
msgid "No Action"
msgstr ""
#: src/constants/errors/cert.ts:25
msgid "No certificate available"
msgstr ""
#: src/views/nginx_log/dashboard/components/ChinaMapChart/ChinaMapChart.vue:196
#: src/views/nginx_log/dashboard/components/ChinaMapChart/ChinaMapChart.vue:232
msgid "No China geographic data available"
@@ -4051,6 +4119,10 @@ msgstr ""
msgid "No data"
msgstr ""
#: src/views/upstream/SocketList.vue:42
msgid "No Data"
msgstr ""
#: src/views/nginx_log/structured/StructuredLogViewer.vue:820
msgid "No entries in current page"
msgstr ""
@@ -4096,6 +4168,10 @@ msgstr ""
msgid "Node"
msgstr ""
#: src/constants/errors/analytic.ts:2
msgid "Node analytics failed: {0}"
msgstr ""
#: src/views/preference/tabs/NodeSettings.vue:15
msgid "Node name"
msgstr ""
@@ -4911,6 +4987,10 @@ msgstr ""
msgid "Reinstall"
msgstr ""
#: src/constants/errors/version.ts:3
msgid "Release API request failed: {0}"
msgstr ""
#: src/views/system/Upgrade.vue:270
msgid "Release Note"
msgstr ""
@@ -5713,6 +5793,10 @@ msgstr ""
msgid "Sleep time between cache manager iterations"
msgstr ""
#: src/views/upstream/SocketList.vue:18
msgid "Socket"
msgstr ""
#: src/views/nginx_log/structured/StructuredLogViewer.vue:735
msgid "Sorted by"
msgstr ""
@@ -6505,10 +6589,14 @@ msgstr ""
msgid "Unsupported backup type: {0}"
msgstr ""
#: src/constants/errors/nginx_log.ts:11
#: src/constants/errors/nginx_log.parser.ts:4
msgid "Unsupported log format"
msgstr ""
#: src/constants/errors/upgrader.ts:6
msgid "Update already in progress"
msgstr ""
#: src/views/user/UserProfile.vue:218
msgid "Update Password"
msgstr ""
@@ -6552,6 +6640,14 @@ msgstr ""
msgid "Upgraded successfully"
msgstr ""
#: src/constants/errors/upgrader.ts:3
msgid "Upgrader core digest is empty"
msgstr ""
#: src/constants/errors/upgrader.ts:2
msgid "Upgrader core downloadUrl is empty"
msgstr ""
#: src/views/node/BatchUpgrader.vue:88
#: src/views/system/Upgrade.vue:80
msgid "Upgrading Nginx UI, please wait..."
@@ -6570,6 +6666,8 @@ msgid "Upload Folders"
msgstr ""
#: src/composables/useUpstreamStatus.ts:132
#: src/routes/modules/upstream.ts:10
#: src/views/upstream/SocketList.vue:25
msgid "Upstream"
msgstr ""
@@ -6577,6 +6675,10 @@ msgstr ""
msgid "Upstream Name"
msgstr ""
#: src/views/upstream/SocketList.vue:134
msgid "Upstream Sockets"
msgstr ""
#: src/views/namespace/columns.ts:59
msgid "Upstream Test Type"
msgstr ""
@@ -6823,7 +6925,7 @@ msgid "Workers"
msgstr ""
#: src/layouts/HeaderLayout.vue:61
#: src/routes/index.ts:59
#: src/routes/index.ts:61
#: src/views/workspace/WorkSpace.vue:51
msgid "Workspace"
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2
View File
@@ -18,6 +18,7 @@ import { sitesRoutes } from './modules/sites'
import { streamsRoutes } from './modules/streams'
import { systemRoutes } from './modules/system'
import { terminalRoutes } from './modules/terminal'
import { upstreamRoutes } from './modules/upstream'
import { userRoutes } from './modules/user'
import 'nprogress/nprogress.css'
@@ -26,6 +27,7 @@ const mainLayoutChildren: RouteRecordRaw[] = [
...dashboardRoutes,
...sitesRoutes,
...streamsRoutes,
...upstreamRoutes,
...configRoutes,
...certificatesRoutes,
...terminalRoutes,
+14
View File
@@ -0,0 +1,14 @@
import type { RouteRecordRaw } from 'vue-router'
import { ClusterOutlined } from '@ant-design/icons-vue'
export const upstreamRoutes: RouteRecordRaw[] = [
{
path: 'upstream',
name: 'Upstream Management',
component: () => import('@/views/upstream/SocketList.vue'),
meta: {
name: () => $gettext('Upstream'),
icon: ClusterOutlined,
},
},
]
+193
View File
@@ -0,0 +1,193 @@
<script setup lang="ts">
import type { ColumnsType } from 'ant-design-vue/es/table'
import type { SocketInfo } from '@/api/upstream'
import { ReloadOutlined } from '@ant-design/icons-vue'
import { message, Tag } from 'ant-design-vue'
import upstream from '@/api/upstream'
import { formatDateTime } from '@/lib/helper'
import { useProxyAvailabilityStore } from '@/pinia/moudule/proxyAvailability'
const dataSource = ref<SocketInfo[]>([])
const loading = ref(false)
// Initialize proxy availability store
const proxyAvailabilityStore = useProxyAvailabilityStore()
const columns: ColumnsType<SocketInfo> = [
{
title: () => $gettext('Socket'),
dataIndex: 'socket',
key: 'socket',
width: 200,
fixed: 'left',
},
{
title: () => $gettext('Upstream'),
dataIndex: 'upstream_name',
key: 'upstream_name',
width: 150,
customRender: ({ record }) => {
if (!record.upstream_name) {
return $gettext('Direct')
}
return record.upstream_name
},
},
{
title: () => $gettext('Health Status'),
key: 'status',
width: 180,
customRender: ({ record }) => {
if (!record.status) {
return $gettext('No Data')
}
const status = record.status
return h('div', { class: 'flex items-center' }, [
h(Tag, { color: status.online ? 'success' : 'error', class: 'mr-2' }, () => status.online ? 'Online' : 'Offline'),
status.online ? h('span', `${status.latency.toFixed(2)}ms`) : null,
])
},
},
{
title: () => $gettext('Last Check'),
dataIndex: 'last_check',
key: 'last_check',
width: 180,
customRender: ({ text }) => {
return text ? formatDateTime(text) : '-'
},
},
{
title: () => $gettext('Health Check'),
key: 'enabled',
width: 150,
fixed: 'right',
},
]
// Merge socket list with real-time availability data
function mergeSocketData(sockets: SocketInfo[]): SocketInfo[] {
return sockets.map(socket => {
// Get real-time status from availability store
const availabilityResult = proxyAvailabilityStore.availabilityResults[socket.socket]
if (availabilityResult) {
return {
...socket,
status: {
online: availabilityResult.online,
latency: availabilityResult.latency,
},
last_check: new Date().toISOString(),
}
}
return socket
})
}
// Computed data source that combines socket list with real-time availability
const enrichedDataSource = computed(() => {
return mergeSocketData(dataSource.value)
})
async function loadData() {
loading.value = true
try {
const res = await upstream.getSocketList()
dataSource.value = res.data
}
catch {
message.error('Failed to load socket data')
}
finally {
loading.value = false
}
}
async function handleToggleEnabled(socket: string, enabled: boolean | string | number) {
const isEnabled = typeof enabled === 'boolean' ? enabled : Boolean(enabled)
try {
await upstream.updateSocketConfig(socket, { enabled: isEnabled })
message.success(`Health check ${isEnabled ? 'enabled' : 'disabled'} for ${socket}`)
await loadData()
}
catch {
message.error('Failed to update socket configuration')
}
}
// Start monitoring when component mounts
onMounted(async () => {
await loadData()
// Start real-time monitoring for availability updates
proxyAvailabilityStore.startMonitoring()
})
// Clean up WebSocket connections when component unmounts
onUnmounted(() => {
proxyAvailabilityStore.stopMonitoring()
})
</script>
<template>
<ACard :title="$gettext('Upstream Sockets')">
<template #extra>
<AButton :loading @click="loadData">
<template #icon>
<ReloadOutlined />
</template>
</AButton>
</template>
<ATable
:columns="columns"
:data-source="enrichedDataSource"
:loading="loading"
:pagination="{
pageSize: 20,
showSizeChanger: true,
showTotal: (total: number) => `Total ${total} items`,
}"
:scroll="{ x: 1400 }"
row-key="socket"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'socket'">
<ATag color="default" :bordered="false" class="socket-tag">
<template #icon>
<span v-if="record.type === 'upstream'" class="target-type-icon">U</span>
<span v-else class="target-type-icon">P</span>
</template>
{{ record.socket }}
</ATag>
</template>
<template v-if="column.key === 'enabled'">
<ASwitch
v-model:checked="record.enabled"
@change="handleToggleEnabled(record.socket, $event)"
/>
</template>
</template>
</ATable>
</ACard>
</template>
<style scoped lang="less">
.socket-tag {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
.target-type-icon {
display: inline-block;
width: 12px;
height: 12px;
line-height: 12px;
text-align: center;
border-radius: 2px;
font-weight: bold;
font-size: 10px;
flex-shrink: 0;
}
}
</style>
Binary file not shown.
Binary file not shown.
+24
View File
@@ -1212,6 +1212,30 @@
"https://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_fetch_ciphers"
]
},
"js_fetch_keepalive": {
"links": [
"https://nginx.org/en/docs/http/ngx_http_js_module.html#js_fetch_keepalive",
"https://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_fetch_keepalive"
]
},
"js_fetch_keepalive_requests": {
"links": [
"https://nginx.org/en/docs/http/ngx_http_js_module.html#js_fetch_keepalive_requests",
"https://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_fetch_keepalive_requests"
]
},
"js_fetch_keepalive_time": {
"links": [
"https://nginx.org/en/docs/http/ngx_http_js_module.html#js_fetch_keepalive_time",
"https://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_fetch_keepalive_time"
]
},
"js_fetch_keepalive_timeout": {
"links": [
"https://nginx.org/en/docs/http/ngx_http_js_module.html#js_fetch_keepalive_timeout",
"https://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_fetch_keepalive_timeout"
]
},
"js_fetch_max_response_buffer_size": {
"links": [
"https://nginx.org/en/docs/http/ngx_http_js_module.html#js_fetch_max_response_buffer_size",
+43 -8
View File
@@ -32,12 +32,13 @@ type Service struct {
availabilityMap map[string]*Status // key: host:port
configTargets map[string][]string // configPath -> []targetKeys
// Public upstream definitions storage
Upstreams map[string]*Definition // key: upstream name
upstreamsMutex sync.RWMutex
targetsMutex sync.RWMutex
lastUpdateTime time.Time
testInProgress bool
testMutex sync.Mutex
Upstreams map[string]*Definition // key: upstream name
upstreamsMutex sync.RWMutex
targetsMutex sync.RWMutex
lastUpdateTime time.Time
testInProgress bool
testMutex sync.Mutex
disabledSocketsChecker func() map[string]bool
}
var (
@@ -236,18 +237,30 @@ func (s *Service) PerformAvailabilityTest() {
// logger.Debug("Performing availability test for", targetCount, "unique targets")
// Get disabled sockets from database
disabledSockets := make(map[string]bool)
if s.disabledSocketsChecker != nil {
disabledSockets = s.disabledSocketsChecker()
}
// Separate targets into traditional and consul groups from the start
s.targetsMutex.RLock()
regularTargetKeys := make([]string, 0, len(s.targets))
consulTargets := make([]ProxyTarget, 0, len(s.targets))
for _, targetInfo := range s.targets {
// Check if this socket is disabled
socketAddr := formatSocketAddress(targetInfo.ProxyTarget.Host, targetInfo.ProxyTarget.Port)
if disabledSockets[socketAddr] {
// logger.Debug("Skipping disabled socket:", socketAddr)
continue
}
if targetInfo.ProxyTarget.IsConsul {
consulTargets = append(consulTargets, targetInfo.ProxyTarget)
} else {
// Traditional target - use properly formatted socket address
key := formatSocketAddress(targetInfo.ProxyTarget.Host, targetInfo.ProxyTarget.Port)
regularTargetKeys = append(regularTargetKeys, key)
regularTargetKeys = append(regularTargetKeys, socketAddr)
}
}
s.targetsMutex.RUnlock()
@@ -277,6 +290,28 @@ func (s *Service) PerformAvailabilityTest() {
// logger.Debug("Availability test completed for", len(results), "targets")
}
// findUpstreamNameForTarget finds which upstream a target belongs to
func (s *Service) findUpstreamNameForTarget(target ProxyTarget) string {
s.upstreamsMutex.RLock()
defer s.upstreamsMutex.RUnlock()
targetKey := formatSocketAddress(target.Host, target.Port)
for name, upstream := range s.Upstreams {
for _, server := range upstream.Servers {
serverKey := formatSocketAddress(server.Host, server.Port)
if serverKey == targetKey {
return name
}
}
}
return ""
}
// SetDisabledSocketsChecker sets a callback function to check disabled sockets
func (s *Service) SetDisabledSocketsChecker(checker func() map[string]bool) {
s.disabledSocketsChecker = checker
}
// ClearTargets clears all targets (useful for testing or reloading)
func (s *Service) ClearTargets() {
s.targetsMutex.Lock()
+1
View File
@@ -53,6 +53,7 @@ func GenerateAllModel() []any {
AutoBackup{},
SiteConfig{},
NginxLogIndex{},
UpstreamConfig{},
}
}
+7
View File
@@ -0,0 +1,7 @@
package model
type UpstreamConfig struct {
Model
Socket string `json:"socket" gorm:"uniqueIndex"` // host:port address
Enabled bool `json:"enabled" gorm:"default:true"`
}
+8
View File
@@ -35,6 +35,7 @@ var (
Site *site
SiteConfig *siteConfig
Stream *stream
UpstreamConfig *upstreamConfig
User *user
)
@@ -58,6 +59,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
Site = &Q.Site
SiteConfig = &Q.SiteConfig
Stream = &Q.Stream
UpstreamConfig = &Q.UpstreamConfig
User = &Q.User
}
@@ -82,6 +84,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
Site: newSite(db, opts...),
SiteConfig: newSiteConfig(db, opts...),
Stream: newStream(db, opts...),
UpstreamConfig: newUpstreamConfig(db, opts...),
User: newUser(db, opts...),
}
}
@@ -107,6 +110,7 @@ type Query struct {
Site site
SiteConfig siteConfig
Stream stream
UpstreamConfig upstreamConfig
User user
}
@@ -133,6 +137,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
Site: q.Site.clone(db),
SiteConfig: q.SiteConfig.clone(db),
Stream: q.Stream.clone(db),
UpstreamConfig: q.UpstreamConfig.clone(db),
User: q.User.clone(db),
}
}
@@ -166,6 +171,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
Site: q.Site.replaceDB(db),
SiteConfig: q.SiteConfig.replaceDB(db),
Stream: q.Stream.replaceDB(db),
UpstreamConfig: q.UpstreamConfig.replaceDB(db),
User: q.User.replaceDB(db),
}
}
@@ -189,6 +195,7 @@ type queryCtx struct {
Site *siteDo
SiteConfig *siteConfigDo
Stream *streamDo
UpstreamConfig *upstreamConfigDo
User *userDo
}
@@ -212,6 +219,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
Site: q.Site.WithContext(ctx),
SiteConfig: q.SiteConfig.WithContext(ctx),
Stream: q.Stream.WithContext(ctx),
UpstreamConfig: q.UpstreamConfig.WithContext(ctx),
User: q.User.WithContext(ctx),
}
}
+1 -5
View File
@@ -32,7 +32,6 @@ func newLLMSession(db *gorm.DB, opts ...gen.DOOption) lLMSession {
_lLMSession.SessionID = field.NewString(tableName, "session_id")
_lLMSession.Title = field.NewString(tableName, "title")
_lLMSession.Path = field.NewString(tableName, "path")
_lLMSession.SessionType = field.NewString(tableName, "session_type")
_lLMSession.Messages = field.NewField(tableName, "messages")
_lLMSession.MessageCount = field.NewInt(tableName, "message_count")
_lLMSession.IsActive = field.NewBool(tableName, "is_active")
@@ -53,7 +52,6 @@ type lLMSession struct {
SessionID field.String
Title field.String
Path field.String
SessionType field.String
Messages field.Field
MessageCount field.Int
IsActive field.Bool
@@ -80,7 +78,6 @@ func (l *lLMSession) updateTableName(table string) *lLMSession {
l.SessionID = field.NewString(table, "session_id")
l.Title = field.NewString(table, "title")
l.Path = field.NewString(table, "path")
l.SessionType = field.NewString(table, "session_type")
l.Messages = field.NewField(table, "messages")
l.MessageCount = field.NewInt(table, "message_count")
l.IsActive = field.NewBool(table, "is_active")
@@ -103,12 +100,11 @@ func (l *lLMSession) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
func (l *lLMSession) fillFieldMap() {
l.fieldMap = make(map[string]field.Expr, 11)
l.fieldMap = make(map[string]field.Expr, 10)
l.fieldMap["id"] = l.ID
l.fieldMap["session_id"] = l.SessionID
l.fieldMap["title"] = l.Title
l.fieldMap["path"] = l.Path
l.fieldMap["session_type"] = l.SessionType
l.fieldMap["messages"] = l.Messages
l.fieldMap["message_count"] = l.MessageCount
l.fieldMap["is_active"] = l.IsActive
+370
View File
@@ -0,0 +1,370 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package query
import (
"context"
"strings"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"github.com/0xJacky/Nginx-UI/model"
)
func newUpstreamConfig(db *gorm.DB, opts ...gen.DOOption) upstreamConfig {
_upstreamConfig := upstreamConfig{}
_upstreamConfig.upstreamConfigDo.UseDB(db, opts...)
_upstreamConfig.upstreamConfigDo.UseModel(&model.UpstreamConfig{})
tableName := _upstreamConfig.upstreamConfigDo.TableName()
_upstreamConfig.ALL = field.NewAsterisk(tableName)
_upstreamConfig.ID = field.NewUint64(tableName, "id")
_upstreamConfig.CreatedAt = field.NewTime(tableName, "created_at")
_upstreamConfig.UpdatedAt = field.NewTime(tableName, "updated_at")
_upstreamConfig.DeletedAt = field.NewField(tableName, "deleted_at")
_upstreamConfig.Socket = field.NewString(tableName, "socket")
_upstreamConfig.Enabled = field.NewBool(tableName, "enabled")
_upstreamConfig.fillFieldMap()
return _upstreamConfig
}
type upstreamConfig struct {
upstreamConfigDo
ALL field.Asterisk
ID field.Uint64
CreatedAt field.Time
UpdatedAt field.Time
DeletedAt field.Field
Socket field.String
Enabled field.Bool
fieldMap map[string]field.Expr
}
func (u upstreamConfig) Table(newTableName string) *upstreamConfig {
u.upstreamConfigDo.UseTable(newTableName)
return u.updateTableName(newTableName)
}
func (u upstreamConfig) As(alias string) *upstreamConfig {
u.upstreamConfigDo.DO = *(u.upstreamConfigDo.As(alias).(*gen.DO))
return u.updateTableName(alias)
}
func (u *upstreamConfig) updateTableName(table string) *upstreamConfig {
u.ALL = field.NewAsterisk(table)
u.ID = field.NewUint64(table, "id")
u.CreatedAt = field.NewTime(table, "created_at")
u.UpdatedAt = field.NewTime(table, "updated_at")
u.DeletedAt = field.NewField(table, "deleted_at")
u.Socket = field.NewString(table, "socket")
u.Enabled = field.NewBool(table, "enabled")
u.fillFieldMap()
return u
}
func (u *upstreamConfig) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := u.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (u *upstreamConfig) fillFieldMap() {
u.fieldMap = make(map[string]field.Expr, 6)
u.fieldMap["id"] = u.ID
u.fieldMap["created_at"] = u.CreatedAt
u.fieldMap["updated_at"] = u.UpdatedAt
u.fieldMap["deleted_at"] = u.DeletedAt
u.fieldMap["socket"] = u.Socket
u.fieldMap["enabled"] = u.Enabled
}
func (u upstreamConfig) clone(db *gorm.DB) upstreamConfig {
u.upstreamConfigDo.ReplaceConnPool(db.Statement.ConnPool)
return u
}
func (u upstreamConfig) replaceDB(db *gorm.DB) upstreamConfig {
u.upstreamConfigDo.ReplaceDB(db)
return u
}
type upstreamConfigDo struct{ gen.DO }
// FirstByID Where("id=@id")
func (u upstreamConfigDo) FirstByID(id uint64) (result *model.UpstreamConfig, err error) {
var params []interface{}
var generateSQL strings.Builder
params = append(params, id)
generateSQL.WriteString("id=? ")
var executeSQL *gorm.DB
executeSQL = u.UnderlyingDB().Where(generateSQL.String(), params...).Take(&result) // ignore_security_alert
err = executeSQL.Error
return
}
// DeleteByID update @@table set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=@id
func (u upstreamConfigDo) DeleteByID(id uint64) (err error) {
var params []interface{}
var generateSQL strings.Builder
params = append(params, id)
generateSQL.WriteString("update upstream_configs set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=? ")
var executeSQL *gorm.DB
executeSQL = u.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
err = executeSQL.Error
return
}
func (u upstreamConfigDo) Debug() *upstreamConfigDo {
return u.withDO(u.DO.Debug())
}
func (u upstreamConfigDo) WithContext(ctx context.Context) *upstreamConfigDo {
return u.withDO(u.DO.WithContext(ctx))
}
func (u upstreamConfigDo) ReadDB() *upstreamConfigDo {
return u.Clauses(dbresolver.Read)
}
func (u upstreamConfigDo) WriteDB() *upstreamConfigDo {
return u.Clauses(dbresolver.Write)
}
func (u upstreamConfigDo) Session(config *gorm.Session) *upstreamConfigDo {
return u.withDO(u.DO.Session(config))
}
func (u upstreamConfigDo) Clauses(conds ...clause.Expression) *upstreamConfigDo {
return u.withDO(u.DO.Clauses(conds...))
}
func (u upstreamConfigDo) Returning(value interface{}, columns ...string) *upstreamConfigDo {
return u.withDO(u.DO.Returning(value, columns...))
}
func (u upstreamConfigDo) Not(conds ...gen.Condition) *upstreamConfigDo {
return u.withDO(u.DO.Not(conds...))
}
func (u upstreamConfigDo) Or(conds ...gen.Condition) *upstreamConfigDo {
return u.withDO(u.DO.Or(conds...))
}
func (u upstreamConfigDo) Select(conds ...field.Expr) *upstreamConfigDo {
return u.withDO(u.DO.Select(conds...))
}
func (u upstreamConfigDo) Where(conds ...gen.Condition) *upstreamConfigDo {
return u.withDO(u.DO.Where(conds...))
}
func (u upstreamConfigDo) Order(conds ...field.Expr) *upstreamConfigDo {
return u.withDO(u.DO.Order(conds...))
}
func (u upstreamConfigDo) Distinct(cols ...field.Expr) *upstreamConfigDo {
return u.withDO(u.DO.Distinct(cols...))
}
func (u upstreamConfigDo) Omit(cols ...field.Expr) *upstreamConfigDo {
return u.withDO(u.DO.Omit(cols...))
}
func (u upstreamConfigDo) Join(table schema.Tabler, on ...field.Expr) *upstreamConfigDo {
return u.withDO(u.DO.Join(table, on...))
}
func (u upstreamConfigDo) LeftJoin(table schema.Tabler, on ...field.Expr) *upstreamConfigDo {
return u.withDO(u.DO.LeftJoin(table, on...))
}
func (u upstreamConfigDo) RightJoin(table schema.Tabler, on ...field.Expr) *upstreamConfigDo {
return u.withDO(u.DO.RightJoin(table, on...))
}
func (u upstreamConfigDo) Group(cols ...field.Expr) *upstreamConfigDo {
return u.withDO(u.DO.Group(cols...))
}
func (u upstreamConfigDo) Having(conds ...gen.Condition) *upstreamConfigDo {
return u.withDO(u.DO.Having(conds...))
}
func (u upstreamConfigDo) Limit(limit int) *upstreamConfigDo {
return u.withDO(u.DO.Limit(limit))
}
func (u upstreamConfigDo) Offset(offset int) *upstreamConfigDo {
return u.withDO(u.DO.Offset(offset))
}
func (u upstreamConfigDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *upstreamConfigDo {
return u.withDO(u.DO.Scopes(funcs...))
}
func (u upstreamConfigDo) Unscoped() *upstreamConfigDo {
return u.withDO(u.DO.Unscoped())
}
func (u upstreamConfigDo) Create(values ...*model.UpstreamConfig) error {
if len(values) == 0 {
return nil
}
return u.DO.Create(values)
}
func (u upstreamConfigDo) CreateInBatches(values []*model.UpstreamConfig, batchSize int) error {
return u.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (u upstreamConfigDo) Save(values ...*model.UpstreamConfig) error {
if len(values) == 0 {
return nil
}
return u.DO.Save(values)
}
func (u upstreamConfigDo) First() (*model.UpstreamConfig, error) {
if result, err := u.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.UpstreamConfig), nil
}
}
func (u upstreamConfigDo) Take() (*model.UpstreamConfig, error) {
if result, err := u.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.UpstreamConfig), nil
}
}
func (u upstreamConfigDo) Last() (*model.UpstreamConfig, error) {
if result, err := u.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.UpstreamConfig), nil
}
}
func (u upstreamConfigDo) Find() ([]*model.UpstreamConfig, error) {
result, err := u.DO.Find()
return result.([]*model.UpstreamConfig), err
}
func (u upstreamConfigDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.UpstreamConfig, err error) {
buf := make([]*model.UpstreamConfig, 0, batchSize)
err = u.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (u upstreamConfigDo) FindInBatches(result *[]*model.UpstreamConfig, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return u.DO.FindInBatches(result, batchSize, fc)
}
func (u upstreamConfigDo) Attrs(attrs ...field.AssignExpr) *upstreamConfigDo {
return u.withDO(u.DO.Attrs(attrs...))
}
func (u upstreamConfigDo) Assign(attrs ...field.AssignExpr) *upstreamConfigDo {
return u.withDO(u.DO.Assign(attrs...))
}
func (u upstreamConfigDo) Joins(fields ...field.RelationField) *upstreamConfigDo {
for _, _f := range fields {
u = *u.withDO(u.DO.Joins(_f))
}
return &u
}
func (u upstreamConfigDo) Preload(fields ...field.RelationField) *upstreamConfigDo {
for _, _f := range fields {
u = *u.withDO(u.DO.Preload(_f))
}
return &u
}
func (u upstreamConfigDo) FirstOrInit() (*model.UpstreamConfig, error) {
if result, err := u.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.UpstreamConfig), nil
}
}
func (u upstreamConfigDo) FirstOrCreate() (*model.UpstreamConfig, error) {
if result, err := u.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.UpstreamConfig), nil
}
}
func (u upstreamConfigDo) FindByPage(offset int, limit int) (result []*model.UpstreamConfig, count int64, err error) {
result, err = u.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = u.Offset(-1).Limit(-1).Count()
return
}
func (u upstreamConfigDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = u.Count()
if err != nil {
return
}
err = u.Offset(offset).Limit(limit).Scan(result)
return
}
func (u upstreamConfigDo) Scan(result interface{}) (err error) {
return u.DO.Scan(result)
}
func (u upstreamConfigDo) Delete(models ...*model.UpstreamConfig) (result gen.ResultInfo, err error) {
return u.DO.Delete(models)
}
func (u *upstreamConfigDo) withDO(do gen.Dao) *upstreamConfigDo {
u.DO = *do.(*gen.DO)
return u
}