mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
refactor: fix propagation metrics handling and update API response schemas
This commit is contained in:
parent
8b66423424
commit
415abdb00a
7 changed files with 139 additions and 79 deletions
|
|
@ -11620,15 +11620,30 @@ class ReticulumMeshChat:
|
|||
except Exception:
|
||||
pass
|
||||
sync_metrics = self._collect_propagation_sync_metrics()
|
||||
progress_raw = getattr(
|
||||
self.message_router,
|
||||
"propagation_transfer_progress",
|
||||
0.0,
|
||||
)
|
||||
try:
|
||||
progress_pct = float(progress_raw) * 100
|
||||
except (TypeError, ValueError):
|
||||
progress_pct = 0.0
|
||||
last_result = getattr(
|
||||
self.message_router,
|
||||
"propagation_transfer_last_result",
|
||||
None,
|
||||
)
|
||||
if not isinstance(last_result, (int, float, str, type(None))):
|
||||
last_result = None
|
||||
return web.json_response(
|
||||
{
|
||||
"propagation_node_status": {
|
||||
"state": convert_propagation_node_state_to_string(
|
||||
self.message_router.propagation_transfer_state,
|
||||
),
|
||||
"progress": self.message_router.propagation_transfer_progress
|
||||
* 100, # convert to percentage
|
||||
"messages_received": self.message_router.propagation_transfer_last_result,
|
||||
"progress": progress_pct,
|
||||
"messages_received": last_result,
|
||||
"messages_stored": sync_metrics["messages_stored"],
|
||||
"delivery_confirmations": sync_metrics[
|
||||
"delivery_confirmations"
|
||||
|
|
@ -11931,9 +11946,22 @@ class ReticulumMeshChat:
|
|||
|
||||
# determine next hop and hop count
|
||||
hops = RNS.Transport.hops_to(destination_hash)
|
||||
if not isinstance(hops, int):
|
||||
pm = reticulum_pathfinding.path_metadata_for_api(destination_hash)
|
||||
return web.json_response(
|
||||
{
|
||||
"path": None,
|
||||
**pm,
|
||||
},
|
||||
)
|
||||
next_hop_bytes = None
|
||||
if hasattr(self, "reticulum") and self.reticulum:
|
||||
next_hop_bytes = self.reticulum.get_next_hop(destination_hash)
|
||||
if next_hop_bytes is not None and not isinstance(
|
||||
next_hop_bytes,
|
||||
(bytes, bytearray),
|
||||
):
|
||||
next_hop_bytes = None
|
||||
|
||||
# ensure next hop provided
|
||||
if next_hop_bytes is None:
|
||||
|
|
|
|||
|
|
@ -361,6 +361,7 @@ def _try_pnpm_licenses(repo_root: Path) -> list[dict[str, Any]] | None:
|
|||
text=True,
|
||||
timeout=120,
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.TimeoutExpired, OSError):
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -191,14 +191,16 @@ def should_rediscover_path(destination_hash: bytes) -> bool:
|
|||
|
||||
def path_metadata_for_api(destination_hash: bytes) -> dict[str, bool]:
|
||||
has = RNS.Transport.has_path(destination_hash)
|
||||
if not has:
|
||||
if has is not True:
|
||||
return {
|
||||
"path_stale": True,
|
||||
"path_unresponsive": False,
|
||||
}
|
||||
stale = transport_path_table_entry_is_expired(destination_hash)
|
||||
unresponsive = RNS.Transport.path_is_unresponsive(destination_hash)
|
||||
return {
|
||||
"path_stale": transport_path_table_entry_is_expired(destination_hash),
|
||||
"path_unresponsive": RNS.Transport.path_is_unresponsive(destination_hash),
|
||||
"path_stale": stale if isinstance(stale, bool) else True,
|
||||
"path_unresponsive": unresponsive if isinstance(unresponsive, bool) else False,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -350,6 +350,7 @@ def check_meshchatx_run_module() -> dict[str, str]:
|
|||
timeout=45,
|
||||
check=False,
|
||||
env=env,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
detail = (result.stderr or result.stdout or "").strip()[-500:]
|
||||
|
|
@ -406,6 +407,7 @@ def check_subprocess_spawn() -> dict[str, str]:
|
|||
timeout=30,
|
||||
check=False,
|
||||
env=env,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return _status(
|
||||
|
|
|
|||
|
|
@ -319,6 +319,8 @@ HTTP_JSON_GET_CONTRACTS: tuple[HttpJsonContract, ...] = (
|
|||
"/api/v1/sticker-packs/{pack_id}",
|
||||
STICKER_PACK_DETAIL_SCHEMA,
|
||||
match_info={"pack_id": _NODE_ID},
|
||||
allow_statuses=(200, 404),
|
||||
alt_schemas=(ERROR_ENVELOPE_SCHEMA,),
|
||||
),
|
||||
HttpJsonContract("GET", "/api/v1/map/drawings", MAP_DRAWINGS_SCHEMA),
|
||||
HttpJsonContract("GET", "/api/v1/map/offline", MAP_OFFLINE_SCHEMA),
|
||||
|
|
@ -349,6 +351,8 @@ HTTP_JSON_GET_CONTRACTS: tuple[HttpJsonContract, ...] = (
|
|||
"/api/v1/rrc/hubs/{hub_hash}/rooms/{room}/messages",
|
||||
RRC_MESSAGES_SCHEMA,
|
||||
match_info={"hub_hash": _HEX32, "room": _ROOM},
|
||||
allow_statuses=(200, 404),
|
||||
alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
|
||||
),
|
||||
HttpJsonContract(
|
||||
"GET",
|
||||
|
|
@ -380,6 +384,8 @@ HTTP_JSON_GET_CONTRACTS: tuple[HttpJsonContract, ...] = (
|
|||
"/api/v1/rncp/transfer/{transfer_id}",
|
||||
RNCP_TRANSFER_SCHEMA,
|
||||
match_info={"transfer_id": _TRANSFER_ID},
|
||||
allow_statuses=(200, 404),
|
||||
alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
|
||||
),
|
||||
HttpJsonContract("GET", "/api/v1/rnpath/table", RNPATH_TABLE_SCHEMA),
|
||||
HttpJsonContract("GET", "/api/v1/rnpath/rates", RNPATH_RATES_SCHEMA),
|
||||
|
|
@ -388,6 +394,8 @@ HTTP_JSON_GET_CONTRACTS: tuple[HttpJsonContract, ...] = (
|
|||
"/api/v1/rnpath/trace/{destination_hash}",
|
||||
RNPATH_TRACE_SCHEMA,
|
||||
match_info={"destination_hash": _HEX32},
|
||||
allow_statuses=(200, 500),
|
||||
alt_schemas=(ERROR_ENVELOPE_SCHEMA,),
|
||||
),
|
||||
HttpJsonContract("GET", "/api/v1/rnsh/sessions", RNSH_SESSIONS_SCHEMA),
|
||||
HttpJsonContract(
|
||||
|
|
@ -395,10 +403,18 @@ HTTP_JSON_GET_CONTRACTS: tuple[HttpJsonContract, ...] = (
|
|||
"/api/v1/rnsh/sessions/{session_id}/output",
|
||||
RNSH_OUTPUT_SCHEMA,
|
||||
match_info={"session_id": _SESSION_ID},
|
||||
allow_statuses=(200, 404),
|
||||
alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
|
||||
),
|
||||
HttpJsonContract("GET", "/api/v1/rnstatus", RNSTATUS_SCHEMA),
|
||||
HttpJsonContract("GET", "/api/v1/bots/status", BOTS_STATUS_SCHEMA),
|
||||
HttpJsonContract("GET", "/api/v1/bots/subprocess-log", BOTS_SUBPROCESS_LOG_SCHEMA),
|
||||
HttpJsonContract(
|
||||
"GET",
|
||||
"/api/v1/bots/subprocess-log",
|
||||
BOTS_SUBPROCESS_LOG_SCHEMA,
|
||||
allow_statuses=(200, 400),
|
||||
alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
|
||||
),
|
||||
HttpJsonContract("GET", "/api/v1/spam-keywords", SPAM_KEYWORDS_SCHEMA),
|
||||
HttpJsonContract(
|
||||
"GET", "/api/v1/translator/languages", TRANSLATOR_LANGUAGES_SCHEMA
|
||||
|
|
@ -471,6 +487,17 @@ HTTP_JSON_GET_CONTRACT_EXCLUDED: tuple[str, ...] = (
|
|||
"/api/v1/tools/rnode/download_firmware",
|
||||
"/api/v1/tools/rnode/latest_release",
|
||||
"/api/v1/tools/micron-parser-go-release",
|
||||
"/api/v1/favourites/layout",
|
||||
"/api/v1/map/overlays",
|
||||
"/api/v1/map/overlays/jobs/{job_id}",
|
||||
"/api/v1/map/overlays/{overlay_id}/content",
|
||||
"/api/v1/notification-sounds",
|
||||
"/api/v1/notification-sounds/status",
|
||||
"/api/v1/plugins",
|
||||
"/api/v1/plugins/trusted-publishers",
|
||||
"/api/v1/plugins/{plugin_id}/asset/{asset_path:.*}",
|
||||
"/api/v1/sideband-plugins",
|
||||
"/api/v1/sideband-plugins/config",
|
||||
"/api/v1/lxmf-messages/attachment/{message_hash}/{attachment_type}",
|
||||
"/api/v1/gifs/{gif_id}/image",
|
||||
"/api/v1/stickers/{sticker_id}/image",
|
||||
|
|
|
|||
|
|
@ -98,8 +98,8 @@ INTERFACES_LIST_SCHEMA: dict = {
|
|||
|
||||
COMMUNITY_INTERFACES_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["entries"],
|
||||
"properties": {"entries": _ARRAY},
|
||||
"required": ["interfaces"],
|
||||
"properties": {"interfaces": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -112,8 +112,12 @@ IDENTITIES_LIST_SCHEMA: dict = {
|
|||
|
||||
LICENSES_ENVELOPE_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["licenses"],
|
||||
"properties": {"licenses": _ARRAY},
|
||||
"required": ["backend", "frontend", "meta"],
|
||||
"properties": {
|
||||
"backend": _ARRAY,
|
||||
"frontend": _ARRAY,
|
||||
"meta": _OBJECT,
|
||||
},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -147,8 +151,8 @@ MESHCHATX_DOCS_CONTENT_SCHEMA: dict = {
|
|||
|
||||
DATABASE_HEALTH_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["healthy"],
|
||||
"properties": {"healthy": _BOOLEAN},
|
||||
"required": ["database"],
|
||||
"properties": {"database": _OBJECT},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -199,20 +203,8 @@ MEMORY_DIAGNOSTICS_DISABLED_SCHEMA: dict = {
|
|||
|
||||
DISCOVERY_CONFIG_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"discover_interfaces",
|
||||
"interface_discovery_sources",
|
||||
"interface_discovery_whitelist",
|
||||
"interface_discovery_blacklist",
|
||||
"required_discovery_value",
|
||||
],
|
||||
"properties": {
|
||||
"discover_interfaces": {},
|
||||
"interface_discovery_sources": {},
|
||||
"interface_discovery_whitelist": {},
|
||||
"interface_discovery_blacklist": {},
|
||||
"required_discovery_value": {},
|
||||
},
|
||||
"required": ["discovery"],
|
||||
"properties": {"discovery": _OBJECT},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -264,15 +256,18 @@ RETICULUM_CONFIG_RAW_SCHEMA: dict = {
|
|||
|
||||
BLACKHOLE_STATUS_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["enabled"],
|
||||
"properties": {"enabled": _BOOLEAN},
|
||||
"required": ["blackholed_identities"],
|
||||
"properties": {
|
||||
"blackholed_identities": {"type": ["object", "array"]},
|
||||
"enabled": _BOOLEAN,
|
||||
},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
INTERFACE_STATS_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["interfaces"],
|
||||
"properties": {"interfaces": _ARRAY},
|
||||
"required": ["interface_stats"],
|
||||
"properties": {"interface_stats": _OBJECT},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -294,16 +289,14 @@ LXMF_CONVERSATIONS_SCHEMA: dict = {
|
|||
}
|
||||
|
||||
LXMF_FOLDERS_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["folders"],
|
||||
"properties": {"folders": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
"type": "array",
|
||||
"items": _OBJECT,
|
||||
}
|
||||
|
||||
LXMF_CONVERSATION_PINS_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["pins"],
|
||||
"properties": {"pins": _ARRAY},
|
||||
"required": ["peer_hashes"],
|
||||
"properties": {"peer_hashes": _ARRAY, "pins": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -336,15 +329,13 @@ LXMF_MESSAGE_BLOCKLIST_SCHEMA: dict = {
|
|||
|
||||
LXMF_PROPAGATION_NODES_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["nodes"],
|
||||
"properties": {"nodes": _ARRAY},
|
||||
"required": ["lxmf_propagation_nodes"],
|
||||
"properties": {"lxmf_propagation_nodes": _ARRAY, "nodes": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
LXMF_PROPAGATION_STATUS_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["status"],
|
||||
"properties": {"status": _STRING},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -432,29 +423,27 @@ MAP_DRAWINGS_SCHEMA: dict = {
|
|||
|
||||
MAP_OFFLINE_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["tiles"],
|
||||
"properties": {"tiles": _ARRAY},
|
||||
"required": ["loaded"],
|
||||
"properties": {"loaded": _BOOLEAN, "tiles": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
MAP_MBTILES_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["mbtiles"],
|
||||
"properties": {"mbtiles": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
"type": "array",
|
||||
"items": _OBJECT,
|
||||
}
|
||||
|
||||
TELEMETRY_PEERS_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["peers"],
|
||||
"properties": {"peers": _ARRAY},
|
||||
"required": ["telemetry"],
|
||||
"properties": {"telemetry": _ARRAY, "peers": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
TELEMETRY_TRACKING_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["tracking"],
|
||||
"properties": {"tracking": _ARRAY},
|
||||
"required": ["tracked_peers"],
|
||||
"properties": {"tracked_peers": _ARRAY, "tracking": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -474,8 +463,8 @@ TELEMETRY_LATEST_SCHEMA: dict = {
|
|||
|
||||
TELEMETRY_HISTORY_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["history"],
|
||||
"properties": {"history": _ARRAY},
|
||||
"required": ["telemetry"],
|
||||
"properties": {"telemetry": _ARRAY, "history": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -516,8 +505,6 @@ RRC_ACTIVITY_SCHEMA: dict = {
|
|||
|
||||
RNCP_STATUS_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["status"],
|
||||
"properties": {"status": _STRING},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -544,8 +531,6 @@ RNPATH_RATES_SCHEMA: dict = {
|
|||
|
||||
RNPATH_TRACE_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["trace"],
|
||||
"properties": {"trace": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -572,8 +557,6 @@ RNSTATUS_SCHEMA: dict = {
|
|||
|
||||
BOTS_STATUS_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["bots"],
|
||||
"properties": {"bots": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -586,8 +569,8 @@ BOTS_SUBPROCESS_LOG_SCHEMA: dict = {
|
|||
|
||||
SPAM_KEYWORDS_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["keywords"],
|
||||
"properties": {"keywords": _ARRAY},
|
||||
"required": ["spam_keywords"],
|
||||
"properties": {"spam_keywords": _ARRAY, "keywords": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -625,16 +608,17 @@ REPOSITORY_SERVER_STATUS_SCHEMA: dict = {
|
|||
}
|
||||
|
||||
REPOSITORY_SERVER_LIST_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["entries"],
|
||||
"properties": {"entries": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
"type": "array",
|
||||
"items": _OBJECT,
|
||||
}
|
||||
|
||||
ANNOUNCE_SINGLE_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["announce"],
|
||||
"properties": {"announce": {"type": ["object", "null"]}},
|
||||
"required": ["message"],
|
||||
"properties": {
|
||||
"message": _STRING,
|
||||
"announce": {"type": ["object", "null"]},
|
||||
},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -678,15 +662,18 @@ DESTINATION_STAMP_INFO_SCHEMA: dict = {
|
|||
|
||||
DESTINATION_SIGNAL_METRICS_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["metrics"],
|
||||
"properties": {"metrics": {"type": ["object", "null"]}},
|
||||
"required": ["signal_metrics"],
|
||||
"properties": {
|
||||
"signal_metrics": {"type": ["object", "null"]},
|
||||
"metrics": {"type": ["object", "null"]},
|
||||
},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
LXMF_CONVERSATION_MESSAGES_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["messages"],
|
||||
"properties": {"messages": _ARRAY},
|
||||
"required": ["lxmf_messages"],
|
||||
"properties": {"lxmf_messages": _ARRAY, "messages": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -713,15 +700,13 @@ CHANGELOG_SCHEMA: dict = {
|
|||
|
||||
TELEPHONE_STATUS_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["status"],
|
||||
"properties": {"status": _INTEGER},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
TELEPHONE_HISTORY_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["history"],
|
||||
"properties": {"history": _ARRAY},
|
||||
"required": ["call_history"],
|
||||
"properties": {"call_history": _ARRAY, "history": _ARRAY},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
|
@ -756,7 +741,5 @@ TELEPHONE_CODEC2_STATUS_SCHEMA: dict = {
|
|||
|
||||
TELEPHONE_CALL_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"required": ["call"],
|
||||
"properties": {"call": {"type": ["object", "null"]}},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,13 +55,29 @@ def mock_rns_minimal():
|
|||
|
||||
@pytest.fixture(scope="module")
|
||||
def contract_app(mock_rns_minimal, temp_dir):
|
||||
# Stub Thread only while constructing the app so deferred startup threads do
|
||||
# not start. Leave real Thread available afterward so asyncio.to_thread works
|
||||
# (licenses, memory diag, etc.). Patching Thread for the whole fixture hangs
|
||||
# those handlers and stalls the suite near completion.
|
||||
with (
|
||||
patch("meshchatx.meshchat.generate_ssl_certificate"),
|
||||
patch("psutil.Process") as mock_process,
|
||||
patch("psutil.net_io_counters") as mock_net_io,
|
||||
patch("importlib.metadata.version", return_value="1.2.3"),
|
||||
patch("meshchatx.meshchat.LXST") as mock_lxst,
|
||||
patch("threading.Thread"),
|
||||
patch(
|
||||
"meshchatx.src.backend.licenses_collector.build_licenses_payload",
|
||||
return_value={
|
||||
"backend": [{"name": "rns", "version": "1.0", "license": "MIT"}],
|
||||
"frontend": [{"name": "vue", "version": "3.0", "license": "MIT"}],
|
||||
"meta": {
|
||||
"generated_at": "2026-01-01T00:00:00Z",
|
||||
"backend_count": 1,
|
||||
"frontend_count": 1,
|
||||
"frontend_source": "test",
|
||||
},
|
||||
},
|
||||
),
|
||||
):
|
||||
mock_lxst.__version__ = "1.2.3"
|
||||
mock_proc_instance = mock_process.return_value
|
||||
|
|
@ -73,7 +89,8 @@ def contract_app(mock_rns_minimal, temp_dir):
|
|||
mock_net_instance.bytes_recv = 0
|
||||
mock_net_instance.packets_sent = 0
|
||||
mock_net_instance.packets_recv = 0
|
||||
app = bootstrap_contract_app(make_contract_app(temp_dir, mock_rns_minimal))
|
||||
with patch("threading.Thread"):
|
||||
app = bootstrap_contract_app(make_contract_app(temp_dir, mock_rns_minimal))
|
||||
yield app
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue