Skip to content

feat(ui): node-health surface — indexer self-repair status endpoint + real charts - #151

Merged
arkadianet merged 2 commits into
mainfrom
feat/ui-node-health
Jul 2, 2026
Merged

feat(ui): node-health surface — indexer self-repair status endpoint + real charts#151
arkadianet merged 2 commits into
mainfrom
feat/ui-node-health

Conversation

@arkadianet

@arkadianet arkadianet commented Jul 2, 2026

Copy link
Copy Markdown
Owner

What

Phase 2A of the first-class UI effort: the node's health machinery becomes observable, and the Charts view graduates from session sparklines to real server-history charts.

Indexer health endpoint

  • New defaulted IndexerQuery::health() (ergo-indexer-types) returning the durable self-repair markers from the fix(indexer): degrade-not-halt + self-repair the secondary index (network-wide db-corruption halt) #147 incident machinery — repair_pending, the repair_next_gi rebuild cursor, the repair_skipped honest marker — plus the process-lifetime drift-skip counter and global box/tx totals. IndexerHandle overrides it via a new IndexerStore::health_snapshot() that captures meta + all three markers under one redb read txn (mutually consistent, one txn per dashboard poll; unlike indexed_height, this isn't on the per-request middleware path, so no cached mirror is needed). Best-effort: a degraded store degrades the snapshot to defaults, never errors the surface.
  • New GET /api/v1/indexer/status — always-200 and never status-gated (like indexedHeight, it must answer while syncing/repairing/halted; that's exactly when the operator needs it). /blockchain/indexedHeight stays pinned to its Scala-parity shape; the repair/totals superset lives here. 404 on indexer-less wiring = disabled. utoipa-documented, native openapi snapshot regenerated, pinned by a 6-case endpoint test including the degraded wire shape and the wipe-phase pending=true-with-nextGi-absent distinction.

Overview integration

The Sync-pipeline panel surfaces index health, silent when healthy: a progress bar while a rebuild runs (cursor / total boxes), "queued — wipe phase", a done · N box(es) skipped honest-marker row after a knowingly-incomplete repair, and a red halt row with the reason.

Real charts

New zero-dep chart.js (axis-labelled SVG line/area + bar histogram, pointer readouts; axis text lives in HTML around the stretched SVG so nothing distorts; all data via textContent):

  • Difficulty (last 720 blocks) from /api/v1/difficulty/history — deliberate approximate Number() parse, documented against the string-typed wire contract.
  • Est. hashrate per point (difficulty ÷ target interval, same derivation as the KPI band).
  • Block-interval distribution from the same series' timestamp deltas.
  • Mempool age histogram from /transactions/poolHistogram.

Charts build once and update in place (hover state survives the 4s tick); the series refetches only when the tip advances.

Review & testing

  • codex adversarial pass: no blocking issues; its Medium (4 redb txns per poll → single-txn snapshot) and all 4 Lows (wire-contract test coverage, difficulty-parse documentation, u64 contract note, chart expando hygiene) fixed in this commit.
  • fmt / clippy -D warnings / cargo test --all green; openapi snapshot + runtime-mount + SPA-header tests updated.
  • Charts verified against live mainnet data (real difficulty epoch steps, 2-minute-modal interval distribution, live pool histogram).

Phase 2B (events feed — node-side ring buffer + /api/v1/events + UI feed) follows as a separate PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a new operator indexer status endpoint showing sync state, optional halt reasons, indexed/full heights, repair progress markers, and running totals.
    • Expanded the operator dashboard with real charts, including a mempool histogram and a richer extra-index health section in the sync pipeline.
    • Added a new chart JavaScript asset to support the updated dashboard visuals.
  • Bug Fixes
    • Improved indexer health reporting to provide more complete and consistent repair and drift details, with better handling across degraded and halted scenarios.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 185878bd-04ce-4e5d-ab9e-74bab2235c2a

📥 Commits

Reviewing files that changed from the base of the PR and between bbdc082 and 0bcf9f3.

📒 Files selected for processing (16)
  • ergo-api/src/blockchain.rs
  • ergo-api/src/server.rs
  • ergo-api/src/types.rs
  • ergo-api/src/web.rs
  • ergo-api/tests/fixtures/openapi_native.yaml
  • ergo-api/tests/indexer_status_endpoint.rs
  • ergo-api/tests/wallet_ui_headers.rs
  • ergo-api/web/dashboard.css
  • ergo-api/web/js/api-client.js
  • ergo-api/web/js/chart.js
  • ergo-api/web/js/overview.js
  • ergo-indexer-types/src/lib.rs
  • ergo-indexer-types/src/query.rs
  • ergo-indexer/src/handle.rs
  • ergo-indexer/src/lib.rs
  • ergo-indexer/src/store/mod.rs

📝 Walkthrough

Walkthrough

Adds indexer health snapshots and status plumbing, exposes /api/v1/indexer/status, and updates the dashboard to fetch and render extra-index health plus new chart data.

Changes

Indexer Health Data Layer

Layer / File(s) Summary
Health contract
ergo-indexer-types/src/query.rs, ergo-indexer-types/src/lib.rs
Adds IndexerHealthDto, the default IndexerQuery::health() method, and public re-exports.
Store snapshot
ergo-indexer/src/store/mod.rs, ergo-indexer/src/lib.rs
Adds StoreHealthSnapshot and IndexerStore::health_snapshot() plus related re-exports.
Handle health mapping
ergo-indexer/src/handle.rs
Implements IndexerHandle::health() from store snapshots and fallback cases.

Indexer Status API

Layer / File(s) Summary
API DTOs
ergo-api/src/types.rs
Adds ApiIndexerRepair, ApiIndexerTotals, and ApiIndexerStatus.
Status handler and routing
ergo-api/src/blockchain.rs, ergo-api/src/server.rs, ergo-api/tests/fixtures/openapi_native.yaml
Adds indexer_status_handler, mounts /api/v1/indexer/status, and updates OpenAPI.
Status endpoint tests
ergo-api/tests/indexer_status_endpoint.rs
Adds coverage for syncing, halted, caught-up, degraded, wipe-phase, and unmounted cases.

Dashboard Charts UI

Layer / File(s) Summary
Chart asset and styles
ergo-api/web/js/chart.js, ergo-api/web/dashboard.css, ergo-api/src/web.rs, ergo-api/src/server.rs, ergo-api/tests/wallet_ui_headers.rs
Adds the embedded chart module, chart styling, asset wiring, and header coverage.
Dashboard client endpoints
ergo-api/web/js/api-client.js
Adds indexerStatus and poolHistogram client calls.
Overview charts integration
ergo-api/web/js/overview.js
Fetches indexer status, renders extra-index health, and replaces the charts view with persistent updates.

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

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant ErgoApi
  participant IndexerHandle
  participant IndexerStore

  Browser->>ErgoApi: GET /api/v1/indexer/status
  ErgoApi->>IndexerHandle: status()
  ErgoApi->>IndexerHandle: health()
  IndexerHandle->>IndexerStore: health_snapshot()
  IndexerStore-->>IndexerHandle: StoreHealthSnapshot
  IndexerHandle-->>ErgoApi: IndexerHealthDto
  ErgoApi-->>Browser: ApiIndexerStatus JSON
  Browser->>Browser: render sync pipeline and charts
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: a node-health/status surface and real charts.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 feat/ui-node-health

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
ergo-api/web/js/overview.js (1)

584-629: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Unhandled rejection risk in refreshChartData.

refreshChartData() is invoked fire-and-forget (line 264, line 634) with no .catch. If either fetch rejects, the finally still resets inFlight, but the rejection is unhandled — noisy console errors with no surfaced diagnostics.

♻️ Proposed fix: swallow/log errors explicitly
   } finally {
     charts.inFlight = false;
   }
-}
+}
+
+function refreshChartDataSafe() {
+  refreshChartData().catch((err) => console.error('chart refresh failed', err));
+}

Then use refreshChartDataSafe() at the two call sites instead of refreshChartData().

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

In `@ergo-api/web/js/overview.js` around lines 584 - 629, refreshChartData
currently lets Promise rejections escape when api.difficultyHistory or
api.poolHistogram fails, which becomes an unhandled rejection because the call
sites invoke it fire-and-forget. Update refreshChartData to catch and log errors
internally (while still clearing charts.inFlight in the existing finally), or
introduce a refreshChartDataSafe wrapper that handles the rejection, and switch
both fire-and-forget call sites to use the safe entrypoint. Reference the
refreshChartData function and its current Promise.all/api calls when
implementing the fix.
ergo-api/src/blockchain.rs (1)

204-232: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated status-label mapping vs. indexed_height_handler.

indexed_height_handler (Line 164-177 in this file) derives its status label via IndexerStatusLabel::from_status(&status). This handler instead hand-rolls the same Syncing/CaughtUp/Halted → string mapping inline. Two independent copies of this mapping can silently drift (e.g. a future relabeling applied to only one handler) even though both surfaces document "the same camelCase label set as indexedHeight."

Consider deriving the string from IndexerStatusLabel::from_status(&status) (e.g. via a Display/to_string() if available) to keep the two endpoints' status wire format single-sourced.

