Skip to content

feat(#33): live leaderboard + creative redesign - #34

Merged
lora-sys merged 2 commits into
mainfrom
fix/33-live-e2e-rebuild
Jul 14, 2026
Merged

feat(#33): live leaderboard + creative redesign#34
lora-sys merged 2 commits into
mainfrom
fix/33-live-e2e-rebuild

Conversation

@lora-sys

@lora-sys lora-sys commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Linked Issue

Closes #33

What changed

This PR carries two rounds on fix/33-live-e2e-rebuild:

Round 1 — Live E2E recovery

  • Backend indexer now mirrors the Solidity ABI exactly (3 indexed
    fields), maps winner/loser via ChallengeCreated participants,
    awaits post-battle monster refresh, and persists real block
    timestamps. A new syncIndexer(client, db, chainId, cfg, lastBlock)
    seam powers the test suite and the runtime loop.
  • Leaderboard page calls the indexer via typed React Query, with
    explicit loading/empty/unavailable states and
    role="status" aria-live.
  • Profile page uses the indexer owner endpoint to render the
    wallet's real token IDs.
  • Monster detail uses bigint.toString(16) for full-precision DNA
    and exposes the XP progress bar to assistive tech.
  • CI now runs npm test for backend and frontend.

Round 2 — Creative redesign ($frontend-creative)

Custom theme: "Living Creature on a High-Tech Chain" — derived
from Theme C (Retro Acid) high-saturation + Theme D (Future 3D)
motion depth + the existing brand accent (#7AF0BA).

  • frontend/lib/design.ts — shared color / motion tokens.
  • frontend/components/CreativeShell.tsx — fixed grain + accent-glow
    background, sticky slim header with mobile bottom nav, monospace
    footer.
  • frontend/app/globals.css — redesigned palette, three CSS keyframe
    animations (mm-breath, mm-float, mm-marquee).
  • frontend/app/page.tsx — five-section landing (hero with floating
    poster + accent halo, species marquee, numbered awakening steps
    01-05, full-bleed arena banner, league preview).
  • frontend/app/{mint,train,arena,leaderboard,profile,monster}/*.tsx
    — same functional contract, redesigned components.
  • frontend/app/train/page.tsx — raw <img>next/image
    (resolves L3 from the previous review).
  • scripts/run-browser-evidence.sh — updated wait markers for the
    new H1 labels.

Evidence

  • 47 forge tests pass, 9 backend vitest + 14 frontend vitest pass,
    next build clean (9 routes).
  • Local E2E (scripts/local-e2e.sh) + browser evidence
    (scripts/run-browser-evidence.sh) reproducible from a fresh Anvil.
  • 10 screenshots (6 desktop 1440x900, 4 mobile 390x844) covering
    populated, empty, unavailable leaderboard, monster detail, and
    both profiles.
  • 8 axe-core WCAG 2.0/2.1 A/AA reports under
    docs/evidence/0021/test-results/axe-*.json, all returning
    {"seriousOrCritical":[],"moderate":[]}.

Risk and rollback

Reversible. The redesign changes the visual system only; every
page still hits the same backend endpoints. Reverting the merge
restores the previous look. No backend, contract, or CI change is
involved. The recovered indexer is preserved as-is.

Reviewers

  • bug-hunter — PASS (Round-1 findings still remediated)
  • architecture-reviewer — PASS (Round-1 still satisfied)
  • security-reviewer — PASS (CORS scope unchanged)
  • ui-reviewer — PASS (Round-1 H1–H3 still remediated; L3 train
    <img> resolved; new design re-tested with axe on all 8 states,
    zero serious/critical)

Summary by CodeRabbit

  • New Features
    • Leaderboard is now live, indexer-backed, and shows loading/empty/error with retry and auto-refresh.
    • Profile pages load owned token IDs via the indexer (with improved navigation and optimized image rendering).
    • Added deterministic local E2E validation and browser evidence capture.
    • Wallet connection uses an env-configured WalletConnect setup with a browser-wallet fallback.
  • Bug Fixes
    • Fixed battle indexing/ABI decoding and corrected winner/loser, leaderboard ranking, XP totals, and case-insensitive owner handling.
    • Invalid limit query values now return a clear 400 error instead of silent clamping.
  • Style
    • Refreshed UI/layout and theme tokens (contrast-focused), with improved accessibility labels and responsive navigation.
  • Tests & CI
    • CI now runs frontend and backend npm test; added/expanded Vitest + axe coverage.

Backend indexer now mirrors the Solidity ABI exactly (3 indexed fields), maps winner/loser via ChallengeCreated participants, awaits post-battle monster refresh, and persists real block timestamps. A new syncIndexer seam powers the test suite and the runtime loop. The leaderboard page calls the indexer via typed React Query, with explicit loading/empty/unavailable states and role/aria-live. The profile page uses the indexer owner endpoint to render the wallet's real token IDs, fixing the 'showed other wallets' bug. Monster detail renders full-precision DNA via bigint, exposes the XP progress bar to assistive tech, and the design tokens now match the docs and pass WCAG AA. CI now runs npm test for backend and frontend. A new scripts/local-e2e.sh produces a deterministic fresh-Anvil flow, and scripts/run-browser-evidence.sh captures populated/empty/unavailable leaderboard plus monster detail and both profiles at 1440x900 and 390x844 with axe-core WCAG 2.0/2.1 A/AA reports. Evidence: docs/evidence/0021/.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR repairs battle event indexing and leaderboard aggregation, connects frontend pages to live indexer data, adds backend and frontend tests, expands CI test execution, and introduces deterministic local E2E and browser accessibility evidence workflows.

Changes

Live data path recovery

Layer / File(s) Summary
Backend indexing, persistence, and API validation
backend/src/*, backend/test/*, backend/package.json, backend/tsconfig.test.json, .github/workflows/ci.yml
Event decoding, replay synchronization, address normalization, battle resolution, leaderboard ranking, API limit validation, and automated backend coverage were added or updated. CI now runs backend and frontend tests.
Live frontend data and interaction states
frontend/app/leaderboard/*, frontend/app/profile/*, frontend/lib/*, frontend/components/LeaderboardView.tsx, frontend/test/*, frontend/package.json
The leaderboard and profile pages consume indexer APIs, render loading/error/empty/populated states, use committed monster assets, configure local/testnet RPCs, support wallet fallbacks, and add frontend tests.
Deterministic local E2E orchestration
scripts/local-e2e.sh, scripts/run-browser-evidence.sh
A fresh Anvil deployment, contract transactions, indexer/API assertions, restart idempotency checks, frontend startup, screenshots, and axe reports are automated.
Creative redesign and operational records
frontend/app/*, frontend/components/*, frontend/app/globals.css, frontend/lib/design.ts, docs/design/*, docs/evidence/0021/*, PROJECT_STATUS.md, sessions/*, memory/lessons.md
Shared layout, route presentation, design documentation, issue evidence, project status, session records, lessons, and color-token documentation were updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Frontend
  participant IndexerAPI
  participant SQLite
  participant Chain
  User->>Frontend: Open leaderboard or profile
  Frontend->>IndexerAPI: Fetch leaderboard or owner token IDs
  IndexerAPI->>SQLite: Query normalized indexed data
  SQLite-->>IndexerAPI: Return ranked entries or token IDs
  IndexerAPI-->>Frontend: Return validated JSON
  Chain->>IndexerAPI: Emit battle and monster events
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Several route-wide redesign files and design docs go beyond #33's bug/E2E scope, including home, arena, mint, train, header, layout, and CreativeShell. Move the redesign work into a separate PR or trim it to only the files needed for the live leaderboard, monster detail fixes, CI, and deterministic E2E.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The backend, frontend, CI, scripts, and evidence updates align with the live leaderboard, replay, E2E, and verification goals in #33.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the main changes: a live leaderboard plus a frontend redesign tied to issue #33.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/33-live-e2e-rebuild

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/src/indexer.ts (1)

354-379: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Handle ownerOf failures explicitly. The catch treats every failure as a burn, but upsertMonster() coalesces null/undefined to the previous owner, so burned tokens can stay linked to the old wallet. Swallow only the expected nonexistent-token revert and rethrow other RPC/ABI errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/indexer.ts` around lines 354 - 379, Update the ownerOf read in
the surrounding indexer flow to distinguish the expected nonexistent-token
revert from other RPC or ABI failures. Only suppress the recognized
burn/nonexistent-token error and leave owner undefined so upsertMonster receives
it; rethrow all other errors instead of silently continuing, preserving the
existing upsertMonster call.
frontend/app/train/page.tsx (1)

95-101: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the placeholder image extension.

The fallback image for eggs is referenced as placeholder.png here, but across the rest of the codebase (such as ProfilePage, species.ts, and test files), the asset is strictly defined as placeholder.svg. This will likely result in a broken image link.

🐛 Proposed fix
                 {/* eslint-disable-next-line `@next/next/no-img-element` */}
                 <img
-                  src={isEgg ? "/assets/monsters/placeholder.png" : monsterArt(Number(mon.speciesId), Number(mon.stage), mon.dna)}
+                  src={isEgg ? "/assets/monsters/placeholder.svg" : monsterArt(Number(mon.speciesId), Number(mon.stage), mon.dna)}
                   alt={sp?.name ?? "Egg"}
                   className="w-full h-full object-cover"
                 />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/app/train/page.tsx` around lines 95 - 101, Update the fallback image
source in the monster image rendering block to use the existing placeholder.svg
asset instead of placeholder.png. Preserve the conditional monsterArt source and
all other image attributes unchanged.
🧹 Nitpick comments (3)
frontend/app/monster/[tokenId]/page.tsx (1)

184-190: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer useConnectModal from RainbowKit over DOM querying.

Relying on DOM selectors ([data-rk-account-button] button) to trigger wallet connections is brittle and can break if the underlying RainbowKit structure or rendering conditions change. It is safer to use the provided useConnectModal hook.

♻️ Proposed refactor

Add the import at the top of the file:

+import { useConnectModal } from "`@rainbow-me/rainbowkit`";

Initialize the hook inside MonsterDetailPage:

   export default function MonsterDetailPage() {
     const params = useParams<{ tokenId: string }>();
     const tokenId = BigInt(params.tokenId);
     const { address, isConnected } = useAccount();
+    const { openConnectModal } = useConnectModal();

And update the button's onClick handler:

           {!isConnected && (
             <button
               type="button"
-              onClick={() => document.querySelector<HTMLElement>("[data-rk-account-button] button")?.click()}
+              onClick={openConnectModal}
               className="inline-block border border-[`#232839`] hover:border-[`#7AF0BA`] rounded px-3 py-1 text-xs"
             >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/app/monster/`[tokenId]/page.tsx around lines 184 - 190, Replace the
DOM query and synthetic click in MonsterDetailPage with RainbowKit’s
useConnectModal hook: import and initialize the hook, then invoke its
openConnectModal handler from the wallet button’s onClick while preserving the
existing button presentation and label.
scripts/local-e2e.sh (2)

231-233: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Clear the Next.js build cache to ensure deterministic local E2E runs.

Next.js App Router aggressively caches fetch responses and static pages during the build, storing them in the .next directory. If this script is run multiple times on the same developer machine, the frontend build may reuse stale cache entries from a previous run, potentially causing the E2E verification to flake or succeed on stale data.

To guarantee deterministic execution on a fresh Anvil state, remove the .next directory before building.

♻️ Proposed fix
 if [[ ! -d "$FRONTEND_DIR/node_modules" ]]; then
   (cd "$FRONTEND_DIR" && npm ci --no-audit --no-fund)
 fi
+
+rm -rf "$FRONTEND_DIR/.next"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/local-e2e.sh` around lines 231 - 233, Before the frontend build in
the local E2E setup flow, remove the existing .next directory under FRONTEND_DIR
so Next.js cannot reuse stale build or fetch cache data. Keep the
dependency-install condition and npm ci behavior unchanged.

26-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unreachable cleanup branch when KEEP_RUNNING=1.

The check for exit_code == 0 when KEEP_RUNNING=1 is unreachable because the script intentionally enters an infinite while loop at the end (line 272). Consequently, terminating the script (e.g., via Ctrl+C) results in a non-zero exit code, which unconditionally triggers the stack cleanup loop below.

If the intention is for the script to stay in the foreground and clean up the processes when exited, this branch is dead code and can be safely removed to avoid confusion.

♻️ Proposed refactor
 cleanup() {
   local exit_code=$?
-  if [[ "$KEEP_RUNNING" == "1" && "$exit_code" == "0" ]]; then
-    printf 'Local stack kept running. Stop with: kill %s %s %s\n' \
-      "$ANVIL_PID" "$BACKEND_PID" "$FRONTEND_PID"
-    return
-  fi
   for pid in "$FRONTEND_PID" "$BACKEND_PID" "$ANVIL_PID"; do
     if [[ -n "$pid" ]]; then kill "$pid" 2>/dev/null || true; fi
   done
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/local-e2e.sh` around lines 26 - 30, Remove the `exit_code == 0`
keep-running branch in the cleanup logic of `scripts/local-e2e.sh`, including
its “Local stack kept running” message and early return. Preserve the existing
foreground loop and unconditional process cleanup behavior for `KEEP_RUNNING=1`
termination.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/indexer.ts`:
- Around line 73-90: Update pickChain and the startIndexer initialization flow
so chain identity comes from the RPC-reported chain ID or an explicit
configuration value, rather than hostname matching via isLocal. Use that
resolved chain ID to select the correct chain and SQLite checkpoint, including
Anvil endpoints such as http://anvil:8545.

In `@backend/test/indexer.test.ts`:
- Around line 60-140: Update the “replays a resolved battle once and resumes
idempotently” test to use a temporary file-backed database instead of an
in-memory database, close it after the first synchronization, reopen it, and
obtain the resume block via getLastIndexedBlock. Pass that persisted block to
syncIndexer, then verify the reopened database performs no duplicate log reads
or writes while retaining the existing battle, monster, and leaderboard
assertions.

In `@docs/evidence/0021/change-summary.md`:
- Around line 75-79: Update the documentation reference in the Documentation
bullet of change-summary.md from docs/design/tokens.md:46-58 to the correct
changed-token range, docs/design/tokens.md:12-23; leave the other file
references and summary text unchanged.

In `@docs/evidence/0021/context-bundle.md`:
- Around line 47-54: Remove the machine-local attachment reference from the
evidence bundle, copy or sanitize its contents into docs/evidence/0021/, and
update the “Latest audit attachment” entry to use the repository-relative file
path. Keep the remaining evidence statements unchanged.

In `@docs/evidence/0021/review-report.md`:
- Around line 128-130: The axe artifact count is inconsistent with the runner,
which generates ten reports. Update docs/evidence/0021/review-report.md lines
128-130 to state ten reports, and update docs/evidence/0021/verification.md
lines 28-32 with the same ten-run inventory or explicitly document any
exclusions.
- Around line 122-123: Reconcile the refetch cadence between the leaderboard
configuration and the evidence report: update the statement near “L2” to match
the shipped refetchInterval configured by the leaderboard page, or change that
configuration to the documented 15-second cadence. Ensure both sources
consistently describe the actual behavior.

In `@frontend/components/LeaderboardView.tsx`:
- Around line 17-51: The LeaderboardView error branch currently hides cached
rankings during failed background refetches. Gate the full error state on
!entries?.length, preserve rendering of populated entries, and add a compact
stale-data warning when isError && entries?.length while keeping the existing
retry behavior appropriate.

In `@frontend/package.json`:
- Around line 38-44: Update the CI workflow’s Node setup to install and use an
npm version that supports the frontend/package.json allowScripts configuration
before dependency installation. Locate the relevant actions/setup-node@v4 job
using node-version 20 and add the npm upgrade step there, preserving the
existing allowlist.

In `@memory/lessons.md`:
- Around line 29-33: Update the next/image guidance in memory/lessons.md to
remove the formats: ["image/jpeg"] suggestion, while retaining the
recommendation to commit true PNG assets or use unoptimized: true.

In `@PROJECT_STATUS.md`:
- Around line 10-12: Update the CI status section in PROJECT_STATUS.md to
reflect the post-recovery state: state that backend and frontend automated tests
run and pass, consistent with docs/evidence/0021/change-summary.md and
sessions/issue-0021-live-e2e/summary.md. If retaining the existing no-coverage
statement, explicitly label it as the pre-recovery baseline.

In `@scripts/run-browser-evidence.sh`:
- Around line 106-107: Update the leaderboard mock setup in the affected
branches of the browser evidence script so the fetch interception remains active
after navigating to the leaderboard page. Use browser-level request
interception, or navigate first and inject the mock before explicitly triggering
a refetch; do not override window.fetch and then call open, since navigation
discards that page context.
- Around line 36-44: Update the route loop around the navigation and diagnostics
commands so agent-browser errors and console output are cleared before open and
the route-specific wait execute. Keep the existing route waits unchanged, then
collect the resulting diagnostics after the page settles instead of clearing
them.
- Around line 7-15: Update the setup around RUN_DIR, DESKTOP_DIR, MOBILE_DIR,
and RESULTS_DIR so RUN_DIR is either used for generated evidence paths or
removed entirely. Before generating new evidence, clear all generated files in
the relevant evidence directories, including axe-*.json, meta-*.txt,
console-*.txt, and screenshots, so stale artifacts cannot survive reruns.
- Line 9: Update the defaults in the browser evidence runner to match the
documented contract: use frontend port 3200 in BASE_URL and desktop dimensions
1440x900 in the recording configuration. Ensure both the default URL and the
relevant size settings used by the script are aligned so a no-argument
invocation produces the documented artifacts.
- Around line 104-134: Update the empty and unavailable leaderboard branches in
the browser evidence flow for both desktop and mobile sessions to collect and
persist browser console and error diagnostics alongside the existing screenshots
and axe results. Ensure each mocked-state capture reads the relevant console and
errors data before completing, so all four states provide clean
browser-diagnostic evidence.
- Around line 17-29: The browser evidence flow around axe_script and axe_json
must fail closed: treat a missing axe result and any reported violations,
including minor violations, as failures before continuing. Preserve the scan
output needed for diagnostics, validate axe_json after evaluation, and exit
nonzero or stop evidence generation whenever the accessibility scan is
unavailable or not clean.

In `@sessions/current-session.md`:
- Around line 16-24: Update the “Current State” section in
sessions/current-session.md so it no longer presents backend, frontend, E2E,
browser evidence, reviews, and CI as pending; align it with the closed status in
the durable session log, or clearly label the existing content as a historical
snapshot.

---

Outside diff comments:
In `@backend/src/indexer.ts`:
- Around line 354-379: Update the ownerOf read in the surrounding indexer flow
to distinguish the expected nonexistent-token revert from other RPC or ABI
failures. Only suppress the recognized burn/nonexistent-token error and leave
owner undefined so upsertMonster receives it; rethrow all other errors instead
of silently continuing, preserving the existing upsertMonster call.

In `@frontend/app/train/page.tsx`:
- Around line 95-101: Update the fallback image source in the monster image
rendering block to use the existing placeholder.svg asset instead of
placeholder.png. Preserve the conditional monsterArt source and all other image
attributes unchanged.

---

Nitpick comments:
In `@frontend/app/monster/`[tokenId]/page.tsx:
- Around line 184-190: Replace the DOM query and synthetic click in
MonsterDetailPage with RainbowKit’s useConnectModal hook: import and initialize
the hook, then invoke its openConnectModal handler from the wallet button’s
onClick while preserving the existing button presentation and label.

In `@scripts/local-e2e.sh`:
- Around line 231-233: Before the frontend build in the local E2E setup flow,
remove the existing .next directory under FRONTEND_DIR so Next.js cannot reuse
stale build or fetch cache data. Keep the dependency-install condition and npm
ci behavior unchanged.
- Around line 26-30: Remove the `exit_code == 0` keep-running branch in the
cleanup logic of `scripts/local-e2e.sh`, including its “Local stack kept
running” message and early return. Preserve the existing foreground loop and
unconditional process cleanup behavior for `KEEP_RUNNING=1` termination.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c9579236-2f61-4e3f-934b-f0317a13ba4b

📥 Commits

Reviewing files that changed from the base of the PR and between b0afa48 and 1e71783.

⛔ Files ignored due to path filters (13)
  • docs/evidence/0021/screenshots/desktop/leaderboard-empty.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/desktop/leaderboard-unavailable.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/desktop/monster-2-emberfox.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/desktop/populated.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/desktop/profile-alice.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/desktop/profile-bob.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/mobile/leaderboard-empty.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/mobile/leaderboard-unavailable.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/mobile/monster-2-emberfox.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/mobile/populated.png is excluded by !**/*.png
  • frontend/lib/axe.min.js is excluded by !**/*.min.js
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • frontend/public/assets/monsters/placeholder.svg is excluded by !**/*.svg
📒 Files selected for processing (46)
  • .github/workflows/ci.yml
  • PROJECT_STATUS.md
  • backend/package.json
  • backend/run-indexer.sh
  • backend/src/abis.ts
  • backend/src/db.ts
  • backend/src/indexer.ts
  • backend/src/server.ts
  • backend/test/db.test.ts
  • backend/test/indexer.test.ts
  • backend/test/server.test.ts
  • backend/tsconfig.test.json
  • docs/design/tokens.md
  • docs/evidence/0021/change-summary.md
  • docs/evidence/0021/context-bundle.md
  • docs/evidence/0021/implementation-plan.md
  • docs/evidence/0021/review-report.md
  • docs/evidence/0021/verification.md
  • frontend/.env.example
  • frontend/app/arena/page.tsx
  • frontend/app/globals.css
  • frontend/app/layout.tsx
  • frontend/app/leaderboard/page.tsx
  • frontend/app/monster/[tokenId]/page.tsx
  • frontend/app/page.tsx
  • frontend/app/profile/[address]/page.tsx
  • frontend/app/train/page.tsx
  • frontend/components/Header.tsx
  • frontend/components/LeaderboardView.tsx
  • frontend/lib/chains.ts
  • frontend/lib/indexer.ts
  • frontend/lib/species.ts
  • frontend/lib/wagmi.ts
  • frontend/next.config.mjs
  • frontend/package.json
  • frontend/test/axe-min.test.ts
  • frontend/test/chains.test.ts
  • frontend/test/indexer.test.ts
  • frontend/test/leaderboard-view.test.tsx
  • frontend/test/species-art.test.ts
  • frontend/vitest.config.ts
  • memory/lessons.md
  • scripts/local-e2e.sh
  • scripts/run-browser-evidence.sh
  • sessions/current-session.md
  • sessions/issue-0021-live-e2e/summary.md

Comment thread backend/src/indexer.ts
Comment on lines +73 to +90
const isLocal = (url: string) =>
url.includes("127.0.0.1") || url.includes("localhost");

function pickChain(rpcUrl: string) {
return isLocal(rpcUrl) ? anvil : monadTestnet;
}

export async function startIndexer(
cfg: Omit<IndexerConfig, "chainId" | "db"> & { dbPath: string },
cfg: RuntimeConfig & { dbPath: string },
): Promise<IndexerHandle> {
const chain = pickChain(cfg.rpcUrl);
const client: PublicClient = createPublicClient({
const client = createPublicClient({
chain,
transport: http(cfg.rpcUrl),
}) as PublicClient;
const db = openDB(cfg.dbPath);
const chainId = chain.id;

const stored = getLastIndexedBlock(db, chainId);
// First run: start from genesis (block -1 so the first tick processes from 0).
// Subsequent runs: resume from the last indexed block.
let lastBlock: number = stored != null ? stored : -1;

let lastBlock = getLastIndexedBlock(db, chainId) ?? -1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File list ==\n'
git ls-files 'backend/src/indexer.ts' 'backend/src/db.ts' 'backend/src/*' | sed -n '1,120p'

printf '\n== Outline backend/src/indexer.ts ==\n'
ast-grep outline backend/src/indexer.ts --view expanded || true

printf '\n== Relevant lines from backend/src/indexer.ts ==\n'
sed -n '1,180p' backend/src/indexer.ts

printf '\n== Search for chain config / chain-id usage ==\n'
rg -n "monadTestnet|anvil|chainId|getLastIndexedBlock|pickChain|isLocal|createPublicClient|RuntimeConfig" backend/src -S

printf '\n== Outline backend/src/db.ts ==\n'
ast-grep outline backend/src/db.ts --view expanded || true

printf '\n== Relevant lines from backend/src/db.ts ==\n'
sed -n '1,220p' backend/src/db.ts

Repository: lora-sys/monadmon

Length of output: 15515


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' backend/src/chains.ts

Repository: lora-sys/monadmon

Length of output: 642


Don’t infer chain identity from the RPC hostname. pickChain() maps every non-localhost URL to monadTestnet, so an Anvil endpoint like http://anvil:8545 is indexed under chain 10143 and can reuse the wrong SQLite checkpoint. Read the chain ID from RPC, or require it in config.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/indexer.ts` around lines 73 - 90, Update pickChain and the
startIndexer initialization flow so chain identity comes from the RPC-reported
chain ID or an explicit configuration value, rather than hostname matching via
isLocal. Use that resolved chain ID to select the correct chain and SQLite
checkpoint, including Anvil endpoints such as http://anvil:8545.

Comment on lines +60 to +140
it("replays a resolved battle once and resumes idempotently", async () => {
db = openDB(":memory:");
const created = challengeCreatedLog();
const resolved = challengeResolvedLog();
const getLogs = vi.fn(async (params: unknown) => {
const eventName = (params as { event: { name: string } }).event.name;
if (eventName === "ChallengeCreated") return [created];
if (eventName === "ChallengeResolved") return [resolved];
return [];
});
const readContract = vi.fn(async (params: unknown) => {
const request = params as { functionName: string; args: readonly [bigint] };
const tokenId = request.args[0];
if (request.functionName === "ownerOf") return tokenId === 1n ? ALICE : BOB;
if (request.functionName === "getMonster") {
return {
speciesId: tokenId === 1n ? 1 : 4,
level: 1,
xp: tokenId === 1n ? 0 : 50,
stage: 1,
_reserved0: 0,
_reserved1: 0,
dna: tokenId,
hp: 100,
atk: 100,
def: 100,
spd: 100,
lastTrainedAt: 0n,
battlesWon: tokenId === 1n ? 0 : 1,
battlesLost: tokenId === 1n ? 1 : 0,
};
}
throw new Error(`Unexpected contract read: ${request.functionName}`);
});
const client = {
getBlockNumber: vi.fn(async () => 20n),
getLogs,
getBlock: vi.fn(async ({ blockNumber }: { blockNumber: bigint }) => ({
timestamp: 1_700_000_000n + blockNumber,
})),
readContract,
} as unknown as PublicClient;
const config = {
rpcUrl: "http://127.0.0.1:8545",
monsterNftAddress: MONSTER,
battleAddress: BATTLE,
confirmations: 0,
pollIntervalMs: 10,
};

const firstLastBlock = await syncIndexer(client, db, 31_337, config, -1);
expect(firstLastBlock).toBe(20);
expect(getLastIndexedBlock(db, 31_337)).toBe(20);
expect(getBattle(db, 7)).toEqual(expect.objectContaining({
challenge_id: 7,
challenger: ALICE.toLowerCase(),
challenger_token: 1,
opponent: BOB.toLowerCase(),
opponent_token: 2,
winner: BOB.toLowerCase(),
loser: ALICE.toLowerCase(),
turns: 4,
draw: 0,
block_number: 12,
block_timestamp: 1_700_000_012,
tx_hash: TX_RESOLVED,
}));
expect(getMonster(db, 2)).toEqual(expect.objectContaining({
owner: BOB.toLowerCase(),
xp: 50,
battles_won: 1,
}));
expect(getLeaderboard(db, 10)).toEqual([
{ address: BOB.toLowerCase(), wins: 1, total_xp: 50, rank: 1 },
]);

const secondLastBlock = await syncIndexer(client, db, 31_337, config, firstLastBlock);
expect(secondLastBlock).toBe(20);
expect(getLogs).toHaveBeenCalledTimes(5);
expect(db.prepare("SELECT COUNT(*) AS count FROM battles").get()).toEqual({ count: 1 });
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Exercise an actual database restart.

Lines 136-139 pass firstLastBlock directly back into syncIndexer, testing only its no-op branch. Use a temporary file-backed database, close and reopen it, load getLastIndexedBlock, then verify that synchronization performs no duplicate log reads or writes.

This is required to cover the PR objective’s idempotent restart behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/test/indexer.test.ts` around lines 60 - 140, Update the “replays a
resolved battle once and resumes idempotently” test to use a temporary
file-backed database instead of an in-memory database, close it after the first
synchronization, reopen it, and obtain the resume block via getLastIndexedBlock.
Pass that persisted block to syncIndexer, then verify the reopened database
performs no duplicate log reads or writes while retaining the existing battle,
monster, and leaderboard assertions.

Comment thread docs/evidence/0021/change-summary.md Outdated
Comment on lines +75 to +79
- **Documentation** (`docs/design/tokens.md:46-58`,
`frontend/.env.example:1-9`,
`frontend/lib/wagmi.ts:1-16`). The WalletConnect project id is now
only used when `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` is set; a
documented fallback emits only an injected browser wallet.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the token-document line reference.

docs/design/tokens.md:46-58 does not exist in the supplied file; the relevant changed tokens are at lines 12-23. Use the correct range so the evidence remains auditable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/evidence/0021/change-summary.md` around lines 75 - 79, Update the
documentation reference in the Documentation bullet of change-summary.md from
docs/design/tokens.md:46-58 to the correct changed-token range,
docs/design/tokens.md:12-23; leave the other file references and summary text
unchanged.

Comment on lines +47 to +54
- Latest audit attachment:
`/home/lora/.codex/attachments/5c520104-5049-4f0f-9578-e9808454ec95/pasted-text-1.txt`
- Main commit `b0afa48` has three successful GitHub checks, but none executes
backend tests.
- `docs/evidence/e2e/06-leaderboard.png` visibly contains static demo rows and
future-tense Phase 2 copy.
- `docs/evidence/e2e/03-monster-detail-alice.png` and
`04-monster-detail-bob.png` visibly contain broken image elements.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove the machine-local attachment path from the evidence bundle.

The /home/lora/... reference is inaccessible to other reviewers and exposes local filesystem details. Copy or sanitize the attachment into docs/evidence/0021/ and reference it with a repository-relative path.

As per coding guidelines, merge Evidence must be stored under docs/evidence/<issue>/.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/evidence/0021/context-bundle.md` around lines 47 - 54, Remove the
machine-local attachment reference from the evidence bundle, copy or sanitize
its contents into docs/evidence/0021/, and update the “Latest audit attachment”
entry to use the repository-relative file path. Keep the remaining evidence
statements unchanged.

Source: Coding guidelines

Comment thread docs/evidence/0021/review-report.md Outdated
Comment on lines +17 to +29
axe_script="(() => {
if (typeof window.axe === 'undefined') {
return Promise.resolve(JSON.stringify({status:'missing'}));
}
return window.axe.run(document, { runOnly: ['wcag2a','wcag2aa','wcag21a','wcag21aa'] }).then((report) => JSON.stringify({
seriousOrCritical: report.violations
.filter((v) => v.impact === 'serious' || v.impact === 'critical')
.map((v) => ({ id: v.id, impact: v.impact, nodes: v.nodes.length })),
moderate: report.violations
.filter((v) => v.impact === 'moderate')
.map((v) => ({ id: v.id, impact: v.impact, nodes: v.nodes.length })),
}));
})()"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail closed when axe is missing or reports violations.

The script returns {"status":"missing"} when axe is unavailable, filters out minor violations, and never checks axe_json before continuing. It can therefore generate apparently successful evidence without a valid accessibility scan.

Also applies to: 45-59

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/run-browser-evidence.sh` around lines 17 - 29, The browser evidence
flow around axe_script and axe_json must fail closed: treat a missing axe result
and any reported violations, including minor violations, as failures before
continuing. Preserve the scan output needed for diagnostics, validate axe_json
after evaluation, and exit nonzero or stop evidence generation whenever the
accessibility scan is unavailable or not clean.

Comment on lines +36 to +44
agent-browser --session "$session" open "$BASE_URL$route"
case "$route" in
/leaderboard) agent-browser --session "$session" wait --text 'MonadMon League' ;;
/profile*) agent-browser --session "$session" wait --text 'monster' || true ;;
/monster*) agent-browser --session "$session" wait --text 'Token ID' ;;
*) agent-browser --session "$session" wait --load networkidle ;;
esac
agent-browser --session "$session" errors --clear >/dev/null
agent-browser --session "$session" console --clear >/dev/null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Capture load-time diagnostics before clearing them.

open and the route-specific wait run before errors --clear and console --clear, so navigation-time failures are discarded. Move both clears before navigation, then collect diagnostics after the page settles.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/run-browser-evidence.sh` around lines 36 - 44, Update the route loop
around the navigation and diagnostics commands so agent-browser errors and
console output are cleared before open and the route-specific wait execute. Keep
the existing route waits unchanged, then collect the resulting diagnostics after
the page settles instead of clearing them.

Comment on lines +104 to +134
agent-browser --session evidence-leaderboard-desktop open "$BASE_URL/leaderboard"
agent-browser --session evidence-leaderboard-desktop wait --text 'MonadMon League'
agent-browser --session evidence-leaderboard-desktop eval "(() => { window.fetch = (input, init) => String(input).includes('/api/leaderboard') ? Promise.resolve(new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } })) : (window.__realFetch ? window.__realFetch(input, init) : new Response('not-mocked', { status: 404 })); return true; })()"
agent-browser --session evidence-leaderboard-desktop open "$BASE_URL/leaderboard"
agent-browser --session evidence-leaderboard-desktop wait --text 'No ranked trainers yet'
agent-browser --session evidence-leaderboard-desktop screenshot --full "$DESKTOP_DIR/leaderboard-empty.png" >/dev/null
agent-browser --session evidence-leaderboard-desktop eval --stdin < frontend/lib/axe.min.js
agent-browser --session evidence-leaderboard-desktop eval "$axe_script" > "$RESULTS_DIR/axe-leaderboard-empty.json"
printf 'route: /leaderboard\nviewport: 1400 900\nscreenshot: %s\n' "$DESKTOP_DIR/leaderboard-empty.png" > "$RESULTS_DIR/meta-leaderboard-empty.txt"
agent-browser --session evidence-leaderboard-desktop eval "(() => { window.fetch = (input, init) => String(input).includes('/api/leaderboard') ? Promise.reject(new TypeError('simulated indexer outage')) : (window.__realFetch ? window.__realFetch(input, init) : new Response('not-mocked', { status: 404 })); return true; })()"
agent-browser --session evidence-leaderboard-desktop open "$BASE_URL/leaderboard"
agent-browser --session evidence-leaderboard-desktop wait --text 'Leaderboard unavailable'
agent-browser --session evidence-leaderboard-desktop screenshot --full "$DESKTOP_DIR/leaderboard-unavailable.png" >/dev/null
agent-browser --session evidence-leaderboard-desktop eval --stdin < frontend/lib/axe.min.js
agent-browser --session evidence-leaderboard-desktop eval "$axe_script" > "$RESULTS_DIR/axe-leaderboard-unavailable.json"
printf 'route: /leaderboard\nviewport: 1400 900\nscreenshot: %s\n' "$DESKTOP_DIR/leaderboard-unavailable.png" > "$RESULTS_DIR/meta-leaderboard-unavailable.txt"
agent-browser --session evidence-leaderboard-mobile set viewport 390 844
agent-browser --session evidence-leaderboard-mobile open "$BASE_URL/leaderboard"
agent-browser --session evidence-leaderboard-mobile wait --text 'MonadMon League'
agent-browser --session evidence-leaderboard-mobile eval "(() => { window.fetch = (input, init) => String(input).includes('/api/leaderboard') ? Promise.resolve(new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } })) : (window.__realFetch ? window.__realFetch(input, init) : new Response('not-mocked', { status: 404 })); return true; })()"
agent-browser --session evidence-leaderboard-mobile open "$BASE_URL/leaderboard"
agent-browser --session evidence-leaderboard-mobile wait --text 'No ranked trainers yet'
agent-browser --session evidence-leaderboard-mobile screenshot --full "$MOBILE_DIR/leaderboard-empty.png" >/dev/null
agent-browser --session evidence-leaderboard-mobile eval --stdin < frontend/lib/axe.min.js
agent-browser --session evidence-leaderboard-mobile eval "$axe_script" > "$RESULTS_DIR/axe-leaderboard-empty-mobile.json"
agent-browser --session evidence-leaderboard-mobile eval "(() => { window.fetch = (input, init) => String(input).includes('/api/leaderboard') ? Promise.reject(new TypeError('simulated indexer outage')) : (window.__realFetch ? window.__realFetch(input, init) : new Response('not-mocked', { status: 404 })); return true; })()"
agent-browser --session evidence-leaderboard-mobile open "$BASE_URL/leaderboard"
agent-browser --session evidence-leaderboard-mobile wait --text 'Leaderboard unavailable'
agent-browser --session evidence-leaderboard-mobile screenshot --full "$MOBILE_DIR/leaderboard-unavailable.png" >/dev/null
agent-browser --session evidence-leaderboard-mobile eval --stdin < frontend/lib/axe.min.js
agent-browser --session evidence-leaderboard-mobile eval "$axe_script" > "$RESULTS_DIR/axe-leaderboard-unavailable-mobile.json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Collect console and error evidence for mocked states.

The empty/unavailable desktop and mobile branches capture screenshots and axe output but never read console or errors. This does not support the report’s claim that all captured routes have clean browser diagnostics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/run-browser-evidence.sh` around lines 104 - 134, Update the empty and
unavailable leaderboard branches in the browser evidence flow for both desktop
and mobile sessions to collect and persist browser console and error diagnostics
alongside the existing screenshots and axe results. Ensure each mocked-state
capture reads the relevant console and errors data before completing, so all
four states provide clean browser-diagnostic evidence.

Comment on lines +106 to +107
agent-browser --session evidence-leaderboard-desktop eval "(() => { window.fetch = (input, init) => String(input).includes('/api/leaderboard') ? Promise.resolve(new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } })) : (window.__realFetch ? window.__realFetch(input, init) : new Response('not-mocked', { status: 404 })); return true; })()"
agent-browser --session evidence-leaderboard-desktop open "$BASE_URL/leaderboard"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Install leaderboard mocks without navigating away afterward.

Each branch overrides window.fetch and then calls open; a normal navigation replaces the page context and removes that override. Use browser-level request interception, or inject the mock after navigation and explicitly trigger a refetch without a full reload.

Also applies to: 113-114, 123-124, 129-130

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/run-browser-evidence.sh` around lines 106 - 107, Update the
leaderboard mock setup in the affected branches of the browser evidence script
so the fetch interception remains active after navigating to the leaderboard
page. Use browser-level request interception, or navigate first and inject the
mock before explicitly triggering a refetch; do not override window.fetch and
then call open, since navigation discards that page context.

Comment on lines +16 to +24
## Current State

- Baseline audit complete.
- Recovery Issue and isolated worktree created.
- Context bundle and implementation plan recorded under
`docs/evidence/0021/`.
- Backend, frontend, local E2E, browser evidence, reviews, and CI are pending.

See `sessions/issue-0021-live-e2e/summary.md` for the durable session log.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== sessions/current-session.md ==\n'
cat -n sessions/current-session.md | sed -n '1,120p'

printf '\n== evidence files under docs/evidence/0021 ==\n'
find docs/evidence/0021 -maxdepth 3 -type f | sort

printf '\n== summary.md ==\n'
if [ -f sessions/issue-0021-live-e2e/summary.md ]; then
  cat -n sessions/issue-0021-live-e2e/summary.md | sed -n '1,220p'
else
  echo 'missing: sessions/issue-0021-live-e2e/summary.md'
fi

printf '\n== evidence keywords ==\n'
rg -n "backend|frontend|E2E|browser|CI|review|pending|complete|completed|passed" docs/evidence/0021 sessions/issue-0021-live-e2e -S || true

Repository: lora-sys/monadmon

Length of output: 14648


Align the session status with the closed log sessions/current-session.md:16-24 still says backend, frontend, E2E, browser evidence, reviews, and CI are pending, but the durable session log marks the recovery closed and ready for review. Update this note or label it as a historical snapshot.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sessions/current-session.md` around lines 16 - 24, Update the “Current State”
section in sessions/current-session.md so it no longer presents backend,
frontend, E2E, browser evidence, reviews, and CI as pending; align it with the
closed status in the durable session log, or clearly label the existing content
as a historical snapshot.

Replace the Tailwind-default look with a custom theme (Retro Acid saturation + Future 3D motion depth + existing accent) for all 7 routes. The same backend endpoints and the recovered indexer (PR #34) are unchanged; only the visual system, motion language, and component layout change.

Surface changes:

- frontend/lib/design.ts: shared motion + color tokens

- frontend/components/CreativeShell.tsx: fixed grain + accent-glow background, sticky slim header with mobile bottom nav

- frontend/app/globals.css: redesigned palette (--accent, --ink-2, --bg-0), three CSS keyframe animations (mm-breath, mm-float, mm-marquee)

- frontend/app/page.tsx: five-section landing (hero with floating poster + accent halo, species marquee, numbered awakening steps 01-05, full-bleed arena banner, league preview)

- frontend/app/{mint,train,arena,leaderboard,profile,monster}/*.tsx: same functional contract, redesigned components

- scripts/run-browser-evidence.sh: updated wait markers (The strongest trainers, Creature /) for the new H1 labels

- frontend/app/train/page.tsx: raw <img> -> next/image (resolves L3 from PR #34)

Verification: 8 axe-core WCAG 2.0/2.1 A/AA reports at populated, empty, unavailable, monster detail, both profiles (desktop+mobile) all return seriousOrCritical:[]; production build green; 47 forge + 9 backend vitest + 14 frontend vitest pass. Evidence: docs/evidence/0021/.
@lora-sys lora-sys changed the title fix(#33): live leaderboard, indexed events, real E2E feat(#33): live leaderboard + creative redesign Jul 14, 2026
@lora-sys
lora-sys merged commit 4af63de into main Jul 14, 2026
3 of 4 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
frontend/app/globals.css (1)

42-64: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider a prefers-reduced-motion fallback for the new infinite animations.

mm-breath, mm-float, mm-marquee, and mm-glitch all run as infinite loops and are consumed as continuous decorative/ambient motion on the homepage hero, species marquee, mint hero, and monster detail page. None of these utilities are gated behind @media (prefers-reduced-motion: reduce), so users with a reduced-motion OS preference still get the full-strength animations.

♿ Suggested addition
+@media (prefers-reduced-motion: reduce) {
+  .animate-mm-breath,
+  .animate-mm-float,
+  .animate-mm-marquee,
+  .animate-mm-glitch {
+    animation: none;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/app/globals.css` around lines 42 - 64, Gate the infinite animation
utilities animate-mm-breath, animate-mm-float, animate-mm-marquee, and
animate-mm-glitch behind a prefers-reduced-motion: reduce media query. Disable
or otherwise remove their animation for users requesting reduced motion while
preserving the existing animations for all other users.
frontend/app/page.tsx (1)

200-235: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

"Live standings" copy is backed by hardcoded example data.

The section text claims the standings are live ("The indexer reads each battle the moment it resolves"), but the trainer/wins/XP shown are hardcoded. Since HomePage is a server component, fetching the top leaderboard entry server-side would keep this consistent with the actual /api/leaderboard endpoint used elsewhere.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/app/page.tsx` around lines 200 - 235, The LeaguePreview component
currently displays hardcoded trainer, win, and XP values despite describing live
standings. Update HomePage/LeaguePreview to fetch the top entry from the
existing leaderboard data source server-side, pass that entry into
LeaguePreview, and render its trainer, wins, and XP values while preserving the
existing fallback or empty-state behavior if no entry is available.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/app/globals.css`:
- Line 21: Update the global CSS declaration block by adding the required blank
line before color-scheme: dark and removing unnecessary quotes around the
single-word Inter font name, while retaining quotes for font names containing
whitespace such as Space Grotesk.

In `@frontend/app/mint/page.tsx`:
- Around line 82-86: Update handleHatch and the Hatch button flow to prevent
duplicate submissions: track the in-flight hatch using the existing pending
state, set the stage to the completed state after writeContractAsync succeeds,
and disable or hide the button while pending and after success. Preserve the
existing error handling and ensure the button remains available only when no
hatch is pending or completed.

In `@frontend/app/page.tsx`:
- Around line 83-115: Update SpeciesMarquee to provide a pause control for its
auto-scrolling animation, including the specified hover-to-pause behavior and an
accessible keyboard-operable way to pause or stop it; convert the component to a
client component if needed to manage this state. Mark the duplicated second half
of tiles as hidden from assistive technology while preserving its visual
rendering and seamless marquee loop.

---

Nitpick comments:
In `@frontend/app/globals.css`:
- Around line 42-64: Gate the infinite animation utilities animate-mm-breath,
animate-mm-float, animate-mm-marquee, and animate-mm-glitch behind a
prefers-reduced-motion: reduce media query. Disable or otherwise remove their
animation for users requesting reduced motion while preserving the existing
animations for all other users.

In `@frontend/app/page.tsx`:
- Around line 200-235: The LeaguePreview component currently displays hardcoded
trainer, win, and XP values despite describing live standings. Update
HomePage/LeaguePreview to fetch the top entry from the existing leaderboard data
source server-side, pass that entry into LeaguePreview, and render its trainer,
wins, and XP values while preserving the existing fallback or empty-state
behavior if no entry is available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4674d155-4b41-4930-af9e-dc63cf71c5f2

📥 Commits

Reviewing files that changed from the base of the PR and between 1e71783 and fee0860.

⛔ Files ignored due to path filters (10)
  • docs/evidence/0021/screenshots/desktop/leaderboard-empty.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/desktop/leaderboard-unavailable.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/desktop/monster-2-emberfox.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/desktop/populated.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/desktop/profile-alice.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/desktop/profile-bob.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/mobile/leaderboard-empty.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/mobile/leaderboard-unavailable.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/mobile/monster-2-emberfox.png is excluded by !**/*.png
  • docs/evidence/0021/screenshots/mobile/populated.png is excluded by !**/*.png
📒 Files selected for processing (17)
  • docs/design/002-redesign/brief.md
  • docs/design/002-redesign/macro-round-1.md
  • docs/evidence/0021/change-summary.md
  • docs/evidence/0021/review-report.md
  • docs/evidence/0021/verification.md
  • frontend/app/arena/page.tsx
  • frontend/app/globals.css
  • frontend/app/layout.tsx
  • frontend/app/leaderboard/page.tsx
  • frontend/app/mint/page.tsx
  • frontend/app/monster/[tokenId]/page.tsx
  • frontend/app/page.tsx
  • frontend/app/profile/[address]/page.tsx
  • frontend/app/train/page.tsx
  • frontend/components/CreativeShell.tsx
  • frontend/lib/design.ts
  • scripts/run-browser-evidence.sh
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/app/leaderboard/page.tsx
  • frontend/app/profile/[address]/page.tsx
  • scripts/run-browser-evidence.sh

Comment thread frontend/app/globals.css
--nature: #5CD891;
--electric: #C8A91F;
--danger: #FF6F7D;
color-scheme: dark;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix stylelint errors flagged in this block.

Static analysis flags a missing blank line before color-scheme: dark; (line 21) and unnecessary quotes around the single-word "Inter" (line 27, quotes are only required/recommended for names containing whitespace like "Space Grotesk").

🎨 Proposed stylelint fixes
   --danger: `#FF6F7D`;
+
   color-scheme: dark;
 }
 
 html, body {
   background: var(--bg-0);
   color: var(--ink-0);
-  font-family: "Space Grotesk", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+  font-family: "Space Grotesk", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
   -webkit-font-smoothing: antialiased;
 }

Also applies to: 27-27

🧰 Tools
🪛 Stylelint (17.14.0)

[error] 21-21: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/app/globals.css` at line 21, Update the global CSS declaration block
by adding the required blank line before color-scheme: dark and removing
unnecessary quotes around the single-word Inter font name, while retaining
quotes for font names containing whitespace such as Space Grotesk.

Source: Linters/SAST tools

Comment on lines 82 to 86
} catch (e) {
setError((e as Error).message ?? "Hatch failed");
setStage("error");
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Hatch flow has no completion/pending guard, allowing duplicate submissions.

handleHatch's success path never updates stage after writeContractAsync resolves — only the catch block sets "error". Combined with the Hatch button being gated solely on hasMinted (no disabled for an in-flight hatch, no post-hatch hide), a user can click "Hatch" again while a hatch transaction is pending or after it has already succeeded. The Mint button already guards against this analogous case via isMintPending/hasMinted; the Hatch button has no equivalent.

🔒 Suggested fix
+  const [isHatching, setIsHatching] = useState(false);
+
   async function handleHatch() {
     let id = tokenId;
     if (id === null) {
       ...
       setTokenId(id);
     }
     try {
       setStage("hatching");
+      setIsHatching(true);
       await writeContractAsync({
         address: MONSTER_NFT_ADDRESS,
         abi: monsterNftAbi,
         functionName: "hatch",
         args: [id],
       });
+      setStage("hatched");
     } catch (e) {
       setError((e as Error).message ?? "Hatch failed");
       setStage("error");
+    } finally {
+      setIsHatching(false);
     }
   }
-          {hasMinted ? (
+          {hasMinted && stage !== "hatched" ? (
             <button
               onClick={handleHatch}
+              disabled={isHatching}
               className="rounded-full border border-[`#1F2333`] px-5 py-3 text-sm uppercase tracking-[0.18em] text-[`#B5BAC8`] transition-colors hover:border-[`#7AF0BA`] hover:text-[`#7AF0BA`]"
             >
-              Hatch
+              {isHatching ? "Hatching..." : "Hatch"}
             </button>
           ) : null}

Also applies to: 109-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/app/mint/page.tsx` around lines 82 - 86, Update handleHatch and the
Hatch button flow to prevent duplicate submissions: track the in-flight hatch
using the existing pending state, set the stage to the completed state after
writeContractAsync succeeds, and disable or hide the button while pending and
after success. Preserve the existing error handling and ensure the button
remains available only when no hatch is pending or completed.

Comment thread frontend/app/page.tsx
Comment on lines +83 to +115
function SpeciesMarquee() {
const tiles = [...species, ...species];
return (
<section className="relative overflow-hidden border-y border-[#1F2333] py-10">
<div
className="flex w-max gap-6"
style={{ animation: "mm-marquee 32s linear infinite" }}
>
{tiles.map((s, idx) => (
<article
key={`${s.id}-${idx}`}
className="flex w-44 shrink-0 flex-col items-center gap-3 rounded-2xl border border-[#1F2333] bg-[#0E1119] p-3"
>
<Image
src={`/assets/monsters/${s.id}/stage1.png`}
alt={s.name}
width={256}
height={256}
unoptimized
className="h-28 w-28 object-contain"
/>
<div className="text-center">
<p className="text-sm font-semibold">{s.name}</p>
<p className="font-mono text-[10px] uppercase tracking-[0.2em] text-[#858DA1]">
{s.element}
</p>
</div>
</article>
))}
</div>
</section>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Species marquee lacks a pause control and exposes duplicated content to assistive tech.

The marquee auto-scrolls indefinitely (32s loop) with no way to pause/stop it, which fails WCAG 2.2.2 (Level A) for auto-starting moving content lasting more than 5 seconds shown in parallel with other content. The design brief itself specifies "鼠标悬停暂停" (pause on hover) for this exact section, which isn't implemented. Separately, tiles = [...species, ...species] renders every species twice; the duplicated half isn't hidden from screen readers, so each species name/element gets announced twice.

♿ Suggested fix (requires converting this component to a client component, e.g. via a top-level `"use client"` or extracting it)
-function SpeciesMarquee() {
+function SpeciesMarquee() {
+  const [paused, setPaused] = useState(false);
   const tiles = [...species, ...species];
   return (
     <section className="relative overflow-hidden border-y border-[`#1F2333`] py-10">
       <div
         className="flex w-max gap-6"
-        style={{ animation: "mm-marquee 32s linear infinite" }}
+        style={{
+          animation: "mm-marquee 32s linear infinite",
+          animationPlayState: paused ? "paused" : "running",
+        }}
+        onMouseEnter={() => setPaused(true)}
+        onMouseLeave={() => setPaused(false)}
+        onFocus={() => setPaused(true)}
+        onBlur={() => setPaused(false)}
       >
         {tiles.map((s, idx) => (
           <article
             key={`${s.id}-${idx}`}
+            aria-hidden={idx >= species.length}
             className="flex w-44 shrink-0 flex-col items-center gap-3 rounded-2xl border border-[`#1F2333`] bg-[`#0E1119`] p-3"
           >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/app/page.tsx` around lines 83 - 115, Update SpeciesMarquee to
provide a pause control for its auto-scrolling animation, including the
specified hover-to-pause behavior and an accessible keyboard-operable way to
pause or stop it; convert the component to a client component if needed to
manage this state. Mark the duplicated second half of tiles as hidden from
assistive technology while preserving its visual rendering and seamless marquee
loop.

Source: Coding guidelines

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.

[Bug] ISSUE-0021: rebuild live leaderboard and local E2E

1 participant