With vote in src/actions.ts (#5) in place, give the page the one thing this app is named for. No new action and no new column: this pull decides nothing, it draws what the record already says.
What done looks like
[ ] /p/:id shows one vote button per option for a signed-in member — a form post, no JavaScript — at least 44px on a side, with that member's current choice marked in a word, never by colour alone.
[ ] Beside each option: how many counting ballots it has and who cast them. The group is small and votes are public, and the page says so in a sentence rather than leaving it to be discovered.
[ ] Under the options, the tally's reason exactly as src/quorum.ts wrote it, and the rule's describeRule line, so the page and the terminal say the same thing.
[ ] A guest sees the counts and no buttons, with one line saying who may vote. A signed-in person off the roster sees the counts and a line saying this group decides this one — not a dead button.
[ ] A decided proposal shows the winning option, when it was decided and the tally that decided it, and offers no buttons. A withdrawn one says so.
[ ] / shows each open proposal's progress using the tally's reason, and decided proposals with their winner — from the query listProposals already runs, never a tally per row.
[ ] The proposal region stays live on vote.cast and decision.made: read the comment at the top of src/pages/proposal.ts, keep whatever someone is typing outside the swapped region, and reserve the count's width so 9 becoming 10 shifts nothing.
[ ] A proposal whose stored rule no longer parses renders with a plain sentence saying the rule cannot be read, not a 500.
[ ] Tests in src/server.test.ts: a member sees buttons, a guest does not, a non-member sees the explaining line; posting a vote lands and the page returns with the choice marked; a decided proposal offers no buttons and names its winner; a voter's name containing < is escaped on the page; / shows a decided proposal's winner.
[ ] bun test is green and / still answers in single-digit milliseconds with a hundred proposals seeded.
Files I expect to change:src/pages/proposal.ts, src/pages/home.ts, src/routes.ts (the form post only), src/server.test.ts, static/app.css.
Risks: the form post goes through answer/back like every other write, with the same-origin check — a vote is a mutation and must never be reachable by a GET. Do not re-derive the tally in a template; if a number is missing, add it to what the action returns. Every name on this page is a person's name: escape all of them.
Fit: Hearth — the panel a person actually presses, honest about what pressing it will do. A person assigns; I do not.
With
voteinsrc/actions.ts(#5) in place, give the page the one thing this app is named for. No new action and no new column: this pull decides nothing, it draws what the record already says.What done looks like
/p/:idshows one vote button per option for a signed-in member — a form post, no JavaScript — at least 44px on a side, with that member's current choice marked in a word, never by colour alone.reasonexactly assrc/quorum.tswrote it, and the rule'sdescribeRuleline, so the page and the terminal say the same thing./shows each open proposal's progress using the tally'sreason, and decided proposals with their winner — from the querylistProposalsalready runs, never a tally per row.vote.castanddecision.made: read the comment at the top ofsrc/pages/proposal.ts, keep whatever someone is typing outside the swapped region, and reserve the count's width so 9 becoming 10 shifts nothing.src/server.test.ts: a member sees buttons, a guest does not, a non-member sees the explaining line; posting a vote lands and the page returns with the choice marked; a decided proposal offers no buttons and names its winner; a voter's name containing<is escaped on the page;/shows a decided proposal's winner.bun testis green and/still answers in single-digit milliseconds with a hundred proposals seeded.Files I expect to change:
src/pages/proposal.ts,src/pages/home.ts,src/routes.ts(the form post only),src/server.test.ts,static/app.css.Risks: the form post goes through
answer/backlike every other write, with the same-origin check — a vote is a mutation and must never be reachable by a GET. Do not re-derive the tally in a template; if a number is missing, add it to what the action returns. Every name on this page is a person's name: escape all of them.Fit: Hearth — the panel a person actually presses, honest about what pressing it will do. A person assigns; I do not.