# Build the "Long-run progress" pattern: `LongRunProgress`

## Stack (edit this block to match your project)
- React 18+ with TypeScript: one function component, typed props, no `any`.
- Tailwind CSS, using your design system's tokens if you have them.
- Presentational only: data comes in through props, decisions go out through callbacks. No fetching or global state inside.
- If your codebase already has conventions (naming, shared components, tokens), follow them over this spec.

## What the pattern is for
Elapsed time, step count and honest expectation-setting for work measured in minutes or hours.

Use it when: Research agents, batch jobs, renders and migrations.

## What to build
A progress panel for work that takes minutes or hours, showing elapsed time, the current stage out of the total, items processed, the current rate, and an estimated time range based on past runs. When the rate changes it revises the estimate visibly and says why. It renders in a job page, run panel or background tray detail.

### Props
- `title`: string, e.g. "Re-scoring health for 1,284 accounts".
- `startedAt`: Date and `elapsedSeconds`: number.
- `stages`: Array<{ id: string; label: string; state: "done" | "running" | "pending"; durationLabel?: string; estimateLabel?: string }>.
- `items?`: { done: number; total: number; unitLabel: string }, a real count; drives the only determinate bar.
- `rate?`: { text: string; slowReason?: string }, e.g. "About 1.1 accounts a second".
- `estimate?`: { rangeLabel: string; finishWindowLabel: string; basis: string; revisedAt?: string; history?: number[] }, e.g. "18 to 25 min, based on the last 9 runs of this size".
- `status`: "running" | "slow" | "stalled" | "done" | "failed".
- `onNotify?()`: asks to be told when it finishes or stops.

### States it must render
- Running: elapsed time, "Stage 2 of 4", "Scored 412 / 1,284", the stage list with durations, the rate, and the estimate range with a likely finish window.
- Slow: the rate line explains, e.g. "Down to 0.3 a second: the usage service is rate-limiting us"; the estimate shows "Estimate revised at 7:02" with the new range and "Nothing is lost, it's just slower".
- No estimate: with no history, only elapsed time, stage and count are shown, with "No past runs to estimate from".
- Stalled: no progress for a set period; "No progress for 3 min" in amber with the last activity.
- Done: total time against the usual range, e.g. "Finished in 22 min, usual 18 to 25".
- Failed: the stage it stopped in and what was completed.

### Interactions
- Notify calls `onNotify` and the button is replaced by "We'll tell you when it finishes, or if it stops. Safe to close this tab."
- Expanding the estimate shows the history chart of past run durations with this run's elapsed time against them.
- Stage rows can expand to show their own counts when provided.

### Edge cases
- No invented percentages: without `items`, show an indeterminate indicator plus stage and elapsed time, never a bar that implies a fraction.
- Elapsed time beyond the top of the range: widen the range or say "Taking longer than usual" rather than holding at the old estimate.
- Items done greater than total (total grew mid-run): show the new total and note it changed.
- Very long runs (hours): format elapsed as hours and minutes.

## Must avoid
A fake determinate progress bar that sticks at 90% and destroys trust in every future estimate.

## Engineering constraint
Prefer step counts and elapsed time over invented percentages; estimate from historical runs.

## Accessibility
- Announce stage changes, estimate revisions and completion, not every count tick.
- The items bar is a `progressbar` with `aria-valuenow` and `aria-valuemax` from real counts; stage list is an ordered list with states in text.

## House rules
- Real `<button>`/`<input>` elements, full keyboard use, visible focus; announce status changes with `aria-live="polite"`; respect `prefers-reduced-motion`.
- All user-facing text comes from props (or a `labels` prop) so it can be translated.
- One consistent accent marks anything the AI made or is doing; amber means "needs a person", red means risky, green means done. Never rely on colour alone.
- Fits a 360 px wide container without horizontal scrolling.

## Acceptance checks
- [ ] Every state above can be rendered by passing props, with no internal fake data.
- [ ] Every interaction and edge case above behaves as described.
- [ ] Nothing in "Must avoid" can happen with this component.

## Deliver
1. The component file with its exported prop types.
2. A short usage example that renders each state.
