The second half of #7, which is cut down to sending. Once a decision fires a webhook and every attempt writes a delivery row, those rows need somewhere to be read: a webhook that quietly failed three times a week ago is worse than no webhook, because the group believes the thing on the other end knows.
What done looks like
[ ] listDeliveries(db, who, {proposalId?}) in src/actions.ts, admin only at the action (forbidden for a user, signin_required for anon) — the URL may name a private system, so this is not a public log.
[ ] /deliveries renders it for an admin: newest first, each row saying which proposal, when, which attempt, the status, the response code and the error in the words the sender recorded. A user or a guest gets the app's own 403, not a blank page.
[ ] A failed delivery is legible to somebody who did not write it: the row says what it means in a short sentence ("the endpoint answered 500 on all three attempts", "the endpoint did not answer within 5 seconds") rather than only a number.
[ ] Retry is one button per failed delivery, POST /api/deliveries/:id/retry, admin only, going through the same sender as the automatic attempts and writing a new delivery row rather than editing the old one — the log is append-only and the pull says so.
[ ] A retry of a delivery whose proposal has since been… nothing: a decision does not change, so a retry sends exactly the same body and the same signature it would have sent originally. Assert it in a test, because a body rebuilt from current rows is how signatures start disagreeing.
[ ] Retry is rate-limited by the bucket that already exists in src/http.ts — an admin holding the button down must not become an outbound flood.
[ ] /p/:id shows a decided proposal's delivery state in one line for an admin only ("delivered", "failed — see the log"), linking to /deliveries.
[ ] The delivery log is in the record export (#34's exportRecord) or explicitly excluded with a reason in the pull: it holds a URL that may be private, and that decision should be made deliberately rather than by default.
[ ] Tests: a user is refused at the page and the API; a retry writes a new row and sends the identical body and signature; a retry of a sent delivery is refused or is explicit that it will send again — pick one and say which in the README; the page escapes an error string containing <.
[ ] README.md says how to read the log and what retry does. bun test is green.
Files I expect to change:src/actions.ts, src/actions.test.ts, src/pages/deliveries.ts (new), src/pages/layout.ts, src/routes.ts, src/deliver.ts, src/server.test.ts, README.md.
Risks: an error string from the outside world is rendered on a page — escape it like any other untrusted text, and truncate it, because a remote endpoint can answer with a megabyte. Never render the signing secret or a header that carries it. The retry button is the app making an outbound request on a person's click: it goes through the same URL validation as the original send, since the stored setting may have been changed to something private since.
Fit: Hearth — the panel that says plainly what happened and offers the one thing to do about it. A person assigns; I do not.
The second half of #7, which is cut down to sending. Once a decision fires a webhook and every attempt writes a
deliveryrow, those rows need somewhere to be read: a webhook that quietly failed three times a week ago is worse than no webhook, because the group believes the thing on the other end knows.What done looks like
listDeliveries(db, who, {proposalId?})insrc/actions.ts, admin only at the action (forbiddenfor a user,signin_requiredfor anon) — the URL may name a private system, so this is not a public log./deliveriesrenders it for an admin: newest first, each row saying which proposal, when, which attempt, the status, the response code and the error in the words the sender recorded. A user or a guest gets the app's own 403, not a blank page.POST /api/deliveries/:id/retry, admin only, going through the same sender as the automatic attempts and writing a newdeliveryrow rather than editing the old one — the log is append-only and the pull says so.src/http.ts— an admin holding the button down must not become an outbound flood./p/:idshows a decided proposal's delivery state in one line for an admin only ("delivered", "failed — see the log"), linking to/deliveries.exportRecord) or explicitly excluded with a reason in the pull: it holds a URL that may be private, and that decision should be made deliberately rather than by default.sentdelivery is refused or is explicit that it will send again — pick one and say which in the README; the page escapes an error string containing<.README.mdsays how to read the log and what retry does.bun testis green.Files I expect to change:
src/actions.ts,src/actions.test.ts,src/pages/deliveries.ts(new),src/pages/layout.ts,src/routes.ts,src/deliver.ts,src/server.test.ts,README.md.Risks: an error string from the outside world is rendered on a page — escape it like any other untrusted text, and truncate it, because a remote endpoint can answer with a megabyte. Never render the signing secret or a header that carries it. The retry button is the app making an outbound request on a person's click: it goes through the same URL validation as the original send, since the stored setting may have been changed to something private since.
Fit: Hearth — the panel that says plainly what happened and offers the one thing to do about it. A person assigns; I do not.