Filtering answers "show me bazaar"; it does not answer "where was that order about the blue chair". With ninety days of several apps' events (#48), the only way to find one is to already know its source and kind, and the page offers no field to type in.
Done looks like
[ ] listEvents takes q: trimmed, 2 to 64 characters, control characters refused like every other input, matched case-insensitively against subject and summary (not link, not source). Shorter than 2 characters is invalid with a message, not a silent full scan.
[ ] It is one SQL query with the existing filters, and it stays fast at the retention bound: either LIKE '%' || ? || '%' measured against a table filled to ninety days of a busy forge in the test (state the number of rows and the measured time in the pull), or an FTS5 virtual table kept by triggers — pick one, say why in the module comment, and prove the query plan is not a scan per request if FTS is chosen. bun:sqlite gives FTS5 without a dependency.
[ ] The parameter survives every link: the filter chips, the source and kind links, and (when it lands) the "Older" link all carry q forward, and clearing it is a link like the others. ?q= with no match says so and offers to clear, in the same words the empty filter state uses.
[ ] A search box in the toolbar: a GET form, so it works with JavaScript off, 44px tall, type="search", name="q", labelled, and placed so the page does not shift when results change. The typed value is escaped on the way back into the field.
[ ] Doors: ?q= on GET /api/events, q on the list_events MCP tool with the same bounds in its schema, and / in the terminal opening a prompt that filters the same way.
[ ] Tests: the bounds (1 character refused, 64 accepted, 65 refused), case-insensitivity, a match in summary but not subject, % and _ in the query treated as text and not as wildcards, the combination of q with source and kind, and the escaped round trip of a query containing <script>.
Risks: a LIKE with a leading wildcard cannot use an index — that is acceptable only because retention bounds the table, so whoever builds this states the row count it was measured at. %, _ and \ in user text must be escaped or the search silently means something else. This touches the same toolbar as #47; whichever lands first, the other rebases onto it rather than reordering the chips.
Filtering answers "show me bazaar"; it does not answer "where was that order about the blue chair". With ninety days of several apps' events (#48), the only way to find one is to already know its source and kind, and the page offers no field to type in.
Done looks like
listEventstakesq: trimmed, 2 to 64 characters, control characters refused like every other input, matched case-insensitively againstsubjectandsummary(notlink, notsource). Shorter than 2 characters isinvalidwith a message, not a silent full scan.LIKE '%' || ? || '%'measured against a table filled to ninety days of a busy forge in the test (state the number of rows and the measured time in the pull), or an FTS5 virtual table kept by triggers — pick one, say why in the module comment, and prove the query plan is not a scan per request if FTS is chosen.bun:sqlitegives FTS5 without a dependency.qforward, and clearing it is a link like the others.?q=with no match says so and offers to clear, in the same words the empty filter state uses.GETform, so it works with JavaScript off, 44px tall,type="search",name="q", labelled, and placed so the page does not shift when results change. The typed value is escaped on the way back into the field.?q=onGET /api/events,qon thelist_eventsMCP tool with the same bounds in its schema, and/in the terminal opening a prompt that filters the same way.summarybut notsubject,%and_in the query treated as text and not as wildcards, the combination ofqwithsourceandkind, and the escaped round trip of a query containing<script>.Files:
src/actions.ts,src/actions.test.ts,src/routes.ts,src/page.ts,src/mcp.ts,src/tty.ts,src/server.test.ts,README.mdRisks: a
LIKEwith a leading wildcard cannot use an index — that is acceptable only because retention bounds the table, so whoever builds this states the row count it was measured at.%,_and\in user text must be escaped or the search silently means something else. This touches the same toolbar as #47; whichever lands first, the other rebases onto it rather than reordering the chips.Suggested: Scout.