Skip to content

feat(dashboard): surface per-attempt job runs and frozen progress - #225

Merged
BK1031 merged 1 commit into
mainfrom
bk1031/foreman-run-history
Aug 7, 2026
Merged

feat(dashboard): surface per-attempt job runs and frozen progress#225
BK1031 merged 1 commit into
mainfrom
bk1031/foreman-run-history

Conversation

@BK1031

@BK1031 BK1031 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Important

Depends on Gaucho-Racing/Foreman#1. Do not deploy this before Foreman ships include=last_run — see Deploy ordering below.

  • Add JobRunsCard: an Attempts section on the job details page, fetching GET /foreman/jobs/:id/runs, newest-first, one collapsible row per attempt with worker, duration, progress, error, and result JSON
  • Default-expand anything that didn't succeed plus the latest attempt; user toggles keyed by run id so an SSE refetch doesn't reopen collapsed rows
  • Add last_run to the Job model
  • Switch the jobs list Progress column from current_run to last_run, so pending and terminal rows keep their final reading
  • Switch the details Progress card and Overview "Worker" onto last_run (lease stays on current_run — only an in-flight run holds one)
  • Move progressBarClass into lib/job-stream and use it in both views

Why

current_run filters status = 'running' server-side, and run/job transitions are atomic, so it's non-null iff the job is active. That left three gaps:

  1. A failed attempt's error and result were unreachable — job.result is reserved for the winning attempt, so partial-failure payloads were invisible.
  2. Retry history was invisible: "Attempt 2 / 3" with no way to see what attempt 1 did.
  3. Every non-active row showed in the Progress column.

progressBarClass's pending → gray and terminal → white (frozen final reading) branches were unreachable dead code — the only call site sits behind a total > 0 guard that required a non-null current_run. They were written expecting a last-run fallback that Foreman never had. Reading last_run makes them work as intended.

Deploy ordering

Foreman must ship first. Old Foreman compares include by exact string (c.Query("include") != "current_run"), so it answers include=last_run with bare jobs and no run data at all — the list's Progress column would go empty for every row, active ones included. There's no way to degrade gracefully around that from the client, so the ordering is a hard requirement.

The details page does degrade gracefully (job.last_run ?? job.current_run), since SSE still carries current_run on old Foreman.

Notes

  • Prettier reformatted one unrelated line in JobsPage.tsx; that file was already failing npm run check on main, so this fixes it rather than adding churn.

Test plan

  • tsc --noEmit reports nothing in the touched files; ESLint clean at --max-warnings 0; Prettier clean
  • Pre-existing on main and unrelated: tsc fails to resolve echarts / chrono-node, so npm run build is red regardless of this branch — worth an npm ci
  • Not yet exercised against a live Foreman; needs the dependency merged and deployed first

Foreman keeps outcome per attempt: job.result holds only the winning
attempt's payload, and current_run is non-null exclusively while an
attempt holds the lease. So a failed attempt's error, result, and
progress had nowhere to surface — including the partial-failure payloads
handlers now attach to a failure.

Add an Attempts card to the job details page, fetching /jobs/:id/runs
and rendering one collapsible row per attempt with its worker, duration,
progress, error, and result. Anything that didn't succeed starts
expanded, as does the latest attempt; toggles are keyed by run id so a
new attempt landing over SSE doesn't reopen what was collapsed.

Switch the jobs list and the details progress/worker fields onto
last_run, so pending-after-retry and terminal rows keep their final
reading instead of showing an em dash. This makes progressBarClass's
pending and terminal branches reachable for the first time — they were
dead code, written against a last-run fallback that current_run never
provided. Moved to lib/job-stream so both views share it.

Requires foreman's include=last_run (Gaucho-Racing/Foreman#1).
@BK1031
BK1031 merged commit f74627c into main Aug 7, 2026
17 checks passed
@BK1031
BK1031 deleted the bk1031/foreman-run-history branch August 7, 2026 16:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 366bdf7c72

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// into its row so the table renders progress without a second
// fetch per row — and unlike current_run it stays populated after
// the attempt finishes, so pending and terminal rows keep theirs.
params.set("include", "last_run");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve current-run progress until Foreman is upgraded

In the checked docker-compose.yaml configuration, Foreman remains pinned to 2.0.2, whose repository-side client only supports include=current_run; the old server therefore returns bare jobs for this new value. Running the dashboard against the committed local stack makes job.last_run undefined and removes progress from every row, including active jobs. Upgrade the pinned Foreman deployment in lockstep or retain a compatible fallback before switching this query.

Useful? React with 👍 / 👎.

Comment on lines +45 to +47
useEffect(() => {
void fetchRuns();
}, [fetchRuns, attemptCount, status]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh the active attempt when heartbeat progress changes

While an attempt is running, heartbeat SSE events can change its progress and message without changing either attemptCount or status, so this effect never refetches and the expanded attempt row remains at the values captured when the card first loaded. On the same page the main progress card updates from the stream, producing contradictory readings until the attempt terminates or another attempt starts; pass the streamed run/update timestamp through or otherwise refresh on heartbeat updates.

Useful? React with 👍 / 👎.

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.

1 participant