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:
- The server runs apps with
ProgressDisplay.live(showProgress = false) (CcasServer.scala:54), suppressing bar redraws.
- 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
References
Parent epic: #40
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 (theChessComClientAPI: n/maxgauge plus per-appforeachParProgressbars) in the CLI, drawn at the bottom with log lines scrolling above.Why bars don't reach the CLI today
ccasis a thin HTTP client: it submits a job to the local server, then tails the per-job log file viaGET /api/jobs/{id}/logs, printing each line (JobFollower). Two things block bars on that path:ProgressDisplay.live(showProgress = false)(CcasServer.scala:54), suppressing bar redraws.ZLogger -> logAboveBarsSync -> per-job JobLogSink -> FileSink -> /logs stream. Bars goProgressBar.print -> render -> drawAllSync -> the display's out stream(serverSystem.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 = falsefor 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 existingskipReplay/ 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
/logsprotocol byte-for-byte intact, lets the CLI pick its own bar refresh rate (or skip bars entirely), and leaves every other/logsconsumer bar-free with no filtering.Server-side: a per-job
SubscriptionRef[List[BarSnapshot]]updated on each bar mutation;/progressstreams its.changes. Make bars per-job addressable (mirroringJobLogSink.currentSink) so a job'sProgressBarwrites land in itsSubscriptionRefinstead of the globaldrawAllSync -> outpath.Client rendering: the CLI runs one
ProgressDisplay. The log-follow fiber routes lines throughlogAboveBars; the/progressfiber routes snapshots throughrender. 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.COLUMNS/tput cols/ JLine).NO_COLOR(Dispatcher.hasTtyalready distinguishes).Constraints
/logsis unchanged:JobFollower.skipReplaynewline-counting, theJobLogStream.KeepAliveLineNUL tick, and "stream close = job done" all stay as-is. No bar traffic ever rides/logs./progressis 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..logfiles stay bar-free and ANSI-stripped (JobLogSink.stripAnsi).Out of scope
ccas config) #125) — this is streaming bars in a scrolling CLI, not a full-screen app.Acceptance criteria
ccas recruit/history/membership/matchrefshow the liveAPI: n/maxgauge and per-app bars during a followed job.NO_COLOR./logsreconnect (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;/logsoutput is identical whether or not a/progressconsumer is attached.--no-progress/ non-TTY opens no/progressconnection and produces no bar traffic./progressis live-only: closes when the job is terminal; per-job.logfile unchanged.showProgress = false) behaviour unchanged.References
CcasServer.scala:54— servershowProgress = falseProgressDisplay.scala—render/drawAllSync(bar ->out),logAboveBarsSync(line -> sink), single-line draw at:171-178JobLogSink.scala— per-job sinkFiberRef(currentSink),stripAnsiJobLogStream.scala— keepalive protocolJobFollower.scala— CLI tail,skipReplay, reconnect (Log-follow drops at server 60s read-idle timeout; #152 keepalive resets only the client timer, not the server ReadTimeoutHandler #161)ccas config) #125 (TUI, distinct)Parent epic: #40