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,958 @@
1
+ import { exec } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import https from 'node:https';
4
+ import net from 'node:net';
5
+ import { promisify } from 'node:util';
6
+ import { Hono } from 'hono';
7
+ import { logger } from '../../lib/logger';
8
+
9
+ const execAsync = promisify(exec);
10
+
11
+ /**
12
+ * Internal endpoint to get status of infrastructure services.
13
+ * Checks health endpoints of known services running in Docker.
14
+ *
15
+ * Usage:
16
+ * GET /api/_internal/services/status - Get all service statuses
17
+ * POST /api/_internal/services/restart - Restart all services
18
+ * POST /api/_internal/services/restart/:container - Restart specific service
19
+ *
20
+ * Returns array of service statuses with health information.
21
+ */
22
+
23
+ interface ServiceStatus {
24
+ name: string;
25
+ container: string;
26
+ status: 'healthy' | 'unhealthy' | 'unknown';
27
+ latencyMs?: number;
28
+ error?: string;
29
+ }
30
+
31
+ interface ServiceConfig {
32
+ name: string;
33
+ container: string;
34
+ healthUrl: string;
35
+ timeout: number;
36
+ acceptCodes?: number[]; // HTTP status codes considered healthy (default: 200-399)
37
+ k8sUnavailable?: boolean; // Service cannot be checked in Kubernetes
38
+ }
39
+
40
+ // Detect runtime environment
41
+ const isKubernetes = !!process.env.KUBERNETES_SERVICE_HOST;
42
+ const isDocker =
43
+ !isKubernetes && (process.env.NODE_ENV === 'production' || process.env.DOCKER === 'true');
44
+
45
+ // Local dev port resolution: respects DEV_PORT_OFFSET and explicit per-service overrides
46
+ // (mirrors the logic in scripts/docker-up.js)
47
+ function localPort(envKey: string, defaultPort: number): number {
48
+ const explicit = process.env[envKey];
49
+ if (explicit) return Number.parseInt(explicit, 10);
50
+ const offset = Number.parseInt(process.env.DEV_PORT_OFFSET || '0', 10);
51
+ return defaultPort + offset;
52
+ }
53
+
54
+ const kongPort = localPort('DEV_KONG_PORT', 8000);
55
+ const traefikPort = localPort('DEV_TRAEFIK_PORT', 80);
56
+
57
+ // Core Supabase services - always present
58
+ const CORE_SERVICES: ServiceConfig[] = [
59
+ {
60
+ name: 'PostgreSQL',
61
+ container: 'db',
62
+ // In k8s: checked indirectly via PostgREST through Kong (handled specially in checkServiceHealth)
63
+ healthUrl: isDocker ? 'http://db:5432' : `http://localhost:${localPort('DEV_DB_PORT', 5432)}`,
64
+ timeout: 2000,
65
+ },
66
+ {
67
+ name: 'API Gateway',
68
+ container: 'kong',
69
+ healthUrl: isKubernetes
70
+ ? 'http://kong:8000/'
71
+ : isDocker
72
+ ? 'http://kong:8000/'
73
+ : `http://localhost:${kongPort}/`,
74
+ timeout: 2000,
75
+ acceptCodes: [404], // 404 = running but no default route
76
+ },
77
+ {
78
+ name: 'Authentication',
79
+ container: 'auth',
80
+ healthUrl: isKubernetes
81
+ ? 'http://kong:8000/auth/v1/health'
82
+ : isDocker
83
+ ? 'http://auth:9999/health'
84
+ : `http://localhost:${kongPort}/auth/v1/health`,
85
+ timeout: 2000,
86
+ acceptCodes: isDocker ? undefined : [200, 401], // Via Kong requires API key
87
+ },
88
+ {
89
+ name: 'REST API',
90
+ container: 'rest',
91
+ healthUrl: isKubernetes
92
+ ? 'http://kong:8000/rest/v1/'
93
+ : isDocker
94
+ ? 'http://rest:3000/'
95
+ : `http://localhost:${kongPort}/rest/v1/`,
96
+ timeout: 2000,
97
+ acceptCodes: [200, 401], // 401 = running but requires auth
98
+ },
99
+ {
100
+ name: 'Realtime',
101
+ container: 'realtime',
102
+ healthUrl: isKubernetes
103
+ ? 'http://kong:8000/realtime/v1/'
104
+ : isDocker
105
+ ? 'http://realtime:4000/api/tenants'
106
+ : `http://localhost:${kongPort}/realtime/v1/`,
107
+ timeout: 2000,
108
+ acceptCodes: [200, 401, 403, 426], // May require auth or websocket upgrade
109
+ },
110
+ {
111
+ name: 'File Storage',
112
+ container: 'storage',
113
+ healthUrl: isKubernetes
114
+ ? 'http://kong:8000/storage/v1/status'
115
+ : isDocker
116
+ ? 'http://storage:5000/status'
117
+ : `http://localhost:${kongPort}/storage/v1/status`,
118
+ timeout: 2000,
119
+ acceptCodes: isDocker ? undefined : [200, 401], // Via Kong requires API key
120
+ },
121
+ {
122
+ name: 'Studio',
123
+ container: 'studio',
124
+ healthUrl: isKubernetes
125
+ ? 'http://studio:3000/'
126
+ : isDocker
127
+ ? 'http://studio:3000/'
128
+ : `http://studio.localhost:${traefikPort}/`,
129
+ timeout: 3000,
130
+ acceptCodes: [200, 307], // 307 redirect is fine
131
+ },
132
+ {
133
+ name: 'DB Management',
134
+ container: 'meta',
135
+ healthUrl: isKubernetes
136
+ ? 'http://kong:8000/pg/'
137
+ : isDocker
138
+ ? 'http://meta:8080/health'
139
+ : `http://localhost:${kongPort}/pg/`,
140
+ timeout: 2000,
141
+ acceptCodes: [200, 401], // May require auth via Kong
142
+ },
143
+ ];
144
+
145
+ // Optional services - added based on environment configuration
146
+ const OPTIONAL_SERVICES: Record<string, ServiceConfig[]> = {
147
+ redis: [
148
+ {
149
+ name: 'Redis',
150
+ container: 'redis',
151
+ healthUrl: 'tcp://redis:6379', // Special marker for TCP check
152
+ timeout: 2000,
153
+ },
154
+ ],
155
+ n8n: [
156
+ {
157
+ name: 'n8n',
158
+ container: 'n8n',
159
+ // In k8s, use the k8s Service DNS name (same as Docker network name)
160
+ healthUrl:
161
+ isKubernetes || isDocker ? 'http://n8n:5678/healthz' : 'http://localhost:5678/healthz',
162
+ timeout: 3000,
163
+ },
164
+ ],
165
+ metabase: [
166
+ {
167
+ name: 'Metabase',
168
+ container: 'metabase',
169
+ healthUrl:
170
+ isKubernetes || isDocker
171
+ ? 'http://metabase:3000/api/health'
172
+ : 'http://localhost:3001/api/health',
173
+ timeout: 5000,
174
+ },
175
+ ],
176
+ observability: [
177
+ {
178
+ name: 'Prometheus',
179
+ container: 'prometheus',
180
+ healthUrl:
181
+ isKubernetes || isDocker
182
+ ? 'http://prometheus:9090/-/healthy'
183
+ : `http://localhost:${localPort('DEV_PROMETHEUS_PORT', 9190)}/-/healthy`,
184
+ timeout: 2000,
185
+ },
186
+ {
187
+ name: 'Grafana',
188
+ container: 'grafana',
189
+ healthUrl:
190
+ isKubernetes || isDocker
191
+ ? 'http://grafana:3000/api/health'
192
+ : `http://localhost:${localPort('DEV_GRAFANA_PORT', 3002)}/api/health`,
193
+ timeout: 3000,
194
+ },
195
+ {
196
+ name: 'Loki',
197
+ container: 'loki',
198
+ healthUrl:
199
+ isKubernetes || isDocker
200
+ ? 'http://loki:3100/ready'
201
+ : `http://localhost:${localPort('DEV_LOKI_PORT', 3100)}/ready`,
202
+ timeout: 2000,
203
+ },
204
+ ],
205
+ };
206
+
207
+ // Cache for running containers (refreshed on each status check)
208
+ let runningContainersCache: Set<string> = new Set();
209
+ let containersCacheTime = 0;
210
+ const CACHE_TTL_MS = 5000; // 5 seconds
211
+
212
+ /**
213
+ * Detect which containers are currently running via Docker
214
+ */
215
+ async function getRunningContainers(): Promise<Set<string>> {
216
+ // Docker is not available in Kubernetes
217
+ if (isKubernetes) return new Set();
218
+
219
+ const now = Date.now();
220
+ if (now - containersCacheTime < CACHE_TTL_MS && runningContainersCache.size > 0) {
221
+ return runningContainersCache;
222
+ }
223
+
224
+ try {
225
+ const { stdout } = await execAsync('docker ps --format "{{.Names}}" 2>/dev/null', {
226
+ timeout: 5000,
227
+ });
228
+ const containers = new Set(
229
+ stdout
230
+ .split('\n')
231
+ .map((name) => name.trim())
232
+ .filter(Boolean)
233
+ // Extract service name from container name (e.g., "vibecarbon-n8n" -> "n8n")
234
+ .map((name) => name.replace(/^[^-]+-/, ''))
235
+ );
236
+ runningContainersCache = containers;
237
+ containersCacheTime = now;
238
+ return containers;
239
+ } catch {
240
+ // If Docker check fails, return empty set (will only show core services)
241
+ return new Set();
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Get all services to check - core + any running optional services
247
+ */
248
+ async function getEnabledServices(): Promise<ServiceConfig[]> {
249
+ const services = [...CORE_SERVICES];
250
+ const runningContainers = await getRunningContainers();
251
+
252
+ // Add optional services if their containers are running
253
+ for (const [, configs] of Object.entries(OPTIONAL_SERVICES)) {
254
+ const isRunning = configs.some((config) => runningContainers.has(config.container));
255
+ if (isRunning) {
256
+ services.push(...configs);
257
+ }
258
+ }
259
+
260
+ // Also check env vars as fallback (for when Docker detection fails)
261
+ if (process.env.REDIS_ENABLED === 'true' && !services.some((s) => s.container === 'redis')) {
262
+ services.push(...OPTIONAL_SERVICES.redis);
263
+ }
264
+ if (process.env.N8N_ENABLED === 'true' && !services.some((s) => s.container === 'n8n')) {
265
+ services.push(...OPTIONAL_SERVICES.n8n);
266
+ }
267
+ if (
268
+ process.env.METABASE_ENABLED === 'true' &&
269
+ !services.some((s) => s.container === 'metabase')
270
+ ) {
271
+ services.push(...OPTIONAL_SERVICES.metabase);
272
+ }
273
+ if (
274
+ process.env.OBSERVABILITY_ENABLED === 'true' &&
275
+ !services.some((s) => s.container === 'prometheus')
276
+ ) {
277
+ services.push(...OPTIONAL_SERVICES.observability);
278
+ }
279
+
280
+ return services;
281
+ }
282
+
283
+ // List of optional service containers that should use Docker health check when not in Docker
284
+ const DOCKER_HEALTH_CHECK_CONTAINERS = [
285
+ 'redis',
286
+ 'n8n',
287
+ 'metabase',
288
+ 'prometheus',
289
+ 'grafana',
290
+ 'loki',
291
+ ];
292
+
293
+ async function checkServiceHealth(service: ServiceConfig): Promise<ServiceStatus> {
294
+ const start = Date.now();
295
+
296
+ // Services that cannot be reached from the app pod in Kubernetes
297
+ if (isKubernetes && service.k8sUnavailable) {
298
+ return {
299
+ name: service.name,
300
+ container: service.container,
301
+ status: 'unknown',
302
+ error: 'Not accessible in Kubernetes (exposed via Traefik only)',
303
+ };
304
+ }
305
+
306
+ // For optional services when running on host (not in Docker or Kubernetes), check via Docker container health status
307
+ // This is needed because the API server can't reach Docker internal network hostnames from the host
308
+ if (!isDocker && !isKubernetes && DOCKER_HEALTH_CHECK_CONTAINERS.includes(service.container)) {
309
+ try {
310
+ const { stdout } = await execAsync(
311
+ `docker inspect --format="{{.State.Health.Status}}" $(docker ps -q -f name=-${service.container}) 2>/dev/null || echo "not_found"`,
312
+ { timeout: service.timeout }
313
+ );
314
+ const status = stdout.trim();
315
+ const isHealthy = status === 'healthy';
316
+ const isUnhealthy = status === 'unhealthy' || status === 'not_found' || status === '';
317
+ const isStarting = status === 'starting';
318
+
319
+ return {
320
+ name: service.name,
321
+ container: service.container,
322
+ status: isHealthy ? 'healthy' : isStarting ? 'unknown' : 'unhealthy',
323
+ latencyMs: Date.now() - start,
324
+ error: isUnhealthy ? 'Container not healthy' : isStarting ? 'Starting up' : undefined,
325
+ };
326
+ } catch {
327
+ return {
328
+ name: service.name,
329
+ container: service.container,
330
+ status: 'unknown',
331
+ error: 'Cannot check container status',
332
+ };
333
+ }
334
+ }
335
+
336
+ // Special case for PostgreSQL - we can't HTTP check it, rely on other services
337
+ if (service.container === 'db') {
338
+ // Check if PostgREST can connect (implies DB is healthy)
339
+ const postgrestUrl = isKubernetes
340
+ ? 'http://kong:8000/rest/v1/'
341
+ : isDocker
342
+ ? 'http://rest:3000/'
343
+ : `http://localhost:${kongPort}/rest/v1/`;
344
+ try {
345
+ const controller = new AbortController();
346
+ const timeoutId = setTimeout(() => controller.abort(), service.timeout);
347
+
348
+ const response = await fetch(postgrestUrl, {
349
+ method: 'GET',
350
+ signal: controller.signal,
351
+ });
352
+ clearTimeout(timeoutId);
353
+
354
+ // 401 via Kong is fine - means PostgREST (and thus DB) is running
355
+ const isHealthy = response.ok || response.status === 401;
356
+
357
+ return {
358
+ name: service.name,
359
+ container: service.container,
360
+ status: isHealthy ? 'healthy' : 'unhealthy',
361
+ latencyMs: Date.now() - start,
362
+ };
363
+ } catch {
364
+ return {
365
+ name: service.name,
366
+ container: service.container,
367
+ status: 'unknown',
368
+ error: 'Cannot verify (checked via PostgREST)',
369
+ };
370
+ }
371
+ }
372
+
373
+ // Special case: TCP check for services without an HTTP health endpoint (e.g. Redis)
374
+ if (service.healthUrl.startsWith('tcp://')) {
375
+ const url = new URL(service.healthUrl);
376
+ const host = url.hostname;
377
+ const port = Number.parseInt(url.port, 10);
378
+ return new Promise((resolve) => {
379
+ const socket = new net.Socket();
380
+ const latencyStart = Date.now();
381
+ const done = (ok: boolean, err?: string) => {
382
+ socket.destroy();
383
+ resolve({
384
+ name: service.name,
385
+ container: service.container,
386
+ status: ok ? 'healthy' : 'unhealthy',
387
+ latencyMs: Date.now() - latencyStart,
388
+ error: err,
389
+ });
390
+ };
391
+ socket.setTimeout(service.timeout);
392
+ socket.once('connect', () => done(true));
393
+ socket.once('timeout', () => done(false, 'Timeout'));
394
+ socket.once('error', (e) =>
395
+ done(false, e.message.includes('ECONNREFUSED') ? 'Not running' : e.message)
396
+ );
397
+ socket.connect(port, host);
398
+ });
399
+ }
400
+
401
+ try {
402
+ const controller = new AbortController();
403
+ const timeoutId = setTimeout(() => controller.abort(), service.timeout);
404
+
405
+ const response = await fetch(service.healthUrl, {
406
+ method: 'GET',
407
+ signal: controller.signal,
408
+ });
409
+ clearTimeout(timeoutId);
410
+
411
+ const latencyMs = Date.now() - start;
412
+
413
+ // Check if status code is acceptable
414
+ const isHealthy = service.acceptCodes
415
+ ? service.acceptCodes.includes(response.status)
416
+ : response.status >= 200 && response.status < 400;
417
+
418
+ return {
419
+ name: service.name,
420
+ container: service.container,
421
+ status: isHealthy ? 'healthy' : 'unhealthy',
422
+ latencyMs,
423
+ };
424
+ } catch (error) {
425
+ const latencyMs = Date.now() - start;
426
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
427
+
428
+ // Check if it's a timeout or connection refused
429
+ const isTimeout = errorMessage.includes('abort');
430
+ const isConnectionRefused =
431
+ errorMessage.includes('ECONNREFUSED') || errorMessage.includes('fetch failed');
432
+
433
+ return {
434
+ name: service.name,
435
+ container: service.container,
436
+ status: 'unhealthy',
437
+ latencyMs,
438
+ error: isTimeout ? 'Timeout' : isConnectionRefused ? 'Not running' : errorMessage,
439
+ };
440
+ }
441
+ }
442
+
443
+ import type { HonoVariables } from '../../types';
444
+
445
+ const servicesStatusRoutes = new Hono<{ Variables: HonoVariables }>();
446
+
447
+ // Helper to check if user is a super admin
448
+ function requireSuperAdmin(c: {
449
+ get: (key: string) => unknown;
450
+ }): { error: string; status: 401 | 403 } | null {
451
+ const user = c.get('user') as { app_metadata?: { role?: string } } | null;
452
+ if (!user) return { error: 'Unauthorized', status: 401 };
453
+ if (user.app_metadata?.role !== 'super_admin') return { error: 'Forbidden', status: 403 };
454
+ return null;
455
+ }
456
+
457
+ servicesStatusRoutes.get('/', async (c) => {
458
+ const authError = requireSuperAdmin(c);
459
+ if (authError) return c.json({ error: authError.error }, authError.status);
460
+
461
+ logger.debug('Checking services status');
462
+
463
+ // Get enabled services (auto-detects running containers)
464
+ const services = await getEnabledServices();
465
+
466
+ // Check all services in parallel
467
+ const results = await Promise.all(services.map(checkServiceHealth));
468
+
469
+ // Sort by name for consistent ordering
470
+ results.sort((a, b) => a.name.localeCompare(b.name));
471
+
472
+ const healthy = results.filter((r) => r.status === 'healthy').length;
473
+ const total = results.length;
474
+
475
+ logger.debug({ healthy, total }, 'Services status check complete');
476
+
477
+ return c.json({
478
+ summary: {
479
+ healthy,
480
+ unhealthy: total - healthy,
481
+ total,
482
+ },
483
+ services: results,
484
+ timestamp: new Date().toISOString(),
485
+ });
486
+ });
487
+
488
+ // All known container names (core + all optional)
489
+ const ALL_KNOWN_CONTAINERS = [
490
+ ...CORE_SERVICES.map((s) => s.container),
491
+ ...Object.values(OPTIONAL_SERVICES).flatMap((configs) => configs.map((c) => c.container)),
492
+ ];
493
+
494
+ /**
495
+ * Restart a Docker container using docker compose
496
+ */
497
+ async function restartContainer(container: string): Promise<{ success: boolean; error?: string }> {
498
+ // Only allow restarting known containers
499
+ if (!ALL_KNOWN_CONTAINERS.includes(container)) {
500
+ return { success: false, error: `Invalid container: ${container}` };
501
+ }
502
+
503
+ try {
504
+ logger.info({ container }, 'Restarting container');
505
+ await execAsync(`docker compose restart ${container}`, {
506
+ timeout: 60000, // 60 second timeout
507
+ });
508
+ logger.info({ container }, 'Container restarted successfully');
509
+ return { success: true };
510
+ } catch (error) {
511
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
512
+ logger.error({ container, error: errorMessage }, 'Failed to restart container');
513
+ return { success: false, error: errorMessage };
514
+ }
515
+ }
516
+
517
+ // POST /restart - Restart all running services
518
+ servicesStatusRoutes.post('/restart', async (c) => {
519
+ const authError = requireSuperAdmin(c);
520
+ if (authError) return c.json({ error: authError.error }, authError.status);
521
+
522
+ if (isKubernetes) {
523
+ return c.json({ success: false, error: 'Not supported in Kubernetes' }, 501);
524
+ }
525
+
526
+ logger.info('Restarting all services');
527
+
528
+ const services = await getEnabledServices();
529
+ const containers = services.map((s) => s.container);
530
+
531
+ const results = await Promise.all(
532
+ containers.map(async (container) => {
533
+ const result = await restartContainer(container);
534
+ return { container, ...result };
535
+ })
536
+ );
537
+
538
+ const successful = results.filter((r) => r.success).length;
539
+ const failed = results.filter((r) => !r.success);
540
+
541
+ logger.info({ successful, failed: failed.length }, 'Restart all complete');
542
+
543
+ return c.json({
544
+ success: failed.length === 0,
545
+ summary: {
546
+ successful,
547
+ failed: failed.length,
548
+ total: containers.length,
549
+ },
550
+ results,
551
+ });
552
+ });
553
+
554
+ // POST /restart/:container - Restart a specific service
555
+ servicesStatusRoutes.post('/restart/:container', async (c) => {
556
+ const authError = requireSuperAdmin(c);
557
+ if (authError) return c.json({ error: authError.error }, authError.status);
558
+
559
+ if (isKubernetes) {
560
+ return c.json({ success: false, error: 'Not supported in Kubernetes' }, 501);
561
+ }
562
+
563
+ const container = c.req.param('container');
564
+
565
+ if (!ALL_KNOWN_CONTAINERS.includes(container)) {
566
+ return c.json({ success: false, error: `Invalid container: ${container}` }, 400);
567
+ }
568
+
569
+ const result = await restartContainer(container);
570
+
571
+ if (!result.success) {
572
+ return c.json(result, 500);
573
+ }
574
+
575
+ return c.json(result);
576
+ });
577
+
578
+ // ============================================================================
579
+ // KUBERNETES LOG HELPERS
580
+ // ============================================================================
581
+
582
+ // Load k8s service account credentials once at startup
583
+ const k8sCredentials = (() => {
584
+ if (!isKubernetes) return null;
585
+ try {
586
+ return {
587
+ token: fs.readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/token', 'utf8').trim(),
588
+ ca: fs.readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'),
589
+ namespace: fs
590
+ .readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/namespace', 'utf8')
591
+ .trim(),
592
+ };
593
+ } catch {
594
+ return null;
595
+ }
596
+ })();
597
+
598
+ /** Read the pod's default gateway from /proc/net/route (little-endian hex) */
599
+ function getPodDefaultGateway(): string | null {
600
+ try {
601
+ const routes = fs.readFileSync('/proc/net/route', 'utf8');
602
+ for (const line of routes.split('\n').slice(1)) {
603
+ const parts = line.split('\t');
604
+ if (parts[1] === '00000000' && parts[2] && parts[2] !== '00000000') {
605
+ const h = parts[2];
606
+ return [
607
+ parseInt(h.slice(6, 8), 16),
608
+ parseInt(h.slice(4, 6), 16),
609
+ parseInt(h.slice(2, 4), 16),
610
+ parseInt(h.slice(0, 2), 16),
611
+ ].join('.');
612
+ }
613
+ }
614
+ } catch {
615
+ /* ignore */
616
+ }
617
+ return null;
618
+ }
619
+
620
+ function testTcpConnection(host: string, port: number): Promise<boolean> {
621
+ return new Promise((resolve) => {
622
+ const sock = new net.Socket();
623
+ sock.setTimeout(2000);
624
+ sock.connect(port, host, () => {
625
+ sock.destroy();
626
+ resolve(true);
627
+ });
628
+ sock.on('error', () => {
629
+ sock.destroy();
630
+ resolve(false);
631
+ });
632
+ sock.on('timeout', () => {
633
+ sock.destroy();
634
+ resolve(false);
635
+ });
636
+ });
637
+ }
638
+
639
+ // Lazily detected API server endpoint (ClusterIP or pod gateway fallback)
640
+ let k8sApiEndpoint: { host: string; port: number } | null | undefined;
641
+
642
+ async function getK8sApiEndpoint(): Promise<{ host: string; port: number } | null> {
643
+ if (k8sApiEndpoint !== undefined) return k8sApiEndpoint;
644
+
645
+ const clusterIpHost = process.env.KUBERNETES_SERVICE_HOST ?? '';
646
+ const clusterIpPort =
647
+ Number(process.env.KUBERNETES_SERVICE_PORT_HTTPS || process.env.KUBERNETES_SERVICE_PORT) || 443;
648
+
649
+ // Try ClusterIP first (works on worker nodes via kube-proxy DNAT)
650
+ if (clusterIpHost && (await testTcpConnection(clusterIpHost, clusterIpPort))) {
651
+ k8sApiEndpoint = { host: clusterIpHost, port: clusterIpPort };
652
+ return k8sApiEndpoint;
653
+ }
654
+
655
+ // Fall back to pod gateway on port 6443 (k3s master: kube-proxy doesn't install
656
+ // KUBE-SERVICES rules for the kubernetes ClusterIP on the master node itself)
657
+ const gateway = getPodDefaultGateway();
658
+ if (gateway && (await testTcpConnection(gateway, 6443))) {
659
+ logger.info({ gateway }, 'K8s API: using pod gateway fallback (k3s master node)');
660
+ k8sApiEndpoint = { host: gateway, port: 6443 };
661
+ return k8sApiEndpoint;
662
+ }
663
+
664
+ k8sApiEndpoint = null;
665
+ return null;
666
+ }
667
+
668
+ /** Human-readable names for k8s app labels */
669
+ const K8S_LABEL_NAMES: Record<string, string> = {
670
+ 'vibecarbon-app': 'App',
671
+ 'vibecarbon-kong': 'API Gateway',
672
+ 'vibecarbon-auth': 'Authentication',
673
+ 'vibecarbon-rest': 'REST API',
674
+ 'vibecarbon-realtime': 'Realtime',
675
+ 'vibecarbon-storage': 'File Storage',
676
+ 'vibecarbon-studio': 'Studio',
677
+ 'vibecarbon-meta': 'DB Management',
678
+ 'vibecarbon-postgres': 'PostgreSQL',
679
+ n8n: 'n8n',
680
+ metabase: 'Metabase',
681
+ prometheus: 'Prometheus',
682
+ grafana: 'Grafana',
683
+ loki: 'Loki',
684
+ };
685
+
686
+ function sinceToSeconds(since: string): number {
687
+ const match = since.match(/^(\d+)([smhd])$/);
688
+ if (!match) return 600;
689
+ const value = Number(match[1]);
690
+ const multipliers: Record<string, number> = { s: 1, m: 60, h: 3600, d: 86400 };
691
+ return value * (multipliers[match[2]] ?? 60);
692
+ }
693
+
694
+ async function k8sRequest(path: string, acceptText = false): Promise<string> {
695
+ const creds = k8sCredentials;
696
+ if (!creds) throw new Error('K8s credentials not available');
697
+
698
+ const endpoint = await getK8sApiEndpoint();
699
+ if (!endpoint) throw new Error('K8s API server not reachable');
700
+
701
+ return new Promise((resolve, reject) => {
702
+ const req = https.request(
703
+ {
704
+ hostname: endpoint.host,
705
+ port: endpoint.port,
706
+ path,
707
+ method: 'GET',
708
+ headers: {
709
+ Authorization: `Bearer ${creds.token}`,
710
+ Accept: acceptText ? 'text/plain' : 'application/json',
711
+ },
712
+ ca: creds.ca,
713
+ // When falling back to gateway:6443, the TLS cert won't match the gateway IP
714
+ // (it's issued for the ClusterIP/master hostname), so we must disable verification
715
+ rejectUnauthorized: endpoint.host === (process.env.KUBERNETES_SERVICE_HOST ?? ''),
716
+ },
717
+ (res) => {
718
+ let data = '';
719
+ res.on('data', (chunk: Buffer) => {
720
+ data += chunk.toString();
721
+ });
722
+ res.on('end', () => resolve(data));
723
+ }
724
+ );
725
+ req.setTimeout(30000, () => req.destroy(new Error('K8s API timeout')));
726
+ req.on('error', reject);
727
+ req.end();
728
+ });
729
+ }
730
+
731
+ interface K8sPod {
732
+ metadata: { name: string; labels?: Record<string, string> };
733
+ status?: { phase?: string };
734
+ }
735
+
736
+ async function listK8sPods(labelSelector?: string): Promise<K8sPod[]> {
737
+ const ns = k8sCredentials?.namespace;
738
+ if (!ns) return [];
739
+ const qs = labelSelector ? `?labelSelector=${encodeURIComponent(labelSelector)}` : '';
740
+ const body = await k8sRequest(`/api/v1/namespaces/${ns}/pods${qs}`);
741
+ const parsed = JSON.parse(body) as { items?: K8sPod[] };
742
+ return parsed.items ?? [];
743
+ }
744
+
745
+ async function getK8sPodLogs(
746
+ podName: string,
747
+ tail: number,
748
+ sinceSeconds: number
749
+ ): Promise<string[]> {
750
+ const ns = k8sCredentials?.namespace;
751
+ if (!ns) return [];
752
+ const qs = new URLSearchParams({
753
+ tailLines: String(tail),
754
+ sinceSeconds: String(sinceSeconds),
755
+ timestamps: 'true',
756
+ });
757
+ const text = await k8sRequest(`/api/v1/namespaces/${ns}/pods/${podName}/log?${qs}`, true);
758
+ return text.split('\n').filter((l) => l.trim());
759
+ }
760
+
761
+ // ============================================================================
762
+ // DOCKER LOGS
763
+ // ============================================================================
764
+
765
+ /**
766
+ * Fetch logs from Docker containers
767
+ * Query params:
768
+ * - tail: number of lines (default 200)
769
+ * - since: time filter (e.g., "5m", "1h", "30s")
770
+ * - container: specific container (optional, defaults to all)
771
+ */
772
+ servicesStatusRoutes.get('/logs', async (c) => {
773
+ const authError = requireSuperAdmin(c);
774
+ if (authError) return c.json({ error: authError.error }, authError.status);
775
+
776
+ const tail = Math.min(Number(c.req.query('tail')) || 200, 1000); // Max 1000 lines
777
+ const since = c.req.query('since') || '10m';
778
+ const container = c.req.query('container');
779
+
780
+ // Validate since format (should be like "5m", "1h", "30s")
781
+ if (!/^\d+[smhd]$/.test(since)) {
782
+ return c.json(
783
+ { success: false, error: 'Invalid since format. Use format like "5m", "1h", "30s"' },
784
+ 400
785
+ );
786
+ }
787
+
788
+ // --- Kubernetes: fetch logs via K8s API ---
789
+ if (isKubernetes) {
790
+ // Validate container param (must be a safe label value)
791
+ if (container && !/^[a-z0-9-]+$/.test(container)) {
792
+ return c.json({ success: false, error: 'Invalid container', logs: [] }, 400);
793
+ }
794
+
795
+ if (!k8sCredentials) {
796
+ return c.json(
797
+ {
798
+ success: false,
799
+ error: 'K8s service account credentials not available. Check RBAC configuration.',
800
+ logs: [],
801
+ },
802
+ 503
803
+ );
804
+ }
805
+
806
+ try {
807
+ const sinceSeconds = sinceToSeconds(since);
808
+ const labelSelector = container ? `app=${container}` : undefined;
809
+ const pods = await listK8sPods(labelSelector);
810
+ const runningPods = pods.filter((p) => p.status?.phase === 'Running');
811
+
812
+ if (runningPods.length === 0) {
813
+ return c.json({
814
+ success: true,
815
+ logs: [],
816
+ meta: {
817
+ tail,
818
+ since,
819
+ container: container || 'all',
820
+ lineCount: 0,
821
+ timestamp: new Date().toISOString(),
822
+ },
823
+ });
824
+ }
825
+
826
+ const allLogs = await Promise.all(
827
+ runningPods.map(async (pod) => {
828
+ const appLabel = pod.metadata.labels?.app ?? pod.metadata.name;
829
+ const displayName = appLabel.replace(/^vibecarbon-/, '');
830
+ try {
831
+ const lines = await getK8sPodLogs(pod.metadata.name, tail, sinceSeconds);
832
+ return lines.map((line) => `${displayName} | ${line}`);
833
+ } catch {
834
+ return [];
835
+ }
836
+ })
837
+ );
838
+
839
+ const logs = allLogs.flat().sort();
840
+
841
+ logger.debug({ tail, since, container, podCount: runningPods.length }, 'Fetched K8s logs');
842
+
843
+ return c.json({
844
+ success: true,
845
+ logs,
846
+ meta: {
847
+ tail,
848
+ since,
849
+ container: container || 'all',
850
+ lineCount: logs.length,
851
+ timestamp: new Date().toISOString(),
852
+ },
853
+ });
854
+ } catch (error) {
855
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
856
+ logger.error({ error: errorMessage }, 'Failed to fetch K8s logs');
857
+ return c.json({ success: false, error: errorMessage, logs: [] }, 500);
858
+ }
859
+ }
860
+
861
+ // --- Docker: fetch logs via docker compose ---
862
+ // Validate container if specified
863
+ if (container && !ALL_KNOWN_CONTAINERS.includes(container)) {
864
+ return c.json({ success: false, error: `Invalid container: ${container}` }, 400);
865
+ }
866
+
867
+ try {
868
+ const containerArg = container || '';
869
+ const cmd = `docker compose logs --tail ${tail} --since ${since} --timestamps ${containerArg} 2>&1`;
870
+
871
+ logger.debug({ cmd, tail, since, container }, 'Fetching Docker logs');
872
+
873
+ const { stdout } = await execAsync(cmd, {
874
+ timeout: 30000, // 30 second timeout
875
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer
876
+ });
877
+
878
+ // Parse and structure the logs
879
+ const lines = stdout.split('\n').filter((line) => line.trim());
880
+
881
+ return c.json({
882
+ success: true,
883
+ logs: lines,
884
+ meta: {
885
+ tail,
886
+ since,
887
+ container: container || 'all',
888
+ lineCount: lines.length,
889
+ timestamp: new Date().toISOString(),
890
+ },
891
+ });
892
+ } catch (error) {
893
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
894
+ logger.error({ error: errorMessage }, 'Failed to fetch Docker logs');
895
+
896
+ // Check if Docker is not available
897
+ if (errorMessage.includes('docker') && errorMessage.includes('not found')) {
898
+ return c.json(
899
+ {
900
+ success: false,
901
+ error: 'Docker is not available',
902
+ logs: [],
903
+ },
904
+ 503
905
+ );
906
+ }
907
+
908
+ return c.json(
909
+ {
910
+ success: false,
911
+ error: errorMessage,
912
+ logs: [],
913
+ },
914
+ 500
915
+ );
916
+ }
917
+ });
918
+
919
+ // GET /logs/containers - List available containers for log filtering
920
+ servicesStatusRoutes.get('/logs/containers', async (c) => {
921
+ const authError = requireSuperAdmin(c);
922
+ if (authError) return c.json({ error: authError.error }, authError.status);
923
+
924
+ if (isKubernetes) {
925
+ try {
926
+ const pods = await listK8sPods();
927
+ const seen = new Set<string>();
928
+ const containers: Array<{ name: string; container: string }> = [];
929
+ for (const pod of pods) {
930
+ if (pod.status?.phase !== 'Running') continue;
931
+ const appLabel = pod.metadata.labels?.app;
932
+ if (appLabel && !seen.has(appLabel)) {
933
+ seen.add(appLabel);
934
+ containers.push({
935
+ name: K8S_LABEL_NAMES[appLabel] ?? appLabel,
936
+ container: appLabel,
937
+ });
938
+ }
939
+ }
940
+ containers.sort((a, b) => a.name.localeCompare(b.name));
941
+ return c.json({ containers });
942
+ } catch (error) {
943
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
944
+ logger.error({ error: errorMessage }, 'Failed to list K8s pods');
945
+ return c.json({ containers: [] });
946
+ }
947
+ }
948
+
949
+ const services = await getEnabledServices();
950
+ return c.json({
951
+ containers: services.map((s) => ({
952
+ name: s.name,
953
+ container: s.container,
954
+ })),
955
+ });
956
+ });
957
+
958
+ export { servicesStatusRoutes };