@allbluecn/admin-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/package.json +86 -0
  2. package/src/client.tsx +31 -0
  3. package/src/components/admin/admin-user-menu.tsx +90 -0
  4. package/src/components/admin/developer-log.tsx +356 -0
  5. package/src/components/ai-provider/model-list-editor.tsx +147 -0
  6. package/src/components/ai-provider/provider-form.tsx +434 -0
  7. package/src/components/ai-provider/provider-icon.tsx +43 -0
  8. package/src/components/ai-provider/provider-logo.tsx +87 -0
  9. package/src/components/ai-provider/provider-table.tsx +258 -0
  10. package/src/components/auth/admin-login-form.tsx +214 -0
  11. package/src/components/auth/auth-brand-panel.tsx +60 -0
  12. package/src/components/auth/auth-header.tsx +38 -0
  13. package/src/components/auth/auth-layout.tsx +66 -0
  14. package/src/components/auth/auth-providers.tsx +34 -0
  15. package/src/components/auth/icon-input.tsx +46 -0
  16. package/src/components/auth/oauth-section.tsx +51 -0
  17. package/src/components/auth/password-input.tsx +67 -0
  18. package/src/components/common/page-header.tsx +42 -0
  19. package/src/components/common/placeholder-page.tsx +42 -0
  20. package/src/components/component/component-form-dialog.tsx +348 -0
  21. package/src/components/component/component-import-dialog.tsx +235 -0
  22. package/src/components/component/component-table.tsx +153 -0
  23. package/src/components/data-table/data-table-pagination.tsx +114 -0
  24. package/src/components/data-table/data-table.tsx +218 -0
  25. package/src/components/data-table/table-detail-layout.tsx +99 -0
  26. package/src/components/dependency/changelog-dialog.tsx +108 -0
  27. package/src/components/dependency/columns.tsx +232 -0
  28. package/src/components/dependency/dependency-table.tsx +368 -0
  29. package/src/components/dependency/edit-dialog.tsx +200 -0
  30. package/src/components/knowledge-public/bookmark-form-dialog.tsx +215 -0
  31. package/src/components/knowledge-public/compliance-form-dialog.tsx +308 -0
  32. package/src/components/knowledge-public/legal-form-dialog.tsx +315 -0
  33. package/src/components/layout/sidebar-02/app-sidebar.tsx +160 -0
  34. package/src/components/layout/sidebar-02/index.tsx +33 -0
  35. package/src/components/layout/sidebar-02/logo.tsx +40 -0
  36. package/src/components/layout/sidebar-02/nav-main.tsx +153 -0
  37. package/src/components/layout/sidebar-02/nav-notifications.tsx +78 -0
  38. package/src/components/layout/sidebar-02/team-switcher.tsx +106 -0
  39. package/src/components/magicui/animated-theme-toggler.tsx +272 -0
  40. package/src/components/magicui/rainbow-button.tsx +78 -0
  41. package/src/components/market/columns.tsx +151 -0
  42. package/src/components/market/market-app-form.tsx +237 -0
  43. package/src/components/market/market-app-list.tsx +156 -0
  44. package/src/components/monitoring/error-list.tsx +143 -0
  45. package/src/components/monitoring/health-cards.tsx +152 -0
  46. package/src/components/monitoring/metrics-stats.tsx +184 -0
  47. package/src/components/monitoring/request-log-table.tsx +179 -0
  48. package/src/components/providers/theme-toggle.tsx +59 -0
  49. package/src/components/settings/avatar/avatar-config.ts +46 -0
  50. package/src/components/ui/alert-dialog.tsx +216 -0
  51. package/src/components/ui/alert.tsx +76 -0
  52. package/src/components/ui/avatar.tsx +129 -0
  53. package/src/components/ui/badge.tsx +66 -0
  54. package/src/components/ui/button.tsx +84 -0
  55. package/src/components/ui/card.tsx +120 -0
  56. package/src/components/ui/checkbox.tsx +50 -0
  57. package/src/components/ui/collapsible.tsx +50 -0
  58. package/src/components/ui/dialog.tsx +185 -0
  59. package/src/components/ui/dropdown-menu.tsx +286 -0
  60. package/src/components/ui/field.tsx +255 -0
  61. package/src/components/ui/input-group.tsx +171 -0
  62. package/src/components/ui/input.tsx +36 -0
  63. package/src/components/ui/label.tsx +41 -0
  64. package/src/components/ui/pagination.tsx +146 -0
  65. package/src/components/ui/select.tsx +209 -0
  66. package/src/components/ui/separator.tsx +45 -0
  67. package/src/components/ui/sheet.tsx +164 -0
  68. package/src/components/ui/sidebar.tsx +719 -0
  69. package/src/components/ui/skeleton.tsx +30 -0
  70. package/src/components/ui/sonner.tsx +37 -0
  71. package/src/components/ui/switch.tsx +48 -0
  72. package/src/components/ui/table.tsx +133 -0
  73. package/src/components/ui/textarea.tsx +35 -0
  74. package/src/components/ui/tooltip.tsx +74 -0
  75. package/src/components/user/columns.tsx +153 -0
  76. package/src/components/user/settings-activity-log.tsx +358 -0
  77. package/src/components/user/settings-profile/constants.ts +42 -0
  78. package/src/components/user/settings-profile/types.ts +47 -0
  79. package/src/components/user/settings-profile.tsx +664 -0
  80. package/src/components/user/user-create-dialog.tsx +137 -0
  81. package/src/components/user/user-filters.tsx +117 -0
  82. package/src/hooks/use-mobile.ts +36 -0
  83. package/src/instrument.client.ts +34 -0
  84. package/src/lib/anyhub/capabilities.ts +59 -0
  85. package/src/lib/anyhub/types.ts +43 -0
  86. package/src/lib/auth-client.ts +83 -0
  87. package/src/lib/auth-utils.ts +24 -0
  88. package/src/lib/auth.config.ts +264 -0
  89. package/src/lib/avatar.ts +41 -0
  90. package/src/lib/i18n/client.ts +32 -0
  91. package/src/lib/i18n/config.ts +40 -0
  92. package/src/lib/i18n/detect.ts +43 -0
  93. package/src/lib/i18n/locales/en/admin-ai.json +128 -0
  94. package/src/lib/i18n/locales/en/admin-billing.json +56 -0
  95. package/src/lib/i18n/locales/en/admin-users.json +88 -0
  96. package/src/lib/i18n/locales/en/common.json +877 -0
  97. package/src/lib/i18n/locales/en/enterprise.json +208 -0
  98. package/src/lib/i18n/locales/zh/admin-ai.json +128 -0
  99. package/src/lib/i18n/locales/zh/admin-billing.json +56 -0
  100. package/src/lib/i18n/locales/zh/admin-users.json +88 -0
  101. package/src/lib/i18n/locales/zh/common.json +877 -0
  102. package/src/lib/i18n/locales/zh/enterprise.json +208 -0
  103. package/src/lib/i18n/server.ts +18 -0
  104. package/src/lib/i18n/types.ts +5 -0
  105. package/src/lib/monitoring-benchmarks.ts +85 -0
  106. package/src/lib/monitoring-descriptions.ts +78 -0
  107. package/src/lib/prisma.ts +18 -0
  108. package/src/lib/router-context.ts +38 -0
  109. package/src/lib/session.ts +38 -0
  110. package/src/lib/utils.ts +23 -0
  111. package/src/providers/index.tsx +45 -0
  112. package/src/routeTree.gen.ts +1199 -0
  113. package/src/router.tsx +51 -0
  114. package/src/routes/__root.tsx +170 -0
  115. package/src/routes/_nav/ai-providers/$id/edit/route.lazy.tsx +74 -0
  116. package/src/routes/_nav/ai-providers/$id/edit/route.tsx +35 -0
  117. package/src/routes/_nav/ai-providers/index/route.lazy.tsx +33 -0
  118. package/src/routes/_nav/ai-providers/new/route.lazy.tsx +31 -0
  119. package/src/routes/_nav/ai-providers/new/route.tsx +20 -0
  120. package/src/routes/_nav/ai-providers/route.lazy.tsx +26 -0
  121. package/src/routes/_nav/ai-providers/route.tsx +31 -0
  122. package/src/routes/_nav/apps/route.lazy.tsx +30 -0
  123. package/src/routes/_nav/apps/route.tsx +20 -0
  124. package/src/routes/_nav/audit/route.lazy.tsx +100 -0
  125. package/src/routes/_nav/audit/route.tsx +25 -0
  126. package/src/routes/_nav/billing/invoices/route.lazy.tsx +86 -0
  127. package/src/routes/_nav/billing/invoices/route.tsx +25 -0
  128. package/src/routes/_nav/billing/route.lazy.tsx +48 -0
  129. package/src/routes/_nav/billing/route.tsx +27 -0
  130. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +160 -0
  131. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.tsx +25 -0
  132. package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +113 -0
  133. package/src/routes/_nav/billing/subscriptions/route.tsx +28 -0
  134. package/src/routes/_nav/compliance/route.lazy.tsx +184 -0
  135. package/src/routes/_nav/compliance/route.tsx +25 -0
  136. package/src/routes/_nav/components/route.lazy.tsx +184 -0
  137. package/src/routes/_nav/components/route.tsx +40 -0
  138. package/src/routes/_nav/dashboard/route.lazy.tsx +33 -0
  139. package/src/routes/_nav/dashboard/route.tsx +20 -0
  140. package/src/routes/_nav/dependencies/route.lazy.tsx +66 -0
  141. package/src/routes/_nav/dependencies/route.tsx +32 -0
  142. package/src/routes/_nav/developer-logs/route.lazy.tsx +6 -0
  143. package/src/routes/_nav/developer-logs/route.tsx +14 -0
  144. package/src/routes/_nav/docs/route.lazy.tsx +30 -0
  145. package/src/routes/_nav/docs/route.tsx +20 -0
  146. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +355 -0
  147. package/src/routes/_nav/knowledge-public/bookmarks/route.tsx +40 -0
  148. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +245 -0
  149. package/src/routes/_nav/knowledge-public/compliance/route.tsx +40 -0
  150. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +242 -0
  151. package/src/routes/_nav/knowledge-public/legal/route.tsx +40 -0
  152. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +235 -0
  153. package/src/routes/_nav/licenses/$licenseId/route.tsx +25 -0
  154. package/src/routes/_nav/licenses/new/route.lazy.tsx +237 -0
  155. package/src/routes/_nav/licenses/route.lazy.tsx +143 -0
  156. package/src/routes/_nav/licenses/route.tsx +25 -0
  157. package/src/routes/_nav/market/route.lazy.tsx +31 -0
  158. package/src/routes/_nav/market/route.tsx +20 -0
  159. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +305 -0
  160. package/src/routes/_nav/monitoring/ai-usage/route.tsx +3 -0
  161. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +111 -0
  162. package/src/routes/_nav/monitoring/health/route.lazy.tsx +260 -0
  163. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +165 -0
  164. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +240 -0
  165. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +138 -0
  166. package/src/routes/_nav/monitoring/route.lazy.tsx +68 -0
  167. package/src/routes/_nav/monitoring/route.tsx +20 -0
  168. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +399 -0
  169. package/src/routes/_nav/monitoring/sentry/route.tsx +20 -0
  170. package/src/routes/_nav/permissions/route.lazy.tsx +504 -0
  171. package/src/routes/_nav/permissions/route.tsx +26 -0
  172. package/src/routes/_nav/route.lazy.tsx +31 -0
  173. package/src/routes/_nav/route.tsx +34 -0
  174. package/src/routes/_nav/settings/route.lazy.tsx +30 -0
  175. package/src/routes/_nav/settings/route.tsx +20 -0
  176. package/src/routes/_nav/sso/route.lazy.tsx +145 -0
  177. package/src/routes/_nav/sso/route.tsx +25 -0
  178. package/src/routes/_nav/users/$userId/route.lazy.tsx +432 -0
  179. package/src/routes/_nav/users/$userId/route.tsx +27 -0
  180. package/src/routes/_nav/users/-components/users-table.tsx +38 -0
  181. package/src/routes/_nav/users/route.lazy.tsx +74 -0
  182. package/src/routes/_nav/users/route.tsx +43 -0
  183. package/src/routes/api/compliance-download/$jobId.ts +35 -0
  184. package/src/routes/api/observability/errors.ts +41 -0
  185. package/src/routes/api/observability/health.ts +30 -0
  186. package/src/routes/api.auth.$.ts +31 -0
  187. package/src/routes/api.sentry-test.ts +33 -0
  188. package/src/routes/index/route.lazy.tsx +22 -0
  189. package/src/routes/index/route.tsx +24 -0
  190. package/src/routes/login/route.lazy.tsx +46 -0
  191. package/src/routes/login/route.tsx +28 -0
  192. package/src/routes/sentry-tunnel.ts +61 -0
  193. package/src/routes/setup/route.lazy.tsx +94 -0
  194. package/src/routes/setup/route.tsx +28 -0
  195. package/src/server/__tests__/admin-ai-alerts.test.ts +113 -0
  196. package/src/server/__tests__/licenses-issue.test.ts +98 -0
  197. package/src/server/admin-ai-alerts.ts +177 -0
  198. package/src/server/admin-ai-usage.ts +134 -0
  199. package/src/server/auth-helpers.ts +51 -0
  200. package/src/server/entitlements.ts +24 -0
  201. package/src/server/knowledge-public/canonical-id.ts +55 -0
  202. package/src/server/knowledge-public/github-sync.ts +86 -0
  203. package/src/server/license-issue.ts +78 -0
  204. package/src/server/middlewares/request.ts +26 -0
  205. package/src/server/observability/debug-middleware.ts +39 -0
  206. package/src/server/observability/error-reporter.ts +99 -0
  207. package/src/server/observability/health-check.ts +140 -0
  208. package/src/server/observability/index.ts +33 -0
  209. package/src/server/observability/logger.ts +49 -0
  210. package/src/server/observability/metrics.ts +105 -0
  211. package/src/server/observability/request-middleware.ts +125 -0
  212. package/src/server/observability/timing.ts +42 -0
  213. package/src/server/pro-services.ts +32 -0
  214. package/src/server/serverFns/admin-billing.ts +94 -0
  215. package/src/server/serverFns/ai-provider/create.ts +92 -0
  216. package/src/server/serverFns/ai-provider/delete.ts +42 -0
  217. package/src/server/serverFns/ai-provider/get.ts +35 -0
  218. package/src/server/serverFns/ai-provider/list.ts +92 -0
  219. package/src/server/serverFns/ai-provider/update.ts +73 -0
  220. package/src/server/serverFns/anyhub/market.ts +64 -0
  221. package/src/server/serverFns/audit.ts +57 -0
  222. package/src/server/serverFns/auth/session.ts +26 -0
  223. package/src/server/serverFns/auth/setup.ts +73 -0
  224. package/src/server/serverFns/compliance.ts +85 -0
  225. package/src/server/serverFns/components/import.ts +197 -0
  226. package/src/server/serverFns/components/list.ts +51 -0
  227. package/src/server/serverFns/components/mutations.ts +84 -0
  228. package/src/server/serverFns/dependency/helpers.ts +245 -0
  229. package/src/server/serverFns/dependency/list.ts +127 -0
  230. package/src/server/serverFns/dependency/sync.ts +123 -0
  231. package/src/server/serverFns/dependency/update.ts +51 -0
  232. package/src/server/serverFns/developer-log-auto-generate.ts +176 -0
  233. package/src/server/serverFns/developer-logs.ts +132 -0
  234. package/src/server/serverFns/knowledge-public/bookmarks/create.ts +53 -0
  235. package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +31 -0
  236. package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +32 -0
  237. package/src/server/serverFns/knowledge-public/bookmarks/list.ts +66 -0
  238. package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +56 -0
  239. package/src/server/serverFns/knowledge-public/bookmarks/update.ts +50 -0
  240. package/src/server/serverFns/knowledge-public/compliance/create.ts +58 -0
  241. package/src/server/serverFns/knowledge-public/compliance/delete.ts +31 -0
  242. package/src/server/serverFns/knowledge-public/compliance/detail.ts +32 -0
  243. package/src/server/serverFns/knowledge-public/compliance/list.ts +59 -0
  244. package/src/server/serverFns/knowledge-public/compliance/update.ts +53 -0
  245. package/src/server/serverFns/knowledge-public/legal/create.ts +62 -0
  246. package/src/server/serverFns/knowledge-public/legal/delete.ts +31 -0
  247. package/src/server/serverFns/knowledge-public/legal/detail.ts +32 -0
  248. package/src/server/serverFns/knowledge-public/legal/list.ts +60 -0
  249. package/src/server/serverFns/knowledge-public/legal/update.ts +55 -0
  250. package/src/server/serverFns/licenses.ts +92 -0
  251. package/src/server/serverFns/locale.ts +10 -0
  252. package/src/server/serverFns/monitoring/check-ai-alerts.ts +13 -0
  253. package/src/server/serverFns/monitoring/get-ai-usage-overview.ts +25 -0
  254. package/src/server/serverFns/monitoring/get-errors.ts +43 -0
  255. package/src/server/serverFns/monitoring/get-health-settings.ts +35 -0
  256. package/src/server/serverFns/monitoring/get-health.ts +55 -0
  257. package/src/server/serverFns/monitoring/get-metrics.ts +46 -0
  258. package/src/server/serverFns/monitoring/get-request-logs.ts +66 -0
  259. package/src/server/serverFns/monitoring/report-error.ts +55 -0
  260. package/src/server/serverFns/monitoring/reset.ts +70 -0
  261. package/src/server/serverFns/permissions.ts +214 -0
  262. package/src/server/serverFns/sentry/config.ts +88 -0
  263. package/src/server/serverFns/sso-config.ts +83 -0
  264. package/src/server/serverFns/user/create.ts +40 -0
  265. package/src/server/serverFns/user/delete.ts +29 -0
  266. package/src/server/serverFns/user/detail.ts +45 -0
  267. package/src/server/serverFns/user/list.ts +56 -0
  268. package/src/server/serverFns/user/reset-password.ts +28 -0
  269. package/src/server/serverFns/user/update-role.ts +26 -0
  270. package/src/server/serverFns/user/update-status.ts +26 -0
  271. package/src/server.ts +27 -0
  272. package/src/start.ts +85 -0
  273. package/src/styles/_keyframe-animations.scss +91 -0
  274. package/src/styles/_variables.scss +296 -0
  275. package/src/styles/globals.css +200 -0
  276. package/src/types/start-authjs.d.ts +27 -0
@@ -0,0 +1,165 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createLazyFileRoute } from '@tanstack/react-router'
19
+ import { useState, useEffect } from 'react'
20
+ import { useTranslation } from 'react-i18next'
21
+ import { Activity, Filter, RefreshCw, Trash2 } from 'lucide-react'
22
+ import { PageHeader } from '@/components/common/page-header'
23
+ import { RequestLogTable } from '@/components/monitoring/request-log-table'
24
+ import { Checkbox } from '@/components/ui/checkbox'
25
+ import { getRequestLogs } from '@/server/serverFns/monitoring/get-request-logs'
26
+ import { resetMonitoring } from '@/server/serverFns/monitoring/reset'
27
+ import { DataTablePagination } from '@/components/data-table/data-table-pagination'
28
+ import { toast } from 'sonner'
29
+
30
+ export const Route = createLazyFileRoute('/_nav/monitoring/logs')({
31
+ component: MonitoringLogs,
32
+ })
33
+
34
+ function MonitoringLogs() {
35
+ const { t } = useTranslation('common')
36
+ const [loading, setLoading] = useState(true)
37
+ const [logs, setLogs] = useState<any[]>([])
38
+ const [total, setTotal] = useState(0)
39
+ const [page, setPage] = useState(1)
40
+ const [method, setMethod] = useState('')
41
+ const [status, setStatus] = useState('')
42
+ const [path, setPath] = useState('')
43
+ const [filterMonitoring, setFilterMonitoring] = useState(false)
44
+ const limit = 50
45
+
46
+ const loadLogs = async () => {
47
+ setLoading(true)
48
+ try {
49
+ const result = await getRequestLogs({
50
+ data: {
51
+ limit,
52
+ offset: (page - 1) * limit,
53
+ method: method || undefined,
54
+ status: status ? Number(status) : undefined,
55
+ path: path || undefined,
56
+ excludeMonitoring: filterMonitoring,
57
+ },
58
+ })
59
+ setLogs(result.logs)
60
+ setTotal(result.total)
61
+ } catch {
62
+ // ignore
63
+ }
64
+ setLoading(false)
65
+ }
66
+
67
+ useEffect(() => {
68
+ loadLogs()
69
+ }, [page, method, status, path, filterMonitoring])
70
+
71
+ const handleClearOld = async () => {
72
+ try {
73
+ await resetMonitoring({ data: { type: 'request-logs', days: 7 } })
74
+ toast.success(t('monitoring.logs.clearSuccess'))
75
+ loadLogs()
76
+ } catch {
77
+ toast.error(t('monitoring.logs.clearFailed'))
78
+ }
79
+ }
80
+
81
+ return (
82
+ <div className="flex flex-col flex-1 h-full space-y-4 my-4 overflow-hidden">
83
+ <PageHeader title={t('monitoring.logs.title')} description={t('monitoring.logs.description')} icon={Activity} />
84
+
85
+ <div className="flex items-center gap-3 rounded-lg border bg-card p-3 shrink-0">
86
+ <Filter className="size-4 text-muted-foreground" />
87
+ <select
88
+ value={method}
89
+ onChange={(e) => { setMethod(e.target.value); setPage(1) }}
90
+ className="rounded-md border bg-background px-2 py-1 text-sm"
91
+ >
92
+ <option value="">{t('monitoring.logs.filter.allMethods')}</option>
93
+ <option value="GET">GET</option>
94
+ <option value="POST">POST</option>
95
+ <option value="PUT">PUT</option>
96
+ <option value="DELETE">DELETE</option>
97
+ </select>
98
+ <select
99
+ value={status}
100
+ onChange={(e) => { setStatus(e.target.value); setPage(1) }}
101
+ className="rounded-md border bg-background px-2 py-1 text-sm"
102
+ >
103
+ <option value="">{t('monitoring.logs.filter.allStatus')}</option>
104
+ <option value="200">200</option>
105
+ <option value="400">4xx</option>
106
+ <option value="500">5xx</option>
107
+ </select>
108
+ <input
109
+ type="text"
110
+ value={path}
111
+ onChange={(e) => { setPath(e.target.value); setPage(1) }}
112
+ placeholder={t('monitoring.logs.filter.searchPath')}
113
+ className="flex-1 rounded-md border bg-background px-2 py-1 text-sm outline-none focus:ring-2 focus:ring-ring/50"
114
+ />
115
+ <button
116
+ onClick={loadLogs}
117
+ disabled={loading}
118
+ className="inline-flex items-center gap-1 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50"
119
+ >
120
+ <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />
121
+ {t('monitoring.logs.query')}
122
+ </button>
123
+ <button
124
+ onClick={handleClearOld}
125
+ className="inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-accent transition-colors"
126
+ >
127
+ <Trash2 className="size-4" />
128
+ {t('monitoring.logs.clear7Days')}
129
+ </button>
130
+ <label className="inline-flex items-center gap-1.5 text-sm shrink-0 cursor-pointer">
131
+ <Checkbox
132
+ checked={filterMonitoring}
133
+ onCheckedChange={(checked) => {
134
+ setFilterMonitoring(Boolean(checked))
135
+ setPage(1)
136
+ }}
137
+ />
138
+ {t('monitoring.logs.filterMonitoring')}
139
+ </label>
140
+ </div>
141
+
142
+ {loading ? (
143
+ <div className="flex items-center justify-center h-32 shrink-0">
144
+ <RefreshCw className="size-5 animate-spin text-muted-foreground" />
145
+ </div>
146
+ ) : logs.length > 0 ? (
147
+ <div className="flex-1 min-h-0 flex flex-col">
148
+ <RequestLogTable
149
+ logs={logs}
150
+ actions={
151
+ <DataTablePagination
152
+ page={page}
153
+ totalPages={Math.ceil(total / limit)}
154
+ total={total}
155
+ onPageChange={setPage}
156
+ />
157
+ }
158
+ />
159
+ </div>
160
+ ) : (
161
+ <div className="flex items-center justify-center h-32 text-sm text-muted-foreground shrink-0">{t('monitoring.logs.empty')}</div>
162
+ )}
163
+ </div>
164
+ )
165
+ }
@@ -0,0 +1,240 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { describeMetric } from '@/lib/monitoring-descriptions'
19
+ import { getBenchmark, getBenchmarkSource, compareDuration } from '@/lib/monitoring-benchmarks'
20
+ import { formatTimestamp } from '@allbluecn/web-app/lib/datetime'
21
+ import { createLazyFileRoute } from '@tanstack/react-router'
22
+ import { useState, useEffect } from 'react'
23
+ import { useTranslation } from 'react-i18next'
24
+ import { BarChart3, RefreshCw, Trash2, MemoryStick } from 'lucide-react'
25
+ import { ArrowDown, ArrowUp } from 'lucide-react'
26
+ import { PageHeader } from '@/components/common/page-header'
27
+ import { MetricsStats } from '@/components/monitoring/metrics-stats'
28
+ import { getMetrics } from '@/server/serverFns/monitoring/get-metrics'
29
+ import { resetMonitoring } from '@/server/serverFns/monitoring/reset'
30
+ import { DataTable } from '@/components/data-table/data-table'
31
+ import { toast } from 'sonner'
32
+
33
+ export const Route = createLazyFileRoute('/_nav/monitoring/metrics')({
34
+ component: MonitoringMetrics,
35
+ })
36
+
37
+ function formatBytes(bytes: number): string {
38
+ if (bytes < 1024) return `${bytes}B`
39
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`
40
+ return `${(bytes / 1048576).toFixed(1)}MB`
41
+ }
42
+
43
+ function MonitoringMetrics() {
44
+ const { t } = useTranslation('common')
45
+ const [loading, setLoading] = useState(true)
46
+ const [data, setData] = useState<any>(null)
47
+
48
+ const loadData = async () => {
49
+ setLoading(true)
50
+ try {
51
+ const result = await getMetrics()
52
+ setData(result)
53
+ } catch {
54
+ // ignore
55
+ }
56
+ setLoading(false)
57
+ }
58
+
59
+ useEffect(() => {
60
+ loadData()
61
+ }, [])
62
+
63
+ const handleReset = async () => {
64
+ try {
65
+ await resetMonitoring({ data: { type: 'metrics' } })
66
+ toast.success(t('monitoring.metrics.clearSuccess'))
67
+ loadData()
68
+ } catch {
69
+ toast.error(t('monitoring.metrics.clearFailed'))
70
+ }
71
+ }
72
+
73
+ return (
74
+ <div className="flex flex-col flex-1 h-full space-y-4 my-4 overflow-hidden">
75
+ <div className="flex items-center justify-between">
76
+ <PageHeader title={t('monitoring.metrics.title')} description={t('monitoring.metrics.description')} icon={BarChart3} />
77
+ <div className="flex items-center gap-2">
78
+ <button
79
+ onClick={loadData}
80
+ disabled={loading}
81
+ className="inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-accent transition-colors disabled:opacity-50"
82
+ >
83
+ <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />
84
+ {t('common.refresh')}
85
+ </button>
86
+ <button
87
+ onClick={handleReset}
88
+ className="inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-accent transition-colors"
89
+ >
90
+ <Trash2 className="size-4" />
91
+ {t('monitoring.metrics.clearData')}
92
+ </button>
93
+ </div>
94
+ </div>
95
+
96
+ <div className="rounded-lg border bg-card p-4">
97
+ <div className="flex items-center gap-2 mb-3">
98
+ <MemoryStick className="size-4 text-muted-foreground" />
99
+ <h3 className="text-sm font-medium">{t('monitoring.metrics.memoryUsage')}</h3>
100
+ </div>
101
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
102
+ <div>
103
+ <p className="text-xs text-muted-foreground">RSS</p>
104
+ <p className="text-sm font-medium">{data?.memory?.rss ? formatBytes(data.memory.rss) : '-'}</p>
105
+ </div>
106
+ <div>
107
+ <p className="text-xs text-muted-foreground">Heap Total</p>
108
+ <p className="text-sm font-medium">{data?.memory?.heapTotal ? formatBytes(data.memory.heapTotal) : '-'}</p>
109
+ </div>
110
+ <div>
111
+ <p className="text-xs text-muted-foreground">Heap Used</p>
112
+ <p className="text-sm font-medium">{data?.memory?.heapUsed ? formatBytes(data.memory.heapUsed) : '-'}</p>
113
+ </div>
114
+ <div>
115
+ <p className="text-xs text-muted-foreground">External</p>
116
+ <p className="text-sm font-medium">{data?.memory?.external ? formatBytes(data.memory.external) : '-'}</p>
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ {loading ? (
122
+ <div className="flex items-center justify-center h-32">
123
+ <RefreshCw className="size-5 animate-spin text-muted-foreground" />
124
+ </div>
125
+ ) : data?.current && Object.keys(data.current).length > 0 ? (
126
+ <MetricsStats data={data.current} />
127
+ ) : (
128
+ <div className="flex items-center justify-center h-32 text-sm text-muted-foreground">{t('monitoring.metrics.empty')}</div>
129
+ )}
130
+
131
+ {data?.history?.length > 0 && (
132
+ <div className="rounded-lg border bg-card flex-1 min-h-0 flex flex-col">
133
+ <div className="border-b px-4 py-3 shrink-0">
134
+ <h3 className="text-sm font-medium">{t('monitoring.metrics.history')}</h3>
135
+ </div>
136
+ <div className="p-4 flex-1 min-h-0">
137
+ <MetricsHistoryTable data={data.history} />
138
+ </div>
139
+ </div>
140
+ )}
141
+ </div>
142
+ )
143
+ }
144
+
145
+ interface MetricsHistoryRow {
146
+ metricName: string
147
+ count: number
148
+ avg: number
149
+ p95: number
150
+ p99: number
151
+ recordedAt: string
152
+ }
153
+
154
+ function MetricsBenchmarkCell({ value, metricName }: { value: number; metricName: string }) {
155
+ const path = metricName.replace(/^request\./, '')
156
+ const benchmark = getBenchmark(path)
157
+ const comp = compareDuration(value, benchmark)
158
+ const isGood = value <= benchmark
159
+ const pct = Math.round((Math.abs(value - benchmark) / benchmark) * 100)
160
+
161
+ return (
162
+ <span className="inline-flex items-center gap-1">
163
+ {value.toFixed(1)}ms
164
+ {isGood ? (
165
+ <span
166
+ className="inline-flex items-center gap-0.5 text-green-600 dark:text-green-400"
167
+ title={`${getBenchmarkSource(path)}: ${benchmark}ms P95`}
168
+ >
169
+ <ArrowUp className="size-3" />
170
+ <span className="text-[10px]">{pct}%</span>
171
+ </span>
172
+ ) : (
173
+ <span
174
+ className="inline-flex items-center gap-0.5 text-red-600 dark:text-red-400"
175
+ title={`${getBenchmarkSource(path)}: ${benchmark}ms P95`}
176
+ >
177
+ <ArrowDown className="size-3" strokeWidth={3} />
178
+ <span className="text-[10px] font-bold">{pct}%</span>
179
+ </span>
180
+ )}
181
+ </span>
182
+ )
183
+ }
184
+
185
+ function MetricsHistoryTable({ data }: { data: MetricsHistoryRow[] }) {
186
+ const { t } = useTranslation('common')
187
+ const columns = [
188
+ {
189
+ header: t('monitoring.metrics.columns.time'),
190
+ accessorKey: 'recordedAt',
191
+ meta: { style: { width: 30 } },
192
+ cell: ({ row }: { row: { original: MetricsHistoryRow } }) => (
193
+ <span className="text-xs">
194
+ {formatTimestamp(row.original.recordedAt, 'yyyy-MM-dd HH:mm:ss')}
195
+ </span>
196
+ ),
197
+ },
198
+ {
199
+ header: t('monitoring.metrics.columns.metricName'),
200
+ accessorKey: 'metricName',
201
+ meta: { style: { width: 150 } },
202
+ cell: ({ row }: { row: { original: MetricsHistoryRow } }) => (
203
+ <span className="font-mono text-xs block truncate">
204
+ {row.original.metricName}
205
+ </span>
206
+ ),
207
+ },
208
+ {
209
+ header: t('monitoring.metrics.columns.count'),
210
+ accessorKey: 'count',
211
+ meta: { style: { width: 15, textAlign: 'right' } },
212
+ },
213
+ {
214
+ header: t('monitoring.metrics.columns.avg'),
215
+ accessorKey: 'avg',
216
+ meta: { style: { width: 30, textAlign: 'right' } },
217
+ cell: ({ row }: { row: { original: MetricsHistoryRow } }) => (
218
+ <MetricsBenchmarkCell value={row.original.avg} metricName={row.original.metricName} />
219
+ ),
220
+ },
221
+ {
222
+ header: 'P95',
223
+ accessorKey: 'p95',
224
+ meta: { style: { width: 30, textAlign: 'right' } },
225
+ cell: ({ row }: { row: { original: MetricsHistoryRow } }) => (
226
+ <MetricsBenchmarkCell value={row.original.p95} metricName={row.original.metricName} />
227
+ ),
228
+ },
229
+ {
230
+ header: 'P99',
231
+ accessorKey: 'p99',
232
+ meta: { style: { width: 30, textAlign: 'right' } },
233
+ cell: ({ row }: { row: { original: MetricsHistoryRow } }) => (
234
+ <MetricsBenchmarkCell value={row.original.p99} metricName={row.original.metricName} />
235
+ ),
236
+ },
237
+ ]
238
+
239
+ return <DataTable columns={columns as any} data={data} />
240
+ }
@@ -0,0 +1,138 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createLazyFileRoute } from '@tanstack/react-router'
19
+ import { useState, useEffect } from 'react'
20
+ import { useTranslation } from 'react-i18next'
21
+ import { Activity, RefreshCw } from 'lucide-react'
22
+ import { PageHeader } from '@/components/common/page-header'
23
+ import { HealthCards } from '@/components/monitoring/health-cards'
24
+ import { ErrorList } from '@/components/monitoring/error-list'
25
+ import { MetricsStats } from '@/components/monitoring/metrics-stats'
26
+ import { getHealthStatus } from '@/server/serverFns/monitoring/get-health'
27
+ import { getErrors } from '@/server/serverFns/monitoring/get-errors'
28
+ import { getMetrics } from '@/server/serverFns/monitoring/get-metrics'
29
+
30
+ export const Route = createLazyFileRoute('/_nav/monitoring/overview')({
31
+ component: MonitoringOverview,
32
+ })
33
+
34
+ function MonitoringOverview() {
35
+ const { t } = useTranslation('common')
36
+ const [loading, setLoading] = useState(true)
37
+ const [health, setHealth] = useState<any>(null)
38
+ const [errors, setErrors] = useState<any>(null)
39
+ const [metrics, setMetrics] = useState<any>(null)
40
+
41
+ const loadData = async () => {
42
+ setLoading(true)
43
+ try {
44
+ const [h, e, m] = await Promise.all([
45
+ getHealthStatus(),
46
+ getErrors(),
47
+ getMetrics(),
48
+ ])
49
+ setHealth(h)
50
+ setErrors(e)
51
+ setMetrics(m)
52
+ } catch {
53
+ // ignore
54
+ }
55
+ setLoading(false)
56
+ }
57
+
58
+ useEffect(() => {
59
+ loadData()
60
+ }, [])
61
+
62
+ if (loading) {
63
+ return (
64
+ <div className="flex items-center justify-center h-64">
65
+ <RefreshCw className="size-5 animate-spin text-muted-foreground" />
66
+ </div>
67
+ )
68
+ }
69
+
70
+ return (
71
+ <div className="space-y-4 my-4">
72
+ <div className="flex items-center justify-between">
73
+ <PageHeader title={t('monitoring.overview.title')} description={t('monitoring.overview.description')} icon={Activity} />
74
+ <button
75
+ onClick={loadData}
76
+ className="inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium hover:bg-accent transition-colors"
77
+ >
78
+ <RefreshCw className="size-4" />
79
+ {t('common.refresh')}
80
+ </button>
81
+ </div>
82
+
83
+ <HealthCards health={health?.current || {}} />
84
+
85
+ <div className="grid gap-4 md:grid-cols-2">
86
+ <div className="rounded-lg border bg-card">
87
+ <div className="border-b px-4 py-3">
88
+ <h3 className="text-sm font-medium">{t('monitoring.overview.requestStats.title')}</h3>
89
+ </div>
90
+ <div className="p-4 grid grid-cols-2 gap-3">
91
+ <div>
92
+ <p className="text-xs text-muted-foreground">{t('monitoring.overview.requestStats.totalRequests')}</p>
93
+ <p className="text-lg font-semibold">{health?.requestStats?.total || 0}</p>
94
+ </div>
95
+ <div>
96
+ <p className="text-xs text-muted-foreground">{t('monitoring.overview.requestStats.avgDuration')}</p>
97
+ <p className="text-lg font-semibold">{health?.requestStats?.avgDuration?.toFixed(1) || 0}ms</p>
98
+ </div>
99
+ <div>
100
+ <p className="text-xs text-muted-foreground">{t('monitoring.overview.requestStats.maxDuration')}</p>
101
+ <p className="text-lg font-semibold">{health?.requestStats?.maxDuration || 0}ms</p>
102
+ </div>
103
+ <div>
104
+ <p className="text-xs text-muted-foreground">{t('monitoring.overview.requestStats.errorTypes')}</p>
105
+ <p className="text-lg font-semibold">{health?.errorCount || 0}</p>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ <div className="rounded-lg border bg-card">
111
+ <div className="border-b px-4 py-3">
112
+ <h3 className="text-sm font-medium">{t('monitoring.overview.recentErrors.title')}</h3>
113
+ </div>
114
+ <div className="p-4">
115
+ {errors?.errors?.slice(0, 5).length > 0 ? (
116
+ <ErrorList errors={errors.errors.slice(0, 5)} />
117
+ ) : (
118
+ <p className="text-sm text-muted-foreground text-center py-4">{t('monitoring.overview.recentErrors.empty')}</p>
119
+ )}
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <div className="rounded-lg border bg-card">
125
+ <div className="border-b px-4 py-3">
126
+ <h3 className="text-sm font-medium">{t('monitoring.overview.metricsSnapshot.title')}</h3>
127
+ </div>
128
+ <div className="p-4">
129
+ {metrics?.current && Object.keys(metrics.current).length > 0 ? (
130
+ <MetricsStats data={metrics.current} />
131
+ ) : (
132
+ <p className="text-sm text-muted-foreground text-center py-4">{t('monitoring.overview.metricsSnapshot.empty')}</p>
133
+ )}
134
+ </div>
135
+ </div>
136
+ </div>
137
+ )
138
+ }
@@ -0,0 +1,68 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createLazyFileRoute, Outlet, useLocation } from '@tanstack/react-router'
19
+ import { useTranslation } from 'react-i18next'
20
+ import { SidebarTrigger } from '@/components/ui/sidebar'
21
+ import { Separator } from '@/components/ui/separator'
22
+
23
+ export const Route = createLazyFileRoute('/_nav/monitoring')({
24
+ component: MonitoringLayout,
25
+ })
26
+
27
+ function MonitoringLayout() {
28
+ const { t } = useTranslation('common')
29
+ const location = useLocation()
30
+
31
+ const subNavItems = [
32
+ { id: 'overview', label: t('monitoring.nav.overview'), path: '/monitoring/overview' },
33
+ { id: 'logs', label: t('monitoring.nav.logs'), path: '/monitoring/logs' },
34
+ { id: 'errors', label: t('monitoring.nav.errors'), path: '/monitoring/errors' },
35
+ { id: 'metrics', label: t('monitoring.nav.metrics'), path: '/monitoring/metrics' },
36
+ { id: 'health', label: t('monitoring.nav.health'), path: '/monitoring/health' },
37
+ { id: 'sentry', label: t('monitoring.nav.sentry'), path: '/monitoring/sentry' },
38
+ { id: 'ai-usage', label: t('monitoring.nav.aiUsage'), path: '/monitoring/ai-usage' },
39
+ ]
40
+
41
+ const currentId = subNavItems.find((item) => location.pathname.startsWith(item.path))?.id || 'overview'
42
+
43
+ return (
44
+ <div className="flex flex-col min-h-0 h-full overflow-hidden">
45
+ <header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
46
+ <SidebarTrigger className="-ml-1" />
47
+ <Separator orientation="vertical" className="mr-2 h-4" />
48
+ <nav className="flex gap-1">
49
+ {subNavItems.map((item) => (
50
+ <a
51
+ key={item.id}
52
+ href={item.path}
53
+ className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${currentId === item.id
54
+ ? 'bg-accent text-accent-foreground'
55
+ : 'text-muted-foreground hover:text-foreground hover:bg-accent/50'
56
+ }`}
57
+ >
58
+ {item.label}
59
+ </a>
60
+ ))}
61
+ </nav>
62
+ </header>
63
+ <div className="flex-1 min-h-0 p-4 pt-0 flex flex-col">
64
+ <Outlet />
65
+ </div>
66
+ </div>
67
+ )
68
+ }
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createFileRoute } from '@tanstack/react-router'
19
+
20
+ export const Route = createFileRoute('/_nav/monitoring')({})