Stability fixes

This commit is contained in:
Daniel 2026-06-20 17:15:24 +02:00
parent 901a7fa7d4
commit 0de0168f63
5 changed files with 298 additions and 105 deletions

View File

@ -183,19 +183,22 @@ Rectangle {
? root.backend.status === BlockchainBackend.Running
: false
// Channel Deposit requires a running node. If the node stops while
// it's selected, fall back to Accounts so the user isn't stranded
// on a disabled nav item.
// Wallet operations require a running node. If the node stops while
// the Operations tab is open, fall back to the Node tab so the user
// isn't stranded on a disabled tab.
onNodeRunningChanged: {
if (!nodeRunning && operationIndex === 3)
operationIndex = 0
if (!nodeRunning)
operationTabBar.currentIndex = 0
}
LogosTabBar {
id: operationTabBar
Layout.fillWidth: true
LogosTabButton { text: qsTr("Node") }
LogosTabButton { text: qsTr("Operations") }
LogosTabButton {
text: qsTr("Operations")
enabled: opPage.nodeRunning
}
}
StackLayout {
@ -266,11 +269,7 @@ Rectangle {
NavItem { label: qsTr("Accounts"); index: 0 }
NavItem { label: qsTr("Transfer"); index: 1 }
NavItem { label: qsTr("Leader Rewards"); index: 2 }
NavItem {
label: qsTr("Channel Deposit")
index: 3
itemEnabled: opPage.nodeRunning
}
NavItem { label: qsTr("Channel Deposit"); index: 3 }
Item { Layout.fillHeight: true }
}
@ -406,7 +405,6 @@ Rectangle {
component NavItem: Rectangle {
property string label
property int index
property bool itemEnabled: true
Layout.fillWidth: true
Layout.preferredHeight: 40
@ -414,7 +412,6 @@ Rectangle {
color: opPage.operationIndex === index
? Theme.palette.backgroundTertiary
: (navMouse.containsMouse ? Theme.palette.backgroundSecondary : "transparent")
opacity: itemEnabled ? 1.0 : 0.4
LogosText {
anchors.left: parent.left
@ -434,7 +431,6 @@ Rectangle {
MouseArea {
id: navMouse
anchors.fill: parent
enabled: itemEnabled
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: opPage.operationIndex = index

View File

@ -19,6 +19,12 @@ Rectangle {
// The transactions role is a QStringList; surface it for the Repeater.
readonly property var transactionsList: model.transactions || []
// Roles read as `undefined` during the brief QtRO replica sync at node
// startup. Treat the fallback as active ONLY when the model says so
// explicitly (parsed === false); undefined means "still loading", not
// "unparsed" otherwise freshly-arrived blocks flash as Unparsed.
readonly property bool isUnparsed: model.parsed === false
width: ListView.view ? ListView.view.width : implicitWidth
implicitHeight: col.implicitHeight + 2 * Theme.spacing.medium
@ -53,7 +59,7 @@ Rectangle {
}
LogosText {
visible: model.parsed
visible: !del.isUnparsed
text: qsTr("slot %1").arg(model.slot || qsTr("?"))
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
@ -61,7 +67,7 @@ Rectangle {
// Version badge
Rectangle {
visible: model.parsed && (model.version || "").length > 0
visible: !del.isUnparsed && (model.version || "").length > 0
radius: Theme.spacing.radiusSmall
color: Theme.palette.backgroundSecondary
border.color: Theme.palette.border
@ -78,7 +84,7 @@ Rectangle {
}
LogosText {
visible: !model.parsed
visible: del.isUnparsed
text: qsTr("Unparsed block")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.warning
@ -87,7 +93,7 @@ Rectangle {
Item { Layout.fillWidth: true }
LogosText {
visible: model.parsed
visible: !del.isUnparsed
text: qsTr("%1 tx").arg(model.txCount || 0)
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
@ -109,14 +115,23 @@ Rectangle {
}
// Parsed: structured header
HashRow { label: qsTr("Parent block"); value: model.parentBlock || ""; visible: model.parsed }
HashRow { label: qsTr("Block root"); value: model.blockRoot || ""; visible: model.parsed }
HashRow { label: qsTr("Signature"); value: model.signature || ""; visible: model.parsed }
HashRow {
label: qsTr("Parent block"); value: model.parentBlock || ""; visible: !del.isUnparsed
onCopyRequested: (t) => del.copyToClipboard(t)
}
HashRow {
label: qsTr("Block root"); value: model.blockRoot || ""; visible: !del.isUnparsed
onCopyRequested: (t) => del.copyToClipboard(t)
}
HashRow {
label: qsTr("Signature"); value: model.signature || ""; visible: !del.isUnparsed
onCopyRequested: (t) => del.copyToClipboard(t)
}
// Proof of leadership (collapsible sub-group)
RowLayout {
Layout.fillWidth: true
visible: model.parsed
visible: !del.isUnparsed
spacing: Theme.spacing.small
LogosText {
text: (del.proofExpanded ? "▾ " : "▸ ") + qsTr("Proof of leadership")
@ -129,34 +144,35 @@ Rectangle {
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: Theme.spacing.medium
visible: model.parsed && del.proofExpanded
visible: !del.isUnparsed && del.proofExpanded
spacing: Theme.spacing.small
HashRow { label: qsTr("Leader key"); value: model.leaderKey || "" }
HashRow { label: qsTr("Entropy"); value: model.entropy || "" }
HashRow { label: qsTr("Proof"); value: model.proof || "" }
HashRow { label: qsTr("Voucher cm"); value: model.voucherCm || "" }
HashRow { label: qsTr("Leader key"); value: model.leaderKey || ""; onCopyRequested: (t) => del.copyToClipboard(t) }
HashRow { label: qsTr("Entropy"); value: model.entropy || ""; onCopyRequested: (t) => del.copyToClipboard(t) }
HashRow { label: qsTr("Proof"); value: model.proof || ""; onCopyRequested: (t) => del.copyToClipboard(t) }
HashRow { label: qsTr("Voucher cm"); value: model.voucherCm || ""; onCopyRequested: (t) => del.copyToClipboard(t) }
}
// Transactions
LogosText {
visible: model.parsed
visible: !del.isUnparsed
text: qsTr("Transactions (%1)").arg(model.txCount || 0)
font.pixelSize: Theme.typography.secondaryText
font.bold: true
}
ColumnLayout {
Layout.fillWidth: true
visible: model.parsed
spacing: Theme.spacing.tiny
visible: !del.isUnparsed
spacing: Theme.spacing.small
Repeater {
model: del.expanded ? del.transactionsList : []
delegate: TxItem {
delegate: TransactionDelegate {
required property int index
required property string modelData
Layout.fillWidth: true
idx: index
json: modelData
onCopyToClipboard: (t) => del.copyToClipboard(t)
}
}
@ -171,7 +187,7 @@ Rectangle {
// Unparsed: raw fallback
RowLayout {
Layout.fillWidth: true
visible: !model.parsed
visible: del.isUnparsed
spacing: Theme.spacing.small
LogosText {
text: qsTr("Raw payload")
@ -183,80 +199,9 @@ Rectangle {
}
JsonBlock {
Layout.fillWidth: true
visible: !model.parsed
json: model.rawJson || ""
visible: del.isUnparsed
json: model.rawJson || qsTr("(no payload)")
}
}
}
// ---- Inline helpers ----
component HashRow: RowLayout {
property string label
property string value
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosText {
text: label
Layout.preferredWidth: 110
color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
}
LogosText {
Layout.fillWidth: true
text: value && value.length > 0 ? value : "—"
elide: Text.ElideMiddle
font.pixelSize: Theme.typography.secondaryText
font.family: "monospace"
}
LogosCopyButton {
visible: value && value.length > 0
onCopyText: del.copyToClipboard(value)
}
}
component JsonBlock: Rectangle {
property string json
implicitHeight: jsonText.implicitHeight + 2 * Theme.spacing.small
color: Theme.palette.backgroundSecondary
radius: Theme.spacing.radiusSmall
border.color: Theme.palette.border
border.width: 1
LogosText {
id: jsonText
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacing.small
text: parent.json
font.pixelSize: Theme.typography.secondaryText
font.family: "monospace"
wrapMode: Text.WrapAnywhere
}
}
component TxItem: ColumnLayout {
id: txRoot
property int idx
property string json
property bool open: false
spacing: Theme.spacing.tiny
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosText {
text: (txRoot.open ? "▾ " : "▸ ") + qsTr("Transaction %1").arg(txRoot.idx + 1)
font.pixelSize: Theme.typography.secondaryText
TapHandler { onTapped: txRoot.open = !txRoot.open }
}
Item { Layout.fillWidth: true }
LogosCopyButton { onCopyText: del.copyToClipboard(txRoot.json) }
}
JsonBlock {
Layout.fillWidth: true
visible: txRoot.open
json: txRoot.json
}
}
}

View File

@ -0,0 +1,41 @@
import QtQuick
import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
// A labelled value row with an elided monospace value and a copy button.
// Used for hashes/keys and any short scalar field.
RowLayout {
id: root
property string label: ""
property string value: ""
property int labelWidth: 110
property bool copyable: true
signal copyRequested(string text)
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosText {
visible: root.label.length > 0
text: root.label
Layout.preferredWidth: root.labelWidth
Layout.alignment: Qt.AlignTop
color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
}
LogosText {
Layout.fillWidth: true
text: root.value && root.value.length > 0 ? root.value : "—"
elide: Text.ElideMiddle
font.pixelSize: Theme.typography.secondaryText
font.family: "monospace"
}
LogosCopyButton {
visible: root.copyable && root.value && root.value.length > 0
onCopyText: root.copyRequested(root.value)
}
}

View File

@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
// A boxed, monospace, wrapping JSON/text block. Used for prettified payloads
// and raw fallbacks.
Rectangle {
id: root
property string json: ""
Layout.fillWidth: true
implicitHeight: jsonText.implicitHeight + 2 * Theme.spacing.small
color: Theme.palette.backgroundSecondary
radius: Theme.spacing.radiusSmall
border.color: Theme.palette.border
border.width: 1
LogosText {
id: jsonText
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacing.small
text: root.json
font.pixelSize: Theme.typography.secondaryText
font.family: "monospace"
wrapMode: Text.WrapAnywhere
}
}

View File

@ -0,0 +1,179 @@
import QtQuick
import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
// Structured view of a single block transaction (a SignedMantleTx JSON string).
//
// Shape (see nomos-node core/src/mantle):
// { "mantle_tx": { "ops": [ {opcode, payload}, ... ] },
// "ops_proofs": [ <proof>, ... ] } // paired with ops by index
//
// Each op is labelled by its opcode name; the payload (and paired proof) are
// shown as prettified JSON. Unparsable transactions fall back to raw JSON.
ColumnLayout {
id: txRoot
property string json: ""
property int idx: 0
property bool open: false
signal copyToClipboard(string text)
readonly property var parsed: safeParse(json)
readonly property bool parseFailed: parsed === null
readonly property var ops: (parsed && parsed.mantle_tx && parsed.mantle_tx.ops)
? parsed.mantle_tx.ops : []
readonly property var proofs: (parsed && parsed.ops_proofs) ? parsed.ops_proofs : []
spacing: Theme.spacing.tiny
function safeParse(s) {
try { return JSON.parse(s) } catch (e) { return null }
}
function opName(code) {
switch (code) {
case 0: return qsTr("Transfer")
case 16: return qsTr("Channel Config")
case 17: return qsTr("Channel Inscribe")
case 18: return qsTr("Channel Deposit")
case 19: return qsTr("Channel Withdraw")
case 32: return qsTr("SDP Declare")
case 33: return qsTr("SDP Withdraw")
case 34: return qsTr("SDP Active")
case 48: return qsTr("Leader Claim")
default: return qsTr("Op 0x%1").arg(Number(code).toString(16))
}
}
function opSummary() {
if (parseFailed) return qsTr("unparsed")
if (!ops.length) return qsTr("no ops")
var names = []
for (var i = 0; i < ops.length; i++) names.push(opName(ops[i].opcode))
return names.join(", ")
}
function pretty(v) {
try { return JSON.stringify(v, null, 2) } catch (e) { return String(v) }
}
function proofVariant(p) {
if (!p || typeof p !== "object") return ""
var ks = Object.keys(p)
return ks.length ? ks[0] : ""
}
// ---- Header (toggle) ----
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosText {
text: (txRoot.open ? "▾ " : "▸ ") + qsTr("Transaction %1").arg(txRoot.idx + 1)
font.pixelSize: Theme.typography.secondaryText
font.bold: true
TapHandler { onTapped: txRoot.open = !txRoot.open }
}
LogosText {
Layout.fillWidth: true
text: txRoot.opSummary()
elide: Text.ElideRight
color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
TapHandler { onTapped: txRoot.open = !txRoot.open }
}
LogosCopyButton { onCopyText: txRoot.copyToClipboard(txRoot.json) }
}
// ---- Body ----
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: Theme.spacing.medium
visible: txRoot.open
spacing: Theme.spacing.small
JsonBlock {
Layout.fillWidth: true
visible: txRoot.parseFailed
json: txRoot.json
}
Repeater {
model: txRoot.open && !txRoot.parseFailed ? txRoot.ops : []
delegate: OpView {
required property int index
required property var modelData
Layout.fillWidth: true
op: modelData
proof: txRoot.proofs.length > index ? txRoot.proofs[index] : null
}
}
}
// ---- Inline: one operation (opcode name + payload/proof JSON) ----
component OpView: ColumnLayout {
id: opView
property var op
property var proof: null
spacing: Theme.spacing.tiny
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
Rectangle {
radius: Theme.spacing.radiusSmall
color: Theme.palette.backgroundSecondary
border.color: Theme.palette.border
border.width: 1
implicitWidth: opcodeText.implicitWidth + 2 * Theme.spacing.small
implicitHeight: opcodeText.implicitHeight + Theme.spacing.tiny
LogosText {
id: opcodeText
anchors.centerIn: parent
text: qsTr("op %1").arg(opView.op && opView.op.opcode !== undefined ? opView.op.opcode : "?")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
}
}
LogosText {
Layout.fillWidth: true
text: txRoot.opName(opView.op ? opView.op.opcode : -1)
font.pixelSize: Theme.typography.secondaryText
font.bold: true
}
LogosCopyButton {
onCopyText: txRoot.copyToClipboard(txRoot.pretty(opView.op ? opView.op.payload : null))
}
}
// payload as JSON
JsonBlock {
Layout.fillWidth: true
Layout.leftMargin: Theme.spacing.medium
json: txRoot.pretty(opView.op ? opView.op.payload : null)
}
// paired proof
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: Theme.spacing.medium
visible: opView.proof !== null && opView.proof !== undefined
spacing: Theme.spacing.small
LogosText {
text: qsTr("Proof · %1").arg(txRoot.proofVariant(opView.proof) || qsTr("unknown"))
color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
}
Item { Layout.fillWidth: true }
LogosCopyButton { onCopyText: txRoot.copyToClipboard(txRoot.pretty(opView.proof)) }
}
JsonBlock {
Layout.fillWidth: true
Layout.leftMargin: Theme.spacing.medium
visible: opView.proof !== null && opView.proof !== undefined
json: (opView.proof !== null && opView.proof !== undefined) ? txRoot.pretty(opView.proof) : ""
}
}
}