listEvents pages with before, and no human door passes it: GET / asks for the default fifty and renders them, so the fifty-first event ever posted is reachable from /api/events?before= and from nowhere a person can click. A hub that forgets everything but today is not a hub.
Done looks like
[ ] GET / reads before from the query string, validates it as a positive integer at the boundary (anything else is the same invalid notice the bad-filter path already shows, never a silent ignore), and passes it to listEvents alongside source and kind.
[ ] The page asks for LIMIT_DEFAULT + 1 rows and renders at most LIMIT_DEFAULT; the extra row is how it knows there is more, so "Older" never appears on the last page and no second COUNT(*) is run.
[ ] When there is more, the page ends with a single <a class="button" href="...">Older events</a> whose href carries the current source and kind forward and sets before to the id of the last rendered row — built with the existing filterHref helper extended to take before, not a second way of building a link. At least 44px on a side.
[ ] A page that is not the first says so and can get back: a "Newest" link beside "Older" that drops before and keeps the filters. No "previous page" arithmetic — ids are not offsets, and cursor paging only goes one way honestly.
[ ] Clicking a source or kind chip while paged clears before: a new filter starts at the newest, which is what every reader expects.
[ ] The live region behaves: static/live.js prepends a new event to the list on page one only. On a paged view (before present) a new event must not be inserted into the middle of history — show the count in the existing live region or nothing at all, and say which in a comment.
[ ] The terminal gets the same reach: src/tty.ts loads the next page when the cursor walks off the bottom, so j held down scrolls into the past instead of stopping at fifty.
[ ] Tests in src/server.test.ts: with 51 events the first page shows 50 and an "Older" link whose before is the 51st-from-newest id; following that link shows the rest and no "Older"; ?before=abc renders the invalid notice with a 200 and no rows lost; ?source=x&before=N keeps both in the link.
Issue #12 (subscriptions) also edits src/page.ts's toolbar and src/routes.ts's / handler, and #10 rewrites src/tty.ts around src/screen.ts. Whichever lands second rebases; keeping the paging logic inside filterHref and the route, rather than sprinkled through the renderers, keeps that rebase small. If #10 has landed, put the terminal's paging behind render() instead of in the draw loop.
Ordering is by id DESC, and occurred_at can be backdated up to 30 days — so day headings on an older page can repeat a day already seen. That is correct (arrival order is the only stable cursor) but should be a comment, not a surprise.
Suggested
Scout — one page, one route, keyboard reach, a test that fails without it.
listEventspages withbefore, and no human door passes it:GET /asks for the default fifty and renders them, so the fifty-first event ever posted is reachable from/api/events?before=and from nowhere a person can click. A hub that forgets everything but today is not a hub.Done looks like
GET /readsbeforefrom the query string, validates it as a positive integer at the boundary (anything else is the sameinvalidnotice the bad-filter path already shows, never a silent ignore), and passes it tolistEventsalongsidesourceandkind.LIMIT_DEFAULT + 1rows and renders at mostLIMIT_DEFAULT; the extra row is how it knows there is more, so "Older" never appears on the last page and no secondCOUNT(*)is run.<a class="button" href="...">Older events</a>whose href carries the currentsourceandkindforward and setsbeforeto the id of the last rendered row — built with the existingfilterHrefhelper extended to takebefore, not a second way of building a link. At least 44px on a side.beforeand keeps the filters. No "previous page" arithmetic — ids are not offsets, and cursor paging only goes one way honestly.before: a new filter starts at the newest, which is what every reader expects.static/live.jsprepends a new event to the list on page one only. On a paged view (beforepresent) a new event must not be inserted into the middle of history — show the count in the existing live region or nothing at all, and say which in a comment.src/tty.tsloads the next page when the cursor walks off the bottom, sojheld down scrolls into the past instead of stopping at fifty.src/server.test.ts: with 51 events the first page shows 50 and an "Older" link whosebeforeis the 51st-from-newest id; following that link shows the rest and no "Older";?before=abcrenders the invalid notice with a 200 and no rows lost;?source=x&before=Nkeeps both in the link.Files
src/routes.ts,src/page.ts,src/tty.ts,static/live.js,src/server.test.ts.Risks
src/page.ts's toolbar andsrc/routes.ts's/handler, and #10 rewritessrc/tty.tsaroundsrc/screen.ts. Whichever lands second rebases; keeping the paging logic insidefilterHrefand the route, rather than sprinkled through the renderers, keeps that rebase small. If #10 has landed, put the terminal's paging behindrender()instead of in the draw loop.id DESC, andoccurred_atcan be backdated up to 30 days — so day headings on an older page can repeat a day already seen. That is correct (arrival order is the only stable cursor) but should be a comment, not a surprise.Suggested
Scout — one page, one route, keyboard reach, a test that fails without it.