From 748724be92c5b68ff0b459bef977da75e5fd82c9 Mon Sep 17 00:00:00 2001 From: erhant Date: Mon, 22 Jun 2026 18:38:29 +0300 Subject: [PATCH] feat: add indexer status update to home, reduce settings buttons --- .gitignore | 2 +- flake.lock | 16 ++--- src/lez_explorer_ui.rep | 7 +- src/lez_explorer_ui_backend.cpp | 49 ++++++++++++-- src/lez_explorer_ui_backend.h | 3 + src/qml/pages/HomePage.qml | 115 ++++++++++++++------------------ 6 files changed, 111 insertions(+), 81 deletions(-) diff --git a/.gitignore b/.gitignore index 7a976e0..d3f2e1c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ build lez-explorer-ui-*-lgx-* # ignore rocksdb (may appear here during indexer tests) -rocksdb \ No newline at end of file +rocksdb* \ No newline at end of file diff --git a/flake.lock b/flake.lock index 94624d0..0370f66 100644 --- a/flake.lock +++ b/flake.lock @@ -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" }, diff --git a/src/lez_explorer_ui.rep b/src/lez_explorer_ui.rep index ba05498..84f3188 100644 --- a/src/lez_explorer_ui.rep +++ b/src/lez_explorer_ui.rep @@ -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) diff --git a/src/lez_explorer_ui_backend.cpp b/src/lez_explorer_ui_backend.cpp index 9691129..23a57f7 100644 --- a/src/lez_explorer_ui_backend.cpp +++ b/src/lez_explorer_ui_backend.cpp @@ -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(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(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(tip)); // First successful poll: fill the feed and baseline without double-loading. diff --git a/src/lez_explorer_ui_backend.h b/src/lez_explorer_ui_backend.h index fe407d6..d0c4aa9 100644 --- a/src/lez_explorer_ui_backend.h +++ b/src/lez_explorer_ui_backend.h @@ -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. diff --git a/src/qml/pages/HomePage.qml b/src/qml/pages/HomePage.qml index eeec57e..dd54f6f 100644 --- a/src/qml/pages/HomePage.qml +++ b/src/qml/pages/HomePage.qml @@ -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 } } }