feat(#33): live leaderboard + creative redesign - #34
Conversation
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/.
📝 WalkthroughWalkthroughThe 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. ChangesLive data path recovery
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winHandle
ownerOffailures explicitly. The catch treats every failure as a burn, butupsertMonster()coalescesnull/undefinedto 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 winFix the placeholder image extension.
The fallback image for eggs is referenced as
placeholder.pnghere, but across the rest of the codebase (such asProfilePage,species.ts, and test files), the asset is strictly defined asplaceholder.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 winPrefer
useConnectModalfrom 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 provideduseConnectModalhook.♻️ 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
onClickhandler:{!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 winClear 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
.nextdirectory. 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
.nextdirectory 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 valueUnreachable cleanup branch when
KEEP_RUNNING=1.The check for
exit_code == 0whenKEEP_RUNNING=1is unreachable because the script intentionally enters an infinitewhileloop at the end (line 272). Consequently, terminating the script (e.g., viaCtrl+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
⛔ Files ignored due to path filters (13)
docs/evidence/0021/screenshots/desktop/leaderboard-empty.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/desktop/leaderboard-unavailable.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/desktop/monster-2-emberfox.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/desktop/populated.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/desktop/profile-alice.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/desktop/profile-bob.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/mobile/leaderboard-empty.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/mobile/leaderboard-unavailable.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/mobile/monster-2-emberfox.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/mobile/populated.pngis excluded by!**/*.pngfrontend/lib/axe.min.jsis excluded by!**/*.min.jsfrontend/package-lock.jsonis excluded by!**/package-lock.jsonfrontend/public/assets/monsters/placeholder.svgis excluded by!**/*.svg
📒 Files selected for processing (46)
.github/workflows/ci.ymlPROJECT_STATUS.mdbackend/package.jsonbackend/run-indexer.shbackend/src/abis.tsbackend/src/db.tsbackend/src/indexer.tsbackend/src/server.tsbackend/test/db.test.tsbackend/test/indexer.test.tsbackend/test/server.test.tsbackend/tsconfig.test.jsondocs/design/tokens.mddocs/evidence/0021/change-summary.mddocs/evidence/0021/context-bundle.mddocs/evidence/0021/implementation-plan.mddocs/evidence/0021/review-report.mddocs/evidence/0021/verification.mdfrontend/.env.examplefrontend/app/arena/page.tsxfrontend/app/globals.cssfrontend/app/layout.tsxfrontend/app/leaderboard/page.tsxfrontend/app/monster/[tokenId]/page.tsxfrontend/app/page.tsxfrontend/app/profile/[address]/page.tsxfrontend/app/train/page.tsxfrontend/components/Header.tsxfrontend/components/LeaderboardView.tsxfrontend/lib/chains.tsfrontend/lib/indexer.tsfrontend/lib/species.tsfrontend/lib/wagmi.tsfrontend/next.config.mjsfrontend/package.jsonfrontend/test/axe-min.test.tsfrontend/test/chains.test.tsfrontend/test/indexer.test.tsfrontend/test/leaderboard-view.test.tsxfrontend/test/species-art.test.tsfrontend/vitest.config.tsmemory/lessons.mdscripts/local-e2e.shscripts/run-browser-evidence.shsessions/current-session.mdsessions/issue-0021-live-e2e/summary.md
| 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; |
There was a problem hiding this comment.
🗄️ 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.tsRepository: lora-sys/monadmon
Length of output: 15515
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,120p' backend/src/chains.tsRepository: 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.
| 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 }); | ||
| }); |
There was a problem hiding this comment.
📐 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.
| - **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. |
There was a problem hiding this comment.
📐 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.
| - 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. |
There was a problem hiding this comment.
📐 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
| 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 })), | ||
| })); | ||
| })()" |
There was a problem hiding this comment.
🎯 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.
| 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 |
There was a problem hiding this comment.
🗄️ 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.
| 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" |
There was a problem hiding this comment.
🗄️ 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.
| 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" |
There was a problem hiding this comment.
🎯 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.
| ## 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. |
There was a problem hiding this comment.
🗄️ 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 || trueRepository: 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/.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
frontend/app/globals.css (1)
42-64: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winConsider a
prefers-reduced-motionfallback for the new infinite animations.
mm-breath,mm-float,mm-marquee, andmm-glitchall 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
HomePageis a server component, fetching the top leaderboard entry server-side would keep this consistent with the actual/api/leaderboardendpoint 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
⛔ Files ignored due to path filters (10)
docs/evidence/0021/screenshots/desktop/leaderboard-empty.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/desktop/leaderboard-unavailable.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/desktop/monster-2-emberfox.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/desktop/populated.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/desktop/profile-alice.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/desktop/profile-bob.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/mobile/leaderboard-empty.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/mobile/leaderboard-unavailable.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/mobile/monster-2-emberfox.pngis excluded by!**/*.pngdocs/evidence/0021/screenshots/mobile/populated.pngis excluded by!**/*.png
📒 Files selected for processing (17)
docs/design/002-redesign/brief.mddocs/design/002-redesign/macro-round-1.mddocs/evidence/0021/change-summary.mddocs/evidence/0021/review-report.mddocs/evidence/0021/verification.mdfrontend/app/arena/page.tsxfrontend/app/globals.cssfrontend/app/layout.tsxfrontend/app/leaderboard/page.tsxfrontend/app/mint/page.tsxfrontend/app/monster/[tokenId]/page.tsxfrontend/app/page.tsxfrontend/app/profile/[address]/page.tsxfrontend/app/train/page.tsxfrontend/components/CreativeShell.tsxfrontend/lib/design.tsscripts/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
| --nature: #5CD891; | ||
| --electric: #C8A91F; | ||
| --danger: #FF6F7D; | ||
| color-scheme: dark; |
There was a problem hiding this comment.
📐 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
| } catch (e) { | ||
| setError((e as Error).message ?? "Hatch failed"); | ||
| setStage("error"); | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 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.
| 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🎯 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
Linked Issue
Closes #33
What changed
This PR carries two rounds on
fix/33-live-e2e-rebuild:Round 1 — Live E2E recovery
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.
explicit loading/empty/unavailable states and
role="status" aria-live.wallet's real token IDs.
bigint.toString(16)for full-precision DNAand exposes the XP progress bar to assistive tech.
npm testfor 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-glowbackground, sticky slim header with mobile bottom nav, monospace
footer.
frontend/app/globals.css— redesigned palette, three CSS keyframeanimations (
mm-breath,mm-float,mm-marquee).frontend/app/page.tsx— five-section landing (hero with floatingposter + 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 thenew H1 labels.
Evidence
next buildclean (9 routes).scripts/local-e2e.sh) + browser evidence(
scripts/run-browser-evidence.sh) reproducible from a fresh Anvil.populated, empty, unavailable leaderboard, monster detail, and
both profiles.
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
<img>resolved; new design re-tested with axe on all 8 states,zero serious/critical)
Summary by CodeRabbit
limitquery values now return a clear 400 error instead of silent clamping.npm test; added/expanded Vitest + axe coverage.