The web interface
Haddon comes with a web interface where you can browse the books, review and correct entries and close
periods, if you want to. Nothing requires it: an agent can do all of the same over the API. It is a React
single-page app (web/), built into the Python package and served by the same process as the API.
It has no logic of its own. Every figure it shows comes from /v1, and every change it makes is one of the
named commands an agent calls, recorded under the reviewer’s own actor name (ui:<name>). So anything a
reviewer can see, an agent can query, and what a reviewer does shows up in the audit log next to what the
agents did.
| Tab | Path | What it is for |
|---|---|---|
| Review | /e/{entity}/month?month=YYYY-MM |
One month: does the bank agree with the books, and what still needs a decision. |
| Inbox | /e/{entity}/review |
Everything unfinished, across all periods. |
| Reports | /e/{entity}/reports |
Income statement and balance sheet for a period, with a PDF download, and recoverable tax not recovered. |
| Accounts | /e/{entity}/accounts |
The chart with balances; each account opens its register. |
| Trial balance | /e/{entity}/trial-balance |
Every account’s balance, and whether each commodity nets to zero. |
| Closing | /e/{entity}/close?through=YYYY-MM-DD |
Every close check as the service evaluated it; close, override, reopen. |
From those, a reviewer reaches an entry (/entries/{id}, with its audit timeline and correction chain), a
statement line (/lines/{id}, back to the document it was read from and forward to what was booked for it),
the statements list with replacement, and a form for a manual entry.
Review: one month
Section titled “Review: one month”The month page starts with whether the bank agrees with the books: the two balances from
GET /reconciliation and the difference between them. When the difference is exactly the lines still
unexplained, the page says so.
Below that is everything that wants a decision, such as lines that need an account, lines only partly explained, days no statement covers and bookings waiting on a document. Each item comes with its action, so a person can finish it from the page by giving a line an account, moving a posted booking to another account or excluding a line with a reason.
The close checks are deliberately not on this page. They report over the whole window since the last close, which can span many months, and most of what they say is not actionable from one month’s view. They have their own page.
The inbox puts every list that means “not finished” in one place: unexplained statement lines, entries awaiting a document, recoverable tax not recovered, the checks blocking the next close, and recent changes. It stores nothing itself. Each section is an existing API read without the month filter, so it can’t disagree with the rest of the service.
Closing
Section titled “Closing”Each check is shown with its status. unevaluable is drawn differently from fail, because “could not tell”
is a weaker claim than “here is what is wrong”, and treating them alike invites overriding a check that found
nothing. The page offers to close only when close_period is among the writes this credential’s
/capabilities lists.
What each edit asks for
Section titled “What each edit asks for”The interface asks for whatever explanation couldn’t be reconstructed later, and no more.
A new booking takes an account and a description. The description is recorded both as the entry’s narration
and as the reason, and nothing is filled in on the reviewer’s behalf. Changing a posted booking asks for one
more sentence, saying why the earlier one was wrong. It goes through reexplain_line, which supersedes the
line’s explanation and corrects the entry it booked in one transaction. Excluding a line and closing over a
failing check each take a sentence.
There is no approval step and no second signature. The record shows who did what and why, and that’s all it does.
Running it locally
Section titled “Running it locally”The interface reaches the API at /ui/v1, which a reverse proxy maps to /v1 after authenticating the
person. Locally, the Vite dev server plays the proxy’s part.
-
Add
proxy_authto the service configuration, so the service accepts a user name from a header:"proxy_auth": {"header": "X-Haddon-User"}With this set, anything that can reach the service’s port can claim to be anyone, so only use it on loopback or behind a proxy that sets the header itself.
-
Start the service, then the dev server:
Terminal window bin/haddon-serve --config var/haddon.json # 127.0.0.1:8080make web-dev # Vite on http://localhost:5173make web-devproxies/ui/v1tohttp://127.0.0.1:8080withX-Haddon-User: dev. SetHADDON_APIto point it at another address, andHADDON_DEV_USERto act under another name. -
Open
http://localhost:5173/.
make web builds the production bundle into src/haddon/ui/static/ (gitignored), which haddon-serve then
serves at /. Without a bundle the service still serves the API, and / shows a page saying which command to
run.
Credentials in production
Section titled “Credentials in production”There are two ways in, with the same API behind both:
/v1/takes a bearer token. The proxy blanks the user header on this path, so a token holder cannot also name itself as a person./and/ui/v1/require the proxy’s own login. The proxy sets the user header from the identity it established, and strips/uiso the service sees an ordinary/v1request.proxy_authturns that header into an actor string,ui:<name>, scoped to the entities it lists.
deploy/haddon.nginx.conf.example is a worked example using HTTP basic auth; any authenticating proxy works the
same way. See Operating a Haddon deployment.
Browser tests
Section titled “Browser tests”make browser-check builds the bundle and drives the main reviewer paths in headless Chromium against a real
server: a month from unexplained lines to a closed period followed by another write, replacing a statement,
the review inbox, writing a manual entry and downloading an export.
It catches what neither the API tests nor the type checker can: a page still showing state from before a write.
It needs Playwright (make install-browser) and is not part of the commit gate. See
browser/README.md.