Skip to content

feat: add DA inclusion status tracking from ev-node#24

Open
pthmas wants to merge 23 commits intomainfrom
pierrick/da-inclusion
Open

feat: add DA inclusion status tracking from ev-node#24
pthmas wants to merge 23 commits intomainfrom
pierrick/da-inclusion

Conversation

@pthmas
Copy link
Collaborator

@pthmas pthmas commented Mar 15, 2026

Summary

Adds Data Availability (DA) inclusion tracking to Atlas by querying ev-node's Connect RPC StoreService for the Celestia inclusion heights of each block's header and data.

Backend

  • ev-node client (evnode.rs): Connect RPC client with protobuf/JSON mode detection, 2s timeout, and fast-fail retries
  • DA worker (da_worker.rs): Background two-phase loop (backfill + update-pending) with budget-based scheduling and configurable concurrency
  • Database: Adds block_da_status (block_number, header_da_height, data_da_height, updated_at)
  • API: list_blocks now includes DA status, /api/status exposes features.da_tracking, and /api/events multiplexes new_block + da_batch SSE events
  • Config: EVNODE_URL enables DA tracking, DA_WORKER_CONCURRENCY controls DA worker parallelism

Frontend

  • Blocks list: DA status column with pending/included visual states and pulse animation on live updates
  • Block detail: Header DA and Data DA fields showing Celestia height or Pending
  • Feature gating: DA UI is shown only when features.da_tracking is enabled
  • Live updates: SSE da_batch events apply in-page DA overrides for visible blocks

Summary by CodeRabbit

  • New Features

    • Optional Data Availability (DA) tracking: background DA worker, API feature flag, SSE events (da_batch, da_resync), and DA status shown in block list and block detail.
    • Frontend: real-time DA subscription hooks, conditional DA UI (column, indicators, pending state), and pulse animation for DA updates.
    • Status API: exposes chain feature flags (da_tracking).
  • Documentation

    • Updated environment example, docker-compose, and docs with DA-related configuration and env var guidance.

pthmas added 4 commits March 15, 2026 16:55
Add L2 Data Availability (DA) inclusion tracking by querying ev-node's
Connect RPC StoreService for Celestia DA heights per block.

- New block_da_status table with background DA worker (backfill + retry)
- ev-node client with auto-detecting proto/JSON Connect RPC modes
- API returns da_status on block responses, features flag on /status
- Frontend shows DA rows on block detail when da_tracking is enabled
- Configurable via EVNODE_URL and DA_WORKER_CONCURRENCY env vars

Closes #4
Protobuf mode was silently decoding DA heights as zeros.
JSON mode is universally supported and verified working.
- Add DA SSE: pg_notify from DA worker -> API broadcast -> SSE da_batch
  events pushed to connected frontends for real-time DA dot updates
- DA worker: split budget between backfill (priority) and pending phases,
  skip sleep when work available, process newest blocks first in both phases
- ev-node client: reduce timeout 10s->2s, retries 10->3 with ms-level backoff
- Bump DA_WORKER_CONCURRENCY default 10->50 for higher throughput
- Frontend: DA dot pulse animation on SSE updates, remove gray "awaiting
  check" state (show yellow pending instead), batch SSE for efficiency
- BlockDetailPage: simplified DA display with live SSE override
@coderabbitai
Copy link

coderabbitai bot commented Mar 15, 2026

📝 Walkthrough

Walkthrough

Adds optional Data Availability (DA) tracking: new DA worker, ev-node HTTP client, DB DA status type, config flags and env examples, SSE da_batch/da_resync events, API feature flag and BlockResponse changes, frontend feature hook and SSE subscriptions. All DA behavior is gated by ENABLE_DA_TRACKING/EVNODE_URL.

Changes

Cohort / File(s) Summary
Config & Env
\.env.example, docker-compose.yml, backend/crates/atlas-server/src/config.rs
Adds DA-related env vars and parsing: ENABLE_DA_TRACKING, EVNODE_URL, DA_RPC_REQUESTS_PER_SECOND, DA_WORKER_CONCURRENCY; validation and defaults.
DB & Shared Types
backend/crates/atlas-common/src/types.rs, backend/migrations/20240108000001_block_da_status.sql, frontend/src/types/index.ts
New BlockDaStatus type (backend + frontend), migration comment updated, Block gains optional da_status, and ChainFeatures added.
DA Worker & Ev-node client
backend/crates/atlas-server/src/indexer/da_worker.rs, backend/crates/atlas-server/src/indexer/evnode.rs, backend/crates/atlas-server/src/indexer/mod.rs
Implements DaWorker (backfill + pending update phases, rate-limited, concurrency), EvnodeClient (GetBlock RPC with retries), and re-exports DaSseUpdate/DaWorker.
Server wiring / AppState / main
backend/crates/atlas-server/src/main.rs, backend/crates/atlas-server/src/api/mod.rs, backend/crates/atlas-server/src/config.rs
Adds da_events_tx broadcast channel and da_tracking_enabled to AppState, creates DA pool, conditionally starts DaWorker, and passes DA channel into server state.
API handlers & SSE
backend/crates/atlas-server/src/api/handlers/blocks.rs, backend/crates/atlas-server/src/api/handlers/sse.rs, backend/crates/atlas-server/src/api/handlers/status.rs
Blocks endpoints return BlockResponse including optional BlockDaStatus; SSE emits da_batch and da_resync events and consumes DA broadcasts; status endpoint includes features.da_tracking.
Frontend hooks & context
frontend/src/hooks/useBlockSSE.ts, frontend/src/hooks/useFeatures.ts, frontend/src/hooks/index.ts, frontend/src/context/BlockStatsContext.tsx
Adds useFeatures hook, DA SSE types, subscribeDa/subscribeDaResync APIs, SSE handling for DA batches/resync, and context defaults.
Frontend UI & styles
frontend/src/pages/BlockDetailPage.tsx, frontend/src/pages/BlocksPage.tsx, frontend/src/components/Layout.tsx, frontend/src/api/status.ts, frontend/src/index.css
Feature-flagged DA columns and detail rows, per-row DA indicator and flash animation, updated height/status types, and layout/context wiring.
Docs / Notes
CLAUDE.md
Documentation updated describing new AppState fields, DA worker, SSE events, API status changes, and env vars.

Sequence Diagram(s)

sequenceDiagram
    participant Worker as DA Worker
    participant DB as Database (block_da_status)
    participant EvNode as ev-node (StoreService.GetBlock)
    participant Broadcast as In‑proc Channel (da_events_tx)

    loop DA worker cycle
        Worker->>DB: Query blocks needing DA (backfill/pending)
        alt Backfill work exists
            Worker->>EvNode: GetBlock(height) (rate‑limited)
            EvNode-->>Worker: (header_da_height, data_da_height)
            Worker->>DB: Insert/Update block_da_status
            Worker->>Broadcast: Emit Vec<DaSseUpdate>
        else Pending updates exist
            Worker->>EvNode: GetBlock(height) (remaining budget)
            EvNode-->>Worker: (header_da_height, data_da_height)
            Worker->>DB: Update block_da_status
            Worker->>Broadcast: Emit Vec<DaSseUpdate>
        else No work
            Worker->>Worker: Sleep briefly
        end
    end
Loading
sequenceDiagram
    participant Browser as Client (UI)
    participant SSE as Server SSE (/api/events)
    participant Hook as useBlockSSE
    participant Context as BlockStatsContext
    participant UI as Blocks/BlockDetail

    Browser->>SSE: Subscribe /api/events
    SSE->>Browser: Event: da_batch / da_resync
    Browser->>Hook: Hook receives da_batch/da_resync
    Hook->>Context: Invoke subscribeDa/subscribeDaResync callbacks
    Context->>UI: Provide DA updates via context
    UI->>UI: Re-render and animate DA status change
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • tac0turtle

Poem

🐰 Hoppy whiskers twitch with glee,

DA heights now hop onto my SSE,
Celestia pings and updates spring,
Ev-node hums — the statuses sing,
Atlas hops brighter — track and see!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add DA inclusion status tracking from ev-node' clearly and specifically summarizes the main change: adding Data Availability inclusion tracking functionality by integrating with ev-node.

✏️ 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 pierrick/da-inclusion
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@pthmas pthmas changed the title feat: add live DA status SSE and optimize DA worker feat: add DA inclusion status tracking from ev-node Mar 15, 2026
@pthmas pthmas marked this pull request as draft March 15, 2026 20:53
pthmas added 14 commits March 15, 2026 22:01
Remove protobuf support (prost dependency) from the ev-node client.
ev-node's Connect RPC supports both JSON and protobuf natively, so the
simpler JSON-only path is sufficient. Also removes unused da_to_event.
Remove new_block_event_contains_all_block_fields and
da_update_event_contains_all_fields — they only assert that
#[derive(Serialize)] includes struct fields, adding no real value.
These only verified that #[derive(Serialize)] works on plain structs.
The evnode tests are kept since they cover custom deserializer logic.
- Run cargo fmt on all backend crates
- Fix react-hooks/set-state-in-effect errors by adding subscribeDa
  callback pattern to SSE hook (setState in subscription callbacks
  is allowed, synchronous setState in effect bodies is not)
- Fix react-hooks/refs error by keeping daOverrides/daHighlight as
  state instead of refs
- Fix react-hooks/exhaustive-deps by using drainOneRef indirection
- Fix ref cleanup warning by capturing daHighlightTimeoutsRef.current
  in a local variable before the cleanup function
- Derive daOverride in BlockDetailPage via useMemo instead of
  useState + useEffect
# Conflicts:
#	.env.example
#	backend/crates/atlas-server/src/api/handlers/sse.rs
#	backend/crates/atlas-server/src/api/handlers/status.rs
#	backend/crates/atlas-server/src/api/mod.rs
#	backend/crates/atlas-server/src/main.rs
#	frontend/src/api/status.ts
#	frontend/src/components/Layout.tsx
#	frontend/src/hooks/useBlockSSE.ts
#	frontend/src/hooks/useLatestBlockHeight.ts
@pthmas pthmas marked this pull request as ready for review March 18, 2026 15:05
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: 12