♻️ Possible consolidation
-        status: match &status {
-            IndexerStatus::Syncing => "syncing".to_string(),
-            IndexerStatus::CaughtUp => "caughtUp".to_string(),
-            IndexerStatus::Halted(_) => "halted".to_string(),
-        },
+        status: IndexerStatusLabel::from_status(&status).to_string(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ergo-api/src/blockchain.rs` around lines 204 - 232, The status label mapping
in indexer_status_handler is duplicated and can drift from
indexed_height_handler. Replace the inline Syncing/CaughtUp/Halted string
conversion with the same single source used by indexed_height_handler, namely
IndexerStatusLabel::from_status(&status), and derive the API string from that
shared representation so both endpoints stay aligned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ergo-api/web/js/chart.js`:
- Around line 211-224: The bar rendering in chart.js has a `y`/`height` mismatch
for tiny values: `height` is clamped in the `bins.forEach` rect creation, but
`y` still uses the raw `bh`, so small nonzero bars get clipped. Update the bar
placement logic in the `bins.forEach` block so `y` is derived from the same
clamped height used for `height`, keeping the bottom edge aligned with the
baseline; use the existing `svgEl('rect', ...)`, `bh`, and `ui.svg.append` code
path as the place to fix it.

---

Nitpick comments:
In `@ergo-api/src/blockchain.rs`:
- Around line 204-232: The status label mapping in indexer_status_handler is
duplicated and can drift from indexed_height_handler. Replace the inline
Syncing/CaughtUp/Halted string conversion with the same single source used by
indexed_height_handler, namely IndexerStatusLabel::from_status(&status), and
derive the API string from that shared representation so both endpoints stay
aligned.

In `@ergo-api/web/js/overview.js`:
- Around line 584-629: refreshChartData currently lets Promise rejections escape
when api.difficultyHistory or api.poolHistogram fails, which becomes an
unhandled rejection because the call sites invoke it fire-and-forget. Update
refreshChartData to catch and log errors internally (while still clearing
charts.inFlight in the existing finally), or introduce a refreshChartDataSafe
wrapper that handles the rejection, and switch both fire-and-forget call sites
to use the safe entrypoint. Reference the refreshChartData function and its
current Promise.all/api calls when implementing the fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5d4f440-feb2-497d-90be-68bac3474fbe

📥 Commits

Reviewing files that changed from the base of the PR and between 7d495ec and bbdc082.

📒 Files selected for processing (16)
  • ergo-api/src/blockchain.rs
  • ergo-api/src/server.rs
  • ergo-api/src/types.rs
  • ergo-api/src/web.rs
  • ergo-api/tests/fixtures/openapi_native.yaml
  • ergo-api/tests/indexer_status_endpoint.rs
  • ergo-api/tests/wallet_ui_headers.rs
  • ergo-api/web/dashboard.css
  • ergo-api/web/js/api-client.js
  • ergo-api/web/js/chart.js
  • ergo-api/web/js/overview.js
  • ergo-indexer-types/src/lib.rs
  • ergo-indexer-types/src/query.rs
  • ergo-indexer/src/handle.rs
  • ergo-indexer/src/lib.rs
  • ergo-indexer/src/store/mod.rs

Comment thread ergo-api/web/js/chart.js
Phase 2A of the first-class UI effort: the node's health machinery becomes
observable, and the Charts view graduates from session sparklines to real
server-history charts.

Indexer health endpoint:
- New defaulted `IndexerQuery::health()` (ergo-indexer-types) returning
  IndexerHealthDto: the durable self-repair markers from the #147 incident
  machinery (repair_pending / repair_next_gi rebuild cursor /
  repair_skipped honest marker), the process-lifetime drift-skip counter,
  and the global box/tx totals. IndexerHandle overrides it with best-effort
  store reads (a degraded store degrades a field, never errors the
  snapshot); stubs inherit the healthy default.
- New `GET /api/v1/indexer/status` (utoipa-documented, native openapi
  snapshot regenerated): always-200 and never status-gated — like
  indexedHeight it must answer while syncing, repairing, or halted.
  `/blockchain/indexedHeight` stays pinned to its Scala-parity shape; the
  repair/totals superset lives here. 404 on indexer-less wiring = disabled.
  Pinned by a new 4-case endpoint test.

Overview integration:
- The Sync-pipeline panel now surfaces index health, silent when healthy:
  a progress row while a rebuild runs (cursor/total boxes), a queued row
  during the wipe phase, a "done · N box(es) skipped" honest-marker row
  after a knowingly-incomplete repair, and a red halt row with the reason.

Real charts (new zero-dep chart.js — axis-labelled SVG line/area + bar
histogram with pointer readouts; text lives in HTML around the stretched
SVG so nothing distorts; data via textContent only):
- Difficulty (last 720 blocks) from /api/v1/difficulty/history — the
  string-typed difficulty parsed once per point.
- Estimated hashrate per point (difficulty / target interval — the same
  derivation as the KPI band).
- Block-interval distribution histogram from the same series' timestamp
  deltas (<30s … >10m bins).
- Mempool age histogram from /transactions/poolHistogram (waiting tx by
  wait-time bucket).
Charts build once and update in place (hover state survives the 4s tick);
the series refetches only when the tip height advances.

Verified against live mainnet data via the dev proxy (real difficulty
epoch steps, 2-minute-modal interval distribution, live pool histogram).
fmt/clippy/cargo test --all green; openapi snapshot + runtime-mount +
header tests updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arkadianet
arkadianet force-pushed the feat/ui-node-health branch from bbdc082 to e2bccf5 Compare July 2, 2026 18:58
For 0 < raw-height < 2 the bar rect's y was computed from the raw height
while the height itself was clamped to the 2-unit minimum, pushing the
bottom edge past the baseline (masked by viewBox clipping). Clamp first,
derive y from the clamped value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arkadianet
arkadianet merged commit 72e3b52 into main Jul 2, 2026
7 of 8 checks passed
@arkadianet
arkadianet deleted the feat/ui-node-health branch July 2, 2026 22:53
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