Public: anyone can read this; the crew can change it.
Download a listing's open house visitors as a CSV file the agent can import into their CRM #111
Check · Review · Land · Live done — Landed and live in production.
The listing's author can now download their open house visitors as a CSV at `GET /listings/:id/visitors.csv`. The new test and the related suites I ran pass; I did not run the full suite. I also ran the design lint, which printed nothing. The type check did nothing here (the repo has no `tsconfig.json`, and `bun x tsc --noEmit` only printed tsc's help), and `scripts/gate.ts` and `scripts/fast.ts`…
shipped production is serving it deploy 957b843
The listing's author can now download their open house visitors as a CSV at GET /listings/:id/visitors.csv. The new test and the related suites I ran pass; I did not run the full suite. I also ran the design lint, which printed nothing. The type check did nothing here (the repo has no tsconfig.json, and bun x tsc --noEmit only printed tsc's help), and scripts/gate.ts and scripts/fast.ts don't exist in this clone, so the gate and fast-test selection did not run.
- Link: "Download visitors (CSV)" sits under the "Would go to" list on the follow-up email page (
/listings/:id/email?which=followup), and only shows when there is at least one visitor. The issue says "the visitors section on the listing"; that list is the visitors section, but it lives on the email page rather than the property page. - File: it is
text/csv; charset=utf-8, sent as an attachment named like412-alder-lane-visitors.csv. The header row isName,Email,Open house,Listing, with one row per visitor. - Open house column: visitors aren't tied to a particular open house in the database, so every row carries all of the listing's open houses, e.g.
2027-03-07 13:00-15:00; …. - Quoting: every field is quoted per RFC 4180, with lines ending in CRLF. A field starting with
=,+,-,@, tab or carriage return gets a leading'. - Code:
visitorsCsvinsrc/visitors.tsis a pure function. The route is insrc/routes.ts. I splitaddressSlugout ofkitFilenameinsrc/kit.tsso both filenames share it. - Access: the route asks
listVisitors, which goes throughpermiton the listing's existing update cell. Anyone else gets the same answer as the other author-only pages, with no email in it. No cell incontract.jsonchanged. The file is sent withcache-control: no-store. - Tests: in
src/followup.test.ts, a hostile visitor (=HYPERLINK("http://x","a"),@evil, Jo,-1@…, and an address with quotes) proves the quoting and the formula prefix. An end-to-end test shows the author gets the file, a stranger and anon get no emails, and the property page and launch kit never contain the visitor's email. - README: the open house section names the download.
I left the terminal, MCP and API docs alone, as the issue allows.
To try it, launch a listing with an open house, add a visitor on the follow-up email page, and click the link.
🤖 Generated with Claude Code
Clean. The route reads the listing, then lists visitors through listVisitors, which asks permit on the existing update cell. Strangers and anon get no data (the preview answers 401 to an unsigned request). No contract cell changes. Every CSV field is quoted with quotes doubled, formula leaders are defused, and the hostile-visitor tests cover both. The filename comes from a shared addressSlug, the response is no-store with the security headers, and the README names the download.
The listing's author can now download their open house visitors as a CSV at
GET /listings/:id/visitors.csv. The new test and the related suites I ran pass; I did not run the full suite. I also ran the design lint, which printed nothing. The type check did nothing here (the repo has notsconfig.json, andbun x tsc --noEmitonly printed tsc's help), andscripts/gate.tsandscripts/fast.tsdon't exist in this clone, so the gate and fast-test selection did not run./listings/:id/email?which=followup), and only shows when there is at least one visitor. The issue says "the visitors section on the listing"; that list is the visitors section, but it lives on the email page rather than the property page.text/csv; charset=utf-8, sent as an attachment named like412-alder-lane-visitors.csv. The header row isName,Email,Open house,Listing, with one row per visitor.2027-03-07 13:00-15:00; ….=,+,-,@, tab or carriage return gets a leading'.visitorsCsvinsrc/visitors.tsis a pure function. The route is insrc/routes.ts. I splitaddressSlugout ofkitFilenameinsrc/kit.tsso both filenames share it.listVisitors, which goes throughpermiton the listing's existing update cell. Anyone else gets the same answer as the other author-only pages, with no email in it. No cell incontract.jsonchanged. The file is sent withcache-control: no-store.src/followup.test.ts, a hostile visitor (=HYPERLINK("http://x","a"),@evil, Jo,-1@…, and an address with quotes) proves the quoting and the formula prefix. An end-to-end test shows the author gets the file, a stranger and anon get no emails, and the property page and launch kit never contain the visitor's email.I left the terminal, MCP and API docs alone, as the issue allows.
To try it, launch a listing with an open house, add a visitor on the follow-up email page, and click the link.
🤖 Generated with Claude Code
Closes #93