-
-
Notifications
You must be signed in to change notification settings - Fork 131
feat(admin): builder pool free/used capacity #2901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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() | ||||||||
|
|
@@ -241,6 +264,37 @@ 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')) || null | ||||||||
| } | ||||||||
| catch (error) { | ||||||||
| console.error('[Admin Builder] Error loading builder capacity:', error) | ||||||||
| capacity.value = null | ||||||||
| } | ||||||||
| finally { | ||||||||
| isLoadingCapacity.value = false | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| 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(() => capacityHourlySeries.value.some(s => s.data.some(p => p.value > 0))) | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: A valid period with zero workers is treated as no data because the chart guard requires a positive value, hiding the zero-capacity/outage timeline and contradicting the presence of capacity events. Base this guard on event/run presence (or equivalent metadata), not on a positive plotted value. Prompt for AI agents
Suggested change
|
||||||||
|
|
||||||||
| // ---- builder onboarding analytics (builder_analytics) ---- | ||||||||
| const isLoadingData = ref(false) | ||||||||
| const data = ref<BuilderAnalytics | null>(null) | ||||||||
|
|
@@ -349,7 +403,7 @@ async function spoof(orgId: string) { | |||||||
|
|
||||||||
| // ---- shared lifecycle ---- | ||||||||
| async function loadAll() { | ||||||||
| await Promise.all([loadGlobalStatsTrend(), loadData()]) | ||||||||
| await Promise.all([loadCapacity(), loadGlobalStatsTrend(), loadData()]) | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: A slow or unreachable builder request now keeps the entire Builder page behind Prompt for AI agents
Suggested change
|
||||||||
| } | ||||||||
|
|
||||||||
| function sendNonAdminBack() { | ||||||||
|
|
@@ -388,6 +442,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="capacityLive?.free ?? 0" | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When the builder is unavailable, every live card shows Prompt for AI agents |
||||||||
| color-class="text-emerald-500" | ||||||||
| :is-loading="isLoadingCapacity" | ||||||||
| :subtitle="capacityLive?.builder_reachable ? `${capacityLive?.workers_online ?? 0} online` : 'Builder unreachable'" | ||||||||
| /> | ||||||||
| <AdminStatsCard | ||||||||
| title="Running builders" | ||||||||
| :value="capacityLive?.used ?? 0" | ||||||||
| color-class="text-red-500" | ||||||||
| :is-loading="isLoadingCapacity" | ||||||||
| subtitle="Busy online runners" | ||||||||
| /> | ||||||||
| <AdminStatsCard | ||||||||
| title="Online workers" | ||||||||
| :value="capacityLive?.workers_online ?? 0" | ||||||||
| color-class="text-[#119eff]" | ||||||||
| :is-loading="isLoadingCapacity" | ||||||||
| :subtitle="`${capacityLive?.workers_total ?? 0} registered`" | ||||||||
| /> | ||||||||
| <AdminStatsCard | ||||||||
| title="Waiting jobs" | ||||||||
| :value="capacityLive?.waiting ?? 0" | ||||||||
| color-class="text-amber-500" | ||||||||
| :is-loading="isLoadingCapacity" | ||||||||
| subtitle="Queued for a runner" | ||||||||
| /> | ||||||||
| <AdminStatsCard | ||||||||
| title="Offline workers" | ||||||||
| :value="capacityLive?.offline ?? 0" | ||||||||
| color-class="text-slate-500" | ||||||||
| :is-loading="isLoadingCapacity" | ||||||||
| subtitle="Registered but offline" | ||||||||
| /> | ||||||||
| </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 or build intervals in this period yet" | ||||||||
| > | ||||||||
| <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 | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ import { | |
| TERMINAL_BUILD_STATUSES, | ||
| } from '../utils/build_timeout.ts' | ||
| import { emitBuildTransitionEvent } from '../utils/build_tracking.ts' | ||
| import { isoFromBuilderTimestamp } from '../utils/builder_capacity.ts' | ||
| import { BRES, middlewareAPISecret } from '../utils/hono.ts' | ||
| import { cloudlog, cloudlogErr } from '../utils/logging.ts' | ||
| import { recordBuildTime, supabaseAdmin } from '../utils/supabase.ts' | ||
|
|
@@ -225,6 +226,8 @@ app.post('/', middlewareAPISecret, async (c) => { | |
| status: effectiveStatus, | ||
| last_error: effectiveError, | ||
| runner_wait_seconds: runnerWaitSeconds, | ||
| started_at: isoFromBuilderTimestamp(builderJob.job.started_at) ?? undefined, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This write defines Prompt for AI agents |
||
| completed_at: isoFromBuilderTimestamp(effectiveCompletedAt) ?? undefined, | ||
| updated_at: new Date().toISOString(), | ||
| }) | ||
| .eq('id', build.id) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: Hourly x-axis labels use formatLocalDateTime which yields a full date+time string (dateStyle 'medium' + timeStyle 'short', e.g. "Aug 5, 2026, 10:00 PM") for every hourly point, while the chart keeps maxRotation: 0 on the x-axis. Over a multi-hour/multi-day range the long labels will crowd and overlap. Consider formatting hour labels shorter (hour-only, e.g. toLocaleTimeString with hour:'numeric'), or allowing x-axis tick rotation/autoSkip for the 'hour' granularity.
Prompt for AI agents