Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions cli/src/types/supabase.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -1371,6 +1374,85 @@ 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: [
Comment thread
cursor[bot] marked this conversation as resolved.
{
foreignKeyName: "dedicated_builders_org_id_fkey"
columns: ["org_id"]
isOneToOne: true
referencedRelation: "orgs"
referencedColumns: ["id"]
},
{
foreignKeyName: "dedicated_builders_requested_by_fkey"
columns: ["requested_by"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
},
]
}
deploy_history: {
Row: {
app_id: string
Expand Down
2 changes: 2 additions & 0 deletions cloudflare_workers/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
67 changes: 65 additions & 2 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -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. Our team will provision it and update this page when it is ready.",
"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 builds preferring dedicated",
"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": "Preferred pool",
"builder-pool-dedicated": "Dedicated",
"builder-pool-shared": "Shared",
"plan-feature-dedicated-builder": "Dedicated native builder available"
}
117 changes: 117 additions & 0 deletions src/components/dashboard/DedicatedBuilderBanner.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<script setup lang="ts">
import type { DedicatedBuilder } from '~/services/dedicatedBuilder'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import IconServer from '~icons/heroicons/server-stack'
import { DedicatedBuilderApiError, fetchDedicatedBuilder } from '~/services/dedicatedBuilder'
import { useOrganizationStore } from '~/stores/organization'

const props = defineProps<{
appId: string
/** When true, show even if the org already has a pending/active dedicated builder. */
force?: boolean
}>()

const { t } = useI18n()
const router = useRouter()
const organizationStore = useOrganizationStore()

const dedicatedBuilder = ref<DedicatedBuilder | null>(null)
const loaded = ref(false)
const loadFailed = ref(false)
let reqToken = 0

const visible = computed(() => {
if (!loaded.value || loadFailed.value)
return false
if (props.force)
return true
// Teaser when they don't have a dedicated builder yet, or it's cancelled.
return !dedicatedBuilder.value || dedicatedBuilder.value.status === 'cancelled'
})

const ctaLabel = computed(() => {
if (dedicatedBuilder.value?.status === 'active')
return t('dedicated-builder-banner-view')
if (dedicatedBuilder.value?.status === 'requested' || dedicatedBuilder.value?.status === 'provisioning')
return t('dedicated-builder-banner-view-status')
return t('dedicated-builder-banner-cta')
})

async function load() {
const token = ++reqToken
loaded.value = false
loadFailed.value = false
await organizationStore.awaitInitialLoad()
// App URLs can belong to an org other than the currently selected one.
const orgId = organizationStore.getOrgByAppId(props.appId)?.gid
if (!orgId) {
if (token !== reqToken)
return
dedicatedBuilder.value = null
loadFailed.value = true
loaded.value = true
return
}
try {
const row = await fetchDedicatedBuilder(orgId)
if (token !== reqToken)
return
dedicatedBuilder.value = row
}
catch (error) {
if (token !== reqToken)
return
// Auth/billing failures and transient API errors must not look like "no builder".
dedicatedBuilder.value = null
loadFailed.value = true
if (!(error instanceof DedicatedBuilderApiError && error.status === 403))
console.error('[DedicatedBuilderBanner] failed to load dedicated builder', error)
}
finally {
if (token === reqToken)
loaded.value = true
}
}

watch(
() => [props.appId, organizationStore.currentOrganization?.gid] as const,
() => {
load()
},
{ immediate: true },
)

function goToDedicatedBuilder() {
router.push('/settings/organization/dedicated-builder')
}
</script>

<template>
<div
v-if="visible"
class="flex flex-col gap-3 p-4 mb-6 border rounded-xl sm:flex-row sm:items-center sm:justify-between border-azure-200 dark:border-azure-900/50 bg-gradient-to-r from-sky-50 to-slate-50 dark:from-slate-900 dark:to-slate-800"
>
<div class="flex items-start gap-3">
<div class="flex items-center justify-center w-9 h-9 rounded-lg bg-azure-500/10 text-azure-600 dark:text-azure-400 shrink-0">
<IconServer class="w-5 h-5" />
</div>
<div>
<p class="text-sm font-semibold text-slate-900 dark:text-white">
{{ t('dedicated-builder-banner-title') }}
</p>
<p class="mt-0.5 text-sm text-slate-600 dark:text-slate-300">
{{ t('dedicated-builder-banner-desc') }}
</p>
</div>
</div>
<button
type="button"
class="d-btn d-btn-sm d-btn-primary shrink-0"
@click="goToDedicatedBuilder"
>
{{ ctaLabel }}
</button>
</div>
</template>
Loading
Loading