Move DM timestamp+status row above the message bubble (consistent with
group messages). Increase delivery/SNR meta font size and adjust color
for better readability in both light and dark themes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BlueZ auto-reconnects trusted BLE devices after container restart,
blocking bleak from establishing a new GATT session. Clear the stale
connection via D-Bus before each connect attempt.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BLE connections with retries can take >60s, exceeding the startup
wait timeout. Move runtime_config.set_device_name() into _connect()
so the navbar shows the correct name regardless of connection delay.
Also fixes name update on reconnections.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DM delivery status was lost when switching conversations because
_confirm_delivery() only stored the ACK record and emitted a socket
event, but never set delivery_status='delivered' in direct_messages.
During retries, each attempt generates a new ACK code. The DM record
stores the initial expected_ack, but the actual ACK may arrive for a
later retry's code. The ACK lookup by expected_ack then fails to match.
Now _confirm_delivery() also sets delivery_status='delivered', and
message loading checks this DB field first (like it already did for
'failed'), so delivery persists across page navigations.
Also fixed 213 existing DMs on server via data migration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
bluetoothctl info auto-connects to trusted devices, stealing the
connection from Docker — document hcitool as safe alternative and
add connection loop recovery steps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MC_BLE_PIN was non-functional — bleak in Docker cannot perform
interactive pairing (no BlueZ agent). Pairing must be done on
the host before starting mc-webui. Added comprehensive pairing
guide at docs/meshcore_bluetooth_pairing.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Integrate meshcore library's BLE connection (via bleak) as a third
transport option alongside serial and TCP. Priority: BLE > TCP > Serial.
Config: MC_BLE_ADDRESS and MC_BLE_PIN environment variables.
Docker: bluez/dbus packages, NET_ADMIN cap, D-Bus socket mount.
UI: transport type badge in navbar, transport_type in /api/status.
Watchdog: skip USB reset for BLE connections (same as TCP).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Explains the 4 delivery scenarios, how settings map to behavior,
why actual wait times can exceed configured intervals (firmware
suggested_timeout), and what to look for in the System Log.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Contact Info dialog showed stale path data (e.g. "Flood" instead of
the discovered route) because auto_update_contacts is OFF and PATH_UPDATE
only sets _contacts_dirty=True without refreshing mc.contacts. The API
then served stale in-memory data even after cache invalidation.
Now ensure_contacts(follow=True) is called on PATH_UPDATE to read fresh
contact data from the device before invalidating cache and emitting the
socket event. PATH_UPDATE events are rare (only on path discovery), so
the serial I/O cost is acceptable unlike advertisements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When FLOOD delivery is confirmed, the PATH_UPDATE event payload often
has empty path data because firmware updates the contact's out_path
asynchronously. After 3s delay, read the contact's updated path from
the meshcore library's in-memory contacts dict and backfill the DB.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When both ACK and PATH_UPDATE fire for FLOOD delivery, _on_ack may
store empty path before PATH_UPDATE can provide the discovered route.
Now _on_path_update also checks for recently-delivered DMs with empty
delivery_path and backfills with the discovered path from the event.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Retry counter now renders as a dm-delivery-meta div above the Resend
button instead of inline next to it, matching the position of the
post-delivery info. Prevents text from crowding the button on short
messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Long routes (>4 hops) show truncated with dotted underline; clicking
opens a popup with the full route and hop count, same style as channel
message path popups. Short routes (<=4 hops) display inline as before.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When PATH_UPDATE confirms delivery, use the actual path from the
event data instead of the empty path_desc from _retry_context (which
is empty during FLOOD phase). This captures the route firmware
discovered via the flood delivery.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add hex validation to formatDmRoute to avoid garbling old "DIRECT"
values. When no hex route available (FLOOD delivery), fall back to
delivery_route from ACK (e.g. show "FLOOD" stripped of PATH_ prefix).
Ensures delivery meta always shows something useful.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The _on_ack handler cancels the retry task before _retry() can store
delivery info (attempt count, path). Fix by maintaining a _retry_context
dict updated before each send. _on_ack reads context and stores delivery
info + emits dm_delivered_info BEFORE cancelling the task. Same fix
applied to PATH_UPDATE backup delivery handler.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Store actual hex path instead of DIRECT/FLOOD labels in delivery_path.
Format route as AB→CD→EF (same as channel messages, truncated if >4
hops). Add dm_delivered_info WebSocket event so delivery meta appears
in real-time without needing page reload. Remove path info from failed
messages since it's not meaningful for undelivered messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the attempt counter (e.g. "Attempt 15/24") from next to the status
icon to below the message text, left of the Resend button. Add visible
delivery meta line for delivered/failed messages showing attempt count
and path used. Store attempt info for failed messages too. Replace
Polish abbreviations (ŚK, ŚD, ŚG) with English in all log messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show retry progress in DM message bubble via WebSocket:
- "attempt X/Y" counter updates in real-time during retries
- Failed icon (✗) when all retries exhausted
- Delivery info persisted in DB (attempt number, path used)
Backend: emit dm_retry_status/dm_retry_failed socket events,
store delivery_attempt/delivery_path in direct_messages table.
Frontend: socket listeners update status icon and counter,
delivered tooltip shows attempt info and path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace 3-way branching (configured_paths/has_path/else) with
4-scenario matrix based on (has_path × has_configured_paths):
- S1: No path, no configured paths → FLOOD only
- S2: Has path, no configured paths → DIRECT + optional FLOOD
- S3: No path, has configured paths → FLOOD first, then ŚD rotation
- S4: Has path, has configured paths → DIRECT on ŚK, ŚD rotation, optional FLOOD
Key changes:
- S3: FLOOD before configured paths (discover new routes)
- S4: exhaust retries on current ŚK before rotating ŚD
- S4: dedup ŚG/ŚK to skip redundant retries on same path
- Add _paths_match() helper for path deduplication
- Update tooltip text for settings clarity
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The path_changed socket handler was skipping the refresh when Contact
Info modal was closed. This meant contactsList stayed stale, so opening
the modal later still showed outdated path info. Now always refreshes
contactsList on any path_changed event.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Path info in Contact Info modal was stale due to 60s server cache
and no refresh after path operations. Now:
- Invalidate contacts cache after reset_path, change_path, path_update
- Emit 'path_changed' socket event on PATH_UPDATE from device
- UI listens and re-renders Contact Info when path changes
- Reset to FLOOD button immediately refreshes the path display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The contact list in Existing/Pending Contacts was not using all available
space due to calc(100vh - ...) and max-height rules overriding the
flexbox layout. Remove fixed height constraints from #pendingList and
#existingList in both contacts_base.html and style.css, letting the
flexbox chain (body > main > container > pageContent > list) fill the
remaining viewport space.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Global .contact-name (1.1rem/600) was bleeding into DM sidebar items.
Added explicit 0.88rem/400 override for .dm-sidebar-item .contact-name.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create theme.css with CSS custom properties for light/dark themes
- Dark theme inspired by demo landing page (deep navy palette)
- Update style.css: replace ~145 hardcoded colors with CSS variables
- Extract inline styles from index.html, contacts.html, dm.html to style.css
- Add Appearance tab in Settings modal with theme selector
- Bootstrap 5.3 data-bs-theme integration for native dark mode
- Theme persisted in localStorage, applied before CSS loads (no FOUC)
- Console and System Log panels unchanged (already dark themed)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a Share tab to the Device Info modal that generates a QR code
and copyable URI (meshcore://contact/add?...) for sharing the device
contact with other users.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On screens >= 992px (lg breakpoint), show a persistent sidebar panel:
- Group chat: channel list with unread badges, active highlight, muted state
- DM: conversation/contact list with search, unread dots, type badges
- Desktop contact header with info button replaces mobile selector
- Mobile/narrow screens unchanged (dropdown/top selector still used)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stage 2 of manual contact add feature:
- POST /api/contacts/manual-add endpoint (URI or raw params)
- New /contacts/add page with 3 input tabs (URI, QR code, Manual)
- QR scanning via html5-qrcode (camera + image upload fallback)
- Client-side URI parsing with preview before submission
- Nav card in Contact Management above Pending Contacts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The /api/contacts/detailed endpoint has a 60s cache. Without invalidation
after push-to-device or move-to-cache, the UI showed stale data until
cache expired, making it look like the operation didn't work.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enable moving contacts between device and cache directly from the
Existing Contacts UI:
- "To device" button on cache-only contacts (pushes to device)
- "To cache" button on device contacts (removes from device, keeps in DB)
This helps manage the 350-contact device limit by offloading inactive
contacts to cache and restoring them when needed.
- Add DeviceManager.push_to_device() and move_to_cache() methods
- Add API endpoints: POST /contacts/<pk>/push-to-device, move-to-cache
- Add UI buttons with confirm dialogs in contacts.js
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The meshcore library's update_contact() reads out_path_hash_mode directly
from the contact dict. Without it, add_contact_manual() fails with
KeyError: 'out_path_hash_mode'. Default value 0 is correct for new
contacts with no known path (flood mode).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add support for adding contacts manually using the MeshCore mobile app URI
format (meshcore://contact/add?name=...&public_key=...&type=...) or raw
parameters (public_key, type, name). This enables contact sharing between
mc-webui and the MeshCore Android/iOS app via URI/QR codes.
- Add parse_meshcore_uri() helper to parse mobile app URIs
- Add DeviceManager.add_contact_manual() using CMD_ADD_UPDATE_CONTACT
- Update import_contact_uri() to handle both mobile app and hex blob URIs
- Add manual_add console command with two usage variants
- Update console help text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The DISCOVER_RESPONSE payload uses 'pubkey' and 'node_type', not
'public_key'/'name'/'adv_name'. Now shows pubkey prefix, resolved
contact name, node type, SNR, and RSSI. Also rename CLI->COM type.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backup filenames now derive from the active DB stem (e.g. mc_9cebbd27.2026-03-24.db).
Listing and cleanup glob *.db so existing mc-webui.* backups remain visible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
'from app.main import db' gets None because python -m app.main loads the
module as __main__, creating a separate module instance from app.main.
Use current_app.db (Flask app context) instead — same pattern as api.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove contacts_cache.jsonl and adverts.jsonl file I/O — all contact
data is already in the SQLite contacts/advertisements tables. Clean up
stale JSONL files (acks, echoes, path, dm_sent) at startup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace file-based .read_status.json with DB-backed read_status table.
One-time migration imports existing data at startup. The read_status.py
module keeps the same public API so route handlers need no changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DB filename changes from {device_name}.db to mc_{pubkey[:8]}.db,
making it stable across device renames and preparing for multi-device support.
Existing databases are auto-migrated at startup by probing the device table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_load_channel_secrets() cached secrets in memory only. After dfc3b14
switched /api/messages to use DB channels instead of device calls,
the empty channels table caused Route info and Analyzer links to
disappear from message bubbles.
Now upserts each channel (name + secret) to DB during startup so
the API can compute pkt_payload without hitting the device.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
meshcore 2.3.0's ConnectionManager has a bug: when auto-reconnect creates
a new TCP connection, the old connection's connection_lost callback fires,
triggering another reconnect cycle. Since each success resets the attempt
counter, this loops forever (~1 TCP connection/second).
Disabled library auto_reconnect and added reconnection logic to
_on_disconnected() with 3 attempts and increasing backoff (5/10/15s).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
/api/messages and /api/messages/updates called get_channels_cached()
which blocks on device communication when cache is cold (up to 240s).
Now uses DB-cached channels for pkt_payload computation instead.
Frontend loadMessages() now has a 15s timeout with auto-retry
and clears the loading spinner on error instead of leaving it
spinning indefinitely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
'No Flood' was confusing next to the 'Reset to FLOOD' button.
'Keep path' better describes the behavior: don't auto-reset
the path to FLOOD after failed direct retries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PATH_ROTATION now has 3 phases:
1. Exhaust retries on primary path first (initial send + retries_per_path-1)
2. Rotate through remaining non-primary paths
3. Optional FLOOD fallback (if no_auto_flood=False)
Previously, retry iterated all paths in sort_order giving the primary
path only the initial send attempt before switching to the first path
on the list, which was often an older/worse path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_change_path_async manually set out_path and out_path_len on the contact
dict then called update_contact(contact) with path=None. This path reads
out_path_hash_mode from the contact dict, which is -1 when the contact
is in flood mode (after reset_path or device read with plen=255).
The encoding then produced: hop_count | (-1 << 6) = negative number,
causing "can't convert negative int to unsigned" in to_bytes().
Fix: use mc.commands.change_contact_path() which properly computes all
fields including out_path_hash_mode, avoiding the negative value issue.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>