Someone opens relay and sees what the apps on this forge have been doing, newest first, with the filters as plain links and new events arriving without a reload.
Done looks like
[ ] / renders the newest 50 events server-side, grouped under a heading per day; each row is the source as a badge, the kind, the subject in bold, the one-line summary, a relative time inside <time datetime="...">, and the link as <a rel="noopener noreferrer"> when there is one. Every field through escape().
[ ] Filters are a GET query, ?source=&kind=, rendered as chips that are links: clicking a source badge filters to it, clicking the active chip clears it, and the state lives in the URL so it can be shared and survives a reload. It works with JavaScript off. An invalid filter shows the action's message above the list, not a blank page.
[ ] Live: the list carries data-live="event.posted" and static/live.js prepends a row it builds with textContent (never innerHTML) for poster-controlled text, only when the event matches the filter in the URL, dropping a duplicate id. No layout shift when a row arrives.
[ ] A phone is first class: 360px with no horizontal scroll, every control at least 44px on a side, the summary wrapping rather than truncating to nothing, and the keyboard reaching every chip.
[ ] The empty state is useful: it tells an app how to post — the four headers and one curl — and links docs/adopting.md once that exists.
[ ] Notes are gone from src/page.ts and static/app.css: no dead selectors, no orphan form.
[ ] src/server.test.ts asserts the first paint contains the newest event with no JavaScript, that a <script> in a summary comes back escaped, that a filter round-trips, and that the CSP header is unchanged — nothing inline was added to make this work.
Risks: every string on this page came from another app — the render escapes and the live path uses textContent, or relay becomes an XSS delivery service for the whole forge. The link is validated in the action but still needs escaping in the href. Keep the CSP as it is.
Suggested: Scout — one page, instant feedback, the least that fully works.
Someone opens relay and sees what the apps on this forge have been doing, newest first, with the filters as plain links and new events arriving without a reload.
Done looks like
/renders the newest 50 events server-side, grouped under a heading per day; each row is the source as a badge, the kind, the subject in bold, the one-line summary, a relative time inside<time datetime="...">, and the link as<a rel="noopener noreferrer">when there is one. Every field throughescape().?source=&kind=, rendered as chips that are links: clicking a source badge filters to it, clicking the active chip clears it, and the state lives in the URL so it can be shared and survives a reload. It works with JavaScript off. An invalid filter shows the action's message above the list, not a blank page.data-live="event.posted"andstatic/live.jsprepends a row it builds withtextContent(neverinnerHTML) for poster-controlled text, only when the event matches the filter in the URL, dropping a duplicate id. No layout shift when a row arrives.curl— and linksdocs/adopting.mdonce that exists.src/page.tsandstatic/app.css: no dead selectors, no orphan form.src/server.test.tsasserts the first paint contains the newest event with no JavaScript, that a<script>in a summary comes back escaped, that a filter round-trips, and that the CSP header is unchanged — nothing inline was added to make this work.Files:
src/page.ts,src/routes.ts,static/app.css,static/live.js,src/server.test.tsRisks: every string on this page came from another app — the render escapes and the live path uses
textContent, or relay becomes an XSS delivery service for the whole forge. Thelinkis validated in the action but still needs escaping in thehref. Keep the CSP as it is.Suggested: Scout — one page, instant feedback, the least that fully works.