Caution

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

⚠️ Outside diff range comments (1)
backend/crates/atlas-server/src/api/handlers/blocks.rs (1)

112-127: ⚠️ Potential issue | 🟠 Major

Replace OFFSET pagination for block transactions.

Deep pages on a busy block will make PostgreSQL scan and discard all earlier rows on every request. Use a cursor on block_index instead of LIMIT/OFFSET so this endpoint stays flat-cost as transaction counts grow.

As per coding guidelines, "Never use OFFSET for pagination on large tables — use keyset/cursor pagination instead."

🤖 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/blocks.rs` around lines 112 -
127, The current transactions query uses LIMIT/OFFSET (transactions query block)
which causes expensive scans on deep pages; change it to keyset pagination on
block_index by adding an optional cursor parameter and replacing "LIMIT $2
OFFSET $3" with a WHERE clause like "AND block_index > $3 ORDER BY block_index
ASC LIMIT $2". Update the query binds to
.bind(number).bind(pagination.limit()).bind(pagination.cursor()) (or 0/null when
no cursor) and adjust the request/handler signature to accept/parse the cursor
from the client; ensure the cursor type matches block_index (e.g., i32/i64) and
update any callers/tests that used pagination.offset().
🧹 Nitpick comments (2)
backend/crates/atlas-server/src/indexer/da_worker.rs (1)

69-274: Please add in-file tests for the worker phases.

This new file owns selection order, budget splitting, DB upserts, and SSE emission, but there is no #[cfg(test)] coverage. A few unit tests around backfill vs pending selection and "unchanged probe does not emit" would make this much safer to evolve.

As per coding guidelines, "Add unit tests for new logic in a #[cfg(test)] mod tests block in the same file, and run with cargo test --workspace."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/crates/atlas-server/src/indexer/da_worker.rs` around lines 69 - 274,
Add in-file unit tests under a #[cfg(test)] mod tests block in this file that
exercise DaWorker's phases: write tests that call backfill_new_blocks and
update_pending_blocks (and indirectly run) using a test PgPool (sqlite or a test
Postgres container/mock), mocking EvnodeClient::get_da_status to control
returned (header_da, data_da) values, and asserting (1) selection order and
budget splitting (backfill_new_blocks consumes budget before
update_pending_blocks when both exist via DaWorker::run or by calling both
manually), (2) DB upserts/updates occur (verify block_da_status rows reflect
inserted/updated heights), and (3) notify_da_updates only emits when values
change (mock or capture da_events_tx broadcast to assert no event when probe
returns unchanged values). Target the functions/fields DaWorker::new,
backfill_new_blocks, update_pending_blocks, notify_da_updates, and DaWorker::run
to locate where to hook mocks and assertions.
backend/crates/atlas-server/src/api/handlers/blocks.rs (1)

12-20: Add handler tests for the new da_status API surface.

BlockResponse, the page-level DA join, and the single-block DA lookup are all new behavior, but this file still has no #[cfg(test)] coverage. A couple of in-file tests for da_status = null vs populated would catch schema regressions quickly.

As per coding guidelines, "Add unit tests for new logic in a #[cfg(test)] mod tests block in the same file, and run with cargo test --workspace."

Also applies to: 22-105

