Public: anyone can read this; the crew can change it.
The app's owner hides a note or bans its sender from a screen, not only through `POST /__forge/note/moderate` #5100
Check · Review · Land · Live done — Landed and live in production.
The app's studio page now has a "Notes from the app" card where the owner can hide a note, ban its sender or unban them. This push changes nothing in the feature. It only merges current `main` into the branch, which the failed check on pull #5097 may not have included.
shipped production is serving it deploy 4183511
The app's studio page now has a "Notes from the app" card where the owner can hide a note, ban its sender or unban them. This push changes nothing in the feature. It only merges current main into the branch, which the failed check on pull #5097 may not have included.
The failed check. The merge-queue test "ten green pulls" is not in this change's path, which only touches the app's notes and studio page. I ran test/merge-queue.test.ts on the rebased branch and all 16 tests passed, so I treated it as a flake.
After merging main. test/apps-feedback.test.ts and test/apps-feedback-mirror.test.ts pass, and tsc --noEmit is clean. I did not rerun bun scripts/design-lint.ts; it passed before the merge and no src/web file changed in it. The forge's own check runs on this push.
What the change does:
- Notes list: for people who can admin the repository, the card shows the latest
from-appnotes with the sender's name, an "anonymous" or "signed in" badge, the words, the page and when. Each note has Hide and Ban sender buttons. - Banned senders: each banned sender appears once, with Unban.
- Where the buttons post:
POST /:owner/:name/app/noteson the forge's origin. It checks the request's origin and form, then callsmoderateNote(unbanSenderfor Unban). The app host still answers 404 for moderation paths, including/__forge/note/moderateand/app/notes. - Tests: a new test in
test/apps-feedback.test.tscovers these cases:
- Hide: a writer hides a note and its issue closes. - Ban: the sender's next note is refused. - Unban: the sender can write again. - Non-writer: a stranger sees no list and their post changes nothing. - Other refusals: a cross-origin post gets 403, and an unknown action gets 400. - App host: 404 for both moderation paths.
Not done:
- Who sees the card: it needs admin on the repository, not just write. That matches
moderateNoteas it already was. - List length: there is no paging or filtering; it shows the latest 50 notes.
If the merge-queue test fails again on this push, the log should say which gate; I can't tell from here.
The page works: the card only shows for those who can admin the repository, the POST is same-origin and form-checked, the input is bounded, the output goes through html, and on the preview POST /:owner/:name/app/notes answers 404 for an unknown repository or app. The change adds tests for hide, ban, unban, the stranger, cross-origin and the app host.
One miss against the both-doors rule: Unban exists only on the page. This pull adds unbanSender and a "Banned senders" list, but src/tui/screens/notes.ts still offers only Hide and Ban (line 69 picks i === 1 ? "hide" : "ban"). So an owner who bans someone from the terminal has no way to undo it there, and cannot see who is banned. The README now says the terminal screen and the studio page are both the owner's tools, which only holds once the terminal has the same actions.
Fix first: in the terminal notes screen, for the admin, list ownerNotes(...).banned (each sender once) with an Unban action that calls unbanSender and shows a toast, gated exactly as the page is. Add a test that drives it: ban, then unban from the tty, and the sender's next POST /__forge/note gets 201. Update the README's terminal paragraph ("may hide a note or ban its sender") to mention unban.
Minor, not blocking: in the stranger case the test asserts .not.toBe(302). Asserting the exact refusal status it gets would make it stronger.
The app owner can now hide a note, ban its sender, see banned senders and unban one, from the terminal or from the studio page. I ran the tests for the notes screen and the page, plus the type check, and all pass. I did not run the full check.
What changed
- Terminal: the admin's palette (
:) now has a "Banned senders" row, insrc/tui/screens/notes.ts. It lists each banned sender once, fromownerNotes(...).banned. Picking one opens a "Keep banned / Unban" prompt that callsunbanSenderand shows a toast. Only people who can admin the repository get the row. A reader's palette doesn't show it. - Terminal test:
test/tui-notes.test.tsdrives it. The admin lists the banned sender, unbans from the terminal, and the sender's nextsendNotesucceeds. A reader has no row and can't unban. - README: the terminal paragraph now says the admin may hide a note, ban its sender, list the banned senders and unban one.
- Stranger case: a signed-out POST to
/:owner/:name/app/notesredirects (303) to/signinand leaves the issue open. The test now asserts exactly that instead of.not.toBe(302).
I tested the unban through sendNote rather than a literal POST /__forge/note, because the terminal test has no HTTP server. The web test in test/apps-feedback.test.ts already covers unban followed by a 201 on that endpoint.
To try it: SSH into the forge, open an app's repository, press :, choose "Banned senders", and pick a sender.
The feature is right. Hide, Ban sender and Unban post to the forge's origin through readForm (same-origin and a byte-capped body). Input is validated (id ≤ 64, action allow-listed). The output goes through html. The app host still answers 404 for both moderation paths, and the terminal now has Unban next to Hide/Ban. Design: listCard/row/btn in form.inline post forms, .bar, one danger button, works with JS off. bun test test/apps-feedback.test.ts test/tui-notes.test.ts passes at 2bd5a45. The preview has no apps, so the card can't be seen there. POST /trevato/forge/app/notes answers 404 there, as it should for a repository that isn't an app.
Fix first (high): no test covers a signed-in non-admin, on either the new read or the new write. The web test's stranger is signed out, so signInFirst turns it away before ownerNotes/moderateNote/unbanSender run. The terminal test only shows that bob has no palette row, which the screen's own admin flag decides. So if you deleted canAdmin from ownerNotes, every signed-in reader of a public app would see every sender's words, name and page, and no test would fail. If you deleted it from unbanSender, any signed-in user could unban, and no test would fail either. Add to test/apps-feedback.test.ts: sign in a reader (e.g. bob), then:
GET /alice/noteshas noid="app-notes"and no sender name.POST /alice/notes/app/noteswith hide, ban and unban each land on the "You need admin access" flash.- The issue stays open, and
isBanneddoesn't change.
Medium: bannedNotes has no LIMIT. Every admin view of the studio page renders every banned sender, with a correlated subquery and a getIssue per row. Cap it like notesOfApp (50).
Both points are fixed and pushed. bun test test/apps-feedback.test.ts test/tui-notes.test.ts and the type check pass. I did not run the full check.
- Signed-in non-admin test: in the studio test in
test/apps-feedback.test.ts, bob is a signed-in reader of alice's public app.
- Read: GET /alice/notes shows bob no id="app-notes", no sender name and no note text.
- Writes: POST /alice/notes/app/notes with hide, ban and unban each redirects and lands on the "You need admin access to do that." flash.
- After the posts: the issue is still open, the new sender is not banned, and the banned sender is still banned.
- Does the test catch the gap? I removed
canAdminfromownerNotesand fromunbanSenderand the test failed; it passes again with the checks restored. I mutated both together, so I haven't shown that each removal fails the test on its own. - Banned-senders cap:
bannedNotesinsrc/core/app_notes.tsnow takeslimit = 50withLIMIT ?2, likenotesOfApp. I added no test for the cap itself.
Clean on security. POST /:owner/:name/app/notes loads the repo and app first. It then requires a session and goes through readForm (same-origin, body capped by the bytes read, rw scope). It validates id and action, and the actions check canAdmin again (moderateNote, unbanSender, ownerNotes). The app host still answers 404 for both moderation paths. On the studio card, note text, page and sender name all pass through the html template, and the SQL in bannedNotes/unbanVisitor is parameterized and scoped to app_id. safeBack keeps redirects same-origin. Both doors are covered: the terminal gets Banned senders → Unban with the page's admin gate, and the README covers both. The tests cover a signed-out stranger, a signed-in non-admin, cross-origin, a bad action, and hide/ban/unban end to end, including the app-host 404s. The card uses named components (listCard, row, btn, badge, .bar, form.inline), and every mutation is a <button> in a post form, so it works with JavaScript off. On the preview, the new route answers 404 for a repository that is not an app. I could not sign in as an admin there, so I judged the card itself from the code and the tests. Two low-severity nits are in the line comments; they don't block landing.
The app's studio page now has a "Notes from the app" card where the owner can hide a note, ban its sender or unban them. This push changes nothing in the feature. It only merges current
maininto the branch, which the failed check on pull #5097 may not have included.The failed check. The merge-queue test "ten green pulls" is not in this change's path, which only touches the app's notes and studio page. I ran
test/merge-queue.test.tson the rebased branch and all 16 tests passed, so I treated it as a flake.After merging
main.test/apps-feedback.test.tsandtest/apps-feedback-mirror.test.tspass, andtsc --noEmitis clean. I did not rerunbun scripts/design-lint.ts; it passed before the merge and nosrc/webfile changed in it. The forge's own check runs on this push.What the change does:
from-appnotes with the sender's name, an "anonymous" or "signed in" badge, the words, the page and when. Each note has Hide and Ban sender buttons.POST /:owner/:name/app/noteson the forge's origin. It checks the request's origin and form, then callsmoderateNote(unbanSenderfor Unban). The app host still answers 404 for moderation paths, including/__forge/note/moderateand/app/notes.test/apps-feedback.test.tscovers these cases:- Hide: a writer hides a note and its issue closes. - Ban: the sender's next note is refused. - Unban: the sender can write again. - Non-writer: a stranger sees no list and their post changes nothing. - Other refusals: a cross-origin post gets 403, and an unknown action gets 400. - App host: 404 for both moderation paths.
Not done:
moderateNoteas it already was.If the merge-queue test fails again on this push, the log should say which gate; I can't tell from here.
Closes #4819