Files
coder/coderd
Jeremy Ruppel 3ddf7d3baa fix: stop the template builder build progress bar from looping (#27276)
## Summary

The template builder's "Building your template" loader had a progress
bar that
animated 0→100% every 5s with an infinite repeat, so it visibly
restarted over
and over while a template built. It looked broken and was frustrating to
watch.

This replaces the looping fill with a single ease-out fill that
decelerates
toward 90% and holds until the request resolves and the loader unmounts.

Since the loader is intentionally indeterminate and no progress is
streamed to
the browser, this also removes the now-dead `onUpdate` callback plumbing
from
the backend `waitForProvisionerJob` (its only caller passed `nil`).

Resolves DEVEX-593.


https://github.com/user-attachments/assets/6d5ec04e-9f97-4864-bd18-e1e75055f079

## Commits

- `refactor(coderd): drop unused onUpdate callback from
waitForProvisionerJob`
- `fix(site/src/pages/TemplateBuilder): stop build progress bar from
looping`

## Testing

- `go build ./coderd/` passes with the reduced `waitForProvisionerJob`
signature.
- Biome clean on the changed frontend file.
- Storybook: `pages/TemplateBuilder/BuildingTemplateLoader` shows the
bar fill
  once and hold, with no restart.

<details>
<summary>Implementation plan</summary>

# DEVEX-593: Stop the build progress bar from looping repeatedly

## Problem

While the template builder composes and imports a template, the FE shows
`BuildingTemplateLoader`. Its progress bar animates from 0% to 100% over
5s
with `repeat: Number.POSITIVE_INFINITY`, so it visibly restarts over and
over.
Users report this looks broken and is frustrating to watch while
waiting.

## Decision (scope)

Minimal fix only: **stop the loop**, plus remove the now-dead `onUpdate`
plumbing from the backend. Since the UI is intentionally indeterminate
and no
progress signal is streamed, the callback serves no purpose and should
be
deleted rather than left as dead code.

### Why not "real sync" now

- `POST /api/v2/templatebuilder/compose/template` is a single blocking
request.
  It composes, bundles, inserts the provisioner job, then calls
`waitForProvisionerJob(jobCtx, provisionerJob.ID, nil)` and only
responds once
  the job completes.
- The `onUpdate` callback runs server-side only. Nothing is streamed to
the
  browser during the wait, so the FE has no progress signal to bind to.
- A provisioner job exposes no numeric percentage. Only status
transitions
  (`pending -> running -> succeeded`) and coarse log stages
(`init/plan/graph/apply`) exist. Real sync would require converting the
endpoint to a streaming protocol (SSE/WebSocket) plus FE rework, which
is
  disproportionate for this 1-point ticket.

### Keep polling (do not switch to pubsub-block)

The wait could technically block instead of poll: on completion
`CompleteJob`
publishes `ProvisionerJobLogsNotifyMessage{EndOfLogs: true}` on the job
logs
notify channel, so we could subscribe and wait for that message with the
context timeout as a fallback. We deliberately do not do that here:
correctness
would require subscribe-before-completion plus an initial DB completion
check to
avoid a race, and Postgres LISTEN/NOTIFY is at-most-once (can drop under
load),
so a poll fallback would still be needed. The existing backoff poll
(100ms -> 200ms -> 500ms -> 1s) is simple and robust for a short-lived
synchronous request.

## Approach

Replace the looping fill with a single, non-repeating ease-out fill that
decelerates and approaches (but never reaches) ~90%, holding there until
the
request resolves and the loader unmounts. This reads as continuous
forward
progress for an unknown-duration operation and never restarts. The
floating-icon
animation is intentional ambient motion and is not in scope.

## Out of scope

- Any behavioral change to how the endpoint waits (it still blocks on
the job).
- Streaming real job progress to the browser.
- Changes to the floating-icon animation.

</details>

---

Generated by Coder Agents.
2026-07-16 09:14:07 -04:00
..