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

Two people editing one card do not silently overwrite each other #88

open not picked up yet opened this
opened this

Make a collision on one card visible, instead of resolving it in silence for whoever pressed Enter last.

What happens today, from the code on main at b6cf7bd:

  • db.editCard(boardId, cardId, title) writes unconditionally — there is no check that the row still holds what the editor started from. Two people typing in the same card both save; the second write wins and the first person's text is gone with nothing said. On a board the direction describes as shared by two people and an agent, that is the one place the app quietly loses work.
  • The client half is stranger. static/board.js defers a live card.edited while that card's input has focus (isEditingCard, line 449) and replays it on focusout (line 468), and applyNow's card.edited branch then sets input.value to the remote title. So if you focus a card, type, and click away without pressing Enter, your unsaved text is replaced by theirs — correct as conflict avoidance, invisible as an experience.

What done looks like

  • [ ] A card's edit form carries what the editor started from — the card's updated_at, or a short revision derived from it — as a hidden field, server-rendered.
  • [ ] db.editCard takes that expectation and refuses a stale write: the title is written only if the row still matches, inside the existing transaction, returning a distinct result ("stale") rather than false.
  • [ ] A stale save answers 409 and tells the person both sides: what the card says now, and what they typed, with an explicit Overwrite that saves anyway (carrying the fresh expectation). With JavaScript off this is the board re-rendered with the sentence, the way boardBad already does it; with the script, the toast plus the card's input left holding their text — never dropped.
  • [ ] The deferred-then-replayed case says so too: when a live card.edited replaces the text in an input someone has typed into, the page says the card changed under them rather than swapping the text silently. Their text is not destroyed by the swap.
  • [ ] An edit from the terminal and from /mcp go through the same editCard, so neither can take the unconditional path; an MCP edit_card (#68) passes no expectation and is documented as last-write-wins for agents, or is given the same field — decide and say which in the pull.
  • [ ] Tests: two sequential edits from the same starting point, the second refused; the refusal carries the current title; Overwrite succeeds; an edit with no expectation (an old form, the terminal) still works exactly as today.
  • [ ] bun test passes; the README says what happens when two people edit one card, in the What's simulated section if it stays a partial answer.

Files I expect to change: src/db.ts, src/routes.ts, src/pages/board.ts, static/board.<new hash>.js with BOARD_JS updated, src/db.test.ts, src/server.test.ts, src/live-refresh.test.ts, README.md.

Risks. A conflict check that is too tight turns ordinary editing into a wall of refusals — the expectation must be per card, never per board, and a card nobody else touched must save exactly as it does today. This edits the same card.edited path as #65 and the same card markup as #48; whichever lands second reads the other first. And the honest minimum, if the conflict UI grows past one run, is the detection plus the sentence — the Overwrite flow can be its own issue rather than a half-built dialog.

Who fits: scout to build it, reviewed by mason — the risk lives in the reconciliation code, which is the most intricate part of this app.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs