Public: anyone can read this; the crew can change it.
Propose, read and withdraw: a proposal with options replaces the note #12
Check · Review · Land · Live done — Landed and live in production.
A proposal — a title, a paragraph and two to eight options — is now what this app is about at every door, and the template's note is gone.
shipped production is serving it deploy 2adbe7d
commented
probe
Approved
probe
Sign in to join this conversation.
A proposal — a title, a paragraph and two to eight options — is now what this app is about at every door, and the template's note is gone.
Two appended migrations do the schema:
proposal(title 1–120, body 1–2000,ruledefaultmajority,statechecked againstopen/decided/withdrawn, nullabledecided_option_id, author, timestamps) withoption(label 1–80,position, cascading from its proposal) and an index onoption(proposal_id); thenDROP TABLE note. Migration 1 is untouched, andsrc/db.test.tsbuilds a database that stopped at the note and migrates it forward, so production's existing file is covered.src/actions.tsislistProposals,getProposal,propose,withdrawProposaland an exportedcanWithdraw.proposerefusesanon, a long title, a long paragraph, fewer than two or more than eight options, blank labels, over-long labels and duplicates (compared case-insensitively) — eachinvalidwith a sentence naming what to change — trims every field and writes the proposal with its options in one transaction, so a refused proposal writes nothing.withdrawProposalis the author or an admin and only whileopen, so a second withdrawal isinvalidrather than silently fine. Both emit, and both pages carrydata-live="proposal.opened proposal.withdrawn".The doors follow and decide nothing:
src/pages/layout.ts+home.ts+proposal.ts(/lists open proposals with the composer,/p/:idshows the paragraph, the options in order and a Withdraw form for whoever may; a guest sees both and no forms, every value throughescape), the form and JSON routes insrc/routes.ts(anything that is not a proposal is a 404 in the layout; statuses followFailurethroughanswer),list_proposals/get_proposal/propose/withdraw_proposalover MCP with limits in their schemas, and a terminal wherej/kmoves, Enter opens,nasks for a title then the paragraph then options one per line until a blank line,wwithdraws and Esc cancels.Two things I chose not to do. Only
majorityis an acceptedrule— the other rules arrive with the rules module (#3), and until then an unknown rule isinvalidrather than silently stored. And the composer's blank lines are dropped by the route when it splits the textarea into a list, which is wire shaping, not a rule: the action still refuses a blank label, which is what the API and MCP see.back()gained a location so a form can return to/p/:id;src/server.ts's same-origin refusal and rate limit are exactly as they were. Worth a look in the preview: propose something, watch it appear on/in another tab without a reload, then withdraw it and see it leave the list but stay readable at its own address.bun testis green (26 tests), no dependencies added.🤖 Generated with Claude Code
Closes #2