feat: add receiver2 + visual overhaul (topology, heartbeat, phase timeline, arrows)

This commit is contained in:
Arseniy Klempner 2026-06-04 14:24:16 -06:00
parent 49e5742252
commit e0f300d4e4
No known key found for this signature in database
GPG Key ID: 51653F18863BD24B
6 changed files with 1834 additions and 400 deletions

View File

@ -4,3 +4,4 @@ ADDR_MIX2=0x8e3d4d0a713087e2263e2fcdec894c283c777dcc
ADDR_MIX3=0xca282bbf8bf3636e15af3ad8caf11cdd38bf35d8
ADDR_SENDER=0x0b6872aaae7a2d4f3c701793cde57b93337f4d4a
ADDR_RECEIVER=0xb5dda07309f5ab06e0847f6036c305ea9ae26937
ADDR_RECEIVER2=0x98d57890bd155475735648235a4f099e63776b85

View File

@ -7,3 +7,4 @@ KEY_MIX2=b880df1f571109e646f641636794dfe7ffefc2aab19290ba0d720c407758304d
KEY_MIX3=0b1b5e18839a3e15b119519092e4a94a71122acf57d8b2e1014df0121cb6f0ea
KEY_SENDER=5284ac01fed5fcb6b26933ac4a901412b66fcd7ee5b945b799f147a3b42f49ef
KEY_RECEIVER=a5619d6bfde09f54165ec9da55a7be7380f1b258c8279177dbda5ac235d0e904
KEY_RECEIVER2=3d48489ee2bb7e13a13442ec0e17798c77befb7c635b0b53cf91b2c933869916

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,7 @@ void MonitorBackend::setStateDir(const QString& path, bool replay) {
}
if (m_senderTailer) { m_senderTailer->stop(); delete m_senderTailer; m_senderTailer = nullptr; }
if (m_receiverTailer) { m_receiverTailer->stop(); delete m_receiverTailer; m_receiverTailer = nullptr; }
if (m_receiver2Tailer) { m_receiver2Tailer->stop(); delete m_receiver2Tailer; m_receiver2Tailer = nullptr; }
resetState();
@ -53,6 +54,30 @@ void MonitorBackend::setStateDir(const QString& path, bool replay) {
connect(m_receiverTailer, &LogTailer::newLine, this, [this](const QString& l){ onChatLine(false, l); });
connect(m_receiverTailer, &LogTailer::fileReset, this, [this]{ resetState(); });
m_receiverTailer->start();
// Receiver2 (optional — file may not exist)
m_receiver2Tailer = new LogTailer(QDir(path).filePath("chat_receiver2.log"), replay, this);
connect(m_receiver2Tailer, &LogTailer::newLine, this, [this](const QString& l){
auto ev = LogParser::parseChatLine(l);
auto& chat = m_receiver2;
switch (ev.type) {
case ParsedEvent::ChatInit: chat.phase = "init"; m_hasReceiver2 = true; break;
case ParsedEvent::ChatStart: chat.phase = "start"; break;
case ParsedEvent::ChatMembershipRequested: chat.phase = "request"; break;
case ParsedEvent::ChatMembershipGranted:
chat.phase = QStringLiteral("opt:%1").arg(ev.intVal); chat.optLeaf = ev.intVal; break;
case ParsedEvent::ChatMembershipConfirmed:
chat.phase = QStringLiteral("conf:%1").arg(ev.intVal); chat.authLeaf = ev.intVal; break;
case ParsedEvent::ChatNewMessage: ++chat.msgIn; break;
case ParsedEvent::ChatNewConversation: chat.phase = "intro_accepted"; break;
case ParsedEvent::ChatPeerStatus:
chat.peers = ev.intVal; chat.mixReady = ev.boolVal; chat.mixPool = ev.intVal2; break;
default: return;
}
emit stateChanged();
});
connect(m_receiver2Tailer, &LogTailer::fileReset, this, [this]{ resetState(); });
m_receiver2Tailer->start();
}
void MonitorBackend::resetState() {
@ -63,6 +88,8 @@ void MonitorBackend::resetState() {
for (auto& n : m_nodes) n = {};
m_sender = {};
m_receiver = {};
m_receiver2 = {};
m_hasReceiver2 = false;
m_chainEvents.clear();
emit stateChanged();
}

View File

@ -45,6 +45,15 @@ class MonitorBackend : public QObject {
Q_PROPERTY(int receiverMsgOut READ receiverMsgOut NOTIFY stateChanged)
Q_PROPERTY(int receiverMsgIn READ receiverMsgIn NOTIFY stateChanged)
Q_PROPERTY(QString receiver2Phase READ receiver2Phase NOTIFY stateChanged)
Q_PROPERTY(int receiver2OptLeaf READ receiver2OptLeaf NOTIFY stateChanged)
Q_PROPERTY(int receiver2AuthLeaf READ receiver2AuthLeaf NOTIFY stateChanged)
Q_PROPERTY(int receiver2Peers READ receiver2Peers NOTIFY stateChanged)
Q_PROPERTY(bool receiver2MixReady READ receiver2MixReady NOTIFY stateChanged)
Q_PROPERTY(int receiver2MixPool READ receiver2MixPool NOTIFY stateChanged)
Q_PROPERTY(int receiver2MsgIn READ receiver2MsgIn NOTIFY stateChanged)
Q_PROPERTY(bool hasReceiver2 READ hasReceiver2 NOTIFY stateChanged)
public:
explicit MonitorBackend(QObject* parent = nullptr);
@ -84,6 +93,15 @@ public:
int receiverMsgOut() const { return m_receiver.msgOut; }
int receiverMsgIn() const { return m_receiver.msgIn; }
QString receiver2Phase() const { return m_receiver2.phase; }
int receiver2OptLeaf() const { return m_receiver2.optLeaf; }
int receiver2AuthLeaf() const { return m_receiver2.authLeaf; }
int receiver2Peers() const { return m_receiver2.peers; }
bool receiver2MixReady() const { return m_receiver2.mixReady; }
int receiver2MixPool() const { return m_receiver2.mixPool; }
int receiver2MsgIn() const { return m_receiver2.msgIn; }
bool hasReceiver2() const { return m_hasReceiver2; }
ChainEventModel* chainEventModel() { return &m_chainEvents; }
signals:
@ -131,6 +149,8 @@ private:
NodeState m_nodes[4];
ChatState m_sender;
ChatState m_receiver;
ChatState m_receiver2;
bool m_hasReceiver2 = false;
ChainEventModel m_chainEvents;
QTimer m_blockAgeTimer;
@ -139,4 +159,5 @@ private:
LogTailer* m_nodeTailers[4] = {};
LogTailer* m_senderTailer = nullptr;
LogTailer* m_receiverTailer = nullptr;
LogTailer* m_receiver2Tailer = nullptr;
};

View File

@ -5,11 +5,22 @@ import QtQuick.Layouts 1.15
ApplicationWindow {
id: root
visible: true
width: 900
height: 600
title: "Sim Monitor"
width: 1100
height: 720
color: "#0A0A0A"
// Dynamic title
title: {
var nodes = 0
try { var n = JSON.parse(monitor.mixNodeStates); nodes = n.filter(function(x){return x.lez && x.kad}).length } catch(e){}
var s = "Sim Monitor"
if (monitor.blockId > 0) s += " — block " + monitor.blockId
s += " | " + nodes + "/4 nodes"
if (monitor.senderPhase !== "---") s += " | S:" + monitor.senderPhase
if (monitor.receiverPhase !== "---") s += " | R:" + monitor.receiverPhase
return s
}
readonly property color bgPrimary: "#0A0A0A"
readonly property color bgSecondary: "#111111"
readonly property color bgPanel: "#161616"
@ -18,483 +29,525 @@ ApplicationWindow {
readonly property color textSecond: "#6B7280"
readonly property color textTertiary:"#4B5563"
readonly property color accent: "#10B981"
readonly property color accentDim: "#065F46"
readonly property color yellow: "#F59E0B"
readonly property color red: "#EF4444"
readonly property color blue: "#2563EB"
readonly property string monoFont: "JetBrains Mono, Menlo, Monaco, monospace"
function blockAgeColor(secs) {
if (secs < 0) return textTertiary
if (secs < 15) return accent
if (secs < 30) return yellow
return red
}
function mixDotColor(jsonStr) {
function nodeColor(jsonStr, idx) {
try {
var nodes = JSON.parse(jsonStr)
return nodes.map(function(n) {
if (n.lez && n.kad) return accent
if (n.mounted) return yellow
return textTertiary
})
} catch(e) {
return [textTertiary, textTertiary, textTertiary, textTertiary]
}
var n = nodes[idx]
if (n.lez && n.kad) return accent
if (n.mounted) return yellow
} catch(e) {}
return textTertiary
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 4
anchors.margins: 10
spacing: 6
// INFRA STRIP
//
// INFRA: Sequencer heartbeat + Network topology + Gifter
//
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 52
Layout.preferredHeight: 130
color: bgSecondary
radius: 4
radius: 6
ColumnLayout {
RowLayout {
anchors.fill: parent
anchors.margins: 6
spacing: 2
anchors.margins: 10
spacing: 20
// Line 1: Sequencer
Text {
font.family: root.monoFont
font.pixelSize: 12
color: textPrimary
text: {
var age = monitor.blockAgeSecs
var ageStr = age < 0 ? "---" : age + "s ago"
var rpc = monitor.rpcReachable ? " rpc=" + monitor.rpcBlockId : ""
return "SEQ block=" + monitor.blockId + " (" + ageStr + rpc + ") tx:" +
monitor.txValidated + "✓/" + monitor.txFailed + "✗"
// Sequencer heartbeat
ColumnLayout {
Layout.preferredWidth: 180
spacing: 4
RowLayout {
spacing: 6
// Pulsing heartbeat dot
Rectangle {
id: heartbeat
width: 12; height: 12; radius: 6
color: monitor.blockId > 0 ? root.accent : root.textTertiary
SequentialAnimation on scale {
id: heartbeatAnim
loops: 1
NumberAnimation { to: 1.4; duration: 100; easing.type: Easing.OutQuad }
NumberAnimation { to: 1.0; duration: 300; easing.type: Easing.InQuad }
}
property int _lastBlock: 0
Connections {
target: monitor
function onStateChanged() {
if (monitor.blockId !== heartbeat._lastBlock && monitor.blockId > 0) {
heartbeatAnim.restart()
heartbeat._lastBlock = monitor.blockId
}
}
}
}
Text {
font.family: root.monoFont; font.pixelSize: 11; font.bold: true
color: root.textSecond
text: "SEQUENCER"
}
}
// Block number (large)
Text {
font.family: root.monoFont; font.pixelSize: 28; font.bold: true
color: root.textPrimary
text: monitor.blockId > 0 ? "# " + monitor.blockId : "---"
}
// Block age bar
Rectangle {
Layout.fillWidth: true; height: 4; radius: 2; color: root.border
Rectangle {
width: {
var age = monitor.blockAgeSecs
if (age < 0) return 0
return Math.min(1.0, age / 30.0) * parent.width
}
height: parent.height; radius: 2
color: {
var age = monitor.blockAgeSecs
if (age < 15) return root.accent
if (age < 30) return root.yellow
return root.red
}
}
}
// TX counters
Text {
font.family: root.monoFont; font.pixelSize: 10
color: root.textSecond
text: "tx: " + monitor.txValidated + " ✓ " + monitor.txFailed + " ✗" +
(monitor.rpcReachable ? " rpc:" + monitor.rpcBlockId : "")
}
}
// Line 2: Mix dots + Gifter + Payment
Row {
spacing: 16
// Separator
Rectangle { width: 1; Layout.fillHeight: true; color: root.border }
// Network topology (horizontal)
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text { font.family: root.monoFont; font.pixelSize: 11; font.bold: true; color: root.textSecond; text: "MIX NETWORK"; Layout.alignment: Qt.AlignHCenter }
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 12
Row {
spacing: 2
Text { font.family: root.monoFont; font.pixelSize: 12; color: textSecond; text: "MIX " }
Repeater {
model: 4
Text {
font.pixelSize: 14
text: "●"
color: {
var colors = mixDotColor(monitor.mixNodeStates)
return colors[index] || textTertiary
Column {
spacing: 3
Rectangle {
width: 44; height: 44; radius: 22
color: nodeColor(monitor.mixNodeStates, index)
border.color: Qt.lighter(nodeColor(monitor.mixNodeStates, index), 1.3)
border.width: 2
anchors.horizontalCenter: parent.horizontalCenter
Text {
anchors.centerIn: parent
font.family: root.monoFont; font.pixelSize: 13; font.bold: true
color: "#000"
text: "N" + index
}
}
Rectangle {
visible: index === 0
width: giftLabel.implicitWidth + 8; height: 14; radius: 4
color: monitor.gifterMounted ? root.accent : root.textTertiary
anchors.horizontalCenter: parent.horizontalCenter
Text {
id: giftLabel
anchors.centerIn: parent
font.family: root.monoFont; font.pixelSize: 8; font.bold: true
color: "#000"
text: "GIFTER"
}
}
Text {
visible: index !== 0
font.family: root.monoFont; font.pixelSize: 8
color: root.textTertiary
text: "relay"
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Text {
font.family: root.monoFont; font.pixelSize: 12
color: monitor.gifterMounted ? accent : textTertiary
text: "GIFTER " + monitor.gifterStatus
// Connection line under the nodes
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 4 * 44 + 3 * 12; height: 2; radius: 1
color: {
try {
var nodes = JSON.parse(monitor.mixNodeStates)
var allGreen = nodes.every(function(n) { return n.lez && n.kad })
if (allGreen) return root.accent
var anyMounted = nodes.some(function(n) { return n.mounted })
if (anyMounted) return root.yellow
} catch(e) {}
return root.border
}
}
}
// Separator
Rectangle { width: 1; Layout.fillHeight: true; color: root.border }
// Gifter stats
ColumnLayout {
Layout.preferredWidth: 100
spacing: 4
Text { font.family: root.monoFont; font.pixelSize: 11; font.bold: true; color: root.textSecond; text: "GIFTER" }
Text { font.family: root.monoFont; font.pixelSize: 10; color: root.accent; text: "✓ " + (monitor.gifterMounted ? "mounted" : "---") }
Text { font.family: root.monoFont; font.pixelSize: 10; color: root.textSecond; text: "queue: " + monitor.gifterQueueDepth }
}
}
}
// CHAT PANELS
//
// MESSAGE FLOW: Sender Receiver
//
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 4
spacing: 0
Repeater {
model: ["sender", "receiver"]
// Sender panel
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: bgSecondary
radius: 6
clip: true
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: bgSecondary
radius: 4
property string role: "sender"
property string phase: monitor.senderPhase
property int optLeaf: monitor.senderOptLeaf
property int authLeaf: monitor.senderAuthLeaf
property bool corrected: monitor.senderLeafCorrected
property int peers: monitor.senderPeers
property bool mixRdy: monitor.senderMixReady
property int pool: monitor.senderMixPool
property int out_: monitor.senderMsgOut
property int in_: monitor.senderMsgIn
property string role: modelData
property string phase: role === "sender" ? monitor.senderPhase : monitor.receiverPhase
property int optLeaf: role === "sender" ? monitor.senderOptLeaf : monitor.receiverOptLeaf
property int authLeaf: role === "sender" ? monitor.senderAuthLeaf : monitor.receiverAuthLeaf
property bool corrected: role === "sender" ? monitor.senderLeafCorrected : monitor.receiverLeafCorrected
property int peers: role === "sender" ? monitor.senderPeers : monitor.receiverPeers
property bool mixRdy: role === "sender" ? monitor.senderMixReady : monitor.receiverMixReady
property int pool: role === "sender" ? monitor.senderMixPool : monitor.receiverMixPool
property int out_: role === "sender" ? monitor.senderMsgOut : monitor.receiverMsgOut
property int in_: role === "sender" ? monitor.senderMsgIn : monitor.receiverMsgIn
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 8
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 6
// Header + status summary
RowLayout {
spacing: 8
Text {
font.family: root.monoFont; font.pixelSize: 14; font.bold: true
color: textPrimary
text: role.toUpperCase()
}
Rectangle {
width: statusLabel.implicitWidth + 12; height: 18; radius: 9
color: {
if (phase === "---") return root.textTertiary
if (phase === "msg_sent" || phase === "msg_received") return root.accent
if (phase.indexOf("conf") >= 0 || phase === "ready" ||
phase === "intro_emitted" || phase === "intro_accepted") return "#2563EB"
return root.yellow
}
Text {
id: statusLabel
anchors.centerIn: parent
font.family: root.monoFont; font.pixelSize: 9; font.bold: true
color: "#FFF"
text: {
if (phase === "---") return "WAITING"
if (phase === "init") return "INITIALIZED"
if (phase === "start") return "STARTED"
if (phase === "request") return "REGISTERING"
if (phase.indexOf("opt:") >= 0) return "LEAF " + optLeaf + " (OPTIMISTIC)"
if (phase.indexOf("conf:") >= 0) return "LEAF " + authLeaf + " (CONFIRMED)"
if (phase === "ready") return "MIX READY"
if (phase === "intro_emitted") return "BUNDLE CREATED"
if (phase === "intro_accepted") return "BUNDLE ACCEPTED"
if (phase === "msg_sent") return "SENDING"
if (phase === "msg_received") return "RECEIVING"
return phase.toUpperCase()
}
}
}
// Header
RowLayout {
spacing: 8
Text { font.family: root.monoFont; font.pixelSize: 16; font.bold: true; color: root.textPrimary; text: "SENDER" }
Rectangle {
width: sLabel.implicitWidth + 12; height: 20; radius: 10
color: monitor.senderPhase === "---" ? root.textTertiary :
(monitor.senderPhase === "msg_sent" ? root.accent :
(monitor.senderPhase.indexOf("conf") >= 0 ? root.blue : root.yellow))
Text { id: sLabel; anchors.centerIn: parent; font.family: root.monoFont; font.pixelSize: 9; font.bold: true; color: "#FFF"
text: monitor.senderPhase === "---" ? "WAITING" : monitor.senderPhase.toUpperCase() }
}
}
// Phase progression bar
Row {
spacing: 2
Repeater {
model: ["init", "start", "request", "opt", "conf", "ready",
role === "sender" ? "intro" : "accept",
role === "sender" ? "send" : "recv"]
// Phase timeline
Row {
spacing: 0
Repeater {
model: ["init", "start", "reg", "opt", "conf", "ready", "intro", "send"]
Row {
spacing: 0
property var allPhases: ["init", "start", "request", "opt", "conf", "ready", "intro_emitted", "msg_sent"]
property int currentIdx: allPhases.indexOf(monitor.senderPhase.split(":")[0])
Rectangle {
width: 8; height: 4; radius: 2
color: {
var allPhases = ["init", "start", "request", "opt", "conf", "ready",
role === "sender" ? "intro_emitted" : "intro_accepted",
role === "sender" ? "msg_sent" : "msg_received"]
var current = phase.split(":")[0]
var currentIdx = allPhases.indexOf(current)
if (index < currentIdx) return root.accent
if (index === currentIdx) return root.yellow
return root.border
}
width: 10; height: 10; radius: 5
color: index < currentIdx ? root.accent : (index === currentIdx ? root.yellow : root.border)
border.color: index <= currentIdx ? Qt.lighter(color, 1.3) : "transparent"; border.width: 1
scale: index === currentIdx ? 1.3 : 1.0
Behavior on scale { NumberAnimation { duration: 200 } }
}
Rectangle {
visible: index < 7
width: 16; height: 2; color: index < currentIdx ? root.accent : root.border
anchors.verticalCenter: parent.children[0].verticalCenter
}
}
}
}
// Leaf + membership info
RowLayout {
spacing: 8
Text {
font.family: root.monoFont; font.pixelSize: 10
color: textSecond
text: "RLN MEMBERSHIP"
}
Text {
font.family: root.monoFont; font.pixelSize: 10
color: corrected ? root.yellow : (optLeaf >= 0 && optLeaf === authLeaf ? accent : textSecond)
text: {
if (optLeaf < 0 && authLeaf < 0) return "not registered"
var s = "leaf " + (authLeaf >= 0 ? authLeaf : optLeaf)
if (optLeaf >= 0 && authLeaf < 0) s += " (pending confirmation)"
else if (corrected) s += " (corrected from " + optLeaf + ")"
else if (optLeaf >= 0 && optLeaf === authLeaf) s += " (confirmed ✓)"
return s
}
}
}
// RLN + Network
Text { font.family: root.monoFont; font.pixelSize: 10; color: parent.parent.corrected ? root.yellow : (parent.parent.optLeaf >= 0 && parent.parent.optLeaf === parent.parent.authLeaf ? root.accent : root.textSecond)
text: "RLN " + (parent.parent.authLeaf >= 0 ? "leaf " + parent.parent.authLeaf + " ✓" : (parent.parent.optLeaf >= 0 ? "leaf " + parent.parent.optLeaf + " ⏳" : "not registered")) }
Text { font.family: root.monoFont; font.pixelSize: 10; color: parent.parent.mixRdy ? root.accent : root.textSecond
text: "NET " + parent.parent.peers + " peers" + (parent.parent.mixRdy ? " · mix ✓ pool " + parent.parent.pool : "") }
// Network status
RowLayout {
spacing: 8
Text {
font.family: root.monoFont; font.pixelSize: 10
color: textSecond
text: "NETWORK"
}
Text {
font.family: root.monoFont; font.pixelSize: 10
color: mixRdy ? accent : (peers > 0 ? root.yellow : textTertiary)
text: {
if (peers === 0) return "no peers"
var s = peers + " peers"
if (mixRdy) s += " · mix pool " + pool + " ✓"
else s += " · mix not ready"
return s
}
}
}
// Messages
RowLayout {
spacing: 16
Item { Layout.fillHeight: true }
// MSG badges
RowLayout {
spacing: 12
Repeater {
model: [{ label: "MSG OUT", val: parent.parent.parent.parent.out_, c: root.accent },
{ label: "MSG IN", val: parent.parent.parent.parent.in_, c: root.blue }]
Row {
spacing: 4
Text {
font.family: root.monoFont; font.pixelSize: 10
color: textSecond
text: "MSG OUT"
}
Text { font.family: root.monoFont; font.pixelSize: 9; color: root.textSecond; text: modelData.label }
Rectangle {
width: 28; height: 18; radius: 3
color: out_ > 0 ? root.accent : root.bgPanel
border.color: out_ > 0 ? root.accent : root.border
Text {
anchors.centerIn: parent
font.family: root.monoFont; font.pixelSize: 11; font.bold: true
color: out_ > 0 ? "#000" : root.textTertiary
text: out_
}
width: 32; height: 20; radius: 4
color: modelData.val > 0 ? modelData.c : root.bgPanel
border.color: modelData.val > 0 ? modelData.c : root.border
Text { anchors.centerIn: parent; font.family: root.monoFont; font.pixelSize: 12; font.bold: true
color: modelData.val > 0 ? "#000" : root.textTertiary; text: modelData.val }
}
}
Row {
spacing: 4
Text {
font.family: root.monoFont; font.pixelSize: 10
color: textSecond
text: "MSG IN"
}
Rectangle {
width: 28; height: 18; radius: 3
color: in_ > 0 ? root.accent : root.bgPanel
border.color: in_ > 0 ? root.accent : root.border
Text {
anchors.centerIn: parent
font.family: root.monoFont; font.pixelSize: 11; font.bold: true
color: in_ > 0 ? "#000" : root.textTertiary
text: in_
}
}
}
// Live activity indicator
Rectangle {
id: activityDot
width: 8; height: 8; radius: 4
color: (out_ > 0 || in_ > 0) ? root.accent : root.textTertiary
opacity: activityAnim.running ? 1.0 : 0.3
SequentialAnimation on opacity {
id: activityAnim
running: false
loops: 3
NumberAnimation { to: 1.0; duration: 150 }
NumberAnimation { to: 0.3; duration: 300 }
}
Connections {
target: monitor
function onStateChanged() {
var prevOut = activityDot._lastOut || 0
var prevIn = activityDot._lastIn || 0
if (out_ !== prevOut || in_ !== prevIn) {
activityAnim.restart()
}
activityDot._lastOut = out_
activityDot._lastIn = in_
}
}
property int _lastOut: 0
property int _lastIn: 0
}
}
}
}
}
Item { Layout.fillHeight: true }
// Message flow arrows
Item {
Layout.preferredWidth: 60
Layout.fillHeight: true
ColumnLayout {
anchors.centerIn: parent
spacing: 8
// Forward arrow (sender receiver)
Text {
font.family: root.monoFont; font.pixelSize: 16; font.bold: true
color: monitor.senderMsgOut > 0 ? root.accent : root.border
text: "→"
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
SequentialAnimation on opacity {
id: fwdArrowAnim; loops: 1; running: false
NumberAnimation { to: 1.0; duration: 100 }
NumberAnimation { to: 0.5; duration: 400 }
}
opacity: monitor.senderMsgOut > 0 ? 1.0 : 0.3
}
Text {
font.family: root.monoFont; font.pixelSize: 9; color: root.textSecond
text: monitor.senderMsgOut > 0 ? monitor.senderMsgOut + " sent" : ""
Layout.alignment: Qt.AlignHCenter
}
Text {
font.family: root.monoFont; font.pixelSize: 9; color: root.textSecond
text: monitor.receiverMsgIn > 0 ? monitor.receiverMsgIn + " recv" : ""
Layout.alignment: Qt.AlignHCenter
}
// Reverse arrow (receiver sender)
Text {
font.family: root.monoFont; font.pixelSize: 16; font.bold: true
color: monitor.senderMsgIn > 0 ? root.blue : root.border
text: "←"
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
opacity: monitor.senderMsgIn > 0 ? 1.0 : 0.3
}
}
}
// Receiver panel
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: bgSecondary
radius: 6
clip: true
property string role: "receiver"
property string phase: monitor.receiverPhase
property int optLeaf: monitor.receiverOptLeaf
property int authLeaf: monitor.receiverAuthLeaf
property bool corrected: monitor.receiverLeafCorrected
property int peers: monitor.receiverPeers
property bool mixRdy: monitor.receiverMixReady
property int pool: monitor.receiverMixPool
property int out_: monitor.receiverMsgOut
property int in_: monitor.receiverMsgIn
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 8
RowLayout {
spacing: 8
Text { font.family: root.monoFont; font.pixelSize: 16; font.bold: true; color: root.textPrimary; text: "RECEIVER" }
Rectangle {
width: rLabel.implicitWidth + 12; height: 20; radius: 10
color: monitor.receiverPhase === "---" ? root.textTertiary :
(monitor.receiverPhase === "msg_received" ? root.accent :
(monitor.receiverPhase.indexOf("conf") >= 0 ? root.blue : root.yellow))
Text { id: rLabel; anchors.centerIn: parent; font.family: root.monoFont; font.pixelSize: 9; font.bold: true; color: "#FFF"
text: monitor.receiverPhase === "---" ? "WAITING" : monitor.receiverPhase.toUpperCase() }
}
}
Row {
spacing: 0
Repeater {
model: ["init", "start", "reg", "opt", "conf", "ready", "accept", "recv"]
Row {
spacing: 0
property var allPhases: ["init", "start", "request", "opt", "conf", "ready", "intro_accepted", "msg_received"]
property int currentIdx: allPhases.indexOf(monitor.receiverPhase.split(":")[0])
Rectangle {
width: 10; height: 10; radius: 5
color: index < currentIdx ? root.accent : (index === currentIdx ? root.yellow : root.border)
border.color: index <= currentIdx ? Qt.lighter(color, 1.3) : "transparent"; border.width: 1
scale: index === currentIdx ? 1.3 : 1.0
Behavior on scale { NumberAnimation { duration: 200 } }
}
Rectangle {
visible: index < 7
width: 16; height: 2; color: index < currentIdx ? root.accent : root.border
anchors.verticalCenter: parent.children[0].verticalCenter
}
}
}
}
Text { font.family: root.monoFont; font.pixelSize: 10; color: parent.parent.corrected ? root.yellow : (parent.parent.optLeaf >= 0 && parent.parent.optLeaf === parent.parent.authLeaf ? root.accent : root.textSecond)
text: "RLN " + (parent.parent.authLeaf >= 0 ? "leaf " + parent.parent.authLeaf + " ✓" : (parent.parent.optLeaf >= 0 ? "leaf " + parent.parent.optLeaf + " ⏳" : "not registered")) }
Text { font.family: root.monoFont; font.pixelSize: 10; color: parent.parent.mixRdy ? root.accent : root.textSecond
text: "NET " + parent.parent.peers + " peers" + (parent.parent.mixRdy ? " · mix ✓ pool " + parent.parent.pool : "") }
Item { Layout.fillHeight: true }
RowLayout {
spacing: 12
Repeater {
model: [{ label: "MSG OUT", val: parent.parent.parent.parent.out_, c: root.accent },
{ label: "MSG IN", val: parent.parent.parent.parent.in_, c: root.blue }]
Row {
spacing: 4
Text { font.family: root.monoFont; font.pixelSize: 9; color: root.textSecond; text: modelData.label }
Rectangle {
width: 32; height: 20; radius: 4
color: modelData.val > 0 ? modelData.c : root.bgPanel
border.color: modelData.val > 0 ? modelData.c : root.border
Text { anchors.centerIn: parent; font.family: root.monoFont; font.pixelSize: 12; font.bold: true
color: modelData.val > 0 ? "#000" : root.textTertiary; text: modelData.val }
}
}
}
}
}
}
}
// CHAT HOST PANEL (only in host mode)
//
// CHAIN EVENTS (with icons)
//
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: hostModeEnabled ? 200 : 0
visible: hostModeEnabled
Layout.preferredHeight: 160
color: bgSecondary
radius: 4
radius: 6
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 4
Text {
font.family: root.monoFont; font.pixelSize: 13; font.bold: true
color: textPrimary
text: "CHAT HOST — " + (chatHost ? chatHost.phase : "---")
}
RowLayout {
spacing: 8
Button {
text: "Initialize"
enabled: chatHost && !chatHost.initialized
font.family: root.monoFont; font.pixelSize: 11
onClicked: {
var cfg = chatHost.readConfigFile(
monitor.stateDir + "/chat_sender_config.json")
if (!cfg) cfg = chatHost.buildConfigFromEnv()
chatHost.initChat(cfg)
}
background: Rectangle {
color: parent.enabled
? (parent.pressed ? root.accentPress : parent.hovered ? root.accentHover : root.accent)
: root.textTertiary
radius: 3
}
contentItem: Text { text: parent.text; color: "#000"; font: parent.font; horizontalAlignment: Text.AlignHCenter }
}
Button {
text: "Start"
enabled: chatHost && chatHost.initialized && !chatHost.started
font.family: root.monoFont; font.pixelSize: 11
onClicked: chatHost.startChat()
background: Rectangle {
color: parent.enabled
? (parent.pressed ? root.accentPress : parent.hovered ? root.accentHover : root.accent)
: root.textTertiary
radius: 3
}
contentItem: Text { text: parent.text; color: "#000"; font: parent.font; horizontalAlignment: Text.AlignHCenter }
}
Button {
text: "Create Bundle"
enabled: chatHost && chatHost.started
font.family: root.monoFont; font.pixelSize: 11
onClicked: chatHost.createIntroBundle()
background: Rectangle {
color: parent.enabled
? (parent.pressed ? root.accentPress : parent.hovered ? root.accentHover : root.accent)
: root.textTertiary
radius: 3
}
contentItem: Text { text: parent.text; color: "#000"; font: parent.font; horizontalAlignment: Text.AlignHCenter }
}
}
// Intro bundle display
TextField {
Layout.fillWidth: true
visible: chatHost && chatHost.introBundle.length > 0
text: chatHost ? chatHost.introBundle : ""
readOnly: true
selectByMouse: true
font.family: root.monoFont; font.pixelSize: 10
color: textPrimary
background: Rectangle { color: root.bgPanel; border.color: root.border; radius: 3 }
}
// Send conversation row
RowLayout {
spacing: 4
TextField {
id: bundleInput
Layout.fillWidth: true
placeholderText: "Paste intro bundle..."
font.family: root.monoFont; font.pixelSize: 11
color: textPrimary
background: Rectangle { color: root.bgPanel; border.color: bundleInput.activeFocus ? root.accent : root.border; radius: 3 }
}
TextField {
id: msgInput
Layout.preferredWidth: 200
placeholderText: "Message..."
font.family: root.monoFont; font.pixelSize: 11
color: textPrimary
background: Rectangle { color: root.bgPanel; border.color: msgInput.activeFocus ? root.accent : root.border; radius: 3 }
}
Button {
text: "Send"
enabled: chatHost && chatHost.started && msgInput.text.length > 0
font.family: root.monoFont; font.pixelSize: 11
onClicked: {
if (bundleInput.text.length > 0 && (!chatHost.currentConvId || chatHost.currentConvId.length === 0)) {
chatHost.newConversation(bundleInput.text, msgInput.text)
bundleInput.text = ""
} else if (chatHost.currentConvId && chatHost.currentConvId.length > 0) {
chatHost.sendMessage(chatHost.currentConvId, msgInput.text)
}
msgInput.text = ""
}
background: Rectangle {
color: parent.enabled
? (parent.pressed ? root.accentPress : parent.hovered ? root.accentHover : root.accent)
: root.textTertiary
radius: 3
}
contentItem: Text { text: parent.text; color: "#000"; font: parent.font; horizontalAlignment: Text.AlignHCenter }
}
}
Text {
font.family: root.monoFont; font.pixelSize: 11
color: textSecond
text: "out:" + (chatHost ? chatHost.messagesSent : 0) + " in:" + (chatHost ? chatHost.messagesReceived : 0) +
(chatHost && chatHost.currentConvId ? " conv:" + chatHost.currentConvId.substring(0,6) : "")
}
}
}
// CHAIN EVENTS
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 180
color: bgSecondary
radius: 4
ColumnLayout {
anchors.fill: parent
anchors.margins: 6
spacing: 2
Text {
font.family: root.monoFont; font.pixelSize: 11; font.bold: true
color: textSecond
text: "CHAIN EVENTS"
}
Text { font.family: root.monoFont; font.pixelSize: 11; font.bold: true; color: root.textSecond; text: "CHAIN EVENTS" }
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
model: chainEvents
clip: true
spacing: 1
delegate: Text {
delegate: Rectangle {
width: ListView.view.width
font.family: root.monoFont
font.pixelSize: 11
color: {
if (eventType === "TX_FAIL" || eventType === "WALLET_ERR" || eventType === "REG_FAIL")
return root.red
if (eventType === "REGISTER") return root.accent
if (eventType === "LEAF_FIX") return root.yellow
return root.textSecond
height: 16
color: "transparent"
radius: 2
// Brief flash on new events
Rectangle {
anchors.fill: parent; radius: 2; color: root.accent; opacity: flashAnim.running ? 0.15 : 0
SequentialAnimation on opacity { id: flashAnim; running: index === 0; loops: 1
NumberAnimation { to: 0.2; duration: 100 }
NumberAnimation { to: 0; duration: 500 }
}
}
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: 6
// Icon
Text {
font.pixelSize: 10
text: {
if (eventType === "TX_OK") return "✓"
if (eventType === "TX_FAIL" || eventType === "WALLET_ERR" || eventType === "REG_FAIL") return "✗"
if (eventType === "REGISTER" || eventType === "GIFTER_REQ") return "◆"
if (eventType === "ROOTS") return "◈"
if (eventType === "BUNDLE") return "◉"
if (eventType === "LEAF_FIX") return "⚠"
if (eventType === "GIFTER_AUTHFAIL") return "⛔"
return "·"
}
color: {
if (eventType === "TX_FAIL" || eventType === "WALLET_ERR" || eventType === "REG_FAIL" || eventType === "GIFTER_AUTHFAIL") return root.red
if (eventType === "REGISTER") return root.accent
if (eventType === "LEAF_FIX") return root.yellow
if (eventType === "BUNDLE") return root.blue
return root.textTertiary
}
}
Text {
font.family: root.monoFont; font.pixelSize: 10
color: root.textTertiary
text: timestamp
}
Text {
font.family: root.monoFont; font.pixelSize: 10; font.bold: true
color: {
if (eventType === "TX_FAIL" || eventType === "WALLET_ERR" || eventType === "REG_FAIL") return root.red
if (eventType === "REGISTER") return root.accent
if (eventType === "LEAF_FIX") return root.yellow
return root.textSecond
}
text: eventType
}
Text {
font.family: root.monoFont; font.pixelSize: 10
color: root.textSecond
text: detail
elide: Text.ElideRight
}
}
text: timestamp + " " + eventType + " " + detail
elide: Text.ElideRight
}
}
}