vibecarbon 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 (495) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +188 -0
  3. package/carbon/.claude/hooks/task-completed-gate.sh +19 -0
  4. package/carbon/.claude/hooks/teammate-idle-gate.sh +29 -0
  5. package/carbon/.claude/settings.json +29 -0
  6. package/carbon/.cursor/rules/vibecarbon.mdc +6 -0
  7. package/carbon/.dockerignore +57 -0
  8. package/carbon/.env.example +219 -0
  9. package/carbon/.github/copilot-instructions.md +3 -0
  10. package/carbon/.github/workflows/deploy.yml +346 -0
  11. package/carbon/.github/workflows/vibecarbon-build.yml +81 -0
  12. package/carbon/.windsurfrules +74 -0
  13. package/carbon/AGENTS.md +422 -0
  14. package/carbon/CLAUDE.md +3 -0
  15. package/carbon/DEVELOPMENT.md +187 -0
  16. package/carbon/Dockerfile +98 -0
  17. package/carbon/LICENSE +21 -0
  18. package/carbon/PRODUCTION.md +364 -0
  19. package/carbon/README.md +193 -0
  20. package/carbon/TESTING.md +138 -0
  21. package/carbon/backup/Dockerfile +75 -0
  22. package/carbon/backup/backup.sh +95 -0
  23. package/carbon/backup/compose-backup.sh +140 -0
  24. package/carbon/biome.json +83 -0
  25. package/carbon/cloud-init/docker-ce-setup.yaml +55 -0
  26. package/carbon/cloud-init/k3s/master-init.sh +215 -0
  27. package/carbon/cloud-init/k3s/supabase-init.sh +167 -0
  28. package/carbon/cloud-init/k3s/worker-init.sh +147 -0
  29. package/carbon/components.json +24 -0
  30. package/carbon/content/blog/authentication-guide.mdx +34 -0
  31. package/carbon/content/blog/getting-started.mdx +41 -0
  32. package/carbon/content/changelog/v0-1-0.mdx +40 -0
  33. package/carbon/content/docs/analytics.mdx +90 -0
  34. package/carbon/content/docs/authentication.mdx +231 -0
  35. package/carbon/content/docs/background-jobs.mdx +116 -0
  36. package/carbon/content/docs/cli.mdx +630 -0
  37. package/carbon/content/docs/database.mdx +236 -0
  38. package/carbon/content/docs/deployment.mdx +227 -0
  39. package/carbon/content/docs/development.mdx +238 -0
  40. package/carbon/content/docs/environments.mdx +84 -0
  41. package/carbon/content/docs/getting-started.mdx +112 -0
  42. package/carbon/content/docs/legal/privacy-policy.mdx +89 -0
  43. package/carbon/content/docs/legal/terms-of-service.mdx +99 -0
  44. package/carbon/content/docs/optional-services.mdx +160 -0
  45. package/carbon/db/Dockerfile +23 -0
  46. package/carbon/docker-compose.dev-init.yml +5 -0
  47. package/carbon/docker-compose.metabase.override.yml +14 -0
  48. package/carbon/docker-compose.metabase.prod.yml +28 -0
  49. package/carbon/docker-compose.metabase.yml +84 -0
  50. package/carbon/docker-compose.n8n.override.yml +14 -0
  51. package/carbon/docker-compose.n8n.prod.yml +31 -0
  52. package/carbon/docker-compose.n8n.yml +97 -0
  53. package/carbon/docker-compose.observability.override.yml +18 -0
  54. package/carbon/docker-compose.observability.prod.yml +28 -0
  55. package/carbon/docker-compose.observability.yml +125 -0
  56. package/carbon/docker-compose.override.yml +28 -0
  57. package/carbon/docker-compose.prod.yml +294 -0
  58. package/carbon/docker-compose.yml +508 -0
  59. package/carbon/docker-entrypoint.sh +12 -0
  60. package/carbon/ha/activate-standby.sh +52 -0
  61. package/carbon/ha/primary-init.sql +36 -0
  62. package/carbon/ha/standby-init.sh +74 -0
  63. package/carbon/k8s/LICENSE +99 -0
  64. package/carbon/k8s/README.md +272 -0
  65. package/carbon/k8s/base/app/deployment.yaml +130 -0
  66. package/carbon/k8s/base/app/hpa.yaml +44 -0
  67. package/carbon/k8s/base/app/kustomization.yaml +10 -0
  68. package/carbon/k8s/base/app/network-policy.yaml +124 -0
  69. package/carbon/k8s/base/app/pdb.yaml +14 -0
  70. package/carbon/k8s/base/app/rbac.yaml +36 -0
  71. package/carbon/k8s/base/app/service.yaml +16 -0
  72. package/carbon/k8s/base/backup/cronjob.yaml +120 -0
  73. package/carbon/k8s/base/backup/kustomization.yaml +7 -0
  74. package/carbon/k8s/base/backup/network-policy.yaml +31 -0
  75. package/carbon/k8s/base/backup/rbac.yaml +7 -0
  76. package/carbon/k8s/base/cluster-autoscaler/deployment.yaml +103 -0
  77. package/carbon/k8s/base/cluster-autoscaler/kustomization.yaml +17 -0
  78. package/carbon/k8s/base/cluster-autoscaler/rbac.yaml +109 -0
  79. package/carbon/k8s/base/config/configmap.yaml +48 -0
  80. package/carbon/k8s/base/config/kustomization.yaml +8 -0
  81. package/carbon/k8s/base/hetzner-ccm/kustomization.yaml +9 -0
  82. package/carbon/k8s/base/hetzner-csi/kustomization.yaml +9 -0
  83. package/carbon/k8s/base/kustomization.yaml +43 -0
  84. package/carbon/k8s/base/namespace.yaml +7 -0
  85. package/carbon/k8s/base/network-policies.yaml +95 -0
  86. package/carbon/k8s/base/registry/kustomization.yaml +5 -0
  87. package/carbon/k8s/base/registry/local-registry.yaml +193 -0
  88. package/carbon/k8s/base/traefik/certificate.yaml +21 -0
  89. package/carbon/k8s/base/traefik/configmap.yaml +13 -0
  90. package/carbon/k8s/base/traefik/deployment.yaml +165 -0
  91. package/carbon/k8s/base/traefik/ingressroute.yaml +141 -0
  92. package/carbon/k8s/base/traefik/kustomization.yaml +11 -0
  93. package/carbon/k8s/base/traefik/middleware.yaml +109 -0
  94. package/carbon/k8s/base/traefik/network-policy.yaml +92 -0
  95. package/carbon/k8s/base/traefik/service.yaml +38 -0
  96. package/carbon/k8s/flux/README.md +49 -0
  97. package/carbon/k8s/flux/clusters/primary/vibecarbon.yaml +128 -0
  98. package/carbon/k8s/flux/clusters/standby/vibecarbon.yaml +83 -0
  99. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-release.yaml +38 -0
  100. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-repository.yaml +16 -0
  101. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/kustomization.yaml +10 -0
  102. package/carbon/k8s/gitops/supabase/helm-release.yaml +66 -0
  103. package/carbon/k8s/gitops/supabase/helm-repository.yaml +18 -0
  104. package/carbon/k8s/gitops/supabase/kustomization.yaml +33 -0
  105. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-cloudflare.yaml +51 -0
  106. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-hetzner.yaml +59 -0
  107. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-manual.yaml +43 -0
  108. package/carbon/k8s/infra/cert-manager-resources/kustomization.yaml +15 -0
  109. package/carbon/k8s/infra/kustomization.yaml +21 -0
  110. package/carbon/k8s/infra/traefik-crds/kustomization.yaml +10 -0
  111. package/carbon/k8s/overlays/local/configmap.yaml +23 -0
  112. package/carbon/k8s/overlays/local/ingressroute-studio.yaml +104 -0
  113. package/carbon/k8s/overlays/local/kustomization.yaml +215 -0
  114. package/carbon/k8s/overlays/local/namespace.yaml +8 -0
  115. package/carbon/k8s/overlays/local/secrets.yaml +32 -0
  116. package/carbon/k8s/overlays/production/kustomization.yaml +34 -0
  117. package/carbon/k8s/test-local.sh +318 -0
  118. package/carbon/k8s/values/supabase.values.yaml +133 -0
  119. package/carbon/package.json +154 -0
  120. package/carbon/runtime.Dockerfile +17 -0
  121. package/carbon/scripts/_dev-jwt.mjs +45 -0
  122. package/carbon/scripts/check-rls.ts +53 -0
  123. package/carbon/scripts/dev-init.js +191 -0
  124. package/carbon/scripts/dev.js +191 -0
  125. package/carbon/scripts/docker-down.js +63 -0
  126. package/carbon/scripts/docker-logs.js +59 -0
  127. package/carbon/scripts/docker-up.js +222 -0
  128. package/carbon/scripts/generate-dev-configs.sh +131 -0
  129. package/carbon/scripts/generate-rss.ts +116 -0
  130. package/carbon/scripts/generate-sitemap.ts +102 -0
  131. package/carbon/scripts/k8s-apply.js +71 -0
  132. package/carbon/scripts/k8s-delete.js +75 -0
  133. package/carbon/scripts/lib/manifest.js +176 -0
  134. package/carbon/scripts/secret-scan.mjs +278 -0
  135. package/carbon/scripts/validate-dev-configs.sh +101 -0
  136. package/carbon/src/client/App.tsx +202 -0
  137. package/carbon/src/client/assets/hyperformant-dark.svg +29 -0
  138. package/carbon/src/client/assets/hyperformant-light.svg +29 -0
  139. package/carbon/src/client/assets/logos/aider.svg +1 -0
  140. package/carbon/src/client/assets/logos/antigravity.svg +1 -0
  141. package/carbon/src/client/assets/logos/bolt.svg +1 -0
  142. package/carbon/src/client/assets/logos/claude-code.svg +1 -0
  143. package/carbon/src/client/assets/logos/copilot.svg +1 -0
  144. package/carbon/src/client/assets/logos/cursor.svg +1 -0
  145. package/carbon/src/client/assets/logos/gemini-cli.svg +1 -0
  146. package/carbon/src/client/assets/logos/lovable.svg +1 -0
  147. package/carbon/src/client/assets/logos/openai-codex.svg +1 -0
  148. package/carbon/src/client/assets/logos/v0.svg +1 -0
  149. package/carbon/src/client/assets/logos/windsurf.svg +1 -0
  150. package/carbon/src/client/assets/vibecarbon-icon.svg +19 -0
  151. package/carbon/src/client/assets/vibecarbon-logo-dark.svg +29 -0
  152. package/carbon/src/client/assets/vibecarbon-logo-light.svg +29 -0
  153. package/carbon/src/client/components/AIIntegrationSection.tsx +120 -0
  154. package/carbon/src/client/components/AppSidebar.tsx +760 -0
  155. package/carbon/src/client/components/ArchitectureSection.tsx +46 -0
  156. package/carbon/src/client/components/CTAFooter.tsx +59 -0
  157. package/carbon/src/client/components/ComparisonSection.tsx +132 -0
  158. package/carbon/src/client/components/ContentPanel.tsx +66 -0
  159. package/carbon/src/client/components/ContentSkeleton.tsx +21 -0
  160. package/carbon/src/client/components/ErrorBoundary.tsx +46 -0
  161. package/carbon/src/client/components/FAQSection.tsx +76 -0
  162. package/carbon/src/client/components/FileUpload.tsx +210 -0
  163. package/carbon/src/client/components/HeaderActions.tsx +17 -0
  164. package/carbon/src/client/components/Hero.tsx +608 -0
  165. package/carbon/src/client/components/ImpersonationBanner.tsx +31 -0
  166. package/carbon/src/client/components/LanguageSwitcher.tsx +67 -0
  167. package/carbon/src/client/components/Logo.tsx +87 -0
  168. package/carbon/src/client/components/LogoStrip.tsx +76 -0
  169. package/carbon/src/client/components/MetricsStrip.tsx +48 -0
  170. package/carbon/src/client/components/Nav.tsx +195 -0
  171. package/carbon/src/client/components/NewsletterSignup.tsx +147 -0
  172. package/carbon/src/client/components/NotificationDrawer.tsx +171 -0
  173. package/carbon/src/client/components/PageHeader.tsx +24 -0
  174. package/carbon/src/client/components/PlanGate.tsx +36 -0
  175. package/carbon/src/client/components/PricingSection.tsx +371 -0
  176. package/carbon/src/client/components/SEO.tsx +34 -0
  177. package/carbon/src/client/components/SmoothScroll.tsx +45 -0
  178. package/carbon/src/client/components/TechStackSection.tsx +165 -0
  179. package/carbon/src/client/components/WorkflowSection.tsx +604 -0
  180. package/carbon/src/client/components/admin/DockerLogs.tsx +276 -0
  181. package/carbon/src/client/components/admin/PerformanceMetrics.tsx +230 -0
  182. package/carbon/src/client/components/admin/ServiceCard.tsx +45 -0
  183. package/carbon/src/client/components/admin/ServicesStatus.tsx +296 -0
  184. package/carbon/src/client/components/architecture/ArchitectureDiagram.tsx +413 -0
  185. package/carbon/src/client/components/auth/AuthProvider.tsx +466 -0
  186. package/carbon/src/client/components/effects/FilmGrainOverlay.tsx +35 -0
  187. package/carbon/src/client/components/effects/FresnelEdge.tsx +49 -0
  188. package/carbon/src/client/components/effects/GlowTracker.tsx +46 -0
  189. package/carbon/src/client/components/effects/SparkBurst.tsx +145 -0
  190. package/carbon/src/client/components/effects/VGlowEffect.tsx +83 -0
  191. package/carbon/src/client/components/effects/index.ts +2 -0
  192. package/carbon/src/client/components/layouts/SidebarLayout.tsx +20 -0
  193. package/carbon/src/client/components/scroll/ScrollSection.tsx +76 -0
  194. package/carbon/src/client/components/scroll/ScrollytellingProvider.tsx +81 -0
  195. package/carbon/src/client/components/scroll/index.ts +2 -0
  196. package/carbon/src/client/components/ui/accordion.tsx +71 -0
  197. package/carbon/src/client/components/ui/alert-dialog.tsx +162 -0
  198. package/carbon/src/client/components/ui/alert.tsx +73 -0
  199. package/carbon/src/client/components/ui/aspect-ratio.tsx +22 -0
  200. package/carbon/src/client/components/ui/avatar.tsx +91 -0
  201. package/carbon/src/client/components/ui/badge.tsx +50 -0
  202. package/carbon/src/client/components/ui/breadcrumb.tsx +100 -0
  203. package/carbon/src/client/components/ui/button-group.tsx +78 -0
  204. package/carbon/src/client/components/ui/button.tsx +120 -0
  205. package/carbon/src/client/components/ui/calendar.tsx +182 -0
  206. package/carbon/src/client/components/ui/card.tsx +85 -0
  207. package/carbon/src/client/components/ui/carousel.tsx +227 -0
  208. package/carbon/src/client/components/ui/chart.tsx +357 -0
  209. package/carbon/src/client/components/ui/checkbox.tsx +27 -0
  210. package/carbon/src/client/components/ui/collapsible.tsx +15 -0
  211. package/carbon/src/client/components/ui/command.tsx +178 -0
  212. package/carbon/src/client/components/ui/context-menu.tsx +233 -0
  213. package/carbon/src/client/components/ui/dialog.tsx +132 -0
  214. package/carbon/src/client/components/ui/drawer.tsx +118 -0
  215. package/carbon/src/client/components/ui/dropdown-menu.tsx +242 -0
  216. package/carbon/src/client/components/ui/empty.tsx +94 -0
  217. package/carbon/src/client/components/ui/field.tsx +226 -0
  218. package/carbon/src/client/components/ui/hover-card.tsx +44 -0
  219. package/carbon/src/client/components/ui/input-group.tsx +146 -0
  220. package/carbon/src/client/components/ui/input-otp.tsx +83 -0
  221. package/carbon/src/client/components/ui/input.tsx +20 -0
  222. package/carbon/src/client/components/ui/item.tsx +188 -0
  223. package/carbon/src/client/components/ui/kbd.tsx +26 -0
  224. package/carbon/src/client/components/ui/label.tsx +21 -0
  225. package/carbon/src/client/components/ui/menubar.tsx +250 -0
  226. package/carbon/src/client/components/ui/navigation-menu.tsx +155 -0
  227. package/carbon/src/client/components/ui/pagination.tsx +102 -0
  228. package/carbon/src/client/components/ui/popover.tsx +75 -0
  229. package/carbon/src/client/components/ui/progress.tsx +66 -0
  230. package/carbon/src/client/components/ui/radio-group.tsx +36 -0
  231. package/carbon/src/client/components/ui/resizable.tsx +46 -0
  232. package/carbon/src/client/components/ui/scroll-area.tsx +48 -0
  233. package/carbon/src/client/components/ui/select.tsx +186 -0
  234. package/carbon/src/client/components/ui/separator.tsx +21 -0
  235. package/carbon/src/client/components/ui/sheet.tsx +124 -0
  236. package/carbon/src/client/components/ui/sidebar.tsx +702 -0
  237. package/carbon/src/client/components/ui/skeleton.tsx +13 -0
  238. package/carbon/src/client/components/ui/slider.tsx +57 -0
  239. package/carbon/src/client/components/ui/sonner.tsx +45 -0
  240. package/carbon/src/client/components/ui/spinner.tsx +15 -0
  241. package/carbon/src/client/components/ui/switch.tsx +30 -0
  242. package/carbon/src/client/components/ui/table.tsx +89 -0
  243. package/carbon/src/client/components/ui/tabs.tsx +73 -0
  244. package/carbon/src/client/components/ui/textarea.tsx +18 -0
  245. package/carbon/src/client/components/ui/toggle-group.tsx +87 -0
  246. package/carbon/src/client/components/ui/toggle.tsx +44 -0
  247. package/carbon/src/client/components/ui/tooltip.tsx +56 -0
  248. package/carbon/src/client/hooks/api/index.ts +14 -0
  249. package/carbon/src/client/hooks/api/useAuthSettings.ts +80 -0
  250. package/carbon/src/client/hooks/api/useSubscription.ts +87 -0
  251. package/carbon/src/client/hooks/use-mobile.ts +21 -0
  252. package/carbon/src/client/hooks/useMousePosition.ts +91 -0
  253. package/carbon/src/client/hooks/useNotifications.tsx +124 -0
  254. package/carbon/src/client/hooks/useOrganizationMembers.ts +127 -0
  255. package/carbon/src/client/hooks/useOrganizations.tsx +230 -0
  256. package/carbon/src/client/hooks/usePackageManager.ts +69 -0
  257. package/carbon/src/client/hooks/useReducedMotion.ts +20 -0
  258. package/carbon/src/client/hooks/useRunningServices.ts +114 -0
  259. package/carbon/src/client/hooks/useScrollProgress.ts +56 -0
  260. package/carbon/src/client/index.css +467 -0
  261. package/carbon/src/client/index.html +56 -0
  262. package/carbon/src/client/lib/admin-services.ts +151 -0
  263. package/carbon/src/client/lib/api.ts +32 -0
  264. package/carbon/src/client/lib/blog.ts +35 -0
  265. package/carbon/src/client/lib/changelog.ts +33 -0
  266. package/carbon/src/client/lib/docs-search.ts +101 -0
  267. package/carbon/src/client/lib/docs.ts +37 -0
  268. package/carbon/src/client/lib/i18n.ts +32 -0
  269. package/carbon/src/client/lib/supabase.ts +72 -0
  270. package/carbon/src/client/lib/tailwind-colors.ts +357 -0
  271. package/carbon/src/client/lib/theme.ts +117 -0
  272. package/carbon/src/client/lib/utils.ts +22 -0
  273. package/carbon/src/client/locales/de.json +529 -0
  274. package/carbon/src/client/locales/en.json +461 -0
  275. package/carbon/src/client/locales/es.json +529 -0
  276. package/carbon/src/client/locales/fr.json +529 -0
  277. package/carbon/src/client/locales/pt.json +529 -0
  278. package/carbon/src/client/main.tsx +56 -0
  279. package/carbon/src/client/mdx.d.ts +13 -0
  280. package/carbon/src/client/pages/ApiDocs.tsx +76 -0
  281. package/carbon/src/client/pages/AuthCallback.tsx +34 -0
  282. package/carbon/src/client/pages/Blog.tsx +167 -0
  283. package/carbon/src/client/pages/Changelog.tsx +171 -0
  284. package/carbon/src/client/pages/Charts.tsx +388 -0
  285. package/carbon/src/client/pages/Checkout.tsx +227 -0
  286. package/carbon/src/client/pages/Contact.tsx +174 -0
  287. package/carbon/src/client/pages/Dashboard.tsx +368 -0
  288. package/carbon/src/client/pages/Docs.tsx +372 -0
  289. package/carbon/src/client/pages/ForgotPassword.tsx +111 -0
  290. package/carbon/src/client/pages/Home.tsx +187 -0
  291. package/carbon/src/client/pages/Legal.tsx +100 -0
  292. package/carbon/src/client/pages/Login.tsx +408 -0
  293. package/carbon/src/client/pages/MFAVerify.tsx +156 -0
  294. package/carbon/src/client/pages/NotFound.tsx +21 -0
  295. package/carbon/src/client/pages/Onboarding.tsx +246 -0
  296. package/carbon/src/client/pages/ResetPassword.tsx +200 -0
  297. package/carbon/src/client/pages/UIComponents.tsx +390 -0
  298. package/carbon/src/client/pages/admin/ContactSubmissions.tsx +220 -0
  299. package/carbon/src/client/pages/admin/Dashboard.tsx +24 -0
  300. package/carbon/src/client/pages/admin/Infrastructure.tsx +257 -0
  301. package/carbon/src/client/pages/admin/Jobs.tsx +225 -0
  302. package/carbon/src/client/pages/admin/Logs.tsx +18 -0
  303. package/carbon/src/client/pages/admin/Newsletter.tsx +300 -0
  304. package/carbon/src/client/pages/admin/Notifications.tsx +603 -0
  305. package/carbon/src/client/pages/admin/Organizations.tsx +306 -0
  306. package/carbon/src/client/pages/admin/Settings.tsx +314 -0
  307. package/carbon/src/client/pages/admin/Theme.tsx +465 -0
  308. package/carbon/src/client/pages/admin/Users.tsx +365 -0
  309. package/carbon/src/client/pages/api-docs.css +156 -0
  310. package/carbon/src/client/pages/organizations/Details.tsx +200 -0
  311. package/carbon/src/client/pages/organizations/Members.tsx +402 -0
  312. package/carbon/src/client/pages/settings/Billing.tsx +473 -0
  313. package/carbon/src/client/pages/settings/Profile.tsx +160 -0
  314. package/carbon/src/client/pages/settings/Security.tsx +341 -0
  315. package/carbon/src/client/public/favicon.svg +19 -0
  316. package/carbon/src/client/public/robots.txt +8 -0
  317. package/carbon/src/server/billing/index.ts +104 -0
  318. package/carbon/src/server/billing/provider.ts +81 -0
  319. package/carbon/src/server/billing/providers/paddle.ts +314 -0
  320. package/carbon/src/server/billing/providers/polar.ts +325 -0
  321. package/carbon/src/server/billing/providers/stripe.ts +233 -0
  322. package/carbon/src/server/emails/templates.ts +116 -0
  323. package/carbon/src/server/index.ts +554 -0
  324. package/carbon/src/server/lib/auth.ts +6 -0
  325. package/carbon/src/server/lib/email.ts +64 -0
  326. package/carbon/src/server/lib/env.ts +112 -0
  327. package/carbon/src/server/lib/errors.ts +21 -0
  328. package/carbon/src/server/lib/logger.ts +17 -0
  329. package/carbon/src/server/lib/rate-limiter.ts +288 -0
  330. package/carbon/src/server/lib/request.ts +34 -0
  331. package/carbon/src/server/lib/stripe.ts +42 -0
  332. package/carbon/src/server/lib/supabase.ts +65 -0
  333. package/carbon/src/server/middleware/requirePlan.ts +80 -0
  334. package/carbon/src/server/routes/_internal/services-status.ts +958 -0
  335. package/carbon/src/server/routes/_internal/verify-role.ts +185 -0
  336. package/carbon/src/server/routes/health.ts +48 -0
  337. package/carbon/src/server/routes/v1/admin/contact.ts +128 -0
  338. package/carbon/src/server/routes/v1/admin/jobs.ts +171 -0
  339. package/carbon/src/server/routes/v1/admin/newsletter.ts +237 -0
  340. package/carbon/src/server/routes/v1/auth.ts +390 -0
  341. package/carbon/src/server/routes/v1/billing.ts +718 -0
  342. package/carbon/src/server/routes/v1/contact.ts +93 -0
  343. package/carbon/src/server/routes/v1/index.ts +1333 -0
  344. package/carbon/src/server/routes/v1/newsletter.ts +181 -0
  345. package/carbon/src/server/routes/v1/performance.ts +157 -0
  346. package/carbon/src/server/routes/v1/stats.ts +170 -0
  347. package/carbon/src/server/routes/v1/theme.ts +106 -0
  348. package/carbon/src/server/routes/webhooks/billing.ts +376 -0
  349. package/carbon/src/server/routes/webhooks/stripe.ts +276 -0
  350. package/carbon/src/server/types.ts +11 -0
  351. package/carbon/src/shared/pricing.ts +155 -0
  352. package/carbon/src/shared/types.ts +338 -0
  353. package/carbon/supabase/migrations/00001_init.sql +717 -0
  354. package/carbon/supabase/migrations/00002_theme_settings.sql +13 -0
  355. package/carbon/supabase/migrations/00003_pg_cron.sql +121 -0
  356. package/carbon/supabase/migrations/00004_contact_newsletter.sql +81 -0
  357. package/carbon/supabase/migrations/00005_localization_languages.sql +22 -0
  358. package/carbon/supabase/seed.sql +16 -0
  359. package/carbon/tests/_helpers/app.ts +45 -0
  360. package/carbon/tests/_helpers/env.ts +37 -0
  361. package/carbon/tests/_helpers/factories.ts +69 -0
  362. package/carbon/tests/_helpers/jwt.ts +23 -0
  363. package/carbon/tests/_helpers/setup-integration.ts +20 -0
  364. package/carbon/tests/_helpers/setup-rtl.ts +12 -0
  365. package/carbon/tests/component/ErrorBoundary.test.tsx +53 -0
  366. package/carbon/tests/component/use-auth-settings.test.tsx +119 -0
  367. package/carbon/tests/integration/server/routes/contact.test.ts +162 -0
  368. package/carbon/tests/integration/server/routes/health.test.ts +61 -0
  369. package/carbon/tests/structural/i18n-parity.test.ts +42 -0
  370. package/carbon/tests/unit/client/utils.test.ts +49 -0
  371. package/carbon/tests/unit/shared/pricing.test.ts +93 -0
  372. package/carbon/tsconfig.json +27 -0
  373. package/carbon/tsconfig.server.json +27 -0
  374. package/carbon/tsconfig.test.json +9 -0
  375. package/carbon/vite.config.ts +110 -0
  376. package/carbon/vitest.config.ts +74 -0
  377. package/carbon/volumes/db/jwt.sql +57 -0
  378. package/carbon/volumes/db/metabase-init.sh +29 -0
  379. package/carbon/volumes/db/n8n-init.sh +25 -0
  380. package/carbon/volumes/db/realtime.sql +33 -0
  381. package/carbon/volumes/db/roles.sql +93 -0
  382. package/carbon/volumes/db/set-passwords.sh +12 -0
  383. package/carbon/volumes/db/super-admin.dev.sql +113 -0
  384. package/carbon/volumes/db/super-admin.generated.sql +113 -0
  385. package/carbon/volumes/db/super-admin.sql +114 -0
  386. package/carbon/volumes/grafana/dashboards/logs.json +179 -0
  387. package/carbon/volumes/grafana/dashboards/overview.json +523 -0
  388. package/carbon/volumes/grafana/dashboards/postgresql.json +337 -0
  389. package/carbon/volumes/grafana/dashboards.dev/logs.json +179 -0
  390. package/carbon/volumes/grafana/dashboards.dev/overview.json +156 -0
  391. package/carbon/volumes/grafana/dashboards.dev/postgresql.json +337 -0
  392. package/carbon/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +16 -0
  393. package/carbon/volumes/grafana/provisioning/dashboards/dashboards.yml +16 -0
  394. package/carbon/volumes/grafana/provisioning/datasources/datasources.yml +43 -0
  395. package/carbon/volumes/kong/docker-entrypoint.sh +9 -0
  396. package/carbon/volumes/kong/kong.yml +208 -0
  397. package/carbon/volumes/loki/loki-config.yml +58 -0
  398. package/carbon/volumes/n8n/hooks.js +131 -0
  399. package/carbon/volumes/n8n/scripts/setup.sh +66 -0
  400. package/carbon/volumes/prometheus/prometheus.yml +43 -0
  401. package/carbon/volumes/promtail/promtail-config.yml +64 -0
  402. package/carbon/volumes/traefik/middlewares.dev.yml +83 -0
  403. package/carbon/volumes/traefik/middlewares.yml +95 -0
  404. package/carbon/volumes/traefik/vite-dev.yml +20 -0
  405. package/package.json +95 -0
  406. package/src/access.js +354 -0
  407. package/src/activate.js +187 -0
  408. package/src/add.js +718 -0
  409. package/src/backup.js +786 -0
  410. package/src/cli.js +350 -0
  411. package/src/configure.js +967 -0
  412. package/src/console.js +155 -0
  413. package/src/create.js +1499 -0
  414. package/src/deploy.js +311 -0
  415. package/src/destroy.js +2033 -0
  416. package/src/diagnose.js +735 -0
  417. package/src/down.js +80 -0
  418. package/src/failover.js +1032 -0
  419. package/src/lib/backup-s3.js +179 -0
  420. package/src/lib/build.js +33 -0
  421. package/src/lib/checksum.js +28 -0
  422. package/src/lib/ci-setup.js +666 -0
  423. package/src/lib/cli/help.js +129 -0
  424. package/src/lib/cli/parse-flags.js +160 -0
  425. package/src/lib/cli/select-action.js +65 -0
  426. package/src/lib/cli/select-environment.js +108 -0
  427. package/src/lib/cli/tty-guard.js +75 -0
  428. package/src/lib/cloudflare.js +447 -0
  429. package/src/lib/colors.js +52 -0
  430. package/src/lib/command.js +361 -0
  431. package/src/lib/config.js +359 -0
  432. package/src/lib/cost.js +103 -0
  433. package/src/lib/deploy/bundle.js +231 -0
  434. package/src/lib/deploy/compose/acme-verify.js +121 -0
  435. package/src/lib/deploy/compose/build-args.js +78 -0
  436. package/src/lib/deploy/compose/ha.js +1874 -0
  437. package/src/lib/deploy/compose/index.js +1294 -0
  438. package/src/lib/deploy/compose/reconcile.js +74 -0
  439. package/src/lib/deploy/github.js +382 -0
  440. package/src/lib/deploy/image.js +191 -0
  441. package/src/lib/deploy/index.js +119 -0
  442. package/src/lib/deploy/k8s/LICENSE +99 -0
  443. package/src/lib/deploy/k8s/gitops-deploy.js +298 -0
  444. package/src/lib/deploy/k8s/ha/index.js +1000 -0
  445. package/src/lib/deploy/k8s/index.js +62 -0
  446. package/src/lib/deploy/k8s/k3s.js +2515 -0
  447. package/src/lib/deploy/orchestrator.js +1401 -0
  448. package/src/lib/deploy/prompts.js +545 -0
  449. package/src/lib/deploy/remote-build.js +130 -0
  450. package/src/lib/deploy/state.js +120 -0
  451. package/src/lib/deploy/utils.js +328 -0
  452. package/src/lib/deploy-logger.js +93 -0
  453. package/src/lib/dns-propagation.js +48 -0
  454. package/src/lib/environment.js +58 -0
  455. package/src/lib/fetch-retry.js +103 -0
  456. package/src/lib/github-environments.js +335 -0
  457. package/src/lib/hetzner-dns.js +377 -0
  458. package/src/lib/hetzner-guided-setup.js +275 -0
  459. package/src/lib/host-keys.js +37 -0
  460. package/src/lib/iac/cloud-init.js +52 -0
  461. package/src/lib/iac/index.js +325 -0
  462. package/src/lib/iac/programs/hetzner-compose.js +130 -0
  463. package/src/lib/iac/programs/hetzner-k8s.js +320 -0
  464. package/src/lib/kubectl.js +81 -0
  465. package/src/lib/licensing/index.js +259 -0
  466. package/src/lib/licensing/tiers.js +181 -0
  467. package/src/lib/licensing/validator.js +171 -0
  468. package/src/lib/merge-package-json.js +90 -0
  469. package/src/lib/operator-ip.js +381 -0
  470. package/src/lib/package-manager.js +111 -0
  471. package/src/lib/perf.js +71 -0
  472. package/src/lib/project-guard.js +39 -0
  473. package/src/lib/project.js +334 -0
  474. package/src/lib/providers/base.js +276 -0
  475. package/src/lib/providers/hetzner-s3.js +656 -0
  476. package/src/lib/providers/hetzner.js +755 -0
  477. package/src/lib/providers/index.js +164 -0
  478. package/src/lib/s3.js +510 -0
  479. package/src/lib/secret-scan.js +583 -0
  480. package/src/lib/secrets.js +63 -0
  481. package/src/lib/server-types.js +195 -0
  482. package/src/lib/shell.js +91 -0
  483. package/src/lib/ssh.js +241 -0
  484. package/src/lib/tracker.js +242 -0
  485. package/src/lib/upgrade-policy.js +170 -0
  486. package/src/lib/validators.js +105 -0
  487. package/src/lib/version.js +5 -0
  488. package/src/remove.js +292 -0
  489. package/src/reset.js +97 -0
  490. package/src/restore.js +871 -0
  491. package/src/scale.js +1734 -0
  492. package/src/shell.js +222 -0
  493. package/src/status.js +981 -0
  494. package/src/up.js +264 -0
  495. package/src/upgrade.js +721 -0
@@ -0,0 +1,276 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { AlertCircle, ChevronDown, ExternalLink, Loader2, RefreshCw, Terminal } from 'lucide-react';
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { Button } from '@/components/ui/button';
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuTrigger,
10
+ } from '@/components/ui/dropdown-menu';
11
+ import { getAuthHeaders } from '@/lib/api';
12
+ import { cn } from '@/lib/utils';
13
+
14
+ interface LogsResponse {
15
+ success: boolean;
16
+ logs: string[];
17
+ error?: string;
18
+ meta?: {
19
+ tail: number;
20
+ since: string;
21
+ container: string;
22
+ lineCount: number;
23
+ timestamp: string;
24
+ };
25
+ }
26
+
27
+ interface ContainersResponse {
28
+ containers: Array<{
29
+ name: string;
30
+ container: string;
31
+ }>;
32
+ }
33
+
34
+ const TIME_OPTIONS = [
35
+ { label: 'Last 5 minutes', value: '5m' },
36
+ { label: 'Last 10 minutes', value: '10m' },
37
+ { label: 'Last 30 minutes', value: '30m' },
38
+ { label: 'Last 1 hour', value: '1h' },
39
+ { label: 'Last 6 hours', value: '6h' },
40
+ { label: 'Last 24 hours', value: '24h' },
41
+ ];
42
+
43
+ const TAIL_OPTIONS = [
44
+ { label: '100 lines', value: 100 },
45
+ { label: '200 lines', value: 200 },
46
+ { label: '500 lines', value: 500 },
47
+ { label: '1000 lines', value: 1000 },
48
+ ];
49
+
50
+ async function fetchLogs(container: string, since: string, tail: number): Promise<LogsResponse> {
51
+ const params = new URLSearchParams({
52
+ since,
53
+ tail: String(tail),
54
+ });
55
+ if (container !== 'all') {
56
+ params.set('container', container);
57
+ }
58
+ const headers = await getAuthHeaders();
59
+ const response = await fetch(`/api/_internal/services/status/logs?${params}`, { headers });
60
+ return response.json();
61
+ }
62
+
63
+ async function fetchContainers(): Promise<ContainersResponse> {
64
+ const headers = await getAuthHeaders();
65
+ const response = await fetch('/api/_internal/services/status/logs/containers', { headers });
66
+ return response.json();
67
+ }
68
+
69
+ // Color code log lines based on content
70
+ function getLogLineClass(line: string): string {
71
+ const lowerLine = line.toLowerCase();
72
+ if (lowerLine.includes('error') || lowerLine.includes('fatal') || lowerLine.includes('panic')) {
73
+ return 'text-destructive';
74
+ }
75
+ if (lowerLine.includes('warn')) {
76
+ return 'text-warning';
77
+ }
78
+ if (lowerLine.includes('debug') || lowerLine.includes('trace')) {
79
+ return 'text-gray-500';
80
+ }
81
+ return '';
82
+ }
83
+
84
+ // Extract container name from log line (format: "container-name | timestamp log...")
85
+ function parseLogLine(line: string): { container?: string; content: string } {
86
+ const match = line.match(/^([a-z0-9_-]+)\s+\|\s+(.*)$/i);
87
+ if (match) {
88
+ return { container: match[1], content: match[2] };
89
+ }
90
+ return { content: line };
91
+ }
92
+
93
+ const projectName = import.meta.env.VITE_PROJECT_NAME ?? '{{PROJECT_NAME}}';
94
+
95
+ function getGrafanaLogsUrl(): string {
96
+ const { protocol, hostname } = window.location;
97
+ const baseDomain = hostname.includes('localhost')
98
+ ? 'localhost'
99
+ : hostname.replace(/^[^.]+\./, '');
100
+ return `${protocol}//grafana.${baseDomain}/d/${projectName}-logs/logs`;
101
+ }
102
+
103
+ interface DockerLogsProps {
104
+ showGrafanaLink?: boolean;
105
+ }
106
+
107
+ export function DockerLogs({ showGrafanaLink = false }: DockerLogsProps) {
108
+ const [selectedContainer, setSelectedContainer] = useState('all');
109
+ const [since, setSince] = useState('10m');
110
+ const [tail, setTail] = useState(200);
111
+ const [autoScroll, setAutoScroll] = useState(false);
112
+ const logsEndRef = useRef<HTMLDivElement>(null);
113
+
114
+ const { data: containersData } = useQuery({
115
+ queryKey: ['docker-containers'],
116
+ queryFn: fetchContainers,
117
+ });
118
+
119
+ const {
120
+ data: logsData,
121
+ isLoading,
122
+ isFetching,
123
+ error,
124
+ refetch,
125
+ } = useQuery({
126
+ queryKey: ['docker-logs', selectedContainer, since, tail],
127
+ queryFn: () => fetchLogs(selectedContainer, since, tail),
128
+ refetchInterval: 10000, // Auto-refresh every 10 seconds
129
+ staleTime: 5000,
130
+ });
131
+
132
+ // Auto-scroll to bottom when new logs arrive
133
+ useEffect(() => {
134
+ if (autoScroll && logsEndRef.current && logsData?.logs) {
135
+ logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
136
+ }
137
+ }, [logsData?.logs, autoScroll]);
138
+
139
+ const containers = containersData?.containers || [];
140
+ const logs = logsData?.logs || [];
141
+ const selectedTimeOption = TIME_OPTIONS.find((o) => o.value === since);
142
+ const selectedTailOption = TAIL_OPTIONS.find((o) => o.value === tail);
143
+ const selectedContainerName =
144
+ selectedContainer === 'all'
145
+ ? 'All Containers'
146
+ : containers.find((c) => c.container === selectedContainer)?.name || selectedContainer;
147
+
148
+ const handleOpenGrafana = () => {
149
+ window.open(getGrafanaLogsUrl(), '_blank', 'noopener,noreferrer');
150
+ };
151
+
152
+ return (
153
+ <>
154
+ {/* Header with filters and actions */}
155
+ <div className="flex flex-wrap items-center justify-between gap-2 shrink-0">
156
+ {/* Filters */}
157
+ <div className="flex flex-wrap items-center gap-2">
158
+ {/* Container filter */}
159
+ <DropdownMenu>
160
+ <DropdownMenuTrigger className="inline-flex items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-1.5 text-sm font-medium ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 min-w-[140px]">
161
+ {selectedContainerName}
162
+ <ChevronDown className="size-4" />
163
+ </DropdownMenuTrigger>
164
+ <DropdownMenuContent align="start">
165
+ <DropdownMenuItem onClick={() => setSelectedContainer('all')}>
166
+ All Containers
167
+ </DropdownMenuItem>
168
+ {containers.map((c) => (
169
+ <DropdownMenuItem
170
+ key={c.container}
171
+ onClick={() => setSelectedContainer(c.container)}
172
+ >
173
+ {c.name}
174
+ </DropdownMenuItem>
175
+ ))}
176
+ </DropdownMenuContent>
177
+ </DropdownMenu>
178
+
179
+ {/* Time filter */}
180
+ <DropdownMenu>
181
+ <DropdownMenuTrigger className="inline-flex items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-1.5 text-sm font-medium ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 min-w-[140px]">
182
+ {selectedTimeOption?.label}
183
+ <ChevronDown className="size-4" />
184
+ </DropdownMenuTrigger>
185
+ <DropdownMenuContent align="start">
186
+ {TIME_OPTIONS.map((option) => (
187
+ <DropdownMenuItem key={option.value} onClick={() => setSince(option.value)}>
188
+ {option.label}
189
+ </DropdownMenuItem>
190
+ ))}
191
+ </DropdownMenuContent>
192
+ </DropdownMenu>
193
+
194
+ {/* Tail filter */}
195
+ <DropdownMenu>
196
+ <DropdownMenuTrigger className="inline-flex items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-1.5 text-sm font-medium ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 min-w-[120px]">
197
+ {selectedTailOption?.label}
198
+ <ChevronDown className="size-4" />
199
+ </DropdownMenuTrigger>
200
+ <DropdownMenuContent align="start">
201
+ {TAIL_OPTIONS.map((option) => (
202
+ <DropdownMenuItem key={option.value} onClick={() => setTail(option.value)}>
203
+ {option.label}
204
+ </DropdownMenuItem>
205
+ ))}
206
+ </DropdownMenuContent>
207
+ </DropdownMenu>
208
+
209
+ {/* Auto-scroll toggle */}
210
+ <Button
211
+ variant={autoScroll ? 'secondary' : 'outline'}
212
+ size="sm"
213
+ onClick={() => setAutoScroll(!autoScroll)}
214
+ >
215
+ Auto-scroll {autoScroll ? 'ON' : 'OFF'}
216
+ </Button>
217
+
218
+ {/* Refresh button */}
219
+ <Button variant="outline" size="sm" onClick={() => refetch()} disabled={isFetching}>
220
+ <RefreshCw className={cn('size-4', isFetching && 'animate-spin')} />
221
+ </Button>
222
+ </div>
223
+
224
+ {/* Grafana link */}
225
+ {showGrafanaLink && (
226
+ <Button onClick={handleOpenGrafana} size="sm">
227
+ <ExternalLink className="mr-2 size-4" />
228
+ Open Grafana Logs
229
+ </Button>
230
+ )}
231
+ </div>
232
+
233
+ {/* Terminal section - fills remaining space */}
234
+ {isLoading ? (
235
+ <div className="flex min-h-0 flex-1 items-center justify-center rounded-lg border bg-zinc-950">
236
+ <Loader2 className="size-6 animate-spin text-muted-foreground" />
237
+ </div>
238
+ ) : error || (logsData && !logsData.success) ? (
239
+ <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-4 rounded-lg border bg-zinc-950 text-center">
240
+ <AlertCircle className="size-8 text-destructive" />
241
+ <div>
242
+ <p className="font-medium text-zinc-200">Failed to load logs</p>
243
+ <p className="text-sm text-zinc-400">
244
+ {logsData?.error || 'Could not connect to Docker'}
245
+ </p>
246
+ </div>
247
+ <Button variant="outline" size="sm" onClick={() => refetch()}>
248
+ <RefreshCw className="mr-2 size-4" />
249
+ Retry
250
+ </Button>
251
+ </div>
252
+ ) : logs.length === 0 ? (
253
+ <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-2 rounded-lg border bg-zinc-950 text-center">
254
+ <Terminal className="size-8 text-zinc-500" />
255
+ <p className="text-zinc-400">No logs found for the selected filters</p>
256
+ </div>
257
+ ) : (
258
+ <div className="min-h-0 flex-1 overflow-y-auto overflow-x-auto rounded-lg border bg-zinc-950 p-4 font-mono text-xs">
259
+ <div>
260
+ {logs.map((line, index) => {
261
+ const { container, content } = parseLogLine(line);
262
+ const lineKey = `${index}-${line.slice(0, 50)}`;
263
+ return (
264
+ <div key={lineKey} className={cn('whitespace-pre', getLogLineClass(line))}>
265
+ {container && <span className="mr-2 text-info">[{container}]</span>}
266
+ <span className="text-zinc-300">{content}</span>
267
+ </div>
268
+ );
269
+ })}
270
+ <div ref={logsEndRef} />
271
+ </div>
272
+ </div>
273
+ )}
274
+ </>
275
+ );
276
+ }
@@ -0,0 +1,230 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { Activity, Clock, Cpu, HeartPulse } from 'lucide-react';
3
+ import { Area, AreaChart, Bar, BarChart, Cell, XAxis, YAxis } from 'recharts';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import {
6
+ type ChartConfig,
7
+ ChartContainer,
8
+ ChartTooltip,
9
+ ChartTooltipContent,
10
+ } from '@/components/ui/chart';
11
+ import { Skeleton } from '@/components/ui/skeleton';
12
+ import { getAuthHeaders } from '@/lib/api';
13
+
14
+ interface PerformanceData {
15
+ server: {
16
+ uptimeSeconds: number;
17
+ memoryMB: { rss: number; heapUsed: number; heapTotal: number };
18
+ nodeVersion: string;
19
+ platform: string;
20
+ };
21
+ services: {
22
+ healthy: number;
23
+ total: number;
24
+ avgLatencyMs: number;
25
+ items: Array<{
26
+ name: string;
27
+ latencyMs: number;
28
+ status: 'healthy' | 'unhealthy' | 'unknown';
29
+ }>;
30
+ };
31
+ }
32
+
33
+ interface StatsResponse {
34
+ users: {
35
+ weeklySignups: Array<{ week: string; count: number }>;
36
+ };
37
+ }
38
+
39
+ function formatUptime(seconds: number): string {
40
+ const days = Math.floor(seconds / 86400);
41
+ const hours = Math.floor((seconds % 86400) / 3600);
42
+ const minutes = Math.floor((seconds % 3600) / 60);
43
+ return `${days}d ${hours}h ${minutes}m`;
44
+ }
45
+
46
+ function getLatencyColor(ms: number): string {
47
+ if (ms < 50) return 'var(--chart-4)';
48
+ if (ms < 200) return 'var(--chart-2)';
49
+ return 'var(--chart-5)';
50
+ }
51
+
52
+ const latencyConfig = {
53
+ latencyMs: { label: 'Latency', color: 'var(--chart-1)' },
54
+ } satisfies ChartConfig;
55
+
56
+ const signupsConfig = {
57
+ count: { label: 'Signups', color: 'var(--chart-1)' },
58
+ } satisfies ChartConfig;
59
+
60
+ export function PerformanceMetrics() {
61
+ const { data: perfData, isLoading: perfLoading } = useQuery<PerformanceData>({
62
+ queryKey: ['admin', 'performance'],
63
+ queryFn: async () => {
64
+ const headers = await getAuthHeaders();
65
+ const res = await fetch('/api/v1/admin/performance', { headers });
66
+ if (!res.ok) throw new Error('Failed to fetch performance data');
67
+ return res.json();
68
+ },
69
+ refetchInterval: 30_000,
70
+ });
71
+
72
+ const { data: statsData, isLoading: statsLoading } = useQuery<StatsResponse>({
73
+ queryKey: ['admin', 'stats'],
74
+ queryFn: async () => {
75
+ const headers = await getAuthHeaders();
76
+ const res = await fetch('/api/v1/admin/stats', { headers });
77
+ if (!res.ok) throw new Error('Failed to fetch stats');
78
+ return res.json();
79
+ },
80
+ refetchInterval: 30_000,
81
+ });
82
+
83
+ const sortedServices = perfData
84
+ ? [...perfData.services.items].sort((a, b) => b.latencyMs - a.latencyMs)
85
+ : [];
86
+
87
+ const weeklySignups = statsData?.users?.weeklySignups ?? [];
88
+
89
+ return (
90
+ <div className="space-y-6">
91
+ {/* KPI Cards */}
92
+ <div className="grid grid-cols-2 gap-4">
93
+ <Card>
94
+ <CardHeader className="pb-2">
95
+ <CardTitle className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
96
+ <Clock className="size-4" />
97
+ Uptime
98
+ </CardTitle>
99
+ </CardHeader>
100
+ <CardContent>
101
+ {perfLoading ? (
102
+ <Skeleton className="h-7 w-24" />
103
+ ) : (
104
+ <p className="text-2xl font-bold">
105
+ {perfData ? formatUptime(perfData.server.uptimeSeconds) : '--'}
106
+ </p>
107
+ )}
108
+ </CardContent>
109
+ </Card>
110
+
111
+ <Card>
112
+ <CardHeader className="pb-2">
113
+ <CardTitle className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
114
+ <Activity className="size-4" />
115
+ Avg API Latency
116
+ </CardTitle>
117
+ </CardHeader>
118
+ <CardContent>
119
+ {perfLoading ? (
120
+ <Skeleton className="h-7 w-16" />
121
+ ) : (
122
+ <p className="text-2xl font-bold">
123
+ {perfData ? `${Math.round(perfData.services.avgLatencyMs)}ms` : '--'}
124
+ </p>
125
+ )}
126
+ </CardContent>
127
+ </Card>
128
+
129
+ <Card>
130
+ <CardHeader className="pb-2">
131
+ <CardTitle className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
132
+ <Cpu className="size-4" />
133
+ Memory Used
134
+ </CardTitle>
135
+ </CardHeader>
136
+ <CardContent>
137
+ {perfLoading ? (
138
+ <Skeleton className="h-7 w-20" />
139
+ ) : (
140
+ <p className="text-2xl font-bold">
141
+ {perfData ? `${Math.round(perfData.server.memoryMB.heapUsed)} MB` : '--'}
142
+ </p>
143
+ )}
144
+ </CardContent>
145
+ </Card>
146
+
147
+ <Card>
148
+ <CardHeader className="pb-2">
149
+ <CardTitle className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
150
+ <HeartPulse className="size-4" />
151
+ Healthy Services
152
+ </CardTitle>
153
+ </CardHeader>
154
+ <CardContent>
155
+ {perfLoading ? (
156
+ <Skeleton className="h-7 w-16" />
157
+ ) : (
158
+ <p className="text-2xl font-bold">
159
+ {perfData ? `${perfData.services.healthy} / ${perfData.services.total}` : '--'}
160
+ </p>
161
+ )}
162
+ </CardContent>
163
+ </Card>
164
+ </div>
165
+
166
+ {/* Service Latency Chart */}
167
+ <Card>
168
+ <CardHeader>
169
+ <CardTitle>Service Latency</CardTitle>
170
+ </CardHeader>
171
+ <CardContent>
172
+ {perfLoading ? (
173
+ <Skeleton className="h-[200px] w-full" />
174
+ ) : sortedServices.length > 0 ? (
175
+ <ChartContainer config={latencyConfig} className="h-[200px] w-full">
176
+ <BarChart data={sortedServices} layout="vertical">
177
+ <XAxis type="number" tickLine={false} axisLine={false} unit="ms" />
178
+ <YAxis
179
+ type="category"
180
+ dataKey="name"
181
+ tickLine={false}
182
+ axisLine={false}
183
+ width={80}
184
+ tick={{ fontSize: 12 }}
185
+ />
186
+ <ChartTooltip content={<ChartTooltipContent />} />
187
+ <Bar dataKey="latencyMs" radius={4}>
188
+ {sortedServices.map((entry) => (
189
+ <Cell key={entry.name} fill={getLatencyColor(entry.latencyMs)} />
190
+ ))}
191
+ </Bar>
192
+ </BarChart>
193
+ </ChartContainer>
194
+ ) : (
195
+ <p className="text-sm text-muted-foreground">No service data available.</p>
196
+ )}
197
+ </CardContent>
198
+ </Card>
199
+
200
+ {/* User Growth Chart */}
201
+ <Card>
202
+ <CardHeader>
203
+ <CardTitle>User Growth</CardTitle>
204
+ </CardHeader>
205
+ <CardContent>
206
+ {statsLoading ? (
207
+ <Skeleton className="h-[200px] w-full" />
208
+ ) : weeklySignups.length > 0 ? (
209
+ <ChartContainer config={signupsConfig} className="h-[200px] w-full">
210
+ <AreaChart data={weeklySignups}>
211
+ <XAxis dataKey="week" tickLine={false} axisLine={false} />
212
+ <YAxis tickLine={false} axisLine={false} />
213
+ <ChartTooltip content={<ChartTooltipContent />} />
214
+ <Area
215
+ dataKey="count"
216
+ stroke="var(--color-count)"
217
+ fill="var(--color-count)"
218
+ fillOpacity={0.15}
219
+ strokeWidth={2}
220
+ />
221
+ </AreaChart>
222
+ </ChartContainer>
223
+ ) : (
224
+ <p className="text-sm text-muted-foreground">No signup data available.</p>
225
+ )}
226
+ </CardContent>
227
+ </Card>
228
+ </div>
229
+ );
230
+ }
@@ -0,0 +1,45 @@
1
+ import { ExternalLink } from 'lucide-react';
2
+ import { Button } from '@/components/ui/button';
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
4
+ import { type AdminService, getServiceUrl } from '@/lib/admin-services';
5
+
6
+ interface ServiceCardProps {
7
+ service: AdminService;
8
+ }
9
+
10
+ export function ServiceCard({ service }: ServiceCardProps) {
11
+ const url = getServiceUrl(service);
12
+
13
+ const handleOpen = () => {
14
+ if (url) window.open(url, '_blank', 'noopener,noreferrer');
15
+ };
16
+
17
+ return (
18
+ <Card className="flex flex-col">
19
+ <CardHeader>
20
+ <div className="flex items-center gap-3">
21
+ <div className="flex size-10 items-center justify-center rounded-lg bg-muted">
22
+ <service.icon className="size-5 text-muted-foreground" />
23
+ </div>
24
+ <div>
25
+ <CardTitle>{service.name}</CardTitle>
26
+ <CardDescription>{service.description}</CardDescription>
27
+ </div>
28
+ </div>
29
+ </CardHeader>
30
+ <CardContent className="flex flex-1 flex-col justify-between gap-4">
31
+ <p className="text-sm text-muted-foreground">{service.subtitle}</p>
32
+ {url ? (
33
+ <Button variant="outline" onClick={handleOpen} className="w-full">
34
+ Open
35
+ <ExternalLink className="ml-2 size-4" data-icon="inline-end" />
36
+ </Button>
37
+ ) : (
38
+ <Button variant="outline" disabled className="w-full">
39
+ Internal only
40
+ </Button>
41
+ )}
42
+ </CardContent>
43
+ </Card>
44
+ );
45
+ }