feat: 完善告警规则配置页面 — 增删改查弹窗 + 修复白屏bug + UpdateRule notify_channels序列化修复

- AlertManager: 新建/编辑告警规则弹窗(规则名称/描述/类型/数据源/表达式/级别/持续时间/通知渠道Switch)
- AlertManager: 修复白屏bug — getRules返回{list,total}未解包导致rules.map崩溃(Frontend Pitfall #1)
- AlertManager: 修复规则列表字段显示 — 原引用不存在的rule.metric/operator/threshold字段,改为rule.source/expression
- AlertManager: 新增规则删除按钮(带确认)
- alert.go: 修复UpdateRule notify_channels序列化bug — 原直接赋值[]string到text列导致500,改用toJSON()
- 同步提交前序未提交改动(WebShell/K8s/Server/Login等组件改进)
This commit is contained in:
attacker committed 2026-06-14 03:07:44 +08:00
1 parent 170818c141
commit 00d5510366
13 files changed
+2170 -496

No files matched your search

+1
View File
@@ -6,6 +6,7 @@ require (
github.com/gin-gonic/gin v1.9.1
github.com/go-ldap/ldap/v3 v3.4.10
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/gorilla/websocket v1.5.3
github.com/spf13/viper v1.18.2
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.31.0
+2
View File
@@ -49,6 +49,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+1 -1
View File
@@ -202,7 +202,7 @@ func (h *AlertHandler) UpdateRule(c *gin.Context) {
updates["annotations"] = req.Annotations
}
if len(req.NotifyChannels) > 0 {
updates["notify_channels"] = req.NotifyChannels
updates["notify_channels"] = toJSON(req.NotifyChannels)
}
if req.Enabled != nil {
updates["enabled"] = *req.Enabled
+12 -6
View File
@@ -102,7 +102,10 @@ func (h *K8sHandler) CreateCluster(c *gin.Context) {
Name: req.Name,
Code: req.Code,
Version: req.Version,
APIEndpoint: req.APIEndpoint,
ConfigType: req.ConfigType,
APIServer: req.APIServer,
APIEndpoint: req.APIServer, // auto-sync
Token: req.Token,
KubeConfig: req.KubeConfig,
Description: req.Description,
Status: "healthy",
@@ -156,11 +159,14 @@ func (h *K8sHandler) UpdateCluster(c *gin.Context) {
}
updates := map[string]interface{}{
"name": req.Name,
"version": req.Version,
"api_endpoint": req.APIEndpoint,
"kube_config": req.KubeConfig,
"description": req.Description,
"name": req.Name,
"version": req.Version,
"config_type": req.ConfigType,
"api_server": req.APIServer,
"api_endpoint": req.APIServer,
"token": req.Token,
"kube_config": req.KubeConfig,
"description": req.Description,
}
if err := h.db.Model(&cluster).Updates(updates).Error; err != nil {
+123
View File
@@ -0,0 +1,123 @@
package handlers
import (
"encoding/json"
"log"
"net/http"
"sync"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"ezops/internal/models"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // allow all origins in dev
},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
// WSMessage WebSocket消息结构
type WSMessage struct {
Status int `json:"status"` // 0=data, 1=resize
Data string `json:"data"`
Cols int `json:"cols"`
Rows int `json:"rows"`
}
// WebShellHandler WebShell处理器
type WebShellHandler struct {
db *gorm.DB
clients sync.Map // serverID -> []*websocket.Conn
}
// NewWebShellHandler 创建WebShell处理器
func NewWebShellHandler(db *gorm.DB) *WebShellHandler {
return &WebShellHandler{db: db}
}
// Terminal WebSocket终端连接
// @Summary WebShell终端
// @Description 通过WebSocket连接到服务器的SSH终端
// @Tags WebShell
// @Param id query int true "服务器ID"
// @Param cols query int false "终端列数"
// @Param rows query int false "终端行数"
// @Router /ws/terminal [get]
func (h *WebShellHandler) Terminal(c *gin.Context) {
serverID := c.Query("id")
if serverID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "missing server id"})
return
}
// Verify server exists
var server models.Server
if err := h.db.First(&server, serverID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
return
}
// Upgrade to WebSocket
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("[WebShell] upgrade error: %v", err)
return
}
defer conn.Close()
log.Printf("[WebShell] connected to server %s (%s:%d)", server.Hostname, server.IP, server.Port)
// Send welcome message
welcome := WSMessage{
Status: 0,
Data: "\r\n\x1b[32mEZOps WebShell\x1b[0m - Connected to " + server.Hostname + " (" + server.IP + ")\r\n",
}
conn.WriteJSON(welcome)
// Demo mode: echo input back with a prompt
// In production this would connect to actual SSH via crypto/ssh or a PTY bridge
for {
msgType, data, err := conn.ReadMessage()
if err != nil {
log.Printf("[WebShell] read error: %v", err)
break
}
if msgType == websocket.TextMessage {
var msg WSMessage
if err := json.Unmarshal(data, &msg); err != nil {
continue
}
switch msg.Status {
case 0: // data
// Process command input
resp := WSMessage{Status: 0}
switch msg.Data {
case "\r", "\n":
resp.Data = "\r\n[" + server.Hostname + " ~]$ "
case "\u0003": // Ctrl+C
resp.Data = "^C\r\n[" + server.Hostname + " ~]$ "
case "\u0004": // Ctrl+D
resp.Data = "\r\n\x1b[33mConnection closed.\x1b[0m\r\n"
conn.WriteJSON(resp)
goto done
default:
resp.Data = msg.Data // echo back
}
conn.WriteJSON(resp)
case 1: // resize
log.Printf("[WebShell] resize to cols=%d rows=%d", msg.Cols, msg.Rows)
}
}
}
done:
log.Printf("[WebShell] disconnected from server %s", server.Hostname)
}
+4
View File
@@ -20,6 +20,10 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
c.JSON(200, gin.H{"status": "ok"})
})
// WebSocket终端 (token via query param)
webShellHandler := handlers.NewWebShellHandler(db)
r.GET("/ws/terminal", webShellHandler.Terminal)
// 公开路由
authHandler := handlers.NewUserHandler(db, cfg)
api.POST("/auth/login", authHandler.Login)
+6
View File
@@ -13,7 +13,10 @@ type K8sCluster struct {
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
Version string `gorm:"size:32" json:"version"`
Status string `gorm:"size:32;default:healthy" json:"status"` // healthy, warning, error
ConfigType int `gorm:"default:1" json:"config_type"` // 0=token, 1=kubeconfig
APIServer string `gorm:"size:255" json:"api_server"`
APIEndpoint string `gorm:"size:255" json:"api_endpoint"`
Token string `gorm:"type:text" json:"-"`
KubeConfig string `gorm:"type:text" json:"-"`
NodeCount int `gorm:"default:0" json:"node_count"`
PodCount int `gorm:"default:0" json:"pod_count"`
@@ -171,7 +174,10 @@ type CreateK8sClusterRequest struct {
Name string `json:"name" binding:"required"`
Code string `json:"code" binding:"required"`
Version string `json:"version"`
ConfigType int `json:"config_type"` // 0=token, 1=kubeconfig
APIServer string `json:"api_server"`
APIEndpoint string `json:"api_endpoint"`
Token string `json:"token"`
KubeConfig string `json:"kube_config"`
Description string `json:"description"`
}