🤖 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/blocks.rs` around lines 12 - 20,
Add a new #[cfg(test)] mod tests block in the same file with unit tests that
cover the new da_status API surface: create a test that serializes a
BlockResponse where da_status is None (null in JSON) and another where da_status
is Some(BlockDaStatus) to assert the JSON schema and values; additionally add
in-file tests that exercise the page-level DA join and the single-block DA
lookup code paths (the handlers that produce BlockResponse) to verify da_status
is null when no DA data and populated when DA data exists, and run the tests
with cargo test --workspace.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Around line 34-35: The example EVNODE_URL in the .env.example currently shows
localhost:7331 which is misleading for Docker because containers resolve
localhost to themselves; update the EVNODE_URL example to use a placeholder that
indicates a host reachable from the container (e.g., REPLACE_WITH_EVNODE_HOST or
a Docker network alias like ev-node:7331) and adjust the comment to explicitly
state "must be reachable from the atlas-server container / use a Docker network
alias" so users do not copy localhost into their compose env.

In `@backend/crates/atlas-server/src/api/handlers/sse.rs`:
- Around line 108-118: The Lagged(_) branch for da_rx.recv() currently drops
missed DA updates silently; change it to either terminate the SSE stream or emit
an explicit resync event so the frontend can refetch DA state: locate the match
on result from da_rx.recv() and replace the
Err(broadcast::error::RecvError::Lagged(_)) arm with logic that (a) yields a
special SSE resync event (use the same event type path as produced by
da_batch_to_event or a clearly named variant like "ResyncRequired") or (b)
breaks/returns to close the stream so the frontend reconnects and refetches;
ensure the change uses the existing da_batch_to_event/event-yielding flow and
the broadcast::error::RecvError::Lagged(_) symbol for accurate placement.

In `@backend/crates/atlas-server/src/config.rs`:
- Around line 55-71: The code currently only enables DA tracking when
ENABLE_DA_TRACKING is true; change it to enable tracking when either
ENABLE_DA_TRACKING parses true or a non-empty EVNODE_URL is present. Keep the
existing raw_evnode_url parsing (raw_evnode_url), then compute
da_tracking_enabled = parsed ENABLE_DA_TRACKING || raw_evnode_url.is_some();
finally set evnode_url = if da_tracking_enabled { raw_evnode_url.ok_or_else(||
anyhow::anyhow!("EVNODE_URL must be set when DA tracking is enabled"))? } else {
None }; ensure you still return an error if da_tracking_enabled is true but
raw_evnode_url is missing (so the previous validation behavior is preserved).

In `@backend/crates/atlas-server/src/indexer/da_worker.rs`:
- Around line 226-270: The current update_pending_blocks() always rewrites
updated_at and emits DaSseUpdate even when heights haven't changed; modify the
SQL UPDATE in that async map so it only updates on real changes (e.g., add a
WHERE clause comparing existing header_da_height/data_da_height to the new
values using IS DISTINCT FROM or equivalent) and then inspect the execute()
result's rows_affected(); only construct/push a DaSseUpdate and call
notify_da_updates(&updates) for rows_affected() > 0 (or otherwise skip emitting
when rows_affected() == 0). Ensure this logic is applied where
sqlx::query(...).execute(pool).await is called inside update_pending_blocks() so
no unchanged 0/0 probes rewrite updated_at or broadcast identical events.

In `@backend/crates/atlas-server/src/indexer/evnode.rs`:
- Around line 91-119: The retry branch currently always logs "Retrying" and
sleeps even on the final attempt; change the Err(e) handling in the retry loop
(the code that calls self.do_request) to detect the terminal attempt (when
attempt + 1 == MAX_RETRIES) and, in that case, set last_error = Some(e) and
break/exit the loop without logging the retry message or calling
tokio::time::sleep; for non-terminal attempts keep the existing behavior using
RETRY_DELAYS_MS, logging, and sleeping. Ensure the bail afterwards still uses
last_error.unwrap() as before.

In `@backend/crates/atlas-server/src/main.rs`:
- Around line 77-100: The DA worker is currently spawned once and will silently
stop if DaWorker::run() returns; wrap its execution in the same supervised retry
pattern used by other background jobs (e.g., run_with_retry) so fatal DB errors
in backfill_new_blocks() or update_pending_blocks() don't permanently disable
tracking. Replace the direct tokio::spawn of da_worker.run().await with a
supervised loop or call to run_with_retry that accepts the async closure running
da_worker.run(), applies exponential backoff, logs retries via
tracing::error/tracing::info including the error, and preserves the original
concurrency and rate-limit parameters; ensure the supervision logic references
DaWorker::new and DaWorker::run() so it restarts the same worker instance or
recreates it on repeated failures.

In `@CLAUDE.md`:
- Around line 80-94: The AppState/SSE documentation is out of sync: update the
doc to reflect that AppState.da_events_tx is now
broadcast::Sender<Vec<DaSseUpdate>> (not Vec<i64>), include the presence of
head_tracker and da_tracking_enabled in the API state, and remove mention of
storing evnode_url in API state; also correct the /api/status description to
state that handlers/status reads from head_tracker first and only falls back to
indexer_state (per handlers/status.rs), and adjust the DA tracking SSE
description to reference DaSseUpdate payloads and the updated flow for DA worker
updates.

In `@frontend/src/components/Layout.tsx`:
- Around line 189-197: Remove latestDaUpdate from the BlockStatsContext provider
value to avoid unnecessary rerenders: in the BlockStatsContext.Provider value
object (where bps, height, latestBlockEvent, latestDaUpdate, sseConnected,
subscribeDa are set), delete the latestDaUpdate/ sse.latestDaUpdate entry so
only subscribeDa is used for DA updates; keep subscribeDa intact since consumers
(BlockDetailPage, BlocksPage) already use it and verify useStats, WelcomePage,
BlockDetailPage, BlocksPage still work after the change.

In `@frontend/src/hooks/useFeatures.ts`:
- Around line 11-24: The useFeatures hook currently calls getStatus() on every
component mount causing duplicate network requests; refactor it to use a single
cached/shared status fetched once at app initialization (e.g., via a top-level
StatusContext or a module-level cache) and have useFeatures read from that
shared source instead of calling getStatus() directly. Specifically, create or
use a StatusProvider that calls getStatus() once on app bootstrap and exposes
status.features (or store the resolved status in a module-scoped variable with a
promise getter), then update useFeatures to read defaultFeatures initially and
subscribe to the shared status (using StatusContext or the cache getter) to
setFeatures when the single fetched status is available; keep the same return
type ChainFeatures and retain defaultFeatures fallback.

In `@frontend/src/index.css`:
- Around line 171-180: The keyframe name and CSS keyword violate stylelint:
rename the `@keyframes` identifier from "daPulse" to kebab-case "da-pulse" and
update the .animate-da-pulse animation declaration to use that new name, and
change the color keyword "currentColor" to lowercase "currentcolor" inside the
keyframes; update any other references to `@keyframes` daPulse to use `@keyframes`
da-pulse so linting passes.

In `@frontend/src/pages/BlocksPage.tsx`:
- Around line 29-30: Effect retains all da_batch entries causing unbounded
growth; update the subscription/effect that manipulates daOverrides (and the
similar logic around lines with setDaOverrides in the 105-129 region) to no-op
when features.da_tracking is false, and when enabled prune the Map to only
entries for the current visible blocks: compute the visible block ID set from
blocks, filter the existing daOverrides Map to those IDs before applying
updates, and ensure any incoming DA update handler only inserts/updates entries
whose block IDs exist in that visible set so old/off-screen block entries are
dropped and not subscribed to.
- Around line 105-145: The pulse never fires because transitionedToIncluded is
filled inside the async setDaOverrides updater and read immediately after;
compute which block numbers transition to included before calling setDaOverrides
(or else perform both state changes inside a single reducer/updater) by
comparing each incoming update to the current snapshot (use
daOverridesRef.current or baseDaIncludedRef for the current status) to populate
transitionedToIncluded, then call setDaOverrides with the new map and afterwards
iterate transitionedToIncluded to setDaHighlight and manage
daHighlightTimeoutsRef; update functions referenced: subscribeDa,
setDaOverrides, transitionedToIncluded, daHighlightTimeoutsRef, setDaHighlight,
baseDaIncludedRef.

---

Outside diff comments:
In `@backend/crates/atlas-server/src/api/handlers/blocks.rs`:
- Around line 112-127: The current transactions query uses LIMIT/OFFSET
(transactions query block) which causes expensive scans on deep pages; change it
to keyset pagination on block_index by adding an optional cursor parameter and
replacing "LIMIT $2 OFFSET $3" with a WHERE clause like "AND block_index > $3
ORDER BY block_index ASC LIMIT $2". Update the query binds to
.bind(number).bind(pagination.limit()).bind(pagination.cursor()) (or 0/null when
no cursor) and adjust the request/handler signature to accept/parse the cursor
from the client; ensure the cursor type matches block_index (e.g., i32/i64) and
update any callers/tests that used pagination.offset().

---

Nitpick comments:
In `@backend/crates/atlas-server/src/api/handlers/blocks.rs`:
- Around line 12-20: Add a new #[cfg(test)] mod tests block in the same file
with unit tests that cover the new da_status API surface: create a test that
serializes a BlockResponse where da_status is None (null in JSON) and another
where da_status is Some(BlockDaStatus) to assert the JSON schema and values;
additionally add in-file tests that exercise the page-level DA join and the
single-block DA lookup code paths (the handlers that produce BlockResponse) to
verify da_status is null when no DA data and populated when DA data exists, and
run the tests with cargo test --workspace.

In `@backend/crates/atlas-server/src/indexer/da_worker.rs`:
- Around line 69-274: Add in-file unit tests under a #[cfg(test)] mod tests
block in this file that exercise DaWorker's phases: write tests that call
backfill_new_blocks and update_pending_blocks (and indirectly run) using a test
PgPool (sqlite or a test Postgres container/mock), mocking
EvnodeClient::get_da_status to control returned (header_da, data_da) values, and
asserting (1) selection order and budget splitting (backfill_new_blocks consumes
budget before update_pending_blocks when both exist via DaWorker::run or by
calling both manually), (2) DB upserts/updates occur (verify block_da_status
rows reflect inserted/updated heights), and (3) notify_da_updates only emits
when values change (mock or capture da_events_tx broadcast to assert no event
when probe returns unchanged values). Target the functions/fields DaWorker::new,
backfill_new_blocks, update_pending_blocks, notify_da_updates, and DaWorker::run
to locate where to hook mocks and assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46341d1b-b5d4-4d9e-b4a2-e8242f9d82a9

📥 Commits

Reviewing files that changed from the base of the PR and between 6cafa1e and 856a617.

📒 Files selected for processing (24)
  • .env.example
  • CLAUDE.md
  • backend/crates/atlas-common/src/types.rs
  • backend/crates/atlas-server/src/api/handlers/blocks.rs
  • backend/crates/atlas-server/src/api/handlers/sse.rs
  • backend/crates/atlas-server/src/api/handlers/status.rs
  • backend/crates/atlas-server/src/api/mod.rs
  • backend/crates/atlas-server/src/config.rs
  • backend/crates/atlas-server/src/indexer/da_worker.rs
  • backend/crates/atlas-server/src/indexer/evnode.rs
  • backend/crates/atlas-server/src/indexer/mod.rs
  • backend/crates/atlas-server/src/main.rs
  • backend/migrations/20240108000001_block_da_status.sql
  • docker-compose.yml
  • frontend/src/api/status.ts
  • frontend/src/components/Layout.tsx
  • frontend/src/context/BlockStatsContext.tsx
  • frontend/src/hooks/index.ts
  • frontend/src/hooks/useBlockSSE.ts
  • frontend/src/hooks/useFeatures.ts
  • frontend/src/index.css
  • frontend/src/pages/BlockDetailPage.tsx
  • frontend/src/pages/BlocksPage.tsx
  • frontend/src/types/index.ts

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: 4

Caution

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

⚠️ Outside diff range comments (1)
frontend/src/hooks/useBlockSSE.ts (1)

137-154: ⚠️ Potential issue | 🟠 Major

Use the lightweight /api/height fallback here.

Line 142 still calls getStatus(), and this change now does it immediately and every 2s whenever SSE is down. Swap this to the typed /api/height client so disconnected tabs don’t keep hitting the heavier /api/status query path. Based on learnings: GET /api/height returns { block_height, indexed_at } for polling every 2s; GET /api/status is 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/hooks/useBlockSSE.ts` around lines 137 - 154, Replace the heavy
getStatus() calls inside startPolling/poll with the lightweight typed
/api/height client (e.g., getHeight) so polling every POLL_INTERVAL_MS hits GET
/api/height instead of GET /api/status; call the height client, read its
block_height (and indexed_at if needed), and apply the same logic that updates
highestSeenRef.current and setHeight when connectedRef.current is false and the
new block_height is greater. Ensure you import/use the existing typed height
client function (getHeight or equivalent) and keep error swallowing and the
existing interval setup unchanged.
🧹 Nitpick comments (1)
frontend/src/pages/BlockDetailPage.tsx (1)

