From 9cda4eed85a8ba2af0d1ab213bdcf4a714525de6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Jul 2026 14:58:37 +0000 Subject: [PATCH 1/7] feat(builder): add dedicated native builder request and worker UI Let orgs request a dedicated native builder, track provisioning status, prefer that pool for builds with shared fallback, and surface worker state in organization settings and build history. Co-authored-by: Martin DONADIEU --- cli/src/types/supabase.types.ts | 75 +++ cloudflare_workers/api/index.ts | 2 + messages/en.json | 67 ++- .../dashboard/DedicatedBuilderBanner.vue | 106 ++++ src/components/tables/BuildTable.vue | 27 + src/constants/organizationTabs.ts | 2 + src/layouts/settings.vue | 11 + src/pages/app/[app].builds.vue | 2 + .../organization/DedicatedBuilder.vue | 523 ++++++++++++++++++ src/pages/settings/organization/Plans.vue | 1 + src/services/dedicatedBuilder.ts | 102 ++++ src/types/supabase.types.ts | 75 +++ .../_backend/private/dedicated_builder.ts | 334 +++++++++++ .../_backend/public/build/request.ts | 42 +- .../_backend/utils/dedicated_builder.ts | 128 +++++ .../_backend/utils/supabase.types.ts | 75 +++ supabase/functions/private/index.ts | 2 + ...0260723144749_dedicated_native_builder.sql | 139 +++++ tests/builder-payload.unit.test.ts | 42 ++ tests/dedicated-builder.unit.test.ts | 65 +++ 20 files changed, 1816 insertions(+), 4 deletions(-) create mode 100644 src/components/dashboard/DedicatedBuilderBanner.vue create mode 100644 src/pages/settings/organization/DedicatedBuilder.vue create mode 100644 src/services/dedicatedBuilder.ts create mode 100644 supabase/functions/_backend/private/dedicated_builder.ts create mode 100644 supabase/functions/_backend/utils/dedicated_builder.ts create mode 100644 supabase/migrations/20260723144749_dedicated_native_builder.sql create mode 100644 tests/dedicated-builder.unit.test.ts diff --git a/cli/src/types/supabase.types.ts b/cli/src/types/supabase.types.ts index cf1785bbd5..3a1efd9917 100644 --- a/cli/src/types/supabase.types.ts +++ b/cli/src/types/supabase.types.ts @@ -591,6 +591,7 @@ export type Database = { build_config: Json | null build_mode: string builder_job_id: string | null + builder_pool: string | null created_at: string id: string last_error: string | null @@ -611,6 +612,7 @@ export type Database = { build_config?: Json | null build_mode?: string builder_job_id?: string | null + builder_pool?: string | null created_at?: string id?: string last_error?: string | null @@ -631,6 +633,7 @@ export type Database = { build_config?: Json | null build_mode?: string builder_job_id?: string | null + builder_pool?: string | null created_at?: string id?: string last_error?: string | null @@ -1371,6 +1374,78 @@ export type Database = { } Relationships: [] } + + dedicated_builders: { + Row: { + activated_at: string | null + allow_shared_fallback: boolean + cancelled_at: string | null + created_at: string + id: string + monthly_builds_estimate: number | null + org_id: string + platforms: string[] + pool_id: string | null + requested_by: string | null + status: string + suspended_at: string | null + updated_at: string + use_case: string | null + worker_current_job_id: string | null + worker_last_seen_at: string | null + worker_name: string | null + worker_status: string + } + Insert: { + activated_at?: string | null + allow_shared_fallback?: boolean + cancelled_at?: string | null + created_at?: string + id?: string + monthly_builds_estimate?: number | null + org_id: string + platforms?: string[] + pool_id?: string | null + requested_by?: string | null + status?: string + suspended_at?: string | null + updated_at?: string + use_case?: string | null + worker_current_job_id?: string | null + worker_last_seen_at?: string | null + worker_name?: string | null + worker_status?: string + } + Update: { + activated_at?: string | null + allow_shared_fallback?: boolean + cancelled_at?: string | null + created_at?: string + id?: string + monthly_builds_estimate?: number | null + org_id?: string + platforms?: string[] + pool_id?: string | null + requested_by?: string | null + status?: string + suspended_at?: string | null + updated_at?: string + use_case?: string | null + worker_current_job_id?: string | null + worker_last_seen_at?: string | null + worker_name?: string | null + worker_status?: string + } + Relationships: [ + { + foreignKeyName: "dedicated_builders_org_id_fkey" + columns: ["org_id"] + isOneToOne: true + referencedRelation: "orgs" + referencedColumns: ["id"] + }, + ] + } deploy_history: { Row: { app_id: string diff --git a/cloudflare_workers/api/index.ts b/cloudflare_workers/api/index.ts index e04f7391aa..5fde4a4f7b 100644 --- a/cloudflare_workers/api/index.ts +++ b/cloudflare_workers/api/index.ts @@ -11,6 +11,7 @@ import { app as config } from '../../supabase/functions/_backend/private/config. import { app as configBuilder } from '../../supabase/functions/_backend/private/config_builder.ts' import { app as create_device } from '../../supabase/functions/_backend/private/create_device.ts' import { app as credits } from '../../supabase/functions/_backend/private/credits.ts' +import { app as dedicated_builder } from '../../supabase/functions/_backend/private/dedicated_builder.ts' import { app as deleted_failed_version } from '../../supabase/functions/_backend/private/delete_failed_version.ts' import { app as devices_priv } from '../../supabase/functions/_backend/private/devices.ts' import { app as events } from '../../supabase/functions/_backend/private/events.ts' @@ -113,6 +114,7 @@ const functionNamePrivate = 'private' const appPrivate = createHono(functionNamePrivate, version) appPrivate.route('/plans', plans) appPrivate.route('/credits', credits) +appPrivate.route('/dedicated_builder', dedicated_builder) appPrivate.route('/store_top', storeTop) appPrivate.route('/website_stats', publicStats) appPrivate.route('/config', config) diff --git a/messages/en.json b/messages/en.json index 9555f3565b..9819c60d3f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2559,7 +2559,6 @@ "error-categories": "Error categories", "error-share": "{share} of errors", "errors-in-period": "Errors in period", - "events": "Events", "failed-to-fetch-log-insights": "Failed to fetch log insights", "log-insights": "Insights", "log-insights-period-help": "Error categories, affected devices, and trends use this period.", @@ -2573,5 +2572,69 @@ "version-count": "Versions", "view-action-logs": "View action logs", "view-logs": "View logs", - "last-seen": "Last seen" + "last-seen": "Last seen", + "dedicated-builder": "Dedicated builder", + "dedicated-builder-page-subtitle": "Request a Capgo native builder worker dedicated to your organization.", + "dedicated-builder-hero-title": "Your own native build worker", + "dedicated-builder-hero-desc": "Skip the shared queue with a dedicated builder pool. When your worker is busy, builds can fall back to Capgo's shared workers.", + "dedicated-builder-benefit-queue": "Priority on your own pool", + "dedicated-builder-benefit-worker": "One worker reserved for your org", + "dedicated-builder-benefit-fallback": "Shared pool fallback when busy", + "dedicated-builder-request-title": "Request a dedicated builder", + "dedicated-builder-use-case": "What will you use it for?", + "dedicated-builder-use-case-placeholder": "e.g. CI builds for iOS/Android releases every day", + "dedicated-builder-monthly-estimate": "Estimated native builds per month", + "dedicated-builder-monthly-estimate-placeholder": "e.g. 40", + "dedicated-builder-platforms": "Platforms", + "dedicated-builder-platforms-required": "Select at least one platform", + "dedicated-builder-request-cta": "Request dedicated builder", + "dedicated-builder-need-permission": "Why can't I request this?", + "dedicated-builder-request-success": "Dedicated builder requested. We'll provision it and notify you.", + "dedicated-builder-request-error": "Could not submit the dedicated builder request", + "dedicated-builder-already-exists": "A dedicated builder request already exists for this organization", + "dedicated-builder-load-error": "Could not load dedicated builder status", + "dedicated-builder-requested-title": "Request received", + "dedicated-builder-requested-desc": "Our team is reviewing your request and will provision a dedicated worker for your organization.", + "dedicated-builder-provisioning-title": "Provisioning your worker", + "dedicated-builder-provisioning-desc": "Your dedicated builder is being set up. Builds will start using it once it becomes active.", + "dedicated-builder-requested-at": "Requested on {date}", + "dedicated-builder-step-requested": "Request submitted", + "dedicated-builder-step-provisioning": "Worker provisioning", + "dedicated-builder-step-active": "Dedicated pool active", + "dedicated-builder-cancel-cta": "Cancel request", + "dedicated-builder-cancel-title": "Cancel dedicated builder request?", + "dedicated-builder-cancel-description": "You can request a dedicated builder again later.", + "dedicated-builder-cancel-confirm": "Cancel request", + "dedicated-builder-cancel-success": "Dedicated builder request cancelled", + "dedicated-builder-cancel-error": "Could not cancel the dedicated builder request", + "dedicated-builder-your-worker": "Your dedicated worker", + "dedicated-builder-active-desc": "New native builds prefer your dedicated worker. If it is busy or offline, they can use Capgo shared workers when fallback is enabled.", + "dedicated-builder-worker-unknown": "Unknown", + "dedicated-builder-worker-idle": "Idle", + "dedicated-builder-worker-busy": "Busy", + "dedicated-builder-worker-offline": "Offline", + "dedicated-builder-pool-id": "Pool ID", + "dedicated-builder-pool-pending": "Assigned at activation", + "dedicated-builder-active-builds": "Active dedicated builds", + "dedicated-builder-activated-at": "Activated", + "dedicated-builder-fallback-title": "Fall back to shared workers", + "dedicated-builder-fallback-desc": "When your dedicated worker is busy or offline, route builds to Capgo's shared pool instead of waiting.", + "dedicated-builder-fallback-updated": "Fallback setting updated", + "dedicated-builder-fallback-error": "Could not update fallback setting", + "dedicated-builder-how-it-works-title": "How routing works", + "dedicated-builder-how-it-works-1": "New builds prefer your dedicated worker first.", + "dedicated-builder-how-it-works-2": "If that worker is already running a build (or offline), Capgo uses the shared pool when fallback is on.", + "dedicated-builder-how-it-works-3": "Build history shows whether a job used your dedicated pool or the shared pool.", + "dedicated-builder-suspended-title": "Dedicated builder suspended", + "dedicated-builder-suspended-desc": "Your dedicated worker is temporarily unavailable. Contact support if you need it reactivated.", + "dedicated-builder-access-required": "Billing access required", + "dedicated-builder-banner-title": "Need a dedicated native builder?", + "dedicated-builder-banner-desc": "Get your own worker pool for faster, isolated native builds — with shared fallback when busy.", + "dedicated-builder-banner-cta": "Set up dedicated builder", + "dedicated-builder-banner-view": "View dedicated builder", + "dedicated-builder-banner-view-status": "View request status", + "builder-pool": "Pool", + "builder-pool-dedicated": "Dedicated", + "builder-pool-shared": "Shared", + "plan-feature-dedicated-builder": "Dedicated native builder available" } diff --git a/src/components/dashboard/DedicatedBuilderBanner.vue b/src/components/dashboard/DedicatedBuilderBanner.vue new file mode 100644 index 0000000000..87f290f406 --- /dev/null +++ b/src/components/dashboard/DedicatedBuilderBanner.vue @@ -0,0 +1,106 @@ + + + diff --git a/src/components/tables/BuildTable.vue b/src/components/tables/BuildTable.vue index 00e2b00a7a..fe6e25651c 100644 --- a/src/components/tables/BuildTable.vue +++ b/src/components/tables/BuildTable.vue @@ -289,6 +289,18 @@ columns.value = [ class: 'truncate max-w-24', displayFunction: (elem: Element) => elem.status, }, + { + label: t('builder-pool'), + key: 'builder_pool', + class: 'truncate max-w-24', + displayFunction: (elem: Element) => { + if (elem.builder_pool === 'dedicated') + return t('builder-pool-dedicated') + if (elem.builder_pool === 'shared') + return t('builder-pool-shared') + return '—' + }, + }, { label: t('error'), key: 'last_error', @@ -362,6 +374,21 @@ watch(showSetupFlow, (newValue) => { {{ element.status }} +