Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
75 changes: 75 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,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: [
Comment thread
cursor[bot] marked this conversation as resolved.
{
foreignKeyName: "dedicated_builders_org_id_fkey"
columns: ["org_id"]
isOneToOne: true
referencedRelation: "orgs"
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. We'll provision it and notify you.",
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
"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"
}
106 changes: 106 additions & 0 deletions src/components/dashboard/DedicatedBuilderBanner.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<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 { fetchDedicatedBuilder } from '~/services/dedicatedBuilder'
import { useOrganizationStore } from '~/stores/organization'

const props = defineProps<{
/** 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)
let reqToken = 0

const visible = computed(() => {
if (!loaded.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
await organizationStore.awaitInitialLoad()
const orgId = organizationStore.currentOrganization?.gid
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
if (!orgId) {
dedicatedBuilder.value = null
loaded.value = true
return
}
try {
const row = await fetchDedicatedBuilder(orgId)
if (token !== reqToken)
return
dedicatedBuilder.value = row
}
catch {
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
if (token !== reqToken)
return
dedicatedBuilder.value = null
}
finally {
if (token === reqToken)
loaded.value = true
}
}

watch(
() => organizationStore.currentOrganization?.gid,
() => {
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>
27 changes: 27 additions & 0 deletions src/components/tables/BuildTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -362,6 +374,21 @@ watch(showSetupFlow, (newValue) => {
{{ element.status }}
</span>
</template>
<template #builder_pool="{ element }">
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
<span
v-if="element.builder_pool === 'dedicated'"
class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-azure-500/10 text-azure-700 dark:text-azure-300"
>
{{ t('builder-pool-dedicated') }}
</span>
<span
v-else-if="element.builder_pool === 'shared'"
class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300"
>
{{ t('builder-pool-shared') }}
</span>
<span v-else class="text-gray-400 dark:text-gray-600">—</span>
</template>
<template #last_error="{ element }">
<div v-if="element.last_error" class="flex items-center gap-2">
<span class="max-w-xs text-red-600 truncate dark:text-red-400">
Expand Down
2 changes: 2 additions & 0 deletions src/constants/organizationTabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import IconPlan from '~icons/heroicons/credit-card'
import IconCredits from '~icons/heroicons/currency-dollar'
import IconWebhook from '~icons/heroicons/globe-alt'
import IconInfo from '~icons/heroicons/information-circle'
import IconServer from '~icons/heroicons/server-stack'
import IconSecurity from '~icons/heroicons/shield-check'
import IconUsers from '~icons/heroicons/users'

Expand All @@ -14,6 +15,7 @@ export const organizationTabs: Tab[] = [
{ label: 'groups', key: '/settings/organization/groups', icon: IconUsers },
{ label: 'plans', key: '/settings/organization/plans', icon: IconPlan },
{ label: 'credits', key: '/settings/organization/credits', icon: IconCredits },
{ label: 'dedicated-builder', key: '/settings/organization/dedicated-builder', icon: IconServer },
{ label: 'security', key: '/settings/organization/security', icon: IconSecurity },
{ label: 'usage', key: '/settings/organization/usage', icon: IconChart },
{ label: 'audit-logs', key: '/settings/organization/auditlogs', icon: IconAudit },
Expand Down
11 changes: 11 additions & 0 deletions src/layouts/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,17 @@ watchEffect(() => {
if (!needsPlans && hasPlans)
organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/plans')

// Dedicated builder - visible to users who can read billing
const needsDedicatedBuilder = canReadBilling.value
const hasDedicatedBuilder = organizationTabs.value.some(tab => tab.key === '/settings/organization/dedicated-builder')
if (needsDedicatedBuilder && !hasDedicatedBuilder) {
const base = baseOrgTabs.find(t => t.key === '/settings/organization/dedicated-builder')
if (base)
organizationTabs.value.push({ ...base })
}
if (!needsDedicatedBuilder && hasDedicatedBuilder)
organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/dedicated-builder')

// Audit logs - visible only to super_admins
const needsAuditLogs = canReadAuditLogs.value
const hasAuditLogs = organizationTabs.value.find(tab => tab.key === '/settings/organization/audit-logs')
Expand Down
2 changes: 2 additions & 0 deletions src/pages/app/[app].builds.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import IconAlertCircle from '~icons/lucide/alert-circle'
import BuildChartControls from '~/components/dashboard/BuildChartControls.vue'
import BuildStatsCard from '~/components/dashboard/BuildStatsCard.vue'
import BuildTimeCard from '~/components/dashboard/BuildTimeCard.vue'
import DedicatedBuilderBanner from '~/components/dashboard/DedicatedBuilderBanner.vue'
import { useSupabase } from '~/services/supabase'
import { useDisplayStore } from '~/stores/display'

Expand Down Expand Up @@ -73,6 +74,7 @@ watchEffect(async () => {
<div class="mt-0 md:mt-8">
<div class="w-full h-full px-0 pt-0 mx-auto mb-8 overflow-y-auto sm:px-6 md:pt-8 lg:px-8 max-w-9xl max-h-fit">
<div v-if="!showingBuildSteps" class="mb-6">
<DedicatedBuilderBanner />
<BuildChartControls
v-model:use-billing-period="useBillingPeriod"
v-model:show-cumulative="showCumulative"
Expand Down
Loading
Loading