Split out of #43, which was tried twice (pulls #114 and #116, both killed mid-run by the provider's weekly limit) and pushed nothing either time. #43 asks for the database, the route and the page in one run; this is the database half alone, which is the part everything else waits on and the part that can be finished and tested without touching a rendered page.
Done looks like
[ ] Every mutation in src/db.ts writes a short human label into its action row alongside the payload it already logs: moved "Ship it", deleted the column "Doing", renamed the board, added a card. The label is built from the data the write already holds — nothing re-reads the board to make it.
[ ] The label is stored as a field of the existing payload JSON (no new column, no migration), so a row written before this change still parses.
[ ] undo(boardId) returns string | null — the label of what it just reversed, null when there was nothing to undo. An action row written before this change undoes cleanly and reports a change.
[ ] db.peekUndo(boardId) returns the label of what Undo would reverse, as one indexed read (SELECT payload FROM action WHERE board_id = ?1 ORDER BY id DESC LIMIT 1), or null.
[ ] Labels hold card and column titles — someone else's text. This issue stores them raw and says, in a comment at the top of the label helper, that every caller escapes at the boundary (escape() on the page, the control-character strip in the terminal). Nothing here renders anything.
[ ] Tests in src/db.test.ts that fail without the change: a label round-trips through the action row for each kind of mutation; peekUndo names the last change and does not consume it; a legacy payload with no label undoes and reports a change; undo on an empty log returns null.
[ ] Every existing caller of undo() still compiles and behaves: src/routes.ts treats a string as success and null as "nothing to undo" (its 404/{ok:false} path is unchanged), and src/tty.ts/src/keys.ts keep working. No page markup changes in this issue.
Files I expect to change
src/db.ts, src/db.test.ts, and the one-line truthiness adjustments in src/routes.ts and the terminal's undo call site.
Out of scope
The Undo button's title/aria-label preview, the disabled "Nothing to undo" state, and the JSON { ok, undid } shape — all of that stays in #43, which now waits for this.
Risks
#107 removes the three action rows a new board logs for its own default columns. If it lands first there is one fewer call site to label; either order is a trivial merge.
Keep the label short and fixed-shape: it is about to be rendered in an aria-label (#43) and drawn in a terminal (#34). A title longer than ~40 characters should be elided in the label itself, once, here — not by every renderer.
Split out of #43, which was tried twice (pulls #114 and #116, both killed mid-run by the provider's weekly limit) and pushed nothing either time. #43 asks for the database, the route and the page in one run; this is the database half alone, which is the part everything else waits on and the part that can be finished and tested without touching a rendered page.
Done looks like
src/db.tswrites a short human label into its action row alongside the payload it already logs:moved "Ship it",deleted the column "Doing",renamed the board,added a card. The label is built from the data the write already holds — nothing re-reads the board to make it.payloadJSON (no new column, no migration), so a row written before this change still parses.undo(boardId)returnsstring | null— the label of what it just reversed,nullwhen there was nothing to undo. An action row written before this change undoes cleanly and reportsa change.db.peekUndo(boardId)returns the label of what Undo would reverse, as one indexed read (SELECT payload FROM action WHERE board_id = ?1 ORDER BY id DESC LIMIT 1), ornull.escape()on the page, the control-character strip in the terminal). Nothing here renders anything.src/db.test.tsthat fail without the change: a label round-trips through the action row for each kind of mutation;peekUndonames the last change and does not consume it; a legacy payload with no label undoes and reportsa change;undoon an empty log returnsnull.undo()still compiles and behaves:src/routes.tstreats a string as success andnullas "nothing to undo" (its 404/{ok:false}path is unchanged), andsrc/tty.ts/src/keys.tskeep working. No page markup changes in this issue.Files I expect to change
src/db.ts,src/db.test.ts, and the one-line truthiness adjustments insrc/routes.tsand the terminal's undo call site.Out of scope
The Undo button's
title/aria-labelpreview, the disabled "Nothing to undo" state, and the JSON{ ok, undid }shape — all of that stays in #43, which now waits for this.Risks
aria-label(#43) and drawn in a terminal (#34). A title longer than ~40 characters should be elided in the label itself, once, here — not by every renderer.Suggested: scout.