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 } },
];