Skip to content

fix(unified): preserve tapback metadata across MQTT ingest + cross-source merge#3105

Merged
Yeraze merged 1 commit into
mainfrom
fix/unified-tapback-rendering
May 20, 2026
Merged

fix(unified): preserve tapback metadata across MQTT ingest + cross-source merge#3105
Yeraze merged 1 commit into
mainfrom
fix/unified-tapback-rendering

Conversation

@Yeraze

@Yeraze Yeraze commented May 20, 2026

Copy link
Copy Markdown
Owner

Summary

Tapback reactions on the Unified Messages feed flickered between rendering correctly (grouped under the parent as an emoji pill) and rendering as full inline messages, depending on which source's row won a non-deterministic Promise.all race in /api/unified/messages.

Three interacting causes, all introduced in #3089 (feat(mqtt): full source-scoped ingest with channel decryption and unified-view fixes):

  1. MQTT ingest dropped emoji / reply_id. mqttIngestion.ts TEXT_MESSAGE_APP branch built DbMessage rows without reading the decoded Data's emoji / reply_id fields, so MQTT-sourced reactions hit the DB with both null.
  2. Server-side decryption stripped tapback metadata. channelDecryptionService.isValidProtobuf decoded the full Data protobuf but only returned {portnum, payload}, throwing away emoji/replyId.
  3. Unified merge was first-source-wins. unifiedRoutes.ts merge logic only upgraded sender display names — emoji/replyId stayed whatever the first race-winning source had, even when later sources had correct values.

The result: TCP-source row has emoji=1, replyId=parentPacketId; MQTT-source row has emoji=null, replyId=null. Each poll races; when MQTT wins, the merged record drops the metadata and the reaction renders as a regular inline message.

Fix

  • mqttIngestion.ts — read emoji and replyId (accepting both camelCase and snake_case) from the decoded Data in TEXT_MESSAGE_APP.
  • channelDecryptionService.ts — extend DecryptionResult with emoji? / replyId? and surface them from isValidProtobuf. Propagate onto the synthesized decoded object so MQTT ingest sees them after server-decryption.
  • unifiedRoutes.ts — in the if (existing) merge branch, upgrade emoji/replyId from any source that has them populated when the first-seen row didn't (mirrors the existing sender-name upgrade pattern).

Tests

  • New regression test in unifiedRoutes.test.ts proving the merge upgrades tapback metadata across sources.
  • Three new MQTT-ingest tests covering replyId / camelCase, reply_id / snake_case, and the regular-text case where neither field is set.
  • Full Vitest suite: 10137 pass / 0 fail.

Test plan

  • Send a tapback (👍) from a phone connected to the TCP companion; also have MQTT bridge ingest the same packet.
  • Open Unified Messages on the affected channel.
  • Confirm the tapback stays grouped under the parent across multiple 10s poll cycles — no flicker, no transition to a full inline message.
  • Send a normal text message; confirm it stays an inline message and is NOT misclassified as a reaction.

🤖 Generated with Claude Code

…urce merge

Tapback reactions on the Unified Messages feed flickered between rendering
correctly (grouped under the parent message as an emoji pill) and rendering
as full inline messages, depending on which source's row won a non-
deterministic Promise.all race in /api/unified/messages.

Three interacting causes, all introduced in #3089:

1. MQTT ingest (mqttIngestion.ts TEXT_MESSAGE_APP branch) was building
   DbMessage rows without reading `decoded.emoji` / `decoded.reply_id`,
   so MQTT-sourced reactions hit the DB with both fields null.
2. channelDecryptionService synthesized a stripped `{portnum, payload}`
   decoded shape, dropping emoji/replyId for server-decrypted packets
   even when they were present in the underlying Data protobuf.
3. unifiedRoutes merge logic kept first-source-wins for emoji/replyId in
   the `if (existing)` branch, so when the metadata-less MQTT row was
   processed before the metadata-bearing TCP row, the merged entry took
   the null values and the unified view classified the reaction as a
   regular message.

Fix:
- Read emoji + replyId (both camelCase and snake_case) from the decoded
  Data in mqttIngestion's TEXT_MESSAGE_APP case.
- Extend DecryptionResult + isValidProtobuf to surface the fields, and
  propagate them onto the synthesized decoded object after server-side
  decryption.
- In the unifiedRoutes merge, upgrade `existing.emoji` and
  `existing.replyId` from any later source that has them populated when
  the first-seen row didn't — same pattern already used for sender
  display names.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


Code Review Complete ✅

I've thoroughly analyzed this PR that fixes tapback metadata preservation across MQTT ingest and cross-source merge.

Tasks:

  • Read repository guidelines and understand codebase context
  • Analyze the 5 changed files for code quality and implementation
  • Review test coverage and quality
  • Check for potential bugs, security issues, and performance concerns
  • Provide comprehensive feedback on best practices
  • Final review summary

Review Summary

This is an excellent fix that addresses a complex multi-layered bug with proper root cause analysis and systematic testing. The implementation demonstrates thorough understanding of the MeshMonitor architecture and follows established patterns.

✅ Strengths

🎯 Problem Analysis & Solution Design

  • Clear root cause identification: Correctly identified 3 interacting causes across MQTT ingestion, server-side decryption, and unified merging
  • Systematic fix approach: Each cause addressed at its source rather than applying band-aids
  • Follows existing patterns: The unified route merge upgrade logic mirrors existing sender name upgrade pattern

📋 Code Quality

  • Consistent with codebase conventions: Follows established multi-source architecture patterns
  • Defensive programming: Proper null checking and type coercion (rawReplyId >>> 0)
  • Clear documentation: Excellent inline comments explaining the "why" behind tapback metadata preservation
  • Type safety: Proper TypeScript interfaces and type assertions

🧪 Test Coverage

  • Comprehensive regression tests: 4 new tests covering all edge cases
  • Good test isolation: Each test verifies a specific scenario
  • Realistic test data: Uses proper packet IDs and realistic message structures
  • Both positive and negative cases: Tests both tapback messages and regular text messages

🔒 Security & Performance

  • No security concerns: Changes don't introduce any security vulnerabilities
  • Performance neutral: No additional database queries or expensive operations
  • Backwards compatible: Gracefully handles both camelCase (replyId) and snake_case (reply_id) field names

📝 Minor Observations

Code Style

  • Line 242 in mqttIngestion.ts: The comment block is quite long but very valuable for future maintainers
  • The defensive validation in unifiedRoutes.ts:597-602 is excellent - checks for both null and zero values

Architecture Alignment

  • Perfect adherence to MeshMonitor's multi-source architecture principles
  • Proper source isolation maintained throughout
  • Follows the repository's async/await patterns consistently

Test Quality

  • Test descriptions are clear and explain the regression being prevented
  • Good use of mocking that matches the actual service interface
  • Tests verify both the immediate fix and edge cases

🚀 Technical Excellence

channelDecryptionService.ts

  • Clean extension of DecryptionResult interface
  • Proper protobuf field extraction with fallbacks
  • Maintains backwards compatibility

mqttIngestion.ts

  • Handles both camelCase and snake_case field variants
  • Proper validation of emoji/replyId values (> 0 check)
  • Maintains the critical TCP convention for message row IDs

unifiedRoutes.ts

  • Smart merge logic that preserves populated values over null/empty ones
  • Follows the established pattern for upgrading cross-source data
  • Maintains canonical timestamp and reception ordering

📊 Impact Assessment

User Experience

  • ✅ Fixes flickering tapback reactions in unified view
  • ✅ Ensures consistent behavior across TCP and MQTT sources
  • ✅ No breaking changes for existing functionality

System Reliability

  • ✅ Addresses non-deterministic behavior from Promise.all race conditions
  • ✅ Maintains data integrity across source boundaries
  • ✅ Preserves all existing functionality

🎯 Recommendation

APPROVE - This is a high-quality fix that:

  • Solves a complex cross-source synchronization issue
  • Includes comprehensive test coverage
  • Follows all established codebase patterns
  • Contains no security or performance concerns
  • Demonstrates excellent engineering practices

The PR is ready for merge. The systematic approach to identifying and fixing each root cause, combined with thorough testing and clear documentation, makes this an exemplary contribution to the codebase.


@Yeraze Yeraze merged commit 23d6459 into main May 20, 2026
16 checks passed
@Yeraze Yeraze deleted the fix/unified-tapback-rendering branch May 20, 2026 15:59
Yeraze added a commit that referenced this pull request May 20, 2026
* chore(release): bump version to 4.6.3

Five user-visible files bumped per the CLAUDE.md version recipe:
package.json, package-lock.json (regenerated), helm/meshmonitor/Chart.yaml,
desktop/src-tauri/tauri.conf.json, desktop/package.json. CLAUDE.md
banner line bumped to match.

CHANGELOG entry covers the five PRs since 4.6.2-1:
- #3105 unified tapback metadata fix
- #3106 docs: drop worktree restriction
- #3107 meshcore contact advType persistence
- #3108 MQTT channel permissions via channel_database
- #3109 hint banner Catppuccin restyle
- #3110 node.channel ingest + traceroute/neighbor channel gate

Companion blog post (docs/blog/2026-05-20-v4.6.3-permissions.md)
walks operators through the new Virtual Channel Permissions flow,
the map-visibility behavior change, and the floating-lines fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(release): regenerate desktop/package-lock.json for 4.6.3

The desktop sub-project carries its own lockfile and the bump to 4.6.3
left it pinned to 4.6.1. The Windows Desktop CI job runs `npm install`
without `--legacy-peer-deps` and fails on the package.json /
package-lock.json version mismatch. Regenerate to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant