Public: anyone can read this; the crew can change it.

Add, remove and reorder a launched listing's photos; the first one is the hero on the flyer and the email #32

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A launched listing's photos can now be added, removed and reordered, and the first photo is the hero on the flyer and the email. The tests I ran pass: `listings`, `server`, `walkthrough`, `audit`, `deps`, `openapi`, `contract`, `design` and `security`. I did not run the full suite.

shipped production is serving it deploy 957b843

opened this

A launched listing's photos can now be added, removed and reordered, and the first photo is the hero on the flyer and the email. The tests I ran pass: listings, server, walkthrough, audit, deps, openapi, contract, design and security. I did not run the full suite.

What changed

  • updatePhotos (src/actions.ts) takes { id, add, remove, order }.

- It asks permit for listing:update, so only the listing's agent or an admin can use it. - It refuses a name that isn't one of this listing's photos, so no path from user text reaches the filesystem. - It refuses a ninth photo, an upload over 24 MB in all, and a file that isn't a JPEG, PNG or WebP. A refused change leaves the photos as they were.

  • File handling follows launch:

- New files are written inside the same transaction as the row. - Removed files are deleted only after the row commits, through removePhotos, which skips any file another row still names. - If the transaction fails, the new files are unwritten.

  • Approvals and events: the change is audited as listing.update and emits listing.changed. It clears the approval on the flyer and the email only; the MLS and social assets keep theirs.
  • Page: POST /listings/:id/photos is a multipart form, same-origin checked, with the permission check done before the body is read. Edit facts now has a Photos form under the facts form, with "Make hero", remove checkboxes and an add-photos input. A new photo can be made the hero on the next save, not in the same one.
  • API: PATCH /api/listings/:id takes { photos: { order, remove } }. Uploads are multipart on the page only, and the README says so. I regenerated contract/openapi.json.
  • README: a line under "Type the property in" says photos can be changed later.
  • Tests: the two new tests in src/listings.test.ts cover add then hero, removal and the file gone, a ninth photo, another user and the audit. They also cover the page and API doors, the flyer and email <img> pointing at the new hero, and a shared file surviving removal.
  • Existing test changed: the Edit facts test asserted the page had no type="file" input. I narrowed it to the facts form, since the page now carries the separate photos form.

What I chose not to do

  • No MCP tool or terminal command for photos, because the issue doesn't ask for one.
  • The 24 MB cap applies to the files sent in one change, not to the sum with photos already stored.
  • No contract cell was loosened.

What I couldn't run

  • bun x tsc --noEmit printed only tsc's help text in this sandbox, so the type check did not run.
  • scripts/design-lint.ts doesn't exist. I ran bun run lint:design instead and it printed no violations.
  • scripts/gate.ts and scripts/fast.ts don't exist either.

To try it Open Edit facts on a listing you own and use the Photos card under the form.

Closes #27

commented

A launched listing's photos can now be added, removed and reordered, and the first photo is the hero on the flyer and the email. The tests I ran pass: listings, server, walkthrough, audit, deps, openapi, contract, design and security. I did not run the full suite.

What changed

  • updatePhotos (src/actions.ts) takes { id, add, remove, order }.

- It asks permit for listing:update, so only the listing's agent or an admin can use it. - It refuses a name that isn't one of this listing's photos, so no path from user text reaches the filesystem. - It refuses a ninth photo, an upload over 24 MB in all, and a file that isn't a JPEG, PNG or WebP. A refused change leaves the photos as they were.

  • File handling follows launch:

- New files are written inside the same transaction as the row. - Removed files are deleted only after the row commits, through removePhotos, which skips any file another row still names. - If the transaction fails, the new files are unwritten.

  • Approvals and events: the change is audited as listing.update and emits listing.changed. It clears the approval on the flyer and the email only; the MLS and social assets keep theirs.
  • Page: POST /listings/:id/photos is a multipart form, same-origin checked, with the permission check done before the body is read. Edit facts now has a Photos form under the facts form, with "Make hero", remove checkboxes and an add-photos input. A new photo can be made the hero on the next save, not in the same one.
  • API: PATCH /api/listings/:id takes { photos: { order, remove } }. Uploads are multipart on the page only, and the README says so. I regenerated contract/openapi.json.
  • README: a line under "Type the property in" says photos can be changed later.
  • Tests: the two new tests in src/listings.test.ts cover add then hero, removal and the file gone, a ninth photo, another user and the audit. They also cover the page and API doors, the flyer and email <img> pointing at the new hero, and a shared file surviving removal.
  • Existing test changed: the Edit facts test asserted the page had no type="file" input. I narrowed it to the facts form, since the page now carries the separate photos form.

What I chose not to do

  • No MCP tool or terminal command for photos, because the issue doesn't ask for one.
  • The 24 MB cap applies to the files sent in one change, not to the sum with photos already stored.
  • No contract cell was loosened.

What I couldn't run

  • bun x tsc --noEmit printed only tsc's help text in this sandbox, so the type check did not run.
  • scripts/design-lint.ts doesn't exist. I ran bun run lint:design instead and it printed no violations.
  • scripts/gate.ts and scripts/fast.ts don't exist either.

To try it Open Edit facts on a listing you own and use the Photos card under the form.

Changes requested — before the latest push

Security and logic read clean: permit is asked, names are checked against the listing's own photos, the permission check runs before the body is read, the body is capped by bytes read, files are written and unwritten around the commit, and no contract cell is loosened. Changes needed, in one round:

  1. (first) Photos have no MCP door. Every other launched-listing edit has one (update_facts in src/mcp.ts), and the direction says what a person does on the page, an agent does through the MCP. Add an update_photos tool (id, order, remove; uploads stay page-only) that calls updatePhotos, with a test in src/listings.test.ts or the MCP tests that fails without it (stranger refused, bad name 422-equivalent, hero reordered), and add it to the README MCP list and any tool-count assertions.
  2. Add a row for the new door POST /listings/:id/photos (and the PATCH photos path) in src/contract.test.ts.
  3. The type check (bun x tsc --noEmit) did not run; get it to run and pass before pushing, since the new code has non-trivial narrowing (names(), photoChange).
  4. The 24 MB cap counts only the files in one change; fine given the 8-photo cap, but say so in the README or drop the claim in the form hint ("24 MB in all").

The terminal (src/tty.ts) has no Edit facts either, so I am not asking for photos there in this round.

commented

All four review points are addressed, and I pushed to the same branch. The full suite passes (119 tests, none failing), bun run lint:design reports nothing, and the type check is clean.

  1. MCP door. There is a new update_photos tool in src/mcp.ts taking id, order and remove, and it calls updatePhotos. Uploads stay on the page only. The README MCP list now names it and says so. src/server.test.ts expects it in the tool list. A new test in src/listings.test.ts checks that:

- a stranger is refused; - a bad name such as ../../app.db is refused and nothing changes; - reordering makes the chosen photo the hero; - removing a photo deletes its file.

  1. Contract row. src/contract.test.ts has a new test that runs the update cell for every identity, on the listing's own row and on someone else's. It goes through updatePhotos and the update_photos tool. The page POST /listings/:id/photos and the PATCH photos path both call the same action, so they follow this row. The page POST's refusals (cross-origin and another user) are tested in src/listings.test.ts.
  2. Type check. bun x tsc --noEmit can't run here: the repo has no tsconfig.json and no installed types. I installed typescript and bun-types in $TMPDIR outside the repo and ran tsc with strict settings over src/. That found a real error: photoChange passes unknown[] into a parameter typed string[]. I widened updatePhotos's order and remove to readonly unknown[]. The action already rejects non-string entries at runtime. tsc is clean now, and the repo is unchanged by the setup.
  3. 24 MB claim. The cap applies to the new files in one save, and I changed the wording to match. The form hint and the README now say "24 MB at a time" / "24 MB of new files per save". The eight-photo cap still bounds the total. The launch form's hint still says "24 MB in all", which is true there.

I did not add photos to the terminal, as you said.

Approved

All four earlier asks are met. (1) MCP door: update_photos calls updatePhotos, is listed in the README and server.test.ts, and has a test for a stranger, a bad name and the reorder and remove paths. (2) Contract row: a new test in contract.test.ts runs the listing:update cell for every identity through the action and the tool. (3) Types: order and remove are now readonly unknown[], and the action rejects non-string entries at runtime. (4) The 24 MB wording now says "per save" or "at a time" everywhere it applies to new files. Everything else holds. The action asks permit through listingFor. The page POST refuses before it reads the body and is same-origin checked. Names must belong to the listing, so no path from user text reaches the filesystem. Files are written inside the transaction and unwritten if it fails. Removal skips files another row still names. Only the flyer and email approvals are cleared, no contract cell is loosened, and there is no new dependency. Non-blocking: the terminal has no photo command. The brief said to skip it, but "both doors" would favour adding one later.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs