Public: anyone can read this; the crew can change it.
Print an 'Open house' directional sign for a listing: big OPEN HOUSE, the date and time, an arrow, the agent's name and phone, in the agent's brand #136
Check · Review · Land · Live done — Landed and live in production.
`/listings/:id/sign` now prints the open house directional sign, and the listing page links to it as "Print the open house sign" beside the sign-in sheet when the listing has an open house. I did not push-test the print layout in a real browser, so the one-page landscape fit is from the CSS only. The new `src/sign.test.ts` and the neighbouring suites (design, a11y, speed, everywhere, hostile, walk…
shipped production is serving it deploy 957b843
/listings/:id/sign now prints the open house directional sign, and the listing page links to it as "Print the open house sign" beside the sign-in sheet when the listing has an open house. I did not push-test the print layout in a real browser, so the one-page landscape fit is from the CSS only. The new src/sign.test.ts and the neighbouring suites (design, a11y, speed, everywhere, hostile, walkthrough) pass. bun scripts/lint-design.ts exits 0. bun x tsc --noEmit can't run here because the repo has no tsconfig.json, so the type check did not run. I did not run the full suite.
What the sign shows
- It is a Letter landscape page with OPEN HOUSE in the agent's colour and the first upcoming open house's day and time, worded by the same
whenOfformatter the flyer and email use. For the sample listing that reads "Sunday, March 7, 1–3 pm", not the shorter "Sunday 1–3 pm" in the issue. - It also shows the street address, a large arrow, the agent's name and phone, and the property page QR (
qrSvg). The QR is left out while the property page isn't public yet, as on the flyer. - The page has no JavaScript of its own and no remote assets. The layout's usual scripts still load, as on every page.
- The arrow is
?arrow=left,right(the default) orup; any other value givesright. The three choices are links above the printed area and are hidden when printing. - Long addresses wrap inside the page, and the print styles are set to keep it to one page.
Notices instead of a sign
- A listing with no upcoming open house gets a plain notice saying to add one first, with a link back to the listing.
- A sold, withdrawn or expired listing gets a notice saying it is off the market.
Access and styling
- The route reads the listing through
getListing, so the read rule is the same as the flyer's. No contract cell changed, and the flyer and sign-in pages are untouched. - The new component classes are in
docs/design/areas/open-house.md. The CSS is instatic/app.css, with its own@page signfor landscape. - The README sign-in step now describes the sign and names 412 Alder Lane as the sample with an open house.
Tests in src/sign.test.ts
- The sample's day and time, and the same QR as the flyer.
- Each arrow value and an unknown one.
- Both notices.
- A hostile address and agent name stay escaped.
- The sign answers inside 50 ms at the 95th percentile.
Not done
- No format that says "Sunday 1–3 pm" without the date; I reused the existing formatter as the issue's risk note asked.
- No per-arrow persistence; the arrow lives only in the query string.
🤖 Generated with Claude Code
I think I've fixed the two failures from the preview deploy, but I couldn't reproduce them here, so the cause is inferred. The failing src/tablet.test.ts tests pass here in the files I ran. A full local bun test fails one unrelated test, "no lockfile or node_modules: nothing is installed", which the deploy log didn't list.
My best explanation is shared write rationing. src/sign.test.ts signed in as pat, the same identity src/tablet.test.ts uses. If the deploy runs every test file in one process, the rationing limit (a burst of 20 writes per person) is shared. My new sign tests would have spent writes pat needed, so the tablet suite's launches failed fast. The two failures took 10.78 ms and 0.55 ms, which fits a request refused outright rather than a slow page.
The fix is in src/sign.test.ts: it now signs in as its own person, mason-sign. I changed no app code. src/sign.test.ts, src/tablet.test.ts, src/signin.test.ts and src/postcard.test.ts pass together (18 tests), and the fix is pushed. The forge's full check on the new push will show whether the cause was right.
Clean and in the repo's pattern: arrow is whitelisted, all text is escaped, the read goes through getListing (no contract cell changed), tests cover the stated behaviours. Non-blocking: the openHouseLine(l) === null check in src/pages/sign.ts is redundant with nextOpenHouse; the one-page print fit is unverified in a real browser; the day/time reads 'Sunday, March 7, 1–3 pm' (existing whenOf) rather than the shorter wording, which the issue's risk note allows.
/listings/:id/signnow prints the open house directional sign, and the listing page links to it as "Print the open house sign" beside the sign-in sheet when the listing has an open house. I did not push-test the print layout in a real browser, so the one-page landscape fit is from the CSS only. The newsrc/sign.test.tsand the neighbouring suites (design, a11y, speed, everywhere, hostile, walkthrough) pass.bun scripts/lint-design.tsexits 0.bun x tsc --noEmitcan't run here because the repo has notsconfig.json, so the type check did not run. I did not run the full suite.What the sign shows
whenOfformatter the flyer and email use. For the sample listing that reads "Sunday, March 7, 1–3 pm", not the shorter "Sunday 1–3 pm" in the issue.qrSvg). The QR is left out while the property page isn't public yet, as on the flyer.?arrow=left,right(the default) orup; any other value givesright. The three choices are links above the printed area and are hidden when printing.Notices instead of a sign
Access and styling
getListing, so the read rule is the same as the flyer's. No contract cell changed, and the flyer and sign-in pages are untouched.docs/design/areas/open-house.md. The CSS is instatic/app.css, with its own@page signfor landscape.Tests in
src/sign.test.tsNot done
🤖 Generated with Claude Code
Closes #135