feat(web): the remaining six views, an SVG curve nobody has to see, and a stream that says when it stops (#537) - #550
Merged
Conversation
…nd a stream that says when it stops The bulk of the port: `/setup`, `/activity`, `/insights`, `/rules`, `/venues` and `/gates` as client views over #534's API, plus the equity curve and live updates. ── PARITY IS ON THE INFORMATION, AND TWO MORE #548s TURNED UP ON THE WAY ────── Every view is judged against the rendered pages AND `tests/commands/test_tui.py`, and where those disagree the divergence is written into the view's docstring rather than reproduced. Porting found two more instances of #548's pattern -- a lookup that misses, defaulted, with nothing raised: * `render.py::_STEP_KIND_NOTE` has three entries against `StepKind`'s four. The missing one is `operator_input`, the kind of the real `credentials` step, so the rendered `/setup` prints an EMPTY note on the step where "what may a wizard do for you" matters most. `payload._STEP_KIND_NOTES` carries all four. * `render.py::pct` appends `%` to four values that are FRACTIONS. The config ships `max_total_dd_pct: 0.20` and rail 11 compares the raw drawdown against it, so a 20% ceiling renders as "0.20%" on every rendered page and a 5% drawdown as "0.05%" -- a hundredfold understatement of a risk limit. `payload.ratio`'s docstring names this exact trap; the client shows its value. ── THE CHART: THE COORDINATES ARE COMPUTED IN PYTHON ────────────────────────── `build_equity_curve` returns plot coordinates, not just figures, and that is the decision rather than an accident of layering. Where a vertical axis starts is what makes a $3 wobble look like a collapse; keel does not let a front-end decide whether a number is bad, and the axis it is drawn against is the same delegation wearing different clothes. So the baseline is always zero, in Python, where a test can read it. The consequence is checkable: `chart.js` needs no arithmetic, so the lexer scan that guards `render.js` now runs over it too -- proven by mutation, not by reading. The curve is `role="img"` named by its own `<figcaption>`, one string in one element, with the journal table beside it for the figures. ── LIVE UPDATES: A DROPPED CONNECTION IS THE POINT ──────────────────────────── `/api/events` streams an envelope whose `data` holds one revision marker and no figures at all -- `api.js` stays the only place data enters this client, which is the property a reader audits by opening one file. The agent runs daily, so this is not a feed: it is how a dead server stops looking like an unchanged one. Driving it in a real browser found four bugs no test here could have: * the marker watched `keel.db` only, and `keel serve` runs WAL (#470), so a committed write moves nothing. The feature was inert and its own comment said so approvingly. It watches `-wal` now, and not `-shm`, which readers touch. * a tick repainted the banner green over a view saying the report could not be built. The view's own read now wins; a loss of contact still shows fast. * a sort press deferred itself, because the click focused a button inside the view and the rebuild was waiting for focus to leave. Pressing a control and watching nothing happen is indistinguishable from a broken control. * `/activity` and `/gates` scrolled the document sideways at 360px -- on a log path and a dotted call site, in two paragraphs, with every table already scrolling correctly inside itself. ── `/setup` KEEPS ITS GATE, AND ITS ACTION SET IS UNCHANGED ─────────────────── `keel.commands.setup.ACTIONS` is a 0-line diff, `keel/web/security.py` is a 0-line diff, and nothing under `/api/*` answers a POST. The client lists every action keel offers, with its inputs; the button stays on the page that holds the write token, because `/api/setup` ships no CSRF token on purpose and inventing a way to hand the client one would be widening the write surface. A client that hides a button is not a gate, so nothing is hidden -- only the button is elsewhere. #540 deletes that page, and where a browser client gets a write token from is a decision that belongs to the milestone, not to this commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyeggYtojNXCTHeD3JHxb6
This was referenced Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #537.
The bulk of the client port:
/setup,/activity,/insights,/rules,/venuesand/gatesas client views over #534's JSON API, plus the equity curve and live updates over
EventSource.With this, the browser client reaches parity on information with the rendered pages — which is
the precondition #540 (delete
render.py's HTML) and #541 (delete the curses TUI) both wait on.Parity is on the information, and the divergences are written down
Every view is judged against both the rendered pages and
tests/commands/test_tui.py. Wherethose disagree with each other the divergence goes into the view's docstring rather than being
reproduced — porting a bug forward because the old surface had it is not parity.
Porting found two more instances of #548's pattern (a lookup that misses, defaulted, nothing
raised), both of which the client does not inherit:
render.py::_STEP_KIND_NOTEhas three entries againstStepKind's four. The missing one isoperator_input— the kind of the realcredentialsstep — so the rendered/setupprints anempty note on the step where "what may a wizard do for you" matters most.
payload._STEP_KIND_NOTEScarries all four.render.py::pctappends%to four values that are fractions (The browser shows the drawdown breaker 100x too small: pct() appends % to a fraction #542).payload.ratio'sdocstring names the trap; the client shows the value at the scale the CLI does.
The chart: coordinates are computed in Python
build_equity_curvereturns plot coordinates, not just figures. Where a vertical axis starts iswhat makes a $3 wobble look like a collapse; keel does not let a front-end decide whether a number
is bad, and the axis it is drawn against is the same delegation wearing different clothes. So the
baseline is always zero, in Python, where a test can read it.
That is checkable rather than asserted:
chart.jsneeds no arithmetic, so the lexer scan thatguards
render.jsnow runs over it too — proven by mutation. The curve isrole="img"named byits own
<figcaption>, with the journal table beside it for the figures./setupkeeps its gate, and its action set is unchangedkeel.commands.setup.ACTIONSis a 0-line diff,keel/web/security.pyis a 0-line diff, andnothing under
/api/*answers a POST. The client lists every action keel offers, with its inputs;the button stays on the page that holds the write token, because
/api/setupships no CSRF tokenon purpose and inventing a way to hand the client one would widen the write surface. A client that
hides a button is not a gate, so nothing is hidden — only the button is elsewhere. Where a browser
client gets a write token belongs to the milestone (#540), not to this commit.
Verification
ruff,mypyand the full suite are green (4826 passed, 3 skipped). Beyond that, smoked in a realbrowser against a snapshot of the live 155 MB deployment database, because this repo's history
is that unit tests missed the failures that mattered (the empty-DB 500s, the frozen-bundle cwd, the
WAL contention):
positions, subscriptions and freshness — with no console errors (one favicon 404, which PWA: manifest, icons, and a service worker that never caches /api/* #538
covers).
/api/eventsstreamsretry: 2000and atickcarrying a cheap revision fingerprint, so theclient refetches only when the database or config actually moves. No render flicker observed
over 16 s of polling.
cumulative arithmetic correct, ending at +$19.45, with a per-point accessible label and a
<figcaption>naming the range./static/insightsserved directly returns the view, not a 404.keel insights summaryandkeel insights journalon thesame database: same figures, same "no rule track record yet", same drawdown scale.
One difference from the CLI, confirmed deliberate and already documented at
payload.py:349: theclient labels every instant UTC, while
tui._human_dtrenders unlabelled local time. That isthe #381 decision (local time made the activity feed show a permanently stale "today"), not a
regression.
🤖 Generated with Claude Code
https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2