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

An admin can tell the money path is wired: which gateway, whether the webhook is heard, and what is stuck unpaid #62

open not picked up yet opened this
opened this

An admin opening a fresh preview cannot tell whether this deployment takes real test-mode card payments or is simulating them, whether Stripe's webhook can reach it, or that four orders have been sitting at pending_payment since yesterday.

What the pages show today. /admin names the environment and the catalog; src/gateway.ts:47-56 already works out the answer — chosen() returns a kind, a label and a note that says exactly why the mock is in use ("No STRIPE_SECRET_KEY is set", or a key that is not a test key and is therefore refused) — and the only place a shopper or an admin meets it is the checkout button. STRIPE_WEBHOOK_SECRET is read in src/shop.ts at delivery time and nowhere else, so a deployment with a secret key and no webhook secret looks exactly like a healthy one until money goes missing. Nothing counts the orders that never got paid.

Done looks like

  • [ ] A panel on /admin — one small render function in src/pages/admin.ts fed by plain data from src/gateway.ts — says which gateway is live and chosen().note when it is the mock, whether STRIPE_WEBHOOK_SECRET is set, when the last webhook delivery was accepted, when the last payment was recorded, and how many orders are at pending_payment with the age of the oldest.
  • [ ] No secret is ever rendered: the panel says set or unset and test-mode or refused, never a key, a prefix, a length or a hash of one. A test asserts the page body contains no substring of STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET.
  • [ ] The whole panel is admin-only and behind the guard in src/admin.ts like every other admin read — an anonymous request to /admin still gets today's 403 page and nothing about the deployment's configuration.
  • [ ] "When the last webhook was accepted" is derived from rows that already exist (order_event, payment) rather than a new table, so this does not wait on #16; when #16 lands its gateway_event table becomes the better source and the panel moves to it in one line.
  • [ ] Stripe configured but never heard from, and orders stuck unpaid past an hour, read as a warning in the page's existing muted/bad styling — a sentence a person can act on, not a red dot.
  • [ ] The terminal shows the same summary as a header line on the orders tab, for an admin.
  • [ ] The queries are indexed and bounded: one COUNT and one MIN(created_at) over "order" by status, measured against a few thousand seeded orders in the test, and the dashboard's time does not move.
  • [ ] Tests that fail before the change: with no STRIPE_SECRET_KEY, the panel says simulated and gives the reason; with a test key and no webhook secret, it warns; a pending order older than an hour is counted and named; the page never contains a secret.

Files I expect to change. src/gateway.ts (a health() returning plain data), src/admin.ts, src/pages/admin.ts, src/orders.ts (a count by status), src/tty.ts, src/server.test.ts or src/stripe.test.ts, README.md.

Risks. This is a page about secrets, which is the easiest page in the app to leak one from — the test that greps the body is the point, not a formality. #61 also grows the dashboard, so it goes first and this panel follows it; keep them as two render functions rather than one block. "Stuck unpaid" is a judgement, not a fact: say the age and let the admin decide, and do not cancel anything — expiry is #15's work.

Suggested crew. mason to build it, with sentinel reading the pull for what the panel exposes.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs