devpush 0.0.0-stage → 1.0.0-beta.4
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/README.md +22 -2
- package/agent-release.json +37 -0
- package/dist/package/LICENSE.md +21 -0
- package/dist/package/migrate.mjs +182 -0
- package/dist/package/plugins.mjs +125 -0
- package/dist/package/vite.mjs +439 -0
- package/package.json +168 -3
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon.svg +1 -0
- package/public/logo-email.png +0 -0
- package/public/robots.txt +3 -0
- package/public/social.png +0 -0
- package/src/cloudflare-runtime.d.ts +78 -0
- package/src/components/about-dialog.tsx +70 -0
- package/src/components/copy-button.tsx +74 -0
- package/src/components/logo-mark.tsx +20 -0
- package/src/components/pending-collection.tsx +103 -0
- package/src/components/resource-avatar.tsx +65 -0
- package/src/components/route-error-state.tsx +95 -0
- package/src/components/section-navigation.tsx +39 -0
- package/src/components/settings-layout.tsx +111 -0
- package/src/components/storage-capabilities.tsx +11 -0
- package/src/components/table-pager.tsx +101 -0
- package/src/components/ui/alert-dialog.tsx +180 -0
- package/src/components/ui/alert.tsx +78 -0
- package/src/components/ui/avatar.tsx +58 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/button-group.tsx +38 -0
- package/src/components/ui/button.tsx +67 -0
- package/src/components/ui/card.tsx +82 -0
- package/src/components/ui/chart.tsx +307 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/combobox.tsx +256 -0
- package/src/components/ui/dialog.tsx +163 -0
- package/src/components/ui/dropdown-menu.tsx +110 -0
- package/src/components/ui/empty.tsx +65 -0
- package/src/components/ui/field.tsx +192 -0
- package/src/components/ui/input-group.tsx +118 -0
- package/src/components/ui/input-otp.tsx +70 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/pagination.tsx +34 -0
- package/src/components/ui/popover.tsx +86 -0
- package/src/components/ui/select.tsx +145 -0
- package/src/components/ui/skeleton.tsx +19 -0
- package/src/components/ui/sonner.tsx +47 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +81 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +55 -0
- package/src/features/admin/admin-settings.tsx +1048 -0
- package/src/features/auth/current-user-context-query.ts +11 -0
- package/src/features/auth/login-form.tsx +442 -0
- package/src/features/auth/sign-out-menu-item.tsx +42 -0
- package/src/features/deployments/deployment-actions.tsx +336 -0
- package/src/features/deployments/deployment-alias-badge.tsx +35 -0
- package/src/features/deployments/deployment-detail.tsx +596 -0
- package/src/features/deployments/deployment-filters.ts +66 -0
- package/src/features/deployments/deployment-list.tsx +465 -0
- package/src/features/deployments/deployment-visit-links.tsx +104 -0
- package/src/features/deployments/deployments-page.tsx +376 -0
- package/src/features/logs/log-scroll-area.tsx +69 -0
- package/src/features/notifications/email-preferences.tsx +232 -0
- package/src/features/onboarding/profile-onboarding.tsx +162 -0
- package/src/features/plugins/contributions.tsx +118 -0
- package/src/features/projects/backup-job-logs.tsx +143 -0
- package/src/features/projects/backup-policy-delete-dialog.tsx +82 -0
- package/src/features/projects/backup-restore-actions.tsx +573 -0
- package/src/features/projects/backup-status-icon.tsx +33 -0
- package/src/features/projects/backup-target-list.tsx +222 -0
- package/src/features/projects/backup-timestamp.tsx +32 -0
- package/src/features/projects/backup-volume-picker.tsx +158 -0
- package/src/features/projects/compose-service-input.tsx +64 -0
- package/src/features/projects/deploy-readiness.ts +28 -0
- package/src/features/projects/environment-dot.tsx +91 -0
- package/src/features/projects/log-keyword-filter.tsx +48 -0
- package/src/features/projects/manual-deploy-dialog.tsx +563 -0
- package/src/features/projects/pending-section.ts +26 -0
- package/src/features/projects/project-backup-details-pending.tsx +305 -0
- package/src/features/projects/project-backup-details.tsx +936 -0
- package/src/features/projects/project-backup-policy.tsx +644 -0
- package/src/features/projects/project-backup-utils.ts +190 -0
- package/src/features/projects/project-backups-pending.tsx +136 -0
- package/src/features/projects/project-backups.tsx +876 -0
- package/src/features/projects/project-environment-settings.tsx +1128 -0
- package/src/features/projects/project-heading.tsx +68 -0
- package/src/features/projects/project-layout.tsx +188 -0
- package/src/features/projects/project-logs.tsx +461 -0
- package/src/features/projects/project-overview.tsx +280 -0
- package/src/features/projects/project-route-pending.tsx +1064 -0
- package/src/features/projects/project-routing-settings.tsx +663 -0
- package/src/features/projects/project-settings-section.tsx +64 -0
- package/src/features/projects/project-settings-utils.ts +101 -0
- package/src/features/projects/project-settings.tsx +1472 -0
- package/src/features/projects/repository-picker-dialog.tsx +404 -0
- package/src/features/repositories/new-project-dialog-launcher.tsx +44 -0
- package/src/features/repositories/new-project-dialog.tsx +1204 -0
- package/src/features/repositories/new-project-pending.tsx +28 -0
- package/src/features/repositories/repository-list-pending.tsx +59 -0
- package/src/features/servers/server-availability-alert.tsx +44 -0
- package/src/features/servers/server-availability.ts +29 -0
- package/src/features/servers/server-avatar.tsx +52 -0
- package/src/features/servers/server-detail-pending.tsx +315 -0
- package/src/features/servers/server-detail.tsx +855 -0
- package/src/features/servers/server-metric-chart-pending.tsx +38 -0
- package/src/features/servers/server-metric-chart.tsx +182 -0
- package/src/features/servers/server-metric-format.ts +50 -0
- package/src/features/servers/server-metrics-pending.tsx +75 -0
- package/src/features/servers/server-metrics.tsx +136 -0
- package/src/features/servers/team-servers.tsx +432 -0
- package/src/features/settings/settings-route-pending.tsx +406 -0
- package/src/features/teams/backup-storage-settings.tsx +483 -0
- package/src/features/teams/navigation.ts +51 -0
- package/src/features/teams/new-team-dialog.tsx +132 -0
- package/src/features/teams/pending-section.ts +19 -0
- package/src/features/teams/team-header.tsx +1286 -0
- package/src/features/teams/team-heading.tsx +57 -0
- package/src/features/teams/team-home.tsx +391 -0
- package/src/features/teams/team-invite.tsx +155 -0
- package/src/features/teams/team-route-pending.tsx +505 -0
- package/src/features/teams/team-settings.tsx +1555 -0
- package/src/features/teams/use-workspace-updates.ts +126 -0
- package/src/features/users/api-keys-settings.tsx +544 -0
- package/src/features/users/api-scope-selection.tsx +269 -0
- package/src/features/users/connected-apps-settings.tsx +449 -0
- package/src/features/users/mcp-consent.tsx +220 -0
- package/src/features/users/profile-picture.tsx +171 -0
- package/src/features/users/user-settings.tsx +1064 -0
- package/src/lib/account-settings-schema.ts +78 -0
- package/src/lib/action-feedback.ts +81 -0
- package/src/lib/admin-search-schema.ts +8 -0
- package/src/lib/agent-release.d.mts +21 -0
- package/src/lib/agent-release.mjs +90 -0
- package/src/lib/allowlist-schema.ts +79 -0
- package/src/lib/api-key-schema.ts +176 -0
- package/src/lib/auth/client.ts +20 -0
- package/src/lib/avatar-schema.ts +11 -0
- package/src/lib/backup-cron.ts +118 -0
- package/src/lib/backup-database.ts +41 -0
- package/src/lib/backup-feature.ts +2 -0
- package/src/lib/backup-paths.ts +7 -0
- package/src/lib/backup-policy-schema.ts +85 -0
- package/src/lib/backup-resource-schema.ts +12 -0
- package/src/lib/backup-restore-schema.ts +15 -0
- package/src/lib/backup-storage-schema.ts +55 -0
- package/src/lib/deferred-module.ts +38 -0
- package/src/lib/deployment-logs-query.ts +63 -0
- package/src/lib/display-name.ts +7 -0
- package/src/lib/error-message.ts +119 -0
- package/src/lib/form-errors.ts +10 -0
- package/src/lib/internal-redirect.ts +25 -0
- package/src/lib/manual-deploy-options-query.ts +21 -0
- package/src/lib/notification-preferences.ts +43 -0
- package/src/lib/onboarding.ts +11 -0
- package/src/lib/operation-logs-query.ts +51 -0
- package/src/lib/page-head.ts +14 -0
- package/src/lib/project-context-query.ts +257 -0
- package/src/lib/project-creation-schema.ts +64 -0
- package/src/lib/project-env-schema.ts +23 -0
- package/src/lib/project-environments-schema.ts +56 -0
- package/src/lib/project-routing-schema.ts +94 -0
- package/src/lib/project-runtime-config.ts +131 -0
- package/src/lib/project-settings-schema.ts +75 -0
- package/src/lib/public-api-schema.ts +128 -0
- package/src/lib/public-error.ts +337 -0
- package/src/lib/query.ts +13 -0
- package/src/lib/repository-selection-query.ts +11 -0
- package/src/lib/resource-cache.ts +30 -0
- package/src/lib/runtime-id.ts +19 -0
- package/src/lib/search.ts +14 -0
- package/src/lib/server-context-query.ts +43 -0
- package/src/lib/server-name-schema.ts +11 -0
- package/src/lib/settings-conflict.ts +45 -0
- package/src/lib/settings-version.ts +7 -0
- package/src/lib/team-context-query.ts +126 -0
- package/src/lib/team-context-schema.ts +8 -0
- package/src/lib/team-forms-schema.ts +45 -0
- package/src/lib/team-role.ts +13 -0
- package/src/lib/team-settings-schema.ts +48 -0
- package/src/lib/urls.ts +3 -0
- package/src/lib/use-avatar-input.ts +61 -0
- package/src/lib/use-refresh-error.ts +18 -0
- package/src/lib/use-settings-version.ts +23 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validation.ts +5 -0
- package/src/lib/workspace-updates.ts +53 -0
- package/src/package/config.ts +10 -0
- package/src/package/env.mjs +54 -0
- package/src/package/migrate-api.d.ts +5 -0
- package/src/package/migrate.ts +56 -0
- package/src/package/plugins.ts +141 -0
- package/src/package/sqlite.mjs +2 -0
- package/src/package/vite-api.d.ts +5 -0
- package/src/package/vite.ts +324 -0
- package/src/package/watch-guard.cjs +26 -0
- package/src/plugins/client.ts +57 -0
- package/src/plugins/contract.ts +188 -0
- package/src/plugins/default.server.ts +4 -0
- package/src/plugins/default.ui.ts +4 -0
- package/src/plugins/operations.ts +45 -0
- package/src/plugins/server.ts +197 -0
- package/src/plugins/version.ts +1 -0
- package/src/routeTree.gen.ts +1275 -0
- package/src/router.tsx +46 -0
- package/src/routes/$teamSlug.index.tsx +53 -0
- package/src/routes/$teamSlug.new-project.tsx +13 -0
- package/src/routes/$teamSlug.projects.$projectName.backups.$backupId.tsx +40 -0
- package/src/routes/$teamSlug.projects.$projectName.backups.tsx +82 -0
- package/src/routes/$teamSlug.projects.$projectName.deployments.$deploymentId.tsx +52 -0
- package/src/routes/$teamSlug.projects.$projectName.deployments.tsx +86 -0
- package/src/routes/$teamSlug.projects.$projectName.logs.tsx +55 -0
- package/src/routes/$teamSlug.projects.$projectName.settings.tsx +51 -0
- package/src/routes/$teamSlug.projects.$projectName.tsx +106 -0
- package/src/routes/$teamSlug.projects.tsx +40 -0
- package/src/routes/$teamSlug.servers.$serverId.tsx +31 -0
- package/src/routes/$teamSlug.servers.index.tsx +59 -0
- package/src/routes/$teamSlug.servers.tsx +7 -0
- package/src/routes/$teamSlug.settings.tsx +56 -0
- package/src/routes/$teamSlug.tsx +86 -0
- package/src/routes/[.]well-known.oauth-authorization-server.api.auth.ts +13 -0
- package/src/routes/[.]well-known.oauth-protected-resource.mcp.ts +6 -0
- package/src/routes/__root.tsx +155 -0
- package/src/routes/admin.tsx +55 -0
- package/src/routes/api/agents/certificates.ts +21 -0
- package/src/routes/api/agents/credentials/rotate.ts +22 -0
- package/src/routes/api/agents/heartbeat.ts +42 -0
- package/src/routes/api/agents/job-logs.ts +22 -0
- package/src/routes/api/agents/jobs/complete.ts +22 -0
- package/src/routes/api/agents/jobs/next.ts +29 -0
- package/src/routes/api/agents/jobs/progress.ts +32 -0
- package/src/routes/api/agents/jobs/status.ts +22 -0
- package/src/routes/api/agents/register.ts +45 -0
- package/src/routes/api/agents/runtime-logs.ts +22 -0
- package/src/routes/api/agents/switch.ts +25 -0
- package/src/routes/api/auth/$.ts +38 -0
- package/src/routes/api/cron/backups.ts +33 -0
- package/src/routes/api/cron/maintenance.ts +30 -0
- package/src/routes/api/github/install.callback.ts +51 -0
- package/src/routes/api/github/install.ts +41 -0
- package/src/routes/api/github/webhook.ts +11 -0
- package/src/routes/api/health.ts +19 -0
- package/src/routes/api/install/agent.$token.ts +50 -0
- package/src/routes/api/install/agent.binary.ts +83 -0
- package/src/routes/api/install/agent.checksum.ts +45 -0
- package/src/routes/api/install/agent.ts +26 -0
- package/src/routes/api/plugins/$.ts +11 -0
- package/src/routes/api/v1/$.ts +15 -0
- package/src/routes/api/workspaces/updates.ts +8 -0
- package/src/routes/auth.login.tsx +50 -0
- package/src/routes/deployments.$deploymentId.unavailable.tsx +21 -0
- package/src/routes/index.tsx +23 -0
- package/src/routes/invites.$token.tsx +35 -0
- package/src/routes/mcp.ts +13 -0
- package/src/routes/oauth.consent.tsx +54 -0
- package/src/routes/onboarding.tsx +37 -0
- package/src/routes/onboarding.workspace.tsx +44 -0
- package/src/routes/settings.tsx +30 -0
- package/src/routes/uploads/avatars/$fileName.ts +44 -0
- package/src/server/admin/access.ts +67 -0
- package/src/server/admin/functions.ts +122 -0
- package/src/server/admin/service.ts +260 -0
- package/src/server/agents/backup-state.ts +210 -0
- package/src/server/agents/certificates.ts +96 -0
- package/src/server/agents/claims.ts +116 -0
- package/src/server/agents/cleanup.ts +52 -0
- package/src/server/agents/compatibility.ts +116 -0
- package/src/server/agents/completion.ts +83 -0
- package/src/server/agents/container-cleanup-state.ts +51 -0
- package/src/server/agents/deployment-state.ts +111 -0
- package/src/server/agents/job-payloads.ts +159 -0
- package/src/server/agents/jobs.ts +1293 -0
- package/src/server/agents/protocol.ts +400 -0
- package/src/server/agents/reconciliation.ts +43 -0
- package/src/server/agents/runtime-logs.ts +337 -0
- package/src/server/agents/runtime-state.ts +125 -0
- package/src/server/api/admission.ts +12 -0
- package/src/server/api/authorization.ts +115 -0
- package/src/server/api/credentials.ts +569 -0
- package/src/server/api/cursors.ts +72 -0
- package/src/server/api/errors.ts +24 -0
- package/src/server/api/functions.ts +60 -0
- package/src/server/api/handler.ts +274 -0
- package/src/server/api/mutation-operations.ts +913 -0
- package/src/server/api/operation-context.ts +139 -0
- package/src/server/api/operation-contract.ts +1099 -0
- package/src/server/api/operations.ts +64 -0
- package/src/server/api/read-operations.ts +891 -0
- package/src/server/api/retention.ts +33 -0
- package/src/server/auth/access.ts +109 -0
- package/src/server/auth/auth-methods.ts +28 -0
- package/src/server/auth/better-auth.ts +518 -0
- package/src/server/auth/context.ts +28 -0
- package/src/server/auth/email-change.ts +47 -0
- package/src/server/auth/email-delivery.ts +51 -0
- package/src/server/auth/provider-identity.ts +19 -0
- package/src/server/auth/provider-tokens.ts +27 -0
- package/src/server/auth/session.server.ts +19 -0
- package/src/server/backups/database-commands.ts +40 -0
- package/src/server/backups/destination.ts +12 -0
- package/src/server/backups/feature.ts +6 -0
- package/src/server/backups/policy-validation.ts +51 -0
- package/src/server/backups/queue-state.ts +137 -0
- package/src/server/backups/service.ts +918 -0
- package/src/server/backups/storage-functions.ts +42 -0
- package/src/server/backups/storage-state.ts +67 -0
- package/src/server/backups/storage.ts +237 -0
- package/src/server/config.ts +274 -0
- package/src/server/db/atomic.ts +16 -0
- package/src/server/db/d1-connection.ts +83 -0
- package/src/server/db/errors.ts +17 -0
- package/src/server/db/index.cloudflare.ts +15 -0
- package/src/server/db/index.ts +35 -0
- package/src/server/db/libsql-connection.ts +72 -0
- package/src/server/db/log-schema.ts +102 -0
- package/src/server/db/migrate-libsql.ts +29 -0
- package/src/server/db/migrate-sqlite.ts +47 -0
- package/src/server/db/migration-history.ts +12 -0
- package/src/server/db/migrations-logs/0000_initial.sql +48 -0
- package/src/server/db/migrations-logs/0001_runtime_log_order.sql +2 -0
- package/src/server/db/migrations-logs/meta/0000_snapshot.json +327 -0
- package/src/server/db/migrations-logs/meta/0001_snapshot.json +329 -0
- package/src/server/db/migrations-logs/meta/_journal.json +20 -0
- package/src/server/db/migrations-sqlite/0000_initial.sql +894 -0
- package/src/server/db/migrations-sqlite/0001_agent_indexes.sql +5 -0
- package/src/server/db/migrations-sqlite/meta/0000_snapshot.json +5816 -0
- package/src/server/db/migrations-sqlite/meta/0001_snapshot.json +5841 -0
- package/src/server/db/migrations-sqlite/meta/_journal.json +20 -0
- package/src/server/db/oauth-schema.ts +216 -0
- package/src/server/db/remote-config.ts +47 -0
- package/src/server/db/schema.ts +1218 -0
- package/src/server/db/sqlite-connection.ts +94 -0
- package/src/server/db/sqlite-files.ts +27 -0
- package/src/server/db/sqlite-params.ts +19 -0
- package/src/server/db/worker-connection.ts +47 -0
- package/src/server/deleted-resources.ts +133 -0
- package/src/server/deletion-recovery.ts +272 -0
- package/src/server/email/layout.tsx +85 -0
- package/src/server/email/notification-template.tsx +62 -0
- package/src/server/email/otp-template.tsx +48 -0
- package/src/server/email/service.ts +251 -0
- package/src/server/email/team-invite-template.tsx +59 -0
- package/src/server/email/theme.ts +10 -0
- package/src/server/github/http.ts +14 -0
- package/src/server/github/install-state.ts +48 -0
- package/src/server/github/service.ts +831 -0
- package/src/server/github/webhook-handler.ts +198 -0
- package/src/server/github/webhooks.ts +205 -0
- package/src/server/http/auth.ts +11 -0
- package/src/server/http/error-diagnostics.ts +66 -0
- package/src/server/http/plugin-request.ts +15 -0
- package/src/server/http/proxy-request.ts +53 -0
- package/src/server/http/public-errors.ts +48 -0
- package/src/server/http/rate-limit.ts +59 -0
- package/src/server/http/request-body.ts +54 -0
- package/src/server/http/request-ip.ts +54 -0
- package/src/server/http/security-headers.ts +36 -0
- package/src/server/install/agent-binary.ts +230 -0
- package/src/server/install/agent-script.ts +565 -0
- package/src/server/install/release-binary.ts +65 -0
- package/src/server/logs/budget.ts +72 -0
- package/src/server/logs/cleanup.ts +64 -0
- package/src/server/logs/deployments.ts +98 -0
- package/src/server/logs/operations.ts +145 -0
- package/src/server/logs/retention.ts +39 -0
- package/src/server/maintenance.ts +61 -0
- package/src/server/mcp/connections.ts +358 -0
- package/src/server/mcp/functions.ts +63 -0
- package/src/server/mcp/handler.ts +295 -0
- package/src/server/mcp/oauth-discovery.cloudflare.ts +6 -0
- package/src/server/mcp/oauth-discovery.ts +8 -0
- package/src/server/mcp/oauth-provider.ts +220 -0
- package/src/server/mcp/urls.ts +20 -0
- package/src/server/notifications/functions.ts +51 -0
- package/src/server/notifications/observe.ts +303 -0
- package/src/server/notifications/outbox.ts +335 -0
- package/src/server/notifications/preferences.ts +131 -0
- package/src/server/notifications/query.ts +16 -0
- package/src/server/plugins/scope.ts +17 -0
- package/src/server/plugins/server-context.ts +157 -0
- package/src/server/plugins/services.server.ts +10 -0
- package/src/server/plugins/workspace-access.ts +74 -0
- package/src/server/projects/backup-policy-state.ts +68 -0
- package/src/server/projects/backups.ts +219 -0
- package/src/server/projects/compose-metadata.ts +174 -0
- package/src/server/projects/configuration-state.ts +90 -0
- package/src/server/projects/context.ts +909 -0
- package/src/server/projects/creation-state.ts +46 -0
- package/src/server/projects/deletion-state.ts +69 -0
- package/src/server/projects/deployment-config.ts +139 -0
- package/src/server/projects/deployment-logs.ts +97 -0
- package/src/server/projects/deployments.ts +1123 -0
- package/src/server/projects/domain-state.ts +71 -0
- package/src/server/projects/environment-branches.ts +75 -0
- package/src/server/projects/environment-state.ts +158 -0
- package/src/server/projects/environments.ts +424 -0
- package/src/server/projects/functions.ts +748 -0
- package/src/server/projects/generated-hostname.ts +77 -0
- package/src/server/projects/hostnames.ts +244 -0
- package/src/server/projects/mappers.ts +457 -0
- package/src/server/projects/names.ts +41 -0
- package/src/server/projects/project-env-vars.ts +28 -0
- package/src/server/projects/queue-state.ts +118 -0
- package/src/server/projects/repository.ts +368 -0
- package/src/server/projects/route-state.ts +37 -0
- package/src/server/projects/runtime-logs.ts +97 -0
- package/src/server/projects/settings-context.ts +58 -0
- package/src/server/projects/settings.ts +738 -0
- package/src/server/projects/types.ts +355 -0
- package/src/server/realtime/agent-events.ts +32 -0
- package/src/server/realtime/node-broadcaster.ts +71 -0
- package/src/server/realtime/node-handler.ts +55 -0
- package/src/server/realtime/service.ts +125 -0
- package/src/server/realtime/workspace-object.cloudflare.ts +62 -0
- package/src/server/realtime/workspace-object.ts +3 -0
- package/src/server/resources/color.ts +13 -0
- package/src/server/runtime/background.ts +21 -0
- package/src/server/runtime/node-asset-cache-plugin.ts +12 -0
- package/src/server/runtime/node-scheduler-plugin.ts +17 -0
- package/src/server/runtime/node-scheduler.ts +51 -0
- package/src/server/runtime/platform-env.cloudflare.ts +17 -0
- package/src/server/runtime/platform-env.ts +19 -0
- package/src/server/runtime/scheduled-task-error.ts +9 -0
- package/src/server/scheduled-tasks.ts +28 -0
- package/src/server/secrets/crypto.ts +56 -0
- package/src/server/servers/functions.ts +233 -0
- package/src/server/servers/lifecycle.ts +122 -0
- package/src/server/servers/metric-history-schema.ts +24 -0
- package/src/server/servers/metric-history.ts +55 -0
- package/src/server/servers/monitoring.ts +592 -0
- package/src/server/servers/service.ts +1211 -0
- package/src/server/servers/status.ts +32 -0
- package/src/server/servers/switch.ts +96 -0
- package/src/server/settings-version.ts +43 -0
- package/src/server/storage/capabilities.ts +6 -0
- package/src/server/storage/local-object-store.cloudflare.ts +7 -0
- package/src/server/storage/local-object-store.ts +50 -0
- package/src/server/storage/object-store.ts +160 -0
- package/src/server/teams/creation.ts +51 -0
- package/src/server/teams/delete-policy.ts +13 -0
- package/src/server/teams/deletion-state.ts +80 -0
- package/src/server/teams/functions.ts +324 -0
- package/src/server/teams/invitations.ts +38 -0
- package/src/server/teams/membership.ts +86 -0
- package/src/server/teams/queries.ts +261 -0
- package/src/server/teams/service.ts +1127 -0
- package/src/server/teams/types.ts +101 -0
- package/src/server/uploads/avatar.ts +133 -0
- package/src/server/users/deletion-state.ts +93 -0
- package/src/server/users/functions.ts +160 -0
- package/src/server/users/onboarding.ts +17 -0
- package/src/server/users/service.ts +644 -0
- package/src/server.ts +55 -0
- package/src/styles/shadcn.css +653 -0
- package/src/styles/theme.css +76 -0
- package/src/styles.css +177 -0
- package/wrangler.template.jsonc +37 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { deferBackgroundTask } from '#devpush/server/runtime/background'
|
|
2
|
+
import { notificationRows } from './query'
|
|
3
|
+
import { eq, sql } from 'drizzle-orm'
|
|
4
|
+
import { db } from '#devpush/server/db'
|
|
5
|
+
import { config } from '#devpush/server/config'
|
|
6
|
+
import { notificationScanCursors } from '#devpush/server/db/schema'
|
|
7
|
+
import {
|
|
8
|
+
observeOperationalNotification,
|
|
9
|
+
deliverOperationalNotifications,
|
|
10
|
+
repairOperationalRecipients,
|
|
11
|
+
} from './outbox'
|
|
12
|
+
|
|
13
|
+
type ProjectSource = {
|
|
14
|
+
id: string
|
|
15
|
+
teamId: string
|
|
16
|
+
slug: string
|
|
17
|
+
name: string
|
|
18
|
+
teamSlug: string
|
|
19
|
+
}
|
|
20
|
+
async function projectSource(id: string) {
|
|
21
|
+
const [row] = await notificationRows<ProjectSource>(
|
|
22
|
+
sql`select p.id,p.team_id as teamId,p.slug,p.name,t.slug as teamSlug from projects p join teams t on t.id=p.team_id where p.id=${id} and p.status='active' and t.status='active'`,
|
|
23
|
+
['id', 'teamId', 'slug', 'name', 'teamSlug'],
|
|
24
|
+
)
|
|
25
|
+
return row
|
|
26
|
+
}
|
|
27
|
+
export async function observeProjectNotifications(projectId: string) {
|
|
28
|
+
const p = await projectSource(projectId)
|
|
29
|
+
if (!p) return
|
|
30
|
+
const path = `/${encodeURIComponent(p.teamSlug)}/projects/${encodeURIComponent(p.slug)}`
|
|
31
|
+
const deployments = await notificationRows<{
|
|
32
|
+
id: string
|
|
33
|
+
environmentId: string
|
|
34
|
+
status: string
|
|
35
|
+
finishedAt: number
|
|
36
|
+
actorId: string | null
|
|
37
|
+
commitMeta: string
|
|
38
|
+
}>(
|
|
39
|
+
sql`
|
|
40
|
+
select d.id,d.environment_id as environmentId,d.status,d.finished_at as finishedAt,d.created_by_user_id as actorId,d.commit_meta as commitMeta
|
|
41
|
+
from deployments d join environments e on e.id=d.environment_id where e.project_id=${p.id} and e.status='active'
|
|
42
|
+
and d.status in ('failed','succeeded') and d.id=(select x.id from deployments x where x.environment_id=d.environment_id order by x.created_at desc,x.id desc limit 1)`,
|
|
43
|
+
['id', 'environmentId', 'status', 'finishedAt', 'actorId', 'commitMeta'],
|
|
44
|
+
)
|
|
45
|
+
for (const d of deployments) {
|
|
46
|
+
if (!d.finishedAt || d.finishedAt < Date.now() - 86_400_000) continue
|
|
47
|
+
let actorId = d.actorId
|
|
48
|
+
if (!actorId) {
|
|
49
|
+
let sender: unknown
|
|
50
|
+
try {
|
|
51
|
+
sender = (JSON.parse(d.commitMeta) as { githubSenderId?: unknown })
|
|
52
|
+
.githubSenderId
|
|
53
|
+
} catch {
|
|
54
|
+
sender = null
|
|
55
|
+
}
|
|
56
|
+
if (typeof sender === 'string' && /^\d+$/.test(sender)) {
|
|
57
|
+
const linked = await notificationRows<{ userId: string }>(
|
|
58
|
+
sql`select a.user_id as userId from account a join team_members m on m.user_id=a.user_id where a.provider_id='github' and a.account_id=${sender} and m.team_id=${p.teamId}`,
|
|
59
|
+
['userId'],
|
|
60
|
+
)
|
|
61
|
+
actorId = linked.length === 1 ? linked[0]!.userId : null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await observeOperationalNotification({
|
|
65
|
+
id: `deployment:${d.environmentId}`,
|
|
66
|
+
sourceId: d.environmentId,
|
|
67
|
+
teamId: p.teamId,
|
|
68
|
+
projectId: p.id,
|
|
69
|
+
category: 'deployments',
|
|
70
|
+
failed: d.status === 'failed',
|
|
71
|
+
revision: d.id,
|
|
72
|
+
at: d.finishedAt,
|
|
73
|
+
title: `Deployment failed: ${p.name}`,
|
|
74
|
+
message: `A deployment of ${p.name} failed. Open the deployment logs for details.`,
|
|
75
|
+
path: `${path}/deployments/${encodeURIComponent(d.id)}`,
|
|
76
|
+
actorId,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
const backups = await notificationRows<{
|
|
80
|
+
id: string
|
|
81
|
+
environmentId: string
|
|
82
|
+
configId: string | null
|
|
83
|
+
status: string
|
|
84
|
+
finishedAt: number
|
|
85
|
+
}>(
|
|
86
|
+
sql`
|
|
87
|
+
select b.id,b.environment_id as environmentId,b.config_id as configId,b.status,b.finished_at as finishedAt from backup_runs b
|
|
88
|
+
where b.project_id=${p.id} and b.status in ('failed','succeeded') and b.id=(select x.id from backup_runs x where x.environment_id=b.environment_id and coalesce(x.config_id,'')=coalesce(b.config_id,'') order by x.created_at desc,x.id desc limit 1)`,
|
|
89
|
+
['id', 'environmentId', 'configId', 'status', 'finishedAt'],
|
|
90
|
+
)
|
|
91
|
+
for (const b of backups.filter(
|
|
92
|
+
(backup) =>
|
|
93
|
+
backup.finishedAt && backup.finishedAt >= Date.now() - 86_400_000,
|
|
94
|
+
))
|
|
95
|
+
await observeOperationalNotification({
|
|
96
|
+
id: `backup:${b.environmentId}:${b.configId ?? 'manual'}`,
|
|
97
|
+
sourceId: b.environmentId,
|
|
98
|
+
teamId: p.teamId,
|
|
99
|
+
projectId: p.id,
|
|
100
|
+
category: 'backups',
|
|
101
|
+
failed: b.status === 'failed',
|
|
102
|
+
revision: b.id,
|
|
103
|
+
at: b.finishedAt,
|
|
104
|
+
title: `Backup failed: ${p.name}`,
|
|
105
|
+
message: `A backup of ${p.name} failed. Check its storage destination and backup details.`,
|
|
106
|
+
path: `${path}/backups`,
|
|
107
|
+
})
|
|
108
|
+
const domains = await notificationRows<{
|
|
109
|
+
id: string
|
|
110
|
+
hostname: string
|
|
111
|
+
status: string
|
|
112
|
+
updatedAt: number
|
|
113
|
+
}>(
|
|
114
|
+
sql`select id,hostname,status,updated_at as updatedAt from domains where project_id=${p.id} and status in ('active','failed')`,
|
|
115
|
+
['id', 'hostname', 'status', 'updatedAt'],
|
|
116
|
+
)
|
|
117
|
+
for (const d of domains.filter(
|
|
118
|
+
(domain) => domain.updatedAt >= Date.now() - 86_400_000,
|
|
119
|
+
))
|
|
120
|
+
await observeOperationalNotification({
|
|
121
|
+
id: `domain:${d.id}`,
|
|
122
|
+
sourceId: d.id,
|
|
123
|
+
teamId: p.teamId,
|
|
124
|
+
projectId: p.id,
|
|
125
|
+
category: 'domains',
|
|
126
|
+
failed: d.status === 'failed',
|
|
127
|
+
revision: String(d.updatedAt),
|
|
128
|
+
at: d.updatedAt,
|
|
129
|
+
title: `Domain needs attention: ${d.hostname}`,
|
|
130
|
+
message: `Domain verification or HTTPS configuration for ${d.hostname} failed. Open routing settings for details.`,
|
|
131
|
+
path: `${path}/settings#domains`,
|
|
132
|
+
})
|
|
133
|
+
const restores = await notificationRows<{ id: string; finishedAt: number }>(
|
|
134
|
+
sql`select id,finished_at as finishedAt from agent_jobs where project_id=${p.id} and type='restore' and status in ('failed','abandoned') and finished_at>${Date.now() - 86_400_000}`,
|
|
135
|
+
['id', 'finishedAt'],
|
|
136
|
+
)
|
|
137
|
+
for (const r of restores)
|
|
138
|
+
await observeOperationalNotification({
|
|
139
|
+
id: `restore:${r.id}`,
|
|
140
|
+
sourceId: r.id,
|
|
141
|
+
teamId: p.teamId,
|
|
142
|
+
projectId: p.id,
|
|
143
|
+
category: 'backups',
|
|
144
|
+
failed: true,
|
|
145
|
+
revision: r.id,
|
|
146
|
+
at: r.finishedAt,
|
|
147
|
+
title: `Restore failed: ${p.name}`,
|
|
148
|
+
message: `A restore of ${p.name} failed and may need manual recovery. Check restore details before making further changes.`,
|
|
149
|
+
path: `${path}/backups`,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
export async function observeServerNotifications(
|
|
153
|
+
serverId: string,
|
|
154
|
+
now = Date.now(),
|
|
155
|
+
) {
|
|
156
|
+
const [s] = await notificationRows<{
|
|
157
|
+
id: string
|
|
158
|
+
teamId: string
|
|
159
|
+
name: string
|
|
160
|
+
handle: string
|
|
161
|
+
teamSlug: string
|
|
162
|
+
status: string
|
|
163
|
+
lastSeenAt: number | null
|
|
164
|
+
diskUsed: number | null
|
|
165
|
+
diskTotal: number | null
|
|
166
|
+
metricAt: number | null
|
|
167
|
+
}>(
|
|
168
|
+
sql`
|
|
169
|
+
select s.id,s.team_id as teamId,s.name,s.handle,t.slug as teamSlug,s.status,s.last_seen_at as lastSeenAt,
|
|
170
|
+
m.disk_used_bytes as diskUsed,m.disk_total_bytes as diskTotal,m.observed_at as metricAt
|
|
171
|
+
from servers s join teams t on t.id=s.team_id left join server_metrics m on m.id=(select x.id from server_metrics x where x.server_id=s.id order by x.observed_at desc limit 1)
|
|
172
|
+
where s.id=${serverId} and s.status in ('connected','disconnected') and t.status='active'`,
|
|
173
|
+
[
|
|
174
|
+
'id',
|
|
175
|
+
'teamId',
|
|
176
|
+
'name',
|
|
177
|
+
'handle',
|
|
178
|
+
'teamSlug',
|
|
179
|
+
'status',
|
|
180
|
+
'lastSeenAt',
|
|
181
|
+
'diskUsed',
|
|
182
|
+
'diskTotal',
|
|
183
|
+
'metricAt',
|
|
184
|
+
],
|
|
185
|
+
)
|
|
186
|
+
if (!s || !s.lastSeenAt || now - s.lastSeenAt > 86_400_000) return
|
|
187
|
+
const path = `/${encodeURIComponent(s.teamSlug)}/servers/${encodeURIComponent(s.id)}`
|
|
188
|
+
// A five-minute grace period avoids alerts for short network interruptions.
|
|
189
|
+
const offline = now - s.lastSeenAt >= 300_000
|
|
190
|
+
await observeOperationalNotification({
|
|
191
|
+
id: `server:${s.id}`,
|
|
192
|
+
sourceId: s.id,
|
|
193
|
+
teamId: s.teamId,
|
|
194
|
+
projectId: null,
|
|
195
|
+
category: 'servers',
|
|
196
|
+
failed: offline,
|
|
197
|
+
revision: String(s.lastSeenAt),
|
|
198
|
+
at: now,
|
|
199
|
+
title: `Server disconnected: ${s.name}`,
|
|
200
|
+
message: `${s.name} has not contacted devpush for at least five minutes. Existing applications may still be running.`,
|
|
201
|
+
path,
|
|
202
|
+
})
|
|
203
|
+
if (
|
|
204
|
+
s.metricAt &&
|
|
205
|
+
now - s.metricAt < 300_000 &&
|
|
206
|
+
s.diskTotal &&
|
|
207
|
+
s.diskUsed !== null
|
|
208
|
+
) {
|
|
209
|
+
const ratio = s.diskUsed / s.diskTotal
|
|
210
|
+
// Recovery hysteresis prevents repeated notifications around the warning threshold.
|
|
211
|
+
if (ratio >= 0.9 || ratio < 0.85)
|
|
212
|
+
await observeOperationalNotification({
|
|
213
|
+
id: `disk:${s.id}`,
|
|
214
|
+
sourceId: s.id,
|
|
215
|
+
teamId: s.teamId,
|
|
216
|
+
projectId: null,
|
|
217
|
+
category: 'servers',
|
|
218
|
+
failed: ratio >= 0.9,
|
|
219
|
+
revision: String(s.metricAt),
|
|
220
|
+
at: now,
|
|
221
|
+
title: `Low disk space: ${s.name}`,
|
|
222
|
+
message: `Disk usage on ${s.name} is at least 90%. Free space to avoid failed deployments and backups.`,
|
|
223
|
+
path,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function scan(source: 'projects' | 'servers') {
|
|
228
|
+
const [cursor] = await db
|
|
229
|
+
.select()
|
|
230
|
+
.from(notificationScanCursors)
|
|
231
|
+
.where(eq(notificationScanCursors.source, source))
|
|
232
|
+
.limit(1)
|
|
233
|
+
const table = sql.identifier(source)
|
|
234
|
+
const rows = await notificationRows<{ id: string }>(
|
|
235
|
+
sql`select id from ${table} where id>${cursor?.afterId ?? ''} and status ${source === 'projects' ? sql`='active'` : sql`in ('connected','disconnected')`} order by id limit 100`,
|
|
236
|
+
['id'],
|
|
237
|
+
)
|
|
238
|
+
for (const row of rows) {
|
|
239
|
+
if (source === 'projects') await observeProjectNotifications(row.id)
|
|
240
|
+
else await observeServerNotifications(row.id)
|
|
241
|
+
// Advance only after successful observation, so failures are retried.
|
|
242
|
+
await db
|
|
243
|
+
.insert(notificationScanCursors)
|
|
244
|
+
.values({ source, afterId: row.id })
|
|
245
|
+
.onConflictDoUpdate({
|
|
246
|
+
target: notificationScanCursors.source,
|
|
247
|
+
set: { afterId: row.id },
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
if (rows.length < 100)
|
|
251
|
+
await db
|
|
252
|
+
.insert(notificationScanCursors)
|
|
253
|
+
.values({ source, afterId: '' })
|
|
254
|
+
.onConflictDoUpdate({
|
|
255
|
+
target: notificationScanCursors.source,
|
|
256
|
+
set: { afterId: '' },
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
export async function maintainOperationalNotifications() {
|
|
260
|
+
if (!config.EMAIL_FROM || !config.RESEND_API_KEY) return
|
|
261
|
+
await Promise.all([scan('projects'), scan('servers')])
|
|
262
|
+
await repairOperationalRecipients()
|
|
263
|
+
await deliverOperationalNotifications()
|
|
264
|
+
}
|
|
265
|
+
/** Best-effort immediate pass; durable source state and the scheduled scan repair interruptions. */
|
|
266
|
+
export function notifyAfterAgentCompletion(serverId: string) {
|
|
267
|
+
if (!config.EMAIL_FROM || !config.RESEND_API_KEY) return
|
|
268
|
+
deferBackgroundTask(async () => {
|
|
269
|
+
try {
|
|
270
|
+
const rows = await notificationRows<{ id: string }>(
|
|
271
|
+
sql`select distinct project_id as id from agent_jobs where server_id=${serverId} and project_id is not null and finished_at>${Date.now() - 60_000} union select distinct e.project_id as id from deployments d join environments e on e.id=d.environment_id where d.server_id=${serverId} and d.finished_at>${Date.now() - 60_000}`,
|
|
272
|
+
['id'],
|
|
273
|
+
)
|
|
274
|
+
for (const p of rows) await observeProjectNotifications(p.id)
|
|
275
|
+
await deliverOperationalNotifications(10)
|
|
276
|
+
} catch {
|
|
277
|
+
console.warn('Operational notification pass deferred to scheduled retry.')
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function notifyAfterProjectUpdate(projectId: string) {
|
|
283
|
+
if (!config.EMAIL_FROM || !config.RESEND_API_KEY) return
|
|
284
|
+
deferBackgroundTask(async () => {
|
|
285
|
+
try {
|
|
286
|
+
await observeProjectNotifications(projectId)
|
|
287
|
+
await deliverOperationalNotifications(10)
|
|
288
|
+
} catch {
|
|
289
|
+
console.warn('Project notification pass deferred to scheduled retry.')
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
export function notifyAfterServerUpdate(serverId: string) {
|
|
294
|
+
if (!config.EMAIL_FROM || !config.RESEND_API_KEY) return
|
|
295
|
+
deferBackgroundTask(async () => {
|
|
296
|
+
try {
|
|
297
|
+
await observeServerNotifications(serverId)
|
|
298
|
+
await deliverOperationalNotifications(5)
|
|
299
|
+
} catch {
|
|
300
|
+
console.warn('Server notification pass deferred to scheduled retry.')
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { notificationRows } from './query'
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
3
|
+
import { and, eq, sql } from 'drizzle-orm'
|
|
4
|
+
import { db } from '#devpush/server/db'
|
|
5
|
+
import { config } from '#devpush/server/config'
|
|
6
|
+
import {
|
|
7
|
+
notificationIncidents,
|
|
8
|
+
notificationOutbox,
|
|
9
|
+
notificationScanCursors,
|
|
10
|
+
user,
|
|
11
|
+
} from '#devpush/server/db/schema'
|
|
12
|
+
import type { NotificationCategory } from '#devpush/lib/notification-preferences'
|
|
13
|
+
import { readNotificationPreferences } from './preferences'
|
|
14
|
+
import {
|
|
15
|
+
renderNotificationEmail,
|
|
16
|
+
sendEmail,
|
|
17
|
+
} from '#devpush/server/email/service'
|
|
18
|
+
|
|
19
|
+
export type OperationalObservation = {
|
|
20
|
+
id: string
|
|
21
|
+
sourceId: string
|
|
22
|
+
teamId: string
|
|
23
|
+
projectId: string | null
|
|
24
|
+
category: NotificationCategory
|
|
25
|
+
failed: boolean
|
|
26
|
+
revision: string
|
|
27
|
+
at: number
|
|
28
|
+
title: string
|
|
29
|
+
message: string
|
|
30
|
+
path: string
|
|
31
|
+
actorId?: string | null
|
|
32
|
+
}
|
|
33
|
+
export async function observeOperationalNotification(
|
|
34
|
+
input: OperationalObservation,
|
|
35
|
+
) {
|
|
36
|
+
const from = config.EMAIL_FROM
|
|
37
|
+
if (!from) return
|
|
38
|
+
const current = await db
|
|
39
|
+
.select()
|
|
40
|
+
.from(notificationIncidents)
|
|
41
|
+
.where(eq(notificationIncidents.id, input.id))
|
|
42
|
+
.limit(1)
|
|
43
|
+
const previous = current[0]
|
|
44
|
+
if (previous && input.at < previous.updatedAt) return
|
|
45
|
+
if (!input.failed && !previous) return
|
|
46
|
+
const revision = previous?.active
|
|
47
|
+
? previous.revision
|
|
48
|
+
: input.failed
|
|
49
|
+
? input.revision
|
|
50
|
+
: previous!.revision
|
|
51
|
+
const value = {
|
|
52
|
+
id: input.id,
|
|
53
|
+
sourceId: input.sourceId,
|
|
54
|
+
teamId: input.teamId,
|
|
55
|
+
projectId: input.projectId,
|
|
56
|
+
category: input.category,
|
|
57
|
+
revision,
|
|
58
|
+
active: input.failed,
|
|
59
|
+
actorId: input.failed
|
|
60
|
+
? (input.actorId ?? previous?.actorId ?? null)
|
|
61
|
+
: (previous?.actorId ?? null),
|
|
62
|
+
title: input.title,
|
|
63
|
+
message: input.message,
|
|
64
|
+
path: input.path,
|
|
65
|
+
updatedAt: input.at,
|
|
66
|
+
}
|
|
67
|
+
// Repeated observations repair any crash between saving the incident and its recipients.
|
|
68
|
+
await db
|
|
69
|
+
.insert(notificationIncidents)
|
|
70
|
+
.values(value)
|
|
71
|
+
.onConflictDoUpdate({
|
|
72
|
+
target: notificationIncidents.id,
|
|
73
|
+
set: value,
|
|
74
|
+
where: sql`${notificationIncidents.updatedAt} <= ${input.at}`,
|
|
75
|
+
})
|
|
76
|
+
const [incident] = await db
|
|
77
|
+
.select()
|
|
78
|
+
.from(notificationIncidents)
|
|
79
|
+
.where(eq(notificationIncidents.id, input.id))
|
|
80
|
+
.limit(1)
|
|
81
|
+
if (
|
|
82
|
+
!incident ||
|
|
83
|
+
incident.revision !== revision ||
|
|
84
|
+
incident.active !== input.failed
|
|
85
|
+
)
|
|
86
|
+
return
|
|
87
|
+
const recipients = input.failed
|
|
88
|
+
? await notificationRows<{
|
|
89
|
+
id: string
|
|
90
|
+
actor: number
|
|
91
|
+
}>(
|
|
92
|
+
sql`select u.id,case when u.id=${input.actorId ?? incident.actorId ?? ''} then 1 else 0 end as actor from user u join team_members m on m.user_id=u.id
|
|
93
|
+
where m.team_id=${input.teamId} and u.status='active' and (m.role in ('owner','admin') or u.id=${input.actorId ?? incident.actorId ?? ''})`,
|
|
94
|
+
['id', 'actor'],
|
|
95
|
+
)
|
|
96
|
+
: await notificationRows<{ id: string; actor: number }>(
|
|
97
|
+
sql`select user_id as id,actor from notification_outbox where incident_id=${input.id} and revision=${revision} and recovery=0 and delivered_at is not null`,
|
|
98
|
+
['id', 'actor'],
|
|
99
|
+
)
|
|
100
|
+
for (const recipient of recipients) {
|
|
101
|
+
const id = createHash('sha256')
|
|
102
|
+
.update(JSON.stringify([input.id, revision, recipient.id, !input.failed]))
|
|
103
|
+
.digest('hex')
|
|
104
|
+
const [existing] = await db
|
|
105
|
+
.select({ id: notificationOutbox.id })
|
|
106
|
+
.from(notificationOutbox)
|
|
107
|
+
.where(eq(notificationOutbox.id, id))
|
|
108
|
+
.limit(1)
|
|
109
|
+
if (existing) continue
|
|
110
|
+
const [recipientUser] = await db
|
|
111
|
+
.select({ email: user.email })
|
|
112
|
+
.from(user)
|
|
113
|
+
.where(eq(user.id, recipient.id))
|
|
114
|
+
.limit(1)
|
|
115
|
+
if (!recipientUser) continue
|
|
116
|
+
const subject = input.failed
|
|
117
|
+
? incident.title
|
|
118
|
+
: `Recovered: ${incident.title}`
|
|
119
|
+
const message = input.failed
|
|
120
|
+
? incident.message
|
|
121
|
+
: 'The previously reported issue has been resolved.'
|
|
122
|
+
const url = new URL(incident.path, config.DEVPUSH_BASE_URL).toString()
|
|
123
|
+
const payload = {
|
|
124
|
+
from,
|
|
125
|
+
to: recipientUser.email,
|
|
126
|
+
subject,
|
|
127
|
+
text: `${message}\n\nView details: ${url}`,
|
|
128
|
+
html: await renderNotificationEmail({
|
|
129
|
+
to: recipientUser.email,
|
|
130
|
+
title: subject,
|
|
131
|
+
message,
|
|
132
|
+
action: { label: 'View details', url },
|
|
133
|
+
}),
|
|
134
|
+
}
|
|
135
|
+
await db
|
|
136
|
+
.insert(notificationOutbox)
|
|
137
|
+
.values({
|
|
138
|
+
id,
|
|
139
|
+
payload,
|
|
140
|
+
actor: Boolean(recipient.actor),
|
|
141
|
+
incidentId: input.id,
|
|
142
|
+
revision,
|
|
143
|
+
userId: recipient.id,
|
|
144
|
+
recovery: !input.failed,
|
|
145
|
+
createdAt: Date.now(),
|
|
146
|
+
})
|
|
147
|
+
.onConflictDoNothing()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function eligible(
|
|
152
|
+
userId: string,
|
|
153
|
+
incident: typeof notificationIncidents.$inferSelect,
|
|
154
|
+
actor: boolean,
|
|
155
|
+
) {
|
|
156
|
+
const [member] = await notificationRows<{
|
|
157
|
+
email: string
|
|
158
|
+
}>(
|
|
159
|
+
sql`select u.email from user u
|
|
160
|
+
join team_members m on m.user_id=u.id join teams t on t.id=m.team_id
|
|
161
|
+
where u.id=${userId} and u.status='active' and t.id=${incident.teamId} and t.status='active'
|
|
162
|
+
and (m.role in ('owner','admin') or ${actor ? 1 : 0}=1)
|
|
163
|
+
and (${incident.projectId} is null or exists(select 1 from projects p where p.id=${incident.projectId} and p.team_id=t.id and p.status='active'))
|
|
164
|
+
and not exists(select 1 from notification_mutes n where n.user_id=u.id and n.team_id=t.id and (n.project_id is null or n.project_id=${incident.projectId})) limit 1`,
|
|
165
|
+
['email'],
|
|
166
|
+
)
|
|
167
|
+
if (!member) return null
|
|
168
|
+
if (incident.projectId === null) {
|
|
169
|
+
const [server] = await notificationRows<{ id: string }>(
|
|
170
|
+
sql`select id from servers where id=${incident.sourceId} and status in ('connected','disconnected')`,
|
|
171
|
+
['id'],
|
|
172
|
+
)
|
|
173
|
+
if (!server) return null
|
|
174
|
+
}
|
|
175
|
+
if (
|
|
176
|
+
incident.id.startsWith('deployment:') ||
|
|
177
|
+
incident.id.startsWith('backup:')
|
|
178
|
+
) {
|
|
179
|
+
const [environment] = await notificationRows<{ id: string }>(
|
|
180
|
+
sql`select id from environments where id=${incident.sourceId} and status='active'`,
|
|
181
|
+
['id'],
|
|
182
|
+
)
|
|
183
|
+
if (!environment) return null
|
|
184
|
+
}
|
|
185
|
+
const preference = await readNotificationPreferences(userId)
|
|
186
|
+
if (!preference.preferences[incident.category as NotificationCategory])
|
|
187
|
+
return null
|
|
188
|
+
return member.email
|
|
189
|
+
}
|
|
190
|
+
export async function deliverOperationalNotifications(
|
|
191
|
+
limit = 50,
|
|
192
|
+
initialNow = Date.now(),
|
|
193
|
+
) {
|
|
194
|
+
if (!config.EMAIL_FROM || !config.RESEND_API_KEY) return
|
|
195
|
+
const startedAt = Date.now()
|
|
196
|
+
const deadline = Date.now() + 15_000
|
|
197
|
+
for (let index = 0; index < limit && Date.now() < deadline; index++) {
|
|
198
|
+
const now = initialNow + Date.now() - startedAt
|
|
199
|
+
const lease = randomUUID()
|
|
200
|
+
const [row] = await db
|
|
201
|
+
.update(notificationOutbox)
|
|
202
|
+
.set({
|
|
203
|
+
leaseOwner: lease,
|
|
204
|
+
leaseUntil: sql`cast(unixepoch('subsec') * 1000 as integer) + 300000`,
|
|
205
|
+
attempts: sql`${notificationOutbox.attempts}+1`,
|
|
206
|
+
})
|
|
207
|
+
.where(
|
|
208
|
+
sql`${notificationOutbox.id} = (select id from notification_outbox where delivered_at is null and discarded_at is null and failed_at is null and next_attempt_at<=${now} and lease_until<=cast(unixepoch('subsec') * 1000 as integer) order by created_at,id limit 1) and ${notificationOutbox.leaseUntil}<=cast(unixepoch('subsec') * 1000 as integer)`,
|
|
209
|
+
)
|
|
210
|
+
.returning()
|
|
211
|
+
if (!row) break
|
|
212
|
+
const [incident] = await db
|
|
213
|
+
.select()
|
|
214
|
+
.from(notificationIncidents)
|
|
215
|
+
.where(eq(notificationIncidents.id, row.incidentId))
|
|
216
|
+
.limit(1)
|
|
217
|
+
const email =
|
|
218
|
+
incident &&
|
|
219
|
+
incident.revision === row.revision &&
|
|
220
|
+
incident.active !== row.recovery
|
|
221
|
+
? await eligible(row.userId, incident, row.actor)
|
|
222
|
+
: null
|
|
223
|
+
const ownership = and(
|
|
224
|
+
eq(notificationOutbox.id, row.id),
|
|
225
|
+
eq(notificationOutbox.leaseOwner, lease),
|
|
226
|
+
sql`${notificationOutbox.leaseUntil} > cast(unixepoch('subsec') * 1000 as integer)`,
|
|
227
|
+
)
|
|
228
|
+
if (!incident || !email || email !== row.payload.to) {
|
|
229
|
+
await db
|
|
230
|
+
.update(notificationOutbox)
|
|
231
|
+
.set({ discardedAt: now, leaseOwner: null, leaseUntil: 0 })
|
|
232
|
+
.where(ownership)
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
if (
|
|
236
|
+
row.attempts > 10 ||
|
|
237
|
+
(row.firstAttemptAt !== null && now - row.firstAttemptAt >= 86_400_000)
|
|
238
|
+
) {
|
|
239
|
+
await db
|
|
240
|
+
.update(notificationOutbox)
|
|
241
|
+
.set({ failedAt: now, leaseOwner: null, leaseUntil: 0 })
|
|
242
|
+
.where(ownership)
|
|
243
|
+
console.error('Notification email needs operator review.', {
|
|
244
|
+
notificationId: row.id,
|
|
245
|
+
})
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
// Persist the complete provider payload and window before the first call.
|
|
250
|
+
const payload = row.payload
|
|
251
|
+
const held = await db
|
|
252
|
+
.update(notificationOutbox)
|
|
253
|
+
.set({
|
|
254
|
+
payload,
|
|
255
|
+
firstAttemptAt: row.firstAttemptAt ?? now,
|
|
256
|
+
})
|
|
257
|
+
.where(ownership)
|
|
258
|
+
.returning({ id: notificationOutbox.id })
|
|
259
|
+
if (!held.length) continue
|
|
260
|
+
await sendEmail(payload, `notification-${row.id}`)
|
|
261
|
+
await db
|
|
262
|
+
.update(notificationOutbox)
|
|
263
|
+
.set({ deliveredAt: now, leaseOwner: null, leaseUntil: 0 })
|
|
264
|
+
.where(ownership)
|
|
265
|
+
} catch {
|
|
266
|
+
await db
|
|
267
|
+
.update(notificationOutbox)
|
|
268
|
+
.set({
|
|
269
|
+
nextAttemptAt:
|
|
270
|
+
now + Math.min(3_600_000, 60_000 * 2 ** (row.attempts - 1)),
|
|
271
|
+
leaseOwner: null,
|
|
272
|
+
leaseUntil: 0,
|
|
273
|
+
})
|
|
274
|
+
.where(ownership)
|
|
275
|
+
console.warn('Notification email pending; retry persisted.', {
|
|
276
|
+
notificationId: row.id,
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function pruneOperationalNotifications(now = Date.now()) {
|
|
283
|
+
const before = now - 30 * 86_400_000
|
|
284
|
+
await db
|
|
285
|
+
.delete(notificationOutbox)
|
|
286
|
+
.where(
|
|
287
|
+
sql`${notificationOutbox.createdAt}<${before} and exists(select 1 from notification_incidents i where i.id=${notificationOutbox.incidentId} and i.revision<>${notificationOutbox.revision})`,
|
|
288
|
+
)
|
|
289
|
+
await db.delete(notificationIncidents).where(
|
|
290
|
+
sql`${notificationIncidents.updatedAt}<${before} and (${notificationIncidents.active}=0 or ${notificationIncidents.id} like 'restore:%'
|
|
291
|
+
or (${notificationIncidents.projectId} is not null and not exists(select 1 from projects p where p.id=${notificationIncidents.projectId} and p.status='active'))
|
|
292
|
+
or (${notificationIncidents.projectId} is null and not exists(select 1 from servers s where s.id=${notificationIncidents.sourceId} and s.status<>'deleted'))
|
|
293
|
+
or (${notificationIncidents.id} like 'domain:%' and not exists(select 1 from domains d where d.id=${notificationIncidents.sourceId}))
|
|
294
|
+
or ((${notificationIncidents.id} like 'deployment:%' or ${notificationIncidents.id} like 'backup:%') and not exists(select 1 from environments e where e.id=${notificationIncidents.sourceId} and e.status='active')))`,
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function repairOperationalRecipients() {
|
|
299
|
+
const source = 'recipients'
|
|
300
|
+
const [cursor] = await db
|
|
301
|
+
.select()
|
|
302
|
+
.from(notificationScanCursors)
|
|
303
|
+
.where(eq(notificationScanCursors.source, source))
|
|
304
|
+
.limit(1)
|
|
305
|
+
const rows = await db
|
|
306
|
+
.select()
|
|
307
|
+
.from(notificationIncidents)
|
|
308
|
+
.where(sql`${notificationIncidents.id}>${cursor?.afterId ?? ''}`)
|
|
309
|
+
.orderBy(notificationIncidents.id)
|
|
310
|
+
.limit(100)
|
|
311
|
+
for (const incident of rows) {
|
|
312
|
+
if (incident.updatedAt >= Date.now() - 86_400_000)
|
|
313
|
+
await observeOperationalNotification({
|
|
314
|
+
id: incident.id,
|
|
315
|
+
sourceId: incident.sourceId,
|
|
316
|
+
teamId: incident.teamId,
|
|
317
|
+
projectId: incident.projectId,
|
|
318
|
+
category: incident.category as NotificationCategory,
|
|
319
|
+
failed: incident.active,
|
|
320
|
+
revision: incident.revision,
|
|
321
|
+
at: incident.updatedAt,
|
|
322
|
+
title: incident.title,
|
|
323
|
+
message: incident.message,
|
|
324
|
+
path: incident.path,
|
|
325
|
+
actorId: incident.actorId,
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
await db
|
|
329
|
+
.insert(notificationScanCursors)
|
|
330
|
+
.values({ source, afterId: rows.length === 100 ? rows.at(-1)!.id : '' })
|
|
331
|
+
.onConflictDoUpdate({
|
|
332
|
+
target: notificationScanCursors.source,
|
|
333
|
+
set: { afterId: rows.length === 100 ? rows.at(-1)!.id : '' },
|
|
334
|
+
})
|
|
335
|
+
}
|