Skip to content
Merged
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
2 changes: 0 additions & 2 deletions cli/src/build/prescan/overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
// to warning so the rest of the scan still runs and reports.
import type { Finding, PrescanCheck, Severity } from './types'

export type PrescanOverrideMode = 'skip' | 'warn'

export interface PrescanOverrides {
/** Check ids that must not run. */
skip: Set<string>
Expand Down
9 changes: 7 additions & 2 deletions src/components/admin/AdminMultiLineChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { computed } from 'vue'
import { Line } from 'vue-chartjs'
import { createChartColorWithOpacity, resolveAccessibleChartColor } from '~/services/chartConfig'
import { formatLocalDate, formatLocalMonthYear } from '~/services/date'
import { formatLocalDate, formatLocalMonthYear, formatLocalTime } from '~/services/date'
import { formatNumberValue } from '~/services/formatLocale'

interface DataSeries {
Expand All @@ -34,7 +34,7 @@ const props = defineProps({
default: false,
},
dateGranularity: {
type: String as () => 'day' | 'month',
type: String as () => 'day' | 'month' | 'hour',
default: 'day',
},
valuePrefix: {
Expand Down Expand Up @@ -67,6 +67,11 @@ function formatChartDate(date: string) {
if (formattedMonth)
return formattedMonth
}
if (props.dateGranularity === 'hour') {
Comment thread
riderx marked this conversation as resolved.
const formattedHour = formatLocalTime(date)
if (formattedHour)
return formattedHour
}
return formatLocalDate(date) || date
}

Expand Down
156 changes: 155 additions & 1 deletion src/pages/admin/dashboard/builder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ meta:
</route>

<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import AdminBarChart from '~/components/admin/AdminBarChart.vue'
Expand Down Expand Up @@ -100,6 +100,29 @@ interface BuilderAnalytics {
posthog_connected: boolean
}

interface BuilderCapacityLive {
workers_total: number
workers_online: number
used: number
free: number
waiting: number
offline: number
builder_reachable: boolean
}
interface BuilderCapacityHourPoint {
date: string
workers: number
used: number
free: number
waiting: number
}
interface BuilderCapacity {
live: BuilderCapacityLive
hourly: BuilderCapacityHourPoint[]
capacity_events: number
runs_sampled: number
}

const { t } = useI18n()
const displayStore = useDisplayStore()
const mainStore = useMainStore()
Expand Down Expand Up @@ -241,6 +264,50 @@ function buildPeriodSubtitle(stats: { builds: number, days: number, totalSeconds
return `${formatNumberValue(stats.builds)} builds across ${formatNumberValue(stats.days)} active days, ${formatTotalSeconds(stats.totalSeconds)} total in selected period`
}

// ---- builder capacity (live pool + hourly free/used) ----
const isLoadingCapacity = ref(false)
const capacity = ref<BuilderCapacity | null>(null)

async function loadCapacity() {
isLoadingCapacity.value = true
try {
capacity.value = (await adminStore.fetchStats('builder_capacity', true)) || null
}
catch (error) {
console.error('[Admin Builder] Error loading builder capacity:', error)
capacity.value = null
}
finally {
isLoadingCapacity.value = false
}
}
Comment thread
riderx marked this conversation as resolved.

const capacityLive = computed(() => capacity.value?.live)
const capacityHourlySeries = computed(() => {
const hourly = capacity.value?.hourly ?? []
if (!hourly.length)
return []
return [
{ label: 'Workers', color: '#64748b', data: hourly.map(d => ({ date: d.date, value: d.workers })) },
{ label: 'Used', color: '#ef4444', data: hourly.map(d => ({ date: d.date, value: d.used })) },
{ label: 'Free', color: '#10b981', data: hourly.map(d => ({ date: d.date, value: d.free })) },
]
})
const hasCapacityHourly = computed(() => {
const c = capacity.value
if (!c)
return false
// Show the series even when all values are 0 (outage / empty pool), as long as
// we have capacity events or run intervals for the selected period.
return c.hourly.length > 0 && (c.capacity_events > 0 || c.runs_sampled > 0)
})

function liveMetric(value: number | undefined): string | number {
if (!capacityLive.value?.builder_reachable)
return '—'
return value ?? 0
}

// ---- builder onboarding analytics (builder_analytics) ----
const isLoadingData = ref(false)
const data = ref<BuilderAnalytics | null>(null)
Expand Down Expand Up @@ -348,7 +415,26 @@ async function spoof(orgId: string) {
}

// ---- shared lifecycle ----
const CAPACITY_POLL_MS = 30_000
let capacityPollTimer: ReturnType<typeof setInterval> | null = null

function startCapacityPolling() {
stopCapacityPolling()
capacityPollTimer = setInterval(() => {
void loadCapacity()
}, CAPACITY_POLL_MS)
}

function stopCapacityPolling() {
if (!capacityPollTimer)
return
clearInterval(capacityPollTimer)
capacityPollTimer = null
}

async function loadAll() {
void loadCapacity()
startCapacityPolling()
await Promise.all([loadGlobalStatsTrend(), loadData()])
}

Expand All @@ -375,6 +461,10 @@ onMounted(async () => {
isLoading.value = false
})

onUnmounted(() => {
stopCapacityPolling()
})

displayStore.NavTitle = t('builder')
displayStore.defaultBack = '/dashboard'
</script>
Expand All @@ -388,6 +478,70 @@ displayStore.defaultBack = '/dashboard'
<PageLoader v-if="isLoading" />

<div v-else class="space-y-6">
<!-- ===================== Live builder capacity ===================== -->
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 xl:grid-cols-5">
<AdminStatsCard
title="Available builders"
:value="liveMetric(capacityLive?.free)"
color-class="text-emerald-500"
:is-loading="isLoadingCapacity"
:subtitle="capacityLive?.builder_reachable ? `${capacityLive?.workers_online ?? 0} online` : 'Builder unreachable'"
/>
<AdminStatsCard
title="Running builders"
:value="liveMetric(capacityLive?.used)"
color-class="text-red-500"
:is-loading="isLoadingCapacity"
:subtitle="capacityLive?.builder_reachable ? 'Busy online runners' : 'Builder unreachable'"
/>
<AdminStatsCard
title="Online workers"
:value="liveMetric(capacityLive?.workers_online)"
color-class="text-[#119eff]"
:is-loading="isLoadingCapacity"
:subtitle="capacityLive?.builder_reachable ? `${capacityLive?.workers_total ?? 0} registered` : 'Builder unreachable'"
/>
<AdminStatsCard
title="Waiting jobs"
:value="liveMetric(capacityLive?.waiting)"
color-class="text-amber-500"
:is-loading="isLoadingCapacity"
:subtitle="capacityLive?.builder_reachable ? 'Queued for a runner' : 'Builder unreachable'"
/>
<AdminStatsCard
title="Offline workers"
:value="liveMetric(capacityLive?.offline)"
color-class="text-slate-500"
:is-loading="isLoadingCapacity"
:subtitle="capacityLive?.builder_reachable ? 'Registered but offline' : 'Builder unreachable'"
/>
</div>

<div class="grid grid-cols-1 gap-6">
<ChartCard
title="Builder usage by hour"
:is-loading="isLoadingCapacity"
:has-data="hasCapacityHourly"
no-data-message="No capacity events yet — open after the builder reports worker +/-"
>
<template #header>
<div class="flex flex-col gap-1">
<h2 class="text-2xl font-semibold leading-tight dark:text-white text-slate-600">
Builder usage by hour
</h2>
<p class="text-xs text-slate-500 dark:text-slate-400">
Free vs used reconstructed from worker +/− events and build start/end intervals
</p>
</div>
</template>
<AdminMultiLineChart
:series="capacityHourlySeries"
:is-loading="isLoadingCapacity"
date-granularity="hour"
/>
</ChartCard>
</div>

<!-- ===================== Build volume overview (global_stats) ===================== -->
<div class="grid grid-cols-1 gap-6">
<ChartCard
Expand Down
2 changes: 1 addition & 1 deletion src/stores/adminDashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '~/services/dateRange'
import { defaultApiHost, useSupabase } from '~/services/supabase'

export type MetricCategory = 'uploads' | 'distribution' | 'failures' | 'success_rate' | 'platform_overview' | 'org_metrics' | 'mau_trend' | 'success_rate_trend' | 'apps_trend' | 'bundles_trend' | 'deployments_trend' | 'storage_trend' | 'bandwidth_trend' | 'global_stats_trend' | 'plugin_breakdown' | 'trial_organizations' | 'trial_plan_breakdown' | 'onboarding_funnel' | 'cancelled_users' | 'email_type_breakdown' | 'customer_country_breakdown' | 'organization_insights' | 'builder_analytics'
export type MetricCategory = 'uploads' | 'distribution' | 'failures' | 'success_rate' | 'platform_overview' | 'org_metrics' | 'mau_trend' | 'success_rate_trend' | 'apps_trend' | 'bundles_trend' | 'deployments_trend' | 'storage_trend' | 'bandwidth_trend' | 'global_stats_trend' | 'plugin_breakdown' | 'trial_organizations' | 'trial_plan_breakdown' | 'onboarding_funnel' | 'cancelled_users' | 'email_type_breakdown' | 'customer_country_breakdown' | 'organization_insights' | 'builder_analytics' | 'builder_capacity'

export type DateRangeMode = DateRangePreset
export const DEFAULT_DATE_RANGE_MODE = DEFAULT_DATE_RANGE_PRESET
Expand Down
30 changes: 30 additions & 0 deletions src/types/supabase.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,20 +584,46 @@ export type Database = {
},
]
}
builder_capacity_events: {
Row: {
created_at: string
delta: number
id: number
source: string
workers_total: number
}
Insert: {
created_at?: string
delta: number
id?: number
source?: string
workers_total: number
}
Update: {
created_at?: string
delta?: number
id?: number
source?: string
workers_total?: number
}
Relationships: []
}
build_requests: {
Row: {
ai_analyzed: boolean
app_id: string
build_config: Json | null
build_mode: string
builder_job_id: string | null
completed_at: string | null
created_at: string
id: string
last_error: string | null
owner_org: string
platform: string
requested_by: string
runner_wait_seconds: number
started_at: string | null
status: string
updated_at: string
upload_expires_at: string
Expand All @@ -611,13 +637,15 @@ export type Database = {
build_config?: Json | null
build_mode?: string
builder_job_id?: string | null
completed_at?: string | null
created_at?: string
id?: string
last_error?: string | null
owner_org: string
platform: string
requested_by: string
runner_wait_seconds?: number
started_at?: string | null
status?: string
updated_at?: string
upload_expires_at: string
Expand All @@ -631,13 +659,15 @@ export type Database = {
build_config?: Json | null
build_mode?: string
builder_job_id?: string | null
completed_at?: string | null
created_at?: string
id?: string
last_error?: string | null
owner_org?: string
platform?: string
requested_by?: string
runner_wait_seconds?: number
started_at?: string | null
status?: string
updated_at?: string
upload_expires_at?: string
Expand Down
10 changes: 8 additions & 2 deletions supabase/functions/_backend/private/admin_stats.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type Stripe from 'stripe'
import type { MiddlewareKeyVariables } from '../utils/hono.ts'
import { z } from 'zod'
import { Hono } from 'hono/tiny'
import { safeParseSchema } from '../utils/schema_validation.ts'
import { z } from 'zod'
import { getAdminBuilderAnalytics } from '../utils/builder_analytics.ts'
import { getAdminBuilderCapacity } from '../utils/builder_capacity.ts'
import { getAdminAppsTrend, getAdminBandwidthTrend, getAdminBundlesTrend, getAdminDistributionMetrics, getAdminFailureMetrics, getAdminMauTrend, getAdminOrgMetrics, getAdminPlatformOverview, getAdminStorageTrend, getAdminSuccessRate, getAdminSuccessRateTrend, getAdminUploadMetrics } from '../utils/cloudflare.ts'
import { parseBody, simpleError, useCors } from '../utils/hono.ts'
import { middlewareAuth } from '../utils/hono_jwt.ts'
import { cloudlog } from '../utils/logging.ts'
import { getAdminCancelledOrganizations, getAdminCustomerCountryBreakdown, getAdminDeploymentsTrend, getAdminEmailTypeBreakdown, getAdminGlobalStatsTrend, getAdminOnboardingFunnel, getAdminOrganizationInsights, getAdminPluginBreakdown, getAdminTrialOrganizations, getAdminTrialPlanBreakdown } from '../utils/pg.ts'
import { safeParseSchema } from '../utils/schema_validation.ts'
import { getCancellationDetails } from '../utils/stripe.ts'
import { supabaseClient as useSupabaseClient } from '../utils/supabase.ts'

Expand Down Expand Up @@ -41,6 +42,7 @@ const metricCategories = [
'customer_country_breakdown',
'organization_insights',
'builder_analytics',
'builder_capacity',
] as const

const isoUtcDatetimeSchema = z.string().refine(
Expand Down Expand Up @@ -313,6 +315,10 @@ app.post('/', middlewareAuth, async (c) => {
result = await getAdminBuilderAnalytics(c, start_date, end_date)
break

case 'builder_capacity':
result = await getAdminBuilderCapacity(c, start_date, end_date)
break

default:
throw simpleError('invalid_metric_category', 'Invalid metric category', { metric_category })
}
Expand Down
Loading
Loading