Skip to content

feat(admin): builder pool free/used capacity - #2901

Open
riderx wants to merge 1 commit into
mainfrom
feat/admin-builder-capacity
Open

feat(admin): builder pool free/used capacity#2901
riderx wants to merge 1 commit into
mainfrom
feat/admin-builder-capacity

Conversation

@riderx

@riderx riderx commented Aug 6, 2026

Copy link
Copy Markdown
Member

Summary (AI generated)

  • Add builder_capacity_events (+/− worker pool size over time) and persist build_requests.started_at / completed_at
  • New admin_stats category builder_capacity: live available/running from builder runners, hourly free/used reconstructed from capacity events + build intervals
  • Admin Builder tab: live cards + hourly free/used/workers chart

Motivation (AI generated)

Need to see current free vs busy builders and historical hourly utilization without cron snapshots — event log of worker count plus existing build start/end is enough to reconstruct the series.

Business Impact (AI generated)

Faster ops visibility into builder saturation and queue pressure, so we can scale MacInCloud runners before customers wait.

Test Plan (AI generated)

  • bun test:unit includes tests/builder-capacity.unit.test.ts and admin_stats schema acceptance
  • bun run supabase:db:reset applies 20260806202503_builder_capacity_events.sql
  • Open /admin/dashboard/builder/ as platform admin — see Available / Running / Online / Waiting / Offline cards
  • Confirm hourly chart populates after capacity sync + builds with start/end
  • With BUILDER_URL unreachable, cards show builder unreachable and chart still uses stored events/logs

Generated with AI

Made with Cursor


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Review in cubic

Track worker pool size as +/- capacity events and persist build
start/end intervals so the admin Builder tab can show live
available/running workers and reconstruct hourly free vs used.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8ae2773c-b066-46c9-8180-3e6f647b3b72

📥 Commits

Reviewing files that changed from the base of the PR and between ec1c4f1 and b23260e.

📒 Files selected for processing (13)
  • src/components/admin/AdminMultiLineChart.vue
  • src/pages/admin/dashboard/builder.vue
  • src/stores/adminDashboard.ts
  • src/types/supabase.types.ts
  • supabase/functions/_backend/private/admin_stats.ts
  • supabase/functions/_backend/public/build/start.ts
  • supabase/functions/_backend/public/build/status.ts
  • supabase/functions/_backend/triggers/cron_reconcile_build_status.ts
  • supabase/functions/_backend/utils/builder_capacity.ts
  • supabase/functions/_backend/utils/supabase.types.ts
  • supabase/migrations/20260806202503_builder_capacity_events.sql
  • tests/admin-stats.unit.test.ts
  • tests/builder-capacity.unit.test.ts

Comment @coderabbitai help to get the list of available commands.

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_1f9dfe4a-93eb-4221-8487-7bcf9c31c638)

@sonarqubecloud

sonarqubecloud Bot commented Aug 6, 2026

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12 issues found across 13 files

Confidence score: 3/5

  • In supabase/functions/_backend/utils/builder_capacity.ts, utilization is currently derived from started_at values that include queue time, and started_at is written with conflicting semantics in supabase/functions/_backend/triggers/cron_reconcile_build_status.ts; this can materially over/under-count true runner usage and mislead capacity decisions—normalize started_at to runner start semantics and enforce interval validity in supabase/migrations/20260806202503_builder_capacity_events.sql.
  • recordBuilderCapacityIfChanged in supabase/functions/_backend/utils/builder_capacity.ts samples only during admin stats requests and uses a non-atomic read-then-write, so missed transitions and race conditions can leave history incomplete or incorrect—persist transitions from builder-side changes and make event writes atomic/idempotent.
  • Long-range reconstruction in supabase/functions/_backend/utils/builder_capacity.ts rescans run intervals per hour and does not clip first/last bins to the requested window, which risks slow/time-out admin queries and inaccurate edge-hour chart points—switch to a sweep-line reconstruction and clip bins to startMs/endMs.
  • In src/pages/admin/dashboard/builder.vue, the full-page loader waits on the external capacity call and unreachable/zero-worker states are rendered as 0 or hidden, so outages can look healthy and delay unrelated dashboard content—decouple page loading from capacity fetch and preserve explicit unavailable/zero-capacity UI states.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/components/admin/AdminMultiLineChart.vue">

