fix(logs): 恢复 POST /logs/histogram(直方图 404)+ 系统设置改为侧栏五分页

=== 1. 修复 /analysis 与 /logs 直方图 404(我上一轮 patch 的误伤)===
现象:分析页直接报 "API endpoint not found";日志查询页直方图不显示。
根因:37e2f0f 里一次模糊匹配的 patch 把
    logs.POST("/histogram", logsCtrl.Histogram)
  改成了
    logs.GET("/histogram", logsCtrl.Histogram)
前端 logsApi.histogram 一直用 POST(body 带 sourceId/query/start/end),
于是 POST /logs/histogram 全部 404。访问日志实证:
    POST /api/v1/logs/histogram → 404   ×7
这类"方法写错"编译期查不出来,go build / tsc 都是绿的。

修法:同时注册 POST(前端用)与 GET(curl 调试用)。

新增 backend/routers/router_test.go 守门:
  TestCriticalRoutes       断言 19 条关键路由的 method+path 组合都存在
  TestHistogramAcceptsPost 单独断言 histogram 必须同时有 POST 与 GET
以后再把 POST 写成 GET,测试会直接红。

=== 2. 系统设置放回侧栏,改成 5 个顶层 tab ===
按使用反馈调整:
  系统设置(侧栏一个入口)
    ├ 通用配置      /settings            ← tab 起点
    ├ 用户管理      /users
    ├ 审计日志      /audit
    ├ LDAP 接入     /settings/ldap       ← 新拆分
    └ 默认镜像源    /settings/registry   ← 新拆分

为什么拆:原来 /settings 页面内部已有 4 个 tab(通用/告警通知/LDAP/镜像源),
如果侧栏再加一层 tab 就是"两层 tab",点两下才知道自己在哪。现在扁平化成单层。

配套改动:
  - 新增 pages/settings/ldap.tsx、registry.tsx(各自只渲染一块内容)
  - settings 主页去掉内层 Tabs,只留通用配置(外观/语言/改密码/关于)
  - 上一版把设置放到了右上角齿轮,现在移除齿轮入口
  - 菜单文案:settings 页标题 系统设置 → 通用配置;新增 LDAP 接入 / 默认镜像源
  - 删除设置页副标题「通用配置、告警通知、LDAP 接入与默认镜像源」(用户要求去掉)
  - 删除 settings 里已不存在的 tabs.notify 文案
    (注意 settings.notify.* 这些 key 仍被告警页的 NotifySection 用着,保留)

=== 3. 告警通知已不在系统设置里 ===
它在上一轮就搬到了「日志告警 → 通知通道」tab,
本次顺带把系统设置里遗留的 tab 文案一并清掉,避免出现"看似有两处配置入口"。

=== 实测 ===
  POST /api/v1/logs/histogram → 200(修复前 404)
  go test ./... 全绿(含新增的 routers 包)
  tsc -b --force 通过
This commit is contained in:
opsx committed 2026-10-28 23:12:42 +00:00
1 parent 8bea331183
commit 6d6e17745c
11 files changed
+150 -54

No files matched your search

+5
View File
@@ -92,6 +92,11 @@ func SetupRouter() *gin.Engine {
logs.GET("/labels", logsCtrl.Labels)
logs.GET("/label/:name/values", logsCtrl.LabelValues)
logs.GET("/my-scope", logsCtrl.MyScope)
// ⚠️ 必须是 POST:前端 logsApi.histogram 用 POST 传 body(sourceId/query/start/end)。
// 曾被误改成 GET,导致 POST /logs/histogram 全部 404,
// 表现为日志查询页直方图不显示、分析页直接报 "API endpoint not found"。
// 同时保留 GET 作为简单调试入口(参数放 query string)。
logs.POST("/histogram", logsCtrl.Histogram)
logs.GET("/histogram", logsCtrl.Histogram)
logs.GET("/daily-volume", logsCtrl.DailyVolume)
// 总量统计(index/stats):只读索引,用于算总页数
+80
View File
@@ -0,0 +1,80 @@
package routers
import (
"testing"
"github.com/gin-gonic/gin"
)
// 回归:接口的 **HTTP 方法与路径** 必须和前端调用一致。
//
// 线上事故:一次模糊匹配的 patch 把
// logs.POST("/histogram", ...) → logs.GET("/histogram", ...)
// 前端 logsApi.histogram 用的是 POST(body 里带 sourceId/query/start/end),
// 于是所有直方图请求 404:
// - 日志查询页:直方图不显示
// - 日志分析页:直接报 "API endpoint not found",整页不可用
// 这类"方法写错"编译期完全查不出来,只能靠路由表断言。
func TestCriticalRoutes(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := SetupRouter()
// 期望的 method + path(注意 gin 会把 :param 保留为 :id)
expect := []struct{ method, path string }{
// 日志查询 / 分析页的数据来源:必须是 POST(前端传 body)
{"POST", "/api/v1/logs/histogram"},
{"POST", "/api/v1/logs/stats"},
{"POST", "/api/v1/logs/metrics"},
{"POST", "/api/v1/logs/query"},
{"POST", "/api/v1/logs/context"},
// 查询辅助接口:GET + query string
{"GET", "/api/v1/logs/sources"},
{"GET", "/api/v1/logs/labels"},
{"GET", "/api/v1/logs/label/:name/values"},
{"GET", "/api/v1/logs/my-scope"},
{"GET", "/api/v1/logs/daily-volume"},
{"GET", "/api/v1/logs/health"},
// 告警
{"GET", "/api/v1/alerts/rules"},
{"POST", "/api/v1/alerts/rules"},
{"GET", "/api/v1/alerts/channels"},
{"POST", "/api/v1/alerts/check"},
{"GET", "/api/v1/alerts/deliveries"},
// 审计 / 自检 / 备份
{"GET", "/api/v1/audit-logs"},
{"GET", "/api/v1/system/self-check"},
{"GET", "/api/v1/system/backup"},
}
registered := map[string]bool{}
for _, route := range engine.Routes() {
registered[route.Method+" "+route.Path] = true
}
for _, item := range expect {
key := item.method + " " + item.path
if !registered[key] {
t.Errorf("缺少路由: %s(前端按这个方法和路径调用,缺了就是 404)", key)
}
}
}
// 回归:前端用 POST 的接口不能只有 GET 版本。
// 单独断言一次,避免以后又有人"顺手"把 POST 改成 GET。
func TestHistogramAcceptsPost(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := SetupRouter()
methods := map[string]bool{}
for _, route := range engine.Routes() {
if route.Path == "/api/v1/logs/histogram" {
methods[route.Method] = true
}
}
if !methods["POST"] {
t.Fatal("POST /api/v1/logs/histogram 未注册:前端 logsApi.histogram 用 POST,会 404")
}
if !methods["GET"] {
t.Fatal("GET /api/v1/logs/histogram 未注册:curl 调试入口丢了")
}
}