| Keyboard and screen reader |
The run form is not a dialog. No role="dialog", no
aria-modal. Focus never moves into it — after opening, the focused element is still
the button behind the overlay. Tab walks straight out into the page underneath. |
Medium | index.html:90 | U-09 partly fixed |
Escape closes the wrong thing. One unconditional handler:
if (e.key === 'Escape') closeDetail(). Pressing Escape over the run form closes the
panel behind it and leaves the form up. I confirmed both: the side panel does close, the
form does not. The side panel's button even advertises title="close (esc)", so the
app teaches a habit that fails where it is needed most. |
Medium | console.js:467 | U-10 fixed in 0.1.2 |
No focus ring anywhere. The whole 642-line stylesheet has one
:focus rule, and it is .rrow input:focus { outline: 0; border-color:
#a2e65d66 } — a 1px border at 40% opacity, which is not a real indicator. Every other
control falls back to the browser default on a near-black background. |
Bad | theme.css:491 | U-11 fixed in 0.1.2 |
Hidden panels keep their buttons in the tab order. The book and the detail
rail are hidden with width: 0; overflow: hidden, not display: none.
I folded the book and counted: 7 of its buttons were still tabbable while
invisible. With no focus ring, the user has no idea where they are. |
Bad | theme.css:33, :522 | U-12 still open |
"edit" on an alpha card is mouse-only. It is a <span> with
a click handler, it is opacity: 0 until you hover the card, and it sits
inside the card's own <button>. So a keyboard user who tabs to the
card and presses Enter always gets the card action, never edit. There is no keyboard path to
editing an alpha at all. |
Bad | backtests.js:632 | U-13 still open |
11 of 21 form inputs have no label. The shared row() helper puts
the <label> beside the control with no for and no wrapping, so
the link is visual only. A screen reader announces "edit text, blank" eleven times in the run
form — no way to tell the seed from the commission. |
Medium | backtests.js:1169 | U-14 fixed in 0.1.2 |
Sorting a table needs a mouse. The sort handler is bound to
<th>, which is not focusable, with no aria-sort. Sorting is the
only way to reorder a 50-row page of a table that may hold millions of rows. |
Medium | console.js:330 | U-15 still open |
Chart drill-down is pointer-only. Clicking a bar opens one rebalance; dragging
narrows the whole report. Both are on a plain <div>. The drill panel's own
empty state says "Click a bar above" — for a keyboard user that panel can never be filled. |
Medium | backtests.js:822 | U-16 still open |
| Errors and state |
The run can be submitted twice, and the error is wiped. The form closes and
the graph lights up before the server answers. A second submit returns a correct
409 "a replay is already running in this project" — but it is inserted into a panel
that the 4-second repaint overwrites. The user gets a ~4 second flash, then a lit graph and the
previous run's numbers. |
Bad | backtests.js:1207 | U-17 partly fixed |
Errors are shown as raw JSON. All four fetch wrappers do
throw new Error(await r.text()), so the user is shown
{"detail":"a replay is already running in this project"} verbatim. A Pydantic 422 is
worse — a wall of nested JSON. The message is not lost, just never unwrapped. |
Medium | console.js:43 | U-18 fixed in 0.1.2 |
Two polling loops overlap and none back off. Four timers at 2.5s, 4s, 5s and
120ms. The 4s and 5s ones both call refresh(), so every cycle sends duplicate
GET /api/backtests and GET /api/alphas, with no reentrancy guard —
they can interleave and both write the same globals. With the server down the console fires
roughly 2.5 requests a second, forever. |
Medium | backtests.js:1296 | U-19 fixed in 0.1.2 |
Dead affordances. Every run-log row has cursor: pointer and no
click handler — I clicked one and nothing happened. There is also no
prefers-reduced-motion guard anywhere, including on an infinite pulse animation and
a sweep that re-fires once per rebalance during a replay. |
Small | theme.css:132, :334 | U-20 fixed in 0.1.2 |