refactor: add config to disable site health check #1427, #1415, #1413

This commit is contained in:
0xJacky
2025-11-09 09:41:33 +00:00
parent d24d845816
commit cfb6cae78a
17 changed files with 304 additions and 406 deletions

View File

@@ -21,6 +21,36 @@ type PerformanceClient struct {
ctx context.Context
cancel context.CancelFunc
mutex sync.RWMutex
closed bool
}
func (c *PerformanceClient) trySend(message interface{}) bool {
c.mutex.RLock()
if c.closed {
c.mutex.RUnlock()
return false
}
select {
case c.send <- message:
c.mutex.RUnlock()
return true
default:
c.mutex.RUnlock()
return false
}
}
func (c *PerformanceClient) closeSendChannel() {
c.mutex.Lock()
if c.closed {
c.mutex.Unlock()
return
}
close(c.send)
c.closed = true
c.mutex.Unlock()
}
// PerformanceHub manages WebSocket connections for Nginx performance monitoring
@@ -60,20 +90,18 @@ func (h *PerformanceHub) run() {
case client := <-h.register:
h.mutex.Lock()
h.clients[client] = true
currentClients := len(h.clients)
h.mutex.Unlock()
logger.Debug("Nginx performance client connected, total clients:", len(h.clients))
logger.Debug("Nginx performance client connected, total clients:", currentClients)
// Send initial data to the new client
go h.sendPerformanceDataToClient(client)
case client := <-h.unregister:
h.mutex.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
currentClients, removed := h.removeClient(client)
if removed {
logger.Debug("Nginx performance client disconnected, total clients:", currentClients)
}
h.mutex.Unlock()
logger.Debug("Nginx performance client disconnected, total clients:", len(h.clients))
case <-h.ticker.C:
// Send performance data to all connected clients
@@ -82,24 +110,55 @@ func (h *PerformanceHub) run() {
case <-kernel.Context.Done():
logger.Debug("PerformanceHub: Context cancelled, closing WebSocket")
// Shutdown all clients
h.mutex.Lock()
for client := range h.clients {
close(client.send)
delete(h.clients, client)
for _, client := range h.activeClients() {
h.removeClient(client)
}
h.mutex.Unlock()
return
}
}
}
func (h *PerformanceHub) activeClients() []*PerformanceClient {
h.mutex.RLock()
if len(h.clients) == 0 {
h.mutex.RUnlock()
return nil
}
clients := make([]*PerformanceClient, 0, len(h.clients))
for client := range h.clients {
clients = append(clients, client)
}
h.mutex.RUnlock()
return clients
}
func (h *PerformanceHub) removeClient(client *PerformanceClient) (remaining int, removed bool) {
h.mutex.Lock()
_, removed = h.clients[client]
if removed {
delete(h.clients, client)
}
remaining = len(h.clients)
h.mutex.Unlock()
if removed {
client.closeSendChannel()
}
return remaining, removed
}
// sendPerformanceDataToClient sends performance data to a specific client
func (h *PerformanceHub) sendPerformanceDataToClient(client *PerformanceClient) {
select {
case <-client.ctx.Done():
return
default:
}
response := performance.GetPerformanceData()
select {
case client.send <- response:
default:
if !client.trySend(response) {
// Channel is full, remove client
h.unregister <- client
}
@@ -107,27 +166,20 @@ func (h *PerformanceHub) sendPerformanceDataToClient(client *PerformanceClient)
// broadcastPerformanceData sends performance data to all connected clients
func (h *PerformanceHub) broadcastPerformanceData() {
h.mutex.RLock()
// Check if there are any connected clients
if len(h.clients) == 0 {
h.mutex.RUnlock()
clients := h.activeClients()
if len(clients) == 0 {
return
}
// Only get performance data if there are connected clients
response := performance.GetPerformanceData()
for client := range h.clients {
select {
case client.send <- response:
default:
// Channel is full, remove client
close(client.send)
delete(h.clients, client)
for _, client := range clients {
if client.trySend(response) {
continue
}
h.removeClient(client)
}
h.mutex.RUnlock()
}
// WebSocket upgrader configuration

View File

@@ -18,7 +18,7 @@ func InitRouter(r *gin.RouterGroup) {
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/health_check/:id", UpdateHealthCheck)
r.POST("site_navigation/test_health_check/:id", TestHealthCheck)
r.GET("site_navigation_ws", SiteNavigationWebSocket)

View File

@@ -12,6 +12,7 @@ import (
"github.com/spf13/cast"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger"
"gorm.io/gorm/clause"
)
// GetSiteNavigation returns all sites for navigation dashboard
@@ -54,16 +55,26 @@ func UpdateSiteOrder(c *gin.Context) {
}
// updateSiteOrderBatchByIds updates site order in batch using IDs
// Uses INSERT INTO ... ON DUPLICATE KEY UPDATE for better performance
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
}
if len(orderedIds) == 0 {
return nil
}
return nil
sc := query.SiteConfig
records := make([]*model.SiteConfig, 0, len(orderedIds))
for i, id := range orderedIds {
records = append(records, &model.SiteConfig{
Model: model.Model{ID: id},
CustomOrder: i,
})
}
return sc.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"custom_order"}),
}).Create(records...)
}
// GetHealthCheck gets health check configuration for a site
@@ -102,41 +113,7 @@ func ensureHealthCheckConfig(siteConfig *model.SiteConfig) {
// 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",
})
cosy.Core[model.SiteConfig](c).Modify()
}
// TestHealthCheck tests a health check configuration without saving it

