@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,184 @@
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 { useTranslation } from 'react-i18next'
19
+ import { DataTable, type AdminColumnDef } from '@/components/data-table/data-table'
20
+ import { describeMetric } from '@/lib/monitoring-descriptions'
21
+ import { getBenchmark, getBenchmarkSource, compareDuration } from '@/lib/monitoring-benchmarks'
22
+ import { ArrowDown, ArrowUp, Info } from 'lucide-react'
23
+
24
+ interface MetricRow {
25
+ metric: string
26
+ path: string
27
+ count: number
28
+ avg: number
29
+ p50: number
30
+ p95: number
31
+ p99: number
32
+ min: number
33
+ max: number
34
+ }
35
+
36
+ interface MetricsStatsProps {
37
+ data: Record<
38
+ string,
39
+ { count: number; avg: number; p50: number; p95: number; p99: number; min: number; max: number }
40
+ >
41
+ }
42
+
43
+ interface BenchmarkCellProps {
44
+ value: number
45
+ metric: string
46
+ }
47
+
48
+ function BenchmarkCell({ value, metric }: BenchmarkCellProps) {
49
+ const path = metric.replace(/^request\./, '')
50
+ const benchmark = getBenchmark(path)
51
+ const comp = compareDuration(value, benchmark)
52
+
53
+ const isGood = value <= benchmark
54
+ const diff = Math.abs(value - benchmark)
55
+ const pct = Math.round((diff / benchmark) * 100)
56
+
57
+ return (
58
+ <span className="inline-flex items-center gap-1">
59
+ {value.toFixed(1)}ms
60
+ {isGood ? (
61
+ <span
62
+ className="inline-flex items-center gap-0.5 text-green-600 dark:text-green-400"
63
+ title={`${getBenchmarkSource(path)}: ${benchmark}ms P95`}
64
+ >
65
+ <ArrowUp className="size-3" />
66
+ <span className="text-[10px]">{pct}%</span>
67
+ </span>
68
+ ) : (
69
+ <span
70
+ className="inline-flex items-center gap-0.5 text-red-600 dark:text-red-400"
71
+ title={`${getBenchmarkSource(path)}: ${benchmark}ms P95`}
72
+ >
73
+ <ArrowDown className="size-3" strokeWidth={3} />
74
+ <span className="text-[10px] font-bold">{pct}%</span>
75
+ </span>
76
+ )}
77
+ </span>
78
+ )
79
+ }
80
+
81
+ function BenchmarkInfoTooltip({ t }: { t: (key: string) => string }) {
82
+ return (
83
+ <span
84
+ className="inline-flex items-center gap-1 text-xs text-muted-foreground"
85
+ title={t('metricsStats.benchmarkSource')}
86
+ >
87
+ <Info className="size-3" />
88
+ {t('metricsStats.compareBenchmark')}
89
+ </span>
90
+ )
91
+ }
92
+
93
+ export function MetricsStats({ data }: MetricsStatsProps) {
94
+ const { t } = useTranslation('common')
95
+
96
+ const columns: AdminColumnDef<MetricRow>[] = [
97
+ {
98
+ header: t('metricsStats.columns.metricName'),
99
+ accessorKey: 'metric',
100
+ meta: { style: { width: 160 } },
101
+ cell: ({ row }: { row: { original: MetricRow } }) => (
102
+ <span className="font-mono text-xs block truncate">
103
+ {row.original.metric}
104
+ </span>
105
+ ),
106
+ },
107
+ {
108
+ header: t('metricsStats.columns.description'),
109
+ accessorKey: 'metric',
110
+ meta: { style: { width: 30 } },
111
+ cell: ({ row }: { row: { original: MetricRow } }) => (
112
+ <span className="text-xs text-muted-foreground block truncate">
113
+ {describeMetric(row.original.metric)}
114
+ </span>
115
+ ),
116
+ },
117
+ {
118
+ header: t('metricsStats.columns.count'),
119
+ accessorKey: 'count',
120
+ meta: { style: { width: 18, textAlign: 'right' } },
121
+ },
122
+ {
123
+ header: () => (
124
+ <span className="flex items-center gap-1">
125
+ {t('metricsStats.columns.avg')}
126
+ <BenchmarkInfoTooltip t={t} />
127
+ </span>
128
+ ),
129
+ accessorKey: 'avg',
130
+ meta: { style: { width: 40, textAlign: 'right' } },
131
+ cell: ({ row }: { row: { original: MetricRow } }) => (
132
+ <BenchmarkCell value={row.original.avg} metric={row.original.metric} />
133
+ ),
134
+ },
135
+ {
136
+ header: 'P50',
137
+ accessorKey: 'p50',
138
+ meta: { style: { width: 40, textAlign: 'right' } },
139
+ cell: ({ row }: { row: { original: MetricRow } }) => (
140
+ <BenchmarkCell value={row.original.p50} metric={row.original.metric} />
141
+ ),
142
+ },
143
+ {
144
+ header: 'P95',
145
+ accessorKey: 'p95',
146
+ meta: { style: { width: 40, textAlign: 'right' } },
147
+ cell: ({ row }: { row: { original: MetricRow } }) => (
148
+ <BenchmarkCell value={row.original.p95} metric={row.original.metric} />
149
+ ),
150
+ },
151
+ {
152
+ header: 'P99',
153
+ accessorKey: 'p99',
154
+ meta: { style: { width: 40, textAlign: 'right' } },
155
+ cell: ({ row }: { row: { original: MetricRow } }) => (
156
+ <BenchmarkCell value={row.original.p99} metric={row.original.metric} />
157
+ ),
158
+ },
159
+ {
160
+ header: t('metricsStats.columns.min'),
161
+ accessorKey: 'min',
162
+ meta: { style: { width: 30, textAlign: 'right' } },
163
+ cell: ({ row }: { row: { original: MetricRow } }) => (
164
+ <span>{row.original.min.toFixed(1)}ms</span>
165
+ ),
166
+ },
167
+ {
168
+ header: t('metricsStats.columns.max'),
169
+ accessorKey: 'max',
170
+ meta: { style: { width: 30, textAlign: 'right' } },
171
+ cell: ({ row }: { row: { original: MetricRow } }) => (
172
+ <span>{row.original.max.toFixed(1)}ms</span>
173
+ ),
174
+ },
175
+ ]
176
+
177
+ const rows: MetricRow[] = Object.entries(data).map(([metric, stats]) => ({
178
+ metric,
179
+ path: metric.replace(/^request\./, ''),
180
+ ...stats,
181
+ }))
182
+
183
+ return <DataTable columns={columns} data={rows} />
184
+ }
@@ -0,0 +1,179 @@
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 type React from 'react'
19
+ import { useTranslation } from 'react-i18next'
20
+ import { DataTable, type AdminColumnDef } from '@/components/data-table/data-table'
21
+ import { describePath } from '@/lib/monitoring-descriptions'
22
+ import { getBenchmark, getBenchmarkSource, compareDuration } from '@/lib/monitoring-benchmarks'
23
+ import { ArrowDown, ArrowUp, Info } from 'lucide-react'
24
+
25
+ interface RequestLogEntry {
26
+ id: string
27
+ timestamp: string
28
+ method: string
29
+ path: string
30
+ status: number
31
+ duration: number
32
+ ip?: string | null
33
+ userAgent?: string | null
34
+ }
35
+
36
+ interface RequestLogTableProps {
37
+ logs: RequestLogEntry[]
38
+ actions?: React.ReactNode
39
+ }
40
+
41
+ const methodColors: Record<string, string> = {
42
+ GET: 'text-green-600 dark:text-green-400',
43
+ POST: 'text-blue-600 dark:text-blue-400',
44
+ PUT: 'text-yellow-600 dark:text-yellow-400',
45
+ DELETE: 'text-red-600 dark:text-red-400',
46
+ PATCH: 'text-purple-600 dark:text-purple-400',
47
+ }
48
+
49
+ function DurationCell({ path, duration }: { path: string; duration: number }) {
50
+ const benchmark = getBenchmark(path)
51
+ const comp = compareDuration(duration, benchmark)
52
+
53
+ const isGood = duration <= benchmark
54
+ const pct = Math.round((Math.abs(duration - benchmark) / benchmark) * 100)
55
+ const color =
56
+ duration > 1000 ? 'text-red-600' : duration > 300 ? 'text-yellow-600' : 'text-green-600'
57
+
58
+ return (
59
+ <span className="inline-flex items-center gap-1">
60
+ <span className={`text-xs font-medium ${color}`}>{duration}ms</span>
61
+ {isGood ? (
62
+ <span
63
+ className="inline-flex items-center gap-0.5 text-green-600 dark:text-green-400"
64
+ title={`${getBenchmarkSource(path)}: ${benchmark}ms P95`}
65
+ >
66
+ <ArrowUp className="size-3" />
67
+ <span className="text-[10px]">{pct}%</span>
68
+ </span>
69
+ ) : (
70
+ <span
71
+ className="inline-flex items-center gap-0.5 text-red-600 dark:text-red-400"
72
+ title={`${getBenchmarkSource(path)}: ${benchmark}ms P95`}
73
+ >
74
+ <ArrowDown className="size-3" strokeWidth={3} />
75
+ <span className="text-[10px] font-bold">{pct}%</span>
76
+ </span>
77
+ )}
78
+ </span>
79
+ )
80
+ }
81
+
82
+ export function RequestLogTable({ logs, actions }: RequestLogTableProps) {
83
+ const { t } = useTranslation('common')
84
+
85
+ const columns: AdminColumnDef<RequestLogEntry>[] = [
86
+ {
87
+ header: t('requestLogTable.columns.time'),
88
+ accessorKey: 'timestamp',
89
+ meta: { style: { width: 20, textAlign: 'center' } },
90
+ cell: ({ row }: { row: { original: RequestLogEntry } }) => (
91
+ <span className="text-xs">
92
+ {new Date(row.original.timestamp).toLocaleTimeString('zh-CN')}
93
+ </span>
94
+ ),
95
+ },
96
+ {
97
+ header: t('requestLogTable.columns.path'),
98
+ accessorKey: 'path',
99
+ meta: { style: { width: 160 } },
100
+ cell: ({ row }: { row: { original: RequestLogEntry } }) => (
101
+ <span className="font-mono text-xs block truncate">
102
+ {row.original.path}
103
+ </span>
104
+ ),
105
+ },
106
+ {
107
+ header: t('requestLogTable.columns.description'),
108
+ accessorKey: 'path',
109
+ meta: { style: { width: 28 } },
110
+ cell: ({ row }: { row: { original: RequestLogEntry } }) => (
111
+ <span className="text-xs text-muted-foreground block truncate">
112
+ {describePath(row.original.path)}
113
+ </span>
114
+ ),
115
+ },
116
+ {
117
+ header: 'User-Agent',
118
+ accessorKey: 'userAgent',
119
+ meta: { style: { width: 48 } },
120
+ cell: ({ row }: { row: { original: RequestLogEntry } }) => (
121
+ <span className="text-xs block truncate">
122
+ {row.original.userAgent || '-'}
123
+ </span>
124
+ ),
125
+ },
126
+ {
127
+ header: t('requestLogTable.columns.method'),
128
+ accessorKey: 'method',
129
+ meta: { style: { width: 14, textAlign: 'right' } },
130
+ cell: ({ row }: { row: { original: RequestLogEntry } }) => {
131
+ const method = row.original.method
132
+ return (
133
+ <span className={`font-mono text-xs font-medium ${methodColors[method] || ''}`}>
134
+ {method}
135
+ </span>
136
+ )
137
+ },
138
+ },
139
+ {
140
+ header: t('requestLogTable.columns.status'),
141
+ accessorKey: 'status',
142
+ meta: { style: { width: 14, textAlign: 'right' } },
143
+ cell: ({ row }: { row: { original: RequestLogEntry } }) => {
144
+ const status = row.original.status
145
+ const color =
146
+ status >= 500 ? 'text-red-600' : status >= 400 ? 'text-yellow-600' : 'text-green-600'
147
+ return <span className={`text-xs font-medium ${color}`}>{status}</span>
148
+ },
149
+ },
150
+ {
151
+ header: () => (
152
+ <span className="flex items-center gap-1">
153
+ {t('requestLogTable.columns.duration')}
154
+ <Info
155
+ className="size-3 text-muted-foreground"
156
+ data-tooltip={t('requestLogTable.compareBenchmark')}
157
+ />
158
+ </span>
159
+ ),
160
+ accessorKey: 'duration',
161
+ meta: { style: { width: 28, textAlign: 'right' } },
162
+ cell: ({ row }: { row: { original: RequestLogEntry } }) => (
163
+ <DurationCell path={row.original.path} duration={row.original.duration} />
164
+ ),
165
+ },
166
+ {
167
+ header: 'IP',
168
+ accessorKey: 'ip',
169
+ meta: { style: { width: 20, textAlign: 'right' } },
170
+ cell: ({ row }: { row: { original: RequestLogEntry } }) => (
171
+ <span className="font-mono text-xs text-muted-foreground">
172
+ {row.original.ip || '-'}
173
+ </span>
174
+ ),
175
+ },
176
+ ]
177
+
178
+ return <DataTable columns={columns} data={logs} actions={actions} />
179
+ }
@@ -0,0 +1,59 @@
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
+
19
+ import { useTheme } from "next-themes";
20
+ import { Moon, Sun } from "lucide-react";
21
+ import { Button } from "@/components/ui/button";
22
+ import { useEffect, useState } from "react";
23
+
24
+ export function ThemeToggle() {
25
+ const { theme, setTheme } = useTheme();
26
+ const [mounted, setMounted] = useState(false);
27
+
28
+ useEffect(() => {
29
+ setMounted(true);
30
+ }, []);
31
+
32
+ if (!mounted) {
33
+ return (
34
+ <Button variant="ghost" size="icon" className="h-8 w-8 rounded-full">
35
+ <Sun className="h-4 w-4" />
36
+ </Button>
37
+ );
38
+ }
39
+
40
+ const toggleTheme = () => {
41
+ setTheme(theme === "dark" ? "light" : "dark");
42
+ };
43
+
44
+ return (
45
+ <Button
46
+ variant="ghost"
47
+ size="icon"
48
+ className="h-8 w-8 rounded-full"
49
+ onClick={toggleTheme}
50
+ >
51
+ {theme === "dark" ? (
52
+ <Moon className="size-5" />
53
+ ) : (
54
+ <Sun className="size-5" />
55
+ )}
56
+ <span className="sr-only">Toggle theme</span>
57
+ </Button>
58
+ );
59
+ }
@@ -0,0 +1,46 @@
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
+ export type { AvatarPart, AvatarConfig, ShapeType } from "@allbluecn/kernel"
19
+
20
+ export const partMax: Record<import("@allbluecn/kernel").AvatarPart, number> = {
21
+ face: 15,
22
+ eyes: 13,
23
+ eyebrows: 15,
24
+ mouth: 19,
25
+ nose: 13,
26
+ glasses: 14,
27
+ hair: 58,
28
+ accessories: 14,
29
+ details: 13,
30
+ beard: 16,
31
+ }
32
+
33
+ export const presetBgColors = [
34
+ "#fff", "#fa541c", "#faad14", "#fadb14", "#a0d911",
35
+ "#52c41a", "#1890ff", "#2f54eb", "#722ed1", "#eb2f96",
36
+ "#bfbfbf", "#f0d5c0", "#debaba", "#babae1", "#e1bae1",
37
+ "#bae1e1", "#c0d5f0", "#d5f0c0", "#e0e0e0", "#f5c6c6",
38
+ ]
39
+
40
+ export function getRandomConfig(): import("@allbluecn/kernel").AvatarConfig {
41
+ const config: Record<string, number> = {}
42
+ for (const key of Object.keys(partMax) as import("@allbluecn/kernel").AvatarPart[]) {
43
+ config[key] = Math.floor(Math.random() * (partMax[key] + 1))
44
+ }
45
+ return config as import("@allbluecn/kernel").AvatarConfig
46
+ }
@@ -0,0 +1,216 @@
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
+ "use client"
19
+
20
+ import * as React from "react"
21
+ import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
22
+
23
+ import { cn } from "@/lib/utils"
24
+ import { Button } from "@/components/ui/button"
25
+
26
+ function AlertDialog({
27
+ ...props
28
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
29
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
30
+ }
31
+
32
+ function AlertDialogTrigger({
33
+ ...props
34
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
35
+ return (
36
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
37
+ )
38
+ }
39
+
40
+ function AlertDialogPortal({
41
+ ...props
42
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
43
+ return (
44
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
45
+ )
46
+ }
47
+
48
+ function AlertDialogOverlay({
49
+ className,
50
+ ...props
51
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
52
+ return (
53
+ <AlertDialogPrimitive.Overlay
54
+ data-slot="alert-dialog-overlay"
55
+ className={cn(
56
+ "fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function AlertDialogContent({
65
+ className,
66
+ size = "default",
67
+ ...props
68
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
69
+ size?: "default" | "sm"
70
+ }) {
71
+ return (
72
+ <AlertDialogPortal>
73
+ <AlertDialogOverlay />
74
+ <AlertDialogPrimitive.Content
75
+ data-slot="alert-dialog-content"
76
+ data-size={size}
77
+ className={cn(
78
+ "group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
79
+ className
80
+ )}
81
+ {...props}
82
+ />
83
+ </AlertDialogPortal>
84
+ )
85
+ }
86
+
87
+ function AlertDialogHeader({
88
+ className,
89
+ ...props
90
+ }: React.ComponentProps<"div">) {
91
+ return (
92
+ <div
93
+ data-slot="alert-dialog-header"
94
+ className={cn(
95
+ "grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
96
+ className
97
+ )}
98
+ {...props}
99
+ />
100
+ )
101
+ }
102
+
103
+ function AlertDialogFooter({
104
+ className,
105
+ ...props
106
+ }: React.ComponentProps<"div">) {
107
+ return (
108
+ <div
109
+ data-slot="alert-dialog-footer"
110
+ className={cn(
111
+ "-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
112
+ className
113
+ )}
114
+ {...props}
115
+ />
116
+ )
117
+ }
118
+
119
+ function AlertDialogMedia({
120
+ className,
121
+ ...props
122
+ }: React.ComponentProps<"div">) {
123
+ return (
124
+ <div
125
+ data-slot="alert-dialog-media"
126
+ className={cn(
127
+ "mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
128
+ className
129
+ )}
130
+ {...props}
131
+ />
132
+ )
133
+ }
134
+
135
+ function AlertDialogTitle({
136
+ className,
137
+ ...props
138
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
139
+ return (
140
+ <AlertDialogPrimitive.Title
141
+ data-slot="alert-dialog-title"
142
+ className={cn(
143
+ "text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
144
+ className
145
+ )}
146
+ {...props}
147
+ />
148
+ )
149
+ }
150
+
151
+ function AlertDialogDescription({
152
+ className,
153
+ ...props
154
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
155
+ return (
156
+ <AlertDialogPrimitive.Description
157
+ data-slot="alert-dialog-description"
158
+ className={cn(
159
+ "text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
160
+ className
161
+ )}
162
+ {...props}
163
+ />
164
+ )
165
+ }
166
+
167
+ function AlertDialogAction({
168
+ className,
169
+ variant = "default",
170
+ size = "default",
171
+ ...props
172
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
173
+ Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
174
+ return (
175
+ <Button variant={variant} size={size} asChild>
176
+ <AlertDialogPrimitive.Action
177
+ data-slot="alert-dialog-action"
178
+ className={cn(className)}
179
+ {...props}
180
+ />
181
+ </Button>
182
+ )
183
+ }
184
+
185
+ function AlertDialogCancel({
186
+ className,
187
+ variant = "outline",
188
+ size = "default",
189
+ ...props
190
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
191
+ Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
192
+ return (
193
+ <Button variant={variant} size={size} asChild>
194
+ <AlertDialogPrimitive.Cancel
195
+ data-slot="alert-dialog-cancel"
196
+ className={cn(className)}
197
+ {...props}
198
+ />
199
+ </Button>
200
+ )
201
+ }
202
+
203
+ export {
204
+ AlertDialog,
205
+ AlertDialogAction,
206
+ AlertDialogCancel,
207
+ AlertDialogContent,
208
+ AlertDialogDescription,
209
+ AlertDialogFooter,
210
+ AlertDialogHeader,
211
+ AlertDialogMedia,
212
+ AlertDialogOverlay,
213
+ AlertDialogPortal,
214
+ AlertDialogTitle,
215
+ AlertDialogTrigger,
216
+ }