Upload page: show progress, so the user can tell running from hung or crashed #42

Closed
opened 2026-10-02 05:24:09 +00:00 by mathias · 2 comments
Owner

Found 2026-10-02 while the user ran a real 64-minute recording through the two-engine pipeline (PR #39, ADR-034).

Gap. /uploads/{id} shows only queued / transcribing / done / failed. A 1-hour upload with reconciliation takes minutes to tens of minutes, and during that time the page looks identical whether the job is working, hung on a dead engine, or the process has crashed. The user asked for this directly.

What the page cannot say today

  • which phase is running: transcribing with engine A, with engine B, reconciling (and how many of N disputes are adjudicated), or saving
  • how long the job has run, and when it last made progress
  • whether the process is still alive. FailInterruptedUploads fixes a crash only on the next start, so after a crash the row says transcribing until someone restarts the service
  • queue position, since the worker is serialized (a second upload waits behind the first)

Suggested shape (not decided)

  • Store a phase and progress_at heartbeat on uploaded_transcripts (new migration, RLS unchanged). The worker writes the phase on each transition; the reconciler writes adjudicated/total.
  • The existing HTMX status poll renders phase, elapsed time and the heartbeat age. A heartbeat older than a stated threshold shows "no progress for N min, may be stuck" instead of a spinner.
  • The ASR calls are single blocking HTTP requests, so inside an engine call there is no progress to report. The honest display there is elapsed time against an estimate (about 12x realtime for kb-whisper, measured in docs/research/audio-length-ceiling-2026-10-01.md), not a percentage.
  • Open question: whether a heartbeat from a goroutine independent of the engine call is needed, so "alive" is distinguishable from "engine call hung". The job already has a timeout (60 min default).

Test notes. The stuck-versus-working display is control-like, so it needs a test that holds a fake engine open past the threshold and asserts the page changes, not only that a phase string renders.

Related: #37 (deploy plumbing), #38 (unmeasured limits). Not built in PR #39.


Created via git-mcp on behalf of @mathias

Found 2026-10-02 while the user ran a real 64-minute recording through the two-engine pipeline (PR #39, ADR-034). **Gap.** `/uploads/{id}` shows only `queued` / `transcribing` / `done` / `failed`. A 1-hour upload with reconciliation takes minutes to tens of minutes, and during that time the page looks identical whether the job is working, hung on a dead engine, or the process has crashed. The user asked for this directly. **What the page cannot say today** - which phase is running: transcribing with engine A, with engine B, reconciling (and how many of N disputes are adjudicated), or saving - how long the job has run, and when it last made progress - whether the process is still alive. `FailInterruptedUploads` fixes a crash only on the next start, so after a crash the row says `transcribing` until someone restarts the service - queue position, since the worker is serialized (a second upload waits behind the first) **Suggested shape (not decided)** - Store a `phase` and `progress_at` heartbeat on `uploaded_transcripts` (new migration, RLS unchanged). The worker writes the phase on each transition; the reconciler writes `adjudicated/total`. - The existing HTMX status poll renders phase, elapsed time and the heartbeat age. A heartbeat older than a stated threshold shows "no progress for N min, may be stuck" instead of a spinner. - The ASR calls are single blocking HTTP requests, so inside an engine call there is no progress to report. The honest display there is elapsed time against an estimate (about 12x realtime for kb-whisper, measured in `docs/research/audio-length-ceiling-2026-10-01.md`), not a percentage. - Open question: whether a heartbeat from a goroutine independent of the engine call is needed, so "alive" is distinguishable from "engine call hung". The job already has a timeout (60 min default). **Test notes.** The stuck-versus-working display is control-like, so it needs a test that holds a fake engine open past the threshold and asserts the page changes, not only that a phase string renders. Related: #37 (deploy plumbing), #38 (unmeasured limits). Not built in PR #39. --- _Created via git-mcp on behalf of @mathias_
Author
Owner

Requirements from the product owner (2026-10-02), to be treated as part of this issue's acceptance:

  1. Visual style. The indicator continues tapir's existing visual style. Reuse the existing templ components, CSS tokens and layout conventions of the web UI; do not introduce a new design language, a JS framework or a separate stylesheet for it.

  2. Interaction model. The reference is the progress display when a container starts and its layers are pulled and activated (docker/podman-style): a rich, TUI-like experience.

    • a list of named steps, each with its own state (pending, running, done, failed, skipped)
    • a running step that visibly moves: a spinner or bar, with elapsed time
    • finished steps collapse to a done line with their duration, so the page reads as a log of what happened
    • the whole thing updates in place, with no page reload

    For tapir the steps are the real phases of an upload: received, probing, extracting audio, transcribing with each engine, comparing the readings, adjudicating disputes (n of N), saving. The two-engine phases are only shown when reconciliation is on.

Constraint to design around. This only works where the data is real. The two engine calls are single blocking requests, so inside them the display can show elapsed time against the measured ~12x-realtime estimate, never a fabricated percentage. Adjudication has real counts (n of N) and can use a true bar. A step that is genuinely stuck must look different from a running one (see the heartbeat point in the description).

Delivery. HTMX polling or server-sent events both fit the current stack; pick whichever keeps the page working without JS-heavy code. Whatever is chosen needs a screenshot-based review (the UX-tester worker pattern) before it counts as done, since "looks like tapir" and "reads like a TUI" are visual claims that tests cannot assert.


Created via git-mcp on behalf of @mathias

**Requirements from the product owner (2026-10-02), to be treated as part of this issue's acceptance:** 1. **Visual style.** The indicator continues tapir's existing visual style. Reuse the existing templ components, CSS tokens and layout conventions of the web UI; do not introduce a new design language, a JS framework or a separate stylesheet for it. 2. **Interaction model.** The reference is the progress display when a container starts and its layers are pulled and activated (docker/podman-style): a rich, TUI-like experience. - a list of named steps, each with its own state (pending, running, done, failed, skipped) - a running step that visibly moves: a spinner or bar, with elapsed time - finished steps collapse to a done line with their duration, so the page reads as a log of what happened - the whole thing updates in place, with no page reload For tapir the steps are the real phases of an upload: received, probing, extracting audio, transcribing with each engine, comparing the readings, adjudicating disputes (n of N), saving. The two-engine phases are only shown when reconciliation is on. **Constraint to design around.** This only works where the data is real. The two engine calls are single blocking requests, so inside them the display can show elapsed time against the measured ~12x-realtime estimate, never a fabricated percentage. Adjudication has real counts (n of N) and can use a true bar. A step that is genuinely stuck must look different from a running one (see the heartbeat point in the description). **Delivery.** HTMX polling or server-sent events both fit the current stack; pick whichever keeps the page working without JS-heavy code. Whatever is chosen needs a screenshot-based review (the UX-tester worker pattern) before it counts as done, since "looks like tapir" and "reads like a TUI" are visual claims that tests cannot assert. --- _Created via git-mcp on behalf of @mathias_
Author
Owner

Built on PR #39 (head 7029dd1), per ADR-035 (Proposed). Closes on merge.

Done: the page shows real phases (received, audio, each engine, comparing, deciding n of N, saving) as a step log in tapir's tokens and mono font, with a CSS spinner (reduced-motion safe), a bar only for the counted phase, and states running / quiet / lost / failed / done. Finished steps collapse to a line with their duration, and a done job keeps a collapsed "What was done" list. HTMX poll is 2 s while unfinished and none after. Screenshot-reviewed in light and dark across seven states (three defects found and fixed).

Deviation from the issue text: inside an engine call the page shows elapsed time only; no "estimate" was added, because pianissimo's speed is not measured and a made-up expectation would be the fabricated figure this issue warns against. Not done: queue position (a second upload waiting behind the first shows only "Queued").


Created via git-mcp on behalf of @mathias

Built on PR #39 (head `7029dd1`), per ADR-035 (Proposed). Closes on merge. Done: the page shows real phases (received, audio, each engine, comparing, deciding n of N, saving) as a step log in tapir's tokens and mono font, with a CSS spinner (reduced-motion safe), a bar only for the counted phase, and states running / quiet / lost / failed / done. Finished steps collapse to a line with their duration, and a done job keeps a collapsed "What was done" list. HTMX poll is 2 s while unfinished and none after. Screenshot-reviewed in light and dark across seven states (three defects found and fixed). Deviation from the issue text: inside an engine call the page shows elapsed time only; no "estimate" was added, because pianissimo's speed is not measured and a made-up expectation would be the fabricated figure this issue warns against. Not done: queue position (a second upload waiting behind the first shows only "Queued"). --- _Created via git-mcp on behalf of @mathias_
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/tapir#42