- _on_new_contact() in manual mode: upsert to DB as cache (source='advert')
so contacts appear in @mentions and Cache filter before approval
- _on_advertisement(): check mc.pending_contacts for name/metadata fallback
- get_pending_contacts(): include last_advert in response
- /api/contacts/cached: return numeric last_advert timestamp
- contacts.js: fix adv_lat/adv_lon field names (was c.lat/c.lon),
use last_advert timestamp instead of last_seen datetime string
- upsert_contact: source priority — never downgrade 'device' to 'advert'
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ensure_contacts(follow=True) may not return the just-added contact.
Add manual fallback to mc.contacts so /api/contacts/detailed and
send_dm can find it immediately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- get_contacts_with_last_seen() reads from mc.contacts (device firmware)
instead of DB, so /api/contacts/detailed returns only device contacts
- _sync_contacts_to_db() now bidirectional: downgrades stale 'device'
contacts to 'advert' (cache-only) when not on device anymore
- delete_contact() sets source='advert' (cache) instead of 'deleted',
keeping contacts visible in @mentions and cache filter
- get_contacts() returns all contacts (no 'deleted' filter needed)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause is device firmware contact table being empty (after reflash
or reset), not the v1→v2 migration itself. DB retains hundreds of
contacts from advert history but device only has those explicitly added.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents breaking changes including the need to re-add DM contacts
to the device firmware table after migration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- delete_contact() now sets source='deleted' instead of SQL DELETE
- get_contacts() filters out deleted contacts (hidden from UI)
- upsert_contact() on re-add overwrites source, auto-undeleting
- DM FK references stay intact, no more orphaned messages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Stop deleting contacts from DB on device removal (preserves DM history)
- Filter NULL contact_pubkey from DM conversations list
- Match outgoing DMs by contact name in raw_json during relinking
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three fixes for DM sending after contact delete/re-add:
1. approve_contact() now calls ensure_contacts() to refresh mc.contacts
so send_dm can find newly added contacts immediately
2. cli.send_dm() falls back to DB name lookup when mc.contacts misses,
preventing the contact name from being passed as a pubkey string
3. approve_contact() re-links orphaned DMs (NULL contact_pubkey from
ON DELETE SET NULL) back to the re-added contact
New DB methods: get_contact_by_name(), relink_orphaned_dms()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Device firmware requires contacts in its table to send DMs. Passing
a raw pubkey string results in error_code=2. Show actionable error
message instead of generic "Device error".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the contact is not found in the in-memory mc.contacts dict,
fall back to passing the pubkey hex string directly to send_msg()
which handles it natively. Fixes "Contact not found" error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a DM arrives with only pubkey_prefix (short hex) and the sender
is not in mc.contacts, fall back to DB prefix lookup to get the full
64-char public key. Prevents ghost contact entries and "Unknown" DM
conversations.
Also adds get_contact_by_prefix() database helper.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix ACK handler bug: read 'code' field instead of 'expected_ack'
- Add DM retry (up to 3 attempts) with same timestamp for receiver dedup
- Add receiver-side dedup in _on_dm_received() (sender_timestamp or time-window)
- Add PATH_UPDATE as backup delivery signal for flood DMs
- Track pending acks with dm_id for proper ACK→DM linkage
- Return dm_id and expected_ack from POST /dm/messages API
- Add find_dm_duplicate() and get_dm_by_id() database helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Since mc-webui can now connect via TCP to a remote proxy instead of local USB/serial device, the hardware USB bus reset logic in Watchdog will no longer blindly attempt a reset on repeated container crashes.
Added \is_tcp_connection()\ helper to read the config and conditionally skip the USB reset if TCP is active.
meshcore lib 2.2.21 bug: reader.py line 434 does
self.channels = self.channels.extend([...])
which sets self.channels = None (extend returns None).
This corrupts ALL subsequent channel message processing.
Fix: after getting max_channels from device_info, pre-allocate
mc._reader.channels to max_channels slots so the extend path
is never triggered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
meshcore lib 2.2.21 has a bug where list.extend() return value (None)
is assigned to self.channels, corrupting state for indices >= 20.
Stop iterating after 3 consecutive empty/failed channels to avoid
hitting this bug and reduce error log spam.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add MC_TCP_HOST and MC_TCP_PORT options to .env.example with clear
documentation for serial vs TCP transport selection
- Change default MC_TCP_PORT from 5000 to 5555 (avoids Flask conflict)
- Wire MC_TCP_HOST/MC_TCP_PORT through docker-compose.yml from .env
(previously was commented-out hardcoded values)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a native USB ESP32 device freezes, ioctl reset or DTR/RTS is often ignored. This uses sysfs unbind/bind and authorized toggles to forcefully drop the device from the kernel logic, causing it to re-enumerate cleanly without physical power cycles.
Since a standard USB bus reset often isn't enough to revive a hung ESP32, this adds a serial DTR/RTS toggle sequence (used by esptool) to physically reset the chip before trying a USB bus reset.
This prevents the container from holding the serial port open during the hardware reset, which was causing the reset to fail or the device to re-enumerate on a different port.
The v2 branch consolidated meshcore-bridge into mc-webui. Watchdog now:
- Monitors mc-webui logs for specific device connection errors
- Automatically restarts the container when errors are detected
- Performs a hardware USB bus reset if errors persist across 3 restarts
- Updated README.md to reflect the removal of meshcore-bridge
Firmware reports MAX_GROUP_CHANNELS (typically 40 for companion builds)
in the DEVICE_INFO response. Fetch it at startup and use it in all
channel iteration loops. Previously hardcoded range(8) prevented
channels 8+ from appearing and blocked adding new channels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pending echo correlation was assigning ANY first echo to the sent message,
even if it came from a different channel. This caused cross-channel mismatches
(e.g., Public channel echo assigned to #krakow message).
Fix: check that pkt_payload's first byte (channel_hash = sha256(secret)[0])
matches the channel we sent on before accepting correlation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
meshcore library doesn't always provide SNR in CHANNEL_MSG_RECV events.
Use the first echo path's SNR as fallback for inline display.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use array-based metaParts.join(' | ') instead of string concatenation
to avoid ugly leading "| Hops: 0" when meshcore lib doesn't provide SNR.
Also revert temporary INFO-level debug logging back to DEBUG.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Subscribe to RX_LOG_DATA events to capture repeated radio packets
- Parse GRP_TXT (0x05) payload to extract pkt_payload and path
- Classify echoes as sent (pending echo correlation) or incoming
- Register pending echo when sending channel messages for pkt_payload capture
- Add update_message_pkt_payload() DB method for sent message correlation
- Return echo_paths/echo_snrs for ALL messages (not just own) in GET /messages
- Frontend: build paths from echo_paths for incoming message route display
- Emit SocketIO 'echo' event for real-time badge updates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two bugs in Analyzer URL generation:
1. Firmware omits null+padding when header+text exactly fills AES block boundary
(len % 16 == 0), but our code always added \0+padding → wrong MAC → wrong hash
2. content.strip() in event handler removed trailing whitespace that was part of
the original packet. Now uses raw_text from raw_json (preserves original text)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an ADVERTISEMENT arrives for a pubkey not in mc.contacts, the
firmware has auto-added a new contact. Trigger ensure_contacts() to
refresh the contact list and get the name. Also: remove contacts from
mc.contacts cache on delete, add reject/clear pending API endpoints,
promote advert logging to INFO level.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
approve_contact now removes the contact from mc.pending_contacts after
successful approval. Added reject_contact (remove without adding) and
clear_pending_contacts methods with API endpoints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
auto_update_contacts=True triggers ensure_contacts() on every
ADVERTISEMENT event, fetching 324+ contacts over serial (several seconds).
This blocks the serial port and delays MESSAGES_WAITING processing,
causing 10-30s message reception delays. Contacts are synced at startup
and updated individually via NEW_CONTACT events.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When manual approval is enabled in settings, _on_new_contact now skips
DB upsert and emits SocketIO event for pending contacts. When auto mode
is on, contacts are added to DB immediately as before. Also enriched
approve_contact and get_pending_contacts with full contact data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
meshcore v2 doesn't provide pkt_payload in events, so compute it
lazily in the API from channel secrets + message data. Analyzer URLs
now appear for ALL messages (own and incoming), not just own.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Manual approval: _on_new_contact now checks self_info
manual_add_contacts flag. When enabled, new contacts stay in
mc.pending_contacts for UI approval instead of auto-adding to DB.
2. Message delay: disable auto_update_contacts which was triggering
full contact list refresh (270+ records over serial) on every
ADVERTISEMENT event, blocking message reception for seconds.
Contact names for adverts are looked up from cached mc.contacts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ADVERTISEMENT events only contain public_key — look up name/type/lat/lon
from mc.contacts instead of the empty payload
- Enable mc.auto_update_contacts so meshcore refreshes contacts after adverts
- Fix NEW_CONTACT handler to store lat/lon/last_advert (was only storing
name and type)
- Fix type field: NEW_CONTACT uses 'type' not 'adv_type'
- Graceful handling of set_manual_add_contacts firmware incompatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add daily retention job that deletes old channel messages, DMs, and
advertisements based on configurable age threshold
- Add GET/POST /api/retention-settings endpoints
- Extend cleanup_old_messages() to optionally include DMs and adverts
- Wire up APScheduler in create_app() (also enables existing archiving
and contact cleanup schedulers that were never started in v2)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add GET /api/advertisements with optional pubkey filter and limit.
Enriches results with contact name lookup from cache.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 'status' command: connection, name, battery, contacts count
- Add 'channels' command: list configured channels (0-7)
- Add 'help' command: list all available commands with descriptions
- Update unknown command message to suggest 'help'
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add SocketIO /chat client to app.js for real-time message updates
- Listen for new_message (channel) events, refresh current channel
- Update unread badges for other channels in real-time
- Listen for device_status to update connection indicator
- Reduce polling interval from 10s to 60s (fallback only)
- Include socket.io.min.js in base.html template
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add SocketIO /chat client to dm.js for real-time DM and ACK updates
- Listen for new_message (dm), ack, device_status events
- Remove 5x cascading refresh after send (replaced by SocketIO ACK)
- Reduce polling interval from 10s to 60s (fallback only)
- Add data-ack attribute to status icons for real-time ACK updates
- Enrich ACK emission with snr/rssi/route_type (device_manager.py)
- Include socket.io.min.js in dm.html template
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug: echo enrichment at api.py:384 used leaked `row` variable from
previous loop — all messages got echo data from the LAST DB row.
Fix: include pkt_payload in message dict during conversion loop,
then enrich each message with its own echo data and analyzer URL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
currentRecipient keeps full value for sending, displayName() used for
all placeholder/dropdown display to truncate 64-char pubkeys to 8+...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Sync last_advert from device contacts as Unix timestamp (was missing)
- Convert _on_advertisement to store Unix timestamp (was ISO string)
- Add _parse_last_advert() to handle both ISO and Unix formats in API
- Truncate full pubkey to short prefix in DM placeholder and dropdown
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- DM handler: don't overwrite contact name with prefix when name unknown
- Migration: upsert contact with sender name from v1 PRIV entries
- Fixes conversations showing "4e45565e" instead of "demo mc-webui"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Incoming DM events only contain a short pubkey_prefix. Now resolves it
to the full public_key via mc.get_contact_by_key_prefix() so incoming
and outgoing messages end up in the same conversation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Convert bytes to hex string for expected_ack and pkt_payload via _to_str()
- Support pubkey prefix matching in get_dm_messages() (LIKE for short keys)
- Fixes "Object of type bytes is not JSON serializable" error on DM view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migration now imports all archive files (oldest first) in addition to the
live .msgs file, with deduplication. Archives endpoint and message history
now query SQLite by date instead of reading .msgs files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>