Visual plan · reviewable in the browser

Add CSV export to the Reports page

Let a user download the report they are viewing as a CSV, with the same filters applied. The exported rows always match the rows on screen.

● low risk 5 steps 1 open decision

01 What happens when the user clicks Export

The export reuses the table's own query, so the file can never drift from what is on screen.

Export CSV export API query rows to CSV .csv clicks Export same filters attached reuses table query stdlib csv module file downloads ✓ matches screen

02 Implementation, in order

Backend first (the endpoint and serializer), then the button wires to it.

backend

Export endpoint

GET /reports/:id/export reuses build_report_query() — the table's own query.

backend

CSV serializer

rows_to_csv() using the stdlib csv module. Quotes commas, newlines, unicode.

frontend

Export button

An ExportButton by the filter bar. Spinner while in flight; disabled on empty results.

frontend

Filter parity

Pass the identical filter query string the table already builds, so the two never diverge.

tests

Cover it

Unit-test the serializer edge cases; integration-test that export row count equals the table's.

03 One decision to confirm

Everything else has a safe default. This one changes what gets built.

Very large reports could outrun the 30-second request timeout. How do we handle them?

Most reports are a few hundred rows and stream in under a second. A few have tens of thousands. Streaming a huge one synchronously from rows_to_csv() risks holding the request open past the gateway timeout. The endpoint URL is stable either way, so a background path can be added later behind the same URL.

Stream now, cap at 50,000 rowsrecommendedAbove the cap, return “narrow your filters.” No report in production exceeds it today, so no queue or worker is built. The seam to go async stays open.
Background job + emailed linkRobust for any size, but adds a queue, a worker, object storage, and an email template — none of which exist for this feature yet.
OtherNone of these fit — reply in chat with what to do instead.

04 How we'll know it worked

Dev check

  • Apply a filter, click Export, open the file in a spreadsheet.
  • Row count in the file == rows in the filtered table.
  • Over-cap report returns the guidance message, not a timeout.

Tests

  • rows_to_csv: commas, quotes, newlines, empty, unicode.
  • Endpoint returns text/csv + attachment filename.
  • Filtered export count == table endpoint count.