96-108: Consider simplifying the IIFE pattern for conditional DA rows.

The IIFE (() => { ... })() as DetailRow[] works but adds visual noise. A simpler spread would be clearer.

♻️ Optional simplification
-    ...(features.da_tracking ? (() => {
-      const daStatus = currentDaOverride ?? block.da_status;
-      return [
-        {
-          label: 'Header DA',
-          value: formatDaStatus(daStatus?.header_da_height ?? 0),
-        },
-        {
-          label: 'Data DA',
-          value: formatDaStatus(daStatus?.data_da_height ?? 0),
-        },
-      ];
-    })() as DetailRow[] : []),
+    ...(features.da_tracking ? [
+      {
+        label: 'Header DA',
+        value: formatDaStatus((currentDaOverride ?? block.da_status)?.header_da_height ?? 0),
+      },
+      {
+        label: 'Data DA',
+        value: formatDaStatus((currentDaOverride ?? block.da_status)?.data_da_height ?? 0),
+      },
+    ] : []),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/BlockDetailPage.tsx` around lines 96 - 108, The code uses
an unnecessary IIFE when conditionally adding DA rows which adds visual noise;
replace the IIFE expression with a direct conditional array spread: when
features.da_tracking is truthy compute daStatus = currentDaOverride ??
block.da_status and return the two DetailRow objects (using
formatDaStatus(daStatus?.header_da_height ?? 0) and
formatDaStatus(daStatus?.data_da_height ?? 0)) as the array to spread, otherwise
use an empty array; update the expression that currently contains the IIFE
(referencing features.da_tracking, currentDaOverride, block.da_status,
formatDaStatus, and DetailRow) 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 `@backend/crates/atlas-server/src/indexer/da_worker.rs`:
- Around line 89-275: Add a new #[cfg(test)] mod tests block in this file that
adds unit tests covering the DA worker scheduler and SSE logic: specifically
write tests for backfill_new_blocks, update_pending_blocks, notify_da_updates
(and ideally run's loop exit behavior or a single-cycle invocation) using a
mocked sqlx::Pool and a mocked client implementing get_da_status, verify that
backfill inserts, pending updates respect the no-op suppression (rows_affected
== 0), and that DaSseUpdate batching is sent via da_events_tx; reference the
functions backfill_new_blocks, update_pending_blocks, notify_da_updates, run and
the DaSseUpdate struct when locating code to test, and ensure tests run with
cargo test --workspace.
- Around line 70-85: The constructor DaWorker::new must validate that the
incoming concurrency argument is non-zero to avoid creating a zero-capacity
stream used by buffer_unordered; add a check at the start of DaWorker::new that
returns an Err if concurrency == 0 (or convert to a NonZeroUsize/NonZeroU32 and
fail with a clear error message), then store the validated value in the struct
field concurrency (used by buffer_unordered in the backfill and pending flows)
so buffer_unordered(self.concurrency) is never called with 0.

In `@frontend/src/pages/BlocksPage.tsx`:
- Around line 518-530: The status dot is color-only and not accessible; update
the two status <span>s in the features.da_tracking block to include an
accessible label by adding an aria-label (use the existing includedTitle for the
included case and "Pending DA inclusion" for the pending case) and a suitable
role (e.g., role="img" or role="status") so assistive tech can announce the DA
status and heights; keep the existing flash/animate-da-pulse class logic and the
includedTitle computation (references: features.da_tracking, daOverrides,
daHighlight, isDaIncluded, includedTitle).
- Around line 150-166: The DA update handler inside subscribeDa currently skips
updates whose block_number is not in visibleDaBlocksRef.current, which drops
da_batch events for blocks that are still buffered by the SSE logic; change the
check so that you also accept updates whose block_number exists in
pendingSseBlocksRef.current or sseBlocks (whichever is the live buffer ref) and
include them into next (or, if you prefer, place them into a small bounded
staging Map keyed by block_number until the main blocks list advances), updating
daOverridesRef/next accordingly and keeping transitionedToIncluded/changing
logic intact so buffered blocks aren't lost.

---

Outside diff comments:
In `@frontend/src/hooks/useBlockSSE.ts`:
- Around line 137-154: Replace the heavy getStatus() calls inside
startPolling/poll with the lightweight typed /api/height client (e.g.,
getHeight) so polling every POLL_INTERVAL_MS hits GET /api/height instead of GET
/api/status; call the height client, read its block_height (and indexed_at if
needed), and apply the same logic that updates highestSeenRef.current and
setHeight when connectedRef.current is false and the new block_height is
greater. Ensure you import/use the existing typed height client function
(getHeight or equivalent) and keep error swallowing and the existing interval
setup unchanged.

---

Nitpick comments:
In `@frontend/src/pages/BlockDetailPage.tsx`:
- Around line 96-108: The code uses an unnecessary IIFE when conditionally
adding DA rows which adds visual noise; replace the IIFE expression with a
direct conditional array spread: when features.da_tracking is truthy compute
daStatus = currentDaOverride ?? block.da_status and return the two DetailRow
objects (using formatDaStatus(daStatus?.header_da_height ?? 0) and
formatDaStatus(daStatus?.data_da_height ?? 0)) as the array to spread, otherwise
use an empty array; update the expression that currently contains the IIFE
(referencing features.da_tracking, currentDaOverride, block.da_status,
formatDaStatus, and DetailRow) accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 82631fe7-2320-4c43-a499-cf929a3ed108

📥 Commits

Reviewing files that changed from the base of the PR and between 856a617 and 0870e1d.

📒 Files selected for processing (14)
  • .env.example
  • CLAUDE.md
  • backend/crates/atlas-server/src/api/handlers/sse.rs
  • backend/crates/atlas-server/src/config.rs
  • backend/crates/atlas-server/src/indexer/da_worker.rs
  • backend/crates/atlas-server/src/indexer/evnode.rs
  • backend/crates/atlas-server/src/main.rs
  • frontend/src/components/Layout.tsx
  • frontend/src/context/BlockStatsContext.tsx
  • frontend/src/hooks/useBlockSSE.ts
  • frontend/src/hooks/useFeatures.ts
  • frontend/src/index.css
  • frontend/src/pages/BlockDetailPage.tsx
  • frontend/src/pages/BlocksPage.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/index.css
  • .env.example

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.

🧹 Nitpick comments (1)
backend/crates/atlas-server/src/api/handlers/sse.rs (1)

117-135: Resync handling for DA lag is correct.

The Lagged case correctly emits a da_resync_event() allowing the frontend to refetch DA state. The channel capacity at main.rs:76 is 256, sufficient for normal operations. If backfill throughput significantly increases in the future, monitor DA resync frequency and consider increasing capacity if lag events become frequent during peak indexing.

🤖 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/sse.rs` around lines 117 - 135,
No code change required: the RecvError::Lagged arm correctly logs and yields a
da_resync_event to trigger frontend refetch; verify the match handling around
da_rx.recv(), the use of da_batch_to_event(&updates), and the call to
da_resync_event() remain as-is and monitor runtime frequency of Lagged events to
decide later whether to increase the channel capacity configured where the
broadcast channel is created.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@backend/crates/atlas-server/src/api/handlers/sse.rs`:
- Around line 117-135: No code change required: the RecvError::Lagged arm
correctly logs and yields a da_resync_event to trigger frontend refetch; verify
the match handling around da_rx.recv(), the use of da_batch_to_event(&updates),
and the call to da_resync_event() remain as-is and monitor runtime frequency of
Lagged events to decide later whether to increase the channel capacity
configured where the broadcast channel is created.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d9581c14-641f-4a6f-bab8-3a1e4f7ee506

📥 Commits

Reviewing files that changed from the base of the PR and between 0870e1d and d401b25.

📒 Files selected for processing (12)
  • .env.example
  • CLAUDE.md
  • backend/crates/atlas-server/src/api/handlers/sse.rs
  • backend/crates/atlas-server/src/api/handlers/status.rs
  • backend/crates/atlas-server/src/api/mod.rs
  • backend/crates/atlas-server/src/config.rs
  • backend/crates/atlas-server/src/main.rs
  • docker-compose.yml
  • frontend/src/api/status.ts
  • frontend/src/components/Layout.tsx
  • frontend/src/hooks/useBlockSSE.ts
  • frontend/src/hooks/useFeatures.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • backend/crates/atlas-server/src/api/mod.rs
  • frontend/src/hooks/useFeatures.ts
  • frontend/src/api/status.ts
  • .env.example
  • docker-compose.yml
  • frontend/src/components/Layout.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.

♻️ Duplicate comments (1)
frontend/src/pages/BlocksPage.tsx (1)

535-551: ⚠️ Potential issue | 🟡 Minor

Add accessible labels to the DA status indicators.

The DA status dots rely on color alone (green/yellow) and title attributes, which are not reliably announced by screen readers because the spans are not focusable. Add aria-label and an appropriate role so assistive technology can convey the DA status.

♿ Proposed fix for accessibility
                       <td className="table-cell text-center">
                         {included ? (
-                          <span className={`w-2 h-2 rounded-full bg-green-400 inline-block${flash ? ' animate-da-pulse' : ''}`} title={includedTitle} />
+                          <span
+                            role="img"
+                            aria-label={includedTitle}
+                            className={`w-2 h-2 rounded-full bg-green-400 inline-block${flash ? ' animate-da-pulse' : ''}`}
+                            title={includedTitle}
+                          />
                         ) : (
-                          <span className={`w-2 h-2 rounded-full bg-yellow-400 inline-block${flash ? ' animate-da-pulse' : ''}`} title="Pending DA inclusion" />
+                          <span
+                            role="img"
+                            aria-label="Pending DA inclusion"
+                            className={`w-2 h-2 rounded-full bg-yellow-400 inline-block${flash ? ' animate-da-pulse' : ''}`}
+                            title="Pending DA inclusion"
+                          />
                         )}
                       </td>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/BlocksPage.tsx` around lines 535 - 551, The DA status dots
currently rely on color and title only; update the JSX in BlocksPage.tsx (the
render block using features.da_tracking, daOverrides, daHighlight, isDaIncluded,
includedTitle) to add accessibility attributes to the span elements: set an
appropriate role (e.g., role="img" or role="status") and an aria-label that uses
includedTitle for included dots and "Pending DA inclusion" for pending dots;
keep the existing title but ensure the aria-label reflects the same
human-readable text so screen readers announce the DA status.
🧹 Nitpick comments (1)
backend/crates/atlas-server/src/indexer/da_worker.rs (1)

171-208: Consider emitting updates only for newly-included blocks during backfill.

The backfill phase broadcasts DaSseUpdate for every inserted row, including those with 0/0 heights (pending). Since the frontend's da_batch handler applies these as overrides, this causes visible blocks to briefly show "pending" status even if they were already displaying correctly from the initial fetch.

For backfill, consider only emitting updates when the block is actually included (heights > 0), since pending status is the default assumption for new blocks.

♻️ Proposed change to filter backfill notifications
                         Some(DaSseUpdate {
                             block_number,
                             header_da_height: header_da as i64,
                             data_da_height: data_da as i64,
                         })
+                        .filter(|u| u.header_da_height > 0 && u.data_da_height > 0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/crates/atlas-server/src/indexer/da_worker.rs` around lines 171 - 208,
The backfill currently emits a DaSseUpdate for every inserted row (including
pending 0/0 heights); change the logic inside the async map (the closure
handling client.get_da_status and the sqlx insert using INSERT_DA_STATUS_SQL) so
that after a successful insert you only return Some(DaSseUpdate { ... }) when
the DA heights indicate the block is actually included (e.g., header_da > 0 &&
data_da > 0); if the heights are 0 (pending) return None (but still perform the
DB insert and logs), ensuring downstream SSEs are only emitted for
newly-included blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@frontend/src/pages/BlocksPage.tsx`:
- Around line 535-551: The DA status dots currently rely on color and title
only; update the JSX in BlocksPage.tsx (the render block using
features.da_tracking, daOverrides, daHighlight, isDaIncluded, includedTitle) to
add accessibility attributes to the span elements: set an appropriate role
(e.g., role="img" or role="status") and an aria-label that uses includedTitle
for included dots and "Pending DA inclusion" for pending dots; keep the existing
title but ensure the aria-label reflects the same human-readable text so screen
readers announce the DA status.

---

Nitpick comments:
In `@backend/crates/atlas-server/src/indexer/da_worker.rs`:
- Around line 171-208: The backfill currently emits a DaSseUpdate for every
inserted row (including pending 0/0 heights); change the logic inside the async
map (the closure handling client.get_da_status and the sqlx insert using
INSERT_DA_STATUS_SQL) so that after a successful insert you only return
Some(DaSseUpdate { ... }) when the DA heights indicate the block is actually
included (e.g., header_da > 0 && data_da > 0); if the heights are 0 (pending)
return None (but still perform the DB insert and logs), ensuring downstream SSEs
are only emitted for newly-included blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d18ff561-1697-4745-bda5-413166e109b5

📥 Commits

Reviewing files that changed from the base of the PR and between d401b25 and ae1f38c.

📒 Files selected for processing (7)
  • .env.example
  • CLAUDE.md
  • backend/crates/atlas-server/src/api/handlers/sse.rs
  • backend/crates/atlas-server/src/config.rs
  • backend/crates/atlas-server/src/indexer/da_worker.rs
  • backend/crates/atlas-server/src/main.rs
  • frontend/src/pages/BlocksPage.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • .env.example

@pthmas pthmas self-assigned this Mar 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant