Skip to content

feat: web: report options as page controls - #2771

Draft
acinader wants to merge 1 commit into
hledgerorg:mainfrom
acinader:web-playground
Draft

acinader wants to merge 1 commit into
hledgerorg:mainfrom
acinader:web-playground

Conversation

@acinader

@acinader acinader commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

hledger-web report controls for most of hledger's report options. Report options are expressed in the url.

Example url:

/balancesheet?q=depth:1&value=end&list=tree&empty=0&period=monthly%20in%202026&total=1

The controls

Validated parameters mirroring the command line, carried by the page's own links, the report menu, paging and year rows, and column headings whenever it is not at its default:

  • period (as before), accum — now including cumulative
  • value — none, cost, end, then, now, or a date, each with an optional commodity, as --value WHEN[,COMM]
  • list — flat or tree
  • total, avg, sort, pct, calc — -T, -A, -S, -%, --valuechange/--gain/--count
  • infer, date2, empty, and on the register costs
  • filter rows on every page, the journal and register included: status, real/virtual, account type, and depth, each rewriting its own search term

A parameter the page does not know, or a contradiction (calc=gain with value=cost), gets an explanatory alert

Tree mode folds in place

A tree-mode report is computed at full depth; its rows carry depth classes (written by the shared renderer, so any HTML consumer can fold by indent) and the page folds it to the search's depth limit. Carets open each group in place, without touching the url. The sidebar is the same tree: in tree mode it gets carets. Folds are remembered in the browser (a work in progress). The fold runs inline during parse, like the sidebar's scroll restore, to eliminate flicker by rendering and then mutating.

Behavior changes from main

  • The hideemptyaccts cookie is replaced by the empty parameter, part of the url like everything else. The e key now reloads the page with the empty parameter
  • CLI HTML output (--base-url reports) in tree mode now writes depth-N account cell classes, and register links can carry value=.
  • --debug=1 on hledger-web traces each request's url, search, parsed query, and the parameters each page keeps.

Issues

  • Today, startup report options (-V, --tree, --depth, …) become invisible server-side defaults, and page parameters express deviations from them. An alternative: translate startup options into the url that browse mode opens (hledger-web -V --tree opening /journal?value=end&list=tree), leaving the server at stock defaults. That would simplify the handlers and make every view fully described by its url. Startup query arguments seem better left as journal filtering (If nothing else, query arguments can be used to restrict what transactions are available to be further filtered and shaped by the URL).
  • Without a depth limit, the sidebar's caret folds are saved in the browser and restored across pages. With one, the sidebar folds to the limit and caret clicks are deliberately ephemeral: the depth is url state, and the carets are temporary peeks past it. The alternative — remembering clicks there too — needs the saved state to record opened groups as well as folded ones ({folded, opened} applied over the page's baseline), since under a depth the baseline is mostly closed. This is not complete and needs some additional decisions as the current behavior is not acceptable (but worth looking at for general direction).
  • An earlier draft offered the journal's tag names as a filter row. It needed curation heuristics no other row does — hledger makes a tag from any word: in a comment. I couldn't find a useful filter or view of tags, so I removed it.
  • Also tried and removed currency as a filter. It reads plausibly for the two-or-three-currency case, but on journals with many tickers the offered choices were arbitrary.
  • Known follow-ups from self-review, deferred to keep this reviewable: dedupe the BalanceR/StatementsR control assembly (~35 near-identical lines); share the value-word rendering between ReportPage and Anchor.hs. URL-encode in addLinkParams (currently safe, constant values only); compute the Depth row's deepest depth once per journal load instead of per request.
  • TODO: translations. The catalogs are untouched in this PR; the new control strings show in English in every language until the template is regenerated and the de and zh-Hans catalogs are updated, which should wait until the control set has settled.

AI usage: Claude Fable 5, ~300k output tokens.

@simonmichael simonmichael added the web The hledger-web tool. label Oct 10, 2026
@simonmichael

Copy link
Copy Markdown
Member

This looks great; given a toggle so it's not shown by default, it seems a strict improvement for hledger-web users.

The proposed handling of startup options and startup queries sounds good.

Yes, our tag parsing is too loose and needs.. something..

What's the problem with listing all commodities as links ? Too verbose ? It sounds useful.

@simonmichael

simonmichael commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

for hledger-web users.

(A useful learning aid for hledger CLI users too, demonstrating the different modes more easily.)

@acinader

Copy link
Copy Markdown
Contributor Author

Thank you for the feedback. Helpful.

  1. I dropped the commodities because they are an unbounded list. Given your generally positive feedback on the rest of the pr, I will add back the commodities and figure it out.
  2. For sure, the ui needs to be rationalized. I'll add a <details> toggle to this PR. My plan is to develop a few options, either as prototypes or mockups, for feedback after we get this in.
  3. PR Web headinglink: use date queries instead of period parameter #2768 is high value, so I (or @thielema) can add date range selectors to this pr.

@acinader

Copy link
Copy Markdown
Contributor Author

Here's my suggestion for how command line startup options and query arguments map to the web UI.

hledger-web OPTIONS parameterize the url that browse mode opens.

hledger-web -V --tree opens /journal?value=end&list=tree.

hledger-web QUERY ARGUMENTS keep doing what they do today, filtering the journal at load time, bounding what any url can produce.

Today hledger-web merges both into one invisible initial query. So a startup --cleared, --real, or -p 2026 hard-filters the journal exactly like status:cleared or date:2026 would. I propose that all options become visible, editable url state (eg --cleared becomes q=status:cleared in the url), while a query-argument spelling continues to filter the journal available to hledger-web.

