Public: anyone can read this; the crew can change it.
The front page shows more than what is open: a state filter and each proposal's activity #37
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error)
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error)
Amendment from the planner. The issue stands: production's / shows only open proposals, and a withdrawn one is reachable from no page in the app.
Added boundary, now that #44 exists: the progress line on a row — the tally's reason — belongs to #44, not here. This issue gives / its state filter, its counts and each row's activity. Whichever of the two lands second keeps both; neither may re-derive the other's numbers in a template.
Narrowed, to keep it one run: drop sort from MCP's list_proposals and from /api/proposals. / sorts by activity, and that is the whole user-visible point; a sort parameter on two more doors is a second issue's worth of schema, sentences and tests for a thing nobody has asked for yet.
The park here was a DNS failure — nothing was attempted.
/callslistProposalswithstate: "open"and nothing else, so a withdrawn proposal is unreachable from any page in the app — only from/api/proposals?state=withdrawn— and a row says how many options a proposal has but nothing about whether anyone has said a word. Give the list a filter and a pulse.What done looks like
listProposalsreturnsremark_countandlast_activity_at(the later of the proposal'supdated_atand its newest remark'screated_at) on every row, from the one query it already runs — no second query per proposal.listProposalstakes an optionalsort, one ofnewest(default,id DESC, what it does today) oractive(last_activity_at DESC); anything else isinvalidwith a sentence./asks foractive;/api/proposalskeepsnewestunless the caller says otherwise, so no existing script changes shape./takes?state=open|decided|withdrawn|all, defaultopen. The filter is a row of plain links — no JavaScript — each naming its state and the number of proposals in it, with the current one markedaria-current="page". An unknown value renders the default list with 200 and no scolding.static/live.jsrefetcheslocation.href, so the section keeps itsidand a proposal that opens while?state=withdrawnis on screen does not silently appear.src/tty.ts) and MCP'slist_proposalscarry the same two new facts, andlist_proposalsgainssortin its schema with one sentence./and present under?state=withdrawn;?state=nonsenseanswers 200 with the open list; the state counts are right with proposals in all three states;remark_countandlast_activity_atare right for a proposal with remarks and for one without;sort=activeputs a freshly remarked older proposal first; a seeded 200 proposals with 1000 remarks still answers/in well under 50 ms.Files I expect to change:
src/actions.ts,src/actions.test.ts,src/pages/home.ts,src/routes.ts,src/mcp.ts,src/tty.ts,src/server.test.ts,static/app.css.Risks: The state is a word from a query string — whitelist it against
STATESplusalland never interpolate it into SQL; the existingSTATEScheck is the pattern.last_activity_atasmax(p.updated_at, coalesce(newest remark, p.updated_at))in SQL keeps it one query; doing it in TypeScript means pulling every remark. This is not #24: decided proposals get their own page and feed there, and this filter must not grow a feed or a tally. Keep the empty state from #20 — an empty?state=withdrawnneeds its own one-line empty text, not the four-sentence first-visit block.Fit: Ledger — the record as a product: every state reachable, every row saying when it last moved. A person assigns; I do not.