Conversation
- New /api/stats endpoints: blocks-chart, daily-txs, gas-price with configurable time windows (1H/6H/24H/7D/1M) - Status page: daily transactions (14d fixed), avg gas used, tx count, avg gas price charts with window toggle - Token detail page: transfer count and volume charts with window toggle - Per-chart independent loading states; no grid lines; animations disabled for instant render - Tooltip shows exact time range per bucket (e.g. "Mar 25, 10:00 – 11:00") - 7D window uses 12-hour buckets (14 bars); axis labels show dates not times for day+ windows
📝 WalkthroughWalkthroughAdds time-windowed chart endpoints and handlers in the backend, corresponding frontend API clients, polling hooks, and Recharts-based charts on Status and Token pages; includes integration tests and a new frontend dependency ( Changes
Sequence Diagram(s)sequenceDiagram
participant Browser
participant Frontend
participant Backend
participant DB
Browser->>Frontend: user opens Status / Token page
Frontend->>Frontend: useChartData / useTokenChart starts polling
Frontend->>Backend: GET /api/stats/blocks-chart?window=...
Backend->>DB: SQL generate_series + aggregates (blocks/transactions)
DB-->>Backend: aggregated bucketed rows
Backend-->>Frontend: JSON array of buckets
Frontend->>Browser: render Recharts with data
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/pages/StatusPage.tsx (1)
44-71:⚠️ Potential issue | 🟠 MajorStop polling
/api/statusevery 5 seconds.This page already gets live
height/indexed_atfromBlockStatsContext, so the interval just keeps re-downloading the full status payload and adds avoidable backend load. Fetch/api/statusonce on mount, and use a lighter feed if you need live totals too.♻️ Suggested change
fetchStatus(); - const id = setInterval(fetchStatus, 5000); return () => { mounted = false; - clearInterval(id); }; }, []);Based on learnings, GET /api/status returns full chain info fetched once on page load.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/StatusPage.tsx` around lines 44 - 71, The current useEffect in StatusPage.tsx repeatedly polls getChainStatus every 5s; remove the interval to only fetch once on mount: keep the fetchStatus async function (calling getChainStatus and updating state via setStatus, setLoading, setError), invoke fetchStatus() once, and eliminate setInterval(fetchStatus, 5000) and clearInterval(id) logic; retain the mounted flag and its cleanup (mounted = false) to avoid state updates after unmount.
🧹 Nitpick comments (2)
backend/crates/atlas-server/src/api/handlers/stats.rs (1)
142-169: Consider filling missing days for chart consistency.Unlike
get_blocks_chart, this query doesn't usegenerate_seriesto fill missing days. If there are days with zero transactions, they won't appear in the response, potentially causing gaps in the frontend chart's x-axis.For a 14-day window on an active chain this is likely fine, but for consistency with other chart endpoints, consider using
generate_seriesto ensure all 14 days are present.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/crates/atlas-server/src/api/handlers/stats.rs` around lines 142 - 169, The get_daily_txs handler currently returns only days that have transactions; update the SQL in get_daily_txs to use generate_series to produce the full 14-day window and LEFT JOIN aggregated transaction counts so missing days return zero; keep the same mapping to DailyTxPoint and ordering, e.g. generate a series from (max_ts - 13*86400) to max_ts by 1 day (or timestamps adjusted to date) and LEFT JOIN the per-day counts, coalescing NULL counts to 0 before collecting into Vec<DailyTxPoint>.frontend/src/pages/TokenDetailPage.tsx (1)
14-89: Extract the shared chart config/helpers before these pages drift further.
WINDOWS,BUCKET_MS, the bucket formatters, and the card wrapper are duplicated almost verbatim inStatusPage.tsx, and they’re already out of sync (6M/1Yonly appear here). Pulling them into a shared chart module will keep the Status and Token pages aligned.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/TokenDetailPage.tsx` around lines 14 - 89, The chart constants and helpers (WINDOWS, BUCKET_MS, formatBucketTick, formatBucketTooltip, formatCompact) plus the TokenChartCard component are duplicated and drifting between TokenDetailPage and StatusPage; extract them into a shared module (e.g., frontend/src/components/chartUtils or chart/*), move WINDOWS and BUCKET_MS and the formatter functions (formatBucketTick, formatBucketTooltip, formatCompact) plus TokenChartCard into that module, export them, and update TokenDetailPage (and StatusPage) to import and use those shared symbols so both pages stay in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/crates/atlas-server/src/api/handlers/stats.rs`:
- Around line 212-223: The get_gas_price_chart handler currently drops empty
buckets by using filter_map on rows, causing fewer points than get_blocks_chart;
change the transformation of rows (the iterator producing points and the
GasPricePoint construction) to map every (bucket, avg_gas_price) into a
GasPricePoint and replace None with 0.0 (e.g., use avg_gas_price.unwrap_or(0.0)
or equivalent) so the points Vec contains one entry per bucket (bucket ->
bucket.to_rfc3339(), avg_gas_price -> 0.0 when missing), matching
get_blocks_chart behavior.
In `@backend/crates/atlas-server/src/api/handlers/tokens.rs`:
- Line 346: The current divisor calculation uses 10_i64.pow(decimals.clamp(0,
18) as u32) which can overflow/ panic for decimals ≥ 19; replace that with
creating the divisor via BigDecimal::from_str so large exponents are handled
safely (e.g. build the string "1e{decimals}" or otherwise format the
power-of-ten as a string) and parse it into a BigDecimal for the variable
divisor in tokens.rs (use the existing decimals variable and handle parse errors
appropriately rather than using i64::pow).
In `@frontend/src/hooks/useChartData.ts`:
- Around line 12-18: The ChartData shape and useChartData hook currently expose
only blocksChartLoading, gasPriceLoading and a single shared error while
dailyTxs and gasPriceChart failures are swallowed; change the ChartData
interface and the useChartData hook to return per-dataset state objects (e.g.
blocksChart: { data: BlockChartPoint[]; loading: boolean; error: string | null
}, dailyTxs: { data: DailyTxPoint[]; loading: boolean; error: string | null },
gasPriceChart: { data: GasPricePoint[]; loading: boolean; error: string | null
}) instead of raw arrays and a shared error, update the fetch logic in
useChartData to set each dataset's loading and error independently (set loading
true before each fetch, clear/set that dataset's error on success/failure), and
update any consumers to read .data/.loading/.error from blocksChart, dailyTxs,
and gasPriceChart rather than the old top-level fields.
In `@frontend/src/hooks/useTokenChart.ts`:
- Around line 15-20: The effect in useTokenChart's useEffect early-returns when
address is falsy but doesn't reset state; update the effect so that when address
is undefined/null you call setLoading(false) and setData([]) (and preserve any
mounted flag behavior) before returning. Target the useEffect block that
references address, mounted, setLoading and setData to ensure loading isn't left
true when address changes away.
- Around line 27-28: In useTokenChart's catch block, the error extraction only
checks for Error instances so ApiError objects (which carry an error property)
fall back to the generic message; update the catch to detect ApiError shapes
(e.g., err is an object and has an "error" or "message" property) and use that
property for setError when mounted, otherwise fall back to the existing Error
check and finally the generic text—refer to useTokenChart, the mounted flag, and
setError to locate the change.
In `@frontend/src/pages/StatusPage.tsx`:
- Around line 305-310: The formatGwei function currently falls back to raw wei
for values under 1 gwei, which makes labels hard to read; change the final
branch in formatGwei to keep the unit as gwei and format fractional values
(e.g., use gwei.toFixed(3) or similar precision) so sub-gwei prices display as
"0.123gwei" instead of "123000000wei"; update the return in formatGwei to use
the gwei variable with an appropriate fixed decimal precision while leaving the
other branches unchanged.
---
Outside diff comments:
In `@frontend/src/pages/StatusPage.tsx`:
- Around line 44-71: The current useEffect in StatusPage.tsx repeatedly polls
getChainStatus every 5s; remove the interval to only fetch once on mount: keep
the fetchStatus async function (calling getChainStatus and updating state via
setStatus, setLoading, setError), invoke fetchStatus() once, and eliminate
setInterval(fetchStatus, 5000) and clearInterval(id) logic; retain the mounted
flag and its cleanup (mounted = false) to avoid state updates after unmount.
---
Nitpick comments:
In `@backend/crates/atlas-server/src/api/handlers/stats.rs`:
- Around line 142-169: The get_daily_txs handler currently returns only days
that have transactions; update the SQL in get_daily_txs to use generate_series
to produce the full 14-day window and LEFT JOIN aggregated transaction counts so
missing days return zero; keep the same mapping to DailyTxPoint and ordering,
e.g. generate a series from (max_ts - 13*86400) to max_ts by 1 day (or
timestamps adjusted to date) and LEFT JOIN the per-day counts, coalescing NULL
counts to 0 before collecting into Vec<DailyTxPoint>.
In `@frontend/src/pages/TokenDetailPage.tsx`:
- Around line 14-89: The chart constants and helpers (WINDOWS, BUCKET_MS,
formatBucketTick, formatBucketTooltip, formatCompact) plus the TokenChartCard
component are duplicated and drifting between TokenDetailPage and StatusPage;
extract them into a shared module (e.g., frontend/src/components/chartUtils or
chart/*), move WINDOWS and BUCKET_MS and the formatter functions
(formatBucketTick, formatBucketTooltip, formatCompact) plus TokenChartCard into
that module, export them, and update TokenDetailPage (and StatusPage) to import
and use those shared symbols so both pages stay in sync.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 666682f8-16e3-451d-9e3f-5fa63e597d60
⛔ Files ignored due to path filters (1)
frontend/bun.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
backend/crates/atlas-server/src/api/handlers/mod.rsbackend/crates/atlas-server/src/api/handlers/stats.rsbackend/crates/atlas-server/src/api/handlers/tokens.rsbackend/crates/atlas-server/src/api/mod.rsfrontend/package.jsonfrontend/src/api/chartData.tsfrontend/src/hooks/index.tsfrontend/src/hooks/useChartData.tsfrontend/src/hooks/useTokenChart.tsfrontend/src/pages/StatusPage.tsxfrontend/src/pages/TokenDetailPage.tsx
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
frontend/src/pages/TokenDetailPage.tsx (2)
64-72: VerifyBUCKET_MSvalues match backendbucket_secs.These client-side bucket durations are used for tooltip range display. They must stay in sync with the backend's
Window::bucket_secs()implementation. Current mapping appears correct based on context snippet 2, but any backend changes would silently break tooltip accuracy.
Window Backend (secs) Frontend (ms) Match? 1h 300 300,000 ✓ 6h 1,800 1,800,000 ✓ 24h 3,600 3,600,000 ✓ 7d 43,200 43,200,000 ✓ 1m 86,400 86,400,000 ✓ 6m 604,800 604,800,000 ✓ 1y 1,209,600 1,209,600,000 ✓ Consider adding a comment referencing the backend source to help future maintainers keep these in sync.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/TokenDetailPage.tsx` around lines 64 - 72, Update the BUCKET_MS mapping to ensure it remains in sync with the backend Window::bucket_secs() values and add a brief comment above BUCKET_MS referencing the backend source/commit (and the ChartWindow type) so maintainers know to update both places when backend bucket_secs changes; verify the numeric values for each key ('1h','6h','24h','7d','1m','6m','1y') match the backend seconds×1000 and include the reference to Window::bucket_secs() in the comment.
35-48: Simplify redundant branches informatBucketTick.The conditions for
'1m','6m','1y', and'7d'all return the same date format. Consider consolidating:♻️ Suggested simplification
function formatBucketTick(bucket: string, window: ChartWindow): string { const d = new Date(bucket); if (isNaN(d.getTime())) return bucket; - if (window === '1m') { - return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); - } - if (window === '6m' || window === '1y') { - return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); - } - if (window === '7d') { + if (['7d', '1m', '6m', '1y'].includes(window)) { return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); } return d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/TokenDetailPage.tsx` around lines 35 - 48, The function formatBucketTick contains redundant branches for '1m', '6m', '1y', and '7d' that all return the same toLocaleDateString format; simplify by collapsing them into a single condition (e.g., if (['1m','6m','1y','7d'].includes(window)) return d.toLocaleDateString(...)) and keep the NaN check and the fallback to toLocaleTimeString; update the function formatBucketTick (and the ChartWindow type usage) accordingly so behavior is unchanged but branches are consolidated.backend/crates/atlas-server/src/api/handlers/tokens.rs (1)
294-370: Consider adding unit tests for the new chart handler logic.Per coding guidelines, new logic should have unit tests in a
#[cfg(test)] mod testsblock. While integration tests cover the endpoint, unit tests for edge cases like:
- Empty result sets (no transfers in window)
- Tokens with high decimals (> 18)
- Volume calculation accuracy
would improve confidence in the decimal conversion and bucketing logic.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/crates/atlas-server/src/api/handlers/tokens.rs` around lines 294 - 370, Add a #[cfg(test)] mod tests with unit tests that directly exercise the get_token_chart logic (call get_token_chart or extractable helper logic) to cover empty result sets, tokens with decimals > 18, and volume conversion/bucketing accuracy; mock or stub the DB interactions (or factor out the SQL/result-parsing into a testable function) so you can inject rows like Vec<(chrono::DateTime<Utc>, i64, bigdecimal::BigDecimal)> and verify TokenChartPoint outputs (bucket strings, transfer_count, volume) for cases: no rows, decimals > 18 path (triggering the parse "1e{decimals}" branch), and known sums to assert correct division using the divisor logic in get_token_chart. Ensure tests live in the same file scope so they can access normalize_address, WindowQuery, TokenChartPoint and related helpers.frontend/src/pages/StatusPage.tsx (1)
115-144: Don’t present the 14d daily chart as window-controlled.The section-level toggle reads as if it drives all four panels, but the first card is hard-coded to “Daily Transactions (14d)”. Moving that card outside the toggle-controlled group, or labeling only the lower three charts as windowed, would avoid a control/UI mismatch here.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/StatusPage.tsx` around lines 115 - 144, The "Daily Transactions (14d)" ChartCard is inside the window-controlled grid but its title is hard-coded to 14d, causing a UI/control mismatch; either move the ChartCard with title "Daily Transactions (14d)" out of the div that uses WindowToggle (the grid that includes WindowToggle/state window and setWindow) so it's not affected by the toggle, or make the card respect the window state by changing its title and data to derive the period from the window variable (replace the hard-coded "Daily Transactions (14d)" title and any fixed dataKey assumptions), ensuring references to WindowToggle, window, setWindow, and ChartCard are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/pages/StatusPage.tsx`:
- Around line 121-243: The ChartCard instances only accept loading and show
blank charts on fetch failure; add an error prop to ChartCard and thread
per-chart error state into each usage (e.g., pass dailyTxsError to the Daily
Transactions ChartCard, blocksChartError to the Avg Gas Used / Transactions
cards, and gasPriceError to the Avg Gas Price card) and update ChartCard to
render inline feedback when error is present and loading is false; ensure the
data-rendering components (BarChart/AreaChart/LineChart) are shown only when no
error and not loading, and surface the error.message (or a friendly message)
inside the ChartCard fallback area so each panel clearly indicates fetch
failures.
- Around line 266-297: ChartCard and WindowToggle need ARIA hooks: in
WindowToggle, set each toggle button to type="button" and add
aria-pressed={value === w} (referencing WINDOWS and the prop ChartWindow) so
assistive tech knows the active range; in ChartCard, when loading is true, mark
the chart container as aria-busy="true" and ensure the overlay includes a
readable status (e.g., a visually-hidden or role="status" text node like
"Loading chart data") so screen readers announce progress; update the overlay
div and the root ChartCard container to include these attributes while keeping
the same visual structure.
---
Nitpick comments:
In `@backend/crates/atlas-server/src/api/handlers/tokens.rs`:
- Around line 294-370: Add a #[cfg(test)] mod tests with unit tests that
directly exercise the get_token_chart logic (call get_token_chart or extractable
helper logic) to cover empty result sets, tokens with decimals > 18, and volume
conversion/bucketing accuracy; mock or stub the DB interactions (or factor out
the SQL/result-parsing into a testable function) so you can inject rows like
Vec<(chrono::DateTime<Utc>, i64, bigdecimal::BigDecimal)> and verify
TokenChartPoint outputs (bucket strings, transfer_count, volume) for cases: no
rows, decimals > 18 path (triggering the parse "1e{decimals}" branch), and known
sums to assert correct division using the divisor logic in get_token_chart.
Ensure tests live in the same file scope so they can access normalize_address,
WindowQuery, TokenChartPoint and related helpers.
In `@frontend/src/pages/StatusPage.tsx`:
- Around line 115-144: The "Daily Transactions (14d)" ChartCard is inside the
window-controlled grid but its title is hard-coded to 14d, causing a UI/control
mismatch; either move the ChartCard with title "Daily Transactions (14d)" out of
the div that uses WindowToggle (the grid that includes WindowToggle/state window
and setWindow) so it's not affected by the toggle, or make the card respect the
window state by changing its title and data to derive the period from the window
variable (replace the hard-coded "Daily Transactions (14d)" title and any fixed
dataKey assumptions), ensuring references to WindowToggle, window, setWindow,
and ChartCard are updated accordingly.
In `@frontend/src/pages/TokenDetailPage.tsx`:
- Around line 64-72: Update the BUCKET_MS mapping to ensure it remains in sync
with the backend Window::bucket_secs() values and add a brief comment above
BUCKET_MS referencing the backend source/commit (and the ChartWindow type) so
maintainers know to update both places when backend bucket_secs changes; verify
the numeric values for each key ('1h','6h','24h','7d','1m','6m','1y') match the
backend seconds×1000 and include the reference to Window::bucket_secs() in the
comment.
- Around line 35-48: The function formatBucketTick contains redundant branches
for '1m', '6m', '1y', and '7d' that all return the same toLocaleDateString
format; simplify by collapsing them into a single condition (e.g., if
(['1m','6m','1y','7d'].includes(window)) return d.toLocaleDateString(...)) and
keep the NaN check and the fallback to toLocaleTimeString; update the function
formatBucketTick (and the ChartWindow type usage) accordingly so behavior is
unchanged but branches are consolidated.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0f3ff683-ed35-4040-9646-18a169443c40
📒 Files selected for processing (8)
backend/crates/atlas-server/src/api/handlers/stats.rsbackend/crates/atlas-server/src/api/handlers/tokens.rsbackend/crates/atlas-server/tests/integration/status.rsbackend/crates/atlas-server/tests/integration/tokens.rsfrontend/src/hooks/useChartData.tsfrontend/src/hooks/useTokenChart.tsfrontend/src/pages/StatusPage.tsxfrontend/src/pages/TokenDetailPage.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- frontend/src/hooks/useTokenChart.ts
- frontend/src/hooks/useChartData.ts
- backend/crates/atlas-server/src/api/handlers/stats.rs
| <ChartCard title="Daily Transactions (14d)" loading={dailyTxsLoading}> | ||
| <ResponsiveContainer width="100%" height={200}> | ||
| <BarChart data={dailyTxs} margin={{ top: 4, right: 30, left: 0, bottom: 0 }}> | ||
| <XAxis | ||
| dataKey="day" | ||
| tick={{ fill: CHART_AXIS_TEXT, fontSize: 11 }} | ||
| tickFormatter={formatDayLabel} | ||
| interval="preserveStartEnd" | ||
| /> | ||
| <YAxis | ||
| tick={{ fill: CHART_AXIS_TEXT, fontSize: 11 }} | ||
| tickFormatter={formatCompact} | ||
| width={40} | ||
| /> | ||
| <Tooltip | ||
| contentStyle={{ background: CHART_TOOLTIP_BG, border: `1px solid ${CHART_GRID}`, borderRadius: 8 }} | ||
| labelStyle={{ color: CHART_AXIS_TEXT }} | ||
| itemStyle={{ color: '#f8fafc' }} | ||
| formatter={(v: unknown) => [formatCompact(v as number), 'Transactions']} | ||
| /> | ||
| <Bar dataKey="tx_count" fill={CHART_ACCENT} radius={[2, 2, 0, 0]} isAnimationActive={false} /> | ||
| </BarChart> | ||
| </ResponsiveContainer> | ||
| </ChartCard> | ||
|
|
||
| <ChartCard title="Avg Gas Used per Block" loading={blocksChartLoading}> | ||
| <ResponsiveContainer width="100%" height={200}> | ||
| <AreaChart data={blocksChart} margin={{ top: 4, right: 30, left: 0, bottom: 0 }}> | ||
| <defs> | ||
| <linearGradient id="gasGradient" x1="0" y1="0" x2="0" y2="1"> | ||
| <stop offset="5%" stopColor={CHART_ACCENT} stopOpacity={0.3} /> | ||
| <stop offset="95%" stopColor={CHART_ACCENT} stopOpacity={0} /> | ||
| </linearGradient> | ||
| </defs> | ||
| <XAxis | ||
| dataKey="bucket" | ||
| tick={{ fill: CHART_AXIS_TEXT, fontSize: 11 }} | ||
| tickFormatter={(v: string) => formatBucketTick(v, window)} | ||
| interval="preserveStartEnd" | ||
| /> | ||
| <YAxis | ||
| tick={{ fill: CHART_AXIS_TEXT, fontSize: 11 }} | ||
| tickFormatter={formatCompact} | ||
| width={40} | ||
| /> | ||
| <Tooltip | ||
| contentStyle={{ background: CHART_TOOLTIP_BG, border: `1px solid ${CHART_GRID}`, borderRadius: 8 }} | ||
| labelStyle={{ color: CHART_AXIS_TEXT }} | ||
| itemStyle={{ color: '#f8fafc' }} | ||
| formatter={(v: unknown) => [formatCompact(v as number), 'Avg Gas Used']} | ||
| labelFormatter={(v) => formatBucketTooltip(v, window)} | ||
| /> | ||
| <Area | ||
| type="linear" | ||
| dataKey="avg_gas_used" | ||
| stroke={CHART_ACCENT} | ||
| fill="url(#gasGradient)" | ||
| strokeWidth={2} | ||
| dot={false} | ||
| isAnimationActive={false} | ||
| /> | ||
| </AreaChart> | ||
| </ResponsiveContainer> | ||
| </ChartCard> | ||
|
|
||
| <ChartCard title="Transactions" loading={blocksChartLoading}> | ||
| <ResponsiveContainer width="100%" height={200}> | ||
| <BarChart data={blocksChart} margin={{ top: 4, right: 30, left: 0, bottom: 0 }}> | ||
| <XAxis | ||
| dataKey="bucket" | ||
| tick={{ fill: CHART_AXIS_TEXT, fontSize: 11 }} | ||
| tickFormatter={(v: string) => formatBucketTick(v, window)} | ||
| interval="preserveStartEnd" | ||
| /> | ||
| <YAxis | ||
| tick={{ fill: CHART_AXIS_TEXT, fontSize: 11 }} | ||
| tickFormatter={formatCompact} | ||
| width={40} | ||
| /> | ||
| <Tooltip | ||
| contentStyle={{ background: CHART_TOOLTIP_BG, border: `1px solid ${CHART_GRID}`, borderRadius: 8 }} | ||
| labelStyle={{ color: CHART_AXIS_TEXT }} | ||
| itemStyle={{ color: '#f8fafc' }} | ||
| formatter={(v: unknown) => [formatCompact(v as number), 'Transactions']} | ||
| labelFormatter={(v) => formatBucketTooltip(v, window)} | ||
| /> | ||
| <Bar dataKey="tx_count" fill={CHART_ACCENT} radius={[2, 2, 0, 0]} isAnimationActive={false} /> | ||
| </BarChart> | ||
| </ResponsiveContainer> | ||
| </ChartCard> | ||
|
|
||
| <ChartCard title="Avg Gas Price" loading={gasPriceLoading}> | ||
| <ResponsiveContainer width="100%" height={200}> | ||
| <LineChart data={gasPriceChart} margin={{ top: 4, right: 30, left: 0, bottom: 0 }}> | ||
| <XAxis | ||
| dataKey="bucket" | ||
| tick={{ fill: CHART_AXIS_TEXT, fontSize: 11 }} | ||
| tickFormatter={(v: string) => formatBucketTick(v, window)} | ||
| interval="preserveStartEnd" | ||
| /> | ||
| <YAxis | ||
| tick={{ fill: CHART_AXIS_TEXT, fontSize: 11 }} | ||
| tickFormatter={formatGwei} | ||
| width={52} | ||
| /> | ||
| <Tooltip | ||
| contentStyle={{ background: CHART_TOOLTIP_BG, border: `1px solid ${CHART_GRID}`, borderRadius: 8 }} | ||
| labelStyle={{ color: CHART_AXIS_TEXT }} | ||
| itemStyle={{ color: '#f8fafc' }} | ||
| formatter={(v: unknown) => [formatGwei(v as number), 'Avg Gas Price']} | ||
| labelFormatter={(v) => formatBucketTooltip(v, window)} | ||
| /> | ||
| <Line | ||
| type="linear" | ||
| dataKey="avg_gas_price" | ||
| stroke={CHART_ACCENT} | ||
| strokeWidth={2} | ||
| dot={false} | ||
| isAnimationActive={false} | ||
| /> | ||
| </LineChart> | ||
| </ResponsiveContainer> | ||
| </ChartCard> |
There was a problem hiding this comment.
Add an explicit error state for each chart card.
These panels only react to loading. If one of the new chart requests fails on first load, the page has no way to distinguish “fetch failed” from “no activity”, so the feature degrades into a blank chart. Please thread an error prop through ChartCard and render inline feedback per panel.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/StatusPage.tsx` around lines 121 - 243, The ChartCard
instances only accept loading and show blank charts on fetch failure; add an
error prop to ChartCard and thread per-chart error state into each usage (e.g.,
pass dailyTxsError to the Daily Transactions ChartCard, blocksChartError to the
Avg Gas Used / Transactions cards, and gasPriceError to the Avg Gas Price card)
and update ChartCard to render inline feedback when error is present and loading
is false; ensure the data-rendering components (BarChart/AreaChart/LineChart)
are shown only when no error and not loading, and surface the error.message (or
a friendly message) inside the ChartCard fallback area so each panel clearly
indicates fetch failures.
| function ChartCard({ title, children, loading }: { title: string; children: React.ReactNode; loading?: boolean }) { | ||
| return ( | ||
| <div className="bg-dark-700/60 border border-dark-600 rounded-xl p-4 relative"> | ||
| <p className="text-fg-subtle text-xs uppercase tracking-wide mb-3">{title}</p> | ||
| {children} | ||
| {loading && ( | ||
| <div className="absolute inset-0 rounded-xl bg-dark-900/60 flex items-center justify-center"> | ||
| <div className="w-5 h-5 border-2 border-accent-primary border-t-transparent rounded-full animate-spin" /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function WindowToggle({ value, onChange }: { value: ChartWindow; onChange: (w: ChartWindow) => void }) { | ||
| return ( | ||
| <div className="flex gap-1 bg-dark-700/60 border border-dark-600 rounded-lg p-1"> | ||
| {WINDOWS.map(({ label, value: w }) => ( | ||
| <button | ||
| key={w} | ||
| onClick={() => onChange(w)} | ||
| className={`px-3 py-1 text-xs rounded-md transition-colors ${ | ||
| value === w | ||
| ? 'bg-accent-primary text-white' | ||
| : 'text-fg-subtle hover:text-fg' | ||
| }`} | ||
| > | ||
| {label} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
Expose the new chart controls’ state to assistive tech.
WindowToggle never announces which range is active, and the ChartCard spinner overlay is purely visual. Add type="button"/aria-pressed on the toggle buttons plus aria-busy and a status label on the loading overlay so the charts remain usable with a screen reader.
♿ Small accessibility patch
function ChartCard({ title, children, loading }: { title: string; children: React.ReactNode; loading?: boolean }) {
return (
- <div className="bg-dark-700/60 border border-dark-600 rounded-xl p-4 relative">
+ <div
+ className="bg-dark-700/60 border border-dark-600 rounded-xl p-4 relative"
+ aria-busy={loading}
+ >
<p className="text-fg-subtle text-xs uppercase tracking-wide mb-3">{title}</p>
{children}
{loading && (
- <div className="absolute inset-0 rounded-xl bg-dark-900/60 flex items-center justify-center">
- <div className="w-5 h-5 border-2 border-accent-primary border-t-transparent rounded-full animate-spin" />
+ <div
+ className="absolute inset-0 rounded-xl bg-dark-900/60 flex items-center justify-center"
+ role="status"
+ aria-live="polite"
+ >
+ <span className="sr-only">Loading {title}</span>
+ <div
+ aria-hidden="true"
+ className="w-5 h-5 border-2 border-accent-primary border-t-transparent rounded-full animate-spin"
+ />
</div>
)}
</div>
);
}
function WindowToggle({ value, onChange }: { value: ChartWindow; onChange: (w: ChartWindow) => void }) {
return (
<div className="flex gap-1 bg-dark-700/60 border border-dark-600 rounded-lg p-1">
{WINDOWS.map(({ label, value: w }) => (
<button
key={w}
+ type="button"
+ aria-pressed={value === w}
onClick={() => onChange(w)}
className={`px-3 py-1 text-xs rounded-md transition-colors ${
value === w🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/StatusPage.tsx` around lines 266 - 297, ChartCard and
WindowToggle need ARIA hooks: in WindowToggle, set each toggle button to
type="button" and add aria-pressed={value === w} (referencing WINDOWS and the
prop ChartWindow) so assistive tech knows the active range; in ChartCard, when
loading is true, mark the chart container as aria-busy="true" and ensure the
overlay includes a readable status (e.g., a visually-hidden or role="status"
text node like "Loading chart data") so screen readers announce progress; update
the overlay div and the root ChartCard container to include these attributes
while keeping the same visual structure.
Summary
Adds interactive activity charts to the Status page and Token detail page. Charts are bucketed by time window (1H / 6H / 24H / 7D / 1M), load independently per chart, and show exact time ranges on hover.
Status page — daily transactions (fixed 14d), avg gas used, transaction count, avg gas price.
Token detail page — transfer count and volume.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Chores
Tests