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,181 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import { Hono } from 'hono';
3
+ import { z } from 'zod';
4
+ import { sendEmail } from '../../lib/email';
5
+ import { env } from '../../lib/env';
6
+ import { sanitizeError } from '../../lib/errors';
7
+ import { logger } from '../../lib/logger';
8
+ import { createRateLimiter } from '../../lib/rate-limiter';
9
+ import { supabaseAdmin } from '../../lib/supabase';
10
+ import type { HonoVariables } from '../../types';
11
+
12
+ // biome-ignore lint/suspicious/noExplicitAny: Tables not yet in generated Database types
13
+ const adminDb = supabaseAdmin as SupabaseClient<any>;
14
+
15
+ const newsletterRoutes = new Hono<{ Variables: HonoVariables }>();
16
+
17
+ // Rate limiting for subscribe (10 per 15 minutes per IP)
18
+ newsletterRoutes.use('/subscribe', createRateLimiter({ windowMs: 15 * 60 * 1000, max: 10 }));
19
+
20
+ const subscribeSchema = z.object({
21
+ email: z.string().email('Valid email is required').max(200),
22
+ name: z.string().max(200).optional(),
23
+ });
24
+
25
+ /**
26
+ * Subscribe to newsletter (public, rate-limited)
27
+ * Uses double opt-in: sends confirmation email with token.
28
+ */
29
+ newsletterRoutes.post('/subscribe', async (c) => {
30
+ let body: z.infer<typeof subscribeSchema>;
31
+ try {
32
+ const rawBody = await c.req.json();
33
+ const result = subscribeSchema.safeParse(rawBody);
34
+ if (!result.success) {
35
+ return c.json({ error: result.error.issues.map((e) => e.message).join(', ') }, 400);
36
+ }
37
+ body = result.data;
38
+ } catch {
39
+ return c.json({ error: 'Invalid JSON body' }, 400);
40
+ }
41
+
42
+ try {
43
+ // Check if already subscribed
44
+ const { data: existing } = await adminDb
45
+ .from('newsletter_subscribers')
46
+ .select('id, status')
47
+ .eq('email', body.email)
48
+ .maybeSingle();
49
+
50
+ if (existing?.status === 'active') {
51
+ // Already subscribed — don't reveal this to prevent email enumeration
52
+ return c.json({ success: true, message: 'Check your email to confirm your subscription.' });
53
+ }
54
+
55
+ if (existing?.status === 'pending') {
56
+ // Resend confirmation
57
+ return c.json({ success: true, message: 'Check your email to confirm your subscription.' });
58
+ }
59
+
60
+ // Insert or update (if previously unsubscribed)
61
+ const { data: subscriber, error: insertError } = await adminDb
62
+ .from('newsletter_subscribers')
63
+ .upsert(
64
+ {
65
+ email: body.email,
66
+ name: body.name || null,
67
+ status: 'pending',
68
+ confirmation_token: crypto.randomUUID(),
69
+ unsubscribed_at: null,
70
+ },
71
+ { onConflict: 'email' }
72
+ )
73
+ .select('confirmation_token')
74
+ .single();
75
+
76
+ if (insertError) {
77
+ logger.error({ error: insertError }, 'Failed to create newsletter subscriber');
78
+ return c.json({ error: sanitizeError(insertError, 'Failed to subscribe') }, 500);
79
+ }
80
+
81
+ // Send confirmation email (double opt-in)
82
+ const baseUrl = env.SITE_URL || 'http://localhost:5173';
83
+ const confirmUrl = `${baseUrl}/api/v1/newsletter/confirm?token=${subscriber.confirmation_token}`;
84
+
85
+ sendEmail({
86
+ to: body.email,
87
+ subject: 'Confirm your newsletter subscription',
88
+ html: `<h2>Confirm Your Subscription</h2>
89
+ <p>Click the link below to confirm your newsletter subscription:</p>
90
+ <p style="text-align: center; margin: 24px 0;">
91
+ <a href="${confirmUrl}" style="display: inline-block; padding: 10px 24px; background: #18181b; color: #fff; text-decoration: none; border-radius: 6px; font-weight: 500;">Confirm Subscription</a>
92
+ </p>
93
+ <p style="font-size: 12px; color: #71717a;">If you didn't request this, you can safely ignore this email.</p>`,
94
+ }).catch((err) => {
95
+ logger.error({ error: err }, 'Failed to send newsletter confirmation email');
96
+ });
97
+
98
+ return c.json({ success: true, message: 'Check your email to confirm your subscription.' });
99
+ } catch (error) {
100
+ return c.json({ error: sanitizeError(error, 'Failed to subscribe') }, 500);
101
+ }
102
+ });
103
+
104
+ /**
105
+ * Confirm newsletter subscription (via email link)
106
+ */
107
+ newsletterRoutes.get('/confirm', async (c) => {
108
+ const token = c.req.query('token');
109
+
110
+ if (!token) {
111
+ return c.json({ error: 'Missing confirmation token' }, 400);
112
+ }
113
+
114
+ try {
115
+ const { data, error } = await adminDb
116
+ .from('newsletter_subscribers')
117
+ .update({
118
+ status: 'active',
119
+ subscribed_at: new Date().toISOString(),
120
+ confirmation_token: null,
121
+ })
122
+ .eq('confirmation_token', token)
123
+ .eq('status', 'pending')
124
+ .select('email')
125
+ .maybeSingle();
126
+
127
+ if (error) {
128
+ return c.json({ error: sanitizeError(error, 'Failed to confirm') }, 500);
129
+ }
130
+
131
+ if (!data) {
132
+ return c.json({ error: 'Invalid or expired confirmation link' }, 400);
133
+ }
134
+
135
+ // Redirect to homepage with success
136
+ const baseUrl = env.SITE_URL || 'http://localhost:5173';
137
+ return c.redirect(`${baseUrl}/?newsletter=confirmed`);
138
+ } catch (error) {
139
+ return c.json({ error: sanitizeError(error, 'Failed to confirm') }, 500);
140
+ }
141
+ });
142
+
143
+ /**
144
+ * Unsubscribe from newsletter (via email link)
145
+ */
146
+ newsletterRoutes.get('/unsubscribe', async (c) => {
147
+ const email = c.req.query('email');
148
+ const token = c.req.query('token');
149
+
150
+ if (!email) {
151
+ return c.json({ error: 'Missing email' }, 400);
152
+ }
153
+
154
+ try {
155
+ const query = adminDb
156
+ .from('newsletter_subscribers')
157
+ .update({
158
+ status: 'unsubscribed',
159
+ unsubscribed_at: new Date().toISOString(),
160
+ })
161
+ .eq('email', email);
162
+
163
+ // If token provided, verify it matches (for one-click unsubscribe)
164
+ if (token) {
165
+ query.eq('id', token);
166
+ }
167
+
168
+ const { error } = await query;
169
+
170
+ if (error) {
171
+ return c.json({ error: sanitizeError(error, 'Failed to unsubscribe') }, 500);
172
+ }
173
+
174
+ const baseUrl = env.SITE_URL || 'http://localhost:5173';
175
+ return c.redirect(`${baseUrl}/?newsletter=unsubscribed`);
176
+ } catch (error) {
177
+ return c.json({ error: sanitizeError(error, 'Failed to unsubscribe') }, 500);
178
+ }
179
+ });
180
+
181
+ export { newsletterRoutes };
@@ -0,0 +1,157 @@
1
+ import { Hono } from 'hono';
2
+ import { isSuperAdmin } from '../../lib/auth';
3
+ import { logger } from '../../lib/logger';
4
+ import type { HonoVariables } from '../../types';
5
+
6
+ const performanceRoutes = new Hono<{ Variables: HonoVariables }>();
7
+
8
+ // Detect runtime environment (same logic as services-status.ts)
9
+ const isKubernetes = !!process.env.KUBERNETES_SERVICE_HOST;
10
+ const isDocker =
11
+ !isKubernetes && (process.env.NODE_ENV === 'production' || process.env.DOCKER === 'true');
12
+
13
+ interface ServiceHealthCheck {
14
+ name: string;
15
+ url: string;
16
+ acceptCodes?: number[];
17
+ }
18
+
19
+ // Core services to check latency against (subset of services-status.ts)
20
+ const HEALTH_CHECK_SERVICES: ServiceHealthCheck[] = [
21
+ {
22
+ name: 'API Gateway',
23
+ url: isKubernetes
24
+ ? 'http://kong:8000/'
25
+ : isDocker
26
+ ? 'http://kong:8000/'
27
+ : 'http://localhost:8000/',
28
+ acceptCodes: [404],
29
+ },
30
+ {
31
+ name: 'Authentication',
32
+ url: isKubernetes
33
+ ? 'http://kong:8000/auth/v1/health'
34
+ : isDocker
35
+ ? 'http://auth:9999/health'
36
+ : 'http://localhost:8000/auth/v1/health',
37
+ acceptCodes: isDocker ? undefined : [200, 401],
38
+ },
39
+ {
40
+ name: 'REST API',
41
+ url: isKubernetes
42
+ ? 'http://kong:8000/rest/v1/'
43
+ : isDocker
44
+ ? 'http://rest:3000/'
45
+ : 'http://localhost:8000/rest/v1/',
46
+ acceptCodes: [200, 401],
47
+ },
48
+ {
49
+ name: 'Realtime',
50
+ url: isKubernetes
51
+ ? 'http://kong:8000/realtime/v1/'
52
+ : isDocker
53
+ ? 'http://realtime:4000/api/tenants'
54
+ : 'http://localhost:8000/realtime/v1/',
55
+ acceptCodes: [200, 401, 403, 426],
56
+ },
57
+ {
58
+ name: 'File Storage',
59
+ url: isKubernetes
60
+ ? 'http://kong:8000/storage/v1/status'
61
+ : isDocker
62
+ ? 'http://storage:5000/status'
63
+ : 'http://localhost:8000/storage/v1/status',
64
+ acceptCodes: isDocker ? undefined : [200, 401],
65
+ },
66
+ {
67
+ name: 'DB Management',
68
+ url: isKubernetes
69
+ ? 'http://kong:8000/pg/'
70
+ : isDocker
71
+ ? 'http://meta:8080/health'
72
+ : 'http://localhost:8000/pg/',
73
+ acceptCodes: [200, 401],
74
+ },
75
+ ];
76
+
77
+ const SERVICE_TIMEOUT_MS = 2000;
78
+
79
+ async function checkServiceLatency(
80
+ service: ServiceHealthCheck
81
+ ): Promise<{ name: string; latencyMs: number; status: 'healthy' | 'unhealthy' | 'unknown' }> {
82
+ const start = performance.now();
83
+ try {
84
+ const controller = new AbortController();
85
+ const timeoutId = setTimeout(() => controller.abort(), SERVICE_TIMEOUT_MS);
86
+
87
+ const response = await fetch(service.url, {
88
+ method: 'GET',
89
+ signal: controller.signal,
90
+ });
91
+ clearTimeout(timeoutId);
92
+
93
+ const latencyMs = Math.round(performance.now() - start);
94
+
95
+ const isHealthy = service.acceptCodes
96
+ ? service.acceptCodes.includes(response.status)
97
+ : response.status >= 200 && response.status < 400;
98
+
99
+ return { name: service.name, latencyMs, status: isHealthy ? 'healthy' : 'unhealthy' };
100
+ } catch {
101
+ const latencyMs = Math.round(performance.now() - start);
102
+ return { name: service.name, latencyMs, status: 'unhealthy' };
103
+ }
104
+ }
105
+
106
+ // ============================================================================
107
+ // ADMIN PERFORMANCE ENDPOINT (Super Admin only)
108
+ // ============================================================================
109
+
110
+ performanceRoutes.get('/', async (c) => {
111
+ const user = c.get('user');
112
+
113
+ if (!user) {
114
+ return c.json({ error: 'Unauthorized' }, 401);
115
+ }
116
+
117
+ if (!isSuperAdmin(user)) {
118
+ return c.json({ error: 'Super admin access required' }, 403);
119
+ }
120
+
121
+ // Gather server metrics
122
+ const mem = process.memoryUsage();
123
+ const server = {
124
+ uptimeSeconds: Math.round(process.uptime()),
125
+ memoryMB: {
126
+ rss: Math.round(mem.rss / 1024 / 1024),
127
+ heapUsed: Math.round(mem.heapUsed / 1024 / 1024),
128
+ heapTotal: Math.round(mem.heapTotal / 1024 / 1024),
129
+ },
130
+ nodeVersion: process.version,
131
+ platform: `${process.platform} ${process.arch}`,
132
+ };
133
+
134
+ // Check all service latencies in parallel
135
+ const items = await Promise.all(HEALTH_CHECK_SERVICES.map(checkServiceLatency));
136
+
137
+ const healthy = items.filter((s) => s.status === 'healthy').length;
138
+ const healthyItems = items.filter((s) => s.status === 'healthy');
139
+ const avgLatencyMs =
140
+ healthyItems.length > 0
141
+ ? Math.round(healthyItems.reduce((sum, s) => sum + s.latencyMs, 0) / healthyItems.length)
142
+ : 0;
143
+
144
+ logger.debug({ userId: user.id }, 'Admin performance metrics fetched');
145
+
146
+ return c.json({
147
+ server,
148
+ services: {
149
+ healthy,
150
+ total: items.length,
151
+ avgLatencyMs,
152
+ items,
153
+ },
154
+ });
155
+ });
156
+
157
+ export { performanceRoutes };
@@ -0,0 +1,170 @@
1
+ import { Hono } from 'hono';
2
+ import { isSuperAdmin } from '../../lib/auth';
3
+ import { logger } from '../../lib/logger';
4
+ import { supabaseAdmin } from '../../lib/supabase';
5
+ import type { HonoVariables } from '../../types';
6
+
7
+ const statsRoutes = new Hono<{ Variables: HonoVariables }>();
8
+
9
+ /**
10
+ * Count organizations with an optional filter applied to the query builder.
11
+ * Returns 0 on any error for resilience.
12
+ */
13
+ async function countOrgs(
14
+ // biome-ignore lint/suspicious/noExplicitAny: Supabase query builder generics are too complex to type the filter callback
15
+ filter?: (q: any) => PromiseLike<{ count: number | null; error: unknown }>
16
+ ): Promise<number> {
17
+ try {
18
+ const base = supabaseAdmin.from('organizations').select('*', { count: 'exact', head: true });
19
+ const result = filter ? await filter(base) : await base;
20
+ return result.error ? 0 : (result.count ?? 0);
21
+ } catch {
22
+ return 0;
23
+ }
24
+ }
25
+
26
+ // ============================================================================
27
+ // ADMIN STATS ENDPOINT (Super Admin only)
28
+ // ============================================================================
29
+
30
+ statsRoutes.get('/', async (c) => {
31
+ const user = c.get('user');
32
+
33
+ if (!user) {
34
+ return c.json({ error: 'Unauthorized' }, 401);
35
+ }
36
+
37
+ if (!isSuperAdmin(user)) {
38
+ return c.json({ error: 'Super admin access required' }, 403);
39
+ }
40
+
41
+ const now = new Date();
42
+
43
+ // Start of current month (UTC)
44
+ const startOfThisMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
45
+ // Start of previous month (UTC)
46
+ const startOfLastMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1));
47
+
48
+ // 30 days ago and 60 days ago for active user calculations
49
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
50
+ const sixtyDaysAgo = new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000);
51
+
52
+ // 42 days ago for weekly signups (6 weeks)
53
+ const fortyTwoDaysAgo = new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000);
54
+
55
+ // Fetch all user data with a single listUsers call, then derive all counts in-memory.
56
+ // Previously this made 5 separate listUsers calls with perPage: 1000 each.
57
+ // Now we make one call to get the total count (perPage: 1) and one call to fetch
58
+ // all users for segmentation. For apps with many users, pagination fetches all pages.
59
+ const [usersResult, totalOrgsResult, orgsNewThisMonthResult, orgsNewLastMonthResult] =
60
+ await Promise.all([
61
+ // Single listUsers call — fetch all users for date-based segmentation
62
+ (async () => {
63
+ try {
64
+ // First get the total count with a lightweight call
65
+ const countResult = await supabaseAdmin.auth.admin.listUsers({ page: 1, perPage: 1 });
66
+ if (countResult.error) return { total: 0, users: [] };
67
+ const total = countResult.data.total;
68
+
69
+ // Fetch all users (paginated if needed) for segmentation
70
+ const allUsers: typeof countResult.data.users = [];
71
+ const pageSize = 1000;
72
+ const totalPages = Math.ceil(total / pageSize);
73
+ for (let page = 1; page <= totalPages; page++) {
74
+ const pageResult = await supabaseAdmin.auth.admin.listUsers({
75
+ page,
76
+ perPage: pageSize,
77
+ });
78
+ if (pageResult.error) break;
79
+ allUsers.push(...pageResult.data.users);
80
+ }
81
+ return { total, users: allUsers };
82
+ } catch {
83
+ return { total: 0, users: [] };
84
+ }
85
+ })(),
86
+
87
+ // Total organizations
88
+ countOrgs(),
89
+
90
+ // Organizations created this month
91
+ countOrgs((q) => q.gte('created_at', startOfThisMonth.toISOString())),
92
+
93
+ // Organizations created last month
94
+ countOrgs((q) =>
95
+ q
96
+ .gte('created_at', startOfLastMonth.toISOString())
97
+ .lt('created_at', startOfThisMonth.toISOString())
98
+ ),
99
+ ]);
100
+
101
+ // Derive all user stats from the single fetched dataset
102
+ const { total: totalUsersResult, users: allUsers } = usersResult;
103
+
104
+ const newThisMonthResult = allUsers.filter(
105
+ (u) => new Date(u.created_at) >= startOfThisMonth
106
+ ).length;
107
+
108
+ const newLastMonthResult = allUsers.filter(
109
+ (u) => new Date(u.created_at) >= startOfLastMonth && new Date(u.created_at) < startOfThisMonth
110
+ ).length;
111
+
112
+ const activeThisMonthResult = allUsers.filter(
113
+ (u) => u.last_sign_in_at && new Date(u.last_sign_in_at) >= thirtyDaysAgo
114
+ ).length;
115
+
116
+ const activeLastMonthResult = allUsers.filter(
117
+ (u) =>
118
+ u.last_sign_in_at &&
119
+ new Date(u.last_sign_in_at) >= sixtyDaysAgo &&
120
+ new Date(u.last_sign_in_at) < thirtyDaysAgo
121
+ ).length;
122
+
123
+ const weeklySignupsResult = bucketIntoWeeks(
124
+ allUsers
125
+ .filter((u) => new Date(u.created_at) >= fortyTwoDaysAgo)
126
+ .map((u) => new Date(u.created_at)),
127
+ fortyTwoDaysAgo
128
+ );
129
+
130
+ logger.debug({ userId: user.id }, 'Admin stats fetched');
131
+
132
+ return c.json({
133
+ users: {
134
+ total: totalUsersResult,
135
+ newThisMonth: newThisMonthResult,
136
+ newLastMonth: newLastMonthResult,
137
+ activeThisMonth: activeThisMonthResult,
138
+ activeLastMonth: activeLastMonthResult,
139
+ weeklySignups: weeklySignupsResult,
140
+ },
141
+ orgs: {
142
+ total: totalOrgsResult,
143
+ newThisMonth: orgsNewThisMonthResult,
144
+ newLastMonth: orgsNewLastMonthResult,
145
+ },
146
+ });
147
+ });
148
+
149
+ /**
150
+ * Bucket an array of dates into 6 weekly buckets.
151
+ * Week 1 = oldest (starts at `from`), Week 6 = most recent.
152
+ */
153
+ function bucketIntoWeeks(dates: Date[], from: Date): Array<{ week: string; count: number }> {
154
+ const msPerWeek = 7 * 24 * 60 * 60 * 1000;
155
+ const buckets = [0, 0, 0, 0, 0, 0];
156
+
157
+ for (const date of dates) {
158
+ const weekIndex = Math.min(Math.floor((date.getTime() - from.getTime()) / msPerWeek), 5);
159
+ if (weekIndex >= 0 && weekIndex <= 5) {
160
+ buckets[weekIndex]++;
161
+ }
162
+ }
163
+
164
+ return buckets.map((count, i) => ({
165
+ week: `W${i + 1}`,
166
+ count,
167
+ }));
168
+ }
169
+
170
+ export { statsRoutes };
@@ -0,0 +1,106 @@
1
+ import { Hono } from 'hono';
2
+ import { z } from 'zod';
3
+ import { isSuperAdmin } from '../../lib/auth';
4
+ import { logger } from '../../lib/logger';
5
+ import { supabaseAdmin } from '../../lib/supabase';
6
+ import type { HonoVariables } from '../../types';
7
+
8
+ const themeRoutes = new Hono<{ Variables: HonoVariables }>();
9
+
10
+ // Zod schema for theme validation — all fields optional
11
+ const colorSchemaFields = {
12
+ primary: z.string().optional(),
13
+ primaryDim: z.string().optional(),
14
+ primaryForeground: z.string().optional(),
15
+ secondaryAccent: z.string().optional(),
16
+ secondaryAccentForeground: z.string().optional(),
17
+ destructive: z.string().optional(),
18
+ warning: z.string().optional(),
19
+ success: z.string().optional(),
20
+ };
21
+
22
+ const themeSchema = z.object({
23
+ light: z.object(colorSchemaFields).optional(),
24
+ dark: z
25
+ .object({
26
+ primary: z.string().optional(),
27
+ primaryDim: z.string().optional(),
28
+ primaryForeground: z.string().optional(),
29
+ secondaryAccent: z.string().optional(),
30
+ secondaryAccentForeground: z.string().optional(),
31
+ })
32
+ .optional(),
33
+ radius: z.string().optional(),
34
+ smoothScrollEnabled: z.boolean().optional(),
35
+ smoothScrollIntensity: z.number().min(0).max(100).optional(),
36
+ });
37
+
38
+ // ============================================================================
39
+ // GET / — Public. Returns the current app theme.
40
+ // ============================================================================
41
+
42
+ themeRoutes.get('/', async (c): Promise<Response> => {
43
+ try {
44
+ const { data, error } = await supabaseAdmin
45
+ .from('app_settings')
46
+ .select('value')
47
+ .eq('key', 'app_theme')
48
+ .single();
49
+
50
+ if (error || !data) {
51
+ logger.debug({ error }, 'No app_theme found, returning default');
52
+ return c.json({ theme: {} });
53
+ }
54
+
55
+ return c.json({ theme: data.value });
56
+ } catch (err) {
57
+ logger.error({ error: err }, 'Failed to fetch app theme');
58
+ return c.json({ theme: {} });
59
+ }
60
+ });
61
+
62
+ // ============================================================================
63
+ // PATCH / — Super admin only. Upserts the app theme.
64
+ // ============================================================================
65
+
66
+ themeRoutes.patch('/', async (c) => {
67
+ const user = c.get('user');
68
+
69
+ if (!user) {
70
+ return c.json({ error: 'Unauthorized' }, 401);
71
+ }
72
+
73
+ if (!isSuperAdmin(user)) {
74
+ return c.json({ error: 'Super admin access required' }, 403);
75
+ }
76
+
77
+ // Validate request body
78
+ const body = await c.req.json();
79
+ const parsed = themeSchema.safeParse(body);
80
+
81
+ if (!parsed.success) {
82
+ return c.json({ error: 'Invalid theme data', details: parsed.error.flatten() }, 400);
83
+ }
84
+
85
+ const theme = parsed.data;
86
+
87
+ try {
88
+ const { error } = await supabaseAdmin
89
+ .from('app_settings')
90
+ .upsert({ key: 'app_theme', value: theme, updated_by: user.id }, { onConflict: 'key' });
91
+
92
+ if (error) {
93
+ logger.error({ error, userId: user.id }, 'Failed to save app theme');
94
+ return c.json({ error: 'Failed to save theme' }, 500);
95
+ }
96
+
97
+ logger.info({ userId: user.id }, 'App theme updated');
98
+
99
+ return c.json({ success: true, theme });
100
+ } catch (err) {
101
+ logger.error({ error: err, userId: user.id }, 'Failed to save app theme');
102
+ return c.json({ error: 'Failed to save theme' }, 500);
103
+ }
104
+ });
105
+
106
+ export { themeRoutes };