View File

@@ -74,7 +74,6 @@ func (wm *WSManager) BroadcastUpdate(sites []*sitecheck.SiteInfo) {
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()
}

View File

@@ -3,6 +3,7 @@ import { http } from '@uozi-admin/request'
export interface SiteInfo {
id: number // primary identifier for API operations
health_check_enabled: boolean // whether health check is enabled
host: string // host:port format
port: number
scheme: string // http, https, grpc, grpcs
@@ -23,6 +24,7 @@ export interface SiteInfo {
}
export interface HealthCheckConfig {
health_check_enabled?: boolean
check_interval?: number
timeout?: number
user_agent?: string
@@ -135,7 +137,7 @@ export const siteNavigationApi = {
// Update health check configuration
updateHealthCheck(id: number, config: HealthCheckConfig): Promise<{ message: string }> {
return http.put(`/site_navigation/health_check/${id}`, config)
return http.post(`/site_navigation/health_check/${id}`, config)
},
// Test health check configuration

View File

@@ -56,10 +56,6 @@ const name = computed(() => {
margin-bottom: 16px;
}
@media (max-height: 800px) {
display: none;
}
.detail {
display: flex;
/*margin-bottom: 16px;*/

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import type { SiteInfo } from '@/api/site_navigation'
import { GlobalOutlined } from '@ant-design/icons-vue'
import Sortable from 'sortablejs'
import VueDraggable from 'vuedraggable'
import { siteNavigationApi } from '@/api/site_navigation'
import { useWebSocket } from '@/lib/websocket'
import SiteCard from './components/SiteCard.vue'
import SiteHealthCheckModal from './components/SiteHealthCheckModal.vue'
import SiteNavigationToolbar from './components/SiteNavigationToolbar.vue'
@@ -11,64 +12,26 @@ const sites = ref<SiteInfo[]>([])
const { message } = useGlobalApp()
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: WebSocket | null = null
watch(sites, newSites => {
if (!settingsMode.value) {
draggableSites.value = newSites
}
}, { immediate: true })
// Display sites - use draggable sites in settings mode, backend sorted sites otherwise
const displaySites = computed(() => {
return settingsMode.value ? draggableSites.value : sites.value
const { status, data, send, close } = useWebSocket(siteNavigationApi.websocketUrl)
const isConnected = computed(() => status.value === 'OPEN')
watch(data, newData => {
if (newData.type === 'initial' || newData.type === 'update') {
sites.value = newData.data || []
}
})
// WebSocket connection
async function connectWebSocket() {
try {
const { useWebSocket } = await import('@/lib/websocket')
const { ws } = useWebSocket(siteNavigationApi.websocketUrl)
websocket = ws.value!
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
@@ -83,19 +46,11 @@ async function loadSites() {
}
}
// 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'))
}
send(JSON.stringify({ type: 'refresh' }))
message.success($gettext('Site refresh initiated'))
}
catch (error) {
console.error('Failed to refresh sites:', error)
@@ -106,61 +61,20 @@ async function handleRefresh() {
}
}
// 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)
@@ -168,20 +82,16 @@ async function saveOrder() {
}
}
// Cancel settings mode
function cancelSettingsMode() {
draggableSites.value = [...sites.value]
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) {
@@ -195,38 +105,37 @@ async function handleConfigSave(config: import('@/api/site_navigation').HealthCh
}
}
const mounted = ref(false)
onMounted(async () => {
// First load data via HTTP
await loadSites()
// Then connect WebSocket for real-time updates
connectWebSocket()
mounted.value = true
})
onUnmounted(() => {
destroySortable()
if (websocket) {
websocket.close()
}
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"
/>
<Teleport v-if="mounted" to=".action">
<SiteNavigationToolbar
:is-connected="isConnected"
:refreshing="refreshing"
:settings-mode="settingsMode"
@refresh="handleRefresh"
@toggle-settings="toggleSettingsMode"
@save-order="saveOrder"
@cancel-settings="cancelSettingsMode"
/>
</Teleport>
<div v-if="loading" class="flex items-center justify-center py-12">
<ASpin size="large" />
</div>
<div v-else-if="displaySites.length === 0" class="empty-state">
<div v-else-if="draggableSites.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') }}
@@ -236,15 +145,25 @@ onUnmounted(() => {
</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>
<VueDraggable
v-else
v-model="draggableSites"
:disabled="!settingsMode"
class="site-grid"
item-key="id"
:animation="150"
ghost-class="site-card-ghost"
chosen-class="site-card-chosen"
drag-class="site-card-drag"
>
<template #item="{ element }">
<SiteCard
:site="element"
:settings-mode="settingsMode"
@open-config="openConfigModal"
/>
</template>
</VueDraggable>
<SiteHealthCheckModal
v-model:open="configModalVisible"
@@ -256,10 +175,6 @@ onUnmounted(() => {
</template>
<style scoped>
.site-navigation {
@apply p-6;
}
.empty-state {
@apply flex flex-col items-center justify-center py-16 text-center;
}

View File

@@ -130,7 +130,7 @@ function getStatusClass(status: string): string {
</div>
</div>
<div v-if="!settingsMode" class="site-status">
<div v-if="!settingsMode && site.health_check_enabled" class="site-status">
<div
class="status-indicator"
:class="getStatusClass(site.status)"
@@ -293,11 +293,11 @@ function getStatusClass(status: string): string {
}
.site-card-config {
@apply absolute top-2 right-2;
@apply absolute top-3 right-3 opacity-50;
}
.drag-handle {
@apply absolute bottom-2 right-2 opacity-50 hover:opacity-100 transition-opacity;
@apply absolute bottom-3 right-3 opacity-50 hover:opacity-100 transition-opacity;
}
.drag-dots {

View File

@@ -4,12 +4,10 @@ import { CloseOutlined, PlusOutlined } from '@ant-design/icons-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
}
@@ -18,13 +16,9 @@ const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const { message } = useGlobalApp()
const visible = defineModel<boolean>('open', { required: true })
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,
@@ -76,8 +70,8 @@ async function loadExistingConfig() {
// Convert backend config to frontend format
formData.value = {
// Basic settings (health check is always enabled)
enabled: true,
// Basic settings
enabled: config.health_check_enabled ?? true,
interval: config.check_interval ?? 300,
timeout: config.timeout ?? 10,
userAgent: config.user_agent ?? 'Nginx-UI Enhanced Checker/2.0',
@@ -268,7 +262,7 @@ async function handleSave() {
// Create the config object for the backend
const backendConfig = {
url: props.site.url,
health_check_enabled: true, // Always enabled
health_check_enabled: config.enabled,
check_interval: config.interval,
timeout: config.timeout,
user_agent: config.userAgent,
@@ -276,7 +270,7 @@ async function handleSave() {
follow_redirects: config.followRedirects,
check_favicon: config.checkFavicon,
// Enhanced health check config (always included)
// Enhanced health check config
health_check_config: {
protocol: config.protocol,
method: config.method,
@@ -366,7 +360,7 @@ async function handleTest() {
width="800px"
@cancel="handleCancel"
>
<div class="p-2">
<div>
<AForm
:model="formData"
layout="vertical"
@@ -374,6 +368,18 @@ async function handleTest() {
:wrapper-col="{ span: 24 }"
>
<div>
<!-- Enable/Disable Health Check -->
<AFormItem :label="$gettext('Enable Health Check')">
<div class="flex items-center gap-2">
<ASwitch v-model:checked="formData.enabled" />
<span class="text-sm text-gray-500 dark:text-gray-400">
{{ formData.enabled ? $gettext('Health check is enabled') : $gettext('Health check is disabled') }}
</span>
</div>
</AFormItem>
<ADivider />
<!-- Protocol Selection -->
<AFormItem :label="$gettext('Protocol')">
<ARadioGroup v-model:value="formData.protocol">

View File

@@ -25,10 +25,6 @@ defineEmits<Emits>()
<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
@@ -44,7 +40,6 @@ defineEmits<Emits>()
<AButton
v-if="settingsMode"
type="primary"
size="small"
@click="$emit('saveOrder')"
>
<template #icon>
@@ -55,37 +50,31 @@ defineEmits<Emits>()
<AButton
v-if="settingsMode"
size="small"
@click="$emit('cancelSettings')"
>
<template #icon>
<CloseOutlined />
</template>
{{ $gettext('Cancel') }}
</AButton>
<AButton
v-if="!settingsMode"
type="primary"
size="small"
:loading="refreshing"
@click="$emit('refresh')"
>
<template #icon>
<ReloadOutlined />
</template>
{{ $gettext('Refresh') }}
</AButton>
<AButton
v-if="!settingsMode"
size="small"
@click="$emit('toggleSettings')"
>
<template #icon>
<SettingOutlined />
</template>
{{ $gettext('Settings') }}
</AButton>
</div>
</div>
@@ -94,7 +83,7 @@ defineEmits<Emits>()
<style scoped>
.site-navigation-header {
@apply flex items-center justify-between mb-6;
@apply flex items-center justify-end;
}
/* Responsive design */

32
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/BurntSushi/toml v1.5.0
github.com/blevesearch/bleve/v2 v2.5.4
github.com/caarlos0/env/v11 v11.3.1
github.com/casdoor/casdoor-go-sdk v1.29.0
github.com/casdoor/casdoor-go-sdk v1.30.0
github.com/creack/pty v1.1.24
github.com/dgraph-io/ristretto/v2 v2.3.0
github.com/docker/docker v28.5.2+incompatible
@@ -25,7 +25,7 @@ require (
github.com/go-playground/validator/v10 v10.28.0
github.com/go-resty/resty/v2 v2.16.5
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/go-webauthn/webauthn v0.14.0
github.com/go-webauthn/webauthn v0.15.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
@@ -46,7 +46,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/tufanbarisyildirim/gonginx v0.0.0-20250620092546-c3e307e36701
github.com/ulikunitz/xz v0.5.15
github.com/uozi-tech/cosy v1.27.2
github.com/uozi-tech/cosy v1.27.3
github.com/uozi-tech/cosy-driver-sqlite v0.2.1
github.com/urfave/cli/v3 v3.5.0
golang.org/x/crypto v0.43.0
@@ -84,7 +84,7 @@ require (
github.com/Azure/go-autorest/tracing v0.6.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/RoaringBitmap/roaring/v2 v2.14.0 // indirect
github.com/RoaringBitmap/roaring/v2 v2.14.2 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 // indirect
@@ -93,7 +93,7 @@ require (
github.com/alibabacloud-go/tea v1.3.13 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/aliyun/aliyun-log-go-sdk v0.1.111 // indirect
github.com/aliyun/credentials-go v1.4.7 // indirect
github.com/aliyun/credentials-go v1.4.8 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.6 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect
@@ -115,13 +115,13 @@ require (
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.3 // indirect
github.com/blevesearch/bleve_index_api v1.2.10 // indirect
github.com/blevesearch/bleve_index_api v1.2.11 // indirect
github.com/blevesearch/geo v0.2.4 // indirect
github.com/blevesearch/go-faiss v1.0.25 // indirect
github.com/blevesearch/go-faiss v1.0.26 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.3.12 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // indirect
github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
@@ -131,7 +131,7 @@ require (
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
github.com/blevesearch/zapx/v16 v16.2.6 // indirect
github.com/blevesearch/zapx/v16 v16.2.7 // indirect
github.com/blinkbean/dingtalk v1.1.3 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/bsm/redislock v0.9.4 // indirect
@@ -182,7 +182,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/go-webauthn/x v0.1.25 // indirect
github.com/go-webauthn/x v0.1.26 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect
@@ -225,7 +225,7 @@ require (
github.com/labbsr0x/goh v1.0.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/linode/linodego v1.60.0 // indirect
github.com/linode/linodego v1.61.0 // indirect
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
@@ -280,7 +280,7 @@ require (
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/prometheus v0.307.3 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/quic-go/quic-go v0.56.0 // indirect
github.com/redis/go-redis/v9 v9.16.0 // indirect
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
@@ -341,11 +341,11 @@ require (
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.38.0 // indirect

35
go.sum
View File

@@ -681,6 +681,8 @@ github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d/go.mod h1:9XMFaCeRy
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/RoaringBitmap/roaring/v2 v2.14.0 h1:NjlfrI3SmA9Zm5yM1FV+IR096NyVt2R8wRp56y6I8zU=
github.com/RoaringBitmap/roaring/v2 v2.14.0/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
github.com/RoaringBitmap/roaring/v2 v2.14.2 h1:Axst08mZTSH93IhjLibRQ/0FJKVbRTZfW2b7qosyvSI=
github.com/RoaringBitmap/roaring/v2 v2.14.2/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
@@ -752,6 +754,8 @@ github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmP
github.com/aliyun/credentials-go v1.4.5/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/aliyun/credentials-go v1.4.8 h1:MEfZGWGC3L1icM1nGcYF8rWdQBG2k1Sya2pq9uRwd30=
github.com/aliyun/credentials-go v1.4.8/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
@@ -824,10 +828,14 @@ github.com/blevesearch/bleve/v2 v2.5.4 h1:1iur8e+PHsxtncV2xIVuqlQme/V8guEDO2uV6W
github.com/blevesearch/bleve/v2 v2.5.4/go.mod h1:yB4PnV4N2q5rTEpB2ndG8N2ISexBQEFIYgwx4ztfvoo=
github.com/blevesearch/bleve_index_api v1.2.10 h1:FMFmZCmTX6PdoLLvwUnKF2RsmILFFwO3h0WPevXY9fE=
github.com/blevesearch/bleve_index_api v1.2.10/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s=
github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
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=
github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI=
github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
@@ -836,6 +844,8 @@ github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCD
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
github.com/blevesearch/scorch_segment_api/v2 v2.3.12 h1:GGZc2qwbyRBwtckPPkHkLyXw64mmsLJxdturBI1cM+c=
github.com/blevesearch/scorch_segment_api/v2 v2.3.12/go.mod h1:JBRGAneqgLSI2+jCNjtwMqp2B7EBF3/VUzgDPIU33MM=
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY=
github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
@@ -856,6 +866,8 @@ github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFx
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.2.6 h1:OHuUl2GhM+FpBq9RwNsJ4k/QodqbMMHoQEgn/IHYpu8=
github.com/blevesearch/zapx/v16 v16.2.6/go.mod h1:cuAPB+YoIyRngNhno1S1GPr9SfMk+x/SgAHBLXSIq3k=
github.com/blevesearch/zapx/v16 v16.2.7 h1:xcgFRa7f/tQXOwApVq7JWgPYSlzyUMmkuYa54tMDuR0=
github.com/blevesearch/zapx/v16 v16.2.7/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14=
github.com/blinkbean/dingtalk v1.1.3 h1:MbidFZYom7DTFHD/YIs+eaI7kRy52kmWE/sy0xjo6E4=
github.com/blinkbean/dingtalk v1.1.3/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHaJnG9pXwbto=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@@ -884,6 +896,8 @@ github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vaui
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casdoor/casdoor-go-sdk v1.29.0 h1:bINkqgVjTaXRFJwsBshbtAJnSOoP7SB0NL2aWyqXsI8=
github.com/casdoor/casdoor-go-sdk v1.29.0/go.mod h1:hVSgmSdwTCsBEJNt9r2K5aLVsoeMc37/N4Zzescy5SA=
github.com/casdoor/casdoor-go-sdk v1.30.0 h1:EKwkaQfRaXmryJUWEzq7DWh863gfDXfCr3txi2fJIDw=
github.com/casdoor/casdoor-go-sdk v1.30.0/go.mod h1:hVSgmSdwTCsBEJNt9r2K5aLVsoeMc37/N4Zzescy5SA=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
@@ -895,6 +909,7 @@ github.com/cenkalti/backoff/v5 v5.0.3/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=
@@ -906,6 +921,7 @@ 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.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
@@ -1130,8 +1146,12 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0=
github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k=
github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY=
github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A=
github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88=
github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY=
github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs=
github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg=
github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA=
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
@@ -1536,6 +1556,8 @@ 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.60.0 h1:SgsebJFRCi+lSmYy+C40wmKZeJllGGm+W12Qw4+yVdI=
github.com/linode/linodego v1.60.0/go.mod h1:1+Bt0oTz5rBnDOJbGhccxn7LYVytXTIIfAy7QYmijDs=
github.com/linode/linodego v1.61.0 h1:9g20NWl+/SbhDFj6X5EOZXtM2hBm1Mx8I9h8+F3l1LM=
github.com/linode/linodego v1.61.0/go.mod h1:64o30geLNwR0NeYh5HM/WrVCBXcSqkKnRK3x9xoRuJI=
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
github.com/liquidweb/go-lwApi v0.0.5/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
@@ -1753,6 +1775,7 @@ 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=
@@ -1830,6 +1853,8 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
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.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
@@ -2010,6 +2035,8 @@ github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 h1:/VaznPrb/b6
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419/go.mod h1:QN0/PdenvYWB0GRMz6JJbPeZz2Lph2iys1p8AFVHm2c=
github.com/uozi-tech/cosy v1.27.2 h1:iVqMx7+yqqFdfHlGy3XXegQWn9xTNCTNOIHGqill7Cg=
github.com/uozi-tech/cosy v1.27.2/go.mod h1:dCaZpbpw/RXLNuonmYZ8WyPbpdvND8GBur2qxoOnQRI=
github.com/uozi-tech/cosy v1.27.3 h1:Cj/YyXJbtOgxoXyHWjLDQ+x0P+LphneiVYepKPAosp4=
github.com/uozi-tech/cosy v1.27.3/go.mod h1:dCaZpbpw/RXLNuonmYZ8WyPbpdvND8GBur2qxoOnQRI=
github.com/uozi-tech/cosy-driver-mysql v0.2.2 h1:22S/XNIvuaKGqxQPsYPXN8TZ8hHjCQdcJKVQ83Vzxoo=
github.com/uozi-tech/cosy-driver-mysql v0.2.2/go.mod h1:EZnRIbSj1V5U0gEeTobrXai/d1SV11lkl4zP9NFEmyE=
github.com/uozi-tech/cosy-driver-postgres v0.2.1 h1:OICakGuT+omva6QOJCxTJ5Lfr7CGXLmk/zD+aS51Z2o=
@@ -2147,6 +2174,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -2374,6 +2403,8 @@ golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -2395,6 +2426,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -2525,6 +2558,8 @@ 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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

View File

@@ -120,24 +120,15 @@ func (sc *SiteChecker) CollectSites() {
}
// 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(),
SiteConfig: *siteConfig,
Name: extractDomainName(url),
Status: StatusChecking,
LastChecked: time.Now().Unix(),
// Legacy fields for backward compatibility
URL: url,
HealthCheckProtocol: protocol,
HostPort: hostPort,
}
sc.sites[url] = siteInfo
}
@@ -301,23 +292,43 @@ func getOrCreateSiteConfigForURL(url string) *model.SiteConfig {
func (sc *SiteChecker) CheckSite(ctx context.Context, siteURL string) (*SiteInfo, error) {
// Try enhanced health check first if config exists
config, err := LoadSiteConfig(siteURL)
// If health check is disabled, return a SiteInfo without status
if err == nil && config != nil && !config.HealthCheckEnabled {
protocol := "http"
if config.HealthCheckConfig != nil && config.HealthCheckConfig.Protocol != "" {
protocol = config.HealthCheckConfig.Protocol
}
siteInfo := &SiteInfo{
SiteConfig: *config,
Name: extractDomainName(siteURL),
Title: config.DisplayURL,
}
// Try to get favicon if enabled and not a gRPC check
if sc.options.CheckFavicon && !isGRPCProtocol(protocol) {
faviconURL, faviconData := sc.tryGetFavicon(ctx, siteURL)
siteInfo.FaviconURL = faviconURL
siteInfo.FaviconData = faviconData
}
return siteInfo, nil
}
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.ID = config.ID
siteInfo.HealthCheckEnabled = config.HealthCheckEnabled
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)
@@ -350,53 +361,31 @@ func (sc *SiteChecker) checkSiteBasic(ctx context.Context, siteURL string, origi
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(),
SiteConfig: *siteConfig,
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(),
SiteConfig: *siteConfig,
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

View File

@@ -84,16 +84,9 @@ func (ec *EnhancedSiteChecker) checkHTTP(ctx context.Context, siteURL string, co
// 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,
Status: StatusError,
Error: fmt.Sprintf("Failed to create request: %v", err),
}, err
}
@@ -147,17 +140,10 @@ func (ec *EnhancedSiteChecker) checkHTTP(ctx context.Context, siteURL string, co
// 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,
Status: StatusError,
ResponseTime: time.Since(startTime).Milliseconds(),
Error: err.Error(),
}, err
}
defer resp.Body.Close()
@@ -202,26 +188,15 @@ func (ec *EnhancedSiteChecker) checkHTTP(ctx context.Context, siteURL string, co
}
}
// 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(),
SiteConfig: *siteConfig,
Status: status,
StatusCode: resp.StatusCode,
ResponseTime: responseTime,
Error: errorMsg,
// Legacy fields for backward compatibility
URL: siteURL,
HealthCheckProtocol: config.Protocol,
HostPort: hostPort,
}, nil
}
@@ -242,16 +217,9 @@ func (ec *EnhancedSiteChecker) checkGRPC(ctx context.Context, siteURL string, co
// 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,
Status: StatusError,
Error: fmt.Sprintf("Invalid gRPC URL: %v", err),
}, err
}
@@ -284,11 +252,8 @@ func (ec *EnhancedSiteChecker) checkGRPC(ctx context.Context, siteURL string, co
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...)
// Create gRPC client (connection established lazily on first RPC call)
conn, err := grpc.NewClient(parsedURL.Host, opts...)
if err != nil {
errorMsg := fmt.Sprintf("Failed to connect to gRPC server: %v", err)
@@ -301,17 +266,10 @@ func (ec *EnhancedSiteChecker) checkGRPC(ctx context.Context, siteURL string, co
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,
Status: StatusError,
ResponseTime: time.Since(startTime).Milliseconds(),
Error: errorMsg,
}, err
}
defer conn.Close()
@@ -347,17 +305,10 @@ func (ec *EnhancedSiteChecker) checkGRPC(ctx context.Context, siteURL string, co
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,
Status: StatusError,
ResponseTime: responseTime,
Error: errorMsg,
}, err
}
@@ -367,16 +318,9 @@ func (ec *EnhancedSiteChecker) checkGRPC(ctx context.Context, siteURL string, co
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,
Status: status,
ResponseTime: responseTime,
}, nil
}

View File

@@ -30,8 +30,8 @@ func applyCustomOrdering(sites []*SiteInfo) []*SiteInfo {
// 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]
orderI, hasOrderI := orderMap[sites[i].DisplayURL]
orderJ, hasOrderJ := orderMap[sites[j].DisplayURL]
// If both have custom order, use custom order
if hasOrderI && hasOrderJ {
@@ -91,5 +91,5 @@ func defaultCompare(a, b *SiteInfo) bool {
}
// Final sort: by URL (for complete stability)
return a.URL < b.URL
return a.DisplayURL < b.DisplayURL
}

View File

@@ -2,6 +2,8 @@ package sitecheck
import (
"time"
"github.com/0xJacky/Nginx-UI/model"
)
// Site health check status constants
@@ -14,24 +16,16 @@ const (
// 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
model.SiteConfig
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"`
}
// CheckOptions represents options for site checking

View File

@@ -33,19 +33,19 @@ type HealthCheckConfig struct {
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"`
Host string `gorm:"index" json:"host" cosy:"all:omitempty"` // host:port format
Port int `gorm:"index" json:"port" cosy:"all:omitempty"` // port number
Scheme string `gorm:"default:'http'" json:"scheme" cosy:"all:omitempty"` // http, https, grpc, grpcs
DisplayURL string `json:"display_url" cosy:"all:omitempty"` // computed URL for display
CustomOrder int `gorm:"default:0" json:"custom_order" cosy:"all:omitempty"`
HealthCheckEnabled bool `gorm:"default:true" json:"health_check_enabled" cosy:"all:omitempty"`
CheckInterval int `gorm:"default:300" json:"check_interval" cosy:"all:omitempty"` // seconds
Timeout int `gorm:"default:10" json:"timeout" cosy:"all:omitempty"` // seconds
UserAgent string `gorm:"default:'Nginx-UI Site Checker/1.0'" json:"user_agent" cosy:"all:omitempty"`
MaxRedirects int `gorm:"default:3" json:"max_redirects" cosy:"all:omitempty"`
FollowRedirects bool `gorm:"default:true" json:"follow_redirects" cosy:"all:omitempty"`
CheckFavicon bool `gorm:"default:true" json:"check_favicon" cosy:"all:omitempty"`
HealthCheckConfig *HealthCheckConfig `gorm:"serializer:json" json:"health_check_config" cosy:"all:omitempty"`
}
// GetURL returns the computed URL for this site config