Public: anyone can read this; the crew can change it.

An admin can post an event by hand, from the page and from the terminal #43

open not picked up yet opened this
opened this

postEvent has allowed an admin to post since the spine merged, and the only door that offers it is the MCP tool: on the page and in the terminal relay is read-only, so today the timeline on production is empty and the one person who is allowed to put something on it has to speak JSON-RPC to do it. An admin announcing a deploy, or trying the hub before any app is registered, should not need curl.

Done looks like

  • [ ] A <details> section on /, rendered only when who.role === "admin", closed by default so it costs a reader nothing: a form posting same-origin to POST /events/new with source, kind, subject, summary and an optional link. No JavaScript — it works with the browser's own submit, and returns a redirect to / on success (303) so a refresh does not repost.
  • [ ] Failure is shown, not swallowed: an invalid result re-renders the page with the action's own message above the form and the fields the person typed still in it (escaped), status 422. signin_required and forbidden never reach the form, because the section is not rendered — but the route checks the role anyway, since the route is the thing an attacker calls.
  • [ ] The route decides nothing: it reads the form body (readBody + URLSearchParams, capped, never a Content-Length), calls postEvent, and shapes the answer. Every rule — who may, what a valid kind is, the 5-minute future bound — stays in the action.
  • [ ] The same in the terminal: n in src/tty.ts prompts for the five fields on the status line, calls postEvent, and reports the named failure verbatim. A guest or a plain user pressing n is told the terminal gates exactly as the page does. The prompt reads raw keys without shelling out and without eval.
  • [ ] A terminal post shows on the page on the next load, not live, and the terminal says so in one dim line: the live bus belongs to the web process (src/live.ts says this already — do not invent a second bus to work around it).
  • [ ] 360px with no horizontal scroll, every control at least 44px, the keyboard reaches the form and the submit in order, and the section does not shift the timeline when it opens.
  • [ ] Tests in src/server.test.ts: an admin's form post creates the event and redirects; the same post from a user and from anon is refused by the route with no row written; a cross-site POST is refused by the same-origin check before the action is reached; an invalid kind re-renders with the message and status 422; the form is absent from the HTML for a non-admin.

Files

src/routes.ts, src/page.ts, src/tty.ts, static/app.css, src/server.test.ts.

Risks

  • This reverses the "the page is read-only, posting is a door for apps" note in src/routes.ts. That note stays true for apps; update it to say the human door is admin-only and exists for operating the hub, so the next reader does not think it is a mistake.
  • An admin may type any source, including one a registered app owns. That is why this waits on #42: an admin-posted row must carry posted_by and be marked as such, or the hub's signed-identity promise is quietly breakable from its own page.
  • Rationing already applies to non-GET requests through src/server.ts; do not add a second limiter here.
  • Keep the form's field names identical to the JSON API's, so the two doors cannot drift.

Suggested

Scout — a small form, a redirect, keyboard reach, tests that fail without it.

commented

Two things changed under this issue while it was blocked. First, n is taken: pull #67 (#30) binds n to register an app in the terminal's registry view, so the admin post needs a different key — pick one and say why in the pull (a for add, or n only inside the timeline view if the two views keep separate key maps, which #67 makes possible). Second, #63 is about to make an unsigned POST /api/events answer 501 for a machine; this issue's form is a same-origin page POST from a signed-in admin and is deliberately not that path — keep them apart in src/routes.ts so neither becomes the other's fallback.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs