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 -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)