openship 0.1.0 → 0.1.1
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/.env.example +19 -3
- package/CONTRIBUTING.md +69 -19
- package/LICENSE +661 -0
- package/README.md +304 -108
- package/apps/api/Dockerfile +6 -8
- package/apps/api/package.json +25 -21
- package/apps/api/src/app.ts +46 -3
- package/apps/api/src/config/env.ts +81 -7
- package/apps/api/src/lib/auth-mode.ts +42 -0
- package/apps/api/src/lib/auth.ts +105 -0
- package/apps/api/src/lib/cache.ts +128 -0
- package/apps/api/src/lib/cloud-auth-proxy.ts +310 -0
- package/apps/api/src/lib/cloud-client.ts +103 -0
- package/apps/api/src/lib/compose-parser.ts +138 -0
- package/apps/api/src/lib/controller-helpers.ts +90 -0
- package/apps/api/src/lib/email-templates.ts +94 -0
- package/apps/api/src/lib/encryption.ts +102 -0
- package/apps/api/src/lib/local-user.ts +65 -0
- package/apps/api/src/lib/mail.ts +48 -0
- package/apps/api/src/lib/notifications.ts +195 -0
- package/apps/api/src/lib/openship-cloud.ts +101 -0
- package/apps/api/src/lib/resources.ts +96 -0
- package/apps/api/src/lib/screenshots.ts +189 -0
- package/apps/api/src/lib/ssh-manager.ts +369 -0
- package/apps/api/src/lib/ssl-scheduler.ts +109 -0
- package/apps/api/src/lib/stack-detector.ts +708 -0
- package/apps/api/src/lib/system-debug.ts +10 -0
- package/apps/api/src/middleware/auth.ts +49 -13
- package/apps/api/src/middleware/error-handler.ts +23 -3
- package/apps/api/src/middleware/index.ts +2 -0
- package/apps/api/src/middleware/internal-auth.ts +40 -0
- package/apps/api/src/middleware/local-only.ts +20 -0
- package/apps/api/src/middleware/rate-limiter.ts +9 -0
- package/apps/api/src/modules/analytics/analytics.controller.ts +111 -0
- package/apps/api/src/modules/analytics/analytics.routes.ts +29 -0
- package/apps/api/src/modules/analytics/analytics.schema.ts +37 -0
- package/apps/api/src/modules/analytics/analytics.service.ts +264 -0
- package/apps/api/src/modules/auth/auth.routes.ts +233 -6
- package/apps/api/src/modules/billing/billing.routes.ts +8 -0
- package/apps/api/src/modules/cloud/cloud.controller.ts +225 -0
- package/apps/api/src/modules/cloud/cloud.routes.ts +40 -0
- package/apps/api/src/modules/deployments/build.service.ts +791 -0
- package/apps/api/src/modules/deployments/deployment-lifecycle.ts +138 -0
- package/apps/api/src/modules/deployments/deployment.controller.ts +324 -16
- package/apps/api/src/modules/deployments/deployment.routes.ts +33 -7
- package/apps/api/src/modules/deployments/deployment.schema.ts +37 -5
- package/apps/api/src/modules/deployments/deployment.service.ts +178 -11
- package/apps/api/src/modules/deployments/preflight.ts +280 -0
- package/apps/api/src/modules/deployments/prepare.service.ts +252 -0
- package/apps/api/src/modules/deployments/session-manager.ts +283 -0
- package/apps/api/src/modules/deployments/ssl.service.ts +73 -0
- package/apps/api/src/modules/domains/domain.controller.ts +59 -7
- package/apps/api/src/modules/domains/domain.routes.ts +20 -5
- package/apps/api/src/modules/domains/domain.schema.ts +35 -0
- package/apps/api/src/modules/domains/domain.service.ts +375 -9
- package/apps/api/src/modules/github/github.auth.ts +397 -0
- package/apps/api/src/modules/github/github.controller.ts +387 -0
- package/apps/api/src/modules/github/github.local-auth.ts +251 -0
- package/apps/api/src/modules/github/github.routes.ts +50 -0
- package/apps/api/src/modules/github/github.schema.ts +52 -0
- package/apps/api/src/modules/github/github.service.ts +592 -0
- package/apps/api/src/modules/github/github.types.ts +158 -0
- package/apps/api/src/modules/github/github.webhook.ts +186 -0
- package/apps/api/src/modules/github/index.ts +16 -0
- package/apps/api/src/modules/health/health.routes.ts +35 -0
- package/apps/api/src/modules/projects/project.controller.ts +481 -12
- package/apps/api/src/modules/projects/project.routes.ts +63 -6
- package/apps/api/src/modules/projects/project.schema.ts +104 -6
- package/apps/api/src/modules/projects/project.service.ts +502 -11
- package/apps/api/src/modules/settings/settings.controller.ts +55 -0
- package/apps/api/src/modules/settings/settings.routes.ts +25 -0
- package/apps/api/src/modules/settings/settings.service.ts +49 -0
- package/apps/api/src/modules/system/filesystem.controller.ts +66 -0
- package/apps/api/src/modules/system/index.ts +1 -0
- package/apps/api/src/modules/system/server-check.controller.ts +475 -0
- package/apps/api/src/modules/system/setup-session.ts +255 -0
- package/apps/api/src/modules/system/setup.controller.ts +128 -0
- package/apps/api/src/modules/system/system.routes.ts +42 -0
- package/apps/api/src/modules/webhooks/webhook.controller.ts +57 -15
- package/apps/api/src/modules/webhooks/webhook.routes.ts +11 -8
- package/apps/api/src/modules/webhooks/webhook.service.ts +46 -5
- package/apps/api/src/modules/webhooks/webhook.types.ts +35 -0
- package/apps/api/tsconfig.json +3 -1
- package/apps/cli/package.json +8 -10
- package/apps/cli/tsconfig.json +2 -1
- package/apps/dashboard/Dockerfile +4 -6
- package/apps/dashboard/components.json +20 -0
- package/apps/dashboard/next-env.d.ts +6 -0
- package/apps/dashboard/next.config.mjs +11 -1
- package/apps/dashboard/package.json +25 -13
- package/apps/dashboard/postcss.config.mjs +5 -0
- package/apps/dashboard/public/android-chrome-192x192.png +0 -0
- package/apps/dashboard/public/android-chrome-512x512.png +0 -0
- package/apps/dashboard/public/apple-touch-icon.png +0 -0
- package/apps/dashboard/public/favicon-16x16.png +0 -0
- package/apps/dashboard/public/favicon-32x32.png +0 -0
- package/apps/dashboard/public/favicon.ico +0 -0
- package/apps/dashboard/public/site.webmanifest +19 -0
- package/apps/dashboard/src/app/(auth)/forgot-password/page.tsx +114 -0
- package/apps/dashboard/src/app/(auth)/layout.tsx +18 -3
- package/apps/dashboard/src/app/(auth)/login/page.tsx +215 -6
- package/apps/dashboard/src/app/(auth)/providers.tsx +34 -0
- package/apps/dashboard/src/app/(auth)/register/page.tsx +171 -5
- package/apps/dashboard/src/app/(auth)/reset-password/page.tsx +172 -0
- package/apps/dashboard/src/app/(auth)/verify-email/page.tsx +190 -0
- package/apps/dashboard/src/app/(dashboard)/(deployment)/build/[id]/page.tsx +162 -0
- package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/BuildSummary.tsx +166 -0
- package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/DnsConfiguration.tsx +119 -0
- package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/DomainSettings.tsx +291 -0
- package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/Header.tsx +71 -0
- package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/Sidebar.tsx +270 -0
- package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/components/SkeletonLoader.tsx +87 -0
- package/apps/dashboard/src/app/(dashboard)/(deployment)/deploy/[slug]/page.tsx +169 -0
- package/apps/dashboard/src/app/(dashboard)/(deployment)/layout.tsx +7 -0
- package/apps/dashboard/src/app/(dashboard)/billing/page.tsx +1 -8
- package/apps/dashboard/src/app/(dashboard)/deployments/components/CommitDetailsModal.tsx +240 -0
- package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentCard.tsx +121 -0
- package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentHeader.tsx +89 -0
- package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentMenu.tsx +182 -0
- package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentsContent.tsx +324 -0
- package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentsFilters.tsx +99 -0
- package/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentsList.tsx +31 -0
- package/apps/dashboard/src/app/(dashboard)/deployments/components/EmptyState.tsx +101 -0
- package/apps/dashboard/src/app/(dashboard)/deployments/components/LoadingSkeleton.tsx +57 -0
- package/apps/dashboard/src/app/(dashboard)/deployments/components/ProjectFilter.tsx +109 -0
- package/apps/dashboard/src/app/(dashboard)/deployments/components/index.ts +9 -0
- package/apps/dashboard/src/app/(dashboard)/deployments/page.tsx +9 -3
- package/apps/dashboard/src/app/(dashboard)/deployments/types.ts +44 -0
- package/apps/dashboard/src/app/(dashboard)/deployments/utils.ts +176 -0
- package/apps/dashboard/src/app/(dashboard)/domains/page.tsx +1 -6
- package/apps/dashboard/src/app/(dashboard)/integrations/page.tsx +321 -0
- package/apps/dashboard/src/app/(dashboard)/layout.tsx +22 -32
- package/apps/dashboard/src/app/(dashboard)/library/components/ConnectPrompt.tsx +187 -0
- package/apps/dashboard/src/app/(dashboard)/library/components/LibrarySidebar.tsx +147 -0
- package/apps/dashboard/src/app/(dashboard)/library/components/LoadingSkeleton.tsx +39 -0
- package/apps/dashboard/src/app/(dashboard)/library/components/LocalProjects.tsx +548 -0
- package/apps/dashboard/src/app/(dashboard)/library/components/RepositoryList.tsx +292 -0
- package/apps/dashboard/src/app/(dashboard)/library/components/TemplateGrid.tsx +36 -0
- package/apps/dashboard/src/app/(dashboard)/library/components/UrlImport.tsx +74 -0
- package/apps/dashboard/src/app/(dashboard)/library/page.tsx +121 -0
- package/apps/dashboard/src/app/(dashboard)/library/types.ts +26 -0
- package/apps/dashboard/src/app/(dashboard)/monitoring/page.tsx +1 -6
- package/apps/dashboard/src/app/(dashboard)/page.tsx +485 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/[[...slug]]/layout.tsx +22 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/[[...slug]]/page.tsx +286 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/AdvancedSettings.tsx +235 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/BuildSettings.tsx +174 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/DeleteConfirmationDialog.tsx +45 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/DeletionModal.tsx +95 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/Deployments.tsx +11 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/DomainSettings.tsx +523 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/EnvironmentSettings.tsx +246 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/GeneralSettings.tsx +93 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/GitInfo.tsx +261 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/GitSettings.tsx +391 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/LogsSettings.tsx +133 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/ProjectNotFound.tsx +95 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/ProjectsBottomNavigation.tsx +62 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/ResourceSettings.tsx +333 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/SettingSection.tsx +66 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/SettingsNavigation.tsx +27 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/SleepModeSettings.tsx +134 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/ProductionUrl.tsx +25 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/ProjectIdentity.tsx +159 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/ProjectInfo.tsx +58 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/SkeletonLoader.tsx +170 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/StatsCards.tsx +33 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/TopPaths.tsx +56 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/TrafficChart.tsx +157 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/general/index.ts +14 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/logs/LogsActions.tsx +52 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/logs/ServerLogs.tsx +249 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/logs/TerminalLogs.tsx +682 -0
- package/apps/dashboard/src/app/(dashboard)/projects/[id]/components/logs/logs.css +127 -0
- package/apps/dashboard/src/app/(dashboard)/projects/components/DeploymentCard.tsx +73 -0
- package/apps/dashboard/src/app/(dashboard)/projects/components/ProjectCard.tsx +330 -0
- package/apps/dashboard/src/app/(dashboard)/projects/page.tsx +326 -4
- package/apps/dashboard/src/app/(dashboard)/providers.tsx +23 -0
- package/apps/dashboard/src/app/(dashboard)/servers/[serverId]/_components/components-tab.tsx +329 -0
- package/apps/dashboard/src/app/(dashboard)/servers/[serverId]/_components/overview-tab.tsx +229 -0
- package/apps/dashboard/src/app/(dashboard)/servers/[serverId]/_components/terminal-tab.tsx +38 -0
- package/apps/dashboard/src/app/(dashboard)/servers/[serverId]/page.tsx +437 -0
- package/apps/dashboard/src/app/(dashboard)/servers/new/_components/checking-state.tsx +15 -0
- package/apps/dashboard/src/app/(dashboard)/servers/new/_components/choose-mode.tsx +54 -0
- package/apps/dashboard/src/app/(dashboard)/servers/new/_components/component-row.tsx +65 -0
- package/apps/dashboard/src/app/(dashboard)/servers/new/_components/error-banner.tsx +13 -0
- package/apps/dashboard/src/app/(dashboard)/servers/new/_components/installing-panel.tsx +227 -0
- package/apps/dashboard/src/app/(dashboard)/servers/new/_components/results-panel.tsx +90 -0
- package/apps/dashboard/src/app/(dashboard)/servers/new/_components/setup-header.tsx +56 -0
- package/apps/dashboard/src/app/(dashboard)/servers/new/_components/types.ts +13 -0
- package/apps/dashboard/src/app/(dashboard)/servers/new/page.tsx +666 -0
- package/apps/dashboard/src/app/(dashboard)/servers/page.tsx +283 -0
- package/apps/dashboard/src/app/(dashboard)/settings/_components/BuildPreferences.tsx +110 -0
- package/apps/dashboard/src/app/(dashboard)/settings/_components/CloudConnection.tsx +147 -0
- package/apps/dashboard/src/app/(dashboard)/settings/_components/InstanceInfo.tsx +54 -0
- package/apps/dashboard/src/app/(dashboard)/settings/_components/ServerConnection.tsx +407 -0
- package/apps/dashboard/src/app/(dashboard)/settings/_components/SettingsSection.tsx +32 -0
- package/apps/dashboard/src/app/(dashboard)/settings/page.tsx +62 -3
- package/apps/dashboard/src/app/auth/callback/close/page.tsx +23 -0
- package/apps/dashboard/src/app/auth/callback/install/page.tsx +40 -0
- package/apps/dashboard/src/app/authorize/page.tsx +137 -0
- package/apps/dashboard/src/app/globals.css +57 -3
- package/apps/dashboard/src/app/layout.tsx +43 -3
- package/apps/dashboard/src/components/auth-shell.tsx +66 -0
- package/apps/dashboard/src/components/deployments/types.ts +44 -0
- package/apps/dashboard/src/components/global-deployments/index.ts +17 -0
- package/apps/dashboard/src/components/i18n-provider.tsx +100 -0
- package/apps/dashboard/src/components/import-project/BuildSettings.tsx +376 -0
- package/apps/dashboard/src/components/import-project/BuildSkeleton.tsx +161 -0
- package/apps/dashboard/src/components/import-project/BuildTerminal.tsx +358 -0
- package/apps/dashboard/src/components/import-project/ComposeServices.tsx +455 -0
- package/apps/dashboard/src/components/import-project/DeploymentProcessing.tsx +510 -0
- package/apps/dashboard/src/components/import-project/DockerSettings.tsx +64 -0
- package/apps/dashboard/src/components/import-project/EnvironmentVariables.tsx +481 -0
- package/apps/dashboard/src/components/import-project/Frameworks.tsx +90 -0
- package/apps/dashboard/src/components/import-project/ProjectSettings.tsx +160 -0
- package/apps/dashboard/src/components/import-project/types.ts +82 -0
- package/apps/dashboard/src/components/integrations/GithubPermissionModal.tsx +173 -0
- package/apps/dashboard/src/components/language-switcher.tsx +36 -0
- package/apps/dashboard/src/components/logo.tsx +13 -0
- package/apps/dashboard/src/components/network-error-handler.tsx +27 -0
- package/apps/dashboard/src/components/oauth-buttons.tsx +81 -0
- package/apps/dashboard/src/components/overview/ActivityChart.tsx +247 -0
- package/apps/dashboard/src/components/overview/ApiRequestsCard.tsx +111 -0
- package/apps/dashboard/src/components/overview/CreditsCard.tsx +137 -0
- package/apps/dashboard/src/components/overview/DeploymentsChart.tsx +195 -0
- package/apps/dashboard/src/components/overview/EmptyState.tsx +126 -0
- package/apps/dashboard/src/components/overview/LoadingSkeleton.tsx +92 -0
- package/apps/dashboard/src/components/overview/OverviewHeader.tsx +74 -0
- package/apps/dashboard/src/components/overview/OverviewHeroChart.tsx +344 -0
- package/apps/dashboard/src/components/overview/ProjectsCard.tsx +134 -0
- package/apps/dashboard/src/components/overview/QuickLinks.tsx +119 -0
- package/apps/dashboard/src/components/overview/SandboxesCard.tsx +150 -0
- package/apps/dashboard/src/components/overview/StatsGrid.tsx +166 -0
- package/apps/dashboard/src/components/overview/TokenUsageChart.tsx +206 -0
- package/apps/dashboard/src/components/overview/UsageStats.tsx +79 -0
- package/apps/dashboard/src/components/overview/index.ts +30 -0
- package/apps/dashboard/src/components/overview/types.ts +116 -0
- package/apps/dashboard/src/components/page-header.tsx +25 -0
- package/apps/dashboard/src/components/project-settings/ServerSideSwitch.jsx +60 -0
- package/apps/dashboard/src/components/shared/AlertBox.tsx +39 -0
- package/apps/dashboard/src/components/shared/CTASection.tsx +45 -0
- package/apps/dashboard/src/components/shared/ErrorState.tsx +176 -0
- package/apps/dashboard/src/components/shared/FeatureCard.tsx +20 -0
- package/apps/dashboard/src/components/shared/MDXComponents.tsx +24 -0
- package/apps/dashboard/src/components/shared/MachineSettingsModal.tsx +286 -0
- package/apps/dashboard/src/components/shared/Modal.tsx +39 -0
- package/apps/dashboard/src/components/shared/OTPInput.tsx +122 -0
- package/apps/dashboard/src/components/shared/PageHero.tsx +21 -0
- package/apps/dashboard/src/components/shared/PlatformFeatureCard.tsx +27 -0
- package/apps/dashboard/src/components/shared/StatCard.tsx +14 -0
- package/apps/dashboard/src/components/shared/ValueCard.tsx +20 -0
- package/apps/dashboard/src/components/shared/index.ts +8 -0
- package/apps/dashboard/src/components/sidebar.tsx +282 -0
- package/apps/dashboard/src/components/theme-provider.tsx +96 -0
- package/apps/dashboard/src/components/toast.tsx +106 -0
- package/apps/dashboard/src/components/ui/CustomCursor.tsx +107 -0
- package/apps/dashboard/src/components/ui/CustomSelect.tsx +126 -0
- package/apps/dashboard/src/components/ui/DropdownMenu.tsx +174 -0
- package/apps/dashboard/src/components/ui/FileIcon.jsx +59 -0
- package/apps/dashboard/src/components/ui/IconPickerModal.tsx +273 -0
- package/apps/dashboard/src/components/ui/InfoBanner.jsx +39 -0
- package/apps/dashboard/src/components/ui/Logo.tsx +14 -0
- package/apps/dashboard/src/components/ui/Modal.tsx +117 -0
- package/apps/dashboard/src/components/ui/PageContainer.tsx +12 -0
- package/apps/dashboard/src/components/ui/SectionContainer.jsx +7 -0
- package/apps/dashboard/src/components/ui/SlidingToggle.tsx +128 -0
- package/apps/dashboard/src/components/ui/Toast.tsx +29 -0
- package/apps/dashboard/src/components/ui/button.tsx +58 -0
- package/apps/dashboard/src/components/ui/card.tsx +60 -0
- package/apps/dashboard/src/components/ui/input.tsx +20 -0
- package/apps/dashboard/src/components/ui/label.tsx +19 -0
- package/apps/dashboard/src/components/ui/select.tsx +10 -0
- package/apps/dashboard/src/components/ui/textarea.tsx +9 -0
- package/apps/dashboard/src/constants/lang-colors.ts +25 -0
- package/apps/dashboard/src/constants/mock.ts +38 -0
- package/apps/dashboard/src/context/AuthContext.tsx +91 -0
- package/apps/dashboard/src/context/CloudContext.tsx +275 -0
- package/apps/dashboard/src/context/DeploymentContext.tsx +71 -0
- package/apps/dashboard/src/context/GitHubContext.tsx +288 -0
- package/apps/dashboard/src/context/ModalContext.tsx +300 -0
- package/apps/dashboard/src/context/NetworkContext.tsx +32 -0
- package/apps/dashboard/src/context/ProjectSettingsContext.tsx +727 -0
- package/apps/dashboard/src/context/ToastContext.tsx +129 -0
- package/apps/dashboard/src/context/deployment/index.ts +3 -0
- package/apps/dashboard/src/context/deployment/types.ts +183 -0
- package/apps/dashboard/src/context/deployment/useDeploymentBuild.ts +547 -0
- package/apps/dashboard/src/context/deployment/useDeploymentConfig.ts +176 -0
- package/apps/dashboard/src/hooks/useBuildConnection.ts +334 -0
- package/apps/dashboard/src/hooks/useMonitorStream.ts +146 -0
- package/apps/dashboard/src/hooks/useProjectData.ts +110 -0
- package/apps/dashboard/src/hooks/useSSEConnection.ts +465 -0
- package/apps/dashboard/src/hooks/useSSEStream.ts +309 -0
- package/apps/dashboard/src/hooks/useSessions.ts +104 -0
- package/apps/dashboard/src/hooks/useSetupStream.ts +223 -0
- package/apps/dashboard/src/i18n/dictionaries/ar.json +218 -0
- package/apps/dashboard/src/i18n/dictionaries/en.json +218 -0
- package/apps/dashboard/src/i18n/index.ts +16 -0
- package/apps/dashboard/src/lib/api/ai.ts +22 -0
- package/apps/dashboard/src/lib/api/auth.ts +13 -0
- package/apps/dashboard/src/lib/api/client.ts +199 -0
- package/apps/dashboard/src/lib/api/cloud.ts +15 -0
- package/apps/dashboard/src/lib/api/deploy.ts +58 -0
- package/apps/dashboard/src/lib/api/domains.ts +13 -0
- package/apps/dashboard/src/lib/api/endpoints.ts +129 -0
- package/apps/dashboard/src/lib/api/github.ts +31 -0
- package/apps/dashboard/src/lib/api/icons.ts +12 -0
- package/apps/dashboard/src/lib/api/index.ts +41 -0
- package/apps/dashboard/src/lib/api/projects.ts +153 -0
- package/apps/dashboard/src/lib/api/sandbox.ts +14 -0
- package/apps/dashboard/src/lib/api/settings.ts +21 -0
- package/apps/dashboard/src/lib/api/system.ts +159 -0
- package/apps/dashboard/src/lib/auth-client.ts +54 -0
- package/apps/dashboard/src/lib/country.ts +25 -0
- package/apps/dashboard/src/lib/server/api.ts +140 -0
- package/apps/dashboard/src/lib/server/session.ts +103 -0
- package/apps/dashboard/src/lib/sseClient.ts +241 -0
- package/apps/dashboard/src/lib/sseMessageProcessors.ts +323 -0
- package/apps/dashboard/src/lib/utils.ts +6 -0
- package/apps/dashboard/src/proxy.ts +46 -0
- package/apps/dashboard/src/styles/Toast.module.css +92 -0
- package/apps/dashboard/src/styles/fonts.css +93 -0
- package/apps/dashboard/src/styles/theme.css +336 -0
- package/apps/dashboard/src/types/research.ts +42 -0
- package/apps/dashboard/src/utils/SSEParser.js +150 -0
- package/apps/dashboard/src/utils/authWindow.ts +143 -0
- package/apps/dashboard/src/utils/codeTheme.ts +127 -0
- package/apps/dashboard/src/utils/colors.json +2810 -0
- package/apps/dashboard/src/utils/date.ts +121 -0
- package/apps/dashboard/src/utils/deployment.ts +128 -0
- package/apps/dashboard/src/utils/deploymentPhaseDetector.ts +232 -0
- package/apps/dashboard/src/utils/extToLang.js +103 -0
- package/apps/dashboard/src/utils/github.ts +62 -0
- package/apps/dashboard/src/utils/icons.js +46 -0
- package/apps/dashboard/src/utils/repoSlug.ts +110 -0
- package/apps/dashboard/src/utils/research-api.ts +163 -0
- package/apps/dashboard/src/utils/suggestions.ts +84 -0
- package/apps/dashboard/src/utils/theme.js +1 -0
- package/apps/dashboard/src/utils/ui-helpers.tsx +30 -0
- package/apps/dashboard/src/utils/upload.js +12 -0
- package/apps/dashboard/tsconfig.json +27 -5
- package/apps/desktop/forge.config.json +36 -0
- package/apps/desktop/package.json +26 -0
- package/apps/desktop/scripts/dev.mjs +58 -0
- package/apps/desktop/src/main/index.ts +675 -0
- package/apps/desktop/src/main/types/desktop.d.ts +33 -0
- package/apps/desktop/src/preload/index.ts +88 -0
- package/apps/desktop/src/renderer/index.html +462 -0
- package/apps/desktop/src/renderer/onboarding.js +511 -0
- package/apps/desktop/src/renderer/styles.css +1139 -0
- package/apps/desktop/tsconfig.json +17 -0
- package/apps/web/.source/browser.ts +13 -0
- package/apps/web/.source/dynamic.ts +8 -0
- package/apps/web/.source/server.ts +22 -0
- package/apps/web/.source/source.config.mjs +20 -0
- package/apps/web/Dockerfile +4 -6
- package/apps/web/content/blog/how-ai-builds-work.mdx +55 -0
- package/apps/web/content/blog/introducing-openship.mdx +46 -0
- package/apps/web/content/blog/self-hosting-cost-breakdown.mdx +59 -0
- package/apps/web/content/docs/_meta.json +14 -0
- package/apps/web/content/docs/api.mdx +98 -0
- package/apps/web/content/docs/cli.mdx +80 -0
- package/apps/web/content/docs/first-deployment.mdx +56 -0
- package/apps/web/content/docs/index.mdx +27 -0
- package/apps/web/content/docs/installation.mdx +65 -0
- package/apps/web/content/docs/quickstart.mdx +45 -0
- package/apps/web/next-env.d.ts +6 -0
- package/apps/web/next.config.mjs +16 -2
- package/apps/web/package.json +19 -12
- package/apps/web/postcss.config.mjs +5 -0
- package/apps/web/public/android-chrome-192x192.png +0 -0
- package/apps/web/public/android-chrome-512x512.png +0 -0
- package/apps/web/public/apple-touch-icon.png +0 -0
- package/apps/web/public/favicon-16x16.png +0 -0
- package/apps/web/public/favicon-32x32.png +0 -0
- package/apps/web/public/favicon.ico +0 -0
- package/apps/web/public/site.webmanifest +1 -0
- package/apps/web/source.config.ts +17 -0
- package/apps/web/src/app/(marketing)/layout.tsx +7 -22
- package/apps/web/src/app/blog/[...slug]/page.tsx +247 -0
- package/apps/web/src/app/blog/blog.css +575 -0
- package/apps/web/src/app/blog/layout.tsx +50 -0
- package/apps/web/src/app/blog/page.tsx +235 -0
- package/apps/web/src/app/docs/[[...slug]]/page.tsx +67 -0
- package/apps/web/src/app/docs/layout.tsx +62 -0
- package/apps/web/src/app/docs/toc-theme.css +71 -0
- package/apps/web/src/app/download/page.tsx +396 -0
- package/apps/web/src/app/globals.css +20 -3
- package/apps/web/src/app/layout.tsx +57 -3
- package/apps/web/src/app/page.tsx +47 -6
- package/apps/web/src/app/robots.ts +15 -0
- package/apps/web/src/app/sitemap.ts +35 -0
- package/apps/web/src/components/blog/share-sidebar.tsx +81 -0
- package/apps/web/src/components/blog/writer-sidebar.tsx +81 -0
- package/apps/web/src/components/landing/ai-native.tsx +169 -0
- package/apps/web/src/components/landing/backups.tsx +139 -0
- package/apps/web/src/components/landing/cdn.tsx +109 -0
- package/apps/web/src/components/landing/coming-soon.tsx +180 -0
- package/apps/web/src/components/landing/dark-section.tsx +166 -0
- package/apps/web/src/components/landing/dashboard.tsx +407 -0
- package/apps/web/src/components/landing/developer-experience.tsx +224 -0
- package/apps/web/src/components/landing/dns-domains.tsx +146 -0
- package/apps/web/src/components/landing/download-button.tsx +151 -0
- package/apps/web/src/components/landing/features/ai-visual.tsx +48 -0
- package/apps/web/src/components/landing/features/cloud-visual.tsx +44 -0
- package/apps/web/src/components/landing/features/git-visual.tsx +61 -0
- package/apps/web/src/components/landing/features/index.tsx +642 -0
- package/apps/web/src/components/landing/features/infra-visual.tsx +69 -0
- package/apps/web/src/components/landing/features/rollback-visual.tsx +42 -0
- package/apps/web/src/components/landing/features/ssl-visual.tsx +41 -0
- package/apps/web/src/components/landing/features/stacks-visual.tsx +54 -0
- package/apps/web/src/components/landing/final-cta.tsx +116 -0
- package/apps/web/src/components/landing/footer.tsx +145 -0
- package/apps/web/src/components/landing/hero.tsx +110 -0
- package/apps/web/src/components/landing/how-it-works.tsx +210 -0
- package/apps/web/src/components/landing/index.ts +21 -0
- package/apps/web/src/components/landing/mail-server.tsx +122 -0
- package/apps/web/src/components/landing/navbar.tsx +199 -0
- package/apps/web/src/components/landing/no-lockin.tsx +185 -0
- package/apps/web/src/components/landing/open-source-cta.tsx +151 -0
- package/apps/web/src/components/landing/portability.tsx +190 -0
- package/apps/web/src/components/landing/pricing.tsx +263 -0
- package/apps/web/src/components/landing/scaling.tsx +209 -0
- package/apps/web/src/components/landing/why-openship.tsx +291 -0
- package/apps/web/src/hooks/use-gsap-scroll.ts +158 -0
- package/apps/web/src/hooks/use-platform.ts +86 -0
- package/apps/web/src/lib/source.ts +12 -0
- package/apps/web/src/styles/fonts.css +84 -0
- package/apps/web/src/styles/landing.css +951 -0
- package/apps/web/src/styles/theme.css +227 -0
- package/apps/web/tsconfig.json +8 -3
- package/bun.lock +3260 -0
- package/docker-compose.yml +3 -0
- package/docs/screenshots/.gitkeep +0 -0
- package/docs/screenshots/screen.png +0 -0
- package/openship.code-workspace +7 -0
- package/package.json +23 -9
- package/packages/adapters/docs/ARCHITECTURE.md +160 -0
- package/packages/adapters/docs/BUILD-PIPELINE.md +171 -0
- package/packages/adapters/docs/CLOUD.md +239 -0
- package/packages/adapters/docs/EXECUTOR.md +174 -0
- package/packages/adapters/docs/LOG-STREAMING.md +175 -0
- package/packages/adapters/docs/SYSTEM.md +191 -0
- package/packages/adapters/package.json +14 -9
- package/packages/adapters/src/index.ts +144 -4
- package/packages/adapters/src/infra/cloud.ts +42 -0
- package/packages/adapters/src/infra/index.ts +9 -0
- package/packages/adapters/src/infra/noop.ts +27 -0
- package/packages/adapters/src/infra/traefik.ts +297 -0
- package/packages/adapters/src/infra/types.ts +36 -0
- package/packages/adapters/src/platform.ts +255 -0
- package/packages/adapters/src/runtime/bare.ts +567 -0
- package/packages/adapters/src/runtime/build-pipeline.ts +237 -0
- package/packages/adapters/src/runtime/cloud.ts +791 -0
- package/packages/adapters/src/runtime/deploy-pipeline.ts +146 -0
- package/packages/adapters/src/runtime/docker.ts +443 -0
- package/packages/adapters/src/runtime/index.ts +69 -0
- package/packages/adapters/src/runtime/local-build.ts +112 -0
- package/packages/adapters/src/runtime/transfer.ts +82 -0
- package/packages/adapters/src/runtime/types.ts +138 -0
- package/packages/adapters/src/system/catalog.ts +188 -0
- package/packages/adapters/src/system/checks.ts +228 -0
- package/packages/adapters/src/system/components.ts +45 -0
- package/packages/adapters/src/system/debug.ts +18 -0
- package/packages/adapters/src/system/environment.ts +168 -0
- package/packages/adapters/src/system/errors.ts +34 -0
- package/packages/adapters/src/system/executor.ts +600 -0
- package/packages/adapters/src/system/index.ts +58 -0
- package/packages/adapters/src/system/installer.ts +310 -0
- package/packages/adapters/src/system/setup.ts +442 -0
- package/packages/adapters/src/system/state.ts +96 -0
- package/packages/adapters/src/system/types.ts +116 -0
- package/packages/adapters/src/toolchain/catalog.ts +439 -0
- package/packages/adapters/src/toolchain/checks.ts +181 -0
- package/packages/adapters/src/toolchain/index.ts +38 -0
- package/packages/adapters/src/toolchain/installer.ts +189 -0
- package/packages/adapters/src/toolchain/types.ts +84 -0
- package/packages/adapters/src/types.ts +277 -0
- package/packages/adapters/tsconfig.json +3 -1
- package/packages/core/package.json +9 -9
- package/packages/core/src/constants.ts +44 -1
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/stacks.ts +778 -0
- package/packages/core/src/system.ts +111 -0
- package/packages/core/src/types.ts +22 -1
- package/packages/core/src/utils.ts +8 -0
- package/packages/core/tsconfig.json +3 -1
- package/packages/db/drizzle/0000_init.sql +231 -0
- package/packages/db/drizzle/0001_add_server_name.sql +1 -0
- package/packages/db/drizzle/meta/_journal.json +20 -0
- package/packages/db/drizzle.config.ts +10 -0
- package/packages/db/package.json +26 -14
- package/packages/db/src/client.ts +119 -0
- package/packages/db/src/index.ts +57 -8
- package/packages/db/src/repos/account.repo.ts +92 -0
- package/packages/db/src/repos/deployment.repo.ts +168 -0
- package/packages/db/src/repos/domain.repo.ts +102 -0
- package/packages/db/src/repos/git-installation.repo.ts +95 -0
- package/packages/db/src/repos/index.ts +43 -0
- package/packages/db/src/repos/instance-settings.repo.ts +43 -0
- package/packages/db/src/repos/project.repo.ts +159 -0
- package/packages/db/src/repos/session.repo.ts +82 -0
- package/packages/db/src/repos/settings.repo.ts +51 -0
- package/packages/db/src/repos/user.repo.ts +94 -0
- package/packages/db/src/schema/auth.ts +56 -0
- package/packages/db/src/schema/deployment.ts +87 -0
- package/packages/db/src/schema/domain.ts +46 -0
- package/packages/db/src/schema/github.ts +24 -0
- package/packages/db/src/schema/index.ts +6 -0
- package/packages/db/src/schema/project.ts +117 -0
- package/packages/db/src/schema/settings.ts +103 -0
- package/packages/db/tsconfig.json +3 -1
- package/packages/ui/package.json +17 -17
- package/packages/ui/src/components/status-dot.tsx +1 -1
- package/packages/ui/tsconfig.json +7 -2
- package/pnpm-workspace.yaml +0 -1
- package/{tooling/tsconfig/base.json → tsconfig.base.json} +0 -1
- package/turbo.json +4 -0
- package/apps/api/src/modules/auth/auth.controller.ts +0 -26
- package/apps/api/src/modules/auth/auth.schema.ts +0 -12
- package/apps/api/src/modules/auth/auth.service.ts +0 -23
- package/apps/dashboard/src/app/page.tsx +0 -6
- package/apps/web/src/app/(marketing)/docs/page.tsx +0 -10
- package/packages/adapters/src/base-adapter.ts +0 -49
- package/packages/adapters/src/docker-adapter.ts +0 -43
- package/packages/adapters/src/oblien-adapter.ts +0 -46
- package/packages/adapters/src/registry.ts +0 -22
- package/packages/db/prisma/schema.prisma +0 -160
- package/tooling/tsconfig/nextjs.json +0 -10
- package/tooling/tsconfig/node.json +0 -10
- package/tooling/tsconfig/package.json +0 -7
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { getUserId } from "../../lib/controller-helpers";
|
|
3
|
+
import { repos } from "@repo/db";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { getBuildMode, type BuildMode } from "./settings.service";
|
|
6
|
+
|
|
7
|
+
const VALID_MODES: BuildMode[] = ["auto", "server", "local"];
|
|
8
|
+
|
|
9
|
+
function generateId() {
|
|
10
|
+
return "us_" + randomBytes(12).toString("base64url");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** GET / — return platform settings for the authenticated user */
|
|
14
|
+
export async function get(c: Context) {
|
|
15
|
+
const userId = getUserId(c);
|
|
16
|
+
return c.json({ buildMode: await getBuildMode(userId) });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** PUT / — create or update platform settings */
|
|
20
|
+
export async function upsert(c: Context) {
|
|
21
|
+
const userId = getUserId(c);
|
|
22
|
+
const body = await c.req.json();
|
|
23
|
+
|
|
24
|
+
const buildMode = body.buildMode || "auto";
|
|
25
|
+
if (!VALID_MODES.includes(buildMode)) {
|
|
26
|
+
return c.json({ error: "buildMode must be 'auto', 'server', or 'local'" }, 400);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const row = await repos.settings.upsert({
|
|
30
|
+
id: generateId(),
|
|
31
|
+
userId,
|
|
32
|
+
buildMode,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return c.json({ buildMode: row.buildMode });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** PATCH /build-mode — update just the build mode preference */
|
|
39
|
+
export async function updateBuildMode(c: Context) {
|
|
40
|
+
const userId = getUserId(c);
|
|
41
|
+
const { buildMode } = await c.req.json();
|
|
42
|
+
|
|
43
|
+
if (!VALID_MODES.includes(buildMode)) {
|
|
44
|
+
return c.json({ error: "buildMode must be 'auto', 'server', or 'local'" }, 400);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const existing = await repos.settings.findByUser(userId);
|
|
48
|
+
if (!existing) {
|
|
49
|
+
await repos.settings.upsert({ id: generateId(), userId, buildMode });
|
|
50
|
+
} else {
|
|
51
|
+
await repos.settings.update(userId, { buildMode });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return c.json({ buildMode });
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings routes — mounted at /api/settings in app.ts.
|
|
3
|
+
*
|
|
4
|
+
* All routes require authentication. Manages user platform preferences
|
|
5
|
+
* (build mode, etc.) that sync across devices and to Openship Cloud.
|
|
6
|
+
*
|
|
7
|
+
* System-level settings (SSH creds, server connection) are stored locally
|
|
8
|
+
* in Electron's ConfigStore — they never touch this API.
|
|
9
|
+
*/
|
|
10
|
+
import { Hono } from "hono";
|
|
11
|
+
import { authMiddleware } from "../../middleware/auth";
|
|
12
|
+
import * as ctrl from "./settings.controller";
|
|
13
|
+
|
|
14
|
+
export const settingsRoutes = new Hono();
|
|
15
|
+
|
|
16
|
+
settingsRoutes.use("*", authMiddleware);
|
|
17
|
+
|
|
18
|
+
/** GET / — get current user's workspace settings */
|
|
19
|
+
settingsRoutes.get("/", ctrl.get);
|
|
20
|
+
|
|
21
|
+
/** PUT / — create or update workspace settings */
|
|
22
|
+
settingsRoutes.put("/", ctrl.upsert);
|
|
23
|
+
|
|
24
|
+
/** PATCH /build-mode — update only build mode preference */
|
|
25
|
+
settingsRoutes.patch("/build-mode", ctrl.updateBuildMode);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings service — business logic for user platform preferences.
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - settings.controller.ts (HTTP layer)
|
|
6
|
+
* - build.service.ts (build strategy resolution)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { repos } from "@repo/db";
|
|
10
|
+
import { STACKS, type StackId, type StackDefinition } from "@repo/core";
|
|
11
|
+
import type { BuildStrategy } from "@repo/adapters";
|
|
12
|
+
|
|
13
|
+
export type BuildMode = "auto" | "server" | "local";
|
|
14
|
+
|
|
15
|
+
/** Get the user's build mode preference (defaults to "auto" if no row exists) */
|
|
16
|
+
export async function getBuildMode(userId: string): Promise<BuildMode> {
|
|
17
|
+
const settings = await repos.settings.findByUser(userId);
|
|
18
|
+
return (settings?.buildMode as BuildMode) ?? "auto";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the effective build strategy for a deployment.
|
|
23
|
+
*
|
|
24
|
+
* The per-deploy value sent by the UI is the source of truth.
|
|
25
|
+
* The global user preference is only used as an initial default
|
|
26
|
+
* in the dashboard when preparing a new deploy — it should NOT
|
|
27
|
+
* override an explicit per-deploy choice here.
|
|
28
|
+
*
|
|
29
|
+
* Priority chain:
|
|
30
|
+
* 1. Explicit per-deploy value (always sent by the dashboard)
|
|
31
|
+
* 2. Stack default (STACKS[framework].defaultBuildStrategy)
|
|
32
|
+
* 3. Fallback: "server"
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveStrategy(
|
|
35
|
+
_userId: string,
|
|
36
|
+
framework: string | undefined,
|
|
37
|
+
explicit?: BuildStrategy,
|
|
38
|
+
): Promise<BuildStrategy> {
|
|
39
|
+
// 1. Per-deploy explicit value (source of truth)
|
|
40
|
+
if (explicit) return explicit;
|
|
41
|
+
|
|
42
|
+
// 2. Stack default → 3. Fallback
|
|
43
|
+
const stackId = framework as StackId;
|
|
44
|
+
const stackDef: StackDefinition | undefined =
|
|
45
|
+
stackId && stackId in STACKS
|
|
46
|
+
? (STACKS[stackId] as StackDefinition)
|
|
47
|
+
: undefined;
|
|
48
|
+
return stackDef?.defaultBuildStrategy ?? "server";
|
|
49
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem controller — browse local directories.
|
|
3
|
+
*
|
|
4
|
+
* Self-hosted only. Provides directory listing with project detection
|
|
5
|
+
* so the UI can let users pick a folder to deploy.
|
|
6
|
+
*
|
|
7
|
+
* Security: This module is only loaded via dynamic import in self-hosted
|
|
8
|
+
* mode. The handler also checks CLOUD_MODE as defense-in-depth.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Context } from "hono";
|
|
12
|
+
import { readdir, stat } from "node:fs/promises";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
15
|
+
import { MANIFEST_FILES } from "../../lib/stack-detector";
|
|
16
|
+
import { env } from "../../config";
|
|
17
|
+
|
|
18
|
+
/** package.json + compose files aren't in MANIFEST_FILES (handled separately in stack detector) */
|
|
19
|
+
const PROJECT_MARKERS = new Set([
|
|
20
|
+
"package.json",
|
|
21
|
+
"docker-compose.yml",
|
|
22
|
+
"docker-compose.yaml",
|
|
23
|
+
...MANIFEST_FILES,
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** GET /system/browse?path=/some/dir — list child directories */
|
|
27
|
+
export async function browse(c: Context) {
|
|
28
|
+
if (env.CLOUD_MODE) return c.notFound();
|
|
29
|
+
|
|
30
|
+
const raw = c.req.query("path") || homedir();
|
|
31
|
+
const dirPath = resolve(raw);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const st = await stat(dirPath);
|
|
35
|
+
if (!st.isDirectory()) return c.json({ error: "Not a directory" }, 400);
|
|
36
|
+
} catch {
|
|
37
|
+
return c.json({ error: "Directory not found" }, 404);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
41
|
+
const dirs: { name: string; path: string; isProject: boolean }[] = [];
|
|
42
|
+
|
|
43
|
+
await Promise.all(
|
|
44
|
+
entries
|
|
45
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
46
|
+
.map(async (e) => {
|
|
47
|
+
const childPath = join(dirPath, e.name);
|
|
48
|
+
let isProject = false;
|
|
49
|
+
try {
|
|
50
|
+
const children = await readdir(childPath);
|
|
51
|
+
isProject = children.some((m) => PROJECT_MARKERS.has(m));
|
|
52
|
+
} catch {
|
|
53
|
+
/* unreadable dir — skip marker check */
|
|
54
|
+
}
|
|
55
|
+
dirs.push({ name: e.name, path: childPath, isProject });
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Projects first, then alphabetical
|
|
60
|
+
dirs.sort((a, b) => {
|
|
61
|
+
if (a.isProject !== b.isProject) return a.isProject ? -1 : 1;
|
|
62
|
+
return a.name.localeCompare(b.name);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return c.json({ path: dirPath, directories: dirs });
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { systemRoutes } from "./system.routes";
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server check & install controller — runs system health checks and
|
|
3
|
+
* component installation against the configured remote server.
|
|
4
|
+
*
|
|
5
|
+
* Uses the shared SSH connection manager so all server interactions go
|
|
6
|
+
* through one cached executor handler with idle TTL + optional
|
|
7
|
+
* persistent mode.
|
|
8
|
+
*
|
|
9
|
+
* Security:
|
|
10
|
+
* - Gated behind localOnly + authMiddleware (no cloud, no unauthenticated)
|
|
11
|
+
* - SSH credentials are read from DB, never from request body
|
|
12
|
+
* - Component names are validated against a known allowlist
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Context } from "hono";
|
|
16
|
+
import { streamSSE } from "hono/streaming";
|
|
17
|
+
import { env } from "../../config";
|
|
18
|
+
import {
|
|
19
|
+
checkAllComponents,
|
|
20
|
+
checkComponents,
|
|
21
|
+
COMPONENT_INSTALLERS,
|
|
22
|
+
isSshAuthError,
|
|
23
|
+
SYSTEM_COMPONENTS,
|
|
24
|
+
getSystemComponentDefinition,
|
|
25
|
+
} from "@repo/adapters";
|
|
26
|
+
import { formatDuration, systemDebug } from "@/lib/system-debug";
|
|
27
|
+
import { sshManager } from "../../lib/ssh-manager";
|
|
28
|
+
import {
|
|
29
|
+
createSetupSession,
|
|
30
|
+
getSetupSession,
|
|
31
|
+
getActiveSetupSession,
|
|
32
|
+
updateComponentProgress,
|
|
33
|
+
appendSetupLog,
|
|
34
|
+
finishSetupSession,
|
|
35
|
+
subscribeSetupSession,
|
|
36
|
+
} from "./setup-session";
|
|
37
|
+
|
|
38
|
+
function debugSystemRequest(message: string): void {
|
|
39
|
+
systemDebug("system-check", message);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Allowlisted components ──────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const ALLOWED_COMPONENTS = new Set(
|
|
45
|
+
SYSTEM_COMPONENTS.filter((component) => component.installable).map(
|
|
46
|
+
(component) => component.name,
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// ─── Handlers ────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* POST /system/check
|
|
54
|
+
*
|
|
55
|
+
* Run system health checks against the configured remote server.
|
|
56
|
+
* Optionally accepts { components: ["docker", "git"] } to check a subset.
|
|
57
|
+
*
|
|
58
|
+
* Returns: { components: ComponentStatus[], ready: boolean, missing: string[] }
|
|
59
|
+
*/
|
|
60
|
+
export async function checkServer(c: Context) {
|
|
61
|
+
if (env.CLOUD_MODE) return c.json({ error: "Not available" }, 404);
|
|
62
|
+
|
|
63
|
+
const startedAt = Date.now();
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const body = await c.req.json().catch(() => ({}));
|
|
67
|
+
const requestedComponents = body.components as string[] | undefined;
|
|
68
|
+
debugSystemRequest(
|
|
69
|
+
`check:start ${requestedComponents?.length ? requestedComponents.join(",") : "all"}`,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
let components;
|
|
73
|
+
if (requestedComponents?.length) {
|
|
74
|
+
// Validate against allowlist
|
|
75
|
+
const valid = requestedComponents.filter((n) => ALLOWED_COMPONENTS.has(n));
|
|
76
|
+
if (valid.length === 0) {
|
|
77
|
+
return c.json({ error: "Invalid component names" }, 400);
|
|
78
|
+
}
|
|
79
|
+
components = await sshManager.withExecutor((executor) =>
|
|
80
|
+
checkComponents(executor, valid),
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
components = await sshManager.withExecutor((executor) =>
|
|
84
|
+
checkAllComponents(executor),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const missing = components
|
|
89
|
+
.filter((c) => !c.healthy)
|
|
90
|
+
.map((c) => c.name);
|
|
91
|
+
|
|
92
|
+
debugSystemRequest(
|
|
93
|
+
`check:done ready=${missing.length === 0} missing=${missing.join(",") || "none"} (${formatDuration(startedAt)})`,
|
|
94
|
+
);
|
|
95
|
+
return c.json({
|
|
96
|
+
components,
|
|
97
|
+
ready: missing.length === 0,
|
|
98
|
+
missing,
|
|
99
|
+
});
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const message =
|
|
102
|
+
err instanceof Error ? err.message : "Failed to connect to server";
|
|
103
|
+
debugSystemRequest(`check:failed ${message} (${formatDuration(startedAt)})`);
|
|
104
|
+
if (
|
|
105
|
+
message === "No server configured" ||
|
|
106
|
+
message === "Invalid SSH auth configuration"
|
|
107
|
+
) {
|
|
108
|
+
return c.json({ error: "no_server", message }, 400);
|
|
109
|
+
}
|
|
110
|
+
if (isSshAuthError(err)) {
|
|
111
|
+
return c.json({ error: "auth_failed", message }, 400);
|
|
112
|
+
}
|
|
113
|
+
return c.json({ error: "connection_failed", message }, 502);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* POST /system/install
|
|
119
|
+
*
|
|
120
|
+
* Install a specific component on the configured remote server.
|
|
121
|
+
* Body: { component: "docker" | "traefik" | "git" | "node" | "bun", config?: InstallerConfig }
|
|
122
|
+
*
|
|
123
|
+
* Returns: { success: boolean, component: string, version?: string, error?: string }
|
|
124
|
+
*/
|
|
125
|
+
export async function installComponent(c: Context) {
|
|
126
|
+
if (env.CLOUD_MODE) return c.json({ error: "Not available" }, 404);
|
|
127
|
+
|
|
128
|
+
const body = await c.req.json().catch(() => ({}));
|
|
129
|
+
const componentName = body.component as string;
|
|
130
|
+
|
|
131
|
+
if (!componentName || !ALLOWED_COMPONENTS.has(componentName)) {
|
|
132
|
+
return c.json({ error: "Invalid or missing component name" }, 400);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const installerFn =
|
|
136
|
+
COMPONENT_INSTALLERS[componentName as keyof typeof COMPONENT_INSTALLERS];
|
|
137
|
+
if (!installerFn) {
|
|
138
|
+
return c.json({ error: `No installer for ${componentName}` }, 400);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const logs: string[] = [];
|
|
143
|
+
const installResult = await sshManager.withExecutor((executor) =>
|
|
144
|
+
installerFn(
|
|
145
|
+
executor,
|
|
146
|
+
(log) => logs.push(log.message),
|
|
147
|
+
body.config ?? {},
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return c.json({
|
|
152
|
+
...installResult,
|
|
153
|
+
logs,
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
const message =
|
|
157
|
+
err instanceof Error ? err.message : "Installation failed";
|
|
158
|
+
if (
|
|
159
|
+
message === "No server configured" ||
|
|
160
|
+
message === "Invalid SSH auth configuration"
|
|
161
|
+
) {
|
|
162
|
+
return c.json({ error: "no_server", message }, 400);
|
|
163
|
+
}
|
|
164
|
+
if (isSshAuthError(err)) {
|
|
165
|
+
return c.json({ error: "auth_failed", message }, 400);
|
|
166
|
+
}
|
|
167
|
+
return c.json({ error: "install_failed", message }, 502);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* POST /system/install/stream
|
|
173
|
+
*
|
|
174
|
+
* Install multiple components with real-time SSE log streaming.
|
|
175
|
+
* Body: { components: ["docker", "traefik", ...], config?: InstallerConfig }
|
|
176
|
+
*
|
|
177
|
+
* Returns an SSE stream with events:
|
|
178
|
+
* - progress: component status updates
|
|
179
|
+
* - log: real-time log lines from installers
|
|
180
|
+
* - complete: final result when all installs finish
|
|
181
|
+
* - end: stream terminated
|
|
182
|
+
*/
|
|
183
|
+
export async function installStream(c: Context) {
|
|
184
|
+
if (env.CLOUD_MODE) return c.json({ error: "Not available" }, 404);
|
|
185
|
+
|
|
186
|
+
const body = await c.req.json().catch(() => ({}));
|
|
187
|
+
const requestedComponents = body.components as string[] | undefined;
|
|
188
|
+
const config = body.config ?? {};
|
|
189
|
+
|
|
190
|
+
if (!requestedComponents?.length) {
|
|
191
|
+
return c.json({ error: "No components specified" }, 400);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Validate all component names
|
|
195
|
+
const validNames = requestedComponents.filter((n) => ALLOWED_COMPONENTS.has(n));
|
|
196
|
+
if (validNames.length === 0) {
|
|
197
|
+
return c.json({ error: "Invalid component names" }, 400);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check for already running session
|
|
201
|
+
const existing = getActiveSetupSession();
|
|
202
|
+
if (existing) {
|
|
203
|
+
return c.json({ error: "install_in_progress", sessionId: existing.id }, 409);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Create session
|
|
207
|
+
const componentMeta = validNames.map((name) => {
|
|
208
|
+
const def = getSystemComponentDefinition(name);
|
|
209
|
+
return { name, label: def.label };
|
|
210
|
+
});
|
|
211
|
+
const session = createSetupSession(componentMeta);
|
|
212
|
+
|
|
213
|
+
return streamSSE(c, async (sseStream) => {
|
|
214
|
+
let closed = false;
|
|
215
|
+
|
|
216
|
+
const writer = (event: string, data: string): boolean => {
|
|
217
|
+
if (closed) return false;
|
|
218
|
+
try {
|
|
219
|
+
void sseStream.writeSSE({ event, data });
|
|
220
|
+
return true;
|
|
221
|
+
} catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Subscribe this connection as the first listener
|
|
227
|
+
const { unsubscribe } = subscribeSetupSession(session.id, writer);
|
|
228
|
+
|
|
229
|
+
// Run installs in background — don't await inline,
|
|
230
|
+
// the SSE stream stays open via the promise below
|
|
231
|
+
const installPromise = (async () => {
|
|
232
|
+
let hasFailure = false;
|
|
233
|
+
|
|
234
|
+
for (const name of validNames) {
|
|
235
|
+
if (closed) break;
|
|
236
|
+
|
|
237
|
+
const installerFn = COMPONENT_INSTALLERS[name as keyof typeof COMPONENT_INSTALLERS];
|
|
238
|
+
if (!installerFn) {
|
|
239
|
+
updateComponentProgress(session.id, name, "failed", `No installer for ${name}`);
|
|
240
|
+
hasFailure = true;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
updateComponentProgress(session.id, name, "installing");
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const result = await sshManager.withExecutor((executor) =>
|
|
248
|
+
installerFn(
|
|
249
|
+
executor,
|
|
250
|
+
(log) => appendSetupLog(session.id, name, log.message, log.level),
|
|
251
|
+
config,
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (result.success) {
|
|
256
|
+
appendSetupLog(session.id, name, `${name} installed successfully${result.version ? ` (${result.version})` : ""}`);
|
|
257
|
+
updateComponentProgress(session.id, name, "installed");
|
|
258
|
+
} else {
|
|
259
|
+
appendSetupLog(session.id, name, result.error ?? `${name} installation failed`, "error");
|
|
260
|
+
updateComponentProgress(session.id, name, "failed", result.error);
|
|
261
|
+
hasFailure = true;
|
|
262
|
+
}
|
|
263
|
+
} catch (err) {
|
|
264
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
265
|
+
appendSetupLog(session.id, name, msg, "error");
|
|
266
|
+
updateComponentProgress(session.id, name, "failed", msg);
|
|
267
|
+
hasFailure = true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
finishSetupSession(session.id, hasFailure ? "failed" : "completed");
|
|
272
|
+
})();
|
|
273
|
+
|
|
274
|
+
// Keep the SSE connection open until install finishes or client disconnects
|
|
275
|
+
await new Promise<void>((resolve) => {
|
|
276
|
+
installPromise.then(() => {
|
|
277
|
+
// Give a brief delay for final events to flush
|
|
278
|
+
setTimeout(() => {
|
|
279
|
+
closed = true;
|
|
280
|
+
resolve();
|
|
281
|
+
}, 500);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
sseStream.onAbort(() => {
|
|
285
|
+
closed = true;
|
|
286
|
+
unsubscribe();
|
|
287
|
+
resolve();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* GET /system/install/session
|
|
295
|
+
*
|
|
296
|
+
* Get the active setup session or a specific session by ID.
|
|
297
|
+
* Query: ?id=setup_xxx (optional — returns active session if omitted)
|
|
298
|
+
*
|
|
299
|
+
* Returns: session state or 404
|
|
300
|
+
*/
|
|
301
|
+
export async function getInstallSession(c: Context) {
|
|
302
|
+
if (env.CLOUD_MODE) return c.json({ error: "Not available" }, 404);
|
|
303
|
+
|
|
304
|
+
const sessionId = c.req.query("id");
|
|
305
|
+
|
|
306
|
+
const session = sessionId
|
|
307
|
+
? getSetupSession(sessionId)
|
|
308
|
+
: getActiveSetupSession();
|
|
309
|
+
|
|
310
|
+
if (!session) {
|
|
311
|
+
return c.json({ active: false }, 200);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return c.json({
|
|
315
|
+
active: true,
|
|
316
|
+
sessionId: session.id,
|
|
317
|
+
status: session.status,
|
|
318
|
+
components: session.components,
|
|
319
|
+
startedAt: session.startedAt,
|
|
320
|
+
finishedAt: session.finishedAt,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* GET /system/install/stream
|
|
326
|
+
*
|
|
327
|
+
* Attach to an existing setup session's SSE stream (for page reloads).
|
|
328
|
+
* Query: ?id=setup_xxx
|
|
329
|
+
*/
|
|
330
|
+
export async function attachInstallStream(c: Context) {
|
|
331
|
+
if (env.CLOUD_MODE) return c.json({ error: "Not available" }, 404);
|
|
332
|
+
|
|
333
|
+
const sessionId = c.req.query("id");
|
|
334
|
+
const session = sessionId
|
|
335
|
+
? getSetupSession(sessionId)
|
|
336
|
+
: getActiveSetupSession();
|
|
337
|
+
|
|
338
|
+
if (!session) {
|
|
339
|
+
return c.json({ error: "No active session" }, 404);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return streamSSE(c, async (sseStream) => {
|
|
343
|
+
let closed = false;
|
|
344
|
+
|
|
345
|
+
const writer = (event: string, data: string): boolean => {
|
|
346
|
+
if (closed) return false;
|
|
347
|
+
try {
|
|
348
|
+
void sseStream.writeSSE({ event, data });
|
|
349
|
+
return true;
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const { success, unsubscribe } = subscribeSetupSession(session.id, writer);
|
|
356
|
+
|
|
357
|
+
if (!success) {
|
|
358
|
+
await sseStream.writeSSE({ event: "error", data: JSON.stringify({ error: "Session not found" }) });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// If session is already done, subscribe will have replayed + sent end; just close
|
|
363
|
+
if (session.status !== "running") {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
await new Promise<void>((resolve) => {
|
|
368
|
+
const checkInterval = setInterval(() => {
|
|
369
|
+
if (closed) {
|
|
370
|
+
clearInterval(checkInterval);
|
|
371
|
+
resolve();
|
|
372
|
+
}
|
|
373
|
+
}, 1000);
|
|
374
|
+
|
|
375
|
+
sseStream.onAbort(() => {
|
|
376
|
+
closed = true;
|
|
377
|
+
unsubscribe();
|
|
378
|
+
clearInterval(checkInterval);
|
|
379
|
+
resolve();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ─── Monitoring ──────────────────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Shell one-liner that gathers CPU, memory, disk, uptime, and load average.
|
|
389
|
+
* Outputs a single JSON line. Designed for Linux servers.
|
|
390
|
+
*
|
|
391
|
+
* Fields:
|
|
392
|
+
* cpu - usage % (100 - idle from /proc/stat snapshot)
|
|
393
|
+
* memTotal - total RAM bytes
|
|
394
|
+
* memUsed - used RAM bytes (total - available)
|
|
395
|
+
* memAvail - available RAM bytes
|
|
396
|
+
* diskTotal - root partition total bytes
|
|
397
|
+
* diskUsed - root partition used bytes
|
|
398
|
+
* diskAvail - root partition available bytes
|
|
399
|
+
* uptime - seconds since boot
|
|
400
|
+
* load1 - 1-min load average
|
|
401
|
+
* load5 - 5-min load average
|
|
402
|
+
* load15 - 15-min load average
|
|
403
|
+
*/
|
|
404
|
+
const STATS_COMMAND = [
|
|
405
|
+
// CPU: sample /proc/stat twice (200ms apart) for accurate usage
|
|
406
|
+
'read cpu0_u cpu0_n cpu0_s cpu0_i cpu0_rest <<< $(head -1 /proc/stat | awk \'{print $2,$3,$4,$5}\');',
|
|
407
|
+
'sleep 0.2;',
|
|
408
|
+
'read cpu1_u cpu1_n cpu1_s cpu1_i cpu1_rest <<< $(head -1 /proc/stat | awk \'{print $2,$3,$4,$5}\');',
|
|
409
|
+
'cpu_d=$(( (cpu1_u-cpu0_u)+(cpu1_n-cpu0_n)+(cpu1_s-cpu0_s)+(cpu1_i-cpu0_i) ));',
|
|
410
|
+
'cpu_idle=$(( cpu1_i - cpu0_i ));',
|
|
411
|
+
'[ "$cpu_d" -gt 0 ] && cpu_pct=$(( 100 - (cpu_idle * 100 / cpu_d) )) || cpu_pct=0;',
|
|
412
|
+
// Memory
|
|
413
|
+
'read mem_t mem_a <<< $(awk \'/MemTotal/{t=$2} /MemAvailable/{a=$2} END{print t*1024, a*1024}\' /proc/meminfo);',
|
|
414
|
+
'mem_u=$((mem_t - mem_a));',
|
|
415
|
+
// Disk
|
|
416
|
+
'read disk_t disk_u disk_a <<< $(df -B1 / | awk \'NR==2{print $2,$3,$4}\');',
|
|
417
|
+
// Uptime + load
|
|
418
|
+
'read up_s _ <<< $(cat /proc/uptime);',
|
|
419
|
+
'read l1 l5 l15 _ _ <<< $(cat /proc/loadavg);',
|
|
420
|
+
// Output JSON
|
|
421
|
+
'printf \'{"cpu":%d,"memTotal":%s,"memUsed":%s,"memAvail":%s,"diskTotal":%s,"diskUsed":%s,"diskAvail":%s,"uptime":"%s","load1":"%s","load5":"%s","load15":"%s"}\\n\' "$cpu_pct" "$mem_t" "$mem_u" "$mem_a" "$disk_t" "$disk_u" "$disk_a" "$up_s" "$l1" "$l5" "$l15"',
|
|
422
|
+
].join(" ");
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* GET /system/monitor/stream
|
|
426
|
+
*
|
|
427
|
+
* SSE stream that emits system stats every few seconds.
|
|
428
|
+
* Runs a lightweight stats command via SSH on an interval.
|
|
429
|
+
* Stops when the client disconnects.
|
|
430
|
+
*/
|
|
431
|
+
export async function monitorStream(c: Context) {
|
|
432
|
+
if (env.CLOUD_MODE) return c.json({ error: "Not available" }, 404);
|
|
433
|
+
|
|
434
|
+
const POLL_INTERVAL = 3_000;
|
|
435
|
+
|
|
436
|
+
return streamSSE(c, async (sseStream) => {
|
|
437
|
+
let closed = false;
|
|
438
|
+
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
439
|
+
|
|
440
|
+
const poll = async () => {
|
|
441
|
+
if (closed) return;
|
|
442
|
+
try {
|
|
443
|
+
const raw = await sshManager.withExecutor((executor) =>
|
|
444
|
+
executor.exec(STATS_COMMAND, { timeout: 5_000 }),
|
|
445
|
+
);
|
|
446
|
+
if (closed) return;
|
|
447
|
+
// Validate it's parseable JSON before sending
|
|
448
|
+
JSON.parse(raw);
|
|
449
|
+
await sseStream.writeSSE({ event: "stats", data: raw });
|
|
450
|
+
} catch (err) {
|
|
451
|
+
if (closed) return;
|
|
452
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
453
|
+
await sseStream.writeSSE({
|
|
454
|
+
event: "error",
|
|
455
|
+
data: JSON.stringify({ error: msg }),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// First poll immediately
|
|
461
|
+
await poll();
|
|
462
|
+
|
|
463
|
+
// Then poll on interval
|
|
464
|
+
pollTimer = setInterval(() => void poll(), POLL_INTERVAL);
|
|
465
|
+
|
|
466
|
+
// Wait until client disconnects
|
|
467
|
+
await new Promise<void>((resolve) => {
|
|
468
|
+
sseStream.onAbort(() => {
|
|
469
|
+
closed = true;
|
|
470
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
471
|
+
resolve();
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
}
|