Skip to content

feat(cli): live progress bars while following a job#166

Merged
Sootopolis merged 1 commit into
mainfrom
wip
Jul 16, 2026
Merged

feat(cli): live progress bars while following a job#166
Sootopolis merged 1 commit into
mainfrom
wip

Conversation

@Sootopolis

Copy link
Copy Markdown
Owner

What

Live progress bars in the ccas CLI while it follows a job — the API gauge and per-app bars now render on the operator's terminal, not just the server's (suppressed) console.

  • Server: new GET /api/jobs/{id}/progress — a chunked NDJSON stream of a job's live bar snapshots ({bars:[{id,current,total,text}]}), keepalive-wrapped like /logs, closing when the job is terminal. Bars are per-job (a SubscriptionRef channel scoped per job like the log sink) merged with the shared client's global API gauge, so following one club under --all shows only that club's bars. Frames carry raw current/total/text — only the client knows its terminal width. Frame rate is sampled + de-duplicated (conflation), capped by the DB-tunable app_setting.progress_refresh_interval_ms (default 100ms), so a busy job can't flood the follower and the latest state is always delivered within one interval.
  • Client: streamProgress + ClientProgressRenderer reconcile frames onto a local ProgressDisplay; log lines interleave above the bars through the display's render lock. The progress consumer reconnects across the server's ~60s read-idle drop (like the log follow, Log-follow drops at server 60s read-idle timeout; #152 keepalive resets only the client timer, not the server ReadTimeoutHandler #161) and clears the bars when the follow ends.
  • Multi-line: ProgressDisplay renders one bar per line (cursor-up block) now that the CLI, not IntelliJ's console, is the consumer; terminal width comes from stty (COLUMNS isn't exported to the JVM).
  • Sequential --all: submits + follows one club at a time so each streams live in turn, best-effort per club.
  • --no-progress: opt-out; bars also auto-off on a non-TTY.

Fixes

Closes #165.

Testing

  • sbt test1047 passed, 0 failed (also the green pre-push gate).
  • New coverage: TestClientProgressRenderer (reconcile add/update/remove, dedup, multi-line), TestProgressBar (channel publish, cursor-up multi-line), TestJobFollower (bars path, /progress reconnect-after-drop, timeout ordering), TestJobRunner (progressStream live merge + close), TestRoutes (/progress 200/404), TestCliParser (--no-progress).
  • Three adversarial review rounds (2 multi-agent workflows + 1 focused reviewer) between stages; every confirmed finding fixed — a lock-ordering tear, a missing /progress reconnect, broken terminal-width detection, a rate-cap that dropped the last-of-burst frame, and a settings-read failure that now degrades to the default.

🤖 Generated with Claude Code

The `ccas` CLI is a thin HTTP client: the server runs the job and drew
the progress bars on its own (suppressed) console, so bars never reached
the operator — only the streamed log lines did. Expose the bar state as
data over a second stream so the CLI can render it.

- Server: new `GET /api/jobs/{id}/progress` NDJSON endpoint streaming a
  job's live bar snapshots. Bars are per-job (a `SubscriptionRef` channel
  set per job like the log sink), merged with the shared client's global
  API gauge, so following one club under `--all` shows only that club's
  bars, not its siblings'. Frames carry RAW `current`/`total`/`text`, not
  a pre-rendered bar — only the client knows its terminal width.
- Frame rate is sampled + de-duplicated (conflation), capped by the
  DB-tunable `app_setting.progress_refresh_interval_ms` (default 100ms),
  so a busy job can't flood the follower and the latest state is always
  delivered within one interval.
- Client: `streamProgress` + `ClientProgressRenderer` reconcile frames
  onto a local `ProgressDisplay`; log lines interleave above the bars
  through the display's render lock. The progress consumer reconnects
  across the server's ~60s read-idle drop (as the log follow does, #161)
  and clears the bars when the follow ends.
- `ProgressDisplay` renders one bar per line (cursor-up block) now that
  the CLI, not IntelliJ's console, is the consumer; width comes from the
  real terminal via `stty` (COLUMNS isn't exported to the JVM).
- `--all` now submits + follows one club at a time, so each streams live
  in turn instead of the first hogging the screen while the rest finish
  invisibly; each club is best-effort.
- `--no-progress` opts out; bars also auto-off on a non-TTY.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Sootopolis
Sootopolis merged commit 882622d into main Jul 16, 2026
1 check passed
Sootopolis added a commit that referenced this pull request Jul 16, 2026
…167)

The server's `idleTimeout` installs a read-only Netty `ReadTimeoutHandler`,
which resets only on inbound reads. A job-log / progress follow is write-only
(server -> client) once the GET is sent, so the outbound `JobLogStream`
keepalive can never reset that timer and every live follow is reaped on the
60s schedule -- surfacing as zio-http's `ZIO.logWarningCause("Fatal exception
in Netty", Cause.die(ReadTimeoutException))`. The follower already reconnects
transparently (#161), so the reap is benign transport noise, not a failure;
the WARN spam (two per cycle since the progress stream landed in #166) was the
only real damage.

Drop exactly that WARN at ProgressDisplay's ZLogger. The match is
over-specified -- the exact zio-http message AND a cause that is solely a
`ReadTimeoutException` defect -- so it stays scoped to the server reaper and
fails open: a zio-http rename or wrap just makes the (benign) WARN reappear
rather than hiding any genuine fatal-Netty signal.

Also fix the stale `CcasServer` comment that claimed the keepalive resets the
server's read timer; it cannot (writes don't reset a `ReadTimeoutHandler`).


Claude-Session: https://claude.ai/code/session_015eWRWek8owPM5eDjatPVit

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

Render live progress bars in the ccas CLI during job follow

1 participant