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

An admin's change to the catalog, a discount or a tax rate is a row that names who made it #53

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Pushed. `bun test` is green (92 pass, also under `--randomize`), and the app boots with `/admin/changes` answering 403 for anyone the forge did not sign as an admin.

shipped production is serving it deploy ab962da

opened this

Pushed. bun test is green (92 pass, also under --randomize), and the app boots with /admin/changes answering 403 for anyone the forge did not sign as an admin.

An admin's change to what the shop sells and what it charges is now a row. src/changes.ts is the one concept: a writer that diffs two plain snapshots and writes one admin_change row per field that actually moved, and a reader that returns the last hundred newest-first. Every call site in src/admin.ts — productCreate, productUpdate, variantCreate, variantUpdate, discountCreate, discountToggle, taxSave, taxRemove — wraps its write and its row in one db.transaction, so a change that rolls back takes its audit row with it and a change that lands cannot be missing one. A form submitted with nothing changed writes nothing.

The actor is ctx.who and nothing else; at the terminal it is the account the forge names in the environment, which is why src/desk.ts records its own writes the same way rather than leaving a hole where the ssh session is. Values are capped at 120 characters on the way in (a 4,000-character blurb becomes 120 characters and an ellipsis) and escaped on the way out. No gift card is a subject here: its code is bearer money and its balance already has a ledger.

Worth a look in the preview: /admin/changes — edit a variant's price at /admin/products/:id and the row says $20.00 → $21.50 with the forge id of whoever did it, with the subject linked to its own page; press 7 in the terminal for the same list, marked closed for a non-admin the way orders and cards are. The reader orders by the new admin_change(at DESC, id DESC) index — on a table seeded with 10,000 rows the query is 0.7 ms and the page renders in 1.6 ms.

What I chose not to do: #12's actor columns on the five existing ledgers are still #12's — this is only the half that is not about an order, and it settles the actor shape (actor_id, actor_name from ctx.who) for that change to reuse. tax.upsertRate now returns the row id and tax.rateAt was added so an upsert can record what the rate was before; no arithmetic changed and no dependency was added.

🤖 Generated with Claude Code

Closes #34

Changes requested — before the latest push

Changes requested; see the anchored review on src/changes.ts:71.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs