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.
- package/LICENSE +663 -0
- package/README.md +188 -0
- package/carbon/.claude/hooks/task-completed-gate.sh +19 -0
- package/carbon/.claude/hooks/teammate-idle-gate.sh +29 -0
- package/carbon/.claude/settings.json +29 -0
- package/carbon/.cursor/rules/vibecarbon.mdc +6 -0
- package/carbon/.dockerignore +57 -0
- package/carbon/.env.example +219 -0
- package/carbon/.github/copilot-instructions.md +3 -0
- package/carbon/.github/workflows/deploy.yml +346 -0
- package/carbon/.github/workflows/vibecarbon-build.yml +81 -0
- package/carbon/.windsurfrules +74 -0
- package/carbon/AGENTS.md +422 -0
- package/carbon/CLAUDE.md +3 -0
- package/carbon/DEVELOPMENT.md +187 -0
- package/carbon/Dockerfile +98 -0
- package/carbon/LICENSE +21 -0
- package/carbon/PRODUCTION.md +364 -0
- package/carbon/README.md +193 -0
- package/carbon/TESTING.md +138 -0
- package/carbon/backup/Dockerfile +75 -0
- package/carbon/backup/backup.sh +95 -0
- package/carbon/backup/compose-backup.sh +140 -0
- package/carbon/biome.json +83 -0
- package/carbon/cloud-init/docker-ce-setup.yaml +55 -0
- package/carbon/cloud-init/k3s/master-init.sh +215 -0
- package/carbon/cloud-init/k3s/supabase-init.sh +167 -0
- package/carbon/cloud-init/k3s/worker-init.sh +147 -0
- package/carbon/components.json +24 -0
- package/carbon/content/blog/authentication-guide.mdx +34 -0
- package/carbon/content/blog/getting-started.mdx +41 -0
- package/carbon/content/changelog/v0-1-0.mdx +40 -0
- package/carbon/content/docs/analytics.mdx +90 -0
- package/carbon/content/docs/authentication.mdx +231 -0
- package/carbon/content/docs/background-jobs.mdx +116 -0
- package/carbon/content/docs/cli.mdx +630 -0
- package/carbon/content/docs/database.mdx +236 -0
- package/carbon/content/docs/deployment.mdx +227 -0
- package/carbon/content/docs/development.mdx +238 -0
- package/carbon/content/docs/environments.mdx +84 -0
- package/carbon/content/docs/getting-started.mdx +112 -0
- package/carbon/content/docs/legal/privacy-policy.mdx +89 -0
- package/carbon/content/docs/legal/terms-of-service.mdx +99 -0
- package/carbon/content/docs/optional-services.mdx +160 -0
- package/carbon/db/Dockerfile +23 -0
- package/carbon/docker-compose.dev-init.yml +5 -0
- package/carbon/docker-compose.metabase.override.yml +14 -0
- package/carbon/docker-compose.metabase.prod.yml +28 -0
- package/carbon/docker-compose.metabase.yml +84 -0
- package/carbon/docker-compose.n8n.override.yml +14 -0
- package/carbon/docker-compose.n8n.prod.yml +31 -0
- package/carbon/docker-compose.n8n.yml +97 -0
- package/carbon/docker-compose.observability.override.yml +18 -0
- package/carbon/docker-compose.observability.prod.yml +28 -0
- package/carbon/docker-compose.observability.yml +125 -0
- package/carbon/docker-compose.override.yml +28 -0
- package/carbon/docker-compose.prod.yml +294 -0
- package/carbon/docker-compose.yml +508 -0
- package/carbon/docker-entrypoint.sh +12 -0
- package/carbon/ha/activate-standby.sh +52 -0
- package/carbon/ha/primary-init.sql +36 -0
- package/carbon/ha/standby-init.sh +74 -0
- package/carbon/k8s/LICENSE +99 -0
- package/carbon/k8s/README.md +272 -0
- package/carbon/k8s/base/app/deployment.yaml +130 -0
- package/carbon/k8s/base/app/hpa.yaml +44 -0
- package/carbon/k8s/base/app/kustomization.yaml +10 -0
- package/carbon/k8s/base/app/network-policy.yaml +124 -0
- package/carbon/k8s/base/app/pdb.yaml +14 -0
- package/carbon/k8s/base/app/rbac.yaml +36 -0
- package/carbon/k8s/base/app/service.yaml +16 -0
- package/carbon/k8s/base/backup/cronjob.yaml +120 -0
- package/carbon/k8s/base/backup/kustomization.yaml +7 -0
- package/carbon/k8s/base/backup/network-policy.yaml +31 -0
- package/carbon/k8s/base/backup/rbac.yaml +7 -0
- package/carbon/k8s/base/cluster-autoscaler/deployment.yaml +103 -0
- package/carbon/k8s/base/cluster-autoscaler/kustomization.yaml +17 -0
- package/carbon/k8s/base/cluster-autoscaler/rbac.yaml +109 -0
- package/carbon/k8s/base/config/configmap.yaml +48 -0
- package/carbon/k8s/base/config/kustomization.yaml +8 -0
- package/carbon/k8s/base/hetzner-ccm/kustomization.yaml +9 -0
- package/carbon/k8s/base/hetzner-csi/kustomization.yaml +9 -0
- package/carbon/k8s/base/kustomization.yaml +43 -0
- package/carbon/k8s/base/namespace.yaml +7 -0
- package/carbon/k8s/base/network-policies.yaml +95 -0
- package/carbon/k8s/base/registry/kustomization.yaml +5 -0
- package/carbon/k8s/base/registry/local-registry.yaml +193 -0
- package/carbon/k8s/base/traefik/certificate.yaml +21 -0
- package/carbon/k8s/base/traefik/configmap.yaml +13 -0
- package/carbon/k8s/base/traefik/deployment.yaml +165 -0
- package/carbon/k8s/base/traefik/ingressroute.yaml +141 -0
- package/carbon/k8s/base/traefik/kustomization.yaml +11 -0
- package/carbon/k8s/base/traefik/middleware.yaml +109 -0
- package/carbon/k8s/base/traefik/network-policy.yaml +92 -0
- package/carbon/k8s/base/traefik/service.yaml +38 -0
- package/carbon/k8s/flux/README.md +49 -0
- package/carbon/k8s/flux/clusters/primary/vibecarbon.yaml +128 -0
- package/carbon/k8s/flux/clusters/standby/vibecarbon.yaml +83 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-release.yaml +38 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-repository.yaml +16 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/kustomization.yaml +10 -0
- package/carbon/k8s/gitops/supabase/helm-release.yaml +66 -0
- package/carbon/k8s/gitops/supabase/helm-repository.yaml +18 -0
- package/carbon/k8s/gitops/supabase/kustomization.yaml +33 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-cloudflare.yaml +51 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-hetzner.yaml +59 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-manual.yaml +43 -0
- package/carbon/k8s/infra/cert-manager-resources/kustomization.yaml +15 -0
- package/carbon/k8s/infra/kustomization.yaml +21 -0
- package/carbon/k8s/infra/traefik-crds/kustomization.yaml +10 -0
- package/carbon/k8s/overlays/local/configmap.yaml +23 -0
- package/carbon/k8s/overlays/local/ingressroute-studio.yaml +104 -0
- package/carbon/k8s/overlays/local/kustomization.yaml +215 -0
- package/carbon/k8s/overlays/local/namespace.yaml +8 -0
- package/carbon/k8s/overlays/local/secrets.yaml +32 -0
- package/carbon/k8s/overlays/production/kustomization.yaml +34 -0
- package/carbon/k8s/test-local.sh +318 -0
- package/carbon/k8s/values/supabase.values.yaml +133 -0
- package/carbon/package.json +154 -0
- package/carbon/runtime.Dockerfile +17 -0
- package/carbon/scripts/_dev-jwt.mjs +45 -0
- package/carbon/scripts/check-rls.ts +53 -0
- package/carbon/scripts/dev-init.js +191 -0
- package/carbon/scripts/dev.js +191 -0
- package/carbon/scripts/docker-down.js +63 -0
- package/carbon/scripts/docker-logs.js +59 -0
- package/carbon/scripts/docker-up.js +222 -0
- package/carbon/scripts/generate-dev-configs.sh +131 -0
- package/carbon/scripts/generate-rss.ts +116 -0
- package/carbon/scripts/generate-sitemap.ts +102 -0
- package/carbon/scripts/k8s-apply.js +71 -0
- package/carbon/scripts/k8s-delete.js +75 -0
- package/carbon/scripts/lib/manifest.js +176 -0
- package/carbon/scripts/secret-scan.mjs +278 -0
- package/carbon/scripts/validate-dev-configs.sh +101 -0
- package/carbon/src/client/App.tsx +202 -0
- package/carbon/src/client/assets/hyperformant-dark.svg +29 -0
- package/carbon/src/client/assets/hyperformant-light.svg +29 -0
- package/carbon/src/client/assets/logos/aider.svg +1 -0
- package/carbon/src/client/assets/logos/antigravity.svg +1 -0
- package/carbon/src/client/assets/logos/bolt.svg +1 -0
- package/carbon/src/client/assets/logos/claude-code.svg +1 -0
- package/carbon/src/client/assets/logos/copilot.svg +1 -0
- package/carbon/src/client/assets/logos/cursor.svg +1 -0
- package/carbon/src/client/assets/logos/gemini-cli.svg +1 -0
- package/carbon/src/client/assets/logos/lovable.svg +1 -0
- package/carbon/src/client/assets/logos/openai-codex.svg +1 -0
- package/carbon/src/client/assets/logos/v0.svg +1 -0
- package/carbon/src/client/assets/logos/windsurf.svg +1 -0
- package/carbon/src/client/assets/vibecarbon-icon.svg +19 -0
- package/carbon/src/client/assets/vibecarbon-logo-dark.svg +29 -0
- package/carbon/src/client/assets/vibecarbon-logo-light.svg +29 -0
- package/carbon/src/client/components/AIIntegrationSection.tsx +120 -0
- package/carbon/src/client/components/AppSidebar.tsx +760 -0
- package/carbon/src/client/components/ArchitectureSection.tsx +46 -0
- package/carbon/src/client/components/CTAFooter.tsx +59 -0
- package/carbon/src/client/components/ComparisonSection.tsx +132 -0
- package/carbon/src/client/components/ContentPanel.tsx +66 -0
- package/carbon/src/client/components/ContentSkeleton.tsx +21 -0
- package/carbon/src/client/components/ErrorBoundary.tsx +46 -0
- package/carbon/src/client/components/FAQSection.tsx +76 -0
- package/carbon/src/client/components/FileUpload.tsx +210 -0
- package/carbon/src/client/components/HeaderActions.tsx +17 -0
- package/carbon/src/client/components/Hero.tsx +608 -0
- package/carbon/src/client/components/ImpersonationBanner.tsx +31 -0
- package/carbon/src/client/components/LanguageSwitcher.tsx +67 -0
- package/carbon/src/client/components/Logo.tsx +87 -0
- package/carbon/src/client/components/LogoStrip.tsx +76 -0
- package/carbon/src/client/components/MetricsStrip.tsx +48 -0
- package/carbon/src/client/components/Nav.tsx +195 -0
- package/carbon/src/client/components/NewsletterSignup.tsx +147 -0
- package/carbon/src/client/components/NotificationDrawer.tsx +171 -0
- package/carbon/src/client/components/PageHeader.tsx +24 -0
- package/carbon/src/client/components/PlanGate.tsx +36 -0
- package/carbon/src/client/components/PricingSection.tsx +371 -0
- package/carbon/src/client/components/SEO.tsx +34 -0
- package/carbon/src/client/components/SmoothScroll.tsx +45 -0
- package/carbon/src/client/components/TechStackSection.tsx +165 -0
- package/carbon/src/client/components/WorkflowSection.tsx +604 -0
- package/carbon/src/client/components/admin/DockerLogs.tsx +276 -0
- package/carbon/src/client/components/admin/PerformanceMetrics.tsx +230 -0
- package/carbon/src/client/components/admin/ServiceCard.tsx +45 -0
- package/carbon/src/client/components/admin/ServicesStatus.tsx +296 -0
- package/carbon/src/client/components/architecture/ArchitectureDiagram.tsx +413 -0
- package/carbon/src/client/components/auth/AuthProvider.tsx +466 -0
- package/carbon/src/client/components/effects/FilmGrainOverlay.tsx +35 -0
- package/carbon/src/client/components/effects/FresnelEdge.tsx +49 -0
- package/carbon/src/client/components/effects/GlowTracker.tsx +46 -0
- package/carbon/src/client/components/effects/SparkBurst.tsx +145 -0
- package/carbon/src/client/components/effects/VGlowEffect.tsx +83 -0
- package/carbon/src/client/components/effects/index.ts +2 -0
- package/carbon/src/client/components/layouts/SidebarLayout.tsx +20 -0
- package/carbon/src/client/components/scroll/ScrollSection.tsx +76 -0
- package/carbon/src/client/components/scroll/ScrollytellingProvider.tsx +81 -0
- package/carbon/src/client/components/scroll/index.ts +2 -0
- package/carbon/src/client/components/ui/accordion.tsx +71 -0
- package/carbon/src/client/components/ui/alert-dialog.tsx +162 -0
- package/carbon/src/client/components/ui/alert.tsx +73 -0
- package/carbon/src/client/components/ui/aspect-ratio.tsx +22 -0
- package/carbon/src/client/components/ui/avatar.tsx +91 -0
- package/carbon/src/client/components/ui/badge.tsx +50 -0
- package/carbon/src/client/components/ui/breadcrumb.tsx +100 -0
- package/carbon/src/client/components/ui/button-group.tsx +78 -0
- package/carbon/src/client/components/ui/button.tsx +120 -0
- package/carbon/src/client/components/ui/calendar.tsx +182 -0
- package/carbon/src/client/components/ui/card.tsx +85 -0
- package/carbon/src/client/components/ui/carousel.tsx +227 -0
- package/carbon/src/client/components/ui/chart.tsx +357 -0
- package/carbon/src/client/components/ui/checkbox.tsx +27 -0
- package/carbon/src/client/components/ui/collapsible.tsx +15 -0
- package/carbon/src/client/components/ui/command.tsx +178 -0
- package/carbon/src/client/components/ui/context-menu.tsx +233 -0
- package/carbon/src/client/components/ui/dialog.tsx +132 -0
- package/carbon/src/client/components/ui/drawer.tsx +118 -0
- package/carbon/src/client/components/ui/dropdown-menu.tsx +242 -0
- package/carbon/src/client/components/ui/empty.tsx +94 -0
- package/carbon/src/client/components/ui/field.tsx +226 -0
- package/carbon/src/client/components/ui/hover-card.tsx +44 -0
- package/carbon/src/client/components/ui/input-group.tsx +146 -0
- package/carbon/src/client/components/ui/input-otp.tsx +83 -0
- package/carbon/src/client/components/ui/input.tsx +20 -0
- package/carbon/src/client/components/ui/item.tsx +188 -0
- package/carbon/src/client/components/ui/kbd.tsx +26 -0
- package/carbon/src/client/components/ui/label.tsx +21 -0
- package/carbon/src/client/components/ui/menubar.tsx +250 -0
- package/carbon/src/client/components/ui/navigation-menu.tsx +155 -0
- package/carbon/src/client/components/ui/pagination.tsx +102 -0
- package/carbon/src/client/components/ui/popover.tsx +75 -0
- package/carbon/src/client/components/ui/progress.tsx +66 -0
- package/carbon/src/client/components/ui/radio-group.tsx +36 -0
- package/carbon/src/client/components/ui/resizable.tsx +46 -0
- package/carbon/src/client/components/ui/scroll-area.tsx +48 -0
- package/carbon/src/client/components/ui/select.tsx +186 -0
- package/carbon/src/client/components/ui/separator.tsx +21 -0
- package/carbon/src/client/components/ui/sheet.tsx +124 -0
- package/carbon/src/client/components/ui/sidebar.tsx +702 -0
- package/carbon/src/client/components/ui/skeleton.tsx +13 -0
- package/carbon/src/client/components/ui/slider.tsx +57 -0
- package/carbon/src/client/components/ui/sonner.tsx +45 -0
- package/carbon/src/client/components/ui/spinner.tsx +15 -0
- package/carbon/src/client/components/ui/switch.tsx +30 -0
- package/carbon/src/client/components/ui/table.tsx +89 -0
- package/carbon/src/client/components/ui/tabs.tsx +73 -0
- package/carbon/src/client/components/ui/textarea.tsx +18 -0
- package/carbon/src/client/components/ui/toggle-group.tsx +87 -0
- package/carbon/src/client/components/ui/toggle.tsx +44 -0
- package/carbon/src/client/components/ui/tooltip.tsx +56 -0
- package/carbon/src/client/hooks/api/index.ts +14 -0
- package/carbon/src/client/hooks/api/useAuthSettings.ts +80 -0
- package/carbon/src/client/hooks/api/useSubscription.ts +87 -0
- package/carbon/src/client/hooks/use-mobile.ts +21 -0
- package/carbon/src/client/hooks/useMousePosition.ts +91 -0
- package/carbon/src/client/hooks/useNotifications.tsx +124 -0
- package/carbon/src/client/hooks/useOrganizationMembers.ts +127 -0
- package/carbon/src/client/hooks/useOrganizations.tsx +230 -0
- package/carbon/src/client/hooks/usePackageManager.ts +69 -0
- package/carbon/src/client/hooks/useReducedMotion.ts +20 -0
- package/carbon/src/client/hooks/useRunningServices.ts +114 -0
- package/carbon/src/client/hooks/useScrollProgress.ts +56 -0
- package/carbon/src/client/index.css +467 -0
- package/carbon/src/client/index.html +56 -0
- package/carbon/src/client/lib/admin-services.ts +151 -0
- package/carbon/src/client/lib/api.ts +32 -0
- package/carbon/src/client/lib/blog.ts +35 -0
- package/carbon/src/client/lib/changelog.ts +33 -0
- package/carbon/src/client/lib/docs-search.ts +101 -0
- package/carbon/src/client/lib/docs.ts +37 -0
- package/carbon/src/client/lib/i18n.ts +32 -0
- package/carbon/src/client/lib/supabase.ts +72 -0
- package/carbon/src/client/lib/tailwind-colors.ts +357 -0
- package/carbon/src/client/lib/theme.ts +117 -0
- package/carbon/src/client/lib/utils.ts +22 -0
- package/carbon/src/client/locales/de.json +529 -0
- package/carbon/src/client/locales/en.json +461 -0
- package/carbon/src/client/locales/es.json +529 -0
- package/carbon/src/client/locales/fr.json +529 -0
- package/carbon/src/client/locales/pt.json +529 -0
- package/carbon/src/client/main.tsx +56 -0
- package/carbon/src/client/mdx.d.ts +13 -0
- package/carbon/src/client/pages/ApiDocs.tsx +76 -0
- package/carbon/src/client/pages/AuthCallback.tsx +34 -0
- package/carbon/src/client/pages/Blog.tsx +167 -0
- package/carbon/src/client/pages/Changelog.tsx +171 -0
- package/carbon/src/client/pages/Charts.tsx +388 -0
- package/carbon/src/client/pages/Checkout.tsx +227 -0
- package/carbon/src/client/pages/Contact.tsx +174 -0
- package/carbon/src/client/pages/Dashboard.tsx +368 -0
- package/carbon/src/client/pages/Docs.tsx +372 -0
- package/carbon/src/client/pages/ForgotPassword.tsx +111 -0
- package/carbon/src/client/pages/Home.tsx +187 -0
- package/carbon/src/client/pages/Legal.tsx +100 -0
- package/carbon/src/client/pages/Login.tsx +408 -0
- package/carbon/src/client/pages/MFAVerify.tsx +156 -0
- package/carbon/src/client/pages/NotFound.tsx +21 -0
- package/carbon/src/client/pages/Onboarding.tsx +246 -0
- package/carbon/src/client/pages/ResetPassword.tsx +200 -0
- package/carbon/src/client/pages/UIComponents.tsx +390 -0
- package/carbon/src/client/pages/admin/ContactSubmissions.tsx +220 -0
- package/carbon/src/client/pages/admin/Dashboard.tsx +24 -0
- package/carbon/src/client/pages/admin/Infrastructure.tsx +257 -0
- package/carbon/src/client/pages/admin/Jobs.tsx +225 -0
- package/carbon/src/client/pages/admin/Logs.tsx +18 -0
- package/carbon/src/client/pages/admin/Newsletter.tsx +300 -0
- package/carbon/src/client/pages/admin/Notifications.tsx +603 -0
- package/carbon/src/client/pages/admin/Organizations.tsx +306 -0
- package/carbon/src/client/pages/admin/Settings.tsx +314 -0
- package/carbon/src/client/pages/admin/Theme.tsx +465 -0
- package/carbon/src/client/pages/admin/Users.tsx +365 -0
- package/carbon/src/client/pages/api-docs.css +156 -0
- package/carbon/src/client/pages/organizations/Details.tsx +200 -0
- package/carbon/src/client/pages/organizations/Members.tsx +402 -0
- package/carbon/src/client/pages/settings/Billing.tsx +473 -0
- package/carbon/src/client/pages/settings/Profile.tsx +160 -0
- package/carbon/src/client/pages/settings/Security.tsx +341 -0
- package/carbon/src/client/public/favicon.svg +19 -0
- package/carbon/src/client/public/robots.txt +8 -0
- package/carbon/src/server/billing/index.ts +104 -0
- package/carbon/src/server/billing/provider.ts +81 -0
- package/carbon/src/server/billing/providers/paddle.ts +314 -0
- package/carbon/src/server/billing/providers/polar.ts +325 -0
- package/carbon/src/server/billing/providers/stripe.ts +233 -0
- package/carbon/src/server/emails/templates.ts +116 -0
- package/carbon/src/server/index.ts +554 -0
- package/carbon/src/server/lib/auth.ts +6 -0
- package/carbon/src/server/lib/email.ts +64 -0
- package/carbon/src/server/lib/env.ts +112 -0
- package/carbon/src/server/lib/errors.ts +21 -0
- package/carbon/src/server/lib/logger.ts +17 -0
- package/carbon/src/server/lib/rate-limiter.ts +288 -0
- package/carbon/src/server/lib/request.ts +34 -0
- package/carbon/src/server/lib/stripe.ts +42 -0
- package/carbon/src/server/lib/supabase.ts +65 -0
- package/carbon/src/server/middleware/requirePlan.ts +80 -0
- package/carbon/src/server/routes/_internal/services-status.ts +958 -0
- package/carbon/src/server/routes/_internal/verify-role.ts +185 -0
- package/carbon/src/server/routes/health.ts +48 -0
- package/carbon/src/server/routes/v1/admin/contact.ts +128 -0
- package/carbon/src/server/routes/v1/admin/jobs.ts +171 -0
- package/carbon/src/server/routes/v1/admin/newsletter.ts +237 -0
- package/carbon/src/server/routes/v1/auth.ts +390 -0
- package/carbon/src/server/routes/v1/billing.ts +718 -0
- package/carbon/src/server/routes/v1/contact.ts +93 -0
- package/carbon/src/server/routes/v1/index.ts +1333 -0
- package/carbon/src/server/routes/v1/newsletter.ts +181 -0
- package/carbon/src/server/routes/v1/performance.ts +157 -0
- package/carbon/src/server/routes/v1/stats.ts +170 -0
- package/carbon/src/server/routes/v1/theme.ts +106 -0
- package/carbon/src/server/routes/webhooks/billing.ts +376 -0
- package/carbon/src/server/routes/webhooks/stripe.ts +276 -0
- package/carbon/src/server/types.ts +11 -0
- package/carbon/src/shared/pricing.ts +155 -0
- package/carbon/src/shared/types.ts +338 -0
- package/carbon/supabase/migrations/00001_init.sql +717 -0
- package/carbon/supabase/migrations/00002_theme_settings.sql +13 -0
- package/carbon/supabase/migrations/00003_pg_cron.sql +121 -0
- package/carbon/supabase/migrations/00004_contact_newsletter.sql +81 -0
- package/carbon/supabase/migrations/00005_localization_languages.sql +22 -0
- package/carbon/supabase/seed.sql +16 -0
- package/carbon/tests/_helpers/app.ts +45 -0
- package/carbon/tests/_helpers/env.ts +37 -0
- package/carbon/tests/_helpers/factories.ts +69 -0
- package/carbon/tests/_helpers/jwt.ts +23 -0
- package/carbon/tests/_helpers/setup-integration.ts +20 -0
- package/carbon/tests/_helpers/setup-rtl.ts +12 -0
- package/carbon/tests/component/ErrorBoundary.test.tsx +53 -0
- package/carbon/tests/component/use-auth-settings.test.tsx +119 -0
- package/carbon/tests/integration/server/routes/contact.test.ts +162 -0
- package/carbon/tests/integration/server/routes/health.test.ts +61 -0
- package/carbon/tests/structural/i18n-parity.test.ts +42 -0
- package/carbon/tests/unit/client/utils.test.ts +49 -0
- package/carbon/tests/unit/shared/pricing.test.ts +93 -0
- package/carbon/tsconfig.json +27 -0
- package/carbon/tsconfig.server.json +27 -0
- package/carbon/tsconfig.test.json +9 -0
- package/carbon/vite.config.ts +110 -0
- package/carbon/vitest.config.ts +74 -0
- package/carbon/volumes/db/jwt.sql +57 -0
- package/carbon/volumes/db/metabase-init.sh +29 -0
- package/carbon/volumes/db/n8n-init.sh +25 -0
- package/carbon/volumes/db/realtime.sql +33 -0
- package/carbon/volumes/db/roles.sql +93 -0
- package/carbon/volumes/db/set-passwords.sh +12 -0
- package/carbon/volumes/db/super-admin.dev.sql +113 -0
- package/carbon/volumes/db/super-admin.generated.sql +113 -0
- package/carbon/volumes/db/super-admin.sql +114 -0
- package/carbon/volumes/grafana/dashboards/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards/overview.json +523 -0
- package/carbon/volumes/grafana/dashboards/postgresql.json +337 -0
- package/carbon/volumes/grafana/dashboards.dev/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards.dev/overview.json +156 -0
- package/carbon/volumes/grafana/dashboards.dev/postgresql.json +337 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +16 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.yml +16 -0
- package/carbon/volumes/grafana/provisioning/datasources/datasources.yml +43 -0
- package/carbon/volumes/kong/docker-entrypoint.sh +9 -0
- package/carbon/volumes/kong/kong.yml +208 -0
- package/carbon/volumes/loki/loki-config.yml +58 -0
- package/carbon/volumes/n8n/hooks.js +131 -0
- package/carbon/volumes/n8n/scripts/setup.sh +66 -0
- package/carbon/volumes/prometheus/prometheus.yml +43 -0
- package/carbon/volumes/promtail/promtail-config.yml +64 -0
- package/carbon/volumes/traefik/middlewares.dev.yml +83 -0
- package/carbon/volumes/traefik/middlewares.yml +95 -0
- package/carbon/volumes/traefik/vite-dev.yml +20 -0
- package/package.json +95 -0
- package/src/access.js +354 -0
- package/src/activate.js +187 -0
- package/src/add.js +718 -0
- package/src/backup.js +786 -0
- package/src/cli.js +350 -0
- package/src/configure.js +967 -0
- package/src/console.js +155 -0
- package/src/create.js +1499 -0
- package/src/deploy.js +311 -0
- package/src/destroy.js +2033 -0
- package/src/diagnose.js +735 -0
- package/src/down.js +80 -0
- package/src/failover.js +1032 -0
- package/src/lib/backup-s3.js +179 -0
- package/src/lib/build.js +33 -0
- package/src/lib/checksum.js +28 -0
- package/src/lib/ci-setup.js +666 -0
- package/src/lib/cli/help.js +129 -0
- package/src/lib/cli/parse-flags.js +160 -0
- package/src/lib/cli/select-action.js +65 -0
- package/src/lib/cli/select-environment.js +108 -0
- package/src/lib/cli/tty-guard.js +75 -0
- package/src/lib/cloudflare.js +447 -0
- package/src/lib/colors.js +52 -0
- package/src/lib/command.js +361 -0
- package/src/lib/config.js +359 -0
- package/src/lib/cost.js +103 -0
- package/src/lib/deploy/bundle.js +231 -0
- package/src/lib/deploy/compose/acme-verify.js +121 -0
- package/src/lib/deploy/compose/build-args.js +78 -0
- package/src/lib/deploy/compose/ha.js +1874 -0
- package/src/lib/deploy/compose/index.js +1294 -0
- package/src/lib/deploy/compose/reconcile.js +74 -0
- package/src/lib/deploy/github.js +382 -0
- package/src/lib/deploy/image.js +191 -0
- package/src/lib/deploy/index.js +119 -0
- package/src/lib/deploy/k8s/LICENSE +99 -0
- package/src/lib/deploy/k8s/gitops-deploy.js +298 -0
- package/src/lib/deploy/k8s/ha/index.js +1000 -0
- package/src/lib/deploy/k8s/index.js +62 -0
- package/src/lib/deploy/k8s/k3s.js +2515 -0
- package/src/lib/deploy/orchestrator.js +1401 -0
- package/src/lib/deploy/prompts.js +545 -0
- package/src/lib/deploy/remote-build.js +130 -0
- package/src/lib/deploy/state.js +120 -0
- package/src/lib/deploy/utils.js +328 -0
- package/src/lib/deploy-logger.js +93 -0
- package/src/lib/dns-propagation.js +48 -0
- package/src/lib/environment.js +58 -0
- package/src/lib/fetch-retry.js +103 -0
- package/src/lib/github-environments.js +335 -0
- package/src/lib/hetzner-dns.js +377 -0
- package/src/lib/hetzner-guided-setup.js +275 -0
- package/src/lib/host-keys.js +37 -0
- package/src/lib/iac/cloud-init.js +52 -0
- package/src/lib/iac/index.js +325 -0
- package/src/lib/iac/programs/hetzner-compose.js +130 -0
- package/src/lib/iac/programs/hetzner-k8s.js +320 -0
- package/src/lib/kubectl.js +81 -0
- package/src/lib/licensing/index.js +259 -0
- package/src/lib/licensing/tiers.js +181 -0
- package/src/lib/licensing/validator.js +171 -0
- package/src/lib/merge-package-json.js +90 -0
- package/src/lib/operator-ip.js +381 -0
- package/src/lib/package-manager.js +111 -0
- package/src/lib/perf.js +71 -0
- package/src/lib/project-guard.js +39 -0
- package/src/lib/project.js +334 -0
- package/src/lib/providers/base.js +276 -0
- package/src/lib/providers/hetzner-s3.js +656 -0
- package/src/lib/providers/hetzner.js +755 -0
- package/src/lib/providers/index.js +164 -0
- package/src/lib/s3.js +510 -0
- package/src/lib/secret-scan.js +583 -0
- package/src/lib/secrets.js +63 -0
- package/src/lib/server-types.js +195 -0
- package/src/lib/shell.js +91 -0
- package/src/lib/ssh.js +241 -0
- package/src/lib/tracker.js +242 -0
- package/src/lib/upgrade-policy.js +170 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/version.js +5 -0
- package/src/remove.js +292 -0
- package/src/reset.js +97 -0
- package/src/restore.js +871 -0
- package/src/scale.js +1734 -0
- package/src/shell.js +222 -0
- package/src/status.js +981 -0
- package/src/up.js +264 -0
- 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 };
|