- Clock command now shows datetime like meshcore-cli: "Current time: 2026-03-19 11:39:07 (1773916747)"
- Repeater req_* commands: pass timeout=0 to meshcore library so it uses
device's suggested_timeout instead of hardcoded 30s (matching meshcore-cli behavior)
- Execute timeout raised to 120s to accommodate slow repeater responses
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add device management: get/set params, clock/clock sync, time,
reboot, ver, scope, self_telemetry, node_discover.
Add channel management: get_channel, set_channel, add_channel,
remove_channel. Update help text with all command categories.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 9 new console commands for repeater management:
login, logout, cmd, req_status, req_regions, req_owner,
req_acl, req_clock, req_mma. Add resolve_contact helper
and _parse_time_arg utility. Update help text with categories.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Delete button now sends public_key instead of name to avoid matching
wrong contacts when multiple share similar names.
2. _on_advertisement adds cache-only contacts to mc.pending_contacts when
manual approval is enabled, so they appear in the pending list after
advertising (even if meshcore fires ADVERTISEMENT instead of NEW_CONTACT).
3. Added Delete button for cache-only contacts with dedicated
/api/contacts/cached/delete endpoint and hard_delete_contact DB method.
4. approve_contact/reject_contact now handle DB-only pending contacts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shows an X button when a conversation is selected, allowing quick
clearing of the search field to find another contact.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add resolveConversationName() that prioritizes device contacts over
backend display_name (which falls back to pubkey when DB JOIN fails)
- Add isPubkey() guard to prevent overwriting good names with hex strings
- Add arrow key navigation (Up/Down) in searchable contact dropdown
- Auto-focus message input after selecting contact from dropdown
- Skip filtering when search input contains a pubkey (show all contacts)
- Keep search input and placeholder in sync with best known name
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Prevent mousedown on dropdown from stealing focus (which closed the
dropdown before click could register on desktop)
- After selecting from dropdown, override search input with the known
name to guarantee correct display even if prefix match fails
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When restoring a conversation from localStorage, the saved ID may have
a different pubkey prefix length than the API returns (e.g. pk_e4ce0a07
vs pk_e4ce0a075359459f...). Now selectConversation() does prefix
matching against dmConversations and upgrades the stored ID, so the
display name is resolved correctly instead of showing raw pubkey prefix.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>