@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,399 @@
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, useRouter } from '@tanstack/react-router'
19
+ import { useTranslation } from 'react-i18next'
20
+ import * as Sentry from '@sentry/tanstackstart-react'
21
+ import { BugPlay, ExternalLink, Bug, Activity, AlertTriangle, Eye, EyeOff, Save, RotateCcw, CheckCircle2, XCircle, RefreshCw, ShieldCheck } from 'lucide-react'
22
+ import { IconBrandSentry } from '@tabler/icons-react';
23
+ import { useEffect, useState } from 'react'
24
+ import { toast } from 'sonner'
25
+ import { getSentryConfig, saveSentryConfig, reconfigureSentryServer } from '@/server/serverFns/sentry/config'
26
+ import type { SentryConfig } from '@/server/serverFns/sentry/config'
27
+
28
+ export const Route = createLazyFileRoute('/_nav/monitoring/sentry')({
29
+ component: SentryPage,
30
+ })
31
+
32
+ type ConfigField = {
33
+ key: keyof SentryConfig
34
+ label: string
35
+ placeholder: string
36
+ secret: boolean
37
+ instruction: string[]
38
+ }
39
+
40
+ function SentryPage() {
41
+ const { t } = useTranslation('common')
42
+ const router = useRouter()
43
+ const [config, setConfig] = useState<SentryConfig>({ dsn: '', org: '', project: '', authToken: '' })
44
+ const [loading, setLoading] = useState(true)
45
+ const [saving, setSaving] = useState(false)
46
+ const [changed, setChanged] = useState(false)
47
+ const [visibleFields, setVisibleFields] = useState<Set<string>>(new Set())
48
+ const [verifyResult, setVerifyResult] = useState<{ ok: boolean; detail: string } | null>(null)
49
+ const [verifying, setVerifying] = useState(false)
50
+
51
+ const CONFIG_FIELDS: ConfigField[] = [
52
+ {
53
+ key: 'dsn',
54
+ label: 'DSN',
55
+ placeholder: 'https://<key>@o<orgId>.ingest.sentry.io/<projectId>',
56
+ secret: false,
57
+ instruction: [
58
+ t('monitoring.sentry.fields.dsn.instruction.step1'),
59
+ t('monitoring.sentry.fields.dsn.instruction.step2'),
60
+ t('monitoring.sentry.fields.dsn.instruction.step3'),
61
+ t('monitoring.sentry.fields.dsn.instruction.step4'),
62
+ ],
63
+ },
64
+ {
65
+ key: 'org',
66
+ label: t('monitoring.sentry.fields.org.label'),
67
+ placeholder: 'my-organization',
68
+ secret: false,
69
+ instruction: [
70
+ t('monitoring.sentry.fields.org.instruction.step1'),
71
+ t('monitoring.sentry.fields.org.instruction.step2'),
72
+ t('monitoring.sentry.fields.org.instruction.step3'),
73
+ ],
74
+ },
75
+ {
76
+ key: 'project',
77
+ label: t('monitoring.sentry.fields.project.label'),
78
+ placeholder: 'my-project',
79
+ secret: false,
80
+ instruction: [
81
+ t('monitoring.sentry.fields.project.instruction.step1'),
82
+ t('monitoring.sentry.fields.project.instruction.step2'),
83
+ t('monitoring.sentry.fields.project.instruction.step3'),
84
+ ],
85
+ },
86
+ {
87
+ key: 'authToken',
88
+ label: 'Auth Token',
89
+ placeholder: 'sntrys_********************',
90
+ secret: true,
91
+ instruction: [
92
+ t('monitoring.sentry.fields.authToken.instruction.step1'),
93
+ t('monitoring.sentry.fields.authToken.instruction.step2'),
94
+ t('monitoring.sentry.fields.authToken.instruction.step3'),
95
+ t('monitoring.sentry.fields.authToken.instruction.step4'),
96
+ t('monitoring.sentry.fields.authToken.instruction.step5'),
97
+ ],
98
+ },
99
+ ]
100
+
101
+ const loadConfig = async () => {
102
+ setLoading(true)
103
+ try {
104
+ const cfg = await getSentryConfig()
105
+ setConfig(cfg)
106
+ } catch {
107
+ toast.error(t('monitoring.sentry.loadFailed'))
108
+ }
109
+ setLoading(false)
110
+ }
111
+
112
+ useEffect(() => {
113
+ loadConfig()
114
+ }, [])
115
+
116
+ const dsnConfigured = !!config.dsn || !!import.meta.env.VITE_SENTRY_DSN
117
+
118
+ const updateField = (key: keyof SentryConfig, value: string) => {
119
+ setConfig((prev) => ({ ...prev, [key]: value }))
120
+ setChanged(true)
121
+ }
122
+
123
+ const toggleVisible = (key: string) => {
124
+ setVisibleFields((prev) => {
125
+ const next = new Set(prev)
126
+ if (next.has(key)) next.delete(key)
127
+ else next.add(key)
128
+ return next
129
+ })
130
+ }
131
+
132
+ const reconfigure = (dsn: string) => {
133
+ Sentry.init({
134
+ dsn,
135
+ environment: import.meta.env.VITE_SENTRY_ENVIRONMENT || import.meta.env.MODE,
136
+ dataCollection: {},
137
+ tracesSampleRate: 1.0,
138
+ replaysSessionSampleRate: 0.1,
139
+ replaysOnErrorSampleRate: 1.0,
140
+ enableLogs: true,
141
+ tunnel: '/sentry-tunnel',
142
+ })
143
+ }
144
+
145
+ const handleSave = async () => {
146
+ setSaving(true)
147
+ try {
148
+ await saveSentryConfig({ data: config })
149
+ reconfigure(config.dsn)
150
+ await reconfigureSentryServer({ data: { dsn: config.dsn } })
151
+ toast.success(t('monitoring.sentry.saveSuccess'))
152
+ setChanged(false)
153
+ router.invalidate()
154
+ } catch {
155
+ toast.error(t('monitoring.sentry.saveFailed'))
156
+ }
157
+ setSaving(false)
158
+ }
159
+
160
+ const validateDsn = (dsn: string) => {
161
+ if (!dsn) return { ok: false, detail: t('monitoring.sentry.validate.empty') }
162
+ if (!dsn.startsWith('https://')) return { ok: false, detail: t('monitoring.sentry.validate.httpsPrefix') }
163
+ if (!dsn.includes('.sentry.io')) return { ok: false, detail: t('monitoring.sentry.validate.notSentryIo') }
164
+ if (!dsn.includes('@')) return { ok: false, detail: t('monitoring.sentry.validate.missingAt') }
165
+ const projectId = dsn.split('/').pop()
166
+ if (!projectId || !/^\d+$/.test(projectId)) return { ok: false, detail: t('monitoring.sentry.validate.missingProjectId') }
167
+ return { ok: true, detail: t('monitoring.sentry.validate.valid') }
168
+ }
169
+
170
+ const verifyConfig = async () => {
171
+ const activeDsn = config.dsn || import.meta.env.VITE_SENTRY_DSN
172
+ const fmt = validateDsn(activeDsn || '')
173
+ if (!fmt.ok) {
174
+ setVerifyResult(fmt)
175
+ return
176
+ }
177
+ setVerifying(true)
178
+ setVerifyResult(null)
179
+ try {
180
+ const client = Sentry.getClient()
181
+ const clientDsn = client?.getOptions().dsn
182
+ if (clientDsn !== activeDsn) {
183
+ await reconfigure(activeDsn)
184
+ }
185
+
186
+ const eventId = Sentry.captureException(new Error('Sentry config validation - test error'))
187
+ if (!eventId) {
188
+ setVerifyResult({ ok: false, detail: t('monitoring.sentry.verify.notInitialized') })
189
+ setVerifying(false)
190
+ return
191
+ }
192
+ const flushed = await Sentry.flush(5000)
193
+ if (flushed) {
194
+ setVerifyResult({ ok: true, detail: t('monitoring.sentry.verify.success', { eventId }) })
195
+ } else {
196
+ setVerifyResult({ ok: false, detail: t('monitoring.sentry.verify.timeout') })
197
+ }
198
+ } catch (e) {
199
+ setVerifyResult({ ok: false, detail: t('monitoring.sentry.verify.error', { message: (e as Error).message }) })
200
+ }
201
+ setVerifying(false)
202
+ }
203
+
204
+ return (
205
+ <div className="p-6 space-y-6">
206
+ <div className="flex items-center justify-between">
207
+ <div>
208
+ <h2 className="text-2xl font-bold flex items-center gap-2">
209
+ <IconBrandSentry stroke={2} />
210
+ {t('monitoring.sentry.title')}
211
+ </h2>
212
+ <p className="mt-1 text-muted-foreground">{t('monitoring.sentry.subtitle')}</p>
213
+ </div>
214
+ <a
215
+ href="https://sentry.io"
216
+ target="_blank"
217
+ rel="noopener noreferrer"
218
+ className="inline-flex items-center gap-1.5 rounded-lg border bg-card px-4 py-2 text-sm font-medium hover:bg-accent transition-colors"
219
+ >
220
+ <ExternalLink className="size-4" />
221
+ {t('monitoring.sentry.openSentry')}
222
+ </a>
223
+ </div>
224
+
225
+ <div className="grid gap-4 md:grid-cols-4">
226
+ <StatusCard
227
+ title={t('monitoring.sentry.statusCards.dsn.title')}
228
+ status={dsnConfigured ? 'active' : 'inactive'}
229
+ icon={dsnConfigured ? CheckCircle2 : XCircle}
230
+ description={dsnConfigured ? t('monitoring.sentry.statusCards.dsn.configured') : t('monitoring.sentry.statusCards.dsn.notConfigured')}
231
+ />
232
+ <StatusCard
233
+ title={t('monitoring.sentry.statusCards.errors.title')}
234
+ status={dsnConfigured ? 'active' : 'inactive'}
235
+ icon={Bug}
236
+ description={dsnConfigured ? t('monitoring.sentry.statusCards.errors.enabled') : t('monitoring.sentry.statusCards.errors.disabled')}
237
+ />
238
+ <StatusCard
239
+ title={t('monitoring.sentry.statusCards.performance.title')}
240
+ status={dsnConfigured ? 'active' : 'inactive'}
241
+ icon={Activity}
242
+ description={dsnConfigured ? t('monitoring.sentry.statusCards.performance.enabled') : t('monitoring.sentry.statusCards.performance.disabled')}
243
+ />
244
+ <StatusCard
245
+ title="Session Replay"
246
+ status={dsnConfigured ? 'active' : 'inactive'}
247
+ icon={BugPlay}
248
+ description={dsnConfigured ? t('monitoring.sentry.statusCards.replay.enabled') : t('monitoring.sentry.statusCards.replay.disabled')}
249
+ />
250
+ </div>
251
+
252
+ <div className="rounded-lg border bg-card">
253
+ <div className="border-b px-6 py-4">
254
+ <h3 className="text-lg font-semibold">{t('monitoring.sentry.config.title')}</h3>
255
+ <p className="text-sm text-muted-foreground mt-1">
256
+ {t('monitoring.sentry.config.description')}
257
+ </p>
258
+ </div>
259
+ <div className="divide-y">
260
+ {CONFIG_FIELDS.map((field) => (
261
+ <ConfigFieldCard
262
+ key={field.key}
263
+ field={field}
264
+ value={config[field.key]}
265
+ visible={visibleFields.has(field.key)}
266
+ onChange={(v) => updateField(field.key, v)}
267
+ onToggleVisible={() => toggleVisible(field.key)}
268
+ />
269
+ ))}
270
+ </div>
271
+ <div className="border-t px-6 py-4 flex items-center gap-3">
272
+ <button
273
+ type="button"
274
+ onClick={handleSave}
275
+ disabled={!changed || saving}
276
+ className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50"
277
+ >
278
+ {saving ? <RefreshCw className="size-4 animate-spin" /> : <Save className="size-4" />}
279
+ {t('monitoring.sentry.saveConfig')}
280
+ </button>
281
+ {changed && (
282
+ <span className="text-xs text-muted-foreground">{t('monitoring.sentry.unsaved')}</span>
283
+ )}
284
+ </div>
285
+ </div>
286
+
287
+ {verifyResult && (
288
+ <div
289
+ className={`rounded-lg border p-4 text-sm flex items-start gap-3 ${verifyResult.ok
290
+ ? 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950 text-green-700 dark:text-green-300'
291
+ : 'border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950 text-red-700 dark:text-red-300'
292
+ }`}
293
+ >
294
+ {verifyResult.ok ? <CheckCircle2 className="size-4 mt-0.5 shrink-0" /> : <XCircle className="size-4 mt-0.5 shrink-0" />}
295
+ <span>{verifyResult.detail}</span>
296
+ </div>
297
+ )}
298
+
299
+ <div className="rounded-lg border bg-card p-6">
300
+ <h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
301
+ <ShieldCheck className="size-5" />
302
+ {t('monitoring.sentry.verify.title')}
303
+ </h3>
304
+ <p className="text-sm text-muted-foreground mb-4">
305
+ {t('monitoring.sentry.verify.description')}
306
+ <h4 className="text-xs text-muted-foreground mb-4">
307
+ {t('monitoring.sentry.verify.note')}
308
+ </h4>
309
+ </p>
310
+ <button
311
+ type="button"
312
+ onClick={verifyConfig}
313
+ disabled={verifying || (!config.dsn && !import.meta.env.VITE_SENTRY_DSN)}
314
+ className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-40"
315
+ >
316
+ {verifying ? <RefreshCw className="size-4 animate-spin" /> : <ShieldCheck className="size-4" />}
317
+ {t('monitoring.sentry.verify.button')}
318
+ </button>
319
+ </div>
320
+ </div>
321
+ )
322
+ }
323
+
324
+ function ConfigFieldCard({
325
+ field,
326
+ value,
327
+ visible,
328
+ onChange,
329
+ onToggleVisible,
330
+ }: {
331
+ field: ConfigField
332
+ value: string
333
+ visible: boolean
334
+ onChange: (v: string) => void
335
+ onToggleVisible: () => void
336
+ }) {
337
+ const { t } = useTranslation('common')
338
+ return (
339
+ <div className="px-6 py-5 space-y-3">
340
+ <div className="flex items-center justify-between">
341
+ <label className="text-sm font-medium">{field.label}</label>
342
+ {field.secret && (
343
+ <button
344
+ type="button"
345
+ onClick={onToggleVisible}
346
+ className="text-muted-foreground hover:text-foreground transition-colors"
347
+ >
348
+ {visible ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
349
+ </button>
350
+ )}
351
+ </div>
352
+
353
+ <div className="relative">
354
+ <input
355
+ type={field.secret && !visible ? 'password' : 'text'}
356
+ value={value}
357
+ onChange={(e) => onChange(e.target.value)}
358
+ placeholder={field.placeholder}
359
+ className="w-full rounded-lg border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-ring/50"
360
+ />
361
+ </div>
362
+
363
+ <div className="rounded-lg bg-muted/50 p-3">
364
+ <p className="text-xs font-medium text-muted-foreground mb-1.5">{t('monitoring.sentry.fields.obtainMethod')}</p>
365
+ <ol className="list-decimal list-inside space-y-0.5">
366
+ {field.instruction.map((step, i) => (
367
+ <li key={i} className="text-xs text-muted-foreground">{step}</li>
368
+ ))}
369
+ </ol>
370
+ </div>
371
+ </div>
372
+ )
373
+ }
374
+
375
+ function StatusCard({
376
+ title,
377
+ status,
378
+ icon: Icon,
379
+ description,
380
+ }: {
381
+ title: string
382
+ status: 'active' | 'inactive'
383
+ icon: React.ComponentType<{ className?: string }>
384
+ description: string
385
+ }) {
386
+ return (
387
+ <div className={`rounded-lg border p-4 ${status === 'active' ? 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950' : ''}`}>
388
+ <div className="flex items-center gap-3">
389
+ <div className="flex size-10 items-center justify-center rounded-full bg-background shadow-sm">
390
+ <Icon className={`size-5 ${status === 'active' ? 'text-green-500' : 'text-muted-foreground'}`} />
391
+ </div>
392
+ <div>
393
+ <p className="text-sm font-medium">{title}</p>
394
+ <p className="text-xs text-muted-foreground mt-0.5">{description}</p>
395
+ </div>
396
+ </div>
397
+ </div>
398
+ )
399
+ }
@@ -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/sentry')({})