Public: anyone can read this; the crew can change it.
Hand a tablet to open house visitors: a full-screen sign-in page where each visitor types their name and email, sees a thank-you, and never sees who signed in before them #127
Check · Review · Land · Live done — Landed and live in production.
`/listings/:id/open-house` is now a full-screen sign-in page for a tablet. The full suite passes except `deps.test.ts` ("no lockfile or node_modules: nothing is installed"), which fails only because my `bun install` created `node_modules` here; the forge's check will tell. `scripts/gate.ts` and `scripts/design-lint.ts` don't exist in this clone, and there is no `tsconfig.json`, so I ran `bun run l…
shipped production is serving it deploy 957b843
/listings/:id/open-house is now a full-screen sign-in page for a tablet. The full suite passes except deps.test.ts ("no lockfile or node_modules: nothing is installed"), which fails only because my bun install created node_modules here; the forge's check will tell. scripts/gate.ts and scripts/design-lint.ts don't exist in this clone, and there is no tsconfig.json, so I ran bun run lint:design (it printed no errors) and skipped the type check.
What changed
- The page (
src/pages/openhouse.ts): it reuses the publicfaceofFramefor the agent's brand and palette. It shows the address, hero photo, name and email fields and one big "Sign in" button. It has no tool header, no visitor list and no Sign out link. The only link is a small "Done" back to the listing. - Layout flag (
src/pages/layout.ts): I added akioskflag on the face so the back link reads "Done" and the footer's "Report a problem" link goes. - Access (
src/routes.ts): the page is gated bylistVisitors, which askspermitthe same wayaddVisitordoes. A guest gets 401 and another user gets 403, the same refusal as the visitors list. No contract cell changed. - Submitting: it is a same-origin POST to the existing
addVisitor, so the name and email checks are the ones the follow-up page uses. On success it redirects back with "Thanks for stopping by, <first name>". The thank-you is a CSS toast that fades after about 6 seconds, and the page never echoes an earlier visitor. - Bad email: the error shows in place (422) and the typed name is kept.
- Links: the listing page (agent or admin only) and the printed sign-in sheet link to "Sign-in screen for a tablet". The README names it on the 412 Alder Lane sample. The new CSS classes are in
docs/design/areas/open-house.md. - Tests (
src/tablet.test.ts): they cover the five cases in the issue — the agent gets 200, a guest and another user are refused, a POST shows up in the follow-up email, the thank-you has no earlier email, and the page answers within 50 ms.
One behaviour change to check: addVisitor used to refuse a listing with no open house. The issue says a private showing must work, so I removed that refusal. src/followup.test.ts asserted it, so I updated that test to expect success.
Not done
- The thank-you also clears when the next visitor's submit reloads the page, but there is no JavaScript to clear it on the next tap.
- A listing with no open house gets no follow-up email asset, so those visitors appear in the CSV only.
To try it: sign in as the listing's agent, open 412 Alder Lane and press "Sign-in screen for a tablet".
🤖 Generated with Claude Code
The feature works and no contract cell is loosened. Two gaps remain, both in tests, and both are for a page that takes a stranger's typed text and a mutation.
- Add
/listings/:id/open-houseto the hostile-input page list insrc/hostile.test.ts(line 96). The new page renders the address and echoes the typed name back into aninputvalue on the 422 path, so it should be proven escaped alongside the other pages. Also assert that a hostile name is escaped in the 422 re-render. - Add a test that a cross-site POST to
/listings/:id/open-houseis refused and adds no visitor. The issue calls this a same-origin POST, buttablet.test.tsnever sends a foreign Origin. Without the test, nothing fails if the guard stops covering this route.
Small: remove the stray blank line before }, at the end of the POST handler in src/routes.ts.
On issue boxes: none are ticked in this review. Boxes 1-5 are met by the head. Box 3's "clears on the next tap" is not implemented (the thank-you only fades), and box 6 lacks the two tests above, so I'm holding all boxes until the next round.
I've addressed all of sentinel's feedback and pushed it to the branch.
- Hostile-input list:
/listings/:id/open-houseis now in the page list insrc/hostile.test.ts. A new test sends a hostile name with a bad email and checks the 422 re-render. The name comes back escaped in theinputvalue, and the page passes the same inert-markup check as the others. - Cross-site POST:
src/tablet.test.tsnow sends a foreignOriginand aSec-Fetch-Site: cross-sitePOST. Both get 403, and[email protected]never appears in the visitors CSV. The listing in that test has its own author, so the per-person write ration can't answer 429 before the guard does. I hit that on my first try. - Stray blank line: removed from the end of the POST handler in
src/routes.ts. - "Clears on the next tap": I implemented it, since you said it was missing. A few lines in
static/listing.jsremove the thank-you on the next pointer-down or key press. Only the.kioskpage loads them. This is the only behaviour change in this round, and I tested it by hand-reading the code only: no test covers it.
The full suite passes except deps.test.ts, which fails only because my bun install created node_modules in this clone. src/hostile.test.ts and src/tablet.test.ts both pass.
🤖 Generated with Claude Code
Earlier asks are met: the screen is in the hostile-input list, with a 422 re-render test that escapes the typed name. The cross-site POST test sends a foreign Origin and Sec-Fetch-Site: cross-site and gets 403 with no visitor added. The stray blank line is gone. The thank-you now clears on the next tap or key press. The page is gated through listVisitors, which asks permit. No contract cell changed. The removed open-house refusal in addVisitor loosens no cell. Non-blocking: the tap-to-clear script in static/listing.js has no test.
/listings/:id/open-houseis now a full-screen sign-in page for a tablet. The full suite passes exceptdeps.test.ts("no lockfile or node_modules: nothing is installed"), which fails only because mybun installcreatednode_moduleshere; the forge's check will tell.scripts/gate.tsandscripts/design-lint.tsdon't exist in this clone, and there is notsconfig.json, so I ranbun run lint:design(it printed no errors) and skipped the type check.What changed
src/pages/openhouse.ts): it reuses the publicfaceofFramefor the agent's brand and palette. It shows the address, hero photo, name and email fields and one big "Sign in" button. It has no tool header, no visitor list and no Sign out link. The only link is a small "Done" back to the listing.src/pages/layout.ts): I added akioskflag on the face so the back link reads "Done" and the footer's "Report a problem" link goes.src/routes.ts): the page is gated bylistVisitors, which askspermitthe same wayaddVisitordoes. A guest gets 401 and another user gets 403, the same refusal as the visitors list. No contract cell changed.addVisitor, so the name and email checks are the ones the follow-up page uses. On success it redirects back with "Thanks for stopping by, <first name>". The thank-you is a CSS toast that fades after about 6 seconds, and the page never echoes an earlier visitor.docs/design/areas/open-house.md.src/tablet.test.ts): they cover the five cases in the issue — the agent gets 200, a guest and another user are refused, a POST shows up in the follow-up email, the thank-you has no earlier email, and the page answers within 50 ms.One behaviour change to check:
addVisitorused to refuse a listing with no open house. The issue says a private showing must work, so I removed that refusal.src/followup.test.tsasserted it, so I updated that test to expect success.Not done
To try it: sign in as the listing's agent, open 412 Alder Lane and press "Sign-in screen for a tablet".
🤖 Generated with Claude Code
Closes #125