feat(dashboard): add sites navigation #1054

This commit is contained in:
0xJacky
2025-08-14 10:58:32 +08:00
parent 8f053fa5c5
commit e2b66fd8dd
49 changed files with 4131 additions and 284 deletions
+1 -1
View File
@@ -139,4 +139,4 @@ func RestartNginx(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
}
}
+9 -9
View File
@@ -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,
}
}
}
-1
View File
@@ -2,7 +2,6 @@ package openai
import "github.com/gin-gonic/gin"
func InitRouter(r *gin.RouterGroup) {
// ChatGPT
r.POST("chatgpt", MakeChatCompletionRequest)
+5 -5
View File
@@ -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")),
}
+12
View File
@@ -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
View File
@@ -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"`
+195
View File
@@ -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,
})
}
+164
View File
@@ -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
}
}
}
}
+6 -6
View File
@@ -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
View File
@@ -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,
})
+4
View File
@@ -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']
+151
View File
@@ -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'),
+34
View File
@@ -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
+8
View File
@@ -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'),
},
},
],
},
]
+292
View File
@@ -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="{&quot;key&quot;: &quot;value&quot;}"
/>
</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>
+1 -3
View File
@@ -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
+2 -163
View File
@@ -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=
+7 -7
View File
@@ -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"`
}
+2 -2
View File
@@ -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{
+2 -2
View File
@@ -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
View File
@@ -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"`
}
+5 -5
View File
@@ -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,
}
+6 -6
View File
@@ -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
View File
@@ -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)
}
+1 -1
View File
@@ -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 {
+5
View File
@@ -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
},
}
}
+7
View File
@@ -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",
+3 -3
View File
@@ -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
+8 -8
View File
@@ -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,
}
+1 -1
View File
@@ -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 {
+651
View File
@@ -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"
}
+452
View File
@@ -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
}
+95
View File
@@ -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
}
+155
View File
@@ -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)
}
+55
View File
@@ -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,
}
}
+7 -7
View File
@@ -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}")
)
+5 -5
View File
@@ -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
}
+1 -1
View File
@@ -3,8 +3,8 @@ package model
import (
"database/sql/driver"
"encoding/json"
"fmt"
"errors"
"fmt"
"github.com/sashabaranov/go-openai"
)
+1
View File
@@ -35,6 +35,7 @@ func GenerateAllModel() []any {
Namespace{},
ExternalNotify{},
AutoBackup{},
SiteConfig{},
}
}
+1 -1
View File
@@ -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
View File
@@ -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
}
}
+110
View File
@@ -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
}
+8
View File
@@ -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),
}
+474
View File
@@ -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
}