diff --git a/api/static/dashboard.html b/api/static/dashboard.html index 1876f8f..ca216a0 100644 --- a/api/static/dashboard.html +++ b/api/static/dashboard.html @@ -208,6 +208,11 @@ button { font: 600 12px -apple-system, "Segoe UI", sans-serif; border: 1px solid transparent; border-radius: 6px; padding: 6px 13px; cursor: pointer; transition: filter .12s, transform .1s, box-shadow .12s; } button:hover { filter: brightness(1.16); transform: translateY(-1px); } button:active { transform: translateY(0); } + /* accessibility (UI-REVIEW fix 2): keyboard operators must SEE focus, + esp. on money-path controls (approve/reject/exit/flatten). */ + button:focus-visible, .btn-exit:focus-visible, .btn-approve:focus-visible, + .btn-reject:focus-visible, input:focus-visible, summary:focus-visible { + outline: 2px solid var(--accent); outline-offset: 2px; } button:disabled { opacity: .4; cursor: default; transform: none; } .btn-approve { background: linear-gradient(180deg, #1a5238, #134029); color: #7defb4; border-color: #25714d; } .btn-approve:hover { box-shadow: 0 4px 14px -6px rgba(52,209,126,.5); } @@ -259,9 +264,9 @@ letter-spacing: .6px; margin-right: 5px; } .risk-strip b { color: var(--text); font-weight: 650; } .rdot { font-weight: 700; } - .rdot.ok { color: #2ecc71; text-shadow: 0 0 12px rgba(46,204,113,.45); } + .rdot.ok { color: #34d17e; text-shadow: 0 0 12px rgba(46,204,113,.45); } .rdot.warn { color: #f1c40f; text-shadow: 0 0 12px rgba(241,196,15,.45); } - .rdot.critical { color: #ff4d4f; text-shadow: 0 0 12px rgba(255,77,79,.5); } + .rdot.critical { color: #ff5a60; text-shadow: 0 0 12px rgba(255,77,79,.5); } /* armed-trigger proximity radar */ .trig-row { display: flex; align-items: center; gap: 11px; padding: 6px 6px; border-bottom: 1px solid var(--border); font: 600 12px var(--mono); } @@ -270,6 +275,8 @@ .trig-state { width: 52px; font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; } .trig-meta { width: 96px; color: var(--muted); font: 500 10px var(--mono); } + .trig-levels { width: 150px; color: var(--muted); font: 500 10px var(--mono); } + .trig-levels b { color: var(--text); font-weight: 700; } .trig-bar { flex: 1; height: 16px; position: relative; background: #0d1117; border: 1px solid var(--border); border-radius: 3px; overflow: hidden; min-width: 120px; } .trig-fill { position: absolute; left: 0; top: 0; bottom: 0; transition: width .35s ease; } @@ -334,8 +341,8 @@ .daily-lbl { fill: var(--muted); font: 500 9px var(--mono); text-anchor: middle; } .daily-lbl.daily-sel { fill: var(--accent); font-weight: 700; } .st-armed .trig-state { color: #58a6ff; } .st-armed .trig-fill { background: linear-gradient(90deg,#1f6feb,#58a6ff); } - .st-fired .trig-state { color: #2ecc71; } .st-fired .trig-fill { background: #2ecc71; } - .st-filled .trig-state { color: #2ecc71; } .st-filled .trig-fill { background: linear-gradient(90deg,#1f8f4e,#2ecc71); } + .st-fired .trig-state { color: #34d17e; } .st-fired .trig-fill { background: #34d17e; } + .st-filled .trig-state { color: #34d17e; } .st-filled .trig-fill { background: linear-gradient(90deg,#1f8f4e,#34d17e); } .st-weak .trig-state { color: #7d8590; } .st-weak .trig-fill { background: #39414d; } .st-waiting .trig-state { color: #7d8590; } .st-waiting .trig-fill { background: #30363d; } .st-armed.near .trig-bar { box-shadow: 0 0 0 1px #f1c40f55; } @@ -463,8 +470,8 @@

MOMENTUM / control room

PAPER - - + +
@@ -702,8 +709,8 @@ function openChart(symbol) { timeScale: { timeVisible: true, secondsVisible: false }, }); const series = chart.addSeries(LightweightCharts.CandlestickSeries, { - upColor: "#2fbf71", downColor: "#e5484d", borderVisible: false, - wickUpColor: "#2fbf71", wickDownColor: "#e5484d", + upColor: "#34d17e", downColor: "#ff5a60", borderVisible: false, + wickUpColor: "#34d17e", wickDownColor: "#ff5a60", }); const markersApi = LightweightCharts.createSeriesMarkers(series, []); chartState = { symbol, chart, series, markersApi, lastFetch: 0, priceLines: [] }; @@ -721,8 +728,8 @@ function applyChartLevels(levels) { }; add(levels.avg_entry, "#4f9cf9", "entry"); add(levels.entry_limit, "#4f9cf9", "entry limit"); - add(levels.stop, "#e5484d", "stop"); - add(levels.target, "#2fbf71", "target"); + add(levels.stop, "#ff5a60", "stop"); + add(levels.target, "#34d17e", "target"); } // Collapse a burst of same-reason rejected decisions into one marker per @@ -763,7 +770,7 @@ async function refreshChart(symbol) { time: m.time, position: sell ? "aboveBar" : "belowBar", shape: sell ? "arrowDown" : "arrowUp", - color: unfilled ? "#7884a2" : (sell ? "#e5484d" : "#2fbf71"), + color: unfilled ? "#7884a2" : (sell ? "#ff5a60" : "#34d17e"), text: (sell ? "S " : "B ") + (m.quantity != null ? m.quantity : ""), }; }); @@ -958,19 +965,19 @@ function drawSpark(canvas, bars, entry, stop) { ctx.setLineDash([]); ctx.globalAlpha = 1; }; refLine(entry, "#4f9cf9"); - refLine(stop, "#e5484d"); + refLine(stop, "#ff5a60"); const up = closes[closes.length - 1] >= closes[0]; - ctx.strokeStyle = up ? "#2fbf71" : "#e5484d"; + ctx.strokeStyle = up ? "#34d17e" : "#ff5a60"; ctx.lineWidth = 1.5; ctx.beginPath(); closes.forEach((c, i) => i ? ctx.lineTo(x(i), y(c)) : ctx.moveTo(x(i), y(c))); ctx.stroke(); ctx.lineTo(x(closes.length - 1), H); ctx.lineTo(x(0), H); ctx.closePath(); - ctx.globalAlpha = 0.10; ctx.fillStyle = up ? "#2fbf71" : "#e5484d"; + ctx.globalAlpha = 0.10; ctx.fillStyle = up ? "#34d17e" : "#ff5a60"; ctx.fill(); ctx.globalAlpha = 1; - ctx.fillStyle = up ? "#2fbf71" : "#e5484d"; + ctx.fillStyle = up ? "#34d17e" : "#ff5a60"; ctx.beginPath(); ctx.arc(x(closes.length - 1), y(closes[closes.length - 1]), 2, 0, 7); ctx.fill(); } @@ -1139,6 +1146,7 @@ function renderTriggers(tw, catMap) { + '
' + (t.catalyst ? 'πŸ“° ' : '') + esc(t.symbol) + '
' + '
' + esc(t.state) + '
' + '
' + (t.gap != null ? 'gap ' + t.gap.toFixed(0) + '% ' : '') + (t.rvol != null ? t.rvol.toFixed(1) + 'x' : '') + '
' + + '
entry ' + fmt(trig) + ' stop ' + fmt(stop) + ' now ' + fmt(price) + '
' + '
' + (hasBar ? '
' : '') + '
' @@ -1236,7 +1244,7 @@ function renderEquity(ec) { const Y = v => (pad + (H - 2 * pad) * (1 - (v - lo) / rng)); const pts = eq.map((v, i) => X(i).toFixed(1) + ',' + Y(v).toFixed(1)).join(' '); const up = eq[eq.length - 1] >= (base != null ? base : eq[0]); - const col = up ? '#2ecc71' : '#ff4d4f'; + const col = up ? '#34d17e' : '#ff5a60'; let baseLine = ''; if (base != null) { const by = Y(base).toFixed(1); baseLine = ''; } const area = 'M' + X(0).toFixed(1) + ',' + (H - pad) + ' L' + eq.map((v, i) => X(i).toFixed(1) + ',' + Y(v).toFixed(1)).join(' L') + ' L' + X(eq.length - 1).toFixed(1) + ',' + (H - pad) + ' Z'; @@ -1255,9 +1263,9 @@ function rBar(entry, cur, stop) { const xR = rr => W * (Math.max(LOR, Math.min(HIR, rr)) - LOR) / (HIR - LOR); let inner = ''; if (r != null) { - const z = xR(0), c = xR(r), col = r >= 0 ? '#2ecc71' : '#ff4d4f'; + const z = xR(0), c = xR(r), col = r >= 0 ? '#34d17e' : '#ff5a60'; inner += '
'; - inner += '
'; // stop + inner += '
'; // stop inner += '
'; // breakeven } const txt = r != null ? (r >= 0 ? '+' : '') + r.toFixed(2) + 'R' @@ -1344,8 +1352,10 @@ function renderTrades(pnl) { ? 'net ' + money(net) + ' Β· ' + wins + '/' + trades.length + ' win' : ''; fill("trades", table( - ["Symbol", "Exit", "R", "P/L", "Time"], + ["Symbol", "Entry", "Exit $", "Exit", "R", "P/L", "Time"], trades.map(t => '' + esc(t.symbol) + '' + + '' + fmt(t.entry_price) + '' + + '' + fmt(t.exit_price) + '' + '' + esc(String(t.exit_reason || "").replace(/_/g, " ")) + '' + '' + (t.r_multiple == null ? 'β€”' : (t.r_multiple >= 0 ? '+' : '') + t.r_multiple + 'R') + '' + '' + money(t.realized_pnl) + '' + @@ -1418,7 +1428,7 @@ function render(d) { const fr = $("risk-fresh"); fr.textContent = rk.data_age_s != null ? rk.data_age_s.toFixed(0) + "s" : "β€”"; // broker sync runs ~every 60s, so >90s = a missed sync (amber), >180s = stale (red) - fr.style.color = (rk.data_age_s == null) ? "" : (rk.data_age_s > 180 ? "#ff4d4f" : rk.data_age_s > 90 ? "#f1c40f" : ""); + fr.style.color = (rk.data_age_s == null) ? "" : (rk.data_age_s > 180 ? "#ff5a60" : rk.data_age_s > 90 ? "#f1c40f" : ""); renderTriggers(d.triggers, d.catalyst); renderCatalystEngine(d.catalyst_engine); @@ -1451,8 +1461,8 @@ function render(d) { '' + riskChip(r) + '' + '' + catalystCell(q.catalyst) + '' + '
' + (canAct ? - '' + - '' + '' + + '' : 'read-only') + '
'; }) @@ -1531,7 +1541,7 @@ function render(d) { '' + fmt(targetBy[pos.symbol]) + '' + '' + rBar(entry, cur, stop) + '' + '' + unrealCell(upnl, pct) + '' + - (canCommand ? '' : '') + + (canCommand ? '' : '') + ''); })); $("pos-count").textContent = posRows.length; diff --git a/docs/UI-REVIEW.md b/docs/UI-REVIEW.md new file mode 100644 index 0000000..9e05979 --- /dev/null +++ b/docs/UI-REVIEW.md @@ -0,0 +1,106 @@ +# UI Review β€” Momentum Dashboard (`api/static/dashboard.html`) + +**Audited**: 2026-07-20 Β· retroactive 6-pillar visual audit (no UI-SPEC exists β†’ +audited against abstract standards). Adversarial stance: assume failure until +the code proves otherwise. Scores are 1–4, not averaged upward. + +**Scope**: the primary operator dashboard (`dashboard.html`, ~1600 lines, +single-file inline CSS/JS) plus its siblings `backtest.html` / `admin.html`. +Method: static analysis of color/spacing/typography tokens, structure, and +interaction states (the auditor's grep-based method; a headless render pass is +the standing follow-up). + +## Scorecard + +| Pillar | Score | One-line verdict | +|---|---|---| +| 1. Copywriting | **4** | Domain-precise, explains *why* β€” not generic | +| 2. Visuals / hierarchy | **3** | Clear stripβ†’panel structure; dense but legible | +| 3. Color | **3** | Real token system, undermined by duplicate semantic colors + 90 hardcoded values | +| 4. Typography | **3** | Sensible mono scale; 9px floor is too small | +| 5. Spacing | **2** | No systematic scale β€” 20 distinct px values incl. arbitrary 7/9/11/13 | +| 6. Experience design | **4** | Standout β€” confirm-to-destruct, full empty/loading/error/connection states | + +**Overall**: a genuinely competent, purpose-built operator UI. The failures are +consistency-and-accessibility drift, not structural β€” this is a good dashboard +with three fixable rough edges, not a rebuild. + +## Findings + +### Pillar 1 β€” Copywriting Β· 4 +Labels state intent, not just nouns: "closest to fire on top", "why each entry +was taken or refused Β· value β†’ threshold per gate", "held long with no / short +protective stop". Empty states teach ("No armed gappers β€” waiting for the open"). +No lorem, no vague "Loading…" without context. **No fix needed.** + +### Pillar 2 β€” Visuals / hierarchy Β· 3 +Strong top-down hierarchy: risk-strip β†’ P&L-strip β†’ collapsible `
` +panels, colour-coded side pills, sparklines, R-bars. Weakness: extreme density +(10–11px body across 51 elements) with little whitespace β€” correct for a trading +terminal, but the eye has few rest points. **WARNING** β€” acceptable for the +audience; revisit if non-expert users are ever a target. + +### Pillar 3 β€” Color Β· 3 +Real foundation: 17 CSS custom properties, 149 `var(--…)` uses, coherent dark +theme, defensible 60/30/10 (bg dominant / panels secondary / `#5ba3ff` accent on +interactive). **But** two concrete defects: +- **Duplicate semantic colors** β€” THREE greens (`#2fbf71`, `#2ecc71`, `#34d17e`) + and FOUR reds (`#e5484d`, `#ff4d4f`, `#ff5a60`, `#ff8589`) all meaning + "up/good" and "down/bad". These should be one `--green` / `--red` (+ one + intentional bright variant), not near-identical one-offs. +- **90 hardcoded hex values** bypass the token system that already exists. +**WARNING** β€” the system is right; application drifted. + +### Pillar 4 β€” Typography Β· 3 +One monospace family (correct for aligned numeric data), a real size ladder +(9β†’27px), weight used for hierarchy. Defect: the **9px floor** (4 elements) is +below comfortable-read and fails WCAG minimums; 10–11px carries most of the UI, +which is tight. **WARNING** β€” raise the 9px labels to β‰₯11px. + +### Pillar 5 β€” Spacing Β· 2 (lowest) +**20 distinct padding/margin values** including arbitrary `7px`, `9px`, `11px`, +`13px` with no discernible 4/8 rhythm. Dense layout justifies *tightness*, not +*arbitrariness* β€” the values look hand-tuned per element rather than drawn from a +scale. **WARNING** β€” adopt a spacing scale (e.g. 2/4/8/12/16/24) and snap values +to it; the visual result changes little but future edits stay consistent. + +### Pillar 6 β€” Experience design Β· 4 (standout) +This is where the dashboard earns its keep: +- **Confirm-to-destruct**: EXIT/FLATTEN/PAUSE use an arm-then-confirm pattern + (`armButton`, 8 sites) β€” no accidental market flatten. +- **Full state coverage**: 38 loading/loader refs, 20 explicit empty states, 7 + error/connection-lost handlers ("stream lost β€” polling", "render error"). +- **Resilience**: SSE with polling fallback, degrade-gracefully panels. +- **3 `@media` breakpoints** β€” responsive to phone/tablet. +**No fix needed** β€” this pillar is a model for the rest. + +### Cross-cutting β€” Accessibility (drags 2 & 6) +**0 ARIA roles/labels, only 2 `:focus` styles** on a dashboard whose buttons +submit real orders (Approve / Reject / EXIT / FLATTEN). Keyboard and +screen-reader users cannot see focus or understand controls. Not a visual defect +but a real usability gap for the money-path controls. **WARNING (borderline +BLOCKER for keyboard operators).** + +## Top 3 priority fixes + +1. **Collapse duplicate semantic colors + migrate hardcoded values** (Pillar 3). + Define `--green`/`--red` (+ one bright variant each), replace the 3 greens / 4 + reds and the 90 hardcoded hex. Highest consistency-per-effort. *WARNING.* +2. **Add focus + ARIA to money-path controls** (Accessibility). `:focus-visible` + outlines and `aria-label` on Approve/Reject/EXIT/FLATTEN. A keyboard operator + currently flies blind on the destructive buttons. *WARNING β†’ BLOCKER for + keyboard use.* +3. **Adopt a spacing scale** (Pillar 5). Snap the 20 ad-hoc px values to a + 2/4/8/12/16/24 ladder; raise the 9px type floor to β‰₯11px. *WARNING.* + +## What NOT to touch +Experience design (Pillar 6) and copywriting (Pillar 1) are strengths β€” don't +regress the confirm-to-destruct flow or the explanatory empty states while +refactoring color/spacing. The density is a deliberate, correct choice for the +trading-terminal audience; do not "add whitespace" reflexively. + +--- +*No UI-SPEC exists for this project. If the dashboard becomes a maintained +surface, the highest-leverage next step is extracting the existing `:root` tokens +into a small declared design contract so future changes have a baseline to audit +against β€” the token system is 80% there already.* diff --git a/storage/projections.py b/storage/projections.py index 1769d19..59a05cb 100644 --- a/storage/projections.py +++ b/storage/projections.py @@ -668,6 +668,8 @@ def query_session_pnl(store, session_id: str | None = None, trades.append( { "symbol": payload.get("symbol"), + "entry_price": payload.get("entry_price"), + "exit_price": payload.get("exit_price"), "realized_pnl": round(pnl, 2), "r_multiple": r, "exit_reason": payload.get("exit_reason"),