feat: add sim-monitor Qt6/QML dashboard for live simulation monitoring

Standalone Qt6 app that tails the 7 sim log files (.sim_state/) and
renders a live dashboard: sequencer block counter + RPC health probe,
4 mix node dots (grey/yellow/green), gifter status, sender/receiver
chat phase progression, and scrollable chain event stream.

Supports both old (EVENT: stderr) and lgx ([timestamp] [out] [module])
log formats. Handles file rotation on --fresh restart.

Optional --host-chat mode links against liblogos_core for driving a
chat instance via UI controls (Initialize/Start/Bundle/Send).

Build: cd sim-monitor && nix develop --command bash -c \
  "cmake -B build -GNinja && cmake --build build"
Run:   ./build/sim-monitor --state-dir .sim_state --rpc-url http://127.0.0.1:3040
This commit is contained in:
Arseniy Klempner 2026-06-03 16:08:15 -06:00
parent 6a90c01668
commit 052cb11f00
No known key found for this signature in database
GPG Key ID: 51653F18863BD24B
18 changed files with 2732 additions and 0 deletions

View File

@ -0,0 +1,60 @@
cmake_minimum_required(VERSION 3.16)
project(sim-monitor LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
option(ENABLE_HOST_MODE "Enable chat host mode (requires liblogos_core)" OFF)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick Network)
set(MONITOR_SOURCES
src/main.cpp
src/MonitorBackend.h src/MonitorBackend.cpp
src/LogTailer.h src/LogTailer.cpp
src/LogParser.h src/LogParser.cpp
src/ChainEventModel.h
src/RpcClient.h src/RpcClient.cpp
)
if(ENABLE_HOST_MODE)
list(APPEND MONITOR_SOURCES src/ChatHost.h src/ChatHost.cpp)
endif()
qt_add_executable(sim-monitor ${MONITOR_SOURCES})
qt_add_qml_module(sim-monitor
URI SimMonitor
VERSION 1.0
QML_FILES src/qml/MonitorView.qml
NO_RESOURCE_TARGET_PATH
)
target_link_libraries(sim-monitor PRIVATE
Qt6::Core Qt6::Gui Qt6::Qml Qt6::Quick Qt6::Network
)
if(ENABLE_HOST_MODE)
target_compile_definitions(sim-monitor PRIVATE ENABLE_HOST_MODE=1)
# liblogos_core from LIBLOGOS_LIB / LIBLOGOS_INCLUDE env vars (set by nix flake)
if(DEFINED ENV{LIBLOGOS_LIB})
target_link_directories(sim-monitor PRIVATE $ENV{LIBLOGOS_LIB})
target_link_libraries(sim-monitor PRIVATE logos_core)
else()
message(WARNING "LIBLOGOS_LIB not set — host mode will fail to link")
endif()
if(DEFINED ENV{LIBLOGOS_INCLUDE})
target_include_directories(sim-monitor PRIVATE $ENV{LIBLOGOS_INCLUDE})
else()
message(WARNING "LIBLOGOS_INCLUDE not set — host mode headers not found")
endif()
find_package(Qt6 COMPONENTS RemoteObjects)
if(Qt6RemoteObjects_FOUND)
target_link_libraries(sim-monitor PRIVATE Qt6::RemoteObjects)
endif()
endif()

View File

@ -0,0 +1,741 @@
{
"nodes": {
"logos-capability-module": {
"inputs": {
"logos-cpp-sdk": "logos-cpp-sdk",
"logos-module": "logos-module",
"logos-nix": "logos-nix_3",
"nixpkgs": [
"logos-liblogos",
"logos-capability-module",
"logos-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1774455138,
"narHash": "sha256-szx2dnnY9MP1NpdBnR8E2DRSz9CtQlo/6698zgJcAEM=",
"owner": "logos-co",
"repo": "logos-capability-module",
"rev": "0655be68e0078bede0682bb6a5b53330dac37a72",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-capability-module",
"type": "github"
}
},
"logos-cpp-sdk": {
"inputs": {
"logos-nix": "logos-nix",
"nixpkgs": [
"logos-liblogos",
"logos-capability-module",
"logos-cpp-sdk",
"logos-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1773956385,
"narHash": "sha256-CV0Lo1FrosBt/MSP+GWQGWXnYobxRGXGOREylNuwZ58=",
"owner": "logos-co",
"repo": "logos-cpp-sdk",
"rev": "4b66dac015e4b977d33cfae80a4c8e1d518679f3",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-cpp-sdk",
"type": "github"
}
},
"logos-cpp-sdk_2": {
"inputs": {
"logos-nix": "logos-nix_4",
"nixpkgs": [
"logos-liblogos",
"logos-cpp-sdk",
"logos-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1778167634,
"narHash": "sha256-gbBYvyEDxBHF8iACAE/dFcljBkIUTWr1rP3gBLj62JI=",
"owner": "logos-co",
"repo": "logos-cpp-sdk",
"rev": "25c88f4d48fa95ea4437194bcf60bd8d0cf84a74",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-cpp-sdk",
"rev": "25c88f4d48fa95ea4437194bcf60bd8d0cf84a74",
"type": "github"
}
},
"logos-liblogos": {
"inputs": {
"logos-capability-module": "logos-capability-module",
"logos-cpp-sdk": "logos-cpp-sdk_2",
"logos-module": "logos-module_2",
"logos-nix": "logos-nix_6",
"logos-package-manager": "logos-package-manager",
"nixpkgs": [
"logos-liblogos",
"logos-nix",
"nixpkgs"
],
"process-stats": "process-stats"
},
"locked": {
"lastModified": 1778168472,
"narHash": "sha256-Td7Um8HOx4hG6k8ky3+bTsmgA9bgspI3eDs9LzVWv2s=",
"owner": "logos-co",
"repo": "logos-liblogos",
"rev": "94af58c819038e0eb5c2003f69d3260d964aa8f3",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-liblogos",
"rev": "94af58c819038e0eb5c2003f69d3260d964aa8f3",
"type": "github"
}
},
"logos-module": {
"inputs": {
"logos-nix": "logos-nix_2",
"nixpkgs": [
"logos-liblogos",
"logos-capability-module",
"logos-module",
"logos-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1773963329,
"narHash": "sha256-zdvDHoYWQDse0eJ/UCKIJcfuYJ8NMgl6QfxRcyDEovI=",
"owner": "logos-co",
"repo": "logos-module",
"rev": "ac5a4f06ea94b01dd9c5fbb9ed4f20620beab88d",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-module",
"type": "github"
}
},
"logos-module_2": {
"inputs": {
"logos-nix": "logos-nix_5",
"nixpkgs": [
"logos-liblogos",
"logos-module",
"logos-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1776369033,
"narHash": "sha256-ehePoUEd/u3Ng0TvCmjocXYJWWH6P61PA7tNpgV59lo=",
"owner": "logos-co",
"repo": "logos-module",
"rev": "194778491ef4dd36967ac40cc2fec6b8a8b1d660",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-module",
"type": "github"
}
},
"logos-nix": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1773955630,
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
"owner": "logos-co",
"repo": "logos-nix",
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-nix",
"type": "github"
}
},
"logos-nix_10": {
"inputs": {
"nixpkgs": "nixpkgs_10"
},
"locked": {
"lastModified": 1773955630,
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
"owner": "logos-co",
"repo": "logos-nix",
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-nix",
"type": "github"
}
},
"logos-nix_11": {
"inputs": {
"nixpkgs": "nixpkgs_11"
},
"locked": {
"lastModified": 1773955630,
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
"owner": "logos-co",
"repo": "logos-nix",
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-nix",
"type": "github"
}
},
"logos-nix_12": {
"inputs": {
"nixpkgs": "nixpkgs_12"
},
"locked": {
"lastModified": 1773955630,
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
"owner": "logos-co",
"repo": "logos-nix",
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-nix",
"type": "github"
}
},
"logos-nix_2": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1773955630,
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
"owner": "logos-co",
"repo": "logos-nix",
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-nix",
"type": "github"
}
},
"logos-nix_3": {
"inputs": {
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1773955630,
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
"owner": "logos-co",
"repo": "logos-nix",
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-nix",
"type": "github"
}
},
"logos-nix_4": {
"inputs": {
"nixpkgs": "nixpkgs_4"
},
"locked": {
"lastModified": 1774455309,
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
"owner": "logos-co",
"repo": "logos-nix",
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-nix",
"type": "github"
}
},
"logos-nix_5": {
"inputs": {
"nixpkgs": "nixpkgs_5"
},
"locked": {
"lastModified": 1773955630,
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
"owner": "logos-co",
"repo": "logos-nix",
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-nix",
"type": "github"
}
},
"logos-nix_6": {
"inputs": {
"nixpkgs": "nixpkgs_6"
},
"locked": {
"lastModified": 1774455309,
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
"owner": "logos-co",
"repo": "logos-nix",
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-nix",
"type": "github"
}
},
"logos-nix_7": {
"inputs": {
"nixpkgs": "nixpkgs_7"
},
"locked": {
"lastModified": 1774455309,
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
"owner": "logos-co",
"repo": "logos-nix",
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-nix",
"type": "github"
}
},
"logos-nix_8": {
"inputs": {
"nixpkgs": "nixpkgs_8"
},
"locked": {
"lastModified": 1773955630,
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
"owner": "logos-co",
"repo": "logos-nix",
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-nix",
"type": "github"
}
},
"logos-nix_9": {
"inputs": {
"nixpkgs": "nixpkgs_9"
},
"locked": {
"lastModified": 1773955630,
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
"owner": "logos-co",
"repo": "logos-nix",
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-nix",
"type": "github"
}
},
"logos-package": {
"inputs": {
"logos-nix": "logos-nix_8",
"nixpkgs": [
"logos-liblogos",
"logos-package-manager",
"logos-package",
"logos-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1775835037,
"narHash": "sha256-Cti0DhkzyLQs98BSzcHWMLtGXpa3n+R+5upfSw6vKdQ=",
"owner": "logos-co",
"repo": "logos-package",
"rev": "ff93a0df15ceab255f27687d22d962ea2737efbe",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-package",
"type": "github"
}
},
"logos-package-manager": {
"inputs": {
"logos-nix": "logos-nix_7",
"logos-package": "logos-package",
"nix-bundle-appimage": "nix-bundle-appimage",
"nix-bundle-dir": "nix-bundle-dir_2",
"nixpkgs": [
"logos-liblogos",
"logos-package-manager",
"logos-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1776374462,
"narHash": "sha256-HMkuqSLdScAWTwXEWjhqx9Yk82GiPzPIfRaHTvjG730=",
"owner": "logos-co",
"repo": "logos-package-manager",
"rev": "9101875bc103214855bc6217834e22e66802ed86",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-package-manager",
"type": "github"
}
},
"nix-bundle-appimage": {
"inputs": {
"logos-nix": "logos-nix_9",
"nix-bundle-dir": "nix-bundle-dir",
"nixpkgs": [
"logos-liblogos",
"logos-package-manager",
"nix-bundle-appimage",
"logos-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1774455478,
"narHash": "sha256-S8IMfdDc+2Wwri0krLDsIUwSqmwanmvHAJWHOFo8ykk=",
"owner": "logos-co",
"repo": "nix-bundle-appimage",
"rev": "2428125a4a1b34ad9119efa97edb98676283e3da",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "nix-bundle-appimage",
"type": "github"
}
},
"nix-bundle-dir": {
"inputs": {
"logos-nix": "logos-nix_10",
"nixpkgs": [
"logos-liblogos",
"logos-package-manager",
"nix-bundle-appimage",
"nixpkgs"
]
},
"locked": {
"lastModified": 1773961179,
"narHash": "sha256-bpaTvz//R8WFP5xnnDLv3a9l7unDmBwJjCewx3wCjzM=",
"owner": "logos-co",
"repo": "nix-bundle-dir",
"rev": "cd214dbf15487d80967389847ae2210468be6ebf",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "nix-bundle-dir",
"type": "github"
}
},
"nix-bundle-dir_2": {
"inputs": {
"logos-nix": "logos-nix_11",
"nixpkgs": [
"logos-liblogos",
"logos-package-manager",
"nix-bundle-dir",
"logos-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1774455641,
"narHash": "sha256-HrVJguPxhIoZMCH+x8Wooa0tE6slUhgNOU6P89t2uQc=",
"owner": "logos-co",
"repo": "nix-bundle-dir",
"rev": "3d155cab09051703a0b02ff2de166a53c30cbca8",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "nix-bundle-dir",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_10": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_11": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_12": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_13": {
"locked": {
"lastModified": 1780336545,
"narHash": "sha256-vhVhuXzFrIOfcssC/9hDHx7MHzDKjF3keHuREOQqQiQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4df1b885d76a54e1aa1a318f8d16fd6005b6401f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_6": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_7": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_8": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_9": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"process-stats": {
"inputs": {
"logos-nix": "logos-nix_12",
"nixpkgs": [
"logos-liblogos",
"process-stats",
"logos-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1775744159,
"narHash": "sha256-hTVAnDREBQOVHML6KU3K7Ge0CRBqnFIg7uYL7qDnD8o=",
"owner": "logos-co",
"repo": "process-stats",
"rev": "33ace1270f90c89b3565e803139c0970fcd1ce8f",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "process-stats",
"type": "github"
}
},
"root": {
"inputs": {
"logos-liblogos": "logos-liblogos",
"nixpkgs": "nixpkgs_13"
}
}
},
"root": "root",
"version": 7
}

View File

@ -0,0 +1,64 @@
{
description = "Simulation Monitor Qt/QML desktop app with optional chat host mode";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
logos-liblogos = {
url = "github:logos-co/logos-liblogos/94af58c819038e0eb5c2003f69d3260d964aa8f3";
inputs.logos-cpp-sdk.url = "github:logos-co/logos-cpp-sdk/25c88f4d48fa95ea4437194bcf60bd8d0cf84a74";
};
};
outputs = { self, nixpkgs, logos-liblogos }:
let
systems = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ];
forAll = nixpkgs.lib.genAttrs systems;
in {
packages = forAll (system:
let pkgs = nixpkgs.legacyPackages.${system};
in {
default = pkgs.stdenv.mkDerivation {
pname = "sim-monitor";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [ pkgs.cmake pkgs.qt6.wrapQtAppsHook ];
buildInputs = [
pkgs.qt6.qtbase
pkgs.qt6.qtdeclarative
];
cmakeFlags = [ "-DCMAKE_BUILD_TYPE=Release" ];
};
});
devShells = forAll (system:
let
pkgs = nixpkgs.legacyPackages.${system};
liblogosLib = logos-liblogos.packages.${system}.logos-liblogos-lib;
liblogosInclude = logos-liblogos.packages.${system}.logos-liblogos-include;
liblogosBin = logos-liblogos.packages.${system}.logos-liblogos-bin;
in {
default = pkgs.mkShell {
nativeBuildInputs = [ pkgs.qt6.wrapQtAppsHook ];
packages = [
pkgs.cmake
pkgs.ninja
pkgs.qt6.qtbase
pkgs.qt6.qtdeclarative
pkgs.qt6.qtshadertools
pkgs.qt6.qtremoteobjects
];
shellHook = ''
export QML2_IMPORT_PATH="${pkgs.qt6.qtdeclarative}/lib/qt-6/qml"
export LIBLOGOS_LIB="${liblogosLib}/lib"
export LIBLOGOS_INCLUDE="${liblogosInclude}/include"
export LOGOS_HOST_BIN="${liblogosBin}/bin/logos_host"
echo "sim-monitor dev shell"
echo " LIBLOGOS_LIB=$LIBLOGOS_LIB"
echo " LIBLOGOS_INCLUDE=$LIBLOGOS_INCLUDE"
echo " LOGOS_HOST_BIN=$LOGOS_HOST_BIN"
echo " cmake -B build -GNinja -DENABLE_HOST_MODE=ON && cmake --build build"
'';
};
});
};
}

View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STATE_DIR="${1:-$SCRIPT_DIR/.sim_state}"
if [ ! -f "$SCRIPT_DIR/build/sim-monitor" ]; then
echo "Building sim-monitor..."
(cd "$SCRIPT_DIR" && nix develop --command bash -c "cmake -B build -GNinja && cmake --build build")
fi
exec nix develop "$SCRIPT_DIR" --command "$SCRIPT_DIR/build/sim-monitor" --state-dir "$STATE_DIR"

View File

@ -0,0 +1,50 @@
#pragma once
#include <QAbstractListModel>
#include <QVector>
#include <QString>
class ChainEventModel : public QAbstractListModel {
Q_OBJECT
public:
enum Roles { TimestampRole = Qt::UserRole + 1, EventTypeRole, DetailRole };
explicit ChainEventModel(QObject* parent = nullptr) : QAbstractListModel(parent) {}
int rowCount(const QModelIndex& = {}) const override { return m_items.size(); }
QVariant data(const QModelIndex& index, int role) const override {
if (!index.isValid() || index.row() >= m_items.size()) return {};
const auto& item = m_items[index.row()];
switch (role) {
case TimestampRole: return item.timestamp;
case EventTypeRole: return item.eventType;
case DetailRole: return item.detail;
default: return {};
}
}
QHash<int, QByteArray> roleNames() const override {
return {{TimestampRole, "timestamp"}, {EventTypeRole, "eventType"}, {DetailRole, "detail"}};
}
void prepend(const QString& timestamp, const QString& eventType, const QString& detail) {
if (m_items.size() >= 200) {
beginRemoveRows({}, m_items.size() - 1, m_items.size() - 1);
m_items.removeLast();
endRemoveRows();
}
beginInsertRows({}, 0, 0);
m_items.prepend({timestamp, eventType, detail});
endInsertRows();
}
void clear() {
beginResetModel();
m_items.clear();
endResetModel();
}
private:
struct Item { QString timestamp; QString eventType; QString detail; };
QVector<Item> m_items;
};

View File

@ -0,0 +1,288 @@
#include "ChatHost.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QCoreApplication>
#include <QDir>
#include <QDebug>
#ifdef ENABLE_HOST_MODE
#include <logos_core.h>
#include <logos_api.h>
#include <logos_api_client.h>
#endif
ChatHost::ChatHost(QObject* parent) : QObject(parent) {}
ChatHost::~ChatHost() {
#ifdef ENABLE_HOST_MODE
if (m_modulesLoaded) {
logos_core_cleanup();
}
delete static_cast<LogosAPI*>(m_logosAPI);
#endif
}
bool ChatHost::loadModules(const QString& modulesDir, const QString& dataDir) {
#ifdef ENABLE_HOST_MODE
logos_core_add_modules_dir(modulesDir.toUtf8().constData());
logos_core_set_persistence_base_path(dataDir.toUtf8().constData());
logos_core_start();
const char* modules[] = {
"capability_module",
"logos_execution_zone",
"liblogos_rln_module",
"chat_module",
};
for (const char* mod : modules) {
emit logLine(QStringLiteral("Loading module: %1").arg(QString::fromUtf8(mod)));
int rc = logos_core_load_module_with_dependencies(mod);
if (rc != 1) {
emit logLine(QStringLiteral("ERROR: Failed to load module: %1 (rc=%2)")
.arg(QString::fromUtf8(mod)).arg(rc));
return false;
}
emit logLine(QStringLiteral("Loaded: %1").arg(QString::fromUtf8(mod)));
}
m_modulesLoaded = true;
auto* api = new LogosAPI("sim_monitor", this);
m_logosAPI = api;
m_chatClient = api->getClient("chat_module");
if (!m_chatClient) {
emit logLine("ERROR: Could not get chat_module client");
return false;
}
setupEventHandlers();
emit logLine("Chat host ready — modules loaded, event handlers wired");
return true;
#else
Q_UNUSED(modulesDir); Q_UNUSED(dataDir);
emit logLine("ERROR: Host mode not compiled (ENABLE_HOST_MODE=OFF)");
return false;
#endif
}
void ChatHost::setupEventHandlers() {
#ifdef ENABLE_HOST_MODE
if (!m_chatClient) return;
auto* client = static_cast<LogosAPIClient*>(m_chatClient);
auto* chatObj = client->requestObject("chat_module");
if (!chatObj) {
emit logLine("WARNING: Could not request chat_module object for events");
return;
}
client->onEvent(chatObj, "chatInitResult",
[this](const QString&, const QVariantList& args) { onChatInitResult(args); });
client->onEvent(chatObj, "chatStartResult",
[this](const QString&, const QVariantList& args) { onChatStartResult(args); });
client->onEvent(chatObj, "chatCreateIntroBundleResult",
[this](const QString&, const QVariantList& args) { onIntroBundleResult(args); });
client->onEvent(chatObj, "chatNewConversation",
[this](const QString&, const QVariantList& args) { onNewConversation(args); });
client->onEvent(chatObj, "chatNewMessage",
[this](const QString&, const QVariantList& args) { onNewMessage(args); });
client->onEvent(chatObj, "chatSendMessageResult",
[this](const QString&, const QVariantList& args) { onSendResult(args); });
client->onEvent(chatObj, "chatNewPrivateConversationResult",
[this](const QString&, const QVariantList& args) { onNewPrivateConvResult(args); });
emit logLine("Event handlers wired for chat_module");
#endif
}
void ChatHost::initChat(const QString& configJson) {
#ifdef ENABLE_HOST_MODE
if (!m_chatClient) return;
auto* client = static_cast<LogosAPIClient*>(m_chatClient);
emit logLine("initChat: calling...");
QVariant result = client->invokeRemoteMethod("chat_module", "initChat", configJson);
emit logLine(QStringLiteral("initChat: result=%1").arg(result.toString()));
#else
Q_UNUSED(configJson);
#endif
}
void ChatHost::startChat() {
#ifdef ENABLE_HOST_MODE
if (!m_chatClient) return;
auto* client = static_cast<LogosAPIClient*>(m_chatClient);
emit logLine("startChat: calling...");
client->invokeRemoteMethod("chat_module", "startChat");
client->invokeRemoteMethod("chat_module", "setEventCallback");
emit logLine("startChat + setEventCallback: done");
#endif
}
void ChatHost::createIntroBundle() {
#ifdef ENABLE_HOST_MODE
if (!m_chatClient) return;
auto* client = static_cast<LogosAPIClient*>(m_chatClient);
emit logLine("createIntroBundle: calling...");
client->invokeRemoteMethod("chat_module", "createIntroBundle");
#endif
}
void ChatHost::newConversation(const QString& introBundle, const QString& firstMessage) {
#ifdef ENABLE_HOST_MODE
if (!m_chatClient) return;
auto* client = static_cast<LogosAPIClient*>(m_chatClient);
QString msgHex = toHex(firstMessage);
emit logLine("newPrivateConversation: calling...");
client->invokeRemoteMethod("chat_module", "newPrivateConversation",
QVariant(introBundle), QVariant(msgHex));
#else
Q_UNUSED(introBundle); Q_UNUSED(firstMessage);
#endif
}
void ChatHost::sendMessage(const QString& convId, const QString& message) {
#ifdef ENABLE_HOST_MODE
if (!m_chatClient) return;
auto* client = static_cast<LogosAPIClient*>(m_chatClient);
QString msgHex = toHex(message);
client->invokeRemoteMethod("chat_module", "sendMessage",
QVariant(convId), QVariant(msgHex));
#else
Q_UNUSED(convId); Q_UNUSED(message);
#endif
}
// ─── Event handlers ───
void ChatHost::onChatInitResult(const QVariantList& args) {
if (args.size() > 0 && args[0].toBool()) {
m_initialized = true;
m_phase = "init";
emit logLine("EVENT: chatInitResult → initialized");
emit stateChanged();
}
emit chatEvent("chatInitResult", args);
}
void ChatHost::onChatStartResult(const QVariantList& args) {
if (args.size() > 0 && args[0].toBool()) {
m_started = true;
m_phase = "start";
emit logLine("EVENT: chatStartResult → started");
emit stateChanged();
}
emit chatEvent("chatStartResult", args);
}
void ChatHost::onIntroBundleResult(const QVariantList& args) {
if (args.size() > 1) {
m_introBundle = args[1].toString();
m_phase = "intro_emitted";
emit logLine("EVENT: introBundleResult → " + m_introBundle.left(30) + "...");
emit stateChanged();
}
emit chatEvent("chatCreateIntroBundleResult", args);
}
void ChatHost::onNewConversation(const QVariantList& args) {
if (args.size() > 0) {
m_currentConvId = args[0].toString();
emit logLine("EVENT: newConversation → " + m_currentConvId.left(10));
emit stateChanged();
}
emit chatEvent("chatNewConversation", args);
}
void ChatHost::onNewMessage(const QVariantList& args) {
++m_messagesReceived;
if (m_messagesReceived == 1) m_phase = "msg_received";
QString body;
if (args.size() > 2) body = fromHex(args[2].toString());
emit logLine(QStringLiteral("EVENT: chatNewMessage #%1: %2")
.arg(m_messagesReceived).arg(body.left(40)));
emit stateChanged();
emit chatEvent("chatNewMessage", args);
}
void ChatHost::onSendResult(const QVariantList& args) {
if (args.size() > 0 && args[0].toBool()) {
++m_messagesSent;
if (m_messagesSent == 1) m_phase = "msg_sent";
emit stateChanged();
}
emit chatEvent("chatSendMessageResult", args);
}
void ChatHost::onNewPrivateConvResult(const QVariantList& args) {
if (args.size() > 1) {
m_currentConvId = args[1].toString();
emit logLine("EVENT: newPrivateConvResult → " + m_currentConvId.left(10));
emit stateChanged();
}
emit chatEvent("chatNewPrivateConversationResult", args);
}
// ─── Config builders ───
QString ChatHost::buildConfigFromEnv() {
QJsonObject config;
auto env = [](const char* name, const QString& def = {}) -> QString {
QString val = qEnvironmentVariable(name);
return val.isEmpty() ? def : val;
};
config["name"] = env("CHAT_NAME", QStringLiteral("monitor_user"));
config["clusterId"] = env("CHAT_CLUSTER_ID", "99").toInt();
config["shardId"] = env("CHAT_SHARD_ID", "0").toInt();
config["port"] = env("CHAT_PORT", "0").toInt();
QString mixNodes = env("CHAT_MIX_NODES");
if (!mixNodes.isEmpty()) {
QJsonArray arr;
for (const auto& n : mixNodes.split(',', Qt::SkipEmptyParts))
arr.append(n.trimmed());
config["mixNodes"] = arr;
config["mixEnabled"] = true;
config["minMixPoolSize"] = env("CHAT_MIN_MIX_POOL_SIZE", "4").toInt();
}
QString staticPeers = env("CHAT_STATIC_PEERS");
if (!staticPeers.isEmpty()) {
QJsonArray arr;
for (const auto& p : staticPeers.split(',', Qt::SkipEmptyParts))
arr.append(p.trimmed());
config["staticPeers"] = arr;
}
QString destPeer = env("CHAT_DEST_PEER_ADDR");
if (!destPeer.isEmpty()) config["destPeerAddr"] = destPeer;
QString gifterNode = env("CHAT_GIFTER_NODE_ADDR");
if (!gifterNode.isEmpty()) config["gifterNodeAddr"] = gifterNode;
QString gifterKey = env("CHAT_GIFTER_AUTH_KEY");
if (!gifterKey.isEmpty()) config["gifterAuthKey"] = gifterKey;
return QJsonDocument(config).toJson(QJsonDocument::Compact);
}
QString ChatHost::readConfigFile(const QString& path) {
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) return {};
return QString::fromUtf8(f.readAll());
}
// ─── Helpers ───
QString ChatHost::toHex(const QString& text) {
return QString::fromLatin1(text.toUtf8().toHex());
}
QString ChatHost::fromHex(const QString& hex) {
return QString::fromUtf8(QByteArray::fromHex(hex.toLatin1()));
}

View File

@ -0,0 +1,70 @@
#pragma once
#include <QObject>
#include <QString>
#include <QStringList>
#include <QVariantList>
class ChatHost : public QObject {
Q_OBJECT
Q_PROPERTY(QString phase READ phase NOTIFY stateChanged)
Q_PROPERTY(bool initialized READ initialized NOTIFY stateChanged)
Q_PROPERTY(bool started READ started NOTIFY stateChanged)
Q_PROPERTY(QString introBundle READ introBundle NOTIFY stateChanged)
Q_PROPERTY(QString currentConvId READ currentConvId NOTIFY stateChanged)
Q_PROPERTY(int messagesReceived READ messagesReceived NOTIFY stateChanged)
Q_PROPERTY(int messagesSent READ messagesSent NOTIFY stateChanged)
public:
explicit ChatHost(QObject* parent = nullptr);
~ChatHost();
bool loadModules(const QString& modulesDir, const QString& dataDir);
Q_INVOKABLE void initChat(const QString& configJson);
Q_INVOKABLE void startChat();
Q_INVOKABLE void createIntroBundle();
Q_INVOKABLE void newConversation(const QString& introBundle, const QString& firstMessage);
Q_INVOKABLE void sendMessage(const QString& convId, const QString& message);
Q_INVOKABLE static QString buildConfigFromEnv();
Q_INVOKABLE static QString readConfigFile(const QString& path);
QString phase() const { return m_phase; }
bool initialized() const { return m_initialized; }
bool started() const { return m_started; }
QString introBundle() const { return m_introBundle; }
QString currentConvId() const { return m_currentConvId; }
int messagesReceived() const { return m_messagesReceived; }
int messagesSent() const { return m_messagesSent; }
signals:
void stateChanged();
void chatEvent(const QString& eventName, const QVariantList& args);
void logLine(const QString& line);
private:
void setupEventHandlers();
void onChatInitResult(const QVariantList& args);
void onChatStartResult(const QVariantList& args);
void onIntroBundleResult(const QVariantList& args);
void onNewConversation(const QVariantList& args);
void onNewMessage(const QVariantList& args);
void onSendResult(const QVariantList& args);
void onNewPrivateConvResult(const QVariantList& args);
static QString toHex(const QString& text);
static QString fromHex(const QString& hex);
QString m_phase = "---";
bool m_initialized = false;
bool m_started = false;
QString m_introBundle;
QString m_currentConvId;
int m_messagesReceived = 0;
int m_messagesSent = 0;
bool m_modulesLoaded = false;
void* m_logosAPI = nullptr; // LogosAPI* (only when ENABLE_HOST_MODE)
void* m_chatClient = nullptr; // LogosAPIClient* (only when ENABLE_HOST_MODE)
};

View File

@ -0,0 +1,240 @@
#include "LogParser.h"
static const QRegularExpression s_ansiRe(QStringLiteral("\\x1b\\[[0-9;]*m"));
// Old sim format: Debug: [LOGOS_HOST "module" ]: "..."
static const QRegularExpression s_hostPrefixRe(
QStringLiteral("Debug: \\[LOGOS_HOST \"[^\"]+\" \\]: \"(.*)\"$"));
// LGX sim format: [timestamp] [out] [module] ...content...
static const QRegularExpression s_lgxPrefixRe(
QStringLiteral("^\\[\\d{4}-\\d{2}-\\d{2} [^\\]]+\\] \\[(?:out|err|info|debug|warn)\\] \\[[^\\]]+\\] (.*)$"));
QString LogParser::stripAnsi(const QString& line) {
QString out = line;
out.remove(s_ansiRe);
return out;
}
QString LogParser::stripLogosHostPrefix(const QString& line) {
auto m = s_hostPrefixRe.match(line);
if (m.hasMatch()) return stripAnsi(m.captured(1));
QString clean = stripAnsi(line);
m = s_lgxPrefixRe.match(clean);
if (m.hasMatch()) return m.captured(1);
return clean;
}
ParsedEvent LogParser::parseSequencerLine(const QString& raw) {
QString line = stripAnsi(raw);
ParsedEvent ev;
static const QRegularExpression reBlock(QStringLiteral("Block with id (\\d+) created"));
static const QRegularExpression reTxOk(QStringLiteral("Validated transaction with hash ([0-9a-f]+)"));
static const QRegularExpression reTxFail(
QStringLiteral("Transaction with hash ([0-9a-f]+) failed execution check with error: (.+), skipping"));
auto m = reBlock.match(line);
if (m.hasMatch()) {
ev.type = ParsedEvent::SeqBlockCreated;
ev.intVal = m.captured(1).toInt();
return ev;
}
m = reTxOk.match(line);
if (m.hasMatch()) {
ev.type = ParsedEvent::SeqTxValidated;
ev.strVal = m.captured(1).left(8);
return ev;
}
m = reTxFail.match(line);
if (m.hasMatch()) {
ev.type = ParsedEvent::SeqTxFailed;
ev.strVal = m.captured(1).left(8);
ev.strVal2 = m.captured(2);
return ev;
}
return ev;
}
ParsedEvent LogParser::parseMixNodeLine(const QString& raw) {
QString line = stripLogosHostPrefix(raw);
ParsedEvent ev;
if (line.contains(QStringLiteral("mounting mix protocol"))) {
ev.type = ParsedEvent::MixMounted;
return ev;
}
if (line.contains(QStringLiteral("Wired LEZ callbacks"))) {
ev.type = ParsedEvent::LezWired;
return ev;
}
if (line.contains(QStringLiteral("RLN gifter service mounted"))) {
ev.type = ParsedEvent::GifterMounted;
return ev;
}
if (line.contains(QStringLiteral("Gifter self-registered as mix relay"))) {
ev.type = ParsedEvent::GifterSelfRegistered;
return ev;
}
if (line.contains(QStringLiteral("Successfully started discovery v5")) ||
line.contains(QStringLiteral("kademlia discovery started"))) {
ev.type = ParsedEvent::KadReady;
return ev;
}
if (line.contains(QStringLiteral("address not allowlisted")) ||
line.contains(QStringLiteral("signature verification failed"))) {
ev.type = ParsedEvent::GifterAuthBounce;
int idx = line.indexOf(QStringLiteral(": "));
ev.strVal = idx >= 0 ? line.mid(idx + 2).left(60) : line.right(60);
return ev;
}
static const QRegularExpression reGifterReq(
QStringLiteral("handling RLN gifter request.*requestId=(\\S+)"));
auto m = reGifterReq.match(line);
if (m.hasMatch()) {
ev.type = ParsedEvent::GifterReqReceived;
ev.strVal = m.captured(1).left(10);
return ev;
}
static const QRegularExpression reGifterOk(
QStringLiteral("RLN gifter registration succeeded.*leafIndex=(\\d+)"));
m = reGifterOk.match(line);
if (m.hasMatch()) {
ev.type = ParsedEvent::GifterReqSucceeded;
ev.intVal = m.captured(1).toInt();
return ev;
}
static const QRegularExpression reGifterFail(
QStringLiteral("RLN gifter registration failed.*error=\"(.+)\""));
m = reGifterFail.match(line);
if (m.hasMatch()) {
ev.type = ParsedEvent::GifterReqFailed;
ev.strVal = m.captured(1);
return ev;
}
static const QRegularExpression reWalletErr(
QStringLiteral("send_public_transaction: wallet FFI error (\\d+)"));
m = reWalletErr.match(line);
if (m.hasMatch()) {
ev.type = ParsedEvent::WalletFfiError;
ev.intVal = m.captured(1).toInt();
return ev;
}
return ev;
}
ParsedEvent LogParser::parseChatLine(const QString& raw) {
QString line = stripLogosHostPrefix(raw);
ParsedEvent ev;
// Both formats: EVENT:chatInitResult:true (old stderr) and emitEvent: "chatInitResult" (lgx)
if (line.contains(QStringLiteral("EVENT:chatInitResult:true")) ||
line.contains(QStringLiteral("emitEvent: \"chatInitResult\""))) {
ev.type = ParsedEvent::ChatInit;
return ev;
}
if (line.contains(QStringLiteral("EVENT:chatStartResult:true")) ||
line.contains(QStringLiteral("emitEvent: \"chatStartResult\""))) {
ev.type = ParsedEvent::ChatStart;
return ev;
}
static const QRegularExpression reBundle(
QStringLiteral("EVENT:chatCreateIntroBundleResult:(logos_chatintro_\\S+)"));
auto m = reBundle.match(line);
if (m.hasMatch()) {
ev.type = ParsedEvent::ChatIntroBundleCreated;
ev.strVal = m.captured(1);
return ev;
}
if (line.contains(QStringLiteral("emitEvent: \"chatCreateIntroBundleResult\""))) {
ev.type = ParsedEvent::ChatIntroBundleCreated;
return ev;
}
if (line.contains(QStringLiteral("requesting RLN membership from gifter"))) {
ev.type = ParsedEvent::ChatMembershipRequested;
return ev;
}
static const QRegularExpression reGranted(
QStringLiteral("RLN membership granted.*leafIndex=(\\d+)"));
m = reGranted.match(line);
if (m.hasMatch()) {
ev.type = ParsedEvent::ChatMembershipGranted;
ev.intVal = m.captured(1).toInt();
return ev;
}
static const QRegularExpression reConfirmed(
QStringLiteral("membership confirmed on-chain.*leafIndex=(\\d+)"));
m = reConfirmed.match(line);
if (m.hasMatch()) {
ev.type = ParsedEvent::ChatMembershipConfirmed;
ev.intVal = m.captured(1).toInt();
return ev;
}
static const QRegularExpression reCorrected(
QStringLiteral("membership leaf corrected.*optimistic=(\\d+).*authoritative=(\\d+)"));
m = reCorrected.match(line);
if (m.hasMatch()) {
ev.type = ParsedEvent::ChatLeafCorrected;
ev.intVal = m.captured(1).toInt();
ev.intVal2 = m.captured(2).toInt();
return ev;
}
if (line.contains(QStringLiteral("EVENT:chatNewConversation:")) ||
line.contains(QStringLiteral("emitEvent: \"chatNewConversation\""))) {
ev.type = ParsedEvent::ChatNewConversation;
int idx = line.indexOf(QStringLiteral("EVENT:chatNewConversation:"));
if (idx >= 0) ev.strVal = line.mid(idx + 25);
return ev;
}
if (line.contains(QStringLiteral("EVENT:chatNewMessage:")) ||
line.contains(QStringLiteral("emitEvent: \"chatNewMessage\""))) {
ev.type = ParsedEvent::ChatNewMessage;
return ev;
}
if (line.contains(QStringLiteral("EVENT:chatNewPrivateConversationResult:true")) ||
line.contains(QStringLiteral("EVENT:chatSendMessageResult:true")) ||
line.contains(QStringLiteral("emitEvent: \"chatNewPrivateConversationResult\"")) ||
line.contains(QStringLiteral("emitEvent: \"chatSendMessageResult\""))) {
ev.type = ParsedEvent::ChatSendResult;
ev.boolVal = true;
return ev;
}
static const QRegularExpression rePeerStatus(
QStringLiteral("Peer status.*connectedPeers=(\\d+).*mixReady=(true|false).*mixPoolSize=(\\d+)"));
m = rePeerStatus.match(line);
if (m.hasMatch()) {
ev.type = ParsedEvent::ChatPeerStatus;
ev.intVal = m.captured(1).toInt();
ev.boolVal = (m.captured(2) == QStringLiteral("true"));
ev.intVal2 = m.captured(3).toInt();
return ev;
}
static const QRegularExpression reRoots(
QStringLiteral("get_valid_roots:.*count=\\s*(\\d+)"));
m = reRoots.match(line);
if (m.hasMatch()) {
ev.type = ParsedEvent::RlnRootsPolled;
ev.intVal = m.captured(1).toInt();
return ev;
}
if (line.contains(QStringLiteral("fetchAccountData failed: empty data"))) {
ev.type = ParsedEvent::RlnFetchFailed;
return ev;
}
return ev;
}

View File

@ -0,0 +1,53 @@
#pragma once
#include <QString>
#include <QRegularExpression>
#include <QVariant>
struct ParsedEvent {
enum Type {
None = 0,
SeqBlockCreated,
SeqTxValidated,
SeqTxFailed,
MixMounted,
LezWired,
GifterMounted,
GifterSelfRegistered,
GifterReqReceived,
GifterReqSucceeded,
GifterReqFailed,
WalletFfiError,
KadReady,
GifterAuthBounce,
ChatInit,
ChatStart,
ChatIntroBundleCreated,
ChatMembershipRequested,
ChatMembershipGranted,
ChatMembershipConfirmed,
ChatLeafCorrected,
ChatNewConversation,
ChatNewMessage,
ChatSendResult,
ChatPeerStatus,
RlnRootsPolled,
RlnFetchFailed,
};
Type type = None;
int intVal = 0;
int intVal2 = 0;
bool boolVal = false;
QString strVal;
QString strVal2;
};
class LogParser {
public:
static QString stripAnsi(const QString& line);
static QString stripLogosHostPrefix(const QString& line);
static ParsedEvent parseSequencerLine(const QString& line);
static ParsedEvent parseMixNodeLine(const QString& line);
static ParsedEvent parseChatLine(const QString& line);
};

View File

@ -0,0 +1,80 @@
#include "LogTailer.h"
#include <QFileInfo>
#include <QTextStream>
#include <QDebug>
LogTailer::LogTailer(const QString& filePath, bool replay, QObject* parent)
: QObject(parent), m_filePath(filePath), m_replay(replay) {
m_timer.setInterval(100);
connect(&m_timer, &QTimer::timeout, this, &LogTailer::poll);
}
void LogTailer::start() {
tryOpen();
m_timer.start();
}
void LogTailer::stop() {
m_timer.stop();
if (m_open) { m_file.close(); m_open = false; }
}
void LogTailer::reset() {
if (m_open) { m_file.close(); m_open = false; }
m_lastInode = -1;
m_wasReset = true;
emit fileReset();
}
void LogTailer::tryOpen() {
if (m_open) return;
QFileInfo fi(m_filePath);
if (!fi.exists()) { qDebug() << "TAILER: file not found:" << m_filePath; return; }
m_file.setFileName(m_filePath);
if (!m_file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "TAILER: open failed:" << m_filePath; return; }
m_lastInode = fi.fileTime(QFileDevice::FileModificationTime).toMSecsSinceEpoch();
m_open = true;
if (!m_replay && !m_wasReset) m_file.seek(m_file.size());
qDebug() << "TAILER: opened" << m_filePath << "replay=" << m_replay << "pos=" << m_file.pos() << "size=" << m_file.size();
}
void LogTailer::poll() {
if (!m_open) {
tryOpen();
return;
}
QFileInfo fi(m_filePath);
if (!fi.exists()) {
reset();
return;
}
if (fi.size() < m_file.pos()) {
qDebug() << "TAILER: file shrunk, resetting" << m_filePath;
reset();
tryOpen();
return;
}
readNewLines();
}
void LogTailer::readNewLines() {
int count = 0;
while (!m_file.atEnd()) {
QByteArray bytes = m_file.readLine();
QString line = QString::fromUtf8(bytes).trimmed();
if (!line.isEmpty()) {
emit newLine(line);
++count;
}
}
if (m_debugOnce && count > 0) {
m_debugOnce = false;
qDebug() << "TAILER: first read from" << m_filePath << "lines=" << count;
}
}

View File

@ -0,0 +1,34 @@
#pragma once
#include <QObject>
#include <QFile>
#include <QTimer>
#include <QString>
class LogTailer : public QObject {
Q_OBJECT
public:
explicit LogTailer(const QString& filePath, bool replay = false, QObject* parent = nullptr);
void start();
void stop();
void reset();
signals:
void newLine(const QString& line);
void fileReset();
private slots:
void poll();
private:
void tryOpen();
void readNewLines();
QString m_filePath;
bool m_replay;
QFile m_file;
QTimer m_timer;
qint64 m_lastInode = -1;
bool m_open = false;
bool m_wasReset = false;
bool m_debugOnce = true;
};

View File

@ -0,0 +1,221 @@
#include "MonitorBackend.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDir>
MonitorBackend::MonitorBackend(QObject* parent) : QObject(parent) {
m_blockAgeTimer.setInterval(1000);
connect(&m_blockAgeTimer, &QTimer::timeout, this, &MonitorBackend::onBlockAgeTick);
m_blockAgeTimer.start();
}
void MonitorBackend::setRpcUrl(const QString& url) {
if (m_rpcClient) { m_rpcClient->stop(); delete m_rpcClient; m_rpcClient = nullptr; }
if (url.isEmpty()) return;
m_rpcClient = new RpcClient(url, this);
connect(m_rpcClient, &RpcClient::updated, this, [this]{ emit stateChanged(); });
m_rpcClient->start();
}
void MonitorBackend::setStateDir(const QString& path, bool replay) {
if (m_stateDir == path) return;
m_stateDir = path;
if (m_seqTailer) { m_seqTailer->stop(); delete m_seqTailer; m_seqTailer = nullptr; }
for (int i = 0; i < 4; ++i) {
if (m_nodeTailers[i]) { m_nodeTailers[i]->stop(); delete m_nodeTailers[i]; m_nodeTailers[i] = nullptr; }
}
if (m_senderTailer) { m_senderTailer->stop(); delete m_senderTailer; m_senderTailer = nullptr; }
if (m_receiverTailer) { m_receiverTailer->stop(); delete m_receiverTailer; m_receiverTailer = nullptr; }
resetState();
m_seqTailer = new LogTailer(QDir(path).filePath("sequencer.log"), replay, this);
connect(m_seqTailer, &LogTailer::newLine, this, &MonitorBackend::onSequencerLine);
connect(m_seqTailer, &LogTailer::fileReset, this, [this]{ resetState(); });
m_seqTailer->start();
for (int i = 0; i < 4; ++i) {
m_nodeTailers[i] = new LogTailer(
QDir(path).filePath(QStringLiteral("node%1.log").arg(i)), replay, this);
connect(m_nodeTailers[i], &LogTailer::newLine, this, [this, i](const QString& l){ onNodeLine(i, l); });
connect(m_nodeTailers[i], &LogTailer::fileReset, this, [this]{ resetState(); });
m_nodeTailers[i]->start();
}
m_senderTailer = new LogTailer(QDir(path).filePath("chat_sender.log"), replay, this);
connect(m_senderTailer, &LogTailer::newLine, this, [this](const QString& l){ onChatLine(true, l); });
connect(m_senderTailer, &LogTailer::fileReset, this, [this]{ resetState(); });
m_senderTailer->start();
m_receiverTailer = new LogTailer(QDir(path).filePath("chat_receiver.log"), replay, this);
connect(m_receiverTailer, &LogTailer::newLine, this, [this](const QString& l){ onChatLine(false, l); });
connect(m_receiverTailer, &LogTailer::fileReset, this, [this]{ resetState(); });
m_receiverTailer->start();
}
void MonitorBackend::resetState() {
m_blockId = 0;
m_lastBlockTime = QDateTime();
m_txValidated = 0;
m_txFailed = 0;
for (auto& n : m_nodes) n = {};
m_sender = {};
m_receiver = {};
m_chainEvents.clear();
emit stateChanged();
}
int MonitorBackend::blockAgeSecs() const {
if (!m_lastBlockTime.isValid()) return -1;
return static_cast<int>(m_lastBlockTime.secsTo(QDateTime::currentDateTime()));
}
QString MonitorBackend::mixNodeStates() const {
QJsonArray arr;
for (int i = 0; i < 4; ++i) {
QJsonObject o;
o["mounted"] = m_nodes[i].mixMounted;
o["lez"] = m_nodes[i].lezWired;
o["kad"] = m_nodes[i].kadReady;
arr.append(o);
}
return QJsonDocument(arr).toJson(QJsonDocument::Compact);
}
int MonitorBackend::gifterQueueDepth() const {
const auto& g = m_nodes[0];
int depth = g.gifterReqsIn - g.gifterReqsOk - g.gifterReqsFail;
return depth > 0 ? depth : 0;
}
QString MonitorBackend::gifterStatus() const {
if (!m_nodes[0].gifterMounted) return QStringLiteral("not-mounted");
int depth = gifterQueueDepth();
if (depth > 0) return QStringLiteral("queue:%1").arg(depth);
return QStringLiteral("idle");
}
void MonitorBackend::onSequencerLine(const QString& line) {
static int lineCount = 0;
if (++lineCount <= 3) qDebug() << "SEQ LINE" << lineCount << ":" << line.left(80);
auto ev = LogParser::parseSequencerLine(line);
if (ev.type != ParsedEvent::None && lineCount <= 10) qDebug() << "SEQ MATCH type=" << ev.type;
switch (ev.type) {
case ParsedEvent::SeqBlockCreated:
m_blockId = ev.intVal;
m_lastBlockTime = QDateTime::currentDateTime();
break;
case ParsedEvent::SeqTxValidated:
++m_txValidated;
addChainEvent("TX_OK", QStringLiteral("hash=%1").arg(ev.strVal));
break;
case ParsedEvent::SeqTxFailed:
++m_txFailed;
addChainEvent("TX_FAIL", QStringLiteral("hash=%1 %2").arg(ev.strVal, ev.strVal2));
break;
default: return;
}
emit stateChanged();
}
void MonitorBackend::onNodeLine(int idx, const QString& line) {
auto ev = LogParser::parseMixNodeLine(line);
auto& node = m_nodes[idx];
switch (ev.type) {
case ParsedEvent::MixMounted: node.mixMounted = true; break;
case ParsedEvent::LezWired: node.lezWired = true; break;
case ParsedEvent::KadReady: node.kadReady = true; break;
case ParsedEvent::GifterMounted: node.gifterMounted = true; break;
case ParsedEvent::GifterSelfRegistered: node.gifterSelfReg = true; break;
case ParsedEvent::GifterAuthBounce:
addChainEvent("GIFTER_AUTHFAIL", ev.strVal);
break;
case ParsedEvent::GifterReqReceived:
++node.gifterReqsIn;
addChainEvent("GIFTER_REQ", QStringLiteral("req=%1").arg(ev.strVal));
break;
case ParsedEvent::GifterReqSucceeded:
++node.gifterReqsOk;
addChainEvent("REGISTER", QStringLiteral("leaf=%1 confirmed").arg(ev.intVal));
break;
case ParsedEvent::GifterReqFailed:
++node.gifterReqsFail;
addChainEvent("REG_FAIL", ev.strVal);
break;
case ParsedEvent::WalletFfiError:
addChainEvent("WALLET_ERR", QStringLiteral("FFI error %1").arg(ev.intVal));
break;
default: return;
}
emit stateChanged();
}
void MonitorBackend::onChatLine(bool isSender, const QString& line) {
auto ev = LogParser::parseChatLine(line);
auto& chat = isSender ? m_sender : m_receiver;
switch (ev.type) {
case ParsedEvent::ChatInit: chat.phase = "init"; 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;
if (chat.mixReady && chat.mixPool >= 4)
chat.phase = "ready";
break;
case ParsedEvent::ChatLeafCorrected:
chat.leafCorrected = true;
chat.optLeaf = ev.intVal;
chat.authLeaf = ev.intVal2;
addChainEvent("LEAF_FIX",
QStringLiteral("%1 opt=%2→auth=%3").arg(isSender ? "sender" : "receiver").arg(ev.intVal).arg(ev.intVal2));
break;
case ParsedEvent::ChatIntroBundleCreated:
if (isSender) chat.phase = "intro_emitted";
addChainEvent("BUNDLE", ev.strVal.left(30) + "...");
break;
case ParsedEvent::ChatNewConversation:
if (!isSender) chat.phase = "intro_accepted";
break;
case ParsedEvent::ChatNewMessage:
++chat.msgIn;
if (!isSender && chat.msgIn == 1) chat.phase = "msg_received";
break;
case ParsedEvent::ChatSendResult:
if (ev.boolVal) {
++chat.msgOut;
if (isSender && chat.msgOut == 1) chat.phase = "msg_sent";
}
break;
case ParsedEvent::ChatPeerStatus:
chat.peers = ev.intVal;
chat.mixReady = ev.boolVal;
chat.mixPool = ev.intVal2;
if (chat.phase.startsWith("conf:") && chat.mixReady && chat.mixPool >= 4)
chat.phase = "ready";
break;
case ParsedEvent::RlnRootsPolled:
addChainEvent("ROOTS", QStringLiteral("count=%1").arg(ev.intVal));
break;
default: return;
}
emit stateChanged();
}
void MonitorBackend::onBlockAgeTick() {
if (m_lastBlockTime.isValid()) emit stateChanged();
}
void MonitorBackend::addChainEvent(const QString& type, const QString& detail) {
m_chainEvents.prepend(nowTimestamp(), type, detail);
}
QString MonitorBackend::nowTimestamp() const {
return QDateTime::currentDateTime().toString("HH:mm:ss");
}

View File

@ -0,0 +1,142 @@
#pragma once
#include <QObject>
#include <QTimer>
#include <QDateTime>
#include <QJsonArray>
#include "LogTailer.h"
#include "LogParser.h"
#include "ChainEventModel.h"
#include "RpcClient.h"
class MonitorBackend : public QObject {
Q_OBJECT
Q_PROPERTY(QString stateDir READ stateDir CONSTANT)
Q_PROPERTY(int blockId READ blockId NOTIFY stateChanged)
Q_PROPERTY(int blockAgeSecs READ blockAgeSecs NOTIFY stateChanged)
Q_PROPERTY(int txValidated READ txValidated NOTIFY stateChanged)
Q_PROPERTY(int txFailed READ txFailed NOTIFY stateChanged)
Q_PROPERTY(int rpcBlockId READ rpcBlockId NOTIFY stateChanged)
Q_PROPERTY(bool rpcReachable READ rpcReachable NOTIFY stateChanged)
Q_PROPERTY(QString mixNodeStates READ mixNodeStates NOTIFY stateChanged)
Q_PROPERTY(bool gifterMounted READ gifterMounted NOTIFY stateChanged)
Q_PROPERTY(int gifterQueueDepth READ gifterQueueDepth NOTIFY stateChanged)
Q_PROPERTY(QString gifterStatus READ gifterStatus NOTIFY stateChanged)
Q_PROPERTY(QString senderPhase READ senderPhase NOTIFY stateChanged)
Q_PROPERTY(int senderOptLeaf READ senderOptLeaf NOTIFY stateChanged)
Q_PROPERTY(int senderAuthLeaf READ senderAuthLeaf NOTIFY stateChanged)
Q_PROPERTY(bool senderLeafCorrected READ senderLeafCorrected NOTIFY stateChanged)
Q_PROPERTY(int senderPeers READ senderPeers NOTIFY stateChanged)
Q_PROPERTY(bool senderMixReady READ senderMixReady NOTIFY stateChanged)
Q_PROPERTY(int senderMixPool READ senderMixPool NOTIFY stateChanged)
Q_PROPERTY(int senderMsgOut READ senderMsgOut NOTIFY stateChanged)
Q_PROPERTY(int senderMsgIn READ senderMsgIn NOTIFY stateChanged)
Q_PROPERTY(QString receiverPhase READ receiverPhase NOTIFY stateChanged)
Q_PROPERTY(int receiverOptLeaf READ receiverOptLeaf NOTIFY stateChanged)
Q_PROPERTY(int receiverAuthLeaf READ receiverAuthLeaf NOTIFY stateChanged)
Q_PROPERTY(bool receiverLeafCorrected READ receiverLeafCorrected NOTIFY stateChanged)
Q_PROPERTY(int receiverPeers READ receiverPeers NOTIFY stateChanged)
Q_PROPERTY(bool receiverMixReady READ receiverMixReady NOTIFY stateChanged)
Q_PROPERTY(int receiverMixPool READ receiverMixPool NOTIFY stateChanged)
Q_PROPERTY(int receiverMsgOut READ receiverMsgOut NOTIFY stateChanged)
Q_PROPERTY(int receiverMsgIn READ receiverMsgIn NOTIFY stateChanged)
public:
explicit MonitorBackend(QObject* parent = nullptr);
Q_INVOKABLE void setStateDir(const QString& path, bool replay = false);
Q_INVOKABLE void setRpcUrl(const QString& url);
QString stateDir() const { return m_stateDir; }
int blockId() const { return m_blockId; }
int blockAgeSecs() const;
int txValidated() const { return m_txValidated; }
int txFailed() const { return m_txFailed; }
int rpcBlockId() const { return m_rpcClient ? m_rpcClient->lastBlockId() : -1; }
bool rpcReachable() const { return m_rpcClient ? m_rpcClient->reachable() : false; }
QString mixNodeStates() const;
bool gifterMounted() const { return m_nodes[0].gifterMounted; }
int gifterQueueDepth() const;
QString gifterStatus() const;
QString senderPhase() const { return m_sender.phase; }
int senderOptLeaf() const { return m_sender.optLeaf; }
int senderAuthLeaf() const { return m_sender.authLeaf; }
bool senderLeafCorrected() const { return m_sender.leafCorrected; }
int senderPeers() const { return m_sender.peers; }
bool senderMixReady() const { return m_sender.mixReady; }
int senderMixPool() const { return m_sender.mixPool; }
int senderMsgOut() const { return m_sender.msgOut; }
int senderMsgIn() const { return m_sender.msgIn; }
QString receiverPhase() const { return m_receiver.phase; }
int receiverOptLeaf() const { return m_receiver.optLeaf; }
int receiverAuthLeaf() const { return m_receiver.authLeaf; }
bool receiverLeafCorrected() const { return m_receiver.leafCorrected; }
int receiverPeers() const { return m_receiver.peers; }
bool receiverMixReady() const { return m_receiver.mixReady; }
int receiverMixPool() const { return m_receiver.mixPool; }
int receiverMsgOut() const { return m_receiver.msgOut; }
int receiverMsgIn() const { return m_receiver.msgIn; }
ChainEventModel* chainEventModel() { return &m_chainEvents; }
signals:
void stateChanged();
private slots:
void onSequencerLine(const QString& line);
void onNodeLine(int idx, const QString& line);
void onChatLine(bool isSender, const QString& line);
void onBlockAgeTick();
private:
void resetState();
void addChainEvent(const QString& type, const QString& detail);
QString nowTimestamp() const;
struct NodeState {
bool mixMounted = false;
bool lezWired = false;
bool kadReady = false;
bool gifterMounted = false;
bool gifterSelfReg = false;
int gifterReqsIn = 0;
int gifterReqsOk = 0;
int gifterReqsFail = 0;
};
struct ChatState {
QString phase = QStringLiteral("---");
int optLeaf = -1;
int authLeaf = -1;
bool leafCorrected = false;
int peers = 0;
bool mixReady = false;
int mixPool = 0;
int msgOut = 0;
int msgIn = 0;
};
QString m_stateDir;
int m_blockId = 0;
QDateTime m_lastBlockTime;
int m_txValidated = 0;
int m_txFailed = 0;
NodeState m_nodes[4];
ChatState m_sender;
ChatState m_receiver;
ChainEventModel m_chainEvents;
QTimer m_blockAgeTimer;
RpcClient* m_rpcClient = nullptr;
LogTailer* m_seqTailer = nullptr;
LogTailer* m_nodeTailers[4] = {};
LogTailer* m_senderTailer = nullptr;
LogTailer* m_receiverTailer = nullptr;
};

View File

@ -0,0 +1,57 @@
#include "RpcClient.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QNetworkRequest>
#include <QUrl>
RpcClient::RpcClient(const QString& url, QObject* parent)
: QObject(parent), m_url(url) {
m_timer.setInterval(5000);
connect(&m_timer, &QTimer::timeout, this, &RpcClient::poll);
connect(&m_nam, &QNetworkAccessManager::finished, this, &RpcClient::onReply);
}
void RpcClient::start() {
poll();
m_timer.start();
}
void RpcClient::stop() {
m_timer.stop();
}
void RpcClient::poll() {
QJsonObject req;
req["jsonrpc"] = "2.0";
req["method"] = "getLastBlockId";
req["params"] = QJsonArray();
req["id"] = ++m_requestId;
QNetworkRequest request{QUrl(m_url)};
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
request.setTransferTimeout(4000);
m_nam.post(request, QJsonDocument(req).toJson(QJsonDocument::Compact));
}
void RpcClient::onReply(QNetworkReply* reply) {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
if (m_reachable) { m_reachable = false; emit updated(); }
return;
}
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
if (!doc.isObject()) {
if (m_reachable) { m_reachable = false; emit updated(); }
return;
}
QJsonValue result = doc.object().value("result");
if (result.isDouble()) {
m_lastBlockId = static_cast<int>(result.toDouble());
m_reachable = true;
emit updated();
}
}

View File

@ -0,0 +1,31 @@
#pragma once
#include <QObject>
#include <QTimer>
#include <QNetworkAccessManager>
#include <QNetworkReply>
class RpcClient : public QObject {
Q_OBJECT
public:
explicit RpcClient(const QString& url, QObject* parent = nullptr);
void start();
void stop();
int lastBlockId() const { return m_lastBlockId; }
bool reachable() const { return m_reachable; }
signals:
void updated();
private slots:
void poll();
void onReply(QNetworkReply* reply);
private:
QString m_url;
QNetworkAccessManager m_nam;
QTimer m_timer;
int m_lastBlockId = -1;
bool m_reachable = false;
int m_requestId = 0;
};

View File

@ -0,0 +1,84 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QCommandLineParser>
#include <QDir>
#include "MonitorBackend.h"
#ifdef ENABLE_HOST_MODE
#include "ChatHost.h"
#endif
int main(int argc, char* argv[]) {
qputenv("QT_QUICK_CONTROLS_STYLE", "Basic");
QGuiApplication app(argc, argv);
app.setApplicationName("sim-monitor");
QCommandLineParser parser;
parser.addHelpOption();
parser.addOption({{"d", "state-dir"}, "Path to .sim_state directory", "dir"});
parser.addOption({{"r", "rpc-url"}, "Sequencer JSON-RPC URL", "url"});
parser.addOption({"replay", "Start from beginning of logs instead of tail"});
#ifdef ENABLE_HOST_MODE
parser.addOption({"host-chat", "Enable chat host mode (load chat_module + deps)"});
parser.addOption({"chat-config", "Path to chat config JSON file", "file"});
#endif
parser.process(app);
QString stateDir = parser.value("state-dir");
if (stateDir.isEmpty())
stateDir = QStringLiteral("simulations/mix_lez_chat/.sim_state");
MonitorBackend backend;
#ifdef ENABLE_HOST_MODE
ChatHost* chatHost = nullptr;
bool hostMode = parser.isSet("host-chat");
if (hostMode) {
chatHost = new ChatHost(&backend);
QString userDir = qEnvironmentVariable("LOGOS_USER_DIR");
if (userDir.isEmpty()) {
qWarning() << "LOGOS_USER_DIR not set — chat host mode needs staged modules";
} else {
QString modulesDir = QDir(userDir).filePath("modules");
QString dataDir = QDir(userDir).filePath("module_data");
QDir().mkpath(dataDir);
if (!chatHost->loadModules(modulesDir, dataDir)) {
qWarning() << "Failed to load chat modules — host mode disabled";
hostMode = false;
}
}
}
#endif
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("monitor", &backend);
engine.rootContext()->setContextProperty("chainEvents", backend.chainEventModel());
#ifdef ENABLE_HOST_MODE
engine.rootContext()->setContextProperty("chatHost", chatHost);
engine.rootContext()->setContextProperty("hostModeEnabled",
QVariant(hostMode && chatHost != nullptr));
#else
engine.rootContext()->setContextProperty("chatHost", QVariant());
engine.rootContext()->setContextProperty("hostModeEnabled", QVariant(false));
#endif
engine.load(QUrl("qrc:/src/qml/MonitorView.qml"));
if (engine.rootObjects().isEmpty()) return -1;
if (!stateDir.isEmpty())
backend.setStateDir(stateDir, parser.isSet("replay"));
QString rpcUrl = parser.value("rpc-url");
if (rpcUrl.isEmpty()) rpcUrl = qEnvironmentVariable("SIM_SEQ_RPC");
if (!rpcUrl.isEmpty()) backend.setRpcUrl(rpcUrl);
qDebug() << "Starting event loop...";
int rc = app.exec();
qDebug() << "Event loop exited with" << rc;
return rc;
}

View File

@ -0,0 +1,377 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
ApplicationWindow {
id: root
visible: true
width: 900
height: 600
title: "Sim Monitor"
color: "#0A0A0A"
readonly property color bgPrimary: "#0A0A0A"
readonly property color bgSecondary: "#111111"
readonly property color bgPanel: "#161616"
readonly property color border: "#2a2a2a"
readonly property color textPrimary: "#FAFAFA"
readonly property color textSecond: "#6B7280"
readonly property color textTertiary:"#4B5563"
readonly property color accent: "#10B981"
readonly property color yellow: "#F59E0B"
readonly property color red: "#EF4444"
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) {
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]
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 4
// INFRA STRIP
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 52
color: bgSecondary
radius: 4
ColumnLayout {
anchors.fill: parent
anchors.margins: 6
spacing: 2
// 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 + "✗"
}
}
// Line 2: Mix dots + Gifter + Payment
Row {
spacing: 16
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
}
}
}
}
Text {
font.family: root.monoFont; font.pixelSize: 12
color: monitor.gifterMounted ? accent : textTertiary
text: "GIFTER " + monitor.gifterStatus
}
}
}
}
// CHAT PANELS
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 4
Repeater {
model: ["sender", "receiver"]
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: bgSecondary
radius: 4
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: 8
spacing: 4
// Header
Text {
font.family: root.monoFont; font.pixelSize: 13; font.bold: true
color: textPrimary
text: role.toUpperCase()
}
// State badge
Text {
font.family: root.monoFont; font.pixelSize: 11
color: accent
text: {
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 (currentIdx < 0 && phase !== "---") currentIdx = 99
var labels = []
for (var i = 0; i < allPhases.length; i++) {
var p = allPhases[i]
var label = p
if (p === "opt" && optLeaf >= 0) label = "opt:" + optLeaf
if (p === "conf" && authLeaf >= 0) label = "conf:" + authLeaf
if (i === currentIdx) labels.push("▶" + label)
else if (i < currentIdx) labels.push("✓" + label)
else break
}
return labels.length > 0 ? labels.join(" → ") : phase
}
}
// Leaf info
Text {
font.family: root.monoFont; font.pixelSize: 11
color: corrected ? root.yellow : (optLeaf >= 0 && optLeaf === authLeaf ? accent : textSecond)
text: "leaf: opt=" + (optLeaf >= 0 ? optLeaf : "-") +
" auth=" + (authLeaf >= 0 ? authLeaf : "-") +
(corrected ? " ⚠" : (optLeaf >= 0 && optLeaf === authLeaf ? " ✓" : ""))
}
// Peers
Text {
font.family: root.monoFont; font.pixelSize: 11
color: mixRdy ? accent : textSecond
text: "peers=" + peers + " mix=" + (mixRdy ? "✓" : "✗") + " pool=" + pool
}
// Messages
Text {
font.family: root.monoFont; font.pixelSize: 11
color: textPrimary
text: "out:" + out_ + " in:" + in_
}
Item { Layout.fillHeight: true }
}
}
}
}
// CHAT HOST PANEL (only in host mode)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: hostModeEnabled ? 200 : 0
visible: hostModeEnabled
color: bgSecondary
radius: 4
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"
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
model: chainEvents
clip: true
delegate: Text {
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
}
text: timestamp + " " + eventType + " " + detail
elide: Text.ElideRight
}
}
}
}
}
}

View File

@ -0,0 +1,130 @@
#!/usr/bin/env bash
# Stage .lgx modules + basecamp's capability_module into a directory
# suitable for logos_core_add_modules_dir(). Applies Phase 0.3 dylib
# overrides and ad-hoc codesigns.
#
# Usage:
# bash stage_modules.sh <output_dir> [lgx_dir]
#
# Prerequisites:
# - .lgx files built (nix build in each module repo)
# - basecamp built (for capability_module + logos_host)
set -euo pipefail
die() { echo "FATAL: $*" >&2; exit 1; }
OUTDIR="${1:?Usage: stage_modules.sh <output_dir> [lgx_dir]}"
LGX_DIR="${2:-}"
case "$(uname -s)-$(uname -m)" in
Darwin-arm64) PLATFORM="darwin-arm64-dev"; EXT="dylib";;
Linux-x86_64) PLATFORM="linux-x86_64-dev"; EXT="so";;
Linux-aarch64) PLATFORM="linux-aarch64-dev"; EXT="so";;
*) die "Unsupported platform: $(uname -s)-$(uname -m)";;
esac
install_lgx() {
local mdir="$1" lgx="$2"
local name
name=$(tar xzOf "$lgx" manifest.json | python3 -c 'import json,sys; print(json.load(sys.stdin)["name"])')
[ -z "$name" ] && die "install_lgx: cannot read name from $lgx"
local tmp; tmp=$(mktemp -d)
tar xzf "$lgx" -C "$tmp"
rm -rf "$mdir/$name"
mkdir -p "$mdir/$name"
cp "$tmp/manifest.json" "$mdir/$name/"
if [ -d "$tmp/variants/$PLATFORM" ]; then
cp -L "$tmp"/variants/"$PLATFORM"/* "$mdir/$name/"
else
die "install_lgx: $PLATFORM variant missing in $lgx"
fi
printf '%s' "$PLATFORM" > "$mdir/$name/variant"
rm -rf "$tmp"
echo " Staged: $name"
}
install_extra_lib() {
local mdir="$1" module="$2" lib="$3"
[ -f "$lib" ] || die "install_extra_lib: missing $lib for $module"
cp -L "$lib" "$mdir/$module/"
echo " Override: $(basename "$lib")$module/"
}
resign() {
if [ "$(uname -s)" = "Darwin" ]; then
codesign --force --sign - "$1" 2>/dev/null || true
fi
}
# ─── Create output structure ───
MODULES="$OUTDIR/modules"
mkdir -p "$MODULES"
# ─── Stage capability_module from basecamp ───
echo "=== Staging capability_module ==="
BASECAMP_MODULES=""
for candidate in \
"$(dirname "$(which logoscore 2>/dev/null || true)")/../modules" \
/nix/store/*-logos-basecamp-*/modules; do
[ -d "$candidate/capability_module" ] 2>/dev/null && BASECAMP_MODULES="$candidate" && break
done
if [ -n "$BASECAMP_MODULES" ]; then
cp -r "$BASECAMP_MODULES/capability_module" "$MODULES/"
echo " Staged: capability_module (from basecamp)"
else
echo " WARNING: capability_module not found — host mode may fail"
fi
# ─── Stage .lgx modules ───
if [ -n "$LGX_DIR" ]; then
echo "=== Staging .lgx modules from $LGX_DIR ==="
for lgx in "$LGX_DIR"/*.lgx; do
[ -f "$lgx" ] && install_lgx "$MODULES" "$lgx"
done
fi
# ─── Phase 0.3 dylib overrides ───
echo "=== Applying dylib overrides ==="
# liblogosdelivery.dylib — 17-symbol build
DELIVERY_LIB=$(find /nix/store -name "liblogosdelivery.$EXT" -path '*module-lib*' 2>/dev/null | head -1)
if [ -n "$DELIVERY_LIB" ] && [ -d "$MODULES/delivery_module" ]; then
install_extra_lib "$MODULES" delivery_module "$DELIVERY_LIB"
resign "$MODULES/delivery_module/liblogosdelivery.$EXT"
fi
# liblogoschat.dylib — 48MB sim-runtime version
CHAT_LIB=$(find /nix/store -name "liblogoschat.$EXT" -path '*chat-module-lib*' 2>/dev/null | head -1)
if [ -z "$CHAT_LIB" ]; then
# Fallback: look in sim build output
CHAT_LIB=$(find /Users/arseniy/Waku/Logos/logos-chat/build -name "liblogoschat.$EXT" 2>/dev/null | head -1)
fi
if [ -n "$CHAT_LIB" ] && [ -d "$MODULES/chat_module" ]; then
install_extra_lib "$MODULES" chat_module "$CHAT_LIB"
resign "$MODULES/chat_module/liblogoschat.$EXT"
fi
# ─── Stage logos_host next to monitor binary ───
echo "=== Locating logos_host ==="
LOGOS_HOST=""
for candidate in \
"$(dirname "$(which logoscore 2>/dev/null || true)")/logos_host" \
/nix/store/*-logos-basecamp-*/bin/logos_host; do
[ -x "$candidate" ] 2>/dev/null && LOGOS_HOST="$candidate" && break
done
if [ -n "$LOGOS_HOST" ]; then
mkdir -p "$OUTDIR/bin"
cp -L "$LOGOS_HOST" "$OUTDIR/bin/"
resign "$OUTDIR/bin/logos_host"
echo " logos_host → $OUTDIR/bin/"
else
echo " WARNING: logos_host not found — host mode may fail"
fi
echo "=== Done. Modules staged at: $MODULES ==="
echo "Launch monitor with:"
echo " LOGOS_USER_DIR=$OUTDIR ./build/sim-monitor --host-chat --state-dir .sim_state"