From 07040dd6d0b0b226149e65c2c365328b7cddbb51 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Thu, 29 Jan 2026 20:57:10 +0100 Subject: [PATCH] feat: Add "Heard X repeats" echo tracking for sent messages Track how many repeaters heard and forwarded sent channel messages, similar to the Meshcore mobile app's "Heard X repeats" feature. - Bridge: Detect GRP_TXT echoes from stdout, track unique paths - Bridge: New /register_echo and /echo_counts API endpoints - API: Register messages for echo tracking after send - API: Merge echo counts into /api/messages response - Frontend: Display green badge with broadcast icon next to Resend button - CSS: Echo badge styling with dark mode support Co-Authored-By: Claude Opus 4.5 --- app/routes/api.py | 35 ++++++++++ app/static/css/style.css | 26 ++++++++ app/static/js/app.js | 8 +++ meshcore-bridge/bridge.py | 133 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+) diff --git a/app/routes/api.py b/app/routes/api.py index 90fcdba..7985dbf 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -299,6 +299,30 @@ def get_messages(): channel_idx=channel_idx ) + # Fetch echo counts from bridge (for "Heard X repeats" feature) + if not archive_date: # Only for live messages, not archives + try: + bridge_url = config.MC_BRIDGE_URL.replace('/cli', '/echo_counts') + response = requests.get(bridge_url, timeout=2) + if response.ok: + echo_counts = response.json().get('echo_counts', []) + + # Create lookup by timestamp + channel + echo_lookup = {(ec['timestamp'], ec['channel_idx']): ec['count'] + for ec in echo_counts} + + # Merge into messages + for msg in messages: + if msg.get('is_own'): + # Find matching echo count (within 5 second window) + msg['echo_count'] = 0 + for (ts, ch), count in echo_lookup.items(): + if msg.get('channel_idx') == ch and abs(msg['timestamp'] - ts) < 5: + msg['echo_count'] = count + break + except Exception as e: + logger.debug(f"Echo counts fetch failed (non-critical): {e}") + return jsonify({ 'success': True, 'count': len(messages), @@ -360,6 +384,17 @@ def send_message(): success, message = cli.send_message(text, reply_to=reply_to, channel_index=channel_idx) if success: + # Register for echo tracking ("Heard X repeats" feature) + try: + bridge_url = config.MC_BRIDGE_URL.replace('/cli', '/register_echo') + requests.post( + bridge_url, + json={'channel_idx': channel_idx, 'timestamp': time.time()}, + timeout=2 + ) + except Exception as e: + logger.debug(f"Echo registration failed (non-critical): {e}") + return jsonify({ 'success': True, 'message': 'Message sent successfully', diff --git a/app/static/css/style.css b/app/static/css/style.css index b0a5513..8cc94c7 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -1041,3 +1041,29 @@ main { .scroll-to-bottom-btn:active { transform: scale(0.95); } + +/* ============================================================================= + Echo Badge - "Heard X repeats" indicator for sent messages + ============================================================================= */ + +.echo-badge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + font-size: 0.7rem; + color: #198754; + padding: 0.2rem 0.4rem; + margin-right: 0.25rem; + border-radius: 0.25rem; + background-color: rgba(25, 135, 84, 0.1); +} + +.echo-badge i { + font-size: 0.65rem; +} + +/* Dark mode support */ +[data-bs-theme="dark"] .echo-badge { + color: #75b798; + background-color: rgba(117, 183, 152, 0.15); +} diff --git a/app/static/js/app.js b/app/static/js/app.js index b0e3fee..41540a2 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -712,6 +712,13 @@ function createMessageElement(msg) { if (msg.is_own) { // Own messages: right-aligned, no avatar + // Echo badge shows how many repeaters heard the message + const echoDisplay = msg.echo_count > 0 + ? ` + ${msg.echo_count} + ` + : ''; + wrapper.innerHTML = `