Skip to content

Render live progress bars in the ccas CLI during job follow #165

Description

@Sootopolis

Summary

Long-running jobs (ccas recruit, ccas history, ccas membership, ccas matchref) show no live progress bars in the CLI — only streamed log lines. Render each followed job's bars (the ChessComClient API: n/max gauge plus per-app foreachParProgress bars) in the CLI, drawn at the bottom with log lines scrolling above.

Why bars don't reach the CLI today

ccas is a thin HTTP client: it submits a job to the local server, then tails the per-job log file via GET /api/jobs/{id}/logs, printing each line (JobFollower). Two things block bars on that path:

  1. The server runs apps with ProgressDisplay.live(showProgress = false) (CcasServer.scala:54), suppressing bar redraws.
  2. Bar updates and log lines travel different routes. Log lines go ZLogger -> logAboveBarsSync -> per-job JobLogSink -> FileSink -> /logs stream. Bars go ProgressBar.print -> render -> drawAllSync -> the display's out stream (server System.out) — they never touch the per-job sink, so bar state has no route to the client.

Approach

Bars are per-job and belong on the client. The server keeps showProgress = false for its own console; bar state is exposed to the CLI as data on a dedicated endpoint, and the CLI renders it. (Do not enable server-side bars — one server console multiplexes many jobs, and its stdout isn't the operator's terminal.)

Transport — two independent endpoints:

  • GET /api/jobs/{id}/logs — unchanged. Pure newline-delimited log lines; the existing skipReplay / keepalive / reconnect / "close = job done" contract stays untouched.
  • GET /api/jobs/{id}/progress (new) — the job's live bar state as latest-wins snapshots ([{id, current, total, text}]), removal implicit (a bar absent from the newest snapshot is gone). SSE push on change (client coalesces to its own redraw cap), or client-paced poll with a version/ETag for cheap 304s. Live-only: no persistence, no replay, closes when the job is terminal.

Two endpoints (not a multiplexed stream) because bars are ephemeral, latest-wins, and drawn as a bottom block with no ordering coupling to log lines — separate lifetimes match the data. This keeps the subtle /logs protocol byte-for-byte intact, lets the CLI pick its own bar refresh rate (or skip bars entirely), and leaves every other /logs consumer bar-free with no filtering.

Server-side: a per-job SubscriptionRef[List[BarSnapshot]] updated on each bar mutation; /progress streams its .changes. Make bars per-job addressable (mirroring JobLogSink.currentSink) so a job's ProgressBar writes land in its SubscriptionRef instead of the global drawAllSync -> out path.

Client rendering: the CLI runs one ProgressDisplay. The log-follow fiber routes lines through logAboveBars; the /progress fiber routes snapshots through render. Both share the display's render lock — identical to how the server-side display already interleaves log lines and bar redraws.

Multi-line layout: draw one bar per line (was single-line via \r, a constraint that no longer applies with the CLI as the primary consumer on a real terminal). Track N physical lines; on redraw, cursor-up N (�[{N}A), clear and reprint each bar on its own line; interleave a log line by clearing the block, printing above, and reprinting the bars below.

  • Wrap-safety: a bar wider than the terminal wraps to >=2 physical lines — count physical lines for the cursor-up, or the redraw tears. Needs terminal width (COLUMNS / tput cols / JLine).
  • Fallback to single-line (or no bars) on a non-TTY / dumb terminal / NO_COLOR (Dispatcher.hasTty already distinguishes).

Constraints

  • /logs is unchanged: JobFollower.skipReplay newline-counting, the JobLogStream.KeepAliveLine NUL tick, and "stream close = job done" all stay as-is. No bar traffic ever rides /logs.
  • /progress is live-only: no persistence, no replay; a dropped connection is a non-event (reopen -> fresh snapshot). The client opens it only when it wants bars, so it never affects log following.
  • Per-job .log files stay bar-free and ANSI-stripped (JobLogSink.stripAnsi).

Out of scope

Acceptance criteria

  • ccas recruit/history/membership/matchref show the live API: n/max gauge and per-app bars during a followed job.
  • Bars render one-per-line on a real terminal; single-line (or no bars) fallback on non-TTY / dumb terminal / NO_COLOR.
  • Log lines scroll cleanly above the bar block with no tearing, including on terminal-width wrap.
  • /logs reconnect (Log-follow drops at server 60s read-idle timeout; #152 keepalive resets only the client timer, not the server ReadTimeoutHandler #161) resumes correctly; keepalive still filtered; "stream close = done" intact; /logs output is identical whether or not a /progress consumer is attached.
  • --no-progress / non-TTY opens no /progress connection and produces no bar traffic.
  • /progress is live-only: closes when the job is terminal; per-job .log file unchanged.
  • Server console (showProgress = false) behaviour unchanged.

References


Parent epic: #40

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:cliCLI binary (subcommands, completions, config, packaging)enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions