feat: add indexer status update to home, reduce settings buttons

This commit is contained in:
erhant 2026-06-22 18:38:29 +03:00
parent ec9c0adb42
commit 748724be92
6 changed files with 111 additions and 81 deletions

2
.gitignore vendored
View File

@ -5,4 +5,4 @@ build
lez-explorer-ui-*-lgx-*
# ignore rocksdb (may appear here during indexer tests)
rocksdb
rocksdb*

16
flake.lock generated
View File

@ -21,11 +21,11 @@
"logos-module-builder": "logos-module-builder_4"
},
"locked": {
"lastModified": 1781900595,
"narHash": "sha256-75phyeA9/wOeVo+IDIujzdlHIyK6X+KV1nMsNvDlXww=",
"lastModified": 1782142627,
"narHash": "sha256-IZslE6yROAxl/FpjlEeZRc+vHKc/1M7kIFvRlgWqDIQ=",
"ref": "erhant/migr-to-logos-module-builder-update-LEZ",
"rev": "f6f9cfdb3ae3cca4f5f934c47dddfaaa230c7b1c",
"revCount": 40,
"rev": "014a6dcfe1f5b2529024e8e0fa659e0c546f8652",
"revCount": 42,
"type": "git",
"url": "https://github.com/logos-blockchain/lez-indexer-module"
},
@ -2013,11 +2013,11 @@
"rust-rapidsnark": "rust-rapidsnark"
},
"locked": {
"lastModified": 1781900321,
"narHash": "sha256-zwEhLNqhAL9otUk8cJIygw22YZrAeV0q7RDnne3kmHI=",
"lastModified": 1782142589,
"narHash": "sha256-K5NtBontpPx1XqnX6jbEzPXSchaLbPlG9689W+C/ExA=",
"ref": "erhant/fix-indexer-ffi",
"rev": "49d540e64d74bd4517a0f9c42f16f0c9140f7299",
"revCount": 2771,
"rev": "2a78b1b3d1b4080930c00ecf23b5c0e73cd646b8",
"revCount": 2773,
"type": "git",
"url": "https://github.com/logos-blockchain/logos-execution-zone"
},

View File

@ -13,10 +13,15 @@ class LezExplorerUi
// Recent-blocks feed shown on the home page (newest first). Backend-owned:
// the 2 s poll prepends new heads and "Load more" appends older pages.
PROP(QVariantList recentBlocks READONLY)
// Tip block id (from getLastFinalizedBlockId).
// Tip block id (from the indexer status' indexedBlockId).
PROP(qlonglong chainHeight=0 READONLY)
// "Connecting" / "Connected" / "Error" — drives the health indicator.
PROP(QString connectionStatus READONLY)
// Ingestion status for the first-launch/sync banner, refreshed by the tip
// poll. Map of { state: "starting"|"syncing"|"caught_up"|"error",
// blockId: qulonglong, error: QString }. Lets the view show the current
// phase (or the error) so "catching up" reads differently from "failed".
PROP(QVariantMap syncStatus READONLY)
// The indexer config JSON currently in effect ("" until one is saved). The
// Settings editor seeds from this (falling back to defaultConfig).
PROP(QString configText READONLY)

View File

@ -405,17 +405,54 @@ QVariantMap LezExplorerUiBackend::search(QString query)
return results;
}
quint64 LezExplorerUiBackend::applySyncStatus(const QString& json)
{
// Empty JSON means the indexer isn't running (not configured / not started);
// a real status always carries one of the core's states.
QString state = QStringLiteral("stopped");
quint64 blockId = 0;
QString errorMsg;
if (!json.isEmpty()) {
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8(), &parseError);
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
const QJsonObject obj = doc.object();
state = obj.value(QStringLiteral("state")).toString(QStringLiteral("starting"));
blockId = static_cast<quint64>(obj.value(QStringLiteral("indexedBlockId")).toDouble(0));
errorMsg = obj.value(QStringLiteral("lastError")).toString();
}
}
QVariantMap status;
status.insert(QStringLiteral("state"), state);
status.insert(QStringLiteral("blockId"), static_cast<qulonglong>(blockId));
status.insert(QStringLiteral("error"), errorMsg);
setSyncStatus(status);
// Keep the legacy health indicator in step with the richer status.
if (state == QLatin1String("error")) {
setConnectionStatus(QStringLiteral("Error"));
} else if (state == QLatin1String("syncing") || state == QLatin1String("caught_up")) {
setConnectionStatus(QStringLiteral("Connected"));
} else {
setConnectionStatus(QStringLiteral("Connecting"));
}
return blockId;
}
void LezExplorerUiBackend::pollTip()
{
const QString tipStr = modules().lez_indexer_module.getLastFinalizedBlockId();
bool ok = false;
const quint64 tip = tipStr.toULongLong(&ok);
if (!ok || tip == 0) {
setConnectionStatus(QStringLiteral("Connecting"));
// One status call drives both the sync banner and the feed: indexedBlockId
// is the L2 tip we page from.
const quint64 tip = applySyncStatus(modules().lez_indexer_module.getStatus());
if (tip == 0) {
// No finalized block yet (starting / syncing from genesis, or stopped);
// syncStatus already conveys which, so just wait for the next poll.
return;
}
setConnectionStatus(QStringLiteral("Connected"));
setChainHeight(static_cast<qlonglong>(tip));
// First successful poll: fill the feed and baseline without double-loading.

View File

@ -52,6 +52,9 @@ public:
private:
// Tip poll: detect new heads and append/rebuild the recent-blocks feed.
void pollTip();
// Parse the indexer's status JSON, publish the `syncStatus` PROP + the
// legacy `connectionStatus`, and return the indexed tip (0 if none yet).
quint64 applySyncStatus(const QString& json);
// Fetch a single block as a normalized map ({} if missing).
QVariantMap fetchBlock(quint64 blockId);
// Parse a getBlocks(...) JSON array payload into a list of block maps.

View File

@ -11,13 +11,47 @@ Item {
// Injected by Main: the controller (navigation + backend access).
property var explorer
readonly property var backend: explorer ? explorer.backend : null
readonly property bool connected: backend && backend.connectionStatus === "Connected"
// Ingestion status (from backend.syncStatus) drives the health/sync banner.
readonly property var sync: backend ? backend.syncStatus : null
readonly property string syncState: (sync && sync.state) ? sync.state : "starting"
readonly property string syncError: (sync && sync.error) ? sync.error : ""
// Human-readable line for the banner: the current phase (or the error), so
// the user can tell catching-up from a real failure on first launch.
function statusLine() {
if (!backend)
return "";
var height = backend.chainHeight > 0 ? backend.chainHeight : "—";
if (page.syncState === "error")
return page.syncError !== "" ? page.syncError : "Indexer error";
if (page.syncState === "caught_up")
return "Up to date · block " + height;
if (page.syncState === "syncing")
return "Syncing… · block " + height;
if (page.syncState === "stopped")
return "Indexer not running";
return "Starting indexer…";
}
// Message for the empty block list, tailored to the current phase. Settings
// lives only in the top-right gear, so we point there rather than add a button.
function emptyStateText() {
if (page.syncState === "error")
return (page.syncError !== "" ? "Indexer error: " + page.syncError : "Indexer error.")
+ "\nOpen Settings to reconfigure and restart.";
if (page.syncState === "syncing" || page.syncState === "starting")
return "Indexer is syncing, please wait…";
if (page.syncState === "caught_up")
return "No blocks indexed yet.";
return "No indexer running.\nOpen Settings to configure and start it.";
}
ColumnLayout {
anchors.fill: parent
spacing: Theme.spacing.medium
// Health bar.
// Health / sync bar.
Card {
Layout.fillWidth: true
@ -30,39 +64,17 @@ Item {
height: 9
radius: 4.5
Layout.alignment: Qt.AlignVCenter
color: page.connected ? Theme.palette.success : Theme.palette.warning
color: page.syncState === "error" ? Theme.palette.error
: page.syncState === "caught_up" ? Theme.palette.success
: Theme.palette.warning
}
LogosText {
text: {
if (!page.backend)
return "";
var height = page.backend.chainHeight > 0 ? page.backend.chainHeight : "—";
return "Chain height: " + height + " · " + page.backend.connectionStatus;
}
color: Theme.palette.textMuted
text: page.statusLine()
color: page.syncState === "error" ? Theme.palette.error : Theme.palette.textMuted
font.pixelSize: Theme.typography.secondaryText
Layout.alignment: Qt.AlignVCenter
}
Item { Layout.fillWidth: true }
Rectangle {
Layout.preferredHeight: 34
Layout.preferredWidth: 100
radius: Theme.spacing.radiusMedium
color: settingsHover.hovered ? Theme.palette.backgroundSecondary : Theme.palette.backgroundElevated
border.width: 1
border.color: Theme.palette.borderSecondary
HoverHandler { id: settingsHover }
TapHandler { onTapped: if (page.explorer) page.explorer.navigateSettings() }
LogosText {
anchors.centerIn: parent
text: "Settings"
color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
}
Layout.fillWidth: true
elide: Text.ElideRight
}
}
}
@ -112,43 +124,16 @@ Item {
}
}
// Empty state.
ColumnLayout {
// Empty state phase-aware message; Settings lives in the top-right gear.
LogosText {
anchors.centerIn: parent
visible: blockList.count === 0
width: parent.width * 0.8
spacing: Theme.spacing.medium
LogosText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
text: page.connected
? "No blocks indexed yet."
: "No indexer running yet. Open Settings to configure and start it."
color: Theme.palette.textMuted
font.pixelSize: Theme.typography.secondaryText
}
Rectangle {
visible: !page.connected
Layout.alignment: Qt.AlignHCenter
implicitHeight: 38
implicitWidth: 160
radius: Theme.spacing.radiusMedium
color: openHover.hovered ? Qt.lighter(Theme.palette.primary, 1.1) : Theme.palette.primary
HoverHandler { id: openHover }
TapHandler { onTapped: if (page.explorer) page.explorer.navigateSettings() }
LogosText {
anchors.centerIn: parent
text: "Open Settings"
color: Theme.palette.background
font.pixelSize: Theme.typography.secondaryText
font.weight: Theme.typography.weightBold
}
}
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
text: page.emptyStateText()
color: Theme.palette.textMuted
font.pixelSize: Theme.typography.secondaryText
}
}
}