Remove the TUI; the web app is the frontend

money serve covers every screen the TUI had, so keeping both meant every
behaviour change landing twice. internal/tui goes, and with it bubbletea,
bubbles and lipgloss. `money` with no command now runs serve, the way it
used to open the TUI, and `money tui` is an unknown command.

Three invariants in CLAUDE.md were covered only by TUI tests. Two already
had web counterparts; TestPairedLegsAreNotUntagged is ported to the API:
a paired leg is neither listed as untagged nor offered to the rule
builder, while an unpaired one still is.

README's screen sections now describe the browser, which kept the
behaviour and changed only the controls. CLAUDE.md names the web
equivalents of the TUI functions its invariants pointed at, drops the tab
completion rule that only bubbles' textinput needed, and says how to test
the web app instead of how to drive a terminal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 18:51:54 +02:00
co-authored by Claude Opus 5.5
parent 8e4df08f43
commit ec8a844441
11 changed files with 228 additions and 5437 deletions
+7 -8
View File
@@ -140,8 +140,8 @@ async function loadOverview() {
fillDatalists();
}
// The account and tag fields complete natively, from the lists the TUI
// completes against. (transfer) is never among the tags: it is a label.
// The account and tag fields complete natively, from lists the server
// builds. (transfer) is never among the tags: it is a label.
function fillDatalists() {
const o = state.overview;
if (!o) return;
@@ -186,8 +186,8 @@ function go(id) {
function show() {
const id = location.hash.slice(1) || 'accounts';
const view = VIEWS.find((v) => v.id === id) || VIEWS[0];
// Leaving the builder mid-edit abandons the edit, as esc does in the TUI: a
// new rule must not start out as a copy of the one being edited.
// Leaving the builder mid-edit abandons the edit, as esc does: a new rule
// must not start out as a copy of the one being edited.
if (view.id !== 'rule' && state.rule.editing) state.rule = blankRule();
state.view = view.id;
renderNav();
@@ -481,7 +481,7 @@ function textInput(id, value, placeholder, oninput, list) {
function mountRuleBuilder(main) {
const r = state.rule;
const editing = r.editing;
// A new rule starts on the account being browsed, as in the TUI.
// A new rule starts on the account being browsed.
if (!editing && !r.account && state.filter.account) r.account = state.filter.account;
const summary = h('div', { class: 'summary' });
@@ -857,9 +857,8 @@ function mountTransfers(main) {
// --- keyboard -------------------------------------------------------------
// The TUI's keys, where they do not fight the browser. A builder is a form,
// so outside of chords its keys belong to the field being typed in — exactly
// the rule the TUI keeps.
// Single-key shortcuts, where they do not fight the browser. A builder is a
// form, so outside of chords its keys belong to the field being typed in.
document.addEventListener('keydown', (e) => {
const t = e.target;
const typing = t instanceof HTMLInputElement || t instanceof HTMLSelectElement || t instanceof HTMLTextAreaElement;