mirror of
https://github.com/logos-messaging/nim-chat-poc.git
synced 2026-07-29 11:43:26 +00:00
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:
parent
6a90c01668
commit
052cb11f00
60
simulations/mix_lez_chat/sim-monitor/CMakeLists.txt
Normal file
60
simulations/mix_lez_chat/sim-monitor/CMakeLists.txt
Normal 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()
|
||||
741
simulations/mix_lez_chat/sim-monitor/flake.lock
generated
Normal file
741
simulations/mix_lez_chat/sim-monitor/flake.lock
generated
Normal 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
|
||||
}
|
||||
64
simulations/mix_lez_chat/sim-monitor/flake.nix
Normal file
64
simulations/mix_lez_chat/sim-monitor/flake.nix
Normal 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"
|
||||
'';
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
10
simulations/mix_lez_chat/sim-monitor/run_monitor.sh
Executable file
10
simulations/mix_lez_chat/sim-monitor/run_monitor.sh
Executable 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"
|
||||
50
simulations/mix_lez_chat/sim-monitor/src/ChainEventModel.h
Normal file
50
simulations/mix_lez_chat/sim-monitor/src/ChainEventModel.h
Normal 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;
|
||||
};
|
||||
288
simulations/mix_lez_chat/sim-monitor/src/ChatHost.cpp
Normal file
288
simulations/mix_lez_chat/sim-monitor/src/ChatHost.cpp
Normal 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()));
|
||||
}
|
||||
70
simulations/mix_lez_chat/sim-monitor/src/ChatHost.h
Normal file
70
simulations/mix_lez_chat/sim-monitor/src/ChatHost.h
Normal 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)
|
||||
};
|
||||
240
simulations/mix_lez_chat/sim-monitor/src/LogParser.cpp
Normal file
240
simulations/mix_lez_chat/sim-monitor/src/LogParser.cpp
Normal 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;
|
||||
}
|
||||
53
simulations/mix_lez_chat/sim-monitor/src/LogParser.h
Normal file
53
simulations/mix_lez_chat/sim-monitor/src/LogParser.h
Normal 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);
|
||||
};
|
||||
80
simulations/mix_lez_chat/sim-monitor/src/LogTailer.cpp
Normal file
80
simulations/mix_lez_chat/sim-monitor/src/LogTailer.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
34
simulations/mix_lez_chat/sim-monitor/src/LogTailer.h
Normal file
34
simulations/mix_lez_chat/sim-monitor/src/LogTailer.h
Normal 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;
|
||||
};
|
||||
221
simulations/mix_lez_chat/sim-monitor/src/MonitorBackend.cpp
Normal file
221
simulations/mix_lez_chat/sim-monitor/src/MonitorBackend.cpp
Normal 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");
|
||||
}
|
||||
142
simulations/mix_lez_chat/sim-monitor/src/MonitorBackend.h
Normal file
142
simulations/mix_lez_chat/sim-monitor/src/MonitorBackend.h
Normal 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;
|
||||
};
|
||||
57
simulations/mix_lez_chat/sim-monitor/src/RpcClient.cpp
Normal file
57
simulations/mix_lez_chat/sim-monitor/src/RpcClient.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
31
simulations/mix_lez_chat/sim-monitor/src/RpcClient.h
Normal file
31
simulations/mix_lez_chat/sim-monitor/src/RpcClient.h
Normal 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;
|
||||
};
|
||||
84
simulations/mix_lez_chat/sim-monitor/src/main.cpp
Normal file
84
simulations/mix_lez_chat/sim-monitor/src/main.cpp
Normal 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;
|
||||
}
|
||||
377
simulations/mix_lez_chat/sim-monitor/src/qml/MonitorView.qml
Normal file
377
simulations/mix_lez_chat/sim-monitor/src/qml/MonitorView.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
130
simulations/mix_lez_chat/sim-monitor/stage_modules.sh
Executable file
130
simulations/mix_lez_chat/sim-monitor/stage_modules.sh
Executable 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"
|
||||
Loading…
x
Reference in New Issue
Block a user