From 052cb11f009250687332ad9e81ff3045bcbd394b Mon Sep 17 00:00:00 2001 From: Arseniy Klempner Date: Wed, 3 Jun 2026 16:08:15 -0600 Subject: [PATCH] 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 --- .../mix_lez_chat/sim-monitor/CMakeLists.txt | 60 ++ .../mix_lez_chat/sim-monitor/flake.lock | 741 ++++++++++++++++++ .../mix_lez_chat/sim-monitor/flake.nix | 64 ++ .../mix_lez_chat/sim-monitor/run_monitor.sh | 10 + .../sim-monitor/src/ChainEventModel.h | 50 ++ .../mix_lez_chat/sim-monitor/src/ChatHost.cpp | 288 +++++++ .../mix_lez_chat/sim-monitor/src/ChatHost.h | 70 ++ .../sim-monitor/src/LogParser.cpp | 240 ++++++ .../mix_lez_chat/sim-monitor/src/LogParser.h | 53 ++ .../sim-monitor/src/LogTailer.cpp | 80 ++ .../mix_lez_chat/sim-monitor/src/LogTailer.h | 34 + .../sim-monitor/src/MonitorBackend.cpp | 221 ++++++ .../sim-monitor/src/MonitorBackend.h | 142 ++++ .../sim-monitor/src/RpcClient.cpp | 57 ++ .../mix_lez_chat/sim-monitor/src/RpcClient.h | 31 + .../mix_lez_chat/sim-monitor/src/main.cpp | 84 ++ .../sim-monitor/src/qml/MonitorView.qml | 377 +++++++++ .../mix_lez_chat/sim-monitor/stage_modules.sh | 130 +++ 18 files changed, 2732 insertions(+) create mode 100644 simulations/mix_lez_chat/sim-monitor/CMakeLists.txt create mode 100644 simulations/mix_lez_chat/sim-monitor/flake.lock create mode 100644 simulations/mix_lez_chat/sim-monitor/flake.nix create mode 100755 simulations/mix_lez_chat/sim-monitor/run_monitor.sh create mode 100644 simulations/mix_lez_chat/sim-monitor/src/ChainEventModel.h create mode 100644 simulations/mix_lez_chat/sim-monitor/src/ChatHost.cpp create mode 100644 simulations/mix_lez_chat/sim-monitor/src/ChatHost.h create mode 100644 simulations/mix_lez_chat/sim-monitor/src/LogParser.cpp create mode 100644 simulations/mix_lez_chat/sim-monitor/src/LogParser.h create mode 100644 simulations/mix_lez_chat/sim-monitor/src/LogTailer.cpp create mode 100644 simulations/mix_lez_chat/sim-monitor/src/LogTailer.h create mode 100644 simulations/mix_lez_chat/sim-monitor/src/MonitorBackend.cpp create mode 100644 simulations/mix_lez_chat/sim-monitor/src/MonitorBackend.h create mode 100644 simulations/mix_lez_chat/sim-monitor/src/RpcClient.cpp create mode 100644 simulations/mix_lez_chat/sim-monitor/src/RpcClient.h create mode 100644 simulations/mix_lez_chat/sim-monitor/src/main.cpp create mode 100644 simulations/mix_lez_chat/sim-monitor/src/qml/MonitorView.qml create mode 100755 simulations/mix_lez_chat/sim-monitor/stage_modules.sh diff --git a/simulations/mix_lez_chat/sim-monitor/CMakeLists.txt b/simulations/mix_lez_chat/sim-monitor/CMakeLists.txt new file mode 100644 index 0000000..f11b643 --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/CMakeLists.txt @@ -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() diff --git a/simulations/mix_lez_chat/sim-monitor/flake.lock b/simulations/mix_lez_chat/sim-monitor/flake.lock new file mode 100644 index 0000000..5ea6356 --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/flake.lock @@ -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 +} diff --git a/simulations/mix_lez_chat/sim-monitor/flake.nix b/simulations/mix_lez_chat/sim-monitor/flake.nix new file mode 100644 index 0000000..07b5664 --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/flake.nix @@ -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" + ''; + }; + }); + }; +} diff --git a/simulations/mix_lez_chat/sim-monitor/run_monitor.sh b/simulations/mix_lez_chat/sim-monitor/run_monitor.sh new file mode 100755 index 0000000..41d0c76 --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/run_monitor.sh @@ -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" diff --git a/simulations/mix_lez_chat/sim-monitor/src/ChainEventModel.h b/simulations/mix_lez_chat/sim-monitor/src/ChainEventModel.h new file mode 100644 index 0000000..5cff936 --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/src/ChainEventModel.h @@ -0,0 +1,50 @@ +#pragma once +#include +#include +#include + +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 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 m_items; +}; diff --git a/simulations/mix_lez_chat/sim-monitor/src/ChatHost.cpp b/simulations/mix_lez_chat/sim-monitor/src/ChatHost.cpp new file mode 100644 index 0000000..d15a95d --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/src/ChatHost.cpp @@ -0,0 +1,288 @@ +#include "ChatHost.h" +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_HOST_MODE +#include +#include +#include +#endif + +ChatHost::ChatHost(QObject* parent) : QObject(parent) {} + +ChatHost::~ChatHost() { +#ifdef ENABLE_HOST_MODE + if (m_modulesLoaded) { + logos_core_cleanup(); + } + delete static_cast(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(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(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(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(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(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(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())); +} diff --git a/simulations/mix_lez_chat/sim-monitor/src/ChatHost.h b/simulations/mix_lez_chat/sim-monitor/src/ChatHost.h new file mode 100644 index 0000000..f80c7e6 --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/src/ChatHost.h @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include +#include + +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) +}; diff --git a/simulations/mix_lez_chat/sim-monitor/src/LogParser.cpp b/simulations/mix_lez_chat/sim-monitor/src/LogParser.cpp new file mode 100644 index 0000000..99607cc --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/src/LogParser.cpp @@ -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; +} diff --git a/simulations/mix_lez_chat/sim-monitor/src/LogParser.h b/simulations/mix_lez_chat/sim-monitor/src/LogParser.h new file mode 100644 index 0000000..2743f81 --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/src/LogParser.h @@ -0,0 +1,53 @@ +#pragma once +#include +#include +#include + +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); +}; diff --git a/simulations/mix_lez_chat/sim-monitor/src/LogTailer.cpp b/simulations/mix_lez_chat/sim-monitor/src/LogTailer.cpp new file mode 100644 index 0000000..99a1ea3 --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/src/LogTailer.cpp @@ -0,0 +1,80 @@ +#include "LogTailer.h" +#include +#include +#include + +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; + } +} diff --git a/simulations/mix_lez_chat/sim-monitor/src/LogTailer.h b/simulations/mix_lez_chat/sim-monitor/src/LogTailer.h new file mode 100644 index 0000000..f180448 --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/src/LogTailer.h @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#include +#include + +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; +}; diff --git a/simulations/mix_lez_chat/sim-monitor/src/MonitorBackend.cpp b/simulations/mix_lez_chat/sim-monitor/src/MonitorBackend.cpp new file mode 100644 index 0000000..78204c0 --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/src/MonitorBackend.cpp @@ -0,0 +1,221 @@ +#include "MonitorBackend.h" +#include +#include +#include +#include + +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(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"); +} diff --git a/simulations/mix_lez_chat/sim-monitor/src/MonitorBackend.h b/simulations/mix_lez_chat/sim-monitor/src/MonitorBackend.h new file mode 100644 index 0000000..fe7fafc --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/src/MonitorBackend.h @@ -0,0 +1,142 @@ +#pragma once +#include +#include +#include +#include +#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; +}; diff --git a/simulations/mix_lez_chat/sim-monitor/src/RpcClient.cpp b/simulations/mix_lez_chat/sim-monitor/src/RpcClient.cpp new file mode 100644 index 0000000..9f60866 --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/src/RpcClient.cpp @@ -0,0 +1,57 @@ +#include "RpcClient.h" +#include +#include +#include +#include +#include + +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(result.toDouble()); + m_reachable = true; + emit updated(); + } +} diff --git a/simulations/mix_lez_chat/sim-monitor/src/RpcClient.h b/simulations/mix_lez_chat/sim-monitor/src/RpcClient.h new file mode 100644 index 0000000..5f1c82a --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/src/RpcClient.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include +#include + +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; +}; diff --git a/simulations/mix_lez_chat/sim-monitor/src/main.cpp b/simulations/mix_lez_chat/sim-monitor/src/main.cpp new file mode 100644 index 0000000..e837cc2 --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/src/main.cpp @@ -0,0 +1,84 @@ +#include +#include +#include +#include +#include +#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; +} diff --git a/simulations/mix_lez_chat/sim-monitor/src/qml/MonitorView.qml b/simulations/mix_lez_chat/sim-monitor/src/qml/MonitorView.qml new file mode 100644 index 0000000..72288d9 --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/src/qml/MonitorView.qml @@ -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 + } + } + } + } + } +} diff --git a/simulations/mix_lez_chat/sim-monitor/stage_modules.sh b/simulations/mix_lez_chat/sim-monitor/stage_modules.sh new file mode 100755 index 0000000..1b1458c --- /dev/null +++ b/simulations/mix_lez_chat/sim-monitor/stage_modules.sh @@ -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 [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 [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"