Skip to content

Integrate the eris-dashboard as a workspace, driven by on-chain-derived run data (#63) - #65

Merged
adachi-440 merged 7 commits into
mainfrom
feat/dashboard-workspace
Aug 26, 2026
Merged

Integrate the eris-dashboard as a workspace, driven by on-chain-derived run data (#63)#65
adachi-440 merged 7 commits into
mainfrom
feat/dashboard-workspace

Conversation

@adachi-440

Copy link
Copy Markdown
Contributor

Closes #63.

Imports the eris-dashboard UI as the dashboard/ workspace and drives every view from real run data, per the issue's guiding principle: the chain is the source of truth — numeric series are derived from on-chain reads by the post-run reconstruction, logs supply only reasoning, intent and identity.

What's in here (one commit per phase)

  • Phase 0 — plain copy of the dashboard as an npm workspace; bun artifacts dropped; npm run dashboard
  • Phase 2 (core)core/src/realtime/marketSeries.ts: after the run, the same blockNumber-pinned Multicall3 machinery that reconstructs the value series derives runs/<id>/market.json — per-venue two-sided quotes + pool depth, GMX OI/funding (DataStore keys per Keys.sol, unit-tested against cast-computed vectors), Aave reserve totals, multi-asset fair series, end-of-run GMX positions / Aave accounts, and per-tx USD notionals decoded from receipts. Zero reads added to the per-block coordinator loop; failures degrade the artifact, never the run
  • Phases 1+2 (dashboard)runsProvider builds all five snapshots from summary.json / events.jsonl / blocks.csv / agents/*.jsonl / market.json; run picker in the sidebar; Blockscout deep links behind a one-shot availability probe; VITE_DATA_PROVIDER=seed keeps the seed provider for UI development
  • Phase 3 — live mode: a run in progress shows up as (live) and every view refreshes in place. Data comes from incremental file tails (a dev-server /runs/<id>/tail endpoint), JSON-RPC reads of the chain (endpoint discovered from the agents' own runtime_start log lines), and optionally Blockscout's indexed height so the indexer lag stays visible. Views flip to the archived rendering automatically on completion
  • Phase 4 — the seed fictions retired: order book → per-venue AMM depth panel, rank move redefined as the rank change over the run's final epoch, orders tab hidden until a run produces trigger orders, pair selector driven by the run's actual bases (switching really switches), info tabs rewritten as accurate documentation, ASCON → ERIS, fictional /rules and /register pages deleted
  • Fixes — a teardown bug found watching a real run end (the completing run vanished from the live index mid-teardown and stranded the view on the neighboring run), and ten findings from an external code review (per-unit trade prices, swap-vs-deposit discrimination for volume, live refresh serialization vs StrictMode double-effects, per-base GMX marks, height-pinned fair samples, fork-pool depth, symlink/realpath and tail chunking in the middleware, no-silent-zero funding)

Deviations from the issue sketch (deliberate, documented in the commits)

  • Tx notionals decode receipts directly instead of the Blockscout token-transfers API: the receipt decoder must exist as the explorer-down fallback anyway and yields the same data with one less moving part. Blockscout remains the deep-dive UI
  • Live blocks/txs fall back to direct RPC, not blocks.csv tailing: the coordinator writes blocks.csv after the run (the per-block write was deliberately removed from the hot loop), so there is nothing to tail live

Acceptance criteria

  • clone → npm installnpm run sim:realtimenpm run dashboard renders the completed run with no seed data on screen — verified against a real sim:realtime run (example.yaml roster: self-improving agents + frozen control, lst/liquity venues)
  • a running sim:realtime can be watched live — verified end-to-end twice, including the automatic live → archived flip with real scores and the epoch move column firing on real data
  • every numeric series shown derives from on-chain reads; logs only supply reasoning/intent/identity
  • reconstruction extensions add no reads or logging to the per-block coordinator loop (measured: +755ms post-run on a 60-block backtest, failedReads 0)

🤖 Generated with Claude Code

adachi-440 and others added 7 commits August 26, 2026 14:18
Plain copy of the standalone eris-dashboard repo (Vite + React 19 +
Tailwind v4, seed-data driven) into dashboard/, per issue #63 Phase 0:

- add dashboard to root npm workspaces; drop bun artifacts (bun.lockb,
  packageManager field) — installs and runs under npm
- root script: `npm run dashboard` starts the dev server on :5173
- README: document the dashboard as an optional workspace, still on
  seed data until the runsProvider lands (Phase 1)

Verified: workspace typecheck + production build pass, dev server
serves, root typecheck / check:boundaries / tests (508) unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Phase 2)

Add core/src/realtime/marketSeries.ts: after the run ends, the same
blockNumber-pinned Multicall3 machinery that reconstructs the value series
derives a dashboard-facing market artifact and writes runs/<id>/market.json —
zero reads added to the per-block coordinator loop, and a failure degrades the
artifact, never the run (independent try/catch, market_series_reconstructed /
_failed events).

Per sampled block (config.scoreEvery stride, fromBlock/toBlock pinned):
- per-venue executable two-sided quotes (uniswap slot0 mid +/- fee, curve
  get_dy probes, balancer querySwap probes — the adapters' probe discipline)
  plus pool depth in USD
- GMX open interest long/short + saved funding factor (DataStore keys derived
  per Keys.sol; unit-tested against cast-computed vectors)
- Aave reserve totals (aToken / variableDebtToken supplies, utilization)
- multi-asset fair prices (PriceFeed latestAnswer / answerOf)

Plus end-of-run per-agent GMX positions and Aave account data, and per-tx USD
notionals decoded from receipts (Transfer logs vs the tx sender; unknown
tokens are counted, never silently priced). failedReads/failedReadTargets are
tracked the same way as the value series.

Note: notionals read receipts directly instead of the Blockscout
token-transfers API the issue sketches — the receipt decoder must exist as the
explorer-down fallback anyway, and it yields the same data with one less
moving part. Blockscout remains the deep-dive UI.

Measured (calm, 60-block backtest): 59 cross-sections x 3 venues x 2 bases,
failedReads 0, +755ms post-run; 347/924 txs decoded to notionals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the seed-data provider as the default with runsProvider, which builds
all five snapshots from runs/<id>/ files only: summary.json (standings —
score = M9 scaled to bps/epoch, PnL%, Sharpe, max drawdown from the epoch
series), events.jsonl (reconstructed observations -> price/portfolio series,
typed event tape), blocks.csv (explorer blocks/txs, methods joined from the
agents' submitted-tx self-reports), agents/<id>.jsonl (decision logs), and
market.json when present (per-venue quote series for the cross-venue arb
chart with real buy/sell markers, MarketStats volume/OI/funding/liquidity,
GMX positions table, decoded tx amounts, multi-asset tickers and closing
prices). Runs without market.json degrade to the Phase 1 rendering.

- vite.config.ts: dev-server plugin serving /runs/index.json + artifacts from
  the sibling runs/ dir; /blockscout proxy to :3100
- run picker in the sidebar (localStorage-persisted; hooks refetch on change)
- Blockscout deep links for tx/block/address, feature-flagged on a one-shot
  availability probe — links vanish when the explorer is down, nothing else
- keep localProvider behind VITE_DATA_PROVIDER=seed for UI development
- fix seed-era podium formatting ("+-13.1%" on negative PnL)

Fix found while wiring: reconstructed observations serialize blockNumber as a
string, so the price series must Number() it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A run in progress (no summary.json yet, events.jsonl still moving) now shows
up in the run picker as "(live)" and every view refreshes in place every few
seconds, flipping to the archived rendering automatically when the run
completes. Live data comes from three places, none of them the coordinator:

- file tails: a /runs/<id>/tail/<file>?offset=N dev-server endpoint streams
  appended bytes of events.jsonl (run meta, tape, tx_submitted attribution)
  and agents/<id>.jsonl (live decision log)
- JSON-RPC: the anvil endpoint is discovered from the agents' own
  runtime_start log lines; the browser reads chain height, recent blocks, and
  the on-chain PriceFeed (a live fair ticker accumulates while the page
  watches)
- Blockscout, optionally: its indexed height renders next to the RPC height
  so the indexer lag stays visible

Each live panel carries the block height it reflects. Scores, portfolio
curves, per-venue series and notionals are post-run artifacts by design and
appear on completion. liveRun.ts assembles a synthetic LoadedRun so the
ordinary snapshot builders render both modes; a shared useSnapshot hook
refreshes live data without flashing the loading state.

Note one divergence from the issue sketch: blocks.csv cannot be tailed live —
the coordinator writes it after the run (the per-block write was deliberately
removed from the hot loop), so the explorer-down fallback for live blocks/txs
is direct RPC instead.

Verified against a live backtest (calm#202, 120 blocks): live detection,
countdown, RPC blocks/fair ticker, streamed decision log, and the automatic
live -> archived flip with real scores, all without reload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The seed build shipped UI that described a product which does not exist. This
replaces or removes every piece the acceptance criterion "no seed data on
screen" still caught:

- order book -> AMM depth panel: every venue is an AMM, so the right column
  now shows per-venue pool depth (series + current, from market.json) with
  the executable two-sided quote, instead of fictional asks/bids
- rank move redefined as the rank change over the run's final epoch on
  cumulative value gain — a single run has no previous round, but it has
  epochs, the same unit the score is computed over
- orders tab renders only when a run actually produced trigger orders (no
  current strategy uses them)
- market pair selector driven by the run's actual bases (WETH/WBTC/...), and
  switching pairs now really switches the candles, stats, depth, positions
  and arb view; the decorative timeframe chips are gone
- top-page info tabs rewritten as real documentation of the simulator
  (overview / environment / scoring / artifacts), sourced from the README and
  ADRs instead of invented seasons, prizes and sponsors
- honest chrome: ASCON -> ERIS everywhere, "Round 11 of 13 · live" -> actual
  run number and status with the countdown only while live, leaderboard
  caption states the real metric (mean − λ·std of epoch log returns)
- unreachable fictional pages deleted (/rules with invented regulations,
  /register with a fake signup countdown)

The seed provider stays for UI development; its market snapshot now
synthesizes venue depths and survives old IndexedDB blobs by attaching the
new fields at read time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…63)

Found watching a real run end on screen: the coordinator's teardown (bulk
blocks.csv recording, then the reconstruction sweeps) leaves events.jsonl
silent for tens of seconds while summary.json does not exist yet. The run
index judged liveness by events.jsonl mtime alone with a 30s window, so the
run vanished from the index mid-teardown — the dashboard flipped to the
neighboring run and its refresh loop stopped there, stranded on the wrong
run's data.

- judge liveness on the newest of events.jsonl and blocks.csv, with a 120s
  window that bridges the quiet stretches
- useSnapshot: one grace refresh after a live -> not-live transition, so a
  transitional read settles on the next poll instead of freezing the screen

Re-verified end to end against a live run (calm#404): the run stays selected
through teardown and flips in place to its own archived rendering — real
scores, and the Phase 4 epoch move column firing on real data (venue-arb +1 /
multi-arb -1 over the final epoch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ten findings from an external review of the #63 work, each verified against
the code before fixing:

core (market.json producer):
- summarizeTransfers records baseUnits and a per-unit priceUsd when a tx
  really exchanged both legs; the missing counter leg is what tells an Aave
  supply or GMX collateral move apart from a swap
- fairLookup takes the last sample at or before the tx's block — with a
  thinned series the nearest sample could sit in the tx's future, pricing a
  notional across a crash edge off information the tx never had
- a failed GMX funding read leaves fundingPerHourBps absent instead of
  printing 0.00bps (issue #44's no-silent-zero discipline)
- tokenUsd prices the fork pools' USDC.e/USDT legs at par, so fork runs keep
  their balancer/curve depth series
- document that the uniswap quote is slot0 mid +/- fee (the no-arb monitor's
  approximation), not a tick-walk

dashboard:
- LiveRunState.refresh() shares one in-flight promise: the tail offsets are
  mutable shared state and StrictMode's double-mounted effects were folding
  the same chunk twice on every page load
- live fair samples pin eth_call to the height they are stamped with, not
  "latest" read seconds later
- Trades tab shows base quantity and per-unit price (the "Price" column was
  showing the tx's total USD notional); volume counts only both-leg trades
  and the label says "Volume · run", not "24h volume"
- agent-detail GMX positions mark against their own base's final fair (a
  WBTC perp was priced off the WETH fair)
- the archive view of a live run keeps polling until the final artifacts
  land instead of freezing on a partial snapshot
- runs middleware: realpath check closes symlink escape; tail responses are
  chunk-capped so a first tail of a large log doesn't buffer it whole

Not changed on purpose: the uniswap quote approximation itself, and the
SAVED_FUNDING_FACTOR funding source — both documented instead.

Verified against a fresh backtest: swaps decode with priceUsd/baseUnits,
one-sided transfers carry none, WETH volume corrects from $228,699 to
$203,589, and the Trades tab renders quantity x unit price on screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@adachi-440
adachi-440 merged commit bc23749 into main Aug 26, 2026
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.

[dashboard] Integrate the eris-dashboard UI as a workspace, driven by on-chain-derived run data

1 participant