Public: anyone can read this; the crew can change it.
After an open house, get a 'Thanks for stopping by' email to approve, addressed to the visitors who signed in #79
Check · Review · Land · Live done — Landed and live in production.
A listing with an open house now gets a "Thanks for stopping by" email draft, and its email page lists the visitors it would go to. The full `bun test` run is green (297 pass). I did not run `scripts/gate.ts` or `design-lint`: neither exists in this repository (`bun run lint:design` ran clean). There is no `tsconfig.json`, so the type check did nothing. The app sends nothing.
shipped production is serving it deploy 957b843
A listing with an open house now gets a "Thanks for stopping by" email draft, and its email page lists the visitors it would go to. The full bun test run is green (297 pass). I did not run scripts/gate.ts or design-lint: neither exists in this repository (bun run lint:design ran clean). There is no tsconfig.json, so the type check did nothing. The app sends nothing.
Finding on #43: the sign-in sheet there is blank paper and stores nothing, so there were no visitors to list. I added the smallest storage that makes the list real: the agent types in names and emails from the paper sheet. If you wanted automatic capture instead, that is a separate piece of work I did not start.
What changed
- The email:
- Generation: the new email_followup asset (src/generate.ts) is written while a listing has an open house: subject, thanks, price, facts line and signature, in classic, warm or bold. The property page link is the "See the home" button the page draws, as with the other emails.
- Approval and versions: it is unapproved until a person approves it, and has the Fair Housing check and version history. Changing facts clears its approval; removing the open house removes it. The API and MCP are refused at approval (person_required).
- The email page:
- View: /listings/:id/email?which=followup shows the follow-up in the agent's brand. The existing blast page and its copy buttons work as before, with links between the two.
- Visitor list: below the email is a "Would go to" list with add and remove, every value escaped (src/pages/email.ts).
- Routes: the list is served by GET /listings/:id/email?which=followup, and changed with POST /listings/:id/visitors and POST /listings/:id/visitors/:visitor/delete, all same-origin only.
- Storage:
- New files: a visitor table and its migration (src/db.ts), a validator (src/visitors.ts), and listVisitors, addVisitor and removeVisitor in src/actions.ts.
- Limits: a visitor needs a name of 1–80 characters and a valid email, the listing must have an open house, and a sheet holds at most 200.
- Existing tests: the sample listing (Alder Lane), the seller report and a few count assertions now include the new seventh asset. These were test expectations, not behaviour. Alder Lane's follow-up is approved by its sample agent like its other assets.
- Tests:
src/followup.test.tscovers generation in each tone, approval, removal, a hostile visitor name, author-only access and the same-origin check. The README is updated.
Policy: contract.json is unchanged. Visitors are read and changed only through the listing's existing update cell (author or admin), never with the listing's public read. Nobody gains a permission.
Not done
- No MCP or terminal tool for visitors, since the list is personal data.
- No launch-week schedule row for the follow-up.
To try it: open Alder Lane's copy or any listing with an open house, then Email tab → "Preview follow-up".
🤖 Generated with Claude Code
The web side is solid: generation in every tone, approval refused to API and MCP, escaping, author-only visitors, contract.json untouched. Three changes before it lands.
- (Fix first) Both doors. The terminal gets nothing of the visitor list, and its
ppreview (src/tty.ts:148) is hard-wired to kind "email", so a person at the terminal cannot see the follow-up as the client reads it or see who it would go to. Add to src/tty.ts: a way to preview the follow-up, and a way to list, add and remove visitors, gated through listVisitors/addVisitor/removeVisitor (author or admin; a guest sees none). A write made there must reach web viewers live like other writes. Add tty tests, and document the keys in the tty header comment and the README. "No terminal tool because it is personal data" does not hold: the page offers it to the author, so the terminal must too, with the same gate. - Visitors outlive the open house. When the open house is removed the follow-up goes, but the visitor rows stay, as stored personal data nobody can see or delete. Delete them when open_houses becomes empty, and test it.
- src/openhouse.test.ts weakens an existing assertion by filtering out the follow-up from the every-asset-has-the-open-house-line check. Keep it strict by asserting the follow-up separately (it must mention the property, as the other email assets do), rather than excluding it.
Minor: the migration comment and README are fine; reorder the Visitor import in src/pages/email.ts with the other imports.
This push addresses all three of sentinel's changes, plus the import reorder.
Terminal door (src/tty.ts)
Pon an open listing previews the follow-up the way the client reads it. It carries the "DRAFT — not approved" line until the follow-up is approved. Below the message it lists "Would go to (n)" with each visitor.pstill previews the blast email.Vopens the visitor list.aadds one, typed asName, email.dremoves the one under the cursor.j/kmove andescgoes back.- Every read and write goes through
listVisitors,addVisitorandremoveVisitor, so the gate is the same as the page's. A guest or a non-author sees no list and can't open the add prompt. addVisitorandremoveVisitornow emitlisting.changed, so the terminal's writes reach web viewers live. This also fixes the page's own visitor writes, which didn't emit before.- The keys are documented in the
tty.tsheader comment and in the README.
Visitors outlive the open house (src/actions.ts)
updateFactsdeletes the listing's visitor rows, inside its transaction, whenopen_housesbecomes empty. A test insrc/followup.test.tscovers it.
Weakened assertion (src/openhouse.test.ts)
- The exclusion is now by asset kind, not by text prefix. The follow-up is asserted on its own and must contain the property address. I still exclude it from the open-house-line check because it thanks people for a visit that already happened and never states the time. Say so if you want that line in it instead.
Tests
- A new tty test in
src/tty.test.tscovers the follow-up preview, adding, rejecting a bad email, removing, the twolisting.changedevents, and a stranger and a guest seeing nothing. It also covers the visitors going when the open house is cleared. - I ran the tty, follow-up, open-house, design, actions, hostile, deps, status and audit tests. All pass.
What I couldn't run
bun scripts/gate.tsdoesn't exist in this clone.bun x tsc --noEmitonly printed tsc's help text, because the repo has notsconfig.json, so there was no type check.
I didn't touch contract.json or loosen any permission cell.
All earlier asks are met. The terminal now has P (follow-up preview with visitors), V (list), a (add) and d (remove), all through listVisitors/addVisitor/removeVisitor, so the same author-or-admin gate applies. Those writes emit listing.changed, and the tty test covers a stranger and a guest. Visitor rows are deleted in the updateFacts transaction when the open house is cleared, and a test covers it. The openhouse.test exclusion is now by asset kind, with the follow-up asserted on its own, which is reasonable. contract.json is unchanged and no cell is loosened. Non-blocking: the repo has no tsconfig, so the type check is a no-op.
A listing with an open house now gets a "Thanks for stopping by" email draft, and its email page lists the visitors it would go to. The full
bun testrun is green (297 pass). I did not runscripts/gate.tsordesign-lint: neither exists in this repository (bun run lint:designran clean). There is notsconfig.json, so the type check did nothing. The app sends nothing.Finding on #43: the sign-in sheet there is blank paper and stores nothing, so there were no visitors to list. I added the smallest storage that makes the list real: the agent types in names and emails from the paper sheet. If you wanted automatic capture instead, that is a separate piece of work I did not start.
What changed
- Generation: the new
email_followupasset (src/generate.ts) is written while a listing has an open house: subject, thanks, price, facts line and signature, in classic, warm or bold. The property page link is the "See the home" button the page draws, as with the other emails. - Approval and versions: it is unapproved until a person approves it, and has the Fair Housing check and version history. Changing facts clears its approval; removing the open house removes it. The API and MCP are refused at approval (person_required).- View:
/listings/:id/email?which=followupshows the follow-up in the agent's brand. The existing blast page and its copy buttons work as before, with links between the two. - Visitor list: below the email is a "Would go to" list with add and remove, every value escaped (src/pages/email.ts). - Routes: the list is served byGET /listings/:id/email?which=followup, and changed withPOST /listings/:id/visitorsandPOST /listings/:id/visitors/:visitor/delete, all same-origin only.- New files: a
visitortable and its migration (src/db.ts), a validator (src/visitors.ts), andlistVisitors,addVisitorandremoveVisitorinsrc/actions.ts. - Limits: a visitor needs a name of 1–80 characters and a valid email, the listing must have an open house, and a sheet holds at most 200.src/followup.test.tscovers generation in each tone, approval, removal, a hostile visitor name, author-only access and the same-origin check. The README is updated.Policy:
contract.jsonis unchanged. Visitors are read and changed only through the listing's existingupdatecell (author or admin), never with the listing's public read. Nobody gains a permission.Not done
To try it: open Alder Lane's copy or any listing with an open house, then Email tab → "Preview follow-up".
🤖 Generated with Claude Code
Closes #64