From 6d6e17745cf11b32ec8ab2c606f6f1543bf37839 Mon Sep 17 00:00:00 2001 From: opsx Date: Wed, 28 Oct 2026 23:12:42 +0000 Subject: [PATCH] =?UTF-8?q?fix(logs):=20=E6=81=A2=E5=A4=8D=20POST=20/logs/?= =?UTF-8?q?histogram=EF=BC=88=E7=9B=B4=E6=96=B9=E5=9B=BE=20404=EF=BC=89+?= =?UTF-8?q?=20=E7=B3=BB=E7=BB=9F=E8=AE=BE=E7=BD=AE=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E4=BE=A7=E6=A0=8F=E4=BA=94=E5=88=86=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit === 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 通过 --- backend/routers/router.go | 5 ++ backend/routers/router_test.go | 80 ++++++++++++++++++++++++ frontend/src/components/Layout.tsx | 12 ---- frontend/src/i18n/locales/en/common.ts | 6 +- frontend/src/i18n/locales/en/settings.ts | 4 +- frontend/src/i18n/locales/zh/common.ts | 6 +- frontend/src/i18n/locales/zh/settings.ts | 4 +- frontend/src/pages/settings/index.tsx | 32 ++-------- frontend/src/pages/settings/ldap.tsx | 16 +++++ frontend/src/pages/settings/registry.tsx | 16 +++++ frontend/src/router/routes.tsx | 23 +++++-- 11 files changed, 150 insertions(+), 54 deletions(-) create mode 100644 backend/routers/router_test.go create mode 100644 frontend/src/pages/settings/ldap.tsx create mode 100644 frontend/src/pages/settings/registry.tsx diff --git a/backend/routers/router.go b/backend/routers/router.go index 5c3b5fb..2585696 100644 --- a/backend/routers/router.go +++ b/backend/routers/router.go @@ -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):只读索引,用于算总页数 diff --git a/backend/routers/router_test.go b/backend/routers/router_test.go new file mode 100644 index 0000000..1ef4864 --- /dev/null +++ b/backend/routers/router_test.go @@ -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 调试入口丢了") + } +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 91164be..ed89bab 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -16,7 +16,6 @@ import { Menu, Sun, Moon, - Settings, } from 'lucide-react' import { useTheme } from 'next-themes' import { useTranslation } from 'react-i18next' @@ -238,17 +237,6 @@ const Layout = memo(function Layout({ children }: { children: React.ReactNode })
- {/* 系统设置入口:低频平台配置,放右上角比占侧栏一位更合适 */} - {user?.role === 'admin' && ( - - - - )} {user && (
diff --git a/frontend/src/i18n/locales/en/common.ts b/frontend/src/i18n/locales/en/common.ts index 3b93a2d..5a1ce1c 100644 --- a/frontend/src/i18n/locales/en/common.ts +++ b/frontend/src/i18n/locales/en/common.ts @@ -5,7 +5,7 @@ export default { groups: { resources: 'Resources', services: 'Log Service', - system: 'Users & Audit', + system: 'System Settings', }, dashboard: 'Dashboard', wizard: 'Deploy Wizard', @@ -17,7 +17,9 @@ export default { logs: 'Log Query', alerts: 'Alerts', analysis: 'Log Analysis', - settings: 'Settings', + settings: 'General', + ldap: 'LDAP', + registry: 'Image Registry', users: 'Users', audit: 'Audit Log', login: 'Sign In', diff --git a/frontend/src/i18n/locales/en/settings.ts b/frontend/src/i18n/locales/en/settings.ts index 9eecc40..6eb0553 100644 --- a/frontend/src/i18n/locales/en/settings.ts +++ b/frontend/src/i18n/locales/en/settings.ts @@ -1,11 +1,9 @@ // settings namespace — Settings page (general/password/about, notifications, LDAP, image registry), English copy. // Note: theme & language card titles and ThemeSection copy are owned elsewhere and are not in this dictionary. export default { - title: 'Settings', - description: 'General settings, alert notifications, LDAP integration, and the default image registry', + title: 'General', tabs: { general: 'General', - notify: 'Alert Notifications', ldap: 'LDAP Integration', registry: 'Default Image Registry', }, diff --git a/frontend/src/i18n/locales/zh/common.ts b/frontend/src/i18n/locales/zh/common.ts index 31a5d83..e0981b0 100644 --- a/frontend/src/i18n/locales/zh/common.ts +++ b/frontend/src/i18n/locales/zh/common.ts @@ -5,7 +5,7 @@ export default { groups: { resources: '资源接入', services: '日志服务', - system: '用户与审计', + system: '系统设置', }, dashboard: '仪表盘', wizard: '部署向导', @@ -17,7 +17,9 @@ export default { logs: '日志查询', alerts: '日志告警', analysis: '日志分析', - settings: '系统设置', + settings: '通用配置', + ldap: 'LDAP 接入', + registry: '默认镜像源', users: '用户管理', audit: '审计日志', login: '登录', diff --git a/frontend/src/i18n/locales/zh/settings.ts b/frontend/src/i18n/locales/zh/settings.ts index 418c732..ce36e52 100644 --- a/frontend/src/i18n/locales/zh/settings.ts +++ b/frontend/src/i18n/locales/zh/settings.ts @@ -1,11 +1,9 @@ // settings 命名空间 — 系统设置页(通用配置/密码/关于、告警通知、LDAP 接入、镜像源)中文文案 // 注意:主题卡片与语言卡片的标题、ThemeSection 内文案由其他模块负责,不在本词典内。 export default { - title: '系统设置', - description: '通用配置、告警通知、LDAP 接入与默认镜像源', + title: '通用配置', tabs: { general: '通用配置', - notify: '告警通知', ldap: 'LDAP 接入', registry: '默认镜像源', }, diff --git a/frontend/src/pages/settings/index.tsx b/frontend/src/pages/settings/index.tsx index ab21688..1dea3c1 100644 --- a/frontend/src/pages/settings/index.tsx +++ b/frontend/src/pages/settings/index.tsx @@ -10,12 +10,9 @@ import { authApi } from '@/api' import { clearAuth } from '@/lib/auth-storage' import { toast } from 'sonner' import { PageContainer } from '@/components/PageContainer' -import { LdapSection } from './components/LdapSection' import { ThemeSection } from './components/ThemeSection' import { LanguageSection } from '@/i18n/LanguageSection' import { Palette } from 'lucide-react' -import { ImageRegistrySection } from './components/ImageRegistrySection' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' // 构建指纹由 vite define 注入;本地 tsc/预览时回退 const APP_VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : 'dev' @@ -70,16 +67,10 @@ const SettingsPage = memo(function SettingsPage() { ) return ( - - - - {t('tabs.general')} - {t('tabs.notify')} - {t('tabs.ldap')} - {t('tabs.registry')} - - - + + {/* 不再有内层 tab:LDAP / 镜像源 已拆成独立路由, + 由左侧「系统设置」分组的顶层 tab 切换(单层 tab 更好理解)。 */} +
{/* 外观与语言(紧凑单卡:主题分段器 + 语言分段器) */} @@ -176,20 +167,7 @@ const SettingsPage = memo(function SettingsPage() {
-
- - - {/* 告警通知通道已移到「日志告警 → 通知通道」,避免配置入口分裂 */} - - - - - - - - - -
+
) }) diff --git a/frontend/src/pages/settings/ldap.tsx b/frontend/src/pages/settings/ldap.tsx new file mode 100644 index 0000000..8461b5f --- /dev/null +++ b/frontend/src/pages/settings/ldap.tsx @@ -0,0 +1,16 @@ +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import { PageContainer } from '@/components/PageContainer' +import { LdapSection } from './components/LdapSection' + +/** LDAP 接入:从系统设置里拆出来,作为侧栏「系统设置」分组的一个 tab */ +export const LdapPage = memo(function LdapPage() { + const { t } = useTranslation('settings') + return ( + + + + ) +}) + +export default LdapPage diff --git a/frontend/src/pages/settings/registry.tsx b/frontend/src/pages/settings/registry.tsx new file mode 100644 index 0000000..69e945c --- /dev/null +++ b/frontend/src/pages/settings/registry.tsx @@ -0,0 +1,16 @@ +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import { PageContainer } from '@/components/PageContainer' +import { ImageRegistrySection } from './components/ImageRegistrySection' + +/** 默认镜像源:从系统设置里拆出来,作为侧栏「系统设置」分组的一个 tab */ +export const RegistryPage = memo(function RegistryPage() { + const { t } = useTranslation('settings') + return ( + + + + ) +}) + +export default RegistryPage diff --git a/frontend/src/router/routes.tsx b/frontend/src/router/routes.tsx index 7315f06..8233d87 100644 --- a/frontend/src/router/routes.tsx +++ b/frontend/src/router/routes.tsx @@ -17,6 +17,8 @@ import { BarChart3, History, Boxes, + ShieldCheck, + Container, } from 'lucide-react'; import type { AppRoute, MenuItem, RouteMeta } from '@/types/router'; @@ -28,6 +30,8 @@ const Storage = lazy(() => import('@/pages/storage')); const Deployments = lazy(() => import('@/pages/deployments')); const Collectors = lazy(() => import('@/pages/collectors')); const SettingsPage = lazy(() => import('@/pages/settings')); +const LdapPage = lazy(() => import('@/pages/settings/ldap')); +const RegistryPage = lazy(() => import('@/pages/settings/registry')); const NotFound = lazy(() => import('@/pages/not-found')); const WizardPage = lazy(() => import('@/pages/wizard')); const LogsPage = lazy(() => import('@/pages/logs')); @@ -76,14 +80,23 @@ export const routes: AppRoute[] = [ { path: '/users', component: UsersPage, auth: true, meta: { - title: 'menu.users', icon: UsersRound, order: 8, adminOnly: true, + // 系统设置(tab 起点)/ 用户管理 / 审计日志 同属「系统设置」分组, + // 侧栏只占一个入口,页面内用 tab 切换。 + title: 'menu.users', icon: UsersRound, order: 9, adminOnly: true, + groupKey: 'system', + }, + }, + { path: '/audit', component: AuditPage, auth: true, meta: { title: 'menu.audit', icon: History, order: 10, adminOnly: true, groupKey: 'system' } }, + { path: '/settings/ldap', component: LdapPage, auth: true, meta: { title: 'menu.ldap', icon: ShieldCheck, order: 11, adminOnly: true, groupKey: 'system' } }, + { path: '/settings/registry', component: RegistryPage, auth: true, meta: { title: 'menu.registry', icon: Container, order: 12, adminOnly: true, groupKey: 'system' } }, + // 系统设置放回侧栏,并作为「系统设置」分组的 tab 起点(order 最小 = 排在第一个) + { + path: '/settings', component: SettingsPage, auth: true, + meta: { + title: 'menu.settings', icon: Settings, order: 8, adminOnly: true, group: { key: 'system', label: 'menu.groups.system', icon: Settings, order: 8 }, }, }, - { path: '/audit', component: AuditPage, auth: true, meta: { title: 'menu.audit', icon: History, order: 13, adminOnly: true, groupKey: 'system' } }, - // 系统设置从侧栏移到右上角齿轮入口:属于低频平台配置, - // 和「用户/审计」这类日常安全管理不是一类东西,混在一个分组里太乱。 - { path: '/settings', component: SettingsPage, auth: true, meta: { title: 'menu.settings', icon: Settings, order: 12, adminOnly: true, hideInMenu: true } }, { path: '*', component: NotFound, auth: false, meta: { title: 'menu.notFound', hideInMenu: true } }, ];