Skip to content

feat(web): the remaining six views, an SVG curve nobody has to see, and a stream that says when it stops (#537) - #550

Merged
eaitbrahim merged 1 commit into
mainfrom
feat-537-remaining-views
Aug 25, 2026
Merged

feat(web): the remaining six views, an SVG curve nobody has to see, and a stream that says when it stops (#537)#550
eaitbrahim merged 1 commit into
mainfrom
feat-537-remaining-views

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

Closes #537.

The bulk of the client port: /setup, /activity, /insights, /rules, /venues and /gates
as 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. Where
those 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_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 browser shows the drawdown breaker 100x too small: pct() appends % to a fraction #542). payload.ratio's
    docstring names the trap; the client shows the value at the scale the CLI does.

The chart: coordinates are computed in Python

build_equity_curve returns plot coordinates, not just figures. 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.

That is checkable rather than asserted: chart.js needs no arithmetic, so the lexer scan that
guards render.js now runs over it too — proven by mutation. The curve is role="img" named by
its own <figcaption>, with the journal table beside it for the figures.

/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 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, mypy and the full suite are green (4826 passed, 3 skipped). Beyond that, smoked in a real
browser 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):

  • All six views plus Status render with real data — autonomy ON, 7 live rules, 2 open BTC
    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/events streams retry: 2000 and a tick carrying a cheap revision fingerprint, so the
    client refetches only when the database or config actually moves. No render flicker observed
    over 16 s of polling.
  • The equity curve was exercised by seeding 20 synthetic closed trades into the snapshot:
    cumulative arithmetic correct, ending at +$19.45, with a per-point accessible label and a
    <figcaption> naming the range.
  • Deep links and reloads work — /static/insights served directly returns the view, not a 404.
  • Cross-checked field by field against keel insights summary and keel insights journal on the
    same 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: the
client labels every instant UTC, while tui._human_dt renders unlabelled local time. That is
the #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

…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
@eaitbrahim
eaitbrahim merged commit 3a9a816 into main Aug 25, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the feat-537-remaining-views branch August 25, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The remaining six views, SVG charts, and live updates over EventSource

1 participant