Skip to content

feat: status page charts#40

Open
pthmas wants to merge 3 commits intomainfrom
pthmas/status-page-charts
Open

feat: status page charts#40
pthmas wants to merge 3 commits intomainfrom
pthmas/status-page-charts

Conversation

@pthmas
Copy link
Collaborator

@pthmas pthmas commented Mar 25, 2026

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

    • New stats and token chart endpoints powering time-windowed charts (blocks, daily txs, gas price, token transfers)
    • Interactive charts added to Status and Token pages with time-window controls and live polling
    • Client-side helpers and React hooks for fetching and polling chart data
  • Chores

    • Added charting dependency for frontend
  • Tests

    • Integration tests validating bucketed chart responses for non-aligned windows

- 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
@coderabbitai
Copy link

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

Adds 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 (recharts).

Changes

Cohort / File(s) Summary
Backend: New stats handlers & export
backend/crates/atlas-server/src/api/handlers/mod.rs, backend/crates/atlas-server/src/api/handlers/stats.rs, backend/crates/atlas-server/src/api/mod.rs
Exported new stats module, added Window enum and WindowQuery, and implemented three handlers: get_blocks_chart, get_daily_txs, get_gas_price_chart. Wired routes in router.
Backend: Token chart handler
backend/crates/atlas-server/src/api/handlers/tokens.rs
Added TokenChartPoint and get_token_chart handler that buckets erc20_transfers, normalizes by decimals, and returns time-series token volume/counts.
Backend: Integration tests
backend/crates/atlas-server/tests/integration/status.rs, backend/crates/atlas-server/tests/integration/tokens.rs
Added data seeding helpers and tests asserting exact bucket counts for non-aligned 1h windows for stats and token chart endpoints.
Frontend: Charting dependency
frontend/package.json
Added recharts dependency (^3.8.0).
Frontend: Typed API clients
frontend/src/api/chartData.ts
New typed client functions and types (ChartWindow, BlockChartPoint, DailyTxPoint, GasPricePoint, TokenChartPoint) for fetching charts.
Frontend: Hooks & re-export
frontend/src/hooks/useChartData.ts, frontend/src/hooks/useTokenChart.ts, frontend/src/hooks/index.ts
Added useChartData (polling, multi-endpoint state/loading/error) and useTokenChart (address-aware polling) and re-exported useTokenChart.
Frontend: Pages / UI
frontend/src/pages/StatusPage.tsx, frontend/src/pages/TokenDetailPage.tsx
Inserted "Chain Activity" and "Token Activity" sections with window toggles and Recharts visualizations (multiple synchronized charts, tooltips, loading overlays).

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • tac0turtle

Poem

🐰
I hopped through buckets, counted ticks of time,
From Rusty roots to React's bright clime,
Polls that hum and charts that bloom,
Buckets lined up, dispelling gloom,
A carrot-coded graph — oh what a climb!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: status page charts' directly describes the main changes: adding interactive charts to the status page and related pages.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 pthmas/status-page-charts

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 | 🟠 Major

Stop polling /api/status every 5 seconds.

This page already gets live height/indexed_at from BlockStatsContext, so the interval just keeps re-downloading the full status payload and adds avoidable backend load. Fetch /api/status once 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 use generate_series to 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_series to 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 in StatusPage.tsx, and they’re already out of sync (6M/1Y only 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

📥 Commits

Reviewing files that changed from the base of the PR and between ba5b44e and 30ef34c.

⛔ Files ignored due to path filters (1)
  • frontend/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • backend/crates/atlas-server/src/api/handlers/mod.rs
  • backend/crates/atlas-server/src/api/handlers/stats.rs
  • backend/crates/atlas-server/src/api/handlers/tokens.rs
  • backend/crates/atlas-server/src/api/mod.rs
  • frontend/package.json
  • frontend/src/api/chartData.ts
  • frontend/src/hooks/index.ts
  • frontend/src/hooks/useChartData.ts
  • frontend/src/hooks/useTokenChart.ts
  • frontend/src/pages/StatusPage.tsx
  • frontend/src/pages/TokenDetailPage.tsx

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
frontend/src/pages/TokenDetailPage.tsx (2)

64-72: Verify BUCKET_MS values match backend bucket_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 in formatBucketTick.

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 tests block. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 30ef34c and e980170.

📒 Files selected for processing (8)
  • backend/crates/atlas-server/src/api/handlers/stats.rs
  • backend/crates/atlas-server/src/api/handlers/tokens.rs
  • backend/crates/atlas-server/tests/integration/status.rs
  • backend/crates/atlas-server/tests/integration/tokens.rs
  • frontend/src/hooks/useChartData.ts
  • frontend/src/hooks/useTokenChart.ts
  • frontend/src/pages/StatusPage.tsx
  • frontend/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

Comment on lines +121 to +243
<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>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +266 to +297
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>
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

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.

1 participant