Redesign DM chat contact selector:
- Replace <select> dropdown with searchable text input + filtered dropdown
- Show only device contacts (from /api/contacts/detailed), not all cached
- Sort contacts alphabetically, conversations by recency
- Type badge (CLI/REP/ROOM/SENS) shown in dropdown items
- Keyboard support: Enter selects first match, Escape closes
Add Contact Info modal (replaces Retry toggle in header):
- Shows contact name, type, public key, last advert, path/route, GPS
- Auto Retry toggle moved into modal footer
- Designed for future extensibility (manual path setting etc.)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a DM is retried, the retry send generates a NEW ack code. The
backend correctly maps retry ack → dm_id via _pending_acks, but
the WebSocket emit was sending the retry ack code. The frontend DOM
still has data-ack="<original_ack>" from the first send, so it could
never match retry ACKs → delivery checkmark never appeared.
Now both _on_ack() and _confirm_delivery() look up the original
expected_ack from the database before emitting to the frontend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Path buffer from firmware contains trailing garbage bytes beyond the
actual hop data. out_path_len encodes both hop count (lower 6 bits)
and hash size (upper 2 bits). Now we:
- Truncate out_path to meaningful bytes (hop_count * hash_size)
- Format as readable E7→DE→54→54→D8 instead of raw hex string
- Show hop count derived from actual path arrows
Example: out_path_len=5 with out_path="e7de5454d81c49dfb86f8a"
now correctly displays as "E7→DE→54→54→D8 (5 hops)" instead of
showing the full 11-byte buffer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Console `contacts` now shows device-only contacts with path info
(matching meshcore-cli format: name, type, pubkey, path)
- New `contacts_all` command shows all contacts (device + cached from DB)
- Contact cards in UI now always show routing mode for device contacts
(Flood, Direct 0 hop, or hex path with hop count)
- Fix path_or_mode computation: prioritize out_path over out_path_len
to handle firmware edge case where out_path exists but out_path_len=-1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- DIRECT (known path): 10 attempts (8 DIRECT + 2 FLOOD), 30s wait between
- FLOOD (no path): 3 attempts only, 60s wait between
- Prevents flooding the mesh with rapid retries when no path is known
- Longer DIRECT wait gives ACKs more time to return through multi-hop paths
- Log retry mode at task start for easier debugging
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Cancel retry task immediately when _on_ack or PATH handler confirms delivery
- Keep pending_acks for 60s after retry exhaustion so late ACKs are matched
- Prevents orphaned ACKs (no dm_id) when ACK arrives shortly after exhaustion
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of reloading the entire message list when echo data arrives,
now updates only the affected message elements in the DOM:
- Add data-msg-id attribute to message wrappers for targeted lookup
- Add GET /api/messages/<id>/meta endpoint returning metadata for a
single message (computes pkt_payload, looks up echoes, analyzer URL)
- Replace loadMessages() echo handler with refreshMessagesMeta() that
finds messages missing metadata and updates them individually
- Fix path_len=0 treated as falsy (use ?? instead of ||)
Flow: message appears instantly via WebSocket (with SNR + hops), then
~2s later echo data triggers targeted meta fetch → route info and
analyzer button appear smoothly without any chat window reload.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The backend already emits 'echo' SocketIO events when RX_LOG_DATA arrives
with route/path data, but the frontend wasn't listening. Now the frontend
handles echo events with a debounced loadMessages() refresh (2s delay) to
pick up computed pkt_payload, analyzer_url, hops, and route info.
This fixes messages appearing without metadata until manual page refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The SocketIO new_message emit for channel messages was missing snr,
path_len, pkt_payload and analyzer_url fields, causing messages received
via WebSocket to render without metadata until a full page refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Channel names from device already include # prefix — removed hardcoded #
from search results badge. Added (?) help button with search syntax
examples and link to FTS5 docs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep direct path retries longer before falling back to flood mode,
giving more time for ACK delivery on known routes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Stats: battery fallback used ${bat} on an object — now uses battery_mv
from core stats when dedicated get_bat returns null
- Info: remove old v1 regex JSON parsing, use v2 dict response directly
- Search FAB: change from bright orange to muted teal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Search, backup, stats endpoints used current_app.config.get('DEVICE_MANAGER')
which doesn't exist — replaced with _get_dm()/_get_db() helpers
- /api/device/info used old v1 CLI — replaced with DeviceManager.get_device_info()
returning structured dict instead of string (fixes map own device marker)
- Moved search button from navbar to FAB menu (between filter and DM buttons)
- Bump SW cache to v7
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The /chat namespace had no server-side connect handler registered. With
python-socketio 5.x (always_connect=False), client connections to
unregistered namespaces are silently rejected. This caused all SocketIO
events (new_message, ack, echo) to never reach the frontend — messages
only appeared via the 60s polling fallback.
Fixes:
- Add @socketio.on('connect', namespace='/chat') handler in main.py
- Add optimistic message append: sent messages appear instantly before
API round-trip (eliminates 3-4s serial command delay)
- Skip own-message SocketIO events to prevent duplicates
- Add connect_error handler for frontend debugging
- Bump SW cache to v6
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The _db reference is set by init_retention_schedule() which runs after
schedule_daily_archiving(). The backup job checks _db at runtime.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Own device shown as red star marker on map (from self_info GPS)
- Contact popups now show "Last seen: X min ago" from last_advert
- New formatTimeAgo() utility for relative timestamps
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New GET /api/device/stats endpoint (core, radio, packets, DB stats)
- Device Info modal now has Info and Stats tabs
- Stats tab shows: battery, uptime, TX/RX air time, packet counts,
DB row counts, and database size
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- stats: device uptime, TX/RX air time, packet counts, errors
- telemetry <name>: request sensor data from remote node
- neighbors <name>: list neighbors of a remote node
- trace [tag]: send trace packet for mesh topology discovery
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- POST /api/backup/create — trigger immediate backup
- GET /api/backup/list — list backups with sizes
- GET /api/backup/download — download backup file
- Backup modal accessible from menu with create/download buttons
- Daily automatic backup via APScheduler (configurable hour/retention)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New GET /api/messages/search endpoint using existing FTS5 indexes
- Search modal accessible from navbar search icon
- Debounced search (300ms) across all channel and DM messages
- Results show source (channel/DM), sender, timestamp with highlights
- Click result navigates to the relevant channel or DM conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of reloading all messages 3 times (1s, 6s, 15s) after sending,
the sent message now appears instantly via SocketIO new_message event.
Only one deferred reload remains at 15s to pick up echo data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Map modal (from main menu) now has a "Cached" toggle switch that,
when enabled, also displays contacts stored in the DB cache alongside
device contacts. Cached markers are slightly smaller and more
transparent to visually distinguish them from device contacts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both /api/messages/updates (channel) and /api/dm/updates endpoints
now filter out blocked contacts when computing unread counts, so
badge numbers no longer include messages from blocked users.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
showToast is only defined in contacts.js, not app.js. The chat page
uses showNotification. The ReferenceError was silently caught, preventing
loadBlockedNames() and loadMessages() from executing after blocking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added client-side blocked name filtering in displayMessages() as
defense-in-depth alongside server-side filtering. This ensures blocked
sender messages are hidden immediately after blocking from chat.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
applySortAndFilters() had duplicate const declarations for sourceFilter
and selectedSource, causing a SyntaxError that prevented the entire
contacts.js from loading. Both Pending and Existing pages were broken.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New blocked_names table for blocking bots without known public_key
- get_blocked_contact_names() returns union of pubkey-blocked + name-blocked
- POST /api/contacts/block-name endpoint for name-based blocking
- GET /api/contacts/blocked-names-list for management UI
- Block button always visible in chat (falls back to name-based block)
- Blocked Names section shown in Existing Contacts Blocked filter
- CSS breakpoint for icon-only buttons: 768px → 428px (iPhone-sized)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix pending contacts always showing "CLI" — compute type_label in API
- Remove Copy Key button from pending cards, make key clickable instead
- Responsive contact buttons: icon+text on desktop, icon-only on <=768px
- Add flex-wrap for button rows on small screens
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- _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>