<violation number="1" location="src/components/admin/AdminMultiLineChart.vue:70">
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.</violation>
</file>

<file name="supabase/functions/_backend/utils/builder_capacity.ts">

<violation number="1" location="supabase/functions/_backend/utils/builder_capacity.ts:140">
P2: The hourly chart includes run time outside the selected date range for any non-hour-aligned range, which makes the first and last points inaccurate; clipping each bin to `startMs`/`endMs` keeps the reconstruction within the requested period.</violation>

<violation number="2" location="supabase/functions/_backend/utils/builder_capacity.ts:141">
P2: Long-range builder capacity requests repeatedly rescan all build intervals for every hour, making reconstruction O(hours × runs) and risking slow or timed-out admin stats at 6–12 month ranges. A single sweep-line pass or database-side hourly aggregation would preserve the chart while avoiding repeated work.</violation>

<violation number="3" location="supabase/functions/_backend/utils/builder_capacity.ts:291">
P2: recordBuilderCapacityIfChanged performs a non-atomic read-then-write: it SELECTs the latest event to compute `delta`, then INSERTs a new row. When two admin_stats calls (e.g. two open builder tabs, or a poll racing a page load) run concurrently, both read the same `previous` workers_total and both insert a new row with the same absolute `workers_total` but a duplicated, non-zero `delta`. The event log then contains two consecutive rows with an unchanged `workers_total` yet a non-zero delta, corrupting the reconstructed pool series (`workersAt`/delta history) that this whole feature is built on. Consider making the write atomic (e.g. a single INSERT ... ON CONFLICT / advisory lock, or compute the delta from the max(created_at) row within the same transaction) so concurrent writers cannot derive the delta from a stale read.</violation>

<violation number="4" location="supabase/functions/_backend/utils/builder_capacity.ts:384">
P1: Hourly `used` capacity includes queue time rather than only runner execution: `build_requests.started_at` is populated at submission, while `waiting_runner` jobs can remain unassigned. Reconstruct from the builder-reported start (or exclude the pre-run portion) so waiting jobs do not consume worker capacity in the chart.</violation>

<violation number="5" location="supabase/functions/_backend/utils/builder_capacity.ts:450">
P3: In the degraded path (source == 'ok' or builder unreachable) `used` is approximated from `countActiveBuilds`, which counts build_requests in `['starting', 'waiting_runner', 'running']`. A build in `waiting_runner` is queued awaiting a runner, not currently occupying a machine, so including it inflates the live "Used" card and deflates "Free" whenever the pool has queued work. Consider excluding `waiting_runner` from the used estimate (or counting only `running`/`starting`) so the fallback reflects machines actually consumed rather than demand queued behind them.</violation>

<violation number="6" location="supabase/functions/_backend/utils/builder_capacity.ts:455">
P2: Capacity history is sampled only when an admin opens/refreshes the dashboard, so changes between admin stats requests are lost and the stored-event fallback cannot reconstruct those hours. Persist capacity transitions from the builder sync/lifecycle path independently of the read-only stats request.</violation>
</file>

<file name="supabase/migrations/20260806202503_builder_capacity_events.sql">

<violation number="1" location="supabase/migrations/20260806202503_builder_capacity_events.sql:5">
P2: Invalid build intervals can be stored and silently undercount utilization because the reconstruction code discards completions at or before their start. A table check such as `started_at IS NULL OR completed_at IS NULL OR completed_at >= started_at` would keep persisted run intervals valid.</violation>
</file>

<file name="src/pages/admin/dashboard/builder.vue">

<violation number="1" location="src/pages/admin/dashboard/builder.vue:296">
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.</violation>

<violation number="2" location="src/pages/admin/dashboard/builder.vue:406">
P2: A slow or unreachable builder request now keeps the entire Builder page behind `PageLoader` until the external capacity call settles, delaying the rest of the admin dashboard even though capacity has its own loading state. Start capacity loading independently of the initial page readiness so the other dashboard sections can render while the live card/chart is pending.</violation>

<violation number="3" location="src/pages/admin/dashboard/builder.vue:449">
P2: When the builder is unavailable, every live card shows `0` even though the response explicitly says `builder_reachable: false`, making an outage look like a healthy pool with no workers or jobs. Preserve the unreachable state and render an unknown value/degraded label for all live metrics until a valid snapshot is available.</violation>
</file>

<file name="supabase/functions/_backend/triggers/cron_reconcile_build_status.ts">

<violation number="1" location="supabase/functions/_backend/triggers/cron_reconcile_build_status.ts:229">
P2: This write defines `started_at` as the builder-reported run start, but the same column is set elsewhere to the submission wall-clock time: `start.ts` writes `started_at: new Date().toISOString()` when the build advances to `startedStatus`. Because this cron reconciles every non-terminal stale build (~1/min) and overwrites `started_at` even on ticks where no transition occurred, a queued build's stored `started_at` silently changes from its queue-entry time to the later run-start once a runner picks it up. `reconstructHourlyCapacity`/the `builder_capacity` SQL feed "used"/free off these `started_at`/`completed_at` intervals, so the hourly capacity numbers become non-deterministic and depend on which writer ran last and how long the build sat in the queue.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}>(
`WITH request_runs AS (
SELECT
br.started_at,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Hourly used capacity includes queue time rather than only runner execution: build_requests.started_at is populated at submission, while waiting_runner jobs can remain unassigned. Reconstruct from the builder-reported start (or exclude the pre-run portion) so waiting jobs do not consume worker capacity in the chart.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/_backend/utils/builder_capacity.ts, line 384:

<comment>Hourly `used` capacity includes queue time rather than only runner execution: `build_requests.started_at` is populated at submission, while `waiting_runner` jobs can remain unassigned. Reconstruct from the builder-reported start (or exclude the pre-run portion) so waiting jobs do not consume worker capacity in the chart.</comment>

<file context>
@@ -0,0 +1,471 @@
+    }>(
+      `WITH request_runs AS (
+         SELECT
+           br.started_at,
+           br.completed_at,
+           br.created_at,
</file context>

}

const poolSize = live.workers_online > 0 ? live.workers_online : live.workers_total
if (live.builder_reachable)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Capacity history is sampled only when an admin opens/refreshes the dashboard, so changes between admin stats requests are lost and the stored-event fallback cannot reconstruct those hours. Persist capacity transitions from the builder sync/lifecycle path independently of the read-only stats request.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/_backend/utils/builder_capacity.ts, line 455:

<comment>Capacity history is sampled only when an admin opens/refreshes the dashboard, so changes between admin stats requests are lost and the stored-event fallback cannot reconstruct those hours. Persist capacity transitions from the builder sync/lifecycle path independently of the read-only stats request.</comment>

<file context>
@@ -0,0 +1,471 @@
+  }
+
+  const poolSize = live.workers_online > 0 ? live.workers_online : live.workers_total
+  if (live.builder_reachable)
+    await recordBuilderCapacityIfChanged(c, poolSize, `admin_${source}`)
+
</file context>

const hourEnd = t + HOUR_MS
const date = new Date(t).toISOString()
const workers = workersAt(sortedEvents, hourEnd - 1)
const used = maxConcurrentUsed(intervals, t, hourEnd)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Long-range builder capacity requests repeatedly rescan all build intervals for every hour, making reconstruction O(hours × runs) and risking slow or timed-out admin stats at 6–12 month ranges. A single sweep-line pass or database-side hourly aggregation would preserve the chart while avoiding repeated work.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/_backend/utils/builder_capacity.ts, line 141:

<comment>Long-range builder capacity requests repeatedly rescan all build intervals for every hour, making reconstruction O(hours × runs) and risking slow or timed-out admin stats at 6–12 month ranges. A single sweep-line pass or database-side hourly aggregation would preserve the chart while avoiding repeated work.</comment>

<file context>
@@ -0,0 +1,471 @@
+    const hourEnd = t + HOUR_MS
+    const date = new Date(t).toISOString()
+    const workers = workersAt(sortedEvents, hourEnd - 1)
+    const used = maxConcurrentUsed(intervals, t, hourEnd)
+    const free = Math.max(0, workers - used)
+    points.push({
</file context>

for (let t = hourStart; t < endMs; t += HOUR_MS) {
const hourEnd = t + HOUR_MS
const date = new Date(t).toISOString()
const workers = workersAt(sortedEvents, hourEnd - 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The hourly chart includes run time outside the selected date range for any non-hour-aligned range, which makes the first and last points inaccurate; clipping each bin to startMs/endMs keeps the reconstruction within the requested period.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/_backend/utils/builder_capacity.ts, line 140:

<comment>The hourly chart includes run time outside the selected date range for any non-hour-aligned range, which makes the first and last points inaccurate; clipping each bin to `startMs`/`endMs` keeps the reconstruction within the requested period.</comment>

<file context>
@@ -0,0 +1,471 @@
+  for (let t = hourStart; t < endMs; t += HOUR_MS) {
+    const hourEnd = t + HOUR_MS
+    const date = new Date(t).toISOString()
+    const workers = workersAt(sortedEvents, hourEnd - 1)
+    const used = maxConcurrentUsed(intervals, t, hourEnd)
+    const free = Math.max(0, workers - used)
</file context>

-- on build_requests. Hourly free/used is reconstructed from these events —
-- no polling cron snapshots.

ALTER TABLE public.build_requests

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Invalid build intervals can be stored and silently undercount utilization because the reconstruction code discards completions at or before their start. A table check such as started_at IS NULL OR completed_at IS NULL OR completed_at >= started_at would keep persisted run intervals valid.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/migrations/20260806202503_builder_capacity_events.sql, line 5:

<comment>Invalid build intervals can be stored and silently undercount utilization because the reconstruction code discards completions at or before their start. A table check such as `started_at IS NULL OR completed_at IS NULL OR completed_at >= started_at` would keep persisted run intervals valid.</comment>

<file context>
@@ -0,0 +1,65 @@
+-- on build_requests. Hourly free/used is reconstructed from these events —
+-- no polling cron snapshots.
+
+ALTER TABLE public.build_requests
+  ADD COLUMN IF NOT EXISTS started_at timestamp with time zone,
+  ADD COLUMN IF NOT EXISTS completed_at timestamp with time zone;
</file context>

// ---- shared lifecycle ----
async function loadAll() {
await Promise.all([loadGlobalStatsTrend(), loadData()])
await Promise.all([loadCapacity(), loadGlobalStatsTrend(), loadData()])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 PageLoader until the external capacity call settles, delaying the rest of the admin dashboard even though capacity has its own loading state. Start capacity loading independently of the initial page readiness so the other dashboard sections can render while the live card/chart is pending.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/pages/admin/dashboard/builder.vue, line 406:

<comment>A slow or unreachable builder request now keeps the entire Builder page behind `PageLoader` until the external capacity call settles, delaying the rest of the admin dashboard even though capacity has its own loading state. Start capacity loading independently of the initial page readiness so the other dashboard sections can render while the live card/chart is pending.</comment>

<file context>
@@ -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()])
 }
 
</file context>
Suggested change
await Promise.all([loadCapacity(), loadGlobalStatsTrend(), loadData()])
void loadCapacity()
await Promise.all([loadGlobalStatsTrend(), loadData()])

status: effectiveStatus,
last_error: effectiveError,
runner_wait_seconds: runnerWaitSeconds,
started_at: isoFromBuilderTimestamp(builderJob.job.started_at) ?? undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This write defines started_at as the builder-reported run start, but the same column is set elsewhere to the submission wall-clock time: start.ts writes started_at: new Date().toISOString() when the build advances to startedStatus. Because this cron reconciles every non-terminal stale build (~1/min) and overwrites started_at even on ticks where no transition occurred, a queued build's stored started_at silently changes from its queue-entry time to the later run-start once a runner picks it up. reconstructHourlyCapacity/the builder_capacity SQL feed "used"/free off these started_at/completed_at intervals, so the hourly capacity numbers become non-deterministic and depend on which writer ran last and how long the build sat in the queue.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/_backend/triggers/cron_reconcile_build_status.ts, line 229:

<comment>This write defines `started_at` as the builder-reported run start, but the same column is set elsewhere to the submission wall-clock time: `start.ts` writes `started_at: new Date().toISOString()` when the build advances to `startedStatus`. Because this cron reconciles every non-terminal stale build (~1/min) and overwrites `started_at` even on ticks where no transition occurred, a queued build's stored `started_at` silently changes from its queue-entry time to the later run-start once a runner picks it up. `reconstructHourlyCapacity`/the `builder_capacity` SQL feed "used"/free off these `started_at`/`completed_at` intervals, so the hourly capacity numbers become non-deterministic and depend on which writer ran last and how long the build sat in the queue.</comment>

<file context>
@@ -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,
+          completed_at: isoFromBuilderTimestamp(effectiveCompletedAt) ?? undefined,
           updated_at: new Date().toISOString(),
</file context>

return null

const delta = previous === null ? total : total - previous
const { data: inserted, error: insertError } = await admin

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: recordBuilderCapacityIfChanged performs a non-atomic read-then-write: it SELECTs the latest event to compute delta, then INSERTs a new row. When two admin_stats calls (e.g. two open builder tabs, or a poll racing a page load) run concurrently, both read the same previous workers_total and both insert a new row with the same absolute workers_total but a duplicated, non-zero delta. The event log then contains two consecutive rows with an unchanged workers_total yet a non-zero delta, corrupting the reconstructed pool series (workersAt/delta history) that this whole feature is built on. Consider making the write atomic (e.g. a single INSERT ... ON CONFLICT / advisory lock, or compute the delta from the max(created_at) row within the same transaction) so concurrent writers cannot derive the delta from a stale read.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/_backend/utils/builder_capacity.ts, line 291:

<comment>recordBuilderCapacityIfChanged performs a non-atomic read-then-write: it SELECTs the latest event to compute `delta`, then INSERTs a new row. When two admin_stats calls (e.g. two open builder tabs, or a poll racing a page load) run concurrently, both read the same `previous` workers_total and both insert a new row with the same absolute `workers_total` but a duplicated, non-zero `delta`. The event log then contains two consecutive rows with an unchanged `workers_total` yet a non-zero delta, corrupting the reconstructed pool series (`workersAt`/delta history) that this whole feature is built on. Consider making the write atomic (e.g. a single INSERT ... ON CONFLICT / advisory lock, or compute the delta from the max(created_at) row within the same transaction) so concurrent writers cannot derive the delta from a stale read.</comment>

<file context>
@@ -0,0 +1,471 @@
+    return null
+
+  const delta = previous === null ? total : total - previous
+  const { data: inserted, error: insertError } = await admin
+    .from('builder_capacity_events')
+    .insert({
</file context>

if (formattedMonth)
return formattedMonth
}
if (props.dateGranularity === 'hour') {

Copy link
Copy Markdown

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
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/admin/AdminMultiLineChart.vue, line 70:

<comment>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.</comment>

<file context>
@@ -67,6 +67,11 @@ function formatChartDate(date: string) {
     if (formattedMonth)
       return formattedMonth
   }
+  if (props.dateGranularity === 'hour') {
+    const formattedHour = formatLocalDateTime(date)
+    if (formattedHour)
</file context>

// Prefer builder machine occupancy; fall back to Capgo active jobs when /ok
// path cannot see currentJobId.
if (source === 'ok' || (!live.builder_reachable && activeBuilds > 0)) {
live.used = Math.min(live.workers_online || activeBuilds, activeBuilds)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: In the degraded path (source == 'ok' or builder unreachable) used is approximated from countActiveBuilds, which counts build_requests in ['starting', 'waiting_runner', 'running']. A build in waiting_runner is queued awaiting a runner, not currently occupying a machine, so including it inflates the live "Used" card and deflates "Free" whenever the pool has queued work. Consider excluding waiting_runner from the used estimate (or counting only running/starting) so the fallback reflects machines actually consumed rather than demand queued behind them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/_backend/utils/builder_capacity.ts, line 450:

<comment>In the degraded path (source == 'ok' or builder unreachable) `used` is approximated from `countActiveBuilds`, which counts build_requests in `['starting', 'waiting_runner', 'running']`. A build in `waiting_runner` is queued awaiting a runner, not currently occupying a machine, so including it inflates the live "Used" card and deflates "Free" whenever the pool has queued work. Consider excluding `waiting_runner` from the used estimate (or counting only `running`/`starting`) so the fallback reflects machines actually consumed rather than demand queued behind them.</comment>

<file context>
@@ -0,0 +1,471 @@
+  // Prefer builder machine occupancy; fall back to Capgo active jobs when /ok
+  // path cannot see currentJobId.
+  if (source === 'ok' || (!live.builder_reachable && activeBuilds > 0)) {
+    live.used = Math.min(live.workers_online || activeBuilds, activeBuilds)
+    live.free = Math.max(0, (live.workers_online || activeBuilds) - live.used)
+  }
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant