feat(console): wire payroll to the real CSV run lifecycle#67
Conversation
Rewrite the Payroll screen around the backend's real lifecycle: compose a CSV (handles/addresses + amounts) -> validate (POST /orgs/:id/payroll, showing the summary + per-row resolved/failed items) -> start (POST /payroll/:runId/start) -> live progress via 2s polling of GET /payroll/:runId (pending -> proving -> submitted -> confirmed counts) with pause/resume, and clear treasury-underfunded guidance. Persists the active run per org and reattaches on reload. Removed the demo-era contractor/rate-card + 4-proof model and the nonexistent payroll list read. Demo mode animates a full run. Epic #58 step E. Closes #66.
📝 WalkthroughWalkthroughThe console replaces legacy payroll batches and proof steps with CSV-based payroll runs, typed lifecycle endpoints, progress subscriptions, pause/resume controls, demo simulation, persisted run hydration, and updated dashboard and status rendering. ChangesPayroll run lifecycle
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant PayrollScreen
participant api
participant PayrollBackend
PayrollScreen->>api: createPayrollRun(orgId, csv, token)
api->>PayrollBackend: POST /orgs/:id/payroll
PayrollBackend-->>PayrollScreen: runId and validation summary
PayrollScreen->>api: startPayrollRun(runId)
api->>PayrollBackend: POST /payroll/:runId/start
PayrollScreen->>api: subscribePayrollProgress(runId)
api->>PayrollBackend: poll GET /payroll/:runId
PayrollBackend-->>PayrollScreen: progress counts
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
| clearPayrollTimers(runId); | ||
| run.status = "paused"; |
There was a problem hiding this comment.
When a pause request resolves after the scheduled timers have already confirmed the last item, this path still clears timers and changes the completed run back to paused. The UI can then offer Resume for an already-settled demo run, and the dashboard counts it as scheduled again.
| clearPayrollTimers(runId); | |
| run.status = "paused"; | |
| if (run.status !== "running") throw new Error("Run is not running."); | |
| clearPayrollTimers(runId); | |
| run.status = "paused"; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/console/src/demo/api.ts
Line: 475-476
Comment:
**Terminal Runs Become Paused**
When a pause request resolves after the scheduled timers have already confirmed the last item, this path still clears timers and changes the completed run back to `paused`. The UI can then offer Resume for an already-settled demo run, and the dashboard counts it as scheduled again.
```suggestion
if (run.status !== "running") throw new Error("Run is not running.");
clearPayrollTimers(runId);
run.status = "paused";
```
How can I resolve this? If you propose a fix, please make it concise.| .filter((row) => row.line.length > 0); | ||
| const dataRows = rows[0] && /^recipient\s*,\s*amount/i.test(rows[0].line) ? rows.slice(1) : rows; | ||
| const items = dataRows.map(({ line, rowIndex }) => { | ||
| const cells = line.split(",").map((cell) => cell.trim()); |
There was a problem hiding this comment.
normalizePayrollAmount accepts comma-formatted amounts, but this split runs before normalization. A pasted row such as @aisha,8,500 or @aisha,"8,500" produces more than two cells and is marked invalid instead of validating as 8500.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/console/src/demo/api.ts
Line: 191
Comment:
**Comma Amounts Fail Parsing**
`normalizePayrollAmount` accepts comma-formatted amounts, but this split runs before normalization. A pasted row such as `@aisha,8,500` or `@aisha,"8,500"` produces more than two cells and is marked invalid instead of validating as `8500`.
How can I resolve this? If you propose a fix, please make it concise.| const hasPolicy = policies.length > 0; | ||
| const hasContractor = counterparties.some((c) => c.type === "contractor"); | ||
| const ranPayroll = payrolls.length > 0; | ||
| const ranPayroll = (dashboard?.scheduledPayrolls ?? 0) > 0 || !!dashboard?.recentActivity.some((a) => a.kind === "payroll"); |
There was a problem hiding this comment.
This first-run check now depends on scheduled payrolls or payroll activity, but the new demo lifecycle counts only ready/running/paused runs and does not add payroll activity on completion. After a successful demo run completes, the count drops to zero and the setup checklist can show payroll as not done.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/console/src/screens/Dashboard.tsx
Line: 86
Comment:
**Completed Payroll Looks Unrun**
This first-run check now depends on scheduled payrolls or payroll activity, but the new demo lifecycle counts only `ready`/`running`/`paused` runs and does not add payroll activity on completion. After a successful demo run completes, the count drops to zero and the setup checklist can show payroll as not done.
How can I resolve this? If you propose a fix, please make it concise.|
The three Greptile findings are all demo-mode only, so they don't affect the real L1 path:
Real payroll (validate → start → poll progress → pause/resume) is unaffected. Tracking these as demo-polish. Merging. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/console/src/lib/api.ts (1)
393-397: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider bounding the polling error retries.
On a persistent
getPayrollRunfailure (e.g., a 404 after a run is deleted, or repeated 5xx), the poll reschedules unconditionally every 2s and re-invokesonErroreach time. Since the run status never advances, the subscription only stops when the effect unmounts/closes it, so failing requests and error-state churn continue for the lifetime of the screen. A consecutive-failure cap (or backoff) would prevent an unbounded failing-request loop. The demosubscribePayrollProgressshares the same pattern.♻️ Example: cap consecutive failures
let closed = false; let pollTimer: number | null = null; + let failures = 0; + const MAX_FAILURES = 5; ... (error) => { if (closed) return; onError?.(error instanceof Error ? error : new Error("Payroll progress polling failed.")); - pollTimer = window.setTimeout(poll, 2_000); + if (++failures >= MAX_FAILURES) { close(); return; } + pollTimer = window.setTimeout(poll, 2_000); },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/console/src/lib/api.ts` around lines 393 - 397, Bound consecutive polling failures in both the production and demo subscribePayrollProgress implementations: track consecutive getPayrollRun errors, stop polling after a reasonable maximum (or apply bounded backoff), and reset the counter after a successful status response. Preserve closed/unsubscribe checks and notify onError appropriately without scheduling unbounded retries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@apps/console/src/lib/api.ts`:
- Around line 393-397: Bound consecutive polling failures in both the production
and demo subscribePayrollProgress implementations: track consecutive
getPayrollRun errors, stop polling after a reasonable maximum (or apply bounded
backoff), and reset the counter after a successful status response. Preserve
closed/unsubscribe checks and notify onError appropriately without scheduling
unbounded retries.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: dc9ce4ca-4f20-4dab-bb5c-31a13f8de1cc
📒 Files selected for processing (13)
apps/console/src/demo/api.tsapps/console/src/demo/data.tsapps/console/src/lib/api.test.tsapps/console/src/lib/api.tsapps/console/src/lib/store.test.tsxapps/console/src/lib/store.tsxapps/console/src/screens/Dashboard.test.tsxapps/console/src/screens/Dashboard.tsxapps/console/src/screens/Payroll.test.tsxapps/console/src/screens/Payroll.tsxapps/console/src/ui/primitives.tsxpackages/types/src/api.tspackages/types/src/payroll.ts
💤 Files with no reviewable changes (1)
- apps/console/src/lib/store.test.tsx
Epic: #58 · Step E (Payroll — flagship) · Closes #66
Rewires the Payroll screen off its demo-era model (contractors + rate cards + 4
prove-*steps) onto the backend's real CSV run → start → live progress lifecycle. This is the "encrypted payroll, live on our L1" path.What changed
lib/api.ts—createPayrollRun(orgId, {csv, token?})→POST /orgs/:id/payroll;getPayrollRun(runId)→GET /payroll/:runId;startPayrollRun/pausePayrollRun/resumePayrollRun(runId)→POST /payroll/:runId/{start,pause,resume};subscribePayrollProgress(runId, …)— 2s polling ofgetPayrollRun(the server's dual-mode SSE can't setAcceptfromEventSource; polling returns the sameprogresscounts), stopping on terminal status. Removed the demo-era fns (payrollslist,createPayroll,approvePayroll, the fourprove-*,payslips) — zero dead references.lib/store.tsx— dropped thepayrollsbatch read (the backend has no list-runs endpoint); the Payroll screen owns its run state.screens/Payroll.tsx— compose (CSV / rows) → validate (createPayrollRun, shows summary + per-row resolved/failed) → start → live progress (pending → proving → submitted → confirmed, with pause/resume) → complete. Handlestreasury_underfunded(amount short + link to Treasury). Persists the active run id per org and reattaches on reload. Trimmed ~1200 → ~540 lines.packages/types—PayrollRun,PayrollProgressCounts, run-create + item types matching the backend; dropped the demo-eraPayrollBatch/PayrollLine/proof types from the real path.VITE_DEMO_MODE=1validates a seeded CSV and animates the progress counts to completion.Verification (all green)
typecheck·test(70) ·build·VITE_DEMO_MODE=1 build·biome lint .Summary by CodeRabbit
New Features
Bug Fixes
Greptile Summary
This PR rewires console payroll to the CSV run lifecycle. The main changes are:
Confidence Score: 4/5
The payroll flow is close, but demo pause handling can put completed runs back into an active state.
paused.apps/console/src/demo/api.ts, apps/console/src/screens/Dashboard.tsx
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant User participant PayrollScreen participant API participant Backend User->>PayrollScreen: Paste CSV and select token PayrollScreen->>API: createPayrollRun(orgId, csv, token) API->>Backend: POST /orgs/:orgId/payroll Backend-->>API: runId, summary, items API-->>PayrollScreen: validation result PayrollScreen->>API: startPayrollRun(runId) API->>Backend: POST /payroll/:runId/start Backend-->>API: running progress loop Until complete or failed API->>Backend: GET /payroll/:runId Backend-->>API: run and progress counts API-->>PayrollScreen: progress event end User->>PayrollScreen: Pause or resume PayrollScreen->>API: POST /payroll/:runId/pause or resume%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant User participant PayrollScreen participant API participant Backend User->>PayrollScreen: Paste CSV and select token PayrollScreen->>API: createPayrollRun(orgId, csv, token) API->>Backend: POST /orgs/:orgId/payroll Backend-->>API: runId, summary, items API-->>PayrollScreen: validation result PayrollScreen->>API: startPayrollRun(runId) API->>Backend: POST /payroll/:runId/start Backend-->>API: running progress loop Until complete or failed API->>Backend: GET /payroll/:runId Backend-->>API: run and progress counts API-->>PayrollScreen: progress event end User->>PayrollScreen: Pause or resume PayrollScreen->>API: POST /payroll/:runId/pause or resumePrompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat(console): wire payroll to the real ..." | Re-trigger Greptile