A depth: query argument becomes url state too, since a depth limit shapes reports rather than filtering the journal. The manual paragraph would need to be reworded.

For example:

$ hledger-web --cleared -3 -p 2026 assets

opens

/journal?period=2026&q=status:cleared%20depth:3

and assets does not appear in the url: it filters the journal at load time, as today.

If we make these changes, OPTIONS only take effect through the url hledger-web opens or prints.

So in the case of --serve, or for anyone editing a url by hand, startup report options no longer have any server-side effect: /journal with no query string shows the stock view.

For --serve we would print the fully parameterized url in the startup output, so the link in the terminal carries all the options:

$ hledger-web -V --tree --serve
Serving web UI and json API at IP address 127.0.0.1, port 5000
with base url http://127.0.0.1:5000
Web UI with these startup options: http://127.0.0.1:5000/journal?value=end&list=tree

A hand-edited bare url gets stock defaults.

This is a behavior change, but I think it is a good one that makes hledger-web permalinks reliable.

Implementation plan is to derive the url parameters from the original ReportOpts, then rebuild the app's ReportSpec from stock defaults plus the query arguments. Everything downstream that reads the startup spec then sees stock defaults automatically, so startup options no longer act as hidden server-side state. Journal loading is untouched.

Reasonable?

@simonmichael

Copy link
Copy Markdown
Member

It sounds intuitive and good to me.

Using startup arguments as hidden limits rather than adding them to the url is optional, but consistent with past behaviour and seems possibly useful.

Not using startup options as hidden limits is a behaviour change, but I guess it's fine; options are mostly display modes, which are best managed at runtime.

@thielema

thielema commented Oct 14, 2026 via email

Copy link
Copy Markdown
Contributor

@simonmichael

Copy link
Copy Markdown
Member

I'd be fine with that too, as it's simpler. If you want to run a restricted web ui, you would probably limit it by file, not by query.

@thielema

Copy link
Copy Markdown
Contributor

Interesting: When I choose tree mode, I can fold sub-accounts not only in the report page but also in the left side-bar. Hiding sub-accounts seems to be generally useful for the left sidebar, but requires more space.

@thielema

Copy link
Copy Markdown
Contributor

The links for "Type:" should not be a choice but a set, since showing multiple types of accounts is very useful.

@thielema

Copy link
Copy Markdown
Contributor

Unfortunately, code will conflict with changes that I have not yet published, because they are blocked by #2768 .

@acinader

Copy link
Copy Markdown
Contributor Author

...but requires more space.

I don't understand.

@thielema

Copy link
Copy Markdown
Contributor

...but requires more space.

I don't understand.

It might depend on the web browser but in my Firefox a triangle is shown left of every parent account. This takes additional space and increases indentation.

@thielema

Copy link
Copy Markdown
Contributor

withoutKs is what I named removeAttributes in #2768

@acinader

Copy link
Copy Markdown
Contributor Author

in my Firefox a triangle is shown left of every parent account. This takes additional space and increases indentation.

May I see a screenshot?

@thielema

thielema commented Oct 16, 2026 via email

Copy link
Copy Markdown
Contributor

@thielema

Copy link
Copy Markdown
Contributor
Bildschirmfoto_20261016_182211 Bildschirmfoto_20261016_182233

The report pages now offer hledger's report-shaping options as rows of
links above each report, so reports can be reshaped in the browser and
every view has a shareable url. Each control is a validated page
parameter mirroring the command line's vocabulary: period, accum
(change, historical, or cumulative), value (--value's WHEN[,COMM]
syntax: none, cost, end, then, now, or a date), list (flat or tree),
total and avg (-T, -A), sort (-S), pct (-%), calc (--valuechange,
--gain, --count), infer (--infer-market-prices), date2, and empty (-E).
A parameter the page does not know, or a contradictory combination, is
explained in an alert. The pages' own links, the report menu, the
paging and year rows, and the column headings all carry each parameter
that is not at its default.

Filter persist on every page including the journal and
register: status, real and virtual postings, account type, and depth,
each rewriting its own search term.

Tree mode gets in-place folding. A tree-mode report is computed at full
depth, its account cells carry depth classes (from the shared renderer,
so any HTML consumer can fold by indent), and the page folds it to the
search's depth limit, with a caret at each group opening it without
changing the url. The sidebar is always the account tree; in the page's
tree mode it gets the same carets, folded accounts are remembered per
browser, and a depth limit folds it to that depth instead of clipping
it. The folding runs inline right after the sidebar's markup, like its
scroll restore, so the tree never paints unfolded.

Amount conversion runs end to end: the sidebar converts with the page
and labels its net row by the mode, a report's figures link to
registers converting the same way, and the CLI's --base-url reports now
write that value parameter into their register links too. The register
gains a costs toggle (costs=1), shows its running balance without
costs, and converts amounts as its value parameter says.

Zero-balance visibility moves from the hideemptyaccts cookie to the
empty parameter, so it is part of the url like everything else; the e
key now follows the Zero balances row's other link. --debug=1 traces
each request's url, search, parsed query, and kept parameters.

Docs in the hledger-web manual; tests in the yesod-test suite, the
Playwright browser suite, and the balance shelltests. The translation
files are untouched for now: the new strings show in English until
the template is regenerated and the catalogs updated, a follow-up for
when the control set has settled.

AI usage: Claude Fable 5, ~300k output tokens.
@acinader

Copy link
Copy Markdown
Contributor Author

withoutKs is what I named removeAttributes in #2768

conformed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

web The hledger-web tool.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants