@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,237 @@
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 { useEffect, useState } from "react"
20
+ import { useTranslation } from 'react-i18next'
21
+ import { useTheme } from "next-themes"
22
+ import { MarketAppFormData } from "@/lib/anyhub/types"
23
+ import { CAPABILITY_GROUPS } from "@/lib/anyhub/capabilities"
24
+ import {
25
+ Dialog,
26
+ DialogContent,
27
+ DialogFooter,
28
+ DialogHeader,
29
+ DialogTitle,
30
+ } from "@/components/ui/dialog"
31
+ import { Button } from "@/components/ui/button"
32
+ import { Input } from "@/components/ui/input"
33
+ import { Label } from "@/components/ui/label"
34
+ import {
35
+ Select,
36
+ SelectContent,
37
+ SelectItem,
38
+ SelectTrigger,
39
+ SelectValue,
40
+ } from "@/components/ui/select"
41
+ import { toast } from '@/components/ui/sonner'
42
+
43
+ interface MarketAppFormProps {
44
+ open: boolean
45
+ onOpenChange: (open: boolean) => void
46
+ editData?: { id: string; data: MarketAppFormData } | null
47
+ onSuccess: () => void
48
+ }
49
+
50
+ const CATEGORY_KEYS = ["productivity", "ai-assistant", "project-mgmt", "docs-collab", "data-analysis", "other"] as const
51
+
52
+ const emptyForm: MarketAppFormData = {
53
+ name: "",
54
+ displayName: "",
55
+ description: "",
56
+ entryUrl: "",
57
+ icon: "",
58
+ category: "",
59
+ tags: "",
60
+ capabilities: {},
61
+ }
62
+
63
+ export function MarketAppForm({ open, onOpenChange, editData, onSuccess }: MarketAppFormProps) {
64
+ const [form, setForm] = useState<MarketAppFormData>(emptyForm)
65
+ const [loading, setLoading] = useState(false)
66
+ const { t } = useTranslation('common')
67
+
68
+ useEffect(() => {
69
+ if (editData) {
70
+ setForm(editData.data)
71
+ } else {
72
+ setForm(emptyForm)
73
+ }
74
+ }, [editData, open])
75
+
76
+ const updateField = (field: keyof MarketAppFormData, value: string) => {
77
+ setForm((prev) => ({ ...prev, [field]: value }))
78
+ }
79
+
80
+ const toggleCapability = (capId: string) => {
81
+ setForm((prev) => ({
82
+ ...prev,
83
+ capabilities: { ...prev.capabilities, [capId]: !prev.capabilities[capId] },
84
+ }))
85
+ }
86
+
87
+ const handleSubmit = async () => {
88
+ if (!form.name.trim() || !form.displayName.trim() || !form.entryUrl.trim()) {
89
+ toast.error(t('market.form.fillRequired'))
90
+ return
91
+ }
92
+
93
+ setLoading(true)
94
+ try {
95
+ const url = editData ? `/api/anyhub/market/${editData.id}` : "/api/anyhub/market"
96
+ const method = editData ? "PATCH" : "POST"
97
+ const res = await fetch(url, {
98
+ method,
99
+ headers: { "Content-Type": "application/json" },
100
+ body: JSON.stringify(form),
101
+ })
102
+
103
+ if (!res.ok) {
104
+ const err = await res.json()
105
+ throw new Error(err.error || t('market.form.operationFailed'))
106
+ }
107
+
108
+ toast.success(editData ? t('market.form.updated') : t('market.form.created'))
109
+ onOpenChange(false)
110
+ onSuccess()
111
+ } catch (e) {
112
+ toast.error(e instanceof Error ? e.message : t('market.form.operationFailed'))
113
+ } finally {
114
+ setLoading(false)
115
+ }
116
+ }
117
+
118
+ return (
119
+ <Dialog open={open} onOpenChange={onOpenChange}>
120
+ <DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-lg">
121
+ <DialogHeader>
122
+ <DialogTitle>{editData ? t('market.form.edit') : t('market.form.create')}</DialogTitle>
123
+ </DialogHeader>
124
+
125
+ <div className="grid gap-4 py-4">
126
+ <div className="grid gap-2">
127
+ <Label>{t('market.form.name')}</Label>
128
+ <Input
129
+ value={form.name}
130
+ onChange={(e) => updateField("name", e.target.value)}
131
+ placeholder={t('market.form.namePlaceholder')}
132
+ disabled={!!editData}
133
+ />
134
+ </div>
135
+
136
+ <div className="grid gap-2">
137
+ <Label>{t('market.form.displayName')}</Label>
138
+ <Input
139
+ value={form.displayName}
140
+ onChange={(e) => updateField("displayName", e.target.value)}
141
+ placeholder={t('market.form.displayNamePlaceholder')}
142
+ />
143
+ </div>
144
+
145
+ <div className="grid gap-2">
146
+ <Label>{t('market.form.entryUrl')}</Label>
147
+ <Input
148
+ value={form.entryUrl}
149
+ onChange={(e) => updateField("entryUrl", e.target.value)}
150
+ placeholder="https://..."
151
+ />
152
+ </div>
153
+
154
+ <div className="grid gap-2">
155
+ <Label>{t('market.form.description')}</Label>
156
+ <Input
157
+ value={form.description || ""}
158
+ onChange={(e) => updateField("description", e.target.value)}
159
+ placeholder={t('market.form.descriptionPlaceholder')}
160
+ />
161
+ </div>
162
+
163
+ <div className="grid gap-2">
164
+ <Label>{t('market.form.icon')}</Label>
165
+ <Input
166
+ value={form.icon || ""}
167
+ onChange={(e) => updateField("icon", e.target.value)}
168
+ placeholder={t('market.form.iconPlaceholder')}
169
+ />
170
+ </div>
171
+
172
+ <div className="grid gap-2">
173
+ <Label>{t('market.form.category')}</Label>
174
+ <Select value={form.category || ""} onValueChange={(v) => updateField("category", v === "__none__" ? "" : v)}>
175
+ <SelectTrigger>
176
+ <SelectValue placeholder={t('market.form.categoryPlaceholder')} />
177
+ </SelectTrigger>
178
+ <SelectContent>
179
+ <SelectItem value="__none__">{t('common.none')}</SelectItem>
180
+ {CATEGORY_KEYS.map((k) => (
181
+ <SelectItem key={k} value={t(`market.categories.${k}`)}>
182
+ {t(`market.categories.${k}`)}
183
+ </SelectItem>
184
+ ))}
185
+ </SelectContent>
186
+ </Select>
187
+ </div>
188
+
189
+ <div className="grid gap-2">
190
+ <Label>{t('market.form.tags')}</Label>
191
+ <Input
192
+ value={form.tags || ""}
193
+ onChange={(e) => updateField("tags", e.target.value)}
194
+ placeholder={t('market.form.tagsPlaceholder')}
195
+ />
196
+ </div>
197
+
198
+ <div className="grid gap-2">
199
+ <Label>{t('market.form.capabilities')}</Label>
200
+ <div className="space-y-3 rounded-md border p-3">
201
+ {CAPABILITY_GROUPS.map((group) => (
202
+ <div key={group.id}>
203
+ <p className="mb-1.5 text-sm font-medium">{group.label}</p>
204
+ <div className="flex flex-wrap gap-3">
205
+ {group.capabilities.map((cap) => (
206
+ <label
207
+ key={cap.id}
208
+ className="flex cursor-pointer items-center gap-1.5 text-sm"
209
+ >
210
+ <input
211
+ type="checkbox"
212
+ checked={!!form.capabilities[cap.id]}
213
+ onChange={() => toggleCapability(cap.id)}
214
+ className="rounded border-input"
215
+ />
216
+ {cap.label}
217
+ </label>
218
+ ))}
219
+ </div>
220
+ </div>
221
+ ))}
222
+ </div>
223
+ </div>
224
+ </div>
225
+
226
+ <DialogFooter>
227
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
228
+ {t('common.cancel')}
229
+ </Button>
230
+ <Button onClick={handleSubmit} disabled={loading}>
231
+ {loading ? t('status.loading') : editData ? t('common.save') : t('common.create')}
232
+ </Button>
233
+ </DialogFooter>
234
+ </DialogContent>
235
+ </Dialog>
236
+ )
237
+ }
@@ -0,0 +1,156 @@
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 { useCallback, useEffect, useState } from "react"
19
+ import { useTranslation } from 'react-i18next'
20
+ import { DataTable } from "@/components/data-table/data-table"
21
+ import { getMarketColumns, MarketAppRow } from "./columns"
22
+ import { MarketAppForm } from "./market-app-form"
23
+ import { MarketAppFormData } from "@/lib/anyhub/types"
24
+ import { Button } from "@/components/ui/button"
25
+ import { PlusIcon } from "lucide-react"
26
+ import { toast } from '@/components/ui/sonner'
27
+ import { listMarketAppsFn, updateMarketAppFn, deleteMarketAppFn } from "@/server/serverFns/anyhub/market"
28
+ import {
29
+ AlertDialog,
30
+ AlertDialogAction,
31
+ AlertDialogCancel,
32
+ AlertDialogContent,
33
+ AlertDialogDescription,
34
+ AlertDialogFooter,
35
+ AlertDialogHeader,
36
+ AlertDialogTitle,
37
+ } from "@/components/ui/alert-dialog"
38
+
39
+ export function MarketAppList() {
40
+ const { t } = useTranslation('common')
41
+ const [apps, setApps] = useState<MarketAppRow[]>([])
42
+ const [loading, setLoading] = useState(true)
43
+ const [formOpen, setFormOpen] = useState(false)
44
+ const [editData, setEditData] = useState<{ id: string; data: MarketAppFormData } | null>(null)
45
+ const [deleteTarget, setDeleteTarget] = useState<MarketAppRow | null>(null)
46
+
47
+ const fetchApps = useCallback(async () => {
48
+ try {
49
+ const data = await listMarketAppsFn({ data: {} })
50
+ setApps(data.items.map((app) => ({
51
+ ...app,
52
+ capabilities: (app.capabilities || {}) as Record<string, boolean>,
53
+ createdAt: app.createdAt.toISOString(),
54
+ updatedAt: app.updatedAt?.toISOString() ?? '',
55
+ publishedAt: app.publishedAt?.toISOString() ?? null,
56
+ })))
57
+ } catch {
58
+ toast.error(t('market.fetchFailed'))
59
+ } finally {
60
+ setLoading(false)
61
+ }
62
+ }, [t])
63
+
64
+ useEffect(() => {
65
+ fetchApps()
66
+ }, [fetchApps])
67
+
68
+ const handleEdit = (app: MarketAppRow) => {
69
+ setEditData({
70
+ id: app.id,
71
+ data: {
72
+ name: app.name,
73
+ displayName: app.displayName,
74
+ description: app.description || "",
75
+ entryUrl: app.entryUrl,
76
+ icon: app.icon || "",
77
+ category: app.category || "",
78
+ tags: app.tags || "",
79
+ capabilities: app.capabilities || {},
80
+ },
81
+ })
82
+ setFormOpen(true)
83
+ }
84
+
85
+ const handleToggleStatus = async (app: MarketAppRow) => {
86
+ const newStatus = app.status === "PUBLISHED" ? "UNPUBLISHED" : "PUBLISHED"
87
+ try {
88
+ await updateMarketAppFn({ data: { id: app.id, isPublished: newStatus === "PUBLISHED" } })
89
+ toast.success(t('market.statusUpdated', { status: newStatus === "PUBLISHED" ? 'published' : 'unpublished' }))
90
+ fetchApps()
91
+ } catch {
92
+ toast.error(t('market.operationFailed'))
93
+ }
94
+ }
95
+
96
+ const handleDelete = async () => {
97
+ if (!deleteTarget) return
98
+ try {
99
+ await deleteMarketAppFn({ data: { id: deleteTarget.id } })
100
+ toast.success(t('market.deleted'))
101
+ setDeleteTarget(null)
102
+ fetchApps()
103
+ } catch {
104
+ toast.error(t('market.deleteFailed'))
105
+ }
106
+ }
107
+
108
+ const columns = getMarketColumns(handleEdit, handleToggleStatus, (app) => setDeleteTarget(app))
109
+
110
+ return (
111
+ <div className="space-y-4">
112
+ <div className="flex items-center justify-between">
113
+ <h1 className="text-2xl font-bold">{t('market.title')}</h1>
114
+ <Button
115
+ onClick={() => {
116
+ setEditData(null)
117
+ setFormOpen(true)
118
+ }}
119
+ >
120
+ <PlusIcon className="mr-2 size-4" />
121
+ {t('market.addApp')}
122
+ </Button>
123
+ </div>
124
+
125
+ {loading ? (
126
+ <p className="text-muted-foreground">{t('common.loading')}</p>
127
+ ) : (
128
+ <DataTable columns={columns} data={apps} />
129
+ )}
130
+
131
+ <MarketAppForm
132
+ open={formOpen}
133
+ onOpenChange={setFormOpen}
134
+ editData={editData}
135
+ onSuccess={fetchApps}
136
+ />
137
+
138
+ <AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
139
+ <AlertDialogContent>
140
+ <AlertDialogHeader>
141
+ <AlertDialogTitle>{t('market.deleteConfirmTitle')}</AlertDialogTitle>
142
+ <AlertDialogDescription>
143
+ {t('market.deleteConfirmDesc', { name: deleteTarget?.displayName })}
144
+ </AlertDialogDescription>
145
+ </AlertDialogHeader>
146
+ <AlertDialogFooter>
147
+ <AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
148
+ <AlertDialogAction onClick={handleDelete} className="bg-destructive text-white hover:bg-destructive/90">
149
+ {t('common.delete')}
150
+ </AlertDialogAction>
151
+ </AlertDialogFooter>
152
+ </AlertDialogContent>
153
+ </AlertDialog>
154
+ </div>
155
+ )
156
+ }
@@ -0,0 +1,143 @@
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 { useState } from 'react'
19
+ import { useTranslation } from 'react-i18next'
20
+ import { DataTable, type AdminColumnDef } from '@/components/data-table/data-table'
21
+
22
+ function formatRelativeTime(dateStr: string, t: (key: string, params?: Record<string, number | string>) => string): string {
23
+ const now = new Date()
24
+ const target = new Date(dateStr)
25
+ const diff = now.getTime() - target.getTime()
26
+ const seconds = Math.floor(diff / 1000)
27
+ if (seconds < 60) return t('common.timeAgo.seconds', { count: seconds })
28
+ const minutes = Math.floor(seconds / 60)
29
+ if (minutes < 60) return t('common.timeAgo.minutes', { count: minutes })
30
+ const hours = Math.floor(minutes / 60)
31
+ if (hours < 24) return t('common.timeAgo.hours', { count: hours })
32
+ const days = Math.floor(hours / 24)
33
+ return t('common.timeAgo.days', { count: days })
34
+ }
35
+
36
+ interface ErrorItem {
37
+ id: string
38
+ errorName: string
39
+ errorMessage: string
40
+ stackTrace?: string | null
41
+ count: number
42
+ firstSeen: string
43
+ lastSeen: string
44
+ context?: Record<string, unknown>
45
+ }
46
+
47
+ interface ErrorListProps {
48
+ errors: ErrorItem[]
49
+ }
50
+
51
+ export function ErrorList({ errors }: ErrorListProps) {
52
+ const { t } = useTranslation('common')
53
+ const [expandedId, setExpandedId] = useState<string | null>(null)
54
+
55
+ const columns: AdminColumnDef<ErrorItem>[] = [
56
+ {
57
+ header: t('errorList.columns.lastSeen'),
58
+ accessorKey: 'lastSeen',
59
+ meta: { style: { width: 60 } },
60
+ cell: ({ row }: { row: { original: ErrorItem } }) => (
61
+ <span className="text-xs text-muted-foreground">
62
+ {formatRelativeTime(row.original.lastSeen, t)}
63
+ </span>
64
+ ),
65
+ },
66
+ {
67
+ header: t('errorList.columns.errorName'),
68
+ accessorKey: 'errorName',
69
+ meta: { style: { width: 120 } },
70
+ cell: ({ row }: { row: { original: ErrorItem } }) => (
71
+ <span className="font-medium text-xs block truncate">
72
+ {row.original.errorName}
73
+ </span>
74
+ ),
75
+ },
76
+ {
77
+ header: t('errorList.columns.errorMessage'),
78
+ accessorKey: 'errorMessage',
79
+ meta: { style: { width: 280 } },
80
+ cell: ({ row }: { row: { original: ErrorItem } }) => {
81
+ const msg = row.original.errorMessage
82
+ return <span className="text-xs block truncate">{msg}</span>
83
+ },
84
+ },
85
+ {
86
+ header: t('errorList.columns.count'),
87
+ accessorKey: 'count',
88
+ meta: { style: { width: 60, textAlign: 'right' } },
89
+ cell: ({ row }: { row: { original: ErrorItem } }) => (
90
+ <span className="font-medium">{row.original.count}</span>
91
+ ),
92
+ },
93
+ {
94
+ header: t('errorList.columns.firstSeen'),
95
+ accessorKey: 'firstSeen',
96
+ meta: { style: { width: 60 } },
97
+ cell: ({ row }: { row: { original: ErrorItem } }) => (
98
+ <span className="text-xs text-muted-foreground">
99
+ {formatRelativeTime(row.original.firstSeen, t)}
100
+ </span>
101
+ ),
102
+ },
103
+ ]
104
+
105
+ return (
106
+ <div className="space-y-3">
107
+ <DataTable
108
+ columns={columns}
109
+ data={errors}
110
+ onRowClick={(item) => {
111
+ setExpandedId(expandedId === item.id ? null : item.id)
112
+ }}
113
+ />
114
+ {expandedId && (
115
+ <ErrorDetail errors={errors} expandedId={expandedId} t={t} />
116
+ )}
117
+ </div>
118
+ )
119
+ }
120
+
121
+ function ErrorDetail({ errors, expandedId, t }: { errors: ErrorItem[]; expandedId: string; t: (key: string) => string }) {
122
+ const error = errors.find((e) => e.id === expandedId)
123
+ if (!error) return null
124
+
125
+ return (
126
+ <div className="rounded-lg border bg-muted/30 p-4 space-y-3">
127
+ <div className="font-medium text-sm">{t('errorList.detail.title')}</div>
128
+ {error.stackTrace && (
129
+ <pre className="text-xs bg-background rounded-md p-3 overflow-auto max-h-40 border">
130
+ {error.stackTrace}
131
+ </pre>
132
+ )}
133
+ {error.context && JSON.stringify(error.context, null, 2) && (
134
+ <div>
135
+ <div className="text-xs font-medium text-muted-foreground mb-1">{t('errorList.detail.context')}</div>
136
+ <pre className="text-xs bg-background rounded-md p-3 overflow-auto border">
137
+ {JSON.stringify(error.context, null, 2)}
138
+ </pre>
139
+ </div>
140
+ )}
141
+ </div>
142
+ )
143
+ }
@@ -0,0 +1,152 @@
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
+
20
+ interface HealthCardData {
21
+ status?: string
22
+ checks?: {
23
+ uptime?: number
24
+ database?: {
25
+ status?: string
26
+ latency?: number
27
+ error?: string
28
+ }
29
+ memory?: {
30
+ rss?: number
31
+ heapUsed?: number
32
+ heapTotal?: number
33
+ }
34
+ cpu?: {
35
+ usage?: number
36
+ cores?: number
37
+ model?: string
38
+ }
39
+ load?: {
40
+ one?: number
41
+ five?: number
42
+ fifteen?: number
43
+ }
44
+ disk?: {
45
+ total?: number
46
+ free?: number
47
+ usedPct?: number
48
+ }
49
+ system?: {
50
+ platform?: string
51
+ type?: string
52
+ release?: string
53
+ nodeVersion?: string
54
+ }
55
+ }
56
+ requestStats?: {
57
+ total?: number
58
+ avgDuration?: number
59
+ }
60
+ }
61
+
62
+ function formatBytes(bytes: number): string {
63
+ if (bytes < 1024) return `${bytes}B`
64
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`
65
+ return `${(bytes / 1048576).toFixed(1)}MB`
66
+ }
67
+
68
+ interface HealthCardsProps {
69
+ health: HealthCardData
70
+ }
71
+
72
+ export function HealthCards({ health }: HealthCardsProps) {
73
+ const { t } = useTranslation('common')
74
+ const db = health.checks?.database
75
+ const mem = health.checks?.memory
76
+ const cpu = health.checks?.cpu
77
+ const load = health.checks?.load
78
+ const disk = health.checks?.disk
79
+ const system = health.checks?.system
80
+
81
+ return (
82
+ <div className="grid gap-3 md:grid-cols-3 lg:grid-cols-5">
83
+ <div className="rounded-lg border p-3">
84
+ <p className="text-xs text-muted-foreground">{t('monitoring.healthCards.database')}</p>
85
+ <p className={`text-sm font-semibold mt-1 ${db?.status === 'connected' ? 'text-green-600' : 'text-red-600'}`}>
86
+ {db?.status === 'connected' ? t('monitoring.healthCards.connected') : t('monitoring.healthCards.disconnected')}
87
+ </p>
88
+ {db?.status === 'connected' && db.latency != null && (
89
+ <p className="text-xs text-muted-foreground mt-0.5">{t('monitoring.healthCards.latency', { latency: db.latency })}ms</p>
90
+ )}
91
+ </div>
92
+
93
+ <div className="rounded-lg border p-3">
94
+ <p className="text-xs text-muted-foreground">{t('monitoring.healthCards.memoryRSS')}</p>
95
+ <p className="text-sm font-semibold mt-1">
96
+ {mem?.rss ? formatBytes(mem.rss) : '-'}
97
+ </p>
98
+ </div>
99
+
100
+ <div className="rounded-lg border p-3">
101
+ <p className="text-xs text-muted-foreground">{t('monitoring.healthCards.heapUsage')}</p>
102
+ <p className="text-sm font-semibold mt-1">
103
+ {mem?.heapUsed && mem?.heapTotal ? `${((mem.heapUsed / mem.heapTotal) * 100).toFixed(1)}%` : '-'}
104
+ </p>
105
+ <p className="text-xs text-muted-foreground mt-0.5">
106
+ {mem?.heapUsed ? formatBytes(mem.heapUsed) : '-'} / {mem?.heapTotal ? formatBytes(mem.heapTotal) : '-'}
107
+ </p>
108
+ </div>
109
+
110
+ <div className="rounded-lg border p-3">
111
+ <p className="text-xs text-muted-foreground">{t('monitoring.healthCards.cpuCores', { cores: cpu?.cores })}</p>
112
+ <p className="text-sm font-semibold mt-1">
113
+ {cpu?.usage != null ? `${cpu.usage.toFixed(1)}%` : '-'}
114
+ </p>
115
+ <p className="text-xs text-muted-foreground mt-0.5">
116
+ {cpu?.model ? cpu.model.split(' ').slice(0, 3).join(' ') : '-'}
117
+ </p>
118
+ </div>
119
+
120
+ <div className="rounded-lg border p-3">
121
+ <p className="text-xs text-muted-foreground">{t('monitoring.healthCards.systemLoad')}</p>
122
+ <p className="text-sm font-semibold mt-1">
123
+ {load?.one != null ? `${load.one.toFixed(2)} / ${load.five?.toFixed(2) ?? '-'} / ${load.fifteen?.toFixed(2) ?? '-'}` : '-'}
124
+ </p>
125
+ </div>
126
+
127
+ {disk && (
128
+ <div className="rounded-lg border p-3">
129
+ <p className="text-xs text-muted-foreground">{t('monitoring.healthCards.diskUsage')}</p>
130
+ <p className="text-sm font-semibold mt-1">
131
+ {disk.usedPct != null ? `${disk.usedPct.toFixed(1)}%` : '-'}
132
+ </p>
133
+ <p className="text-xs text-muted-foreground mt-0.5">
134
+ {t('monitoring.healthCards.available', { free: disk.free != null ? formatBytes(disk.free) : '-', total: disk.total != null ? formatBytes(disk.total) : '-' })}
135
+ </p>
136
+ </div>
137
+ )}
138
+
139
+ {system && (
140
+ <div className="rounded-lg border p-3">
141
+ <p className="text-xs text-muted-foreground">{t('monitoring.healthCards.runtime')}</p>
142
+ <p className="text-sm font-semibold mt-1">
143
+ {system.platform} {system.release}
144
+ </p>
145
+ <p className="text-xs text-muted-foreground mt-0.5">
146
+ {system.nodeVersion}
147
+ </p>
148
+ </div>
149
+ )}
150
+ </div>
151
+ )
152
+ }