mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-06-19 07:36:59 +00:00
feat(dashboard): add sites navigation #1054
This commit is contained in:
@@ -139,4 +139,4 @@ func RestartNginx(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "ok",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
// ConfigFileEntity represents a generic configuration file entity
|
||||
type ConfigFileEntity struct {
|
||||
path string
|
||||
path string
|
||||
namespaceID uint64
|
||||
namespace *model.Namespace
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func GetConfigs(c *gin.Context) {
|
||||
Search: search,
|
||||
OrderBy: sortBy,
|
||||
Sort: order,
|
||||
NamespaceID: namespaceID,
|
||||
NamespaceID: namespaceID,
|
||||
IncludeDirs: true, // Keep directories for the list.go endpoint
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func GetConfigs(c *gin.Context) {
|
||||
// For generic config files, we don't have database records
|
||||
// so namespaceID and namespace will be 0 and nil
|
||||
entity := &ConfigFileEntity{
|
||||
path: filepath.Join(nginx.GetConfPath(dir), file.Name()),
|
||||
path: filepath.Join(nginx.GetConfPath(dir), file.Name()),
|
||||
namespaceID: 0,
|
||||
namespace: nil,
|
||||
}
|
||||
@@ -124,14 +124,14 @@ func GetConfigs(c *gin.Context) {
|
||||
func createConfigBuilder(dir string) config.ConfigBuilder {
|
||||
return func(fileName string, fileInfo os.FileInfo, status config.ConfigStatus, namespaceID uint64, namespace *model.Namespace) config.Config {
|
||||
return config.Config{
|
||||
Name: fileName,
|
||||
ModifiedAt: fileInfo.ModTime(),
|
||||
Size: fileInfo.Size(),
|
||||
IsDir: fileInfo.IsDir(),
|
||||
Status: status,
|
||||
Name: fileName,
|
||||
ModifiedAt: fileInfo.ModTime(),
|
||||
Size: fileInfo.Size(),
|
||||
IsDir: fileInfo.IsDir(),
|
||||
Status: status,
|
||||
NamespaceID: namespaceID,
|
||||
Namespace: namespace,
|
||||
Dir: dir,
|
||||
Dir: dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package openai
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
|
||||
func InitRouter(r *gin.RouterGroup) {
|
||||
// ChatGPT
|
||||
r.POST("chatgpt", MakeChatCompletionRequest)
|
||||
|
||||
+5
-5
@@ -13,11 +13,11 @@ import (
|
||||
func GetSiteList(c *gin.Context) {
|
||||
// Parse query parameters
|
||||
options := &site.ListOptions{
|
||||
Search: c.Query("search"),
|
||||
Name: c.Query("name"),
|
||||
Status: c.Query("status"),
|
||||
OrderBy: c.Query("sort_by"),
|
||||
Sort: c.DefaultQuery("order", "desc"),
|
||||
Search: c.Query("search"),
|
||||
Name: c.Query("name"),
|
||||
Status: c.Query("status"),
|
||||
OrderBy: c.Query("sort_by"),
|
||||
Sort: c.DefaultQuery("order", "desc"),
|
||||
NamespaceID: cast.ToUint64(c.Query("env_group_id")),
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ package sites
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
func InitRouter(r *gin.RouterGroup) {
|
||||
// Initialize WebSocket notifications for site checking
|
||||
InitWebSocketNotifications()
|
||||
|
||||
r.GET("sites", GetSiteList)
|
||||
r.GET("sites/:name", GetSite)
|
||||
r.PUT("sites", BatchUpdateSites)
|
||||
@@ -10,6 +13,15 @@ func InitRouter(r *gin.RouterGroup) {
|
||||
r.POST("auto_cert/:name", AddDomainToAutoCert)
|
||||
r.DELETE("auto_cert/:name", RemoveDomainFromAutoCert)
|
||||
|
||||
// site navigation endpoints
|
||||
r.GET("site_navigation", GetSiteNavigation)
|
||||
r.GET("site_navigation/status", GetSiteNavigationStatus)
|
||||
r.POST("site_navigation/order", UpdateSiteOrder)
|
||||
r.GET("site_navigation/health_check/:id", GetHealthCheck)
|
||||
r.PUT("site_navigation/health_check/:id", UpdateHealthCheck)
|
||||
r.POST("site_navigation/test_health_check/:id", TestHealthCheck)
|
||||
r.GET("site_navigation_ws", SiteNavigationWebSocket)
|
||||
|
||||
// rename site
|
||||
r.POST("sites/:name/rename", RenameSite)
|
||||
// enable site
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ import (
|
||||
// buildProxyTargets processes proxy targets similar to list.go logic
|
||||
func buildProxyTargets(fileName string) []site.ProxyTarget {
|
||||
indexedSite := site.GetIndexedSite(fileName)
|
||||
|
||||
|
||||
// Convert proxy targets, expanding upstream references
|
||||
var proxyTargets []site.ProxyTarget
|
||||
upstreamService := upstream.GetUpstreamService()
|
||||
@@ -132,7 +132,7 @@ func SaveSite(c *gin.Context) {
|
||||
|
||||
var json struct {
|
||||
Content string `json:"content" binding:"required"`
|
||||
NamespaceID uint64 `json:"env_group_id"`
|
||||
NamespaceID uint64 `json:"env_group_id"`
|
||||
SyncNodeIDs []uint64 `json:"sync_node_ids"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
PostAction string `json:"post_action"`
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package sites
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/internal/sitecheck"
|
||||
"github.com/0xJacky/Nginx-UI/model"
|
||||
"github.com/0xJacky/Nginx-UI/query"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/uozi-tech/cosy"
|
||||
"github.com/uozi-tech/cosy/logger"
|
||||
)
|
||||
|
||||
// GetSiteNavigation returns all sites for navigation dashboard
|
||||
func GetSiteNavigation(c *gin.Context) {
|
||||
service := sitecheck.GetService()
|
||||
sites := service.GetSites()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": sites,
|
||||
})
|
||||
}
|
||||
|
||||
// GetSiteNavigationStatus returns the status of site checking service
|
||||
func GetSiteNavigationStatus(c *gin.Context) {
|
||||
service := sitecheck.GetService()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"running": service.IsRunning(),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateSiteOrder updates the custom order of sites
|
||||
func UpdateSiteOrder(c *gin.Context) {
|
||||
var req struct {
|
||||
OrderedIds []uint64 `json:"ordered_ids" binding:"required"`
|
||||
}
|
||||
|
||||
if !cosy.BindAndValid(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateSiteOrderBatchByIds(req.OrderedIds); err != nil {
|
||||
cosy.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Order updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// updateSiteOrderBatchByIds updates site order in batch using IDs
|
||||
func updateSiteOrderBatchByIds(orderedIds []uint64) error {
|
||||
sc := query.SiteConfig
|
||||
|
||||
for i, id := range orderedIds {
|
||||
if _, err := sc.Where(sc.ID.Eq(id)).Update(sc.CustomOrder, i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetHealthCheck gets health check configuration for a site
|
||||
func GetHealthCheck(c *gin.Context) {
|
||||
id := cast.ToUint64(c.Param("id"))
|
||||
|
||||
sc := query.SiteConfig
|
||||
siteConfig, err := sc.Where(sc.ID.Eq(id)).First()
|
||||
if err != nil {
|
||||
cosy.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
ensureHealthCheckConfig(siteConfig)
|
||||
|
||||
c.JSON(http.StatusOK, siteConfig)
|
||||
}
|
||||
|
||||
// createDefaultHealthCheckConfig creates default health check configuration
|
||||
func createDefaultHealthCheckConfig() *model.HealthCheckConfig {
|
||||
return &model.HealthCheckConfig{
|
||||
Protocol: "http",
|
||||
Method: "GET",
|
||||
Path: "/",
|
||||
ExpectedStatus: []int{200},
|
||||
GRPCMethod: "Check",
|
||||
}
|
||||
}
|
||||
|
||||
// ensureHealthCheckConfig ensures health check config is not nil
|
||||
func ensureHealthCheckConfig(siteConfig *model.SiteConfig) {
|
||||
if siteConfig.HealthCheckConfig == nil {
|
||||
siteConfig.HealthCheckConfig = createDefaultHealthCheckConfig()
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateHealthCheck updates health check configuration for a site
|
||||
func UpdateHealthCheck(c *gin.Context) {
|
||||
id := cast.ToUint64(c.Param("id"))
|
||||
|
||||
var req model.SiteConfig
|
||||
|
||||
if !cosy.BindAndValid(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
sc := query.SiteConfig
|
||||
siteConfig, err := sc.Where(sc.ID.Eq(id)).First()
|
||||
if err != nil {
|
||||
cosy.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
siteConfig.HealthCheckEnabled = req.HealthCheckEnabled
|
||||
siteConfig.CheckInterval = req.CheckInterval
|
||||
siteConfig.Timeout = req.Timeout
|
||||
siteConfig.UserAgent = req.UserAgent
|
||||
siteConfig.MaxRedirects = req.MaxRedirects
|
||||
siteConfig.FollowRedirects = req.FollowRedirects
|
||||
siteConfig.CheckFavicon = req.CheckFavicon
|
||||
|
||||
if req.HealthCheckConfig != nil {
|
||||
siteConfig.HealthCheckConfig = req.HealthCheckConfig
|
||||
}
|
||||
|
||||
if err = query.SiteConfig.Save(siteConfig); err != nil {
|
||||
cosy.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Health check configuration updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// TestHealthCheck tests a health check configuration without saving it
|
||||
func TestHealthCheck(c *gin.Context) {
|
||||
id := cast.ToUint64(c.Param("id"))
|
||||
|
||||
var req struct {
|
||||
Config *model.HealthCheckConfig `json:"config" binding:"required"`
|
||||
}
|
||||
|
||||
if !cosy.BindAndValid(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get site config to determine the host for testing
|
||||
sc := query.SiteConfig
|
||||
siteConfig, err := sc.Where(sc.ID.Eq(id)).First()
|
||||
if err != nil {
|
||||
cosy.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create enhanced checker and test the configuration
|
||||
enhancedChecker := sitecheck.NewEnhancedSiteChecker()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Convert host to URL for testing
|
||||
testURL := siteConfig.Scheme + "://" + siteConfig.Host
|
||||
result, err := enhancedChecker.CheckSiteWithConfig(ctx, testURL, req.Config)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Health check test failed for %s: %v", siteConfig.Host, err)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"error": err.Error(),
|
||||
"response_time": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
success := result.Status == "online"
|
||||
errorMsg := ""
|
||||
if !success && result.Error != "" {
|
||||
errorMsg = result.Error
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": success,
|
||||
"response_time": result.ResponseTime,
|
||||
"status": result.Status,
|
||||
"status_code": result.StatusCode,
|
||||
"error": errorMsg,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package sites
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||
"github.com/0xJacky/Nginx-UI/internal/sitecheck"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/uozi-tech/cosy/logger"
|
||||
)
|
||||
|
||||
// WebSocket message types
|
||||
const (
|
||||
MessageTypeInitial = "initial"
|
||||
MessageTypeUpdate = "update"
|
||||
MessageTypeRefresh = "refresh"
|
||||
MessageTypePing = "ping"
|
||||
MessageTypePong = "pong"
|
||||
)
|
||||
|
||||
// ClientMessage represents incoming WebSocket messages from client
|
||||
type ClientMessage struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// ServerMessage represents outgoing WebSocket messages to client
|
||||
type ServerMessage struct {
|
||||
Type string `json:"type"`
|
||||
Data []*sitecheck.SiteInfo `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// PongMessage represents a pong response
|
||||
type PongMessage struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// WebSocket connection manager
|
||||
type WSManager struct {
|
||||
connections map[*websocket.Conn]bool
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
var wsManager = &WSManager{
|
||||
connections: make(map[*websocket.Conn]bool),
|
||||
}
|
||||
|
||||
// AddConnection adds a WebSocket connection to the manager
|
||||
func (wm *WSManager) AddConnection(conn *websocket.Conn) {
|
||||
wm.mutex.Lock()
|
||||
defer wm.mutex.Unlock()
|
||||
wm.connections[conn] = true
|
||||
}
|
||||
|
||||
// RemoveConnection removes a WebSocket connection from the manager
|
||||
func (wm *WSManager) RemoveConnection(conn *websocket.Conn) {
|
||||
wm.mutex.Lock()
|
||||
defer wm.mutex.Unlock()
|
||||
delete(wm.connections, conn)
|
||||
}
|
||||
|
||||
// BroadcastUpdate sends updates to all connected WebSocket clients
|
||||
func (wm *WSManager) BroadcastUpdate(sites []*sitecheck.SiteInfo) {
|
||||
wm.mutex.RLock()
|
||||
defer wm.mutex.RUnlock()
|
||||
|
||||
for conn := range wm.connections {
|
||||
go func(c *websocket.Conn) {
|
||||
if err := sendSiteData(c, MessageTypeUpdate, sites); err != nil {
|
||||
logger.Error("Failed to send broadcast update:", err)
|
||||
wm.RemoveConnection(c)
|
||||
c.Close()
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// GetManager returns the global WebSocket manager instance
|
||||
func GetManager() *WSManager {
|
||||
return wsManager
|
||||
}
|
||||
|
||||
// InitWebSocketNotifications sets up the callback for site check updates
|
||||
func InitWebSocketNotifications() {
|
||||
service := sitecheck.GetService()
|
||||
service.SetUpdateCallback(func(sites []*sitecheck.SiteInfo) {
|
||||
wsManager.BroadcastUpdate(sites)
|
||||
})
|
||||
}
|
||||
|
||||
// SiteNavigationWebSocket handles WebSocket connections for real-time site status updates
|
||||
func SiteNavigationWebSocket(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error("WebSocket upgrade failed:", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
wsManager.RemoveConnection(conn)
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
logger.Info("Site navigation WebSocket connection established")
|
||||
|
||||
// Register connection with manager
|
||||
wsManager.AddConnection(conn)
|
||||
|
||||
service := sitecheck.GetService()
|
||||
|
||||
// Send initial data
|
||||
if err := sendSiteData(conn, MessageTypeInitial, service.GetSites()); err != nil {
|
||||
logger.Error("Failed to send initial data:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle incoming messages from client
|
||||
go handleClientMessages(conn, service)
|
||||
|
||||
<-ctx.Done()
|
||||
logger.Info("Request context cancelled, closing WebSocket")
|
||||
}
|
||||
|
||||
// sendSiteData sends site data via WebSocket
|
||||
func sendSiteData(conn *websocket.Conn, msgType string, sites []*sitecheck.SiteInfo) error {
|
||||
message := ServerMessage{
|
||||
Type: msgType,
|
||||
Data: sites,
|
||||
}
|
||||
return conn.WriteJSON(message)
|
||||
}
|
||||
|
||||
// handleClientMessages handles incoming WebSocket messages
|
||||
func handleClientMessages(conn *websocket.Conn, service *sitecheck.Service) {
|
||||
for {
|
||||
var msg ClientMessage
|
||||
if err := conn.ReadJSON(&msg); err != nil {
|
||||
if helper.IsUnexpectedWebsocketError(err) {
|
||||
logger.Error("WebSocket read error:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case MessageTypeRefresh:
|
||||
logger.Info("Client requested site refresh")
|
||||
service.RefreshSites()
|
||||
case MessageTypePing:
|
||||
pongMsg := PongMessage{Type: MessageTypePong}
|
||||
if err := conn.WriteJSON(pongMsg); err != nil {
|
||||
logger.Error("Failed to send pong:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ type Stream struct {
|
||||
// buildProxyTargets processes stream proxy targets similar to list.go logic
|
||||
func buildStreamProxyTargets(fileName string) []config.ProxyTarget {
|
||||
indexedStream := stream.GetIndexedStream(fileName)
|
||||
|
||||
|
||||
// Convert proxy targets, expanding upstream references
|
||||
var proxyTargets []config.ProxyTarget
|
||||
upstreamService := upstream.GetUpstreamService()
|
||||
@@ -67,11 +67,11 @@ func buildStreamProxyTargets(fileName string) []config.ProxyTarget {
|
||||
func GetStreams(c *gin.Context) {
|
||||
// Parse query parameters
|
||||
options := &stream.ListOptions{
|
||||
Search: c.Query("search"),
|
||||
Name: c.Query("name"),
|
||||
Status: c.Query("status"),
|
||||
OrderBy: c.Query("order_by"),
|
||||
Sort: c.DefaultQuery("sort", "desc"),
|
||||
Search: c.Query("search"),
|
||||
Name: c.Query("name"),
|
||||
Status: c.Query("status"),
|
||||
OrderBy: c.Query("order_by"),
|
||||
Sort: c.DefaultQuery("sort", "desc"),
|
||||
NamespaceID: cast.ToUint64(c.Query("namespace_id")),
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -155,8 +155,8 @@ func FinishPasskeyLogin(c *gin.Context) {
|
||||
secureSessionID := user.SetSecureSessionID(outUser.ID)
|
||||
|
||||
c.JSON(http.StatusOK, LoginResponse{
|
||||
Code: LoginSuccess,
|
||||
Message: "ok",
|
||||
Code: LoginSuccess,
|
||||
Message: "ok",
|
||||
AccessTokenPayload: token,
|
||||
SecureSessionID: secureSessionID,
|
||||
})
|
||||
|
||||
Vendored
+4
@@ -23,6 +23,7 @@ declare module 'vue' {
|
||||
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
|
||||
AComment: typeof import('ant-design-vue/es')['Comment']
|
||||
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
|
||||
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
|
||||
ADivider: typeof import('ant-design-vue/es')['Divider']
|
||||
ADrawer: typeof import('ant-design-vue/es')['Drawer']
|
||||
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
|
||||
@@ -48,8 +49,11 @@ declare module 'vue' {
|
||||
APopover: typeof import('ant-design-vue/es')['Popover']
|
||||
AProgress: typeof import('ant-design-vue/es')['Progress']
|
||||
AQrcode: typeof import('ant-design-vue/es')['QRCode']
|
||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||
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']
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import type { SiteStatusType } from '@/constants/site-status'
|
||||
import { http } from '@uozi-admin/request'
|
||||
import ws from '@/lib/websocket'
|
||||
|
||||
export interface SiteInfo {
|
||||
id: number // primary identifier for API operations
|
||||
host: string // host:port format
|
||||
port: number
|
||||
scheme: string // http, https, grpc, grpcs
|
||||
display_url: string // computed URL for display
|
||||
name: string
|
||||
status: SiteStatusType
|
||||
status_code: number
|
||||
response_time: number
|
||||
favicon_url: string
|
||||
favicon_data: string
|
||||
title: string
|
||||
last_checked: number
|
||||
error?: string
|
||||
// Legacy fields for backward compatibility
|
||||
url?: string // deprecated, use display_url instead
|
||||
health_check_protocol?: string // deprecated, use scheme instead
|
||||
host_port?: string // deprecated, use host instead
|
||||
}
|
||||
|
||||
export interface HealthCheckConfig {
|
||||
check_interval?: number
|
||||
timeout?: number
|
||||
user_agent?: string
|
||||
max_redirects?: number
|
||||
follow_redirects?: boolean
|
||||
check_favicon?: boolean
|
||||
health_check_config?: {
|
||||
protocol?: string
|
||||
method?: string
|
||||
path?: string
|
||||
headers?: Record<string, string>
|
||||
body?: string
|
||||
expected_status?: number[]
|
||||
expected_text?: string
|
||||
not_expected_text?: string
|
||||
validate_ssl?: boolean
|
||||
verify_hostname?: boolean
|
||||
grpc_service?: string
|
||||
grpc_method?: string
|
||||
dns_resolver?: string
|
||||
source_ip?: string
|
||||
client_cert?: string
|
||||
client_key?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface HeaderItem {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface EnhancedHealthCheckConfig {
|
||||
// Basic settings
|
||||
enabled: boolean
|
||||
interval: number
|
||||
timeout: number
|
||||
userAgent: string
|
||||
maxRedirects: number
|
||||
followRedirects: boolean
|
||||
checkFavicon: boolean
|
||||
|
||||
// Protocol settings
|
||||
protocol: string
|
||||
method: string
|
||||
path: string
|
||||
headers: HeaderItem[]
|
||||
body: string
|
||||
|
||||
// Response validation
|
||||
expectedStatus: number[]
|
||||
expectedText: string
|
||||
notExpectedText: string
|
||||
validateSSL: boolean
|
||||
verifyHostname: boolean
|
||||
|
||||
// gRPC settings
|
||||
grpcService: string
|
||||
grpcMethod: string
|
||||
|
||||
// Advanced settings
|
||||
dnsResolver: string
|
||||
sourceIP: string
|
||||
clientCert: string
|
||||
clientKey: string
|
||||
}
|
||||
|
||||
export interface HealthCheckTestConfig {
|
||||
protocol: string
|
||||
method: string
|
||||
path: string
|
||||
headers: Record<string, string>
|
||||
body: string
|
||||
expected_status: number[]
|
||||
expected_text: string
|
||||
not_expected_text: string
|
||||
validate_ssl: boolean
|
||||
grpc_service: string
|
||||
grpc_method: string
|
||||
timeout: number
|
||||
}
|
||||
|
||||
export interface SiteNavigationResponse {
|
||||
data: SiteInfo[]
|
||||
}
|
||||
|
||||
export interface SiteNavigationStatusResponse {
|
||||
running: boolean
|
||||
}
|
||||
|
||||
export const siteNavigationApi = {
|
||||
// Get all sites for navigation
|
||||
getSites(): Promise<SiteNavigationResponse> {
|
||||
return http.get('/site_navigation')
|
||||
},
|
||||
|
||||
// Get service status
|
||||
getStatus(): Promise<SiteNavigationStatusResponse> {
|
||||
return http.get('/site_navigation/status')
|
||||
},
|
||||
|
||||
// Update sites order
|
||||
updateOrder(orderedIds: number[]): Promise<{ message: string }> {
|
||||
return http.post('/site_navigation/order', { ordered_ids: orderedIds })
|
||||
},
|
||||
|
||||
// Get health check configuration
|
||||
getHealthCheck(id: number): Promise<HealthCheckConfig> {
|
||||
return http.get(`/site_navigation/health_check/${id}`)
|
||||
},
|
||||
|
||||
// Update health check configuration
|
||||
updateHealthCheck(id: number, config: HealthCheckConfig): Promise<{ message: string }> {
|
||||
return http.put(`/site_navigation/health_check/${id}`, config)
|
||||
},
|
||||
|
||||
// Test health check configuration
|
||||
testHealthCheck(id: number, config: HealthCheckTestConfig): Promise<{ success: boolean, response_time?: number, error?: string }> {
|
||||
return http.post(`/site_navigation/test_health_check/${id}`, { config })
|
||||
},
|
||||
|
||||
// WebSocket connection using lib/websocket
|
||||
createWebSocket() {
|
||||
return ws('/api/site_navigation_ws', true)
|
||||
},
|
||||
}
|
||||
@@ -59,35 +59,35 @@ const notifications: Record<string, { title: () => string, content: (args: any)
|
||||
},
|
||||
'Sync Certificate Error': {
|
||||
title: () => $gettext('Sync Certificate Error'),
|
||||
content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} failed', args, true),
|
||||
content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{node_name} failed', args, true),
|
||||
},
|
||||
'Sync Certificate Success': {
|
||||
title: () => $gettext('Sync Certificate Success'),
|
||||
content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} successfully', args, true),
|
||||
content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{node_name} successfully', args, true),
|
||||
},
|
||||
'Sync Config Error': {
|
||||
title: () => $gettext('Sync Config Error'),
|
||||
content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} failed', args, true),
|
||||
content: (args: any) => $gettext('Sync config %{config_name} to %{node_name} failed', args, true),
|
||||
},
|
||||
'Sync Config Success': {
|
||||
title: () => $gettext('Sync Config Success'),
|
||||
content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} successfully', args, true),
|
||||
content: (args: any) => $gettext('Sync config %{config_name} to %{node_name} successfully', args, true),
|
||||
},
|
||||
'Rename Remote Config Error': {
|
||||
title: () => $gettext('Rename Remote Config Error'),
|
||||
content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{env_name} failed', args, true),
|
||||
content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{node_name} failed', args, true),
|
||||
},
|
||||
'Rename Remote Config Success': {
|
||||
title: () => $gettext('Rename Remote Config Success'),
|
||||
content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{env_name} successfully', args, true),
|
||||
content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{node_name} successfully', args, true),
|
||||
},
|
||||
'Delete Remote Config Error': {
|
||||
title: () => $gettext('Delete Remote Config Error'),
|
||||
content: (args: any) => $gettext('Delete %{path} on %{env_name} failed', args, true),
|
||||
content: (args: any) => $gettext('Delete %{path} on %{node_name} failed', args, true),
|
||||
},
|
||||
'Delete Remote Config Success': {
|
||||
title: () => $gettext('Delete Remote Config Success'),
|
||||
content: (args: any) => $gettext('Delete %{path} on %{env_name} successfully', args, true),
|
||||
content: (args: any) => $gettext('Delete %{path} on %{node_name} successfully', args, true),
|
||||
},
|
||||
'External Notification Test': {
|
||||
title: () => $gettext('External Notification Test'),
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Site health check status constants
|
||||
export const SiteStatus = {
|
||||
ONLINE: 'online',
|
||||
OFFLINE: 'offline',
|
||||
ERROR: 'error',
|
||||
CHECKING: 'checking',
|
||||
} as const
|
||||
|
||||
// Type for site status
|
||||
export type SiteStatusType = typeof SiteStatus[keyof typeof SiteStatus]
|
||||
|
||||
// Status display configuration
|
||||
export const SiteStatusConfig = {
|
||||
[SiteStatus.ONLINE]: {
|
||||
label: 'Online',
|
||||
color: 'success',
|
||||
icon: 'CheckCircleOutlined',
|
||||
},
|
||||
[SiteStatus.OFFLINE]: {
|
||||
label: 'Offline',
|
||||
color: 'error',
|
||||
icon: 'CloseCircleOutlined',
|
||||
},
|
||||
[SiteStatus.ERROR]: {
|
||||
label: 'Error',
|
||||
color: 'warning',
|
||||
icon: 'ExclamationCircleOutlined',
|
||||
},
|
||||
[SiteStatus.CHECKING]: {
|
||||
label: 'Checking',
|
||||
color: 'processing',
|
||||
icon: 'SyncOutlined',
|
||||
},
|
||||
} as const
|
||||
@@ -27,6 +27,14 @@ export const dashboardRoutes: RouteRecordRaw[] = [
|
||||
name: () => $gettext('Nginx'),
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'sites',
|
||||
component: () => import('@/views/dashboard/SiteNavigation.vue'),
|
||||
name: 'SiteNavigation',
|
||||
meta: {
|
||||
name: () => $gettext('Sites'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
<script setup lang="ts">
|
||||
import type ReconnectingWebSocket from 'reconnecting-websocket'
|
||||
import type { SiteInfo } from '@/api/site_navigation'
|
||||
import { GlobalOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import Sortable from 'sortablejs'
|
||||
import { siteNavigationApi } from '@/api/site_navigation'
|
||||
import SiteCard from './components/SiteCard.vue'
|
||||
import SiteHealthCheckModal from './components/SiteHealthCheckModal.vue'
|
||||
import SiteNavigationToolbar from './components/SiteNavigationToolbar.vue'
|
||||
|
||||
const sites = ref<SiteInfo[]>([])
|
||||
const loading = ref(true)
|
||||
const refreshing = ref(false)
|
||||
const isConnected = ref(false)
|
||||
const settingsMode = ref(false)
|
||||
const draggableSites = ref<SiteInfo[]>([])
|
||||
const configModalVisible = ref(false)
|
||||
const configTarget = ref<SiteInfo>()
|
||||
|
||||
let sortableInstance: Sortable | null = null
|
||||
let websocket: ReconnectingWebSocket | WebSocket | null = null
|
||||
|
||||
// Display sites - use draggable sites in settings mode, backend sorted sites otherwise
|
||||
const displaySites = computed(() => {
|
||||
return settingsMode.value ? draggableSites.value : sites.value
|
||||
})
|
||||
|
||||
// WebSocket connection
|
||||
function connectWebSocket() {
|
||||
try {
|
||||
websocket = siteNavigationApi.createWebSocket()
|
||||
|
||||
if (!websocket) {
|
||||
isConnected.value = false
|
||||
return
|
||||
}
|
||||
|
||||
websocket.onopen = () => {
|
||||
isConnected.value = true
|
||||
}
|
||||
|
||||
websocket.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.type === 'initial' || data.type === 'update') {
|
||||
sites.value = data.data || []
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error)
|
||||
}
|
||||
}
|
||||
|
||||
websocket.onclose = () => {
|
||||
isConnected.value = false
|
||||
}
|
||||
|
||||
websocket.onerror = error => {
|
||||
console.error('Site navigation WebSocket error:', error)
|
||||
isConnected.value = false
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to connect WebSocket:', error)
|
||||
isConnected.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Load sites via HTTP (fallback)
|
||||
async function loadSites() {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await siteNavigationApi.getSites()
|
||||
sites.value = response.data || []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to load sites:', error)
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh sites
|
||||
async function handleRefresh() {
|
||||
try {
|
||||
refreshing.value = true
|
||||
|
||||
// Only use WebSocket refresh
|
||||
if (websocket && isConnected.value) {
|
||||
websocket.send(JSON.stringify({ type: 'refresh' }))
|
||||
message.success($gettext('Site refresh initiated'))
|
||||
}
|
||||
else {
|
||||
message.warning($gettext('WebSocket not connected, please wait for connection'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to refresh sites:', error)
|
||||
message.error($gettext('Failed to refresh sites'))
|
||||
}
|
||||
finally {
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle settings mode
|
||||
function toggleSettingsMode() {
|
||||
settingsMode.value = !settingsMode.value
|
||||
|
||||
if (settingsMode.value) {
|
||||
draggableSites.value = [...sites.value]
|
||||
nextTick(() => initSortable())
|
||||
}
|
||||
else {
|
||||
destroySortable()
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize sortable
|
||||
function initSortable() {
|
||||
const gridElement = document.querySelector('.site-grid')
|
||||
if (gridElement && !sortableInstance) {
|
||||
sortableInstance = new Sortable(gridElement as HTMLElement, {
|
||||
animation: 150,
|
||||
ghostClass: 'site-card-ghost',
|
||||
chosenClass: 'site-card-chosen',
|
||||
dragClass: 'site-card-drag',
|
||||
onEnd: () => {
|
||||
// Update draggableSites order based on DOM order
|
||||
const cards = Array.from(gridElement.children)
|
||||
const newOrder = cards.map(card => {
|
||||
const url = card.getAttribute('data-url')
|
||||
return draggableSites.value.find(site => site.url === url)!
|
||||
})
|
||||
draggableSites.value = newOrder
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy sortable
|
||||
function destroySortable() {
|
||||
if (sortableInstance) {
|
||||
sortableInstance.destroy()
|
||||
sortableInstance = null
|
||||
}
|
||||
}
|
||||
|
||||
// Save order
|
||||
async function saveOrder() {
|
||||
try {
|
||||
const orderedIds = draggableSites.value.map(site => site.id)
|
||||
await siteNavigationApi.updateOrder(orderedIds)
|
||||
message.success($gettext('Order saved successfully'))
|
||||
|
||||
// Update sites.value immediately to reflect the new order
|
||||
sites.value = [...draggableSites.value]
|
||||
|
||||
settingsMode.value = false
|
||||
destroySortable()
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to save order:', error)
|
||||
message.error($gettext('Failed to save order'))
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel settings mode
|
||||
function cancelSettingsMode() {
|
||||
settingsMode.value = false
|
||||
destroySortable()
|
||||
draggableSites.value = []
|
||||
}
|
||||
|
||||
// Open config modal
|
||||
function openConfigModal(site: SiteInfo) {
|
||||
configTarget.value = site
|
||||
configModalVisible.value = true
|
||||
}
|
||||
|
||||
// Handle health check config save
|
||||
async function handleConfigSave(config: import('@/api/site_navigation').HealthCheckConfig) {
|
||||
try {
|
||||
if (configTarget.value) {
|
||||
await siteNavigationApi.updateHealthCheck(configTarget.value.id, config)
|
||||
message.success($gettext('Health check configuration saved'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to save health check config:', error)
|
||||
message.error($gettext('Failed to save configuration'))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// First load data via HTTP
|
||||
await loadSites()
|
||||
// Then connect WebSocket for real-time updates
|
||||
connectWebSocket()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
destroySortable()
|
||||
if (websocket) {
|
||||
websocket.close()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="site-navigation">
|
||||
<SiteNavigationToolbar
|
||||
:is-connected="isConnected"
|
||||
:refreshing="refreshing"
|
||||
:settings-mode="settingsMode"
|
||||
@refresh="handleRefresh"
|
||||
@toggle-settings="toggleSettingsMode"
|
||||
@save-order="saveOrder"
|
||||
@cancel-settings="cancelSettingsMode"
|
||||
/>
|
||||
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="displaySites.length === 0" class="empty-state">
|
||||
<GlobalOutlined class="text-6xl text-gray-400 mb-4" />
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||
{{ $gettext('No sites found') }}
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-center max-w-md">
|
||||
{{ $gettext('Sites will appear here once you configure nginx server blocks with valid server_name directives.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="site-grid">
|
||||
<SiteCard
|
||||
v-for="site in displaySites"
|
||||
:key="site.id"
|
||||
:site="site"
|
||||
:settings-mode="settingsMode"
|
||||
@open-config="openConfigModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SiteHealthCheckModal
|
||||
v-model:open="configModalVisible"
|
||||
:site="configTarget"
|
||||
@save="handleConfigSave"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.site-navigation {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@apply flex flex-col items-center justify-center py-16 text-center;
|
||||
}
|
||||
|
||||
.site-grid {
|
||||
@apply grid gap-6;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
}
|
||||
|
||||
/* Responsive design for narrow screens */
|
||||
@media (max-width: 768px) {
|
||||
.site-navigation {
|
||||
@apply p-4;
|
||||
}
|
||||
|
||||
.site-grid {
|
||||
grid-template-columns: 1fr;
|
||||
@apply gap-4;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.site-navigation {
|
||||
@apply p-3;
|
||||
}
|
||||
|
||||
.site-grid {
|
||||
@apply gap-3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,323 @@
|
||||
<script setup lang="ts">
|
||||
import type { SiteInfo } from '@/api/site_navigation'
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
CodeOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { truncate, upperFirst } from 'lodash'
|
||||
import { SiteStatus } from '@/constants/site-status'
|
||||
|
||||
interface Props {
|
||||
site: SiteInfo
|
||||
settingsMode: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'openConfig', site: SiteInfo): void
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
|
||||
// Check if site can be opened (only HTTP/HTTPS)
|
||||
function canOpenSite(site: SiteInfo): boolean {
|
||||
const scheme = site.scheme || site.health_check_protocol || 'http'
|
||||
return scheme === 'http' || scheme === 'https'
|
||||
}
|
||||
|
||||
// Open site in new tab (only for HTTP/HTTPS)
|
||||
function openSite(site: SiteInfo) {
|
||||
if (!canOpenSite(site)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Use display_url if available, otherwise construct from scheme and host_port
|
||||
let targetUrl = site.display_url || site.url
|
||||
|
||||
// If we have scheme and host_port, construct the URL
|
||||
if (site.scheme && site.host_port && (site.scheme === 'http' || site.scheme === 'https')) {
|
||||
targetUrl = `${site.scheme}://${site.host_port}`
|
||||
}
|
||||
|
||||
window.open(targetUrl, '_blank')
|
||||
}
|
||||
|
||||
// Handle favicon loading error
|
||||
function handleFaviconError(event: Event) {
|
||||
const img = event.target as HTMLImageElement
|
||||
img.style.display = 'none'
|
||||
}
|
||||
|
||||
// Get avatar color based on site name
|
||||
function getAvatarColor(name: string): string {
|
||||
const colors = [
|
||||
'#f87171',
|
||||
'#fb923c',
|
||||
'#facc15',
|
||||
'#a3e635',
|
||||
'#4ade80',
|
||||
'#22d3ee',
|
||||
'#60a5fa',
|
||||
'#a78bfa',
|
||||
'#f472b6',
|
||||
'#fb7185',
|
||||
]
|
||||
|
||||
let hash = 0
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash)
|
||||
}
|
||||
|
||||
return colors[Math.abs(hash) % colors.length]
|
||||
}
|
||||
|
||||
// Get initials from site name
|
||||
function getInitials(name: string): string {
|
||||
const parts = name.split('.')
|
||||
return truncate(
|
||||
parts
|
||||
.map(part => upperFirst(part.charAt(0)))
|
||||
.join(''),
|
||||
{ length: 2, omission: '' },
|
||||
)
|
||||
}
|
||||
|
||||
// Get status CSS class
|
||||
function getStatusClass(status: string): string {
|
||||
switch (status) {
|
||||
case SiteStatus.ONLINE:
|
||||
return 'status-online'
|
||||
case SiteStatus.OFFLINE:
|
||||
return 'status-offline'
|
||||
case SiteStatus.ERROR:
|
||||
return 'status-error'
|
||||
case SiteStatus.CHECKING:
|
||||
return 'status-checking'
|
||||
default:
|
||||
return 'status-unknown'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="site-card"
|
||||
:class="{
|
||||
'settings-mode': settingsMode,
|
||||
'clickable': !settingsMode && canOpenSite(site),
|
||||
'non-clickable': !settingsMode && !canOpenSite(site),
|
||||
}"
|
||||
:data-url="site.url"
|
||||
@click="!settingsMode && canOpenSite(site) && openSite(site)"
|
||||
>
|
||||
<div class="site-card-header">
|
||||
<div class="site-icon">
|
||||
<img
|
||||
v-if="site.favicon_data"
|
||||
:src="site.favicon_data"
|
||||
:alt="site.name"
|
||||
class="w-8 h-8 rounded"
|
||||
@error="handleFaviconError"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="avatar-fallback"
|
||||
:style="{ backgroundColor: getAvatarColor(site.name) }"
|
||||
>
|
||||
{{ getInitials(site.name) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!settingsMode" class="site-status">
|
||||
<div
|
||||
class="status-indicator"
|
||||
:class="getStatusClass(site.status)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="site-info">
|
||||
<h3 class="site-title">
|
||||
{{ site.title || site.name }}
|
||||
</h3>
|
||||
<p class="site-url">
|
||||
<span v-if="site.scheme && site.host_port" class="url-parts">
|
||||
<span class="scheme">{{ site.scheme }}://</span><span class="host-port">{{ site.host_port }}</span>
|
||||
</span>
|
||||
<span v-else>{{ site.display_url || site.url }}</span>
|
||||
</p>
|
||||
|
||||
<div class="site-details">
|
||||
<div v-if="site.status === SiteStatus.ONLINE" class="detail-item">
|
||||
<ClockCircleOutlined class="detail-icon" />
|
||||
<span>{{ site.response_time }}ms</span>
|
||||
</div>
|
||||
<div v-if="site.status_code" class="detail-item">
|
||||
<CodeOutlined class="detail-icon" />
|
||||
<span>{{ site.status_code }}</span>
|
||||
</div>
|
||||
<div v-if="site.error" class="detail-item error">
|
||||
<ExclamationCircleOutlined class="detail-icon" />
|
||||
<span>{{ site.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings button in settings mode -->
|
||||
<div v-if="settingsMode" class="site-card-config">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click.stop="$emit('openConfig', site)"
|
||||
>
|
||||
<template #icon>
|
||||
<SettingOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- Drag handle in settings mode -->
|
||||
<div v-if="settingsMode" class="drag-handle">
|
||||
<div class="drag-dots">
|
||||
<div class="dot" />
|
||||
<div class="dot" />
|
||||
<div class="dot" />
|
||||
<div class="dot" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.site-card {
|
||||
@apply relative bg-white dark:bg-trueGray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 transition-all duration-200;
|
||||
}
|
||||
|
||||
.site-card.clickable {
|
||||
@apply cursor-pointer hover:scale-105;
|
||||
}
|
||||
|
||||
.site-card.non-clickable {
|
||||
@apply cursor-default;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.site-card.settings-mode {
|
||||
@apply cursor-move;
|
||||
}
|
||||
|
||||
.site-card.settings-mode:hover {
|
||||
@apply scale-100;
|
||||
}
|
||||
|
||||
.site-card-header {
|
||||
@apply flex items-center justify-between mb-3;
|
||||
}
|
||||
|
||||
.site-icon img {
|
||||
@apply w-8 h-8 rounded object-cover;
|
||||
}
|
||||
|
||||
.avatar-fallback {
|
||||
@apply w-8 h-8 rounded flex items-center justify-center text-white font-medium text-sm;
|
||||
}
|
||||
|
||||
.site-status {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
@apply w-3 h-3 rounded-full;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
@apply bg-green-500;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
@apply bg-red-500;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
@apply bg-yellow-500;
|
||||
}
|
||||
|
||||
.status-checking {
|
||||
@apply bg-blue-500 animate-pulse;
|
||||
}
|
||||
|
||||
.status-unknown {
|
||||
@apply bg-gray-400;
|
||||
}
|
||||
|
||||
.site-info {
|
||||
@apply space-y-2;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
@apply font-medium text-gray-900 dark:text-gray-100 text-lg truncate;
|
||||
}
|
||||
|
||||
.scheme {
|
||||
@apply text-sm text-gray-600 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.site-url {
|
||||
@apply text-sm text-gray-600 dark:text-gray-400 truncate;
|
||||
}
|
||||
|
||||
.url-parts {
|
||||
@apply inline;
|
||||
}
|
||||
|
||||
.host-port {
|
||||
@apply text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
|
||||
.site-details {
|
||||
@apply flex flex-wrap gap-3 text-xs;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
@apply flex items-center gap-1 text-gray-600 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.detail-item.error {
|
||||
@apply text-red-600 dark:text-red-400;
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
@apply w-3 h-3;
|
||||
}
|
||||
|
||||
.site-card-config {
|
||||
@apply absolute top-2 right-2;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
@apply absolute bottom-2 right-2 opacity-50 hover:opacity-100 transition-opacity;
|
||||
}
|
||||
|
||||
.drag-dots {
|
||||
@apply grid grid-cols-2 gap-1 p-1;
|
||||
}
|
||||
|
||||
.dot {
|
||||
@apply w-1 h-1 bg-gray-400 rounded-full;
|
||||
}
|
||||
|
||||
/* Sortable states */
|
||||
.site-card-ghost {
|
||||
@apply opacity-50;
|
||||
}
|
||||
|
||||
.site-card-chosen {
|
||||
@apply transform scale-105;
|
||||
}
|
||||
|
||||
.site-card-drag {
|
||||
@apply transform rotate-2;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,709 @@
|
||||
<script setup lang="ts">
|
||||
import type { EnhancedHealthCheckConfig, HeaderItem, SiteInfo } from '@/api/site_navigation'
|
||||
import { CloseOutlined, PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { siteNavigationApi } from '@/api/site_navigation'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
site?: SiteInfo
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:open', value: boolean): void
|
||||
(e: 'save', config: EnhancedHealthCheckConfig): void
|
||||
(e: 'refresh'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const testing = ref(false)
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: value => emit('update:open', value),
|
||||
})
|
||||
|
||||
const formData = ref<EnhancedHealthCheckConfig>({
|
||||
// Basic settings (health check is always enabled)
|
||||
enabled: true,
|
||||
interval: 300,
|
||||
timeout: 10,
|
||||
userAgent: 'Nginx-UI Enhanced Checker/2.0',
|
||||
maxRedirects: 3,
|
||||
followRedirects: true,
|
||||
checkFavicon: true,
|
||||
|
||||
// Protocol settings
|
||||
protocol: 'http',
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
headers: [],
|
||||
body: '',
|
||||
|
||||
// Response validation
|
||||
expectedStatus: [200],
|
||||
expectedText: '',
|
||||
notExpectedText: '',
|
||||
validateSSL: false,
|
||||
verifyHostname: false,
|
||||
|
||||
// gRPC settings
|
||||
grpcService: '',
|
||||
grpcMethod: 'Check',
|
||||
|
||||
// Advanced settings
|
||||
dnsResolver: '',
|
||||
sourceIP: '',
|
||||
clientCert: '',
|
||||
clientKey: '',
|
||||
})
|
||||
|
||||
// Load existing config when site changes
|
||||
watchEffect(async () => {
|
||||
if (props.site) {
|
||||
await loadExistingConfig()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadExistingConfig() {
|
||||
if (!props.site)
|
||||
return
|
||||
|
||||
try {
|
||||
const config = await siteNavigationApi.getHealthCheck(props.site.id)
|
||||
|
||||
// Convert backend config to frontend format
|
||||
formData.value = {
|
||||
// Basic settings (health check is always enabled)
|
||||
enabled: true,
|
||||
interval: config.check_interval ?? 300,
|
||||
timeout: config.timeout ?? 10,
|
||||
userAgent: config.user_agent ?? 'Nginx-UI Enhanced Checker/2.0',
|
||||
maxRedirects: config.max_redirects ?? 3,
|
||||
followRedirects: config.follow_redirects ?? true,
|
||||
checkFavicon: config.check_favicon ?? true,
|
||||
|
||||
// Protocol settings
|
||||
protocol: config.health_check_config?.protocol ?? 'http',
|
||||
method: config.health_check_config?.method ?? 'GET',
|
||||
path: config.health_check_config?.path ?? '/',
|
||||
headers: convertHeadersToArray(config.health_check_config?.headers ?? {}),
|
||||
body: config.health_check_config?.body ?? '',
|
||||
|
||||
// Response validation
|
||||
expectedStatus: config.health_check_config?.expected_status ?? [200],
|
||||
expectedText: config.health_check_config?.expected_text ?? '',
|
||||
notExpectedText: config.health_check_config?.not_expected_text ?? '',
|
||||
validateSSL: config.health_check_config?.validate_ssl ?? false,
|
||||
verifyHostname: config.health_check_config?.verify_hostname ?? false,
|
||||
|
||||
// gRPC settings
|
||||
grpcService: config.health_check_config?.grpc_service ?? '',
|
||||
grpcMethod: config.health_check_config?.grpc_method ?? 'Check',
|
||||
|
||||
// Advanced settings
|
||||
dnsResolver: config.health_check_config?.dns_resolver ?? '',
|
||||
sourceIP: config.health_check_config?.source_ip ?? '',
|
||||
clientCert: config.health_check_config?.client_cert ?? '',
|
||||
clientKey: config.health_check_config?.client_key ?? '',
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to load health check config:', error)
|
||||
// Fallback to defaults
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.value = {
|
||||
// Basic settings (health check is always enabled)
|
||||
enabled: true,
|
||||
interval: 300,
|
||||
timeout: 10,
|
||||
userAgent: 'Nginx-UI Enhanced Checker/2.0',
|
||||
maxRedirects: 3,
|
||||
followRedirects: true,
|
||||
checkFavicon: true,
|
||||
|
||||
// Protocol settings
|
||||
protocol: 'http',
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
headers: [],
|
||||
body: '',
|
||||
|
||||
// Response validation
|
||||
expectedStatus: [200],
|
||||
expectedText: '',
|
||||
notExpectedText: '',
|
||||
validateSSL: false,
|
||||
verifyHostname: false,
|
||||
|
||||
// gRPC settings
|
||||
grpcService: '',
|
||||
grpcMethod: 'Check',
|
||||
|
||||
// Advanced settings
|
||||
dnsResolver: '',
|
||||
sourceIP: '',
|
||||
clientCert: '',
|
||||
clientKey: '',
|
||||
}
|
||||
}
|
||||
|
||||
function convertHeadersToArray(headers: { [key: string]: string }): HeaderItem[] {
|
||||
return Object.entries(headers || {}).map(([name, value]) => ({ name, value }))
|
||||
}
|
||||
|
||||
function isHttpProtocol(protocol: string): boolean {
|
||||
return ['http', 'https'].includes(protocol)
|
||||
}
|
||||
|
||||
function isGrpcProtocol(protocol: string): boolean {
|
||||
return ['grpc', 'grpcs'].includes(protocol)
|
||||
}
|
||||
|
||||
function isDefaultHttpPort(port: string, protocol: string): boolean {
|
||||
return (port === '80' && protocol === 'http')
|
||||
|| (port === '443' && protocol === 'https')
|
||||
|| !port
|
||||
}
|
||||
|
||||
function isDefaultGrpcPort(port: string, protocol: string): boolean {
|
||||
return (port === '80' && protocol === 'grpc')
|
||||
|| (port === '443' && protocol === 'grpcs')
|
||||
}
|
||||
|
||||
function getGrpcDefaultPort(urlProtocol: string, protocol: string): string {
|
||||
return (urlProtocol === 'https:' || protocol === 'grpcs') ? '443' : '80'
|
||||
}
|
||||
|
||||
function buildUrl(protocol: string, hostname: string, port?: string): string {
|
||||
return port ? `${protocol}://${hostname}:${port}` : `${protocol}://${hostname}`
|
||||
}
|
||||
|
||||
function getHttpTestUrl(protocol: string, siteUrl: string): string {
|
||||
try {
|
||||
const url = new URL(siteUrl)
|
||||
const hostname = url.hostname
|
||||
const port = url.port
|
||||
|
||||
if (isDefaultHttpPort(port, protocol)) {
|
||||
return buildUrl(protocol, hostname)
|
||||
}
|
||||
return buildUrl(protocol, hostname, port)
|
||||
}
|
||||
catch {
|
||||
return `${protocol}://${siteUrl}`
|
||||
}
|
||||
}
|
||||
|
||||
function getGrpcTestUrl(protocol: string, siteUrl: string): string {
|
||||
try {
|
||||
const url = new URL(siteUrl)
|
||||
const hostname = url.hostname
|
||||
let port = url.port
|
||||
|
||||
if (!port) {
|
||||
port = getGrpcDefaultPort(url.protocol, protocol)
|
||||
}
|
||||
|
||||
if (isDefaultGrpcPort(port, protocol)) {
|
||||
return buildUrl(protocol, hostname)
|
||||
}
|
||||
return buildUrl(protocol, hostname, port)
|
||||
}
|
||||
catch {
|
||||
return `${protocol}://${siteUrl}`
|
||||
}
|
||||
}
|
||||
|
||||
function getTestUrl(): string {
|
||||
if (!props.site) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const protocol = formData.value.protocol
|
||||
|
||||
if (isHttpProtocol(protocol)) {
|
||||
return getHttpTestUrl(protocol, props.site.display_url || props.site.url || '')
|
||||
}
|
||||
|
||||
if (isGrpcProtocol(protocol)) {
|
||||
return getGrpcTestUrl(protocol, props.site.display_url || props.site.url || '')
|
||||
}
|
||||
|
||||
return props.site.display_url || props.site.url || ''
|
||||
}
|
||||
|
||||
function addHeader() {
|
||||
formData.value.headers.push({ name: '', value: '' })
|
||||
}
|
||||
|
||||
function removeHeader(index: number) {
|
||||
formData.value.headers.splice(index, 1)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!props.site)
|
||||
return
|
||||
|
||||
try {
|
||||
// Convert headers array to map for backend
|
||||
const config = { ...formData.value }
|
||||
const headersMap: { [key: string]: string } = {}
|
||||
config.headers.forEach(header => {
|
||||
if (header.name && header.value) {
|
||||
headersMap[header.name] = header.value
|
||||
}
|
||||
})
|
||||
|
||||
// Create the config object for the backend
|
||||
const backendConfig = {
|
||||
url: props.site.url,
|
||||
health_check_enabled: true, // Always enabled
|
||||
check_interval: config.interval,
|
||||
timeout: config.timeout,
|
||||
user_agent: config.userAgent,
|
||||
max_redirects: config.maxRedirects,
|
||||
follow_redirects: config.followRedirects,
|
||||
check_favicon: config.checkFavicon,
|
||||
|
||||
// Enhanced health check config (always included)
|
||||
health_check_config: {
|
||||
protocol: config.protocol,
|
||||
method: config.method,
|
||||
path: config.path,
|
||||
headers: headersMap,
|
||||
body: config.body,
|
||||
expected_status: config.expectedStatus,
|
||||
expected_text: config.expectedText,
|
||||
not_expected_text: config.notExpectedText,
|
||||
validate_ssl: config.validateSSL,
|
||||
grpc_service: config.grpcService,
|
||||
grpc_method: config.grpcMethod,
|
||||
dns_resolver: config.dnsResolver,
|
||||
source_ip: config.sourceIP,
|
||||
verify_hostname: config.verifyHostname,
|
||||
client_cert: config.clientCert,
|
||||
client_key: config.clientKey,
|
||||
},
|
||||
}
|
||||
|
||||
await siteNavigationApi.updateHealthCheck(props.site.id, backendConfig)
|
||||
message.success($gettext('Health check configuration saved successfully'))
|
||||
|
||||
// Trigger site refresh to update display URLs
|
||||
emit('refresh')
|
||||
|
||||
visible.value = false
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to save health check config:', error)
|
||||
message.error($gettext('Failed to save health check configuration'))
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTest() {
|
||||
if (!props.site)
|
||||
return
|
||||
|
||||
try {
|
||||
testing.value = true
|
||||
|
||||
// Create a test configuration
|
||||
const testConfig = {
|
||||
protocol: formData.value.protocol,
|
||||
method: formData.value.method,
|
||||
path: formData.value.path,
|
||||
headers: formData.value.headers.reduce((acc, header) => {
|
||||
if (header.name && header.value) {
|
||||
acc[header.name] = header.value
|
||||
}
|
||||
return acc
|
||||
}, {} as { [key: string]: string }),
|
||||
body: formData.value.body,
|
||||
expected_status: formData.value.expectedStatus,
|
||||
expected_text: formData.value.expectedText,
|
||||
not_expected_text: formData.value.notExpectedText,
|
||||
validate_ssl: formData.value.validateSSL,
|
||||
grpc_service: formData.value.grpcService,
|
||||
grpc_method: formData.value.grpcMethod,
|
||||
timeout: formData.value.timeout,
|
||||
}
|
||||
|
||||
// Call test API endpoint (we'll need to create this)
|
||||
const result = await siteNavigationApi.testHealthCheck(props.site.id, testConfig)
|
||||
|
||||
if (result.success) {
|
||||
message.success($gettext('Test successful! Response time: %{response_time}ms', { response_time: String(result.response_time || 0) }))
|
||||
}
|
||||
else {
|
||||
message.error($gettext('Test failed: %{error}', { error: result.error || 'Unknown error' }, true))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Health check test failed:', error)
|
||||
message.error($gettext('Test failed: Unable to perform health check'))
|
||||
}
|
||||
finally {
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="`${$gettext('Health Check Configuration')} - ${site?.name || getTestUrl()}`"
|
||||
width="800px"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="p-2">
|
||||
<a-form
|
||||
:model="formData"
|
||||
layout="vertical"
|
||||
:label-col="{ span: 24 }"
|
||||
:wrapper-col="{ span: 24 }"
|
||||
>
|
||||
<div>
|
||||
<!-- Protocol Selection -->
|
||||
<a-form-item :label="$gettext('Protocol')">
|
||||
<a-radio-group v-model:value="formData.protocol">
|
||||
<a-radio value="http">
|
||||
HTTP
|
||||
</a-radio>
|
||||
<a-radio value="https">
|
||||
HTTPS
|
||||
</a-radio>
|
||||
<a-radio value="grpc">
|
||||
gRPC
|
||||
</a-radio>
|
||||
<a-radio value="grpcs">
|
||||
gRPCS
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- HTTP/HTTPS Settings -->
|
||||
<div v-if="!['grpc', 'grpcs'].includes(formData.protocol)">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$gettext('HTTP Method')">
|
||||
<a-select v-model:value="formData.method" style="width: 100%">
|
||||
<a-select-option value="GET">
|
||||
GET
|
||||
</a-select-option>
|
||||
<a-select-option value="POST">
|
||||
POST
|
||||
</a-select-option>
|
||||
<a-select-option value="PUT">
|
||||
PUT
|
||||
</a-select-option>
|
||||
<a-select-option value="HEAD">
|
||||
HEAD
|
||||
</a-select-option>
|
||||
<a-select-option value="OPTIONS">
|
||||
OPTIONS
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$gettext('Path')">
|
||||
<a-input v-model:value="formData.path" placeholder="/" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item :label="$gettext('Custom Headers')" class="mb-4">
|
||||
<div class="space-y-2">
|
||||
<div v-for="(header, index) in formData.headers" :key="index" class="flex gap-2">
|
||||
<a-input v-model:value="header.name" placeholder="Header Name" class="flex-1" />
|
||||
<a-input v-model:value="header.value" placeholder="Header Value" class="flex-1" />
|
||||
<a-button type="text" danger @click="removeHeader(index)">
|
||||
<template #icon>
|
||||
<CloseOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
<a-button type="dashed" class="w-full" @click="addHeader">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
{{ $gettext('Add Header') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="formData.method !== 'GET'" :label="$gettext('Request Body')">
|
||||
<a-textarea
|
||||
v-model:value="formData.body"
|
||||
:rows="3"
|
||||
placeholder="{"key": "value"}"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="$gettext('Expected Status Codes')">
|
||||
<a-select
|
||||
v-model:value="formData.expectedStatus"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="200, 201, 204..."
|
||||
>
|
||||
<a-select-option :value="200">
|
||||
200 OK
|
||||
</a-select-option>
|
||||
<a-select-option :value="201">
|
||||
201 Created
|
||||
</a-select-option>
|
||||
<a-select-option :value="204">
|
||||
204 No Content
|
||||
</a-select-option>
|
||||
<a-select-option :value="301">
|
||||
301 Moved Permanently
|
||||
</a-select-option>
|
||||
<a-select-option :value="302">
|
||||
302 Found
|
||||
</a-select-option>
|
||||
<a-select-option :value="304">
|
||||
304 Not Modified
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$gettext('Expected Text')">
|
||||
<a-input v-model:value="formData.expectedText" placeholder="Success" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$gettext('Not Expected Text')">
|
||||
<a-input v-model:value="formData.notExpectedText" placeholder="Error" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- gRPC/gRPCS Settings -->
|
||||
<div v-if="['grpc', 'grpcs'].includes(formData.protocol)">
|
||||
<a-alert
|
||||
v-if="['grpc', 'grpcs'].includes(formData.protocol)"
|
||||
:message="formData.protocol === 'grpcs'
|
||||
? $gettext('gRPCS uses TLS encryption. Server must implement gRPC Health Check service. For testing, SSL validation is disabled by default.')
|
||||
: $gettext('gRPC health check requires server to implement gRPC Health Check service (grpc.health.v1.Health).')"
|
||||
type="info"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
/>
|
||||
<a-alert
|
||||
:message="$gettext('Note: If the server does not support gRPC Reflection, health checks may fail. Please ensure your gRPC server has Reflection enabled.')"
|
||||
type="warning"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
/>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$gettext('Service Name')">
|
||||
<a-input v-model:value="formData.grpcService" placeholder="my.service.v1.MyService" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$gettext('Method Name')">
|
||||
<a-input v-model:value="formData.grpcMethod" placeholder="Check" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Settings -->
|
||||
<a-collapse>
|
||||
<a-collapse-panel key="advanced" :header="$gettext('Advanced Settings')">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$gettext('Check Interval (seconds)')">
|
||||
<a-input-number
|
||||
v-model:value="formData.interval"
|
||||
:min="30"
|
||||
:max="3600"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$gettext('Timeout (seconds)')">
|
||||
<a-input-number
|
||||
v-model:value="formData.timeout"
|
||||
:min="5"
|
||||
:max="60"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item :label="$gettext('User Agent')">
|
||||
<a-input v-model:value="formData.userAgent" />
|
||||
</a-form-item>
|
||||
|
||||
<div v-if="!['grpc', 'grpcs'].includes(formData.protocol)">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$gettext('Max Redirects')">
|
||||
<a-input-number
|
||||
v-model:value="formData.maxRedirects"
|
||||
:min="0"
|
||||
:max="10"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="formData.followRedirects">
|
||||
{{ $gettext('Follow Redirects') }}
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="formData.validateSSL">
|
||||
{{ $gettext('Validate SSL Certificate') }}
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="formData.verifyHostname">
|
||||
{{ $gettext('Verify Hostname') }}
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="formData.checkFavicon">
|
||||
{{ $gettext('Check Favicon') }}
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<!-- DNS & Network -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$gettext('DNS Resolver')">
|
||||
<a-input v-model:value="formData.dnsResolver" placeholder="8.8.8.8:53" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$gettext('Source IP')">
|
||||
<a-input v-model:value="formData.sourceIP" placeholder="192.168.1.100" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- Client Certificates -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$gettext('Client Certificate')">
|
||||
<a-input v-model:value="formData.clientCert" placeholder="/path/to/client.crt" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$gettext('Client Key')">
|
||||
<a-input v-model:value="formData.clientKey" placeholder="/path/to/client.key" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<a-button @click="handleCancel">
|
||||
{{ $gettext('Cancel') }}
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleSave">
|
||||
{{ $gettext('Save') }}
|
||||
</a-button>
|
||||
<a-button :loading="testing" @click="handleTest">
|
||||
{{ $gettext('Test') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.grpc-help-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.grpc-help-content h4 {
|
||||
color: #1890ff;
|
||||
margin: 16px 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.grpc-help-content h5 {
|
||||
color: #595959;
|
||||
margin: 12px 0 4px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.grpc-help-content p {
|
||||
margin: 8px 0;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.code-examples {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.code-examples pre {
|
||||
background-color: #f6f8fa;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
overflow-x: auto;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.code-examples code {
|
||||
color: #24292e;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dark .code-examples pre {
|
||||
background-color: #161b22;
|
||||
border-color: #30363d;
|
||||
}
|
||||
|
||||
.dark .code-examples code {
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
.dark .grpc-help-content h4 {
|
||||
color: #58a6ff;
|
||||
}
|
||||
|
||||
.dark .grpc-help-content h5,
|
||||
.dark .grpc-help-content p {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CloseOutlined,
|
||||
ReloadOutlined,
|
||||
SaveOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
interface Props {
|
||||
isConnected: boolean
|
||||
refreshing: boolean
|
||||
settingsMode: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'refresh'): void
|
||||
(e: 'toggleSettings'): void
|
||||
(e: 'saveOrder'): void
|
||||
(e: 'cancelSettings'): void
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="site-navigation-header">
|
||||
<h2 class="text-2xl font-500 text-gray-900 dark:text-gray-100 mb-4">
|
||||
{{ $gettext('Site Navigation') }}
|
||||
</h2>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
:class="[isConnected ? 'bg-green-500' : 'bg-red-500']"
|
||||
/>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ isConnected ? $gettext('Connected') : $gettext('Disconnected') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<a-button
|
||||
v-if="settingsMode"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="$emit('saveOrder')"
|
||||
>
|
||||
<template #icon>
|
||||
<SaveOutlined />
|
||||
</template>
|
||||
{{ $gettext('Save Order') }}
|
||||
</a-button>
|
||||
|
||||
<a-button
|
||||
v-if="settingsMode"
|
||||
size="small"
|
||||
@click="$emit('cancelSettings')"
|
||||
>
|
||||
<template #icon>
|
||||
<CloseOutlined />
|
||||
</template>
|
||||
{{ $gettext('Cancel') }}
|
||||
</a-button>
|
||||
|
||||
<a-button
|
||||
v-if="!settingsMode"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="refreshing"
|
||||
@click="$emit('refresh')"
|
||||
>
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
{{ $gettext('Refresh') }}
|
||||
</a-button>
|
||||
|
||||
<a-button
|
||||
v-if="!settingsMode"
|
||||
size="small"
|
||||
@click="$emit('toggleSettings')"
|
||||
>
|
||||
<template #icon>
|
||||
<SettingOutlined />
|
||||
</template>
|
||||
{{ $gettext('Settings') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.site-navigation-header {
|
||||
@apply flex items-center justify-between mb-6;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.site-navigation-header {
|
||||
@apply flex-col items-start gap-4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -50,6 +50,7 @@ require (
|
||||
github.com/urfave/cli/v3 v3.4.1
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/net v0.43.0
|
||||
google.golang.org/grpc v1.74.2
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gen v0.3.27
|
||||
@@ -89,7 +90,6 @@ require (
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.9 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
|
||||
github.com/alibabacloud-go/tea v1.3.10 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/aliyun/aliyun-log-go-sdk v0.1.106 // indirect
|
||||
@@ -348,11 +348,9 @@ require (
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.35.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.245.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
google.golang.org/grpc v1.74.2 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.14.4 // indirect
|
||||
|
||||
@@ -100,8 +100,6 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo
|
||||
cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=
|
||||
cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
|
||||
cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
|
||||
cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
|
||||
cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
|
||||
cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc=
|
||||
cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
@@ -614,18 +612,12 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYs
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2 h1:Hr5FTipp7SL07o2FvoVOX9HRiRH3CR3Mj8pxqCcdD5A=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=
|
||||
@@ -721,9 +713,6 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.11/go.mod h1:wHxkgZT1ClZdcwEVP/pDgYK/9HucsnCfMipmJgCz4xY=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.8 h1:AL+nH363NJFS1NXIjCdmj5MOElgKEqgFeoq7vjje350=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.8/go.mod h1:d+z3ScRqc7PFzg4h9oqE3h8yunRZvAvU7u+iuPYEhpU=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.9 h1:7P0KWfed/YMtpeuW3E2iwokzoz9L7H9rB+VZzg5DeBs=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.9/go.mod h1:kgnXaV74AVjM3ZWJu1GhyXGuCtxljJ677oUfz6MyJOE=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||
@@ -749,24 +738,18 @@ github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/Ke
|
||||
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||
github.com/alibabacloud-go/tea v1.3.9 h1:bjgt1bvdY780vz/17iWNNtbXl4A77HWntWMeaUF3So0=
|
||||
github.com/alibabacloud-go/tea v1.3.9/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
||||
github.com/alibabacloud-go/tea v1.3.10 h1:J0Ke8iMyoxX2daj90hdPr1QgfxJnhR8SOflB910o/Dk=
|
||||
github.com/alibabacloud-go/tea v1.3.10/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/aliyun/aliyun-log-go-sdk v0.1.106 h1:qhAiESgl5qmMkbGu13r72JDXTXeEoitP0YCfQsp5kLA=
|
||||
github.com/aliyun/aliyun-log-go-sdk v0.1.106/go.mod h1:7QcyHasd4WLdC+lx4uCmdIBcl7WcgRHctwz8t1zAuPo=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
||||
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/aliyun/credentials-go v1.4.6 h1:CG8rc/nxCNKfXbZWpWDzI9GjF4Tuu3Es14qT8Y0ClOk=
|
||||
github.com/aliyun/credentials-go v1.4.6/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/aliyun/credentials-go v1.4.7 h1:T17dLqEtPUFvjDRRb5giVvLh6dFT8IcNFJJb7MeyCxw=
|
||||
github.com/aliyun/credentials-go v1.4.7/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
@@ -783,72 +766,42 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.6 h1:zJqGjVbRdTPojeCGWn5IR5pbJwSQSBh5RWFTQcEQGdU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.6/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.37.2 h1:xkW1iMYawzcmYFYEV0UCMxc8gSsjCGEhBXQkdQywVbo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.37.2/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.18 h1:x4T1GRPnqKV8HMJOMtNktbpQMl3bIsfx8KbqmveUO2I=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.18/go.mod h1:bvz8oXugIsH8K7HLhBv06vDqnFv3NsGDt2Znpk7zmOU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.30.3 h1:utupeVnE3bmB221W08P0Moz1lDI3OwYa2fBtUhl7TCc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.30.3/go.mod h1:NDGwOEBdpyZwLPlQkpKIO7frf18BW8PaCmAM9iUxQmI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.71 h1:r2w4mQWnrTMJjOyIsZtGp3R3XGY3nqHn8C26C2lQWgA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.71/go.mod h1:E7VF3acIup4GB5ckzbKFrCK0vTvEQxOxgdq4U3vcMCY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.3 h1:ptfyXmv+ooxzFwyuBth0yqABcjVIkjDL0iTYZBSbum8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.3/go.mod h1:Q43Nci++Wohb0qUh4m54sNln0dbxJw8PvQWkrwOkGOI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 h1:D9ixiWSG4lyUBL2DDNK924Px9V/NBVpML90MHqyTADY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33/go.mod h1:caS/m4DI+cij2paz3rtProRBI4s/+TCiWoaWZuQ9010=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.2 h1:nRniHAvjFJGUCl04F3WaAj7qp/rcz5Gi1OVoj5ErBkc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.2/go.mod h1:eJDFKAMHHUvv4a0Zfa7bQb//wFNUXGrbFpYRCHe2kD0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 h1:osMWfm/sC/L4tvEdQ65Gri5ZZDCUpuYJZbTTDrsn4I0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37/go.mod h1:ZV2/1fbjOPr4G4v38G3Ww5TBT4+hmsK45s/rxu1fGy0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 h1:sPiRHLVUIIQcoVZTNwqQcdtjkqkPopyYmIX0M5ElRf4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2/go.mod h1:ik86P3sgV+Bk7c1tBFCwI3VxMoSEwl4YkRB9xn1s340=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 h1:v+X21AvTb2wZ+ycg1gx+orkB/9U6L7AOp93R7qYxsxM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37/go.mod h1:G0uM1kyssELxmJ2VZEfG0q2npObR3BAkF3c1VsfVnfs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 h1:ZdzDAg075H6stMZtbD2o+PyB933M/f20e9WmCBC17wA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2/go.mod h1:eE1IIzXG9sdZCB0pNNpMpsYTLl4YdOQD3njiVN1e/E4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 h1:vvbXsA2TVO80/KT7ZqCbx934dt6PY+vQ8hZpUZ/cpYg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18/go.mod h1:m2JJHledjBGNMsLOF1g9gbAxprzq3KjC8e4lxtn+eWg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.2 h1:oxmDEO14NBZJbK/M8y3brhMFEIGN4j8a6Aq8eY0sqlo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.2/go.mod h1:4hH+8QCrk1uRWDPsVfsNDUup3taAjO8Dnx63au7smAU=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.5 h1:DYQbfSAWcMwRM0LbCDyQkPB1AcaZcLzLoaFrYcpyMag=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.5/go.mod h1:Lav4KLgncVjjrwLWutOccjEgJ4T/RAdY+Ic0hmNIgI0=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.46.0 h1:nQX2q3dUdqwyxNPEjAw5WgH0F0HuHlVS8iq7TW3xHi8=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.46.0/go.mod h1:c0o7fqQS36cwXMizMSqpG4job2HsU1b8Wb2QoYSWyu0=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1 h1:R3nSX1hguRy6MnknHiepSvqnnL8ansFwK2hidPesAYU=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1/go.mod h1:fmSiB4OAghn85lQgk7XN9l9bpFg5Bm1v3HuaXKytPEw=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.55.0 h1:uWgREKbrY/+EYuU9u4llSkbsIKLSEPriOSHmLCK3GAY=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.55.0/go.mod h1:6G0V3ndXAxeBFSDbUEZ3VTZgmL/9yoIuWM3s3AAV97E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 h1:rGtWqkQbPk7Bkwuv3NzpE/scwwL9sC1Ul3tn9x83DUI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6/go.mod h1:u4ku9OLv4TO4bCPdxf4fA1upaMaJmP9ZijGk3AAOC6Q=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.27.0 h1:j7/jTOjWeJDolPwZ/J4yZ7dUsxsWZEsxNwH5O7F8eEA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.27.0/go.mod h1:M0xdEPQtgpNT7kdAX4/vOAPkFj60hSQRb7TvW9B0iug=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 h1:OV/pxyXh+eMA0TExHEC4jyWdumLxNbzz1P0zJoezkJc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4/go.mod h1:8Mm5VGYwtm+r305FfPSuc+aFkrypeylGYhFim6XEPoc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0 h1:ywQF2N4VjqX+Psw+jLjMmUL2g1RDHlvri3NxHA08MGI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0/go.mod h1:Z+qv5Q6b7sWiclvbJyPSOT1BRVU9wfSUPaqQzZ1Xg3E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 h1:aUrLQwJfZtwv3/ZNG2xRtEen+NqI3iesuacjP51Mv1s=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1/go.mod h1:3wFBZKoWnX3r+Sm7in79i54fBmNfwhdNdQuscCw7QIk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 h1:bRP/a9llXSSgDPk7Rqn5GD/DQCGo6uk95plBFKoXt2M=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.36.0/go.mod h1:tgBsFzxwl65BWkuJ/x2EUs59bD4SfYKgikvFDJi1S58=
|
||||
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
|
||||
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
|
||||
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aziontech/azionapi-go-sdk v0.142.0 h1:1NOHXlC0/7VgbfbTIGVpsYn1THCugM4/SCOXVdUHQ+A=
|
||||
github.com/aziontech/azionapi-go-sdk v0.142.0/go.mod h1:cA5DY/VP4X5Eu11LpQNzNn83ziKjja7QVMIl4J45feA=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/baidubce/bce-sdk-go v0.9.235 h1:iAi+seH9w1Go2szFNzyGumahLGDsuYZ3i8hduX3qiM8=
|
||||
github.com/baidubce/bce-sdk-go v0.9.235/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
|
||||
github.com/baidubce/bce-sdk-go v0.9.237 h1:Y1M6GXubX65LtCKnkrM+8f68Gsl8aVynTGCE1COyu28=
|
||||
github.com/baidubce/bce-sdk-go v0.9.237/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
|
||||
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
|
||||
@@ -862,19 +815,13 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
|
||||
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM=
|
||||
github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/blevesearch/bleve/v2 v2.5.2 h1:Ab0r0MODV2C5A6BEL87GqLBySqp/s9xFgceCju6BQk8=
|
||||
github.com/blevesearch/bleve/v2 v2.5.2/go.mod h1:5Dj6dUQxZM6aqYT3eutTD/GpWKGFSsV8f7LDidFbwXo=
|
||||
github.com/blevesearch/bleve/v2 v2.5.3 h1:9l1xtKaETv64SZc1jc4Sy0N804laSa/LeMbYddq1YEM=
|
||||
github.com/blevesearch/bleve/v2 v2.5.3/go.mod h1:Z/e8aWjiq8HeX+nW8qROSxiE0830yQA071dwR3yoMzw=
|
||||
github.com/blevesearch/bleve_index_api v1.2.8 h1:Y98Pu5/MdlkRyLM0qDHostYo7i+Vv1cDNhqTeR4Sy6Y=
|
||||
github.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
|
||||
github.com/blevesearch/geo v0.2.3 h1:K9/vbGI9ehlXdxjxDRJtoAMt7zGAsMIzc6n8zWcwnhg=
|
||||
github.com/blevesearch/geo v0.2.3/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
|
||||
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
|
||||
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
|
||||
github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U=
|
||||
@@ -912,8 +859,6 @@ github.com/blinkbean/dingtalk v1.1.3/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHa
|
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
|
||||
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
@@ -924,11 +869,8 @@ github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw=
|
||||
github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
|
||||
@@ -936,10 +878,6 @@ github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdf
|
||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
|
||||
github.com/casdoor/casdoor-go-sdk v1.9.0 h1:gJQD+ZpgcwUQefzQUsOf6t/nyubUNjfNXc3GicMNoe4=
|
||||
github.com/casdoor/casdoor-go-sdk v1.9.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
|
||||
github.com/casdoor/casdoor-go-sdk v1.12.0 h1:EtonFIxyI8ttw78hBwwGvvLcxNHLYgltbEU8oM1SzKM=
|
||||
github.com/casdoor/casdoor-go-sdk v1.12.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
|
||||
github.com/casdoor/casdoor-go-sdk v1.14.0 h1:HrvwBxF0Vt+BSuNsf5MfhCvqSfETolpv4hzvP5XcCXc=
|
||||
github.com/casdoor/casdoor-go-sdk v1.14.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
@@ -953,7 +891,6 @@ github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F9
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -965,17 +902,12 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
|
||||
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
|
||||
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
@@ -1030,8 +962,6 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dnsimple/dnsimple-go/v4 v4.0.0 h1:nUCICZSyZDiiqimAAL+E8XL+0sKGks5VRki5S8XotRo=
|
||||
github.com/dnsimple/dnsimple-go/v4 v4.0.0/go.mod h1:AXT2yfAFOntJx6iMeo1J/zKBw0ggXFYBt4e97dqqPnc=
|
||||
github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA=
|
||||
github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
@@ -1046,8 +976,6 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
|
||||
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/ebitengine/purego v0.9.0-alpha.9 h1:+OPDXjPESTGhQ/2zO0aQeUR8r4o1feLMSDQzkA6z9ug=
|
||||
github.com/ebitengine/purego v0.9.0-alpha.9/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.9.0-alpha.10 h1:audsHbrB2mnadP/fVRdQRDc0lymjY7oWTNfzT59XICo=
|
||||
github.com/ebitengine/purego v0.9.0-alpha.10/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
@@ -1068,8 +996,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
|
||||
github.com/exoscale/egoscale/v3 v3.1.24 h1:EUWmjw/JgMj1faX5ojosjrJE5eY0QEWP0KBmLyFU6aE=
|
||||
github.com/exoscale/egoscale/v3 v3.1.24/go.mod h1:A53enXfm8nhVMpIYw0QxiwQ2P6AdCF4F/nVYChNEzdE=
|
||||
github.com/exoscale/egoscale/v3 v3.1.25 h1:Xy4LdmElaUXdf72vCt8gv9DCivKUlmW5Ar5ATInwWU8=
|
||||
github.com/exoscale/egoscale/v3 v3.1.25/go.mod h1:TJCI0OG3Lz2rnleRB0xwiOFg82uNCCytRqw7TxPoIvc=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
@@ -1114,19 +1040,13 @@ github.com/gin-contrib/static v1.1.5 h1:bAPqT4KTZN+4uDY1b90eSrD1t8iNzod7Jj8njwmn
|
||||
github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8Zetl9fd8wM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-acme/alidns-20150109/v4 v4.5.10 h1:epLD0VaHR5XUpiM6mjm4MzQFICrk+zpuqDz2aO1/R/k=
|
||||
github.com/go-acme/alidns-20150109/v4 v4.5.10/go.mod h1:qGRq8kD0xVgn82qRSQmhHwh/oWxKRjF4Db5OI4ScV5g=
|
||||
github.com/go-acme/alidns-20150109/v4 v4.5.11 h1:CtOvASZao+WY9PImlpmWKqn2Dj+O3zzQX55KbqO/QAY=
|
||||
github.com/go-acme/alidns-20150109/v4 v4.5.11/go.mod h1:ZCuTWP0+J6sGCQpMNWhOUVK5vLvNsAF+oT2EmMrJA8U=
|
||||
github.com/go-acme/lego/v4 v4.25.1 h1:AYPUM7quPN/g2PcjjWw8sAMz3eV+Z8UWkr1kitDOyVA=
|
||||
github.com/go-acme/lego/v4 v4.25.1/go.mod h1:OORYyVNZPaNdIdVYCGSBNRNZDIjhQbPuFxwGDgWj/yM=
|
||||
github.com/go-acme/lego/v4 v4.25.2 h1:+D1Q+VnZrD+WJdlkgUEGHFFTcDrwGlE7q24IFtMmHDI=
|
||||
github.com/go-acme/lego/v4 v4.25.2/go.mod h1:OORYyVNZPaNdIdVYCGSBNRNZDIjhQbPuFxwGDgWj/yM=
|
||||
github.com/go-acme/tencentclouddnspod v1.0.1208 h1:xAVy1lmg2KcKKeYmFSBQUttwc1o1S++9QTjAotGC+BM=
|
||||
github.com/go-acme/tencentclouddnspod v1.0.1208/go.mod h1:yxG02mkbbVd7lTb97nOn7oj09djhm7hAwxNQw4B9dpQ=
|
||||
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
|
||||
github.com/go-co-op/gocron/v2 v2.16.2 h1:r08P663ikXiulLT9XaabkLypL/W9MoCIbqgQoAutyX4=
|
||||
github.com/go-co-op/gocron/v2 v2.16.2/go.mod h1:4YTLGCCAH75A5RlQ6q+h+VacO7CgjkgP0EJ+BEOXRSI=
|
||||
github.com/go-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
|
||||
github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
@@ -1144,8 +1064,6 @@ github.com/go-gormigrate/gormigrate/v2 v2.1.4 h1:KOPEt27qy1cNzHfMZbp9YTmEuzkY4F4
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.4/go.mod h1:y/6gPAH6QGAgP1UfHMiXcqGeJ88/GRQbfCReE1JJD5Y=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@@ -1228,8 +1146,6 @@ github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
@@ -1445,8 +1361,6 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
|
||||
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.159 h1:6LZysc4iyO4cHB1aJsRklWfSEJr8CEhW7BmcM0SkYcU=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.159/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.161 h1:1OLCB4r14cD3slEkoT4rjYM+rnQnq6v5We4jZ5YWEXw=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.161/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
|
||||
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
|
||||
@@ -1544,7 +1458,6 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@@ -1552,7 +1465,6 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||
@@ -1581,7 +1493,6 @@ github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -1615,8 +1526,6 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/linode/linodego v1.53.0 h1:UWr7bUUVMtcfsuapC+6blm6+jJLPd7Tf9MZUpdOERnI=
|
||||
github.com/linode/linodego v1.53.0/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA=
|
||||
github.com/linode/linodego v1.54.0 h1:29vTV5YjqjjwPxWLE8Qp1zgDDXM5ifAQ2T6azAYsj/w=
|
||||
github.com/linode/linodego v1.54.0/go.mod h1:VHlFAbhj18634Cd7B7L5D723kFKFQMOxzIutSMcWsB4=
|
||||
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
|
||||
@@ -1636,8 +1545,6 @@ github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mark3labs/mcp-go v0.34.0 h1:eWy7WBGvhk6EyAAyVzivTCprE52iXJwNtvHV6Cv3bR0=
|
||||
github.com/mark3labs/mcp-go v0.34.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
|
||||
github.com/mark3labs/mcp-go v0.37.0 h1:BywvZLPRT6Zx6mMG/MJfxLSZQkTGIcJSEGKsvr4DsoQ=
|
||||
github.com/mark3labs/mcp-go v0.37.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
@@ -1666,8 +1573,6 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
|
||||
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY=
|
||||
github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
|
||||
@@ -1681,16 +1586,12 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
|
||||
github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34=
|
||||
github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M=
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
|
||||
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
|
||||
github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
|
||||
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
|
||||
github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
@@ -1779,12 +1680,8 @@ github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
|
||||
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
|
||||
github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw=
|
||||
github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.95.2 h1:a7QUZD5c+NkrFrdkdyJUO9cOUo8VQJyRkcIzk9Wh+DI=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.95.2/go.mod h1:O6osg9dPzXq7H2ib/1qzimzG5oXSJFgccR7iawg7SwA=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.97.1 h1:sThWygFwYB5gCgM2i9Cnp703igdpQs+cN1P7mH+uIYM=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.97.1/go.mod h1:ILhKsVZzfmkaqe0ugNHmEvbYB0VnGHTGkrIth5ssOWk=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.95.2 h1:yflYnbQu4ciWH/GEztqlAccLPw4k5mp11uhW++al5ow=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.95.2/go.mod h1:atPDu37gu8HT7TtPpovrkgNmDAgOGM6TVEJ7ANTblMs=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.97.1 h1:sRKKsf4qZTvIPK9Dx7cbkVMIUcvsLOOePoSZcnrTKGc=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.97.1/go.mod h1:NzMIBls+KkMpcVoyjU/gzSkWw4HML2BQb3bEHOQBFLc=
|
||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||
@@ -1844,7 +1741,6 @@ github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2
|
||||
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
@@ -1880,8 +1776,6 @@ github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
@@ -1922,8 +1816,6 @@ github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQB
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/redis/go-redis/v9 v9.12.0 h1:XlVPGlflh4nxfhsNXPA8Qp6EmEfTo0rp8oaBzPipXnU=
|
||||
github.com/redis/go-redis/v9 v9.12.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/regfish/regfish-dnsapi-go v0.1.1 h1:TJFtbePHkd47q5GZwYl1h3DIYXmoxdLjW/SBsPtB5IE=
|
||||
@@ -1958,16 +1850,10 @@ github.com/sacloud/iaas-api-go v1.16.1/go.mod h1:QVPHLwYzpECMsuml55I3FWAggsb4XSu
|
||||
github.com/sacloud/packages-go v0.0.11 h1:hrRWLmfPM9w7GBs6xb5/ue6pEMl8t1UuDKyR/KfteHo=
|
||||
github.com/sacloud/packages-go v0.0.11/go.mod h1:XNF5MCTWcHo9NiqWnYctVbASSSZR3ZOmmQORIzcurJ8=
|
||||
github.com/sagikazarmark/crypt v0.10.0/go.mod h1:gwTNHQVoOS3xp9Xvz5LLR+1AauC5M6880z5NWzdhOyQ=
|
||||
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||
github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc=
|
||||
github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw=
|
||||
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
|
||||
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/sashabaranov/go-openai v1.40.5 h1:SwIlNdWflzR1Rxd1gv3pUg6pwPc6cQ2uMoHs8ai+/NY=
|
||||
github.com/sashabaranov/go-openai v1.40.5/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/sashabaranov/go-openai v1.41.0 h1:tPR4Ro4kl4GhY8mroonGQLkSeI8LGzL6atbKLPQkK14=
|
||||
github.com/sashabaranov/go-openai v1.41.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/sashabaranov/go-openai v1.41.1 h1:zf5tM+GuxpyiyD9XZg8nCqu52eYFQg9OOew0gnIuDy4=
|
||||
github.com/sashabaranov/go-openai v1.41.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
@@ -1978,8 +1864,6 @@ github.com/selectel/domains-go v1.1.0 h1:futG50J43ALLKQAnZk9H9yOtLGnSUh7c5hSvuC5
|
||||
github.com/selectel/domains-go v1.1.0/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA=
|
||||
github.com/selectel/go-selvpcclient/v4 v4.1.0 h1:22lBp+rzg9g2MP4iiGhpVAcCt0kMv7I7uV1W3taLSvQ=
|
||||
github.com/selectel/go-selvpcclient/v4 v4.1.0/go.mod h1:eFhL1KUW159KOJVeGO7k/Uxl0TYd/sBkWXjuF5WxmYk=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
@@ -2013,8 +1897,6 @@ github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/sony/sonyflake/v2 v2.2.0 h1:wSzEoewlWnUtc3SZX/MpT8zsWTuAnjwrprUYfuPl9Jg=
|
||||
github.com/sony/sonyflake/v2 v2.2.0/go.mod h1:09EcfmR846JLupbkgVfzp8QtQwJ+Y8e69VVayHdawzg=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
@@ -2037,8 +1919,6 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
@@ -2077,8 +1957,6 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1208/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1210 h1:waSk2KyI2VvXtR+XQJm0v1lWfgbJg51iSWJh4hWnyeo=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1210/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.6 h1:kSt8iJikKhDt2QZOt9BivlR+x3ISQRElm07hX+GOoI8=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.6/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/timtadh/data-structures v0.5.3/go.mod h1:9R4XODhJ8JdWFEI8P/HJKqxuJctfBQw6fDibMQny2oU=
|
||||
@@ -2108,14 +1986,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+UzD/hf3P4Gd1j0JI9ncbxv+nsypPoUYI=
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss=
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 h1:/VaznPrb/b68e3iMvkr27fU7JqPKU4j7tIITZnjQX1k=
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419/go.mod h1:QN0/PdenvYWB0GRMz6JJbPeZz2Lph2iys1p8AFVHm2c=
|
||||
github.com/uozi-tech/cosy v1.24.10 h1:rACflQ8RboZ0QX+riYRq8RaF5mhukt1HxVT9JmGkKOQ=
|
||||
github.com/uozi-tech/cosy v1.24.10/go.mod h1:h0ViTCx65zdRTW0nL+t96WKUi8cW5ThbE+ciKzsWjsY=
|
||||
github.com/uozi-tech/cosy v1.25.1 h1:ftXZYBLdBHDOGRX6ZIa7tAi6xvEr6sLe6vWa/FCx1fk=
|
||||
github.com/uozi-tech/cosy v1.25.1/go.mod h1:o0gM8j/bjRM3LApTp+UTVR1PRxcJ1EhQkoJ9nSGvRpA=
|
||||
github.com/uozi-tech/cosy v1.25.3 h1:sC3ctnZva7ziuFDiO9YM+5tTvEBt7grLKRaLNbhP55o=
|
||||
github.com/uozi-tech/cosy v1.25.3/go.mod h1:o0gM8j/bjRM3LApTp+UTVR1PRxcJ1EhQkoJ9nSGvRpA=
|
||||
github.com/uozi-tech/cosy-driver-mysql v0.2.2 h1:22S/XNIvuaKGqxQPsYPXN8TZ8hHjCQdcJKVQ83Vzxoo=
|
||||
@@ -2125,10 +1997,6 @@ github.com/uozi-tech/cosy-driver-postgres v0.2.1/go.mod h1:eAy1A89yHbAEfjkhNAifa
|
||||
github.com/uozi-tech/cosy-driver-sqlite v0.2.1 h1:W+Z4pY25PSJCeReqroG7LIBeffsqotbpHzgqSMqZDIM=
|
||||
github.com/uozi-tech/cosy-driver-sqlite v0.2.1/go.mod h1:2ya7Z5P3HzFi1ktfL8gvwaAGx0DDV0bmWxNSNpaLlwo=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E=
|
||||
github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
|
||||
github.com/urfave/cli/v3 v3.3.9 h1:54roEDJcTWuucl6MSQ3B+pQqt1ePh/xOQokhEYl5Gfs=
|
||||
github.com/urfave/cli/v3 v3.3.9/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
|
||||
github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
|
||||
github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
|
||||
@@ -2150,16 +2018,10 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yandex-cloud/go-genproto v0.14.0 h1:yDqD260mICkjodXyAaDhESfrLr6gIGwwRc9MYE0jvW0=
|
||||
github.com/yandex-cloud/go-genproto v0.14.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
|
||||
github.com/yandex-cloud/go-genproto v0.15.0 h1:1E9ITJGki4g1F/6TkaLxyOXtTxjv772sQ7ifsEfFrxs=
|
||||
github.com/yandex-cloud/go-genproto v0.15.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
|
||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.3 h1:erphTBXKSpm/tETa6FXrw4niSHjhySzAKHUc2/BZKx0=
|
||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.3/go.mod h1:lbBaFJVouETfVnd3YzNF5vW6vgYR2FVfGLUzLexyGlI=
|
||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.5 h1:yrUPX9G97WB4jTeuCNzwWT1NwUo2CiXZWH5FSbjJztw=
|
||||
github.com/yandex-cloud/go-sdk/services/dns v0.0.5/go.mod h1:UWqmruzRLUXgKJkHXilIuKB6I92d6xM3yPAx4rdz+x8=
|
||||
github.com/yandex-cloud/go-sdk/v2 v2.0.8 h1:wQNIzEZYnClSQyo2fjEgnGEErWjJNBpSAinaKcP+VSg=
|
||||
github.com/yandex-cloud/go-sdk/v2 v2.0.8/go.mod h1:9Gqpq7d0EUAS+H2OunILtMi3hmMPav+fYoy9rmydM4s=
|
||||
github.com/yandex-cloud/go-sdk/v2 v2.2.0 h1:AJrGhvISAeVgqJdbWfrZSCv7UeT6eg6/LLDkc0X+Urc=
|
||||
github.com/yandex-cloud/go-sdk/v2 v2.2.0/go.mod h1:MLYmaUcfc0FmergrmXsv0USCIkjzITVsAUoP4acEJYI=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
@@ -2307,8 +2169,6 @@ golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZP
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -2460,8 +2320,6 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -2644,8 +2502,6 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
@@ -2669,8 +2525,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -2692,8 +2548,6 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -2795,8 +2649,6 @@ golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
|
||||
@@ -2863,8 +2715,6 @@ google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60c
|
||||
google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
|
||||
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
|
||||
google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
|
||||
google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg=
|
||||
google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
||||
google.golang.org/api v0.245.0 h1:YliGvz1rjXB+sTLNIST6Ffeji9WlRdLQ+LPl9ruSa5Y=
|
||||
google.golang.org/api v0.245.0/go.mod h1:dMVhVcylamkirHdzEBAIQWUCgqY885ivNeZYd7VAVr8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
@@ -3009,12 +2859,8 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -3059,8 +2905,6 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
|
||||
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
@@ -3142,14 +2986,10 @@ gorm.io/gen v0.3.27 h1:ziocAFLpE7e0g4Rum69pGfB9S6DweTxK8gAun7cU8as=
|
||||
gorm.io/gen v0.3.27/go.mod h1:9zquz2xD1f3Eb/eHq4oLn2z6vDVvQlCY5S3uMBLv4EA=
|
||||
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
|
||||
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gorm.io/hints v1.1.2 h1:b5j0kwk5p4+3BtDtYqqfY+ATSxjj+6ptPgVveuynn9o=
|
||||
gorm.io/hints v1.1.2/go.mod h1:/ARdpUHAtyEMCh5NNi3tI7FsGh+Cj/MIUlvNxCNCFWg=
|
||||
gorm.io/plugin/dbresolver v1.6.0 h1:XvKDeOtTn1EIX6s4SrKpEH82q0gXVemhYjbYZFGFVcw=
|
||||
gorm.io/plugin/dbresolver v1.6.0/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
|
||||
gorm.io/plugin/dbresolver v1.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc=
|
||||
gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
@@ -3205,7 +3045,6 @@ modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw
|
||||
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
|
||||
@@ -27,13 +27,13 @@ type NodeInfo struct {
|
||||
}
|
||||
|
||||
type NodeStat struct {
|
||||
AvgLoad *load.AvgStat `json:"avg_load"`
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
MemoryPercent float64 `json:"memory_percent"`
|
||||
DiskPercent float64 `json:"disk_percent"`
|
||||
Network net.IOCountersStat `json:"network"`
|
||||
Status bool `json:"status"`
|
||||
ResponseAt time.Time `json:"response_at"`
|
||||
AvgLoad *load.AvgStat `json:"avg_load"`
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
MemoryPercent float64 `json:"memory_percent"`
|
||||
DiskPercent float64 `json:"disk_percent"`
|
||||
Network net.IOCountersStat `json:"network"`
|
||||
Status bool `json:"status"`
|
||||
ResponseAt time.Time `json:"response_at"`
|
||||
UpstreamStatusMap map[string]*upstream.Status `json:"upstream_status_map"`
|
||||
}
|
||||
|
||||
|
||||
@@ -46,12 +46,12 @@ func GetNodeStat() (data NodeStat) {
|
||||
|
||||
// Get upstream status for current node
|
||||
upstreamService := upstream.GetUpstreamService()
|
||||
|
||||
|
||||
// Ensure upstream availability test is performed if targets exist
|
||||
if upstreamService.GetTargetCount() > 0 {
|
||||
upstreamService.PerformAvailabilityTest()
|
||||
}
|
||||
|
||||
|
||||
upstreamStatusMap := upstreamService.GetAvailabilityMap()
|
||||
|
||||
return NodeStat{
|
||||
|
||||
@@ -80,7 +80,7 @@ func SyncToRemoteServer(c *model.Cert) (err error) {
|
||||
type SyncNotificationPayload struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
CertName string `json:"cert_name"`
|
||||
NodeName string `json:"node_name"`
|
||||
NodeName string `json:"node_name"`
|
||||
Response string `json:"response"`
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ func deploy(node *model.Node, c *model.Cert, payloadBytes []byte) (err error) {
|
||||
notificationPayload := &SyncNotificationPayload{
|
||||
StatusCode: resp.StatusCode,
|
||||
CertName: c.Name,
|
||||
NodeName: node.Name,
|
||||
NodeName: node.Name,
|
||||
Response: string(respBody),
|
||||
}
|
||||
|
||||
|
||||
+13
-13
@@ -19,18 +19,18 @@ const (
|
||||
type ProxyTarget = upstream.ProxyTarget
|
||||
|
||||
type Config struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
FilePath string `json:"filepath,omitempty"`
|
||||
ModifiedAt time.Time `json:"modified_at"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
NamespaceID uint64 `json:"namespace_id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
FilePath string `json:"filepath,omitempty"`
|
||||
ModifiedAt time.Time `json:"modified_at"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
NamespaceID uint64 `json:"namespace_id"`
|
||||
Namespace *model.Namespace `json:"namespace,omitempty"`
|
||||
Status ConfigStatus `json:"status"`
|
||||
Dir string `json:"dir"`
|
||||
Urls []string `json:"urls,omitempty"`
|
||||
ProxyTargets []ProxyTarget `json:"proxy_targets,omitempty"`
|
||||
SyncNodeIds []uint64 `json:"sync_node_ids,omitempty"`
|
||||
SyncOverwrite bool `json:"sync_overwrite"`
|
||||
Status ConfigStatus `json:"status"`
|
||||
Dir string `json:"dir"`
|
||||
Urls []string `json:"urls,omitempty"`
|
||||
ProxyTargets []ProxyTarget `json:"proxy_targets,omitempty"`
|
||||
SyncNodeIds []uint64 `json:"sync_node_ids,omitempty"`
|
||||
SyncOverwrite bool `json:"sync_overwrite"`
|
||||
}
|
||||
|
||||
@@ -302,11 +302,11 @@ func FuzzyFilterMatcher(fileName string, status ConfigStatus, namespaceID uint64
|
||||
// DefaultConfigBuilder provides basic config building logic
|
||||
func DefaultConfigBuilder(fileName string, fileInfo os.FileInfo, status ConfigStatus, namespaceID uint64, namespace *model.Namespace) Config {
|
||||
return Config{
|
||||
Name: fileName,
|
||||
ModifiedAt: fileInfo.ModTime(),
|
||||
Size: fileInfo.Size(),
|
||||
IsDir: fileInfo.IsDir(),
|
||||
Status: status,
|
||||
Name: fileName,
|
||||
ModifiedAt: fileInfo.ModTime(),
|
||||
Size: fileInfo.Size(),
|
||||
IsDir: fileInfo.IsDir(),
|
||||
Status: status,
|
||||
NamespaceID: namespaceID,
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func SyncRenameOnRemoteServer(origPath, newPath string, syncNodeIds []uint64) (e
|
||||
type SyncNotificationPayload struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
ConfigName string `json:"config_name"`
|
||||
NodeName string `json:"node_name"`
|
||||
NodeName string `json:"node_name"`
|
||||
Response string `json:"response"`
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ func (p *SyncConfigPayload) deploy(node *model.Node, c *model.Config, payloadByt
|
||||
notificationPayload := &SyncNotificationPayload{
|
||||
StatusCode: resp.StatusCode,
|
||||
ConfigName: c.Name,
|
||||
NodeName: node.Name,
|
||||
NodeName: node.Name,
|
||||
Response: string(respBody),
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ type SyncRenameNotificationPayload struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
OrigPath string `json:"orig_path"`
|
||||
NewPath string `json:"new_path"`
|
||||
NodeName string `json:"node_name"`
|
||||
NodeName string `json:"node_name"`
|
||||
Response string `json:"response"`
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ func (p *RenameConfigPayload) rename(node *model.Node) (err error) {
|
||||
StatusCode: resp.StatusCode,
|
||||
OrigPath: p.Filepath,
|
||||
NewPath: p.NewFilepath,
|
||||
NodeName: node.Name,
|
||||
NodeName: node.Name,
|
||||
Response: string(respBody),
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ type DeleteConfigPayload struct {
|
||||
type SyncDeleteNotificationPayload struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Path string `json:"path"`
|
||||
NodeName string `json:"node_name"`
|
||||
NodeName string `json:"node_name"`
|
||||
Response string `json:"response"`
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ func (p *DeleteConfigPayload) delete(node *model.Node) (err error) {
|
||||
notificationPayload := &SyncDeleteNotificationPayload{
|
||||
StatusCode: resp.StatusCode,
|
||||
Path: p.Filepath,
|
||||
NodeName: node.Name,
|
||||
NodeName: node.Name,
|
||||
Response: string(respBody),
|
||||
}
|
||||
|
||||
|
||||
+20
-20
@@ -1,31 +1,31 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func CopyFile(src, dst string) (int64, error) {
|
||||
sourceFileStat, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sourceFileStat, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !sourceFileStat.Mode().IsRegular() {
|
||||
return 0, nil
|
||||
}
|
||||
if !sourceFileStat.Mode().IsRegular() {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
source, err := os.Open(src)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer source.Close()
|
||||
source, err := os.Open(src)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
destination, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer destination.Close()
|
||||
destination, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer destination.Close()
|
||||
|
||||
return io.Copy(destination, source)
|
||||
return io.Copy(destination, source)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func InNginxUIOfficialDocker() bool {
|
||||
return cast.ToBool(os.Getenv("NGINX_UI_OFFICIAL_DOCKER")) &&
|
||||
!cast.ToBool(os.Getenv("NGINX_UI_IGNORE_DOCKER_SOCKET"))
|
||||
!cast.ToBool(os.Getenv("NGINX_UI_IGNORE_DOCKER_SOCKET"))
|
||||
}
|
||||
|
||||
func DockerSocketExists() bool {
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/0xJacky/Nginx-UI/internal/mcp"
|
||||
"github.com/0xJacky/Nginx-UI/internal/passkey"
|
||||
"github.com/0xJacky/Nginx-UI/internal/self_check"
|
||||
"github.com/0xJacky/Nginx-UI/internal/sitecheck"
|
||||
"github.com/0xJacky/Nginx-UI/internal/validation"
|
||||
"github.com/0xJacky/Nginx-UI/model"
|
||||
"github.com/0xJacky/Nginx-UI/query"
|
||||
@@ -85,6 +86,10 @@ func InitAfterDatabase(ctx context.Context) {
|
||||
analytic.RetrieveNodesStatus,
|
||||
passkey.Init,
|
||||
mcp.Init,
|
||||
func(ctx context.Context) {
|
||||
service := sitecheck.GetService()
|
||||
service.Start()
|
||||
},
|
||||
}
|
||||
|
||||
for _, v := range asyncs {
|
||||
|
||||
@@ -121,4 +121,4 @@ var RenameEnvironmentsToNodes = &gormigrate.Migration{
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2857,6 +2857,13 @@
|
||||
"https://nginx.org/en/docs/stream/ngx_stream_ssl_module.html#ssl_certificate_cache"
|
||||
]
|
||||
},
|
||||
"ssl_certificate_compression": {
|
||||
"links": [
|
||||
"https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate_compression",
|
||||
"https://nginx.org/en/docs/mail/ngx_mail_ssl_module.html#ssl_certificate_compression",
|
||||
"https://nginx.org/en/docs/stream/ngx_stream_ssl_module.html#ssl_certificate_compression"
|
||||
]
|
||||
},
|
||||
"ssl_certificate_key": {
|
||||
"links": [
|
||||
"https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate_key",
|
||||
|
||||
@@ -38,16 +38,16 @@ func init() {
|
||||
message := wecomMessage{
|
||||
MsgType: "text",
|
||||
}
|
||||
|
||||
|
||||
title := msg.GetTitle(n.Language)
|
||||
content := msg.GetContent(n.Language)
|
||||
|
||||
|
||||
// Combine title and content
|
||||
fullMessage := title
|
||||
if content != "" {
|
||||
fullMessage = fmt.Sprintf("%s\n\n%s", title, content)
|
||||
}
|
||||
|
||||
|
||||
message.Text.Content = fullMessage
|
||||
|
||||
// Marshal to JSON
|
||||
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
|
||||
// ListOptions represents the options for listing sites
|
||||
type ListOptions struct {
|
||||
Search string
|
||||
Name string
|
||||
Status string
|
||||
OrderBy string
|
||||
Sort string
|
||||
Search string
|
||||
Name string
|
||||
Status string
|
||||
OrderBy string
|
||||
Sort string
|
||||
NamespaceID uint64
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func GetSiteConfigs(ctx context.Context, options *ListOptions, sites []*model.Si
|
||||
Status: options.Status,
|
||||
OrderBy: options.OrderBy,
|
||||
Sort: options.Sort,
|
||||
NamespaceID: options.NamespaceID,
|
||||
NamespaceID: options.NamespaceID,
|
||||
IncludeDirs: false, // Filter out directories for site configurations
|
||||
}
|
||||
|
||||
@@ -81,8 +81,8 @@ func buildConfig(fileName string, fileInfo os.FileInfo, status config.ConfigStat
|
||||
Size: fileInfo.Size(),
|
||||
IsDir: fileInfo.IsDir(),
|
||||
Status: status,
|
||||
NamespaceID: namespaceID,
|
||||
Namespace: namespace,
|
||||
NamespaceID: namespaceID,
|
||||
Namespace: namespace,
|
||||
Urls: indexedSite.Urls,
|
||||
ProxyTargets: proxyTargets,
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func Save(name string, content string, overwrite bool, namespaceId uint64, syncN
|
||||
_, err = s.Where(s.Path.Eq(path)).
|
||||
Select(s.NamespaceID, s.SyncNodeIDs).
|
||||
Updates(&model.Site{
|
||||
NamespaceID: namespaceId,
|
||||
NamespaceID: namespaceId,
|
||||
SyncNodeIDs: syncNodeIds,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,651 @@
|
||||
package sitecheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/internal/site"
|
||||
"github.com/0xJacky/Nginx-UI/model"
|
||||
"github.com/0xJacky/Nginx-UI/query"
|
||||
"github.com/uozi-tech/cosy/logger"
|
||||
)
|
||||
|
||||
type SiteChecker struct {
|
||||
sites map[string]*SiteInfo
|
||||
mu sync.RWMutex
|
||||
options CheckOptions
|
||||
client *http.Client
|
||||
onUpdateCallback func([]*SiteInfo) // Callback for notifying updates
|
||||
}
|
||||
|
||||
// NewSiteChecker creates a new site checker
|
||||
func NewSiteChecker(options CheckOptions) *SiteChecker {
|
||||
transport := &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true, // Skip SSL verification for internal sites
|
||||
},
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: options.Timeout,
|
||||
}
|
||||
|
||||
if !options.FollowRedirects {
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
} else if options.MaxRedirects > 0 {
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= options.MaxRedirects {
|
||||
return fmt.Errorf("stopped after %d redirects", options.MaxRedirects)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return &SiteChecker{
|
||||
sites: make(map[string]*SiteInfo),
|
||||
options: options,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// SetUpdateCallback sets the callback function for site updates
|
||||
func (sc *SiteChecker) SetUpdateCallback(callback func([]*SiteInfo)) {
|
||||
sc.onUpdateCallback = callback
|
||||
}
|
||||
|
||||
// CollectSites collects URLs from enabled indexed sites only
|
||||
func (sc *SiteChecker) CollectSites() {
|
||||
sc.mu.Lock()
|
||||
defer sc.mu.Unlock()
|
||||
|
||||
// Clear existing sites
|
||||
sc.sites = make(map[string]*SiteInfo)
|
||||
|
||||
// Debug: log indexed sites count
|
||||
logger.Infof("Found %d indexed sites", len(site.IndexedSites))
|
||||
|
||||
// Collect URLs from indexed sites, but only from enabled sites
|
||||
for siteName, indexedSite := range site.IndexedSites {
|
||||
// Check site status - only collect from enabled sites
|
||||
siteStatus := site.GetSiteStatus(siteName)
|
||||
if siteStatus != site.SiteStatusEnabled {
|
||||
logger.Debugf("Skipping site %s (status: %s) - only collecting from enabled sites", siteName, siteStatus)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debugf("Processing enabled site: %s with %d URLs", siteName, len(indexedSite.Urls))
|
||||
for _, url := range indexedSite.Urls {
|
||||
if url != "" {
|
||||
logger.Debugf("Adding site URL: %s", url)
|
||||
// Load site config to determine display URL
|
||||
config, err := LoadSiteConfig(url)
|
||||
protocol := "http" // default protocol
|
||||
if err == nil && config != nil && config.HealthCheckConfig != nil && config.HealthCheckConfig.Protocol != "" {
|
||||
protocol = config.HealthCheckConfig.Protocol
|
||||
logger.Debugf("Site %s using protocol: %s", url, protocol)
|
||||
} else {
|
||||
logger.Debugf("Site %s using default protocol: %s (config error: %v)", url, protocol, err)
|
||||
}
|
||||
|
||||
// Parse URL components for legacy fields
|
||||
_, hostPort := parseURLComponents(url, protocol)
|
||||
|
||||
// Get or create site config to get ID
|
||||
siteConfig := getOrCreateSiteConfigForURL(url)
|
||||
|
||||
siteInfo := &SiteInfo{
|
||||
ID: siteConfig.ID,
|
||||
Host: siteConfig.Host,
|
||||
Port: siteConfig.Port,
|
||||
Scheme: siteConfig.Scheme,
|
||||
DisplayURL: siteConfig.GetURL(),
|
||||
Name: extractDomainName(url),
|
||||
Status: StatusChecking,
|
||||
LastChecked: time.Now().Unix(),
|
||||
// Legacy fields for backward compatibility
|
||||
URL: url,
|
||||
HealthCheckProtocol: protocol,
|
||||
HostPort: hostPort,
|
||||
}
|
||||
sc.sites[url] = siteInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("Collected %d sites for checking (enabled sites only)", len(sc.sites))
|
||||
}
|
||||
|
||||
// getOrCreateSiteConfigForURL gets or creates a site config for the given URL
|
||||
func getOrCreateSiteConfigForURL(url string) *model.SiteConfig {
|
||||
// Parse URL to get host:port
|
||||
tempConfig := &model.SiteConfig{}
|
||||
tempConfig.SetFromURL(url)
|
||||
|
||||
sc := query.SiteConfig
|
||||
siteConfig, err := sc.Where(sc.Host.Eq(tempConfig.Host)).First()
|
||||
if err != nil {
|
||||
// Record doesn't exist, create a new one
|
||||
newConfig := &model.SiteConfig{
|
||||
Host: tempConfig.Host,
|
||||
Port: tempConfig.Port,
|
||||
Scheme: tempConfig.Scheme,
|
||||
DisplayURL: url,
|
||||
HealthCheckEnabled: true,
|
||||
CheckInterval: 300,
|
||||
Timeout: 10,
|
||||
UserAgent: "Nginx-UI Site Checker/1.0",
|
||||
MaxRedirects: 3,
|
||||
FollowRedirects: true,
|
||||
CheckFavicon: true,
|
||||
}
|
||||
|
||||
// Create the record in database
|
||||
if err := sc.Create(newConfig); err != nil {
|
||||
logger.Errorf("Failed to create site config for %s: %v", url, err)
|
||||
// Return temp config with a fake ID to avoid crashes
|
||||
tempConfig.ID = 0
|
||||
return tempConfig
|
||||
}
|
||||
|
||||
return newConfig
|
||||
}
|
||||
|
||||
// Record exists, ensure it has the correct URL information
|
||||
if siteConfig.DisplayURL == "" {
|
||||
siteConfig.DisplayURL = url
|
||||
siteConfig.SetFromURL(url)
|
||||
// Try to save the updated config, but don't fail if it doesn't work
|
||||
sc.Save(siteConfig)
|
||||
}
|
||||
|
||||
return siteConfig
|
||||
}
|
||||
|
||||
// CheckSite checks a single site's availability
|
||||
func (sc *SiteChecker) CheckSite(ctx context.Context, siteURL string) (*SiteInfo, error) {
|
||||
// Try enhanced health check first if config exists
|
||||
config, err := LoadSiteConfig(siteURL)
|
||||
if err == nil && config != nil && config.HealthCheckConfig != nil {
|
||||
enhancedChecker := NewEnhancedSiteChecker()
|
||||
siteInfo, err := enhancedChecker.CheckSiteWithConfig(ctx, siteURL, config.HealthCheckConfig)
|
||||
if err == nil && siteInfo != nil {
|
||||
// Fill in additional details
|
||||
siteInfo.Name = extractDomainName(siteURL)
|
||||
siteInfo.LastChecked = time.Now().Unix()
|
||||
|
||||
// Set health check protocol and display URL
|
||||
siteInfo.HealthCheckProtocol = config.HealthCheckConfig.Protocol
|
||||
siteInfo.DisplayURL = generateDisplayURL(siteURL, config.HealthCheckConfig.Protocol)
|
||||
|
||||
// Parse URL components
|
||||
scheme, hostPort := parseURLComponents(siteURL, config.HealthCheckConfig.Protocol)
|
||||
siteInfo.Scheme = scheme
|
||||
siteInfo.HostPort = hostPort
|
||||
|
||||
// Try to get favicon if enabled and not a gRPC check
|
||||
if sc.options.CheckFavicon && !isGRPCProtocol(config.HealthCheckConfig.Protocol) {
|
||||
faviconURL, faviconData := sc.tryGetFavicon(ctx, siteURL)
|
||||
siteInfo.FaviconURL = faviconURL
|
||||
siteInfo.FaviconData = faviconData
|
||||
}
|
||||
|
||||
return siteInfo, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to basic HTTP check, but preserve original protocol if available
|
||||
originalProtocol := "http" // default
|
||||
if config != nil && config.HealthCheckConfig != nil && config.HealthCheckConfig.Protocol != "" {
|
||||
originalProtocol = config.HealthCheckConfig.Protocol
|
||||
}
|
||||
return sc.checkSiteBasic(ctx, siteURL, originalProtocol)
|
||||
}
|
||||
|
||||
// checkSiteBasic performs basic HTTP health check
|
||||
func (sc *SiteChecker) checkSiteBasic(ctx context.Context, siteURL string, originalProtocol string) (*SiteInfo, error) {
|
||||
start := time.Now()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", siteURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", sc.options.UserAgent)
|
||||
|
||||
resp, err := sc.client.Do(req)
|
||||
if err != nil {
|
||||
// Parse URL components for legacy fields
|
||||
_, hostPort := parseURLComponents(siteURL, originalProtocol)
|
||||
|
||||
// Get or create site config to get ID
|
||||
siteConfig := getOrCreateSiteConfigForURL(siteURL)
|
||||
|
||||
return &SiteInfo{
|
||||
ID: siteConfig.ID,
|
||||
Host: siteConfig.Host,
|
||||
Port: siteConfig.Port,
|
||||
Scheme: siteConfig.Scheme,
|
||||
DisplayURL: siteConfig.GetURL(),
|
||||
Name: extractDomainName(siteURL),
|
||||
Status: StatusOffline,
|
||||
ResponseTime: time.Since(start).Milliseconds(),
|
||||
LastChecked: time.Now().Unix(),
|
||||
Error: err.Error(),
|
||||
// Legacy fields for backward compatibility
|
||||
URL: siteURL,
|
||||
HealthCheckProtocol: originalProtocol,
|
||||
HostPort: hostPort,
|
||||
}, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
responseTime := time.Since(start).Milliseconds()
|
||||
|
||||
// Parse URL components for legacy fields
|
||||
_, hostPort := parseURLComponents(siteURL, originalProtocol)
|
||||
|
||||
// Get or create site config to get ID
|
||||
siteConfig := getOrCreateSiteConfigForURL(siteURL)
|
||||
|
||||
siteInfo := &SiteInfo{
|
||||
ID: siteConfig.ID,
|
||||
Host: siteConfig.Host,
|
||||
Port: siteConfig.Port,
|
||||
Scheme: siteConfig.Scheme,
|
||||
DisplayURL: siteConfig.GetURL(),
|
||||
Name: extractDomainName(siteURL),
|
||||
StatusCode: resp.StatusCode,
|
||||
ResponseTime: responseTime,
|
||||
LastChecked: time.Now().Unix(),
|
||||
// Legacy fields for backward compatibility
|
||||
URL: siteURL,
|
||||
HealthCheckProtocol: originalProtocol,
|
||||
HostPort: hostPort,
|
||||
}
|
||||
|
||||
// Determine status based on status code
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
|
||||
siteInfo.Status = StatusOnline
|
||||
} else {
|
||||
siteInfo.Status = StatusError
|
||||
siteInfo.Error = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Read response body for title and favicon extraction
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to read response body for %s: %v", siteURL, err)
|
||||
return siteInfo, nil
|
||||
}
|
||||
|
||||
// Extract title
|
||||
siteInfo.Title = extractTitle(string(body))
|
||||
|
||||
// Extract favicon if enabled
|
||||
if sc.options.CheckFavicon {
|
||||
faviconURL, faviconData := sc.extractFavicon(ctx, siteURL, string(body))
|
||||
siteInfo.FaviconURL = faviconURL
|
||||
siteInfo.FaviconData = faviconData
|
||||
}
|
||||
|
||||
return siteInfo, nil
|
||||
}
|
||||
|
||||
// tryGetFavicon attempts to get favicon for enhanced checks
|
||||
func (sc *SiteChecker) tryGetFavicon(ctx context.Context, siteURL string) (string, string) {
|
||||
// Make a simple GET request to get the HTML
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", siteURL, nil)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", sc.options.UserAgent)
|
||||
|
||||
resp, err := sc.client.Do(req)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return sc.extractFavicon(ctx, siteURL, string(body))
|
||||
}
|
||||
|
||||
// CheckAllSites checks all collected sites concurrently
|
||||
func (sc *SiteChecker) CheckAllSites(ctx context.Context) {
|
||||
sc.mu.RLock()
|
||||
urls := make([]string, 0, len(sc.sites))
|
||||
for url := range sc.sites {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
sc.mu.RUnlock()
|
||||
|
||||
// Use a semaphore to limit concurrent requests
|
||||
semaphore := make(chan struct{}, 10) // Max 10 concurrent requests
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, url := range urls {
|
||||
wg.Add(1)
|
||||
go func(siteURL string) {
|
||||
defer wg.Done()
|
||||
|
||||
semaphore <- struct{}{} // Acquire semaphore
|
||||
defer func() { <-semaphore }() // Release semaphore
|
||||
|
||||
siteInfo, err := sc.CheckSite(ctx, siteURL)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to check site %s: %v", siteURL, err)
|
||||
return
|
||||
}
|
||||
|
||||
sc.mu.Lock()
|
||||
sc.sites[siteURL] = siteInfo
|
||||
sc.mu.Unlock()
|
||||
}(url)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
logger.Infof("Completed checking %d sites", len(urls))
|
||||
|
||||
// Notify WebSocket clients of the update
|
||||
if sc.onUpdateCallback != nil {
|
||||
sites := make([]*SiteInfo, 0, len(sc.sites))
|
||||
sc.mu.RLock()
|
||||
for _, site := range sc.sites {
|
||||
sites = append(sites, site)
|
||||
}
|
||||
sc.mu.RUnlock()
|
||||
sc.onUpdateCallback(sites)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSites returns all checked sites
|
||||
func (sc *SiteChecker) GetSites() map[string]*SiteInfo {
|
||||
sc.mu.RLock()
|
||||
defer sc.mu.RUnlock()
|
||||
|
||||
// Create a copy to avoid race conditions
|
||||
result := make(map[string]*SiteInfo)
|
||||
maps.Copy(result, sc.sites)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetSitesList returns sites as a slice
|
||||
func (sc *SiteChecker) GetSitesList() []*SiteInfo {
|
||||
sc.mu.RLock()
|
||||
defer sc.mu.RUnlock()
|
||||
|
||||
result := make([]*SiteInfo, 0, len(sc.sites))
|
||||
for _, site := range sc.sites {
|
||||
result = append(result, site)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// extractDomainName extracts domain name from URL
|
||||
func extractDomainName(siteURL string) string {
|
||||
parsed, err := url.Parse(siteURL)
|
||||
if err != nil {
|
||||
return siteURL
|
||||
}
|
||||
return parsed.Host
|
||||
}
|
||||
|
||||
// extractTitle extracts title from HTML content
|
||||
func extractTitle(html string) string {
|
||||
titleRegex := regexp.MustCompile(`(?i)<title[^>]*>([^<]+)</title>`)
|
||||
matches := titleRegex.FindStringSubmatch(html)
|
||||
if len(matches) > 1 {
|
||||
return strings.TrimSpace(matches[1])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractFavicon extracts favicon URL and data from HTML
|
||||
func (sc *SiteChecker) extractFavicon(ctx context.Context, siteURL, html string) (string, string) {
|
||||
parsedURL, err := url.Parse(siteURL)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// Look for favicon link in HTML
|
||||
faviconRegex := regexp.MustCompile(`(?i)<link[^>]*rel=["'](?:icon|shortcut icon)["'][^>]*href=["']([^"']+)["']`)
|
||||
matches := faviconRegex.FindStringSubmatch(html)
|
||||
|
||||
var faviconURL string
|
||||
if len(matches) > 1 {
|
||||
faviconURL = matches[1]
|
||||
} else {
|
||||
// Default favicon location
|
||||
faviconURL = "/favicon.ico"
|
||||
}
|
||||
|
||||
// Convert relative URL to absolute
|
||||
if !strings.HasPrefix(faviconURL, "http") {
|
||||
baseURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
|
||||
if strings.HasPrefix(faviconURL, "/") {
|
||||
faviconURL = baseURL + faviconURL
|
||||
} else {
|
||||
faviconURL = baseURL + "/" + faviconURL
|
||||
}
|
||||
}
|
||||
|
||||
// Download favicon
|
||||
faviconData := sc.downloadFavicon(ctx, faviconURL)
|
||||
|
||||
return faviconURL, faviconData
|
||||
}
|
||||
|
||||
// downloadFavicon downloads and encodes favicon as base64
|
||||
func (sc *SiteChecker) downloadFavicon(ctx context.Context, faviconURL string) string {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", faviconURL, nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", sc.options.UserAgent)
|
||||
|
||||
resp, err := sc.client.Do(req)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Limit favicon size to 1MB
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get content type
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
// Try to determine from URL extension
|
||||
if strings.HasSuffix(faviconURL, ".png") {
|
||||
contentType = "image/png"
|
||||
} else if strings.HasSuffix(faviconURL, ".ico") {
|
||||
contentType = "image/x-icon"
|
||||
} else {
|
||||
contentType = "image/x-icon" // default
|
||||
}
|
||||
}
|
||||
|
||||
// Encode as data URL
|
||||
encoded := base64.StdEncoding.EncodeToString(body)
|
||||
return fmt.Sprintf("data:%s;base64,%s", contentType, encoded)
|
||||
}
|
||||
|
||||
// generateDisplayURL generates the URL to display in UI based on health check protocol
|
||||
func generateDisplayURL(originalURL, protocol string) string {
|
||||
parsed, err := url.Parse(originalURL)
|
||||
if err != nil {
|
||||
logger.Debugf("Failed to parse URL %s: %v", originalURL, err)
|
||||
return originalURL
|
||||
}
|
||||
|
||||
logger.Debugf("Generating display URL for %s with protocol %s", originalURL, protocol)
|
||||
|
||||
// Determine the optimal scheme (prefer HTTPS if available)
|
||||
scheme := determineOptimalScheme(parsed, protocol)
|
||||
hostname := parsed.Hostname()
|
||||
port := parsed.Port()
|
||||
|
||||
// For HTTP/HTTPS, return clean URL without default ports
|
||||
if scheme == "http" || scheme == "https" {
|
||||
// Build URL without default ports
|
||||
var result string
|
||||
if port == "" || (port == "80" && scheme == "http") || (port == "443" && scheme == "https") {
|
||||
// No port or default port - don't show port
|
||||
result = fmt.Sprintf("%s://%s", scheme, hostname)
|
||||
} else {
|
||||
// Non-default port - show port
|
||||
result = fmt.Sprintf("%s://%s:%s", scheme, hostname, port)
|
||||
}
|
||||
logger.Debugf("HTTP/HTTPS display URL: %s", result)
|
||||
return result
|
||||
}
|
||||
|
||||
// For gRPC/gRPCS, show the connection address format without default ports
|
||||
if scheme == "grpc" || scheme == "grpcs" {
|
||||
if port == "" {
|
||||
// Determine default port based on scheme
|
||||
if scheme == "grpcs" {
|
||||
port = "443"
|
||||
} else {
|
||||
port = "80"
|
||||
}
|
||||
}
|
||||
|
||||
// Don't show default ports for gRPC either
|
||||
var result string
|
||||
if (port == "80" && scheme == "grpc") || (port == "443" && scheme == "grpcs") {
|
||||
result = fmt.Sprintf("%s://%s", scheme, hostname)
|
||||
} else {
|
||||
result = fmt.Sprintf("%s://%s:%s", scheme, hostname, port)
|
||||
}
|
||||
logger.Debugf("gRPC/gRPCS display URL: %s", result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Fallback to original URL
|
||||
logger.Debugf("Using fallback display URL: %s", originalURL)
|
||||
return originalURL
|
||||
}
|
||||
|
||||
// isGRPCProtocol checks if the protocol is gRPC-based
|
||||
func isGRPCProtocol(protocol string) bool {
|
||||
return protocol == "grpc" || protocol == "grpcs"
|
||||
}
|
||||
|
||||
// parseURLComponents extracts scheme and host:port from URL based on health check protocol
|
||||
func parseURLComponents(originalURL, healthCheckProtocol string) (scheme, hostPort string) {
|
||||
parsed, err := url.Parse(originalURL)
|
||||
if err != nil {
|
||||
logger.Debugf("Failed to parse URL %s: %v", originalURL, err)
|
||||
return healthCheckProtocol, originalURL
|
||||
}
|
||||
|
||||
// Determine the best scheme to use
|
||||
scheme = determineOptimalScheme(parsed, healthCheckProtocol)
|
||||
|
||||
// Extract hostname and port
|
||||
hostname := parsed.Hostname()
|
||||
if hostname == "" {
|
||||
// Fallback to original URL if we can't parse hostname
|
||||
return scheme, originalURL
|
||||
}
|
||||
|
||||
port := parsed.Port()
|
||||
if port == "" {
|
||||
// Use default port based on scheme, but don't include it in hostPort for default ports
|
||||
switch scheme {
|
||||
case "https", "grpcs":
|
||||
// Default HTTPS port 443 - don't show in hostPort
|
||||
hostPort = hostname
|
||||
case "http", "grpc":
|
||||
// Default HTTP port 80 - don't show in hostPort
|
||||
hostPort = hostname
|
||||
default:
|
||||
hostPort = hostname
|
||||
}
|
||||
} else {
|
||||
// Non-default port specified
|
||||
isDefaultPort := (port == "80" && (scheme == "http" || scheme == "grpc")) ||
|
||||
(port == "443" && (scheme == "https" || scheme == "grpcs"))
|
||||
|
||||
if isDefaultPort {
|
||||
// Don't show default ports
|
||||
hostPort = hostname
|
||||
} else {
|
||||
// Show non-default ports
|
||||
hostPort = hostname + ":" + port
|
||||
}
|
||||
}
|
||||
|
||||
return scheme, hostPort
|
||||
}
|
||||
|
||||
// determineOptimalScheme determines the best scheme to use based on original URL and health check protocol
|
||||
func determineOptimalScheme(parsed *url.URL, healthCheckProtocol string) string {
|
||||
// If health check protocol is specified, use it, but with special handling for HTTP/HTTPS
|
||||
if healthCheckProtocol != "" {
|
||||
// Special case: Don't downgrade HTTPS to HTTP
|
||||
if healthCheckProtocol == "http" && parsed.Scheme == "https" {
|
||||
// logger.Debugf("Preserving HTTPS scheme instead of downgrading to HTTP")
|
||||
return "https"
|
||||
}
|
||||
|
||||
// For gRPC protocols, always use the specified protocol
|
||||
if healthCheckProtocol == "grpc" || healthCheckProtocol == "grpcs" {
|
||||
return healthCheckProtocol
|
||||
}
|
||||
|
||||
// For HTTPS health check protocol, always use HTTPS
|
||||
if healthCheckProtocol == "https" {
|
||||
return "https"
|
||||
}
|
||||
|
||||
// For HTTP health check protocol, only use HTTP if original was also HTTP
|
||||
if healthCheckProtocol == "http" && parsed.Scheme == "http" {
|
||||
return "http"
|
||||
}
|
||||
}
|
||||
|
||||
// If no health check protocol, or if we need to fall back, prefer HTTPS if the original URL is HTTPS
|
||||
if parsed.Scheme == "https" {
|
||||
return "https"
|
||||
}
|
||||
|
||||
// Default to HTTP
|
||||
return "http"
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
package sitecheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/model"
|
||||
"github.com/0xJacky/Nginx-UI/query"
|
||||
"github.com/uozi-tech/cosy/logger"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
// EnhancedSiteChecker provides advanced health checking capabilities
|
||||
type EnhancedSiteChecker struct {
|
||||
defaultClient *http.Client
|
||||
}
|
||||
|
||||
// NewEnhancedSiteChecker creates a new enhanced site checker
|
||||
func NewEnhancedSiteChecker() *EnhancedSiteChecker {
|
||||
transport := &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
return &EnhancedSiteChecker{
|
||||
defaultClient: client,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckSiteWithConfig performs enhanced health check using custom configuration
|
||||
func (ec *EnhancedSiteChecker) CheckSiteWithConfig(ctx context.Context, siteURL string, config *model.HealthCheckConfig) (*SiteInfo, error) {
|
||||
if config == nil {
|
||||
// Fallback to basic HTTP check
|
||||
return ec.checkHTTP(ctx, siteURL, &model.HealthCheckConfig{
|
||||
Protocol: "http",
|
||||
Method: "GET",
|
||||
Path: "/",
|
||||
ExpectedStatus: []int{200},
|
||||
})
|
||||
}
|
||||
|
||||
switch config.Protocol {
|
||||
case "grpc", "grpcs":
|
||||
return ec.checkGRPC(ctx, siteURL, config)
|
||||
case "https":
|
||||
return ec.checkHTTPS(ctx, siteURL, config)
|
||||
default: // http
|
||||
return ec.checkHTTP(ctx, siteURL, config)
|
||||
}
|
||||
}
|
||||
|
||||
// checkHTTP performs HTTP health check
|
||||
func (ec *EnhancedSiteChecker) checkHTTP(ctx context.Context, siteURL string, config *model.HealthCheckConfig) (*SiteInfo, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// Build request URL
|
||||
checkURL := siteURL
|
||||
if config.Path != "" && config.Path != "/" {
|
||||
checkURL = strings.TrimRight(siteURL, "/") + "/" + strings.TrimLeft(config.Path, "/")
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequestWithContext(ctx, config.Method, checkURL, nil)
|
||||
if err != nil {
|
||||
// Parse URL components for error case
|
||||
scheme, hostPort := parseURLComponents(siteURL, config.Protocol)
|
||||
|
||||
return &SiteInfo{
|
||||
URL: siteURL,
|
||||
Status: StatusError,
|
||||
Error: fmt.Sprintf("Failed to create request: %v", err),
|
||||
HealthCheckProtocol: config.Protocol,
|
||||
Scheme: scheme,
|
||||
HostPort: hostPort,
|
||||
}, err
|
||||
}
|
||||
|
||||
// Add custom headers
|
||||
for key, value := range config.Headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
// Set User-Agent if not provided
|
||||
if req.Header.Get("User-Agent") == "" {
|
||||
req.Header.Set("User-Agent", "Nginx-UI Enhanced Checker/2.0")
|
||||
}
|
||||
|
||||
// Add request body for POST/PUT methods
|
||||
if config.Body != "" && (config.Method == "POST" || config.Method == "PUT") {
|
||||
req.Body = io.NopCloser(strings.NewReader(config.Body))
|
||||
if req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
}
|
||||
|
||||
// Create custom client if needed
|
||||
client := ec.defaultClient
|
||||
if config.ValidateSSL || config.VerifyHostname {
|
||||
transport := &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: !config.ValidateSSL,
|
||||
},
|
||||
}
|
||||
|
||||
// Load client certificate if provided
|
||||
if config.ClientCert != "" && config.ClientKey != "" {
|
||||
cert, err := tls.LoadX509KeyPair(config.ClientCert, config.ClientKey)
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to load client certificate: %v", err)
|
||||
} else {
|
||||
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
}
|
||||
|
||||
client = &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Make request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// Parse URL components for error case
|
||||
scheme, hostPort := parseURLComponents(siteURL, config.Protocol)
|
||||
|
||||
return &SiteInfo{
|
||||
URL: siteURL,
|
||||
Status: StatusError,
|
||||
ResponseTime: time.Since(startTime).Milliseconds(),
|
||||
Error: err.Error(),
|
||||
HealthCheckProtocol: config.Protocol,
|
||||
Scheme: scheme,
|
||||
HostPort: hostPort,
|
||||
}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
responseTime := time.Since(startTime).Milliseconds()
|
||||
|
||||
// Read response body
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to read response body: %v", err)
|
||||
body = []byte{}
|
||||
}
|
||||
|
||||
// Validate status code
|
||||
statusValid := false
|
||||
if len(config.ExpectedStatus) > 0 {
|
||||
statusValid = slices.Contains(config.ExpectedStatus, resp.StatusCode)
|
||||
} else {
|
||||
statusValid = resp.StatusCode >= 200 && resp.StatusCode < 400
|
||||
}
|
||||
|
||||
// Validate response text
|
||||
bodyText := string(body)
|
||||
textValid := true
|
||||
if config.ExpectedText != "" {
|
||||
textValid = strings.Contains(bodyText, config.ExpectedText)
|
||||
}
|
||||
if config.NotExpectedText != "" {
|
||||
textValid = textValid && !strings.Contains(bodyText, config.NotExpectedText)
|
||||
}
|
||||
|
||||
// Determine final status
|
||||
status := StatusOffline
|
||||
var errorMsg string
|
||||
if statusValid && textValid {
|
||||
status = StatusOnline
|
||||
} else {
|
||||
if !statusValid {
|
||||
errorMsg = fmt.Sprintf("Unexpected status code: %d", resp.StatusCode)
|
||||
} else {
|
||||
errorMsg = "Response content validation failed"
|
||||
}
|
||||
}
|
||||
|
||||
// Parse URL components for legacy fields
|
||||
_, hostPort := parseURLComponents(siteURL, config.Protocol)
|
||||
|
||||
// Get or create site config to get ID
|
||||
siteConfig := getOrCreateSiteConfigForURL(siteURL)
|
||||
|
||||
return &SiteInfo{
|
||||
ID: siteConfig.ID,
|
||||
Host: siteConfig.Host,
|
||||
Port: siteConfig.Port,
|
||||
Scheme: siteConfig.Scheme,
|
||||
DisplayURL: siteConfig.GetURL(),
|
||||
Status: status,
|
||||
StatusCode: resp.StatusCode,
|
||||
ResponseTime: responseTime,
|
||||
Error: errorMsg,
|
||||
// Legacy fields for backward compatibility
|
||||
URL: siteURL,
|
||||
HealthCheckProtocol: config.Protocol,
|
||||
HostPort: hostPort,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// checkHTTPS performs HTTPS health check with SSL validation
|
||||
func (ec *EnhancedSiteChecker) checkHTTPS(ctx context.Context, siteURL string, config *model.HealthCheckConfig) (*SiteInfo, error) {
|
||||
// Force HTTPS protocol
|
||||
httpsConfig := *config
|
||||
httpsConfig.Protocol = "https"
|
||||
httpsConfig.ValidateSSL = true
|
||||
|
||||
return ec.checkHTTP(ctx, siteURL, &httpsConfig)
|
||||
}
|
||||
|
||||
// checkGRPC performs gRPC health check
|
||||
func (ec *EnhancedSiteChecker) checkGRPC(ctx context.Context, siteURL string, config *model.HealthCheckConfig) (*SiteInfo, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// Parse URL to get host and port
|
||||
parsedURL, err := parseGRPCURL(siteURL)
|
||||
if err != nil {
|
||||
// Parse URL components for error case
|
||||
scheme, hostPort := parseURLComponents(siteURL, config.Protocol)
|
||||
|
||||
return &SiteInfo{
|
||||
URL: siteURL,
|
||||
Status: StatusError,
|
||||
Error: fmt.Sprintf("Invalid gRPC URL: %v", err),
|
||||
HealthCheckProtocol: config.Protocol,
|
||||
Scheme: scheme,
|
||||
HostPort: hostPort,
|
||||
}, err
|
||||
}
|
||||
|
||||
// Set up connection options
|
||||
var opts []grpc.DialOption
|
||||
|
||||
// TLS configuration based on protocol setting, not URL scheme
|
||||
if config.Protocol == "grpcs" || config.ValidateSSL {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: !config.ValidateSSL,
|
||||
}
|
||||
|
||||
// For GRPCS, default to skip verification unless explicitly enabled
|
||||
if config.Protocol == "grpcs" && !config.ValidateSSL {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
}
|
||||
|
||||
// Load client certificate if provided
|
||||
if config.ClientCert != "" && config.ClientKey != "" {
|
||||
cert, err := tls.LoadX509KeyPair(config.ClientCert, config.ClientKey)
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to load client certificate: %v", err)
|
||||
} else {
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
}
|
||||
|
||||
opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
|
||||
} else {
|
||||
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
}
|
||||
|
||||
// Create connection with shorter timeout for faster failure detection
|
||||
dialCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := grpc.DialContext(dialCtx, parsedURL.Host, opts...)
|
||||
if err != nil {
|
||||
errorMsg := fmt.Sprintf("Failed to connect to gRPC server: %v", err)
|
||||
|
||||
// Provide more specific error messages
|
||||
if strings.Contains(err.Error(), "connection refused") {
|
||||
errorMsg = fmt.Sprintf("Connection refused - server may not be running on %s", parsedURL.Host)
|
||||
} else if strings.Contains(err.Error(), "context deadline exceeded") {
|
||||
errorMsg = fmt.Sprintf("Connection timeout - server at %s did not respond within 5 seconds", parsedURL.Host)
|
||||
} else if strings.Contains(err.Error(), "EOF") {
|
||||
errorMsg = fmt.Sprintf("Protocol mismatch - %s may not be a gRPC server or wrong TLS configuration", parsedURL.Host)
|
||||
}
|
||||
|
||||
// Parse URL components for error case
|
||||
scheme, hostPort := parseURLComponents(siteURL, config.Protocol)
|
||||
|
||||
return &SiteInfo{
|
||||
URL: siteURL,
|
||||
Status: StatusError,
|
||||
ResponseTime: time.Since(startTime).Milliseconds(),
|
||||
Error: errorMsg,
|
||||
HealthCheckProtocol: config.Protocol,
|
||||
Scheme: scheme,
|
||||
HostPort: hostPort,
|
||||
}, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Use health check service
|
||||
client := grpc_health_v1.NewHealthClient(conn)
|
||||
|
||||
// Determine service name
|
||||
serviceName := ""
|
||||
if config.GRPCService != "" {
|
||||
serviceName = config.GRPCService
|
||||
}
|
||||
|
||||
// Make health check request with shorter timeout
|
||||
checkCtx, checkCancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer checkCancel()
|
||||
|
||||
resp, err := client.Check(checkCtx, &grpc_health_v1.HealthCheckRequest{
|
||||
Service: serviceName,
|
||||
})
|
||||
|
||||
responseTime := time.Since(startTime).Milliseconds()
|
||||
|
||||
if err != nil {
|
||||
errorMsg := fmt.Sprintf("Health check failed: %v", err)
|
||||
|
||||
// Provide more specific error messages for gRPC health check failures
|
||||
if strings.Contains(err.Error(), "Unimplemented") {
|
||||
errorMsg = "Server does not implement gRPC health check service"
|
||||
} else if strings.Contains(err.Error(), "context deadline exceeded") {
|
||||
errorMsg = "Health check timeout - server did not respond within 3 seconds"
|
||||
} else if strings.Contains(err.Error(), "EOF") {
|
||||
errorMsg = "Connection lost during health check"
|
||||
}
|
||||
|
||||
// Parse URL components for error case
|
||||
scheme, hostPort := parseURLComponents(siteURL, config.Protocol)
|
||||
|
||||
return &SiteInfo{
|
||||
URL: siteURL,
|
||||
Status: StatusError,
|
||||
ResponseTime: responseTime,
|
||||
Error: errorMsg,
|
||||
HealthCheckProtocol: config.Protocol,
|
||||
Scheme: scheme,
|
||||
HostPort: hostPort,
|
||||
}, err
|
||||
}
|
||||
|
||||
// Check response status
|
||||
status := StatusOffline
|
||||
if resp.Status == grpc_health_v1.HealthCheckResponse_SERVING {
|
||||
status = StatusOnline
|
||||
}
|
||||
|
||||
// Parse URL components
|
||||
scheme, hostPort := parseURLComponents(siteURL, config.Protocol)
|
||||
|
||||
return &SiteInfo{
|
||||
URL: siteURL,
|
||||
Status: status,
|
||||
ResponseTime: responseTime,
|
||||
HealthCheckProtocol: config.Protocol,
|
||||
Scheme: scheme,
|
||||
HostPort: hostPort,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseGRPCURL parses a URL and extracts host:port for gRPC connection
|
||||
func parseGRPCURL(rawURL string) (*url.URL, error) {
|
||||
// Parse the original URL to extract host and port
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a new URL structure for gRPC connection
|
||||
grpcURL := &url.URL{
|
||||
Scheme: "grpc", // Default to grpc, will be overridden by config.Protocol
|
||||
Host: parsedURL.Host,
|
||||
}
|
||||
|
||||
// If no port is specified, use default ports based on original scheme
|
||||
if parsedURL.Port() == "" {
|
||||
switch parsedURL.Scheme {
|
||||
case "https":
|
||||
grpcURL.Host = parsedURL.Hostname() + ":443"
|
||||
case "http":
|
||||
grpcURL.Host = parsedURL.Hostname() + ":80"
|
||||
case "grpcs":
|
||||
grpcURL.Host = parsedURL.Hostname() + ":443"
|
||||
case "grpc":
|
||||
grpcURL.Host = parsedURL.Hostname() + ":80"
|
||||
default:
|
||||
// For URLs without scheme, default to port 80
|
||||
grpcURL.Host = parsedURL.Host + ":80"
|
||||
}
|
||||
}
|
||||
|
||||
return grpcURL, nil
|
||||
}
|
||||
|
||||
// LoadSiteConfig loads health check configuration for a site
|
||||
func LoadSiteConfig(siteURL string) (*model.SiteConfig, error) {
|
||||
// Parse URL to get host:port
|
||||
tempConfig := &model.SiteConfig{}
|
||||
tempConfig.SetFromURL(siteURL)
|
||||
|
||||
sc := query.SiteConfig
|
||||
config, err := sc.Where(sc.Host.Eq(tempConfig.Host)).First()
|
||||
if err != nil {
|
||||
// Return default config if not found
|
||||
defaultConfig := &model.SiteConfig{
|
||||
HealthCheckEnabled: true,
|
||||
CheckInterval: 300,
|
||||
Timeout: 10,
|
||||
HealthCheckConfig: &model.HealthCheckConfig{
|
||||
Protocol: "http",
|
||||
Method: "GET",
|
||||
Path: "/",
|
||||
ExpectedStatus: []int{200},
|
||||
},
|
||||
}
|
||||
defaultConfig.SetFromURL(siteURL)
|
||||
return defaultConfig, nil
|
||||
}
|
||||
|
||||
// Set default health check config if nil
|
||||
if config.HealthCheckConfig == nil {
|
||||
config.HealthCheckConfig = &model.HealthCheckConfig{
|
||||
Protocol: "http",
|
||||
Method: "GET",
|
||||
Path: "/",
|
||||
ExpectedStatus: []int{200},
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package sitecheck
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/query"
|
||||
"github.com/uozi-tech/cosy/logger"
|
||||
)
|
||||
|
||||
// applyCustomOrdering applies custom ordering from database to sites
|
||||
func applyCustomOrdering(sites []*SiteInfo) []*SiteInfo {
|
||||
if len(sites) == 0 {
|
||||
return sites
|
||||
}
|
||||
|
||||
// Get custom ordering from database
|
||||
sc := query.SiteConfig
|
||||
configs, err := sc.Find()
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to get site configs for ordering: %v", err)
|
||||
// Fall back to default ordering
|
||||
return applyDefaultOrdering(sites)
|
||||
}
|
||||
|
||||
// Create a map of URL to custom order
|
||||
orderMap := make(map[string]int)
|
||||
for _, config := range configs {
|
||||
orderMap[config.GetURL()] = config.CustomOrder
|
||||
}
|
||||
|
||||
// Sort sites based on custom order, with fallback to default ordering
|
||||
sort.Slice(sites, func(i, j int) bool {
|
||||
orderI, hasOrderI := orderMap[sites[i].URL]
|
||||
orderJ, hasOrderJ := orderMap[sites[j].URL]
|
||||
|
||||
// If both have custom order, use custom order
|
||||
if hasOrderI && hasOrderJ {
|
||||
return orderI < orderJ
|
||||
}
|
||||
|
||||
// If only one has custom order, it comes first
|
||||
if hasOrderI && !hasOrderJ {
|
||||
return true
|
||||
}
|
||||
if !hasOrderI && hasOrderJ {
|
||||
return false
|
||||
}
|
||||
|
||||
// If neither has custom order, use default ordering
|
||||
return defaultCompare(sites[i], sites[j])
|
||||
})
|
||||
|
||||
return sites
|
||||
}
|
||||
|
||||
// applyDefaultOrdering applies the default stable sorting
|
||||
func applyDefaultOrdering(sites []*SiteInfo) []*SiteInfo {
|
||||
sort.Slice(sites, func(i, j int) bool {
|
||||
return defaultCompare(sites[i], sites[j])
|
||||
})
|
||||
return sites
|
||||
}
|
||||
|
||||
// defaultCompare implements the default site comparison logic
|
||||
func defaultCompare(a, b *SiteInfo) bool {
|
||||
// Primary sort: by status (online > checking > error > offline)
|
||||
statusPriority := map[string]int{
|
||||
"online": 4,
|
||||
"checking": 3,
|
||||
"error": 2,
|
||||
"offline": 1,
|
||||
}
|
||||
|
||||
priorityA := statusPriority[a.Status]
|
||||
priorityB := statusPriority[b.Status]
|
||||
|
||||
if priorityA != priorityB {
|
||||
return priorityA > priorityB
|
||||
}
|
||||
|
||||
// Secondary sort: by response time (faster first, for online sites)
|
||||
if a.Status == "online" && b.Status == "online" {
|
||||
if a.ResponseTime != b.ResponseTime {
|
||||
return a.ResponseTime < b.ResponseTime
|
||||
}
|
||||
}
|
||||
|
||||
// Tertiary sort: by name (alphabetical, stable)
|
||||
if a.Name != b.Name {
|
||||
return a.Name < b.Name
|
||||
}
|
||||
|
||||
// Final sort: by URL (for complete stability)
|
||||
return a.URL < b.URL
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package sitecheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/uozi-tech/cosy/logger"
|
||||
)
|
||||
|
||||
// Service manages site checking operations
|
||||
type Service struct {
|
||||
checker *SiteChecker
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
ticker *time.Ticker
|
||||
mu sync.RWMutex
|
||||
running bool
|
||||
}
|
||||
|
||||
var (
|
||||
globalService *Service
|
||||
serviceOnce sync.Once
|
||||
)
|
||||
|
||||
// GetService returns the singleton service instance
|
||||
func GetService() *Service {
|
||||
serviceOnce.Do(func() {
|
||||
globalService = NewService(DefaultCheckOptions())
|
||||
})
|
||||
return globalService
|
||||
}
|
||||
|
||||
// NewService creates a new site checking service
|
||||
func NewService(options CheckOptions) *Service {
|
||||
return NewServiceWithContext(context.Background(), options)
|
||||
}
|
||||
|
||||
// NewServiceWithContext creates a new site checking service with a parent context
|
||||
func NewServiceWithContext(parentCtx context.Context, options CheckOptions) *Service {
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
|
||||
return &Service{
|
||||
checker: NewSiteChecker(options),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// SetUpdateCallback sets the callback function for site updates
|
||||
func (s *Service) SetUpdateCallback(callback func([]*SiteInfo)) {
|
||||
s.checker.SetUpdateCallback(callback)
|
||||
}
|
||||
|
||||
// Start begins the site checking service
|
||||
func (s *Service) Start() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.running {
|
||||
return
|
||||
}
|
||||
|
||||
s.running = true
|
||||
logger.Info("Starting site checking service")
|
||||
|
||||
// Initial collection and check with delay to allow cache scanner to complete
|
||||
go func() {
|
||||
// Wait a bit for cache scanner to collect sites
|
||||
time.Sleep(2 * time.Second)
|
||||
s.checker.CollectSites()
|
||||
s.checker.CheckAllSites(s.ctx)
|
||||
}()
|
||||
|
||||
// Start periodic checking (every 5 minutes)
|
||||
s.ticker = time.NewTicker(5 * time.Minute)
|
||||
go s.periodicCheck()
|
||||
}
|
||||
|
||||
// Stop stops the site checking service
|
||||
func (s *Service) Stop() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if !s.running {
|
||||
return
|
||||
}
|
||||
|
||||
s.running = false
|
||||
logger.Info("Stopping site checking service")
|
||||
|
||||
if s.ticker != nil {
|
||||
s.ticker.Stop()
|
||||
}
|
||||
s.cancel()
|
||||
}
|
||||
|
||||
// Restart restarts the site checking service
|
||||
func (s *Service) Restart() {
|
||||
s.Stop()
|
||||
time.Sleep(100 * time.Millisecond) // Brief pause
|
||||
s.Start()
|
||||
}
|
||||
|
||||
// periodicCheck runs periodic site checks
|
||||
func (s *Service) periodicCheck() {
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-s.ticker.C:
|
||||
logger.Debug("Starting periodic site check")
|
||||
s.checker.CollectSites() // Re-collect in case sites changed
|
||||
s.checker.CheckAllSites(s.ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshSites manually triggers a site collection and check
|
||||
func (s *Service) RefreshSites() {
|
||||
go func() {
|
||||
logger.Info("Manually refreshing sites")
|
||||
s.checker.CollectSites()
|
||||
s.checker.CheckAllSites(s.ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
// GetSites returns all checked sites with custom ordering applied
|
||||
func (s *Service) GetSites() []*SiteInfo {
|
||||
sites := s.checker.GetSitesList()
|
||||
|
||||
// Apply custom ordering from database
|
||||
return s.applySiteOrdering(sites)
|
||||
}
|
||||
|
||||
// GetSiteByURL returns a specific site by URL
|
||||
func (s *Service) GetSiteByURL(url string) *SiteInfo {
|
||||
sites := s.checker.GetSites()
|
||||
if site, exists := sites[url]; exists {
|
||||
return site
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning returns whether the service is currently running
|
||||
func (s *Service) IsRunning() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.running
|
||||
}
|
||||
|
||||
// applySiteOrdering applies custom ordering from database to sites
|
||||
func (s *Service) applySiteOrdering(sites []*SiteInfo) []*SiteInfo {
|
||||
return applyCustomOrdering(sites)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package sitecheck
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Site health check status constants
|
||||
const (
|
||||
StatusOnline = "online"
|
||||
StatusOffline = "offline"
|
||||
StatusError = "error"
|
||||
StatusChecking = "checking"
|
||||
)
|
||||
|
||||
// SiteInfo represents the information about a site
|
||||
type SiteInfo struct {
|
||||
ID uint64 `json:"id"` // Site config ID for API operations
|
||||
Host string `json:"host"` // host:port format
|
||||
Port int `json:"port"` // port number
|
||||
Scheme string `json:"scheme"` // http, https, grpc, grpcs
|
||||
DisplayURL string `json:"display_url"` // computed URL for display
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"` // StatusOnline, StatusOffline, StatusError, StatusChecking
|
||||
StatusCode int `json:"status_code"`
|
||||
ResponseTime int64 `json:"response_time"` // in milliseconds
|
||||
FaviconURL string `json:"favicon_url"`
|
||||
FaviconData string `json:"favicon_data"` // base64 encoded favicon
|
||||
Title string `json:"title"`
|
||||
LastChecked int64 `json:"last_checked"` // Unix timestamp in seconds
|
||||
Error string `json:"error,omitempty"`
|
||||
// Legacy fields for backward compatibility
|
||||
URL string `json:"url,omitempty"` // deprecated, use display_url instead
|
||||
HealthCheckProtocol string `json:"health_check_protocol,omitempty"` // deprecated, use scheme instead
|
||||
HostPort string `json:"host_port,omitempty"` // deprecated, use host instead
|
||||
}
|
||||
|
||||
// CheckOptions represents options for site checking
|
||||
type CheckOptions struct {
|
||||
Timeout time.Duration
|
||||
UserAgent string
|
||||
FollowRedirects bool
|
||||
MaxRedirects int
|
||||
CheckFavicon bool
|
||||
}
|
||||
|
||||
// DefaultCheckOptions returns default checking options
|
||||
func DefaultCheckOptions() CheckOptions {
|
||||
return CheckOptions{
|
||||
Timeout: 10 * time.Second,
|
||||
UserAgent: "Nginx-UI Site Checker/1.0",
|
||||
FollowRedirects: true,
|
||||
MaxRedirects: 3,
|
||||
CheckFavicon: true,
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@ package stream
|
||||
import "github.com/uozi-tech/cosy"
|
||||
|
||||
var (
|
||||
e = cosy.NewErrorScope("stream")
|
||||
ErrStreamNotFound = e.New(40401, "stream not found")
|
||||
ErrDstFileExists = e.New(50001, "destination file already exists")
|
||||
ErrStreamIsEnabled = e.New(50002, "stream is enabled")
|
||||
ErrNginxTestFailed = e.New(50003, "nginx test failed: {0}")
|
||||
ErrNginxReloadFailed = e.New(50004, "nginx reload failed: {0}")
|
||||
ErrReadDirFailed = e.New(50005, "read dir failed: {0}")
|
||||
e = cosy.NewErrorScope("stream")
|
||||
ErrStreamNotFound = e.New(40401, "stream not found")
|
||||
ErrDstFileExists = e.New(50001, "destination file already exists")
|
||||
ErrStreamIsEnabled = e.New(50002, "stream is enabled")
|
||||
ErrNginxTestFailed = e.New(50003, "nginx test failed: {0}")
|
||||
ErrNginxReloadFailed = e.New(50004, "nginx reload failed: {0}")
|
||||
ErrReadDirFailed = e.New(50005, "read dir failed: {0}")
|
||||
)
|
||||
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
|
||||
// ListOptions represents the options for listing streams
|
||||
type ListOptions struct {
|
||||
Search string
|
||||
Name string
|
||||
Status string
|
||||
OrderBy string
|
||||
Sort string
|
||||
Search string
|
||||
Name string
|
||||
Status string
|
||||
OrderBy string
|
||||
Sort string
|
||||
NamespaceID uint64
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package model
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ func GenerateAllModel() []any {
|
||||
Namespace{},
|
||||
ExternalNotify{},
|
||||
AutoBackup{},
|
||||
SiteConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -26,4 +26,4 @@ type Namespace struct {
|
||||
OrderID int `json:"-" gorm:"default:0"`
|
||||
PostSyncAction string `json:"post_sync_action" gorm:"default:'reload_nginx'"`
|
||||
UpstreamTestType string `json:"upstream_test_type" gorm:"default:'local'"`
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -66,4 +66,4 @@ func (n *Node) GetWebSocketURL(uri string) (decodedUri string, err error) {
|
||||
// http will be replaced with ws, https will be replaced with wss
|
||||
decodedUri = strings.ReplaceAll(decodedUri, "http", "ws")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HealthCheckConfig struct {
|
||||
// Protocol settings
|
||||
Protocol string `json:"protocol"` // http, https, grpc
|
||||
Method string `json:"method"` // GET, POST, PUT, etc.
|
||||
Path string `json:"path"` // URL path to check
|
||||
Headers map[string]string `json:"headers" gorm:"serializer:json"` // Custom headers
|
||||
Body string `json:"body"` // Request body for POST/PUT
|
||||
|
||||
// Response validation
|
||||
ExpectedStatus []int `json:"expected_status" gorm:"serializer:json"` // Expected HTTP status codes
|
||||
ExpectedText string `json:"expected_text"` // Text that should be present in response
|
||||
NotExpectedText string `json:"not_expected_text"` // Text that should NOT be present
|
||||
ValidateSSL bool `json:"validate_ssl"` // Validate SSL certificate
|
||||
|
||||
// GRPC specific settings
|
||||
GRPCService string `json:"grpc_service"` // GRPC service name
|
||||
GRPCMethod string `json:"grpc_method"` // GRPC method name
|
||||
|
||||
// Advanced settings
|
||||
DNSResolver string `json:"dns_resolver"` // Custom DNS resolver
|
||||
SourceIP string `json:"source_ip"` // Source IP for requests
|
||||
VerifyHostname bool `json:"verify_hostname"` // Verify hostname in SSL cert
|
||||
ClientCert string `json:"client_cert"` // Client certificate path
|
||||
ClientKey string `json:"client_key"` // Client key path
|
||||
}
|
||||
|
||||
type SiteConfig struct {
|
||||
Model
|
||||
Host string `gorm:"index" json:"host"` // host:port format
|
||||
Port int `gorm:"index" json:"port"` // port number
|
||||
Scheme string `gorm:"default:'http'" json:"scheme"` // http, https, grpc, grpcs
|
||||
DisplayURL string `json:"display_url"` // computed URL for display
|
||||
CustomOrder int `gorm:"default:0" json:"custom_order"`
|
||||
HealthCheckEnabled bool `gorm:"default:true" json:"health_check_enabled"`
|
||||
CheckInterval int `gorm:"default:300" json:"check_interval"` // seconds
|
||||
Timeout int `gorm:"default:10" json:"timeout"` // seconds
|
||||
UserAgent string `gorm:"default:'Nginx-UI Site Checker/1.0'" json:"user_agent"`
|
||||
MaxRedirects int `gorm:"default:3" json:"max_redirects"`
|
||||
FollowRedirects bool `gorm:"default:true" json:"follow_redirects"`
|
||||
CheckFavicon bool `gorm:"default:true" json:"check_favicon"`
|
||||
HealthCheckConfig *HealthCheckConfig `gorm:"serializer:json" json:"health_check_config"`
|
||||
}
|
||||
|
||||
// GetURL returns the computed URL for this site config
|
||||
func (sc *SiteConfig) GetURL() string {
|
||||
if sc.DisplayURL != "" {
|
||||
return sc.DisplayURL
|
||||
}
|
||||
return sc.Scheme + "://" + sc.Host
|
||||
}
|
||||
|
||||
// SetFromURL parses a URL and sets the Host, Port, and Scheme fields
|
||||
func (sc *SiteConfig) SetFromURL(url string) error {
|
||||
// Parse URL to extract host, port, and scheme
|
||||
// This is a simplified implementation - you may want to use net/url package
|
||||
if url == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store the original URL as display URL for backward compatibility
|
||||
sc.DisplayURL = url
|
||||
|
||||
// Extract scheme
|
||||
if strings.HasPrefix(url, "https://") {
|
||||
sc.Scheme = "https"
|
||||
url = strings.TrimPrefix(url, "https://")
|
||||
} else if strings.HasPrefix(url, "http://") {
|
||||
sc.Scheme = "http"
|
||||
url = strings.TrimPrefix(url, "http://")
|
||||
} else if strings.HasPrefix(url, "grpcs://") {
|
||||
sc.Scheme = "grpcs"
|
||||
url = strings.TrimPrefix(url, "grpcs://")
|
||||
} else if strings.HasPrefix(url, "grpc://") {
|
||||
sc.Scheme = "grpc"
|
||||
url = strings.TrimPrefix(url, "grpc://")
|
||||
} else {
|
||||
sc.Scheme = "http" // default
|
||||
}
|
||||
|
||||
// Extract host and port
|
||||
if strings.Contains(url, "/") {
|
||||
url = strings.Split(url, "/")[0]
|
||||
}
|
||||
|
||||
if strings.Contains(url, ":") {
|
||||
parts := strings.Split(url, ":")
|
||||
sc.Host = parts[0] + ":" + parts[1]
|
||||
if len(parts) > 1 {
|
||||
if port, err := strconv.Atoi(parts[1]); err == nil {
|
||||
sc.Port = port
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sc.Host = url + ":80" // default port
|
||||
sc.Port = 80
|
||||
if sc.Scheme == "https" || sc.Scheme == "grpcs" {
|
||||
sc.Host = url + ":443"
|
||||
sc.Port = 443
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -32,6 +32,7 @@ var (
|
||||
Notification *notification
|
||||
Passkey *passkey
|
||||
Site *site
|
||||
SiteConfig *siteConfig
|
||||
Stream *stream
|
||||
User *user
|
||||
)
|
||||
@@ -53,6 +54,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
||||
Notification = &Q.Notification
|
||||
Passkey = &Q.Passkey
|
||||
Site = &Q.Site
|
||||
SiteConfig = &Q.SiteConfig
|
||||
Stream = &Q.Stream
|
||||
User = &Q.User
|
||||
}
|
||||
@@ -75,6 +77,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
||||
Notification: newNotification(db, opts...),
|
||||
Passkey: newPasskey(db, opts...),
|
||||
Site: newSite(db, opts...),
|
||||
SiteConfig: newSiteConfig(db, opts...),
|
||||
Stream: newStream(db, opts...),
|
||||
User: newUser(db, opts...),
|
||||
}
|
||||
@@ -98,6 +101,7 @@ type Query struct {
|
||||
Notification notification
|
||||
Passkey passkey
|
||||
Site site
|
||||
SiteConfig siteConfig
|
||||
Stream stream
|
||||
User user
|
||||
}
|
||||
@@ -122,6 +126,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
||||
Notification: q.Notification.clone(db),
|
||||
Passkey: q.Passkey.clone(db),
|
||||
Site: q.Site.clone(db),
|
||||
SiteConfig: q.SiteConfig.clone(db),
|
||||
Stream: q.Stream.clone(db),
|
||||
User: q.User.clone(db),
|
||||
}
|
||||
@@ -153,6 +158,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
||||
Notification: q.Notification.replaceDB(db),
|
||||
Passkey: q.Passkey.replaceDB(db),
|
||||
Site: q.Site.replaceDB(db),
|
||||
SiteConfig: q.SiteConfig.replaceDB(db),
|
||||
Stream: q.Stream.replaceDB(db),
|
||||
User: q.User.replaceDB(db),
|
||||
}
|
||||
@@ -174,6 +180,7 @@ type queryCtx struct {
|
||||
Notification *notificationDo
|
||||
Passkey *passkeyDo
|
||||
Site *siteDo
|
||||
SiteConfig *siteConfigDo
|
||||
Stream *streamDo
|
||||
User *userDo
|
||||
}
|
||||
@@ -195,6 +202,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
||||
Notification: q.Notification.WithContext(ctx),
|
||||
Passkey: q.Passkey.WithContext(ctx),
|
||||
Site: q.Site.WithContext(ctx),
|
||||
SiteConfig: q.SiteConfig.WithContext(ctx),
|
||||
Stream: q.Stream.WithContext(ctx),
|
||||
User: q.User.WithContext(ctx),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
// 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 newSiteConfig(db *gorm.DB, opts ...gen.DOOption) siteConfig {
|
||||
_siteConfig := siteConfig{}
|
||||
|
||||
_siteConfig.siteConfigDo.UseDB(db, opts...)
|
||||
_siteConfig.siteConfigDo.UseModel(&model.SiteConfig{})
|
||||
|
||||
tableName := _siteConfig.siteConfigDo.TableName()
|
||||
_siteConfig.ALL = field.NewAsterisk(tableName)
|
||||
_siteConfig.ID = field.NewUint64(tableName, "id")
|
||||
_siteConfig.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
_siteConfig.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
_siteConfig.DeletedAt = field.NewField(tableName, "deleted_at")
|
||||
_siteConfig.Host = field.NewString(tableName, "host")
|
||||
_siteConfig.Port = field.NewInt(tableName, "port")
|
||||
_siteConfig.Scheme = field.NewString(tableName, "scheme")
|
||||
_siteConfig.DisplayURL = field.NewString(tableName, "display_url")
|
||||
_siteConfig.CustomOrder = field.NewInt(tableName, "custom_order")
|
||||
_siteConfig.HealthCheckEnabled = field.NewBool(tableName, "health_check_enabled")
|
||||
_siteConfig.CheckInterval = field.NewInt(tableName, "check_interval")
|
||||
_siteConfig.Timeout = field.NewInt(tableName, "timeout")
|
||||
_siteConfig.UserAgent = field.NewString(tableName, "user_agent")
|
||||
_siteConfig.MaxRedirects = field.NewInt(tableName, "max_redirects")
|
||||
_siteConfig.FollowRedirects = field.NewBool(tableName, "follow_redirects")
|
||||
_siteConfig.CheckFavicon = field.NewBool(tableName, "check_favicon")
|
||||
_siteConfig.HealthCheckConfigProtocol = field.NewString(tableName, "hc_protocol")
|
||||
_siteConfig.HealthCheckConfigMethod = field.NewString(tableName, "hc_method")
|
||||
_siteConfig.HealthCheckConfigPath = field.NewString(tableName, "hc_path")
|
||||
_siteConfig.HealthCheckConfigHeaders = field.NewField(tableName, "hc_headers")
|
||||
_siteConfig.HealthCheckConfigBody = field.NewString(tableName, "hc_body")
|
||||
_siteConfig.HealthCheckConfigExpectedStatus = field.NewField(tableName, "hc_expected_status")
|
||||
_siteConfig.HealthCheckConfigExpectedText = field.NewString(tableName, "hc_expected_text")
|
||||
_siteConfig.HealthCheckConfigNotExpectedText = field.NewString(tableName, "hc_not_expected_text")
|
||||
_siteConfig.HealthCheckConfigValidateSSL = field.NewBool(tableName, "hc_validate_ssl")
|
||||
_siteConfig.HealthCheckConfigGRPCService = field.NewString(tableName, "hc_g_rpc_service")
|
||||
_siteConfig.HealthCheckConfigGRPCMethod = field.NewString(tableName, "hc_g_rpc_method")
|
||||
_siteConfig.HealthCheckConfigDNSResolver = field.NewString(tableName, "hc_dns_resolver")
|
||||
_siteConfig.HealthCheckConfigSourceIP = field.NewString(tableName, "hc_source_ip")
|
||||
_siteConfig.HealthCheckConfigVerifyHostname = field.NewBool(tableName, "hc_verify_hostname")
|
||||
_siteConfig.HealthCheckConfigClientCert = field.NewString(tableName, "hc_client_cert")
|
||||
_siteConfig.HealthCheckConfigClientKey = field.NewString(tableName, "hc_client_key")
|
||||
|
||||
_siteConfig.fillFieldMap()
|
||||
|
||||
return _siteConfig
|
||||
}
|
||||
|
||||
type siteConfig struct {
|
||||
siteConfigDo
|
||||
|
||||
ALL field.Asterisk
|
||||
ID field.Uint64
|
||||
CreatedAt field.Time
|
||||
UpdatedAt field.Time
|
||||
DeletedAt field.Field
|
||||
Host field.String
|
||||
Port field.Int
|
||||
Scheme field.String
|
||||
DisplayURL field.String
|
||||
CustomOrder field.Int
|
||||
HealthCheckEnabled field.Bool
|
||||
CheckInterval field.Int
|
||||
Timeout field.Int
|
||||
UserAgent field.String
|
||||
MaxRedirects field.Int
|
||||
FollowRedirects field.Bool
|
||||
CheckFavicon field.Bool
|
||||
HealthCheckConfigProtocol field.String
|
||||
HealthCheckConfigMethod field.String
|
||||
HealthCheckConfigPath field.String
|
||||
HealthCheckConfigHeaders field.Field
|
||||
HealthCheckConfigBody field.String
|
||||
HealthCheckConfigExpectedStatus field.Field
|
||||
HealthCheckConfigExpectedText field.String
|
||||
HealthCheckConfigNotExpectedText field.String
|
||||
HealthCheckConfigValidateSSL field.Bool
|
||||
HealthCheckConfigGRPCService field.String
|
||||
HealthCheckConfigGRPCMethod field.String
|
||||
HealthCheckConfigDNSResolver field.String
|
||||
HealthCheckConfigSourceIP field.String
|
||||
HealthCheckConfigVerifyHostname field.Bool
|
||||
HealthCheckConfigClientCert field.String
|
||||
HealthCheckConfigClientKey field.String
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
|
||||
func (s siteConfig) Table(newTableName string) *siteConfig {
|
||||
s.siteConfigDo.UseTable(newTableName)
|
||||
return s.updateTableName(newTableName)
|
||||
}
|
||||
|
||||
func (s siteConfig) As(alias string) *siteConfig {
|
||||
s.siteConfigDo.DO = *(s.siteConfigDo.As(alias).(*gen.DO))
|
||||
return s.updateTableName(alias)
|
||||
}
|
||||
|
||||
func (s *siteConfig) updateTableName(table string) *siteConfig {
|
||||
s.ALL = field.NewAsterisk(table)
|
||||
s.ID = field.NewUint64(table, "id")
|
||||
s.CreatedAt = field.NewTime(table, "created_at")
|
||||
s.UpdatedAt = field.NewTime(table, "updated_at")
|
||||
s.DeletedAt = field.NewField(table, "deleted_at")
|
||||
s.Host = field.NewString(table, "host")
|
||||
s.Port = field.NewInt(table, "port")
|
||||
s.Scheme = field.NewString(table, "scheme")
|
||||
s.DisplayURL = field.NewString(table, "display_url")
|
||||
s.CustomOrder = field.NewInt(table, "custom_order")
|
||||
s.HealthCheckEnabled = field.NewBool(table, "health_check_enabled")
|
||||
s.CheckInterval = field.NewInt(table, "check_interval")
|
||||
s.Timeout = field.NewInt(table, "timeout")
|
||||
s.UserAgent = field.NewString(table, "user_agent")
|
||||
s.MaxRedirects = field.NewInt(table, "max_redirects")
|
||||
s.FollowRedirects = field.NewBool(table, "follow_redirects")
|
||||
s.CheckFavicon = field.NewBool(table, "check_favicon")
|
||||
s.HealthCheckConfigProtocol = field.NewString(table, "hc_protocol")
|
||||
s.HealthCheckConfigMethod = field.NewString(table, "hc_method")
|
||||
s.HealthCheckConfigPath = field.NewString(table, "hc_path")
|
||||
s.HealthCheckConfigHeaders = field.NewField(table, "hc_headers")
|
||||
s.HealthCheckConfigBody = field.NewString(table, "hc_body")
|
||||
s.HealthCheckConfigExpectedStatus = field.NewField(table, "hc_expected_status")
|
||||
s.HealthCheckConfigExpectedText = field.NewString(table, "hc_expected_text")
|
||||
s.HealthCheckConfigNotExpectedText = field.NewString(table, "hc_not_expected_text")
|
||||
s.HealthCheckConfigValidateSSL = field.NewBool(table, "hc_validate_ssl")
|
||||
s.HealthCheckConfigGRPCService = field.NewString(table, "hc_g_rpc_service")
|
||||
s.HealthCheckConfigGRPCMethod = field.NewString(table, "hc_g_rpc_method")
|
||||
s.HealthCheckConfigDNSResolver = field.NewString(table, "hc_dns_resolver")
|
||||
s.HealthCheckConfigSourceIP = field.NewString(table, "hc_source_ip")
|
||||
s.HealthCheckConfigVerifyHostname = field.NewBool(table, "hc_verify_hostname")
|
||||
s.HealthCheckConfigClientCert = field.NewString(table, "hc_client_cert")
|
||||
s.HealthCheckConfigClientKey = field.NewString(table, "hc_client_key")
|
||||
|
||||
s.fillFieldMap()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *siteConfig) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
_f, ok := s.fieldMap[fieldName]
|
||||
if !ok || _f == nil {
|
||||
return nil, false
|
||||
}
|
||||
_oe, ok := _f.(field.OrderExpr)
|
||||
return _oe, ok
|
||||
}
|
||||
|
||||
func (s *siteConfig) fillFieldMap() {
|
||||
s.fieldMap = make(map[string]field.Expr, 32)
|
||||
s.fieldMap["id"] = s.ID
|
||||
s.fieldMap["created_at"] = s.CreatedAt
|
||||
s.fieldMap["updated_at"] = s.UpdatedAt
|
||||
s.fieldMap["deleted_at"] = s.DeletedAt
|
||||
s.fieldMap["host"] = s.Host
|
||||
s.fieldMap["port"] = s.Port
|
||||
s.fieldMap["scheme"] = s.Scheme
|
||||
s.fieldMap["display_url"] = s.DisplayURL
|
||||
s.fieldMap["custom_order"] = s.CustomOrder
|
||||
s.fieldMap["health_check_enabled"] = s.HealthCheckEnabled
|
||||
s.fieldMap["check_interval"] = s.CheckInterval
|
||||
s.fieldMap["timeout"] = s.Timeout
|
||||
s.fieldMap["user_agent"] = s.UserAgent
|
||||
s.fieldMap["max_redirects"] = s.MaxRedirects
|
||||
s.fieldMap["follow_redirects"] = s.FollowRedirects
|
||||
s.fieldMap["check_favicon"] = s.CheckFavicon
|
||||
s.fieldMap["hc_protocol"] = s.HealthCheckConfigProtocol
|
||||
s.fieldMap["hc_method"] = s.HealthCheckConfigMethod
|
||||
s.fieldMap["hc_path"] = s.HealthCheckConfigPath
|
||||
s.fieldMap["hc_headers"] = s.HealthCheckConfigHeaders
|
||||
s.fieldMap["hc_body"] = s.HealthCheckConfigBody
|
||||
s.fieldMap["hc_expected_status"] = s.HealthCheckConfigExpectedStatus
|
||||
s.fieldMap["hc_expected_text"] = s.HealthCheckConfigExpectedText
|
||||
s.fieldMap["hc_not_expected_text"] = s.HealthCheckConfigNotExpectedText
|
||||
s.fieldMap["hc_validate_ssl"] = s.HealthCheckConfigValidateSSL
|
||||
s.fieldMap["hc_g_rpc_service"] = s.HealthCheckConfigGRPCService
|
||||
s.fieldMap["hc_g_rpc_method"] = s.HealthCheckConfigGRPCMethod
|
||||
s.fieldMap["hc_dns_resolver"] = s.HealthCheckConfigDNSResolver
|
||||
s.fieldMap["hc_source_ip"] = s.HealthCheckConfigSourceIP
|
||||
s.fieldMap["hc_verify_hostname"] = s.HealthCheckConfigVerifyHostname
|
||||
s.fieldMap["hc_client_cert"] = s.HealthCheckConfigClientCert
|
||||
s.fieldMap["hc_client_key"] = s.HealthCheckConfigClientKey
|
||||
}
|
||||
|
||||
func (s siteConfig) clone(db *gorm.DB) siteConfig {
|
||||
s.siteConfigDo.ReplaceConnPool(db.Statement.ConnPool)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s siteConfig) replaceDB(db *gorm.DB) siteConfig {
|
||||
s.siteConfigDo.ReplaceDB(db)
|
||||
return s
|
||||
}
|
||||
|
||||
type siteConfigDo struct{ gen.DO }
|
||||
|
||||
// FirstByID Where("id=@id")
|
||||
func (s siteConfigDo) FirstByID(id uint64) (result *model.SiteConfig, err error) {
|
||||
var params []interface{}
|
||||
|
||||
var generateSQL strings.Builder
|
||||
params = append(params, id)
|
||||
generateSQL.WriteString("id=? ")
|
||||
|
||||
var executeSQL *gorm.DB
|
||||
executeSQL = s.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 (s siteConfigDo) DeleteByID(id uint64) (err error) {
|
||||
var params []interface{}
|
||||
|
||||
var generateSQL strings.Builder
|
||||
params = append(params, id)
|
||||
generateSQL.WriteString("update site_configs set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=? ")
|
||||
|
||||
var executeSQL *gorm.DB
|
||||
executeSQL = s.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
|
||||
err = executeSQL.Error
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Debug() *siteConfigDo {
|
||||
return s.withDO(s.DO.Debug())
|
||||
}
|
||||
|
||||
func (s siteConfigDo) WithContext(ctx context.Context) *siteConfigDo {
|
||||
return s.withDO(s.DO.WithContext(ctx))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) ReadDB() *siteConfigDo {
|
||||
return s.Clauses(dbresolver.Read)
|
||||
}
|
||||
|
||||
func (s siteConfigDo) WriteDB() *siteConfigDo {
|
||||
return s.Clauses(dbresolver.Write)
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Session(config *gorm.Session) *siteConfigDo {
|
||||
return s.withDO(s.DO.Session(config))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Clauses(conds ...clause.Expression) *siteConfigDo {
|
||||
return s.withDO(s.DO.Clauses(conds...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Returning(value interface{}, columns ...string) *siteConfigDo {
|
||||
return s.withDO(s.DO.Returning(value, columns...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Not(conds ...gen.Condition) *siteConfigDo {
|
||||
return s.withDO(s.DO.Not(conds...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Or(conds ...gen.Condition) *siteConfigDo {
|
||||
return s.withDO(s.DO.Or(conds...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Select(conds ...field.Expr) *siteConfigDo {
|
||||
return s.withDO(s.DO.Select(conds...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Where(conds ...gen.Condition) *siteConfigDo {
|
||||
return s.withDO(s.DO.Where(conds...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Order(conds ...field.Expr) *siteConfigDo {
|
||||
return s.withDO(s.DO.Order(conds...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Distinct(cols ...field.Expr) *siteConfigDo {
|
||||
return s.withDO(s.DO.Distinct(cols...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Omit(cols ...field.Expr) *siteConfigDo {
|
||||
return s.withDO(s.DO.Omit(cols...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Join(table schema.Tabler, on ...field.Expr) *siteConfigDo {
|
||||
return s.withDO(s.DO.Join(table, on...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) LeftJoin(table schema.Tabler, on ...field.Expr) *siteConfigDo {
|
||||
return s.withDO(s.DO.LeftJoin(table, on...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) RightJoin(table schema.Tabler, on ...field.Expr) *siteConfigDo {
|
||||
return s.withDO(s.DO.RightJoin(table, on...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Group(cols ...field.Expr) *siteConfigDo {
|
||||
return s.withDO(s.DO.Group(cols...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Having(conds ...gen.Condition) *siteConfigDo {
|
||||
return s.withDO(s.DO.Having(conds...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Limit(limit int) *siteConfigDo {
|
||||
return s.withDO(s.DO.Limit(limit))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Offset(offset int) *siteConfigDo {
|
||||
return s.withDO(s.DO.Offset(offset))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *siteConfigDo {
|
||||
return s.withDO(s.DO.Scopes(funcs...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Unscoped() *siteConfigDo {
|
||||
return s.withDO(s.DO.Unscoped())
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Create(values ...*model.SiteConfig) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.DO.Create(values)
|
||||
}
|
||||
|
||||
func (s siteConfigDo) CreateInBatches(values []*model.SiteConfig, batchSize int) error {
|
||||
return s.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 (s siteConfigDo) Save(values ...*model.SiteConfig) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.DO.Save(values)
|
||||
}
|
||||
|
||||
func (s siteConfigDo) First() (*model.SiteConfig, error) {
|
||||
if result, err := s.DO.First(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SiteConfig), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Take() (*model.SiteConfig, error) {
|
||||
if result, err := s.DO.Take(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SiteConfig), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Last() (*model.SiteConfig, error) {
|
||||
if result, err := s.DO.Last(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SiteConfig), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Find() ([]*model.SiteConfig, error) {
|
||||
result, err := s.DO.Find()
|
||||
return result.([]*model.SiteConfig), err
|
||||
}
|
||||
|
||||
func (s siteConfigDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.SiteConfig, err error) {
|
||||
buf := make([]*model.SiteConfig, 0, batchSize)
|
||||
err = s.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 (s siteConfigDo) FindInBatches(result *[]*model.SiteConfig, batchSize int, fc func(tx gen.Dao, batch int) error) error {
|
||||
return s.DO.FindInBatches(result, batchSize, fc)
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Attrs(attrs ...field.AssignExpr) *siteConfigDo {
|
||||
return s.withDO(s.DO.Attrs(attrs...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Assign(attrs ...field.AssignExpr) *siteConfigDo {
|
||||
return s.withDO(s.DO.Assign(attrs...))
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Joins(fields ...field.RelationField) *siteConfigDo {
|
||||
for _, _f := range fields {
|
||||
s = *s.withDO(s.DO.Joins(_f))
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Preload(fields ...field.RelationField) *siteConfigDo {
|
||||
for _, _f := range fields {
|
||||
s = *s.withDO(s.DO.Preload(_f))
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s siteConfigDo) FirstOrInit() (*model.SiteConfig, error) {
|
||||
if result, err := s.DO.FirstOrInit(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SiteConfig), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s siteConfigDo) FirstOrCreate() (*model.SiteConfig, error) {
|
||||
if result, err := s.DO.FirstOrCreate(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.SiteConfig), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s siteConfigDo) FindByPage(offset int, limit int) (result []*model.SiteConfig, count int64, err error) {
|
||||
result, err = s.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 = s.Offset(-1).Limit(-1).Count()
|
||||
return
|
||||
}
|
||||
|
||||
func (s siteConfigDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
|
||||
count, err = s.Count()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Offset(offset).Limit(limit).Scan(result)
|
||||
return
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Scan(result interface{}) (err error) {
|
||||
return s.DO.Scan(result)
|
||||
}
|
||||
|
||||
func (s siteConfigDo) Delete(models ...*model.SiteConfig) (result gen.ResultInfo, err error) {
|
||||
return s.DO.Delete(models)
|
||||
}
|
||||
|
||||
func (s *siteConfigDo) withDO(do gen.Dao) *siteConfigDo {
|
||||
s.DO = *do.(*gen.DO)
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user