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>
Reads the existing .msgs JSONL file and imports channel messages and DMs
into the v2 SQLite database. Runs automatically when device connects and
DB is empty. Handles sender parsing, pubkey resolution, and FK constraints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Channel messages from meshcore arrive as "SenderName: message text".
The library doesn't provide sender name separately. Now parsing it
from the text (split on first colon), matching v1 parser behavior.
Also:
- Look up DM sender names from mc.contacts instead of event payload
- Fix SNR field name (uppercase 'SNR' from meshcore library)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The meshcore Event class has 'payload' not 'data'. All event handlers
were silently getting empty dicts, causing:
- Channel messages showing 'Unknown' sender
- Channel info not returning name/secret
- Sent message event data being lost
Also normalizes channel_name/channel_secret keys from CHANNEL_INFO
events and converts secret bytes to hex string.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MeshCore library exposes command methods (get_channel, send_msg,
send_advert, etc.) on mc.commands, not directly on the MeshCore
instance. Updated all DeviceManager calls accordingly.
Fixes: channels not loading, message sending, advert, battery, etc.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The module-level 'from app.main import device_manager' was returning
None in Flask request context even though device_manager was set.
Now tries current_app.device_manager first (Flask app context),
falling back to module import for non-request contexts.
Fixes 500 errors on /api/contacts, /api/contacts/pending,
/api/contacts/detailed, and /api/channels endpoints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add _connect_with_retry() with exponential backoff (10 attempts)
- Guard against self_info being None after meshcore library disconnects
due to unresponsive device
- Prevents crash when device is busy (e.g. held by orphan container)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add _detect_serial_port() to DeviceManager — resolves 'auto' to
actual device via /dev/serial/by-id with common path fallbacks
- Make channel_idx optional in get_channel_messages() so status and
channel-updates endpoints can query across all channels
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add pkt_payload column to direct_messages table for stable packet
hash generation and Analyzer URL linking
- Update insert_direct_message() and DeviceManager to store pkt_payload
- Add test for DM pkt_payload storage (43 tests pass)
- Update watchdog to monitor only mc-webui (meshcore-bridge removed)
- USB reset trigger now fires for mc-webui container failures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update api.py: messages, contacts, DM endpoints read from SQLite DB
- Add DB fallback paths for parser.py backward compatibility
- Replace bridge echo registration with DeviceManager event handling
- Update status endpoint to use db.get_stats()
- Update channel updates/DM updates endpoints for DB queries
- Delete channel messages via DB instead of parser
- Remove meshcore-bridge/ directory (no longer needed in v2)
- Remove MC_BRIDGE_URL from config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
main.py: Initialize Database + DeviceManager in create_app(), replace
bridge-dependent startup code, simplified console command router.
cli.py: All functions now delegate to DeviceManager instead of HTTP
bridge calls. Same signatures preserved for api.py compatibility.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace two-container bridge architecture with single container.
Dockerfile adds udev for serial device support.
docker-compose.yml: one service with cgroup rules for ttyUSB/ttyACM,
SQLite DB path, backup settings, optional TCP mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Background thread runs meshcore async event loop. Supports both
serial and TCP transports. Flask routes bridge sync→async via
execute() method. Event subscriptions marked as TODO for Phase 1.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tables: device, contacts, channels, channel_messages, direct_messages,
acks, echoes, paths, advertisements, read_status.
Includes schema_version for migrations, FTS5 virtual tables with
auto-sync triggers for full-text search on messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add meshcore>=2.2.0 for direct device communication (Phase 0.1)
- Exclude docs/v2/ from git (local working notes)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create v2 branch for mc-webui direct device communication migration
- Add docs/v2/STATUS.md for development progress tracking
- Exclude PRD documents from git (local-only planning docs)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The retry thread was removing pending_flood_acks immediately after
exhausting retries. PATH_UPDATE arriving even 1 second later would
find no pending entry to match, leaving the message undelivered.
Changes:
- Don't clean up pending_flood_acks in retry threads, keep entries
alive for late PATH_UPDATE arrivals
- Add TTL-based cleanup (2 min) in _process_path_update
- When PATH_UPDATE confirms delivery, save synthetic ACK for the
original ack code AND all retry group ack codes so the frontend
ack_status polling finds a match regardless of which code it checks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When sending a DM to a no-path contact, meshcli can return
"Error sending message" if the device-level flood send fails.
Previously this was treated as a terminal failure with no retry.
Now detects the error and starts a background flood retry thread
that re-attempts up to auto_retry_flood_only times (default 3),
waiting between attempts for a PATH_UPDATE to establish the route.
If a retry succeeds, registers for normal delivery tracking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When sending DM to contacts without a known path (flood mode), ACK is
piggybacked inside the PATH response packet and not visible on stdout.
This adds PATH_UPDATE event parsing to confirm flood DM delivery.
Changes:
- Enable print_path_updates in meshcli init for PATH_UPDATE events
- Parse PATH echoes from json_log_rx -> .path.jsonl log (diagnostics)
- Parse PATH_UPDATE events to confirm flood DM delivery by public_key
- Replace "skip retry for no-path contacts" with proper flood retry
(max 3 attempts, matching standard Meshcore app behavior)
- Split _retry_send into _retry_send_flood and _retry_send_direct
- Refactor _get_contact_path_len -> _get_contact_info (returns both
out_path_len and public_key)
- Add GET /paths endpoint for diagnostics
- Add flood_only config param for auto-retry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a contact has no path set (out_path_len == -1), the initial msg
is already sent as flood. Retrying would spam the network with up to
8 flood messages in quick succession.
Now checks contact's out_path_len via .ci command before starting the
retry loop. If no path exists, logs and skips retry entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extracted dedup_retry_messages() helper (300s window, was 120s which
was too tight) and applied it in three places:
- GET /api/dm/messages - already had inline dedup, now uses helper
- get_dm_conversations() - fixes last_message_timestamp inflation
- GET /api/dm/updates - was missing dedup entirely, counted retries
as unread messages (root cause of persistent unread markers)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Monitor: For .msg commands, keep waiting for expected_ack in response
instead of completing on silence timeout. Waits up to half of cmd_timeout
(5s) for the JSON to arrive before giving up.
Unread: Apply 120s time-window dedup to outgoing messages in conversation
listing, so retry messages don't inflate last_message_timestamp and cause
permanent unread markers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Don't abort retry loop when msg command fails or times out - the device
may be temporarily busy (especially during flood mode)
- Add 120s time-window dedup for outgoing messages with same text+recipient
to prevent duplicate messages in chat when retry acks aren't tracked
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 300ms response silence timeout was too short for .msg commands -
the prompt echo arrived quickly but the JSON with expected_ack could
arrive after a gap > 300ms, causing retry to abort mid-sequence.
- Add 1.5s minimum wait for .msg/.m commands in response monitor
- Don't abort retry loop when ack parsing fails (message was still sent)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The msg command in meshcli only outputs JSON (with expected_ack and
suggested_timeout) when json_output=True, triggered by the dot prefix
(e.g., .msg instead of msg). Without JSON output, the bridge couldn't
extract ack codes and retry was never triggered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement automatic retry for DM messages when ACK is not received,
similar to the MeshCore mobile app's Auto Retry feature. The bridge
monitors for ACK after each send and retries up to 3 times, switching
to flood routing after 2 failed direct attempts via reset_path.
- Bridge: background retry engine with configurable max_attempts,
flood_after; retry group tracking to prevent duplicate messages
- Bridge: enhanced ACK status checks retry groups so delivery is
detected even if only a retry attempt's ACK arrives
- Backend: filter retry SENT_MSG duplicates from message list
- Frontend: extended ACK polling window, auto-retry toggle in DM bar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove nested @media (max-width: 400px) rule that forced btn-group
to flex-direction: column, causing buttons to stack on mobile.
Also remove now-unused .list-group-item small styles (channel keys
no longer shown).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add confirm() dialog before marking all messages as read, showing
list of unread channels with counts. Remove channel key/ID from
Manage Channels modal to save vertical space on mobile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ability to mute notifications for individual channels via Manage
Channels modal (bell/bell-slash toggle button). Muted channels are
excluded from unread badge counts, browser notifications, and app icon
badge. Bell icon click now marks all channels as read in bulk.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add person icon button in filter bar that inserts the current device
name into the search field, for filtering own messages
- DM filter bar already benefits from the CSS sibling push-down rule
added in previous commit (same class names used)
- Add collapsible FAB toggle to DM view, same pattern as channel chat
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add collapsible FAB container with chevron toggle button to
temporarily hide floating action buttons that overlap messages
- Make filter bar push messages down instead of overlaying the first
matched message (CSS sibling selector adds padding-top)
- Add @mentions autocomplete to filter search bar - typing @ shows
contact list dropdown, selecting inserts plain name (not @[] format)
so all messages from/mentioning that user are found
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, the internal container port was hardcoded to 5000, so setting
FLASK_PORT to a different value would break the port mapping and healthcheck.
Credit: Tymo3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement a smart auto-detection and low-level fcntl ioctl reset mechanism for LoRa USB devices. This 'last resort' recovery is triggered if the meshcore-bridge container fails to recover after 3 restarts within an 8-minute window. Includes updates to the installer, systemd service, and newly added README.
Co-Authored-By: Gemini CLI <noreply@google.com>
When MC_DEVICE_NAME=auto, _load_echoes() runs with "auto.echoes.jsonl"
which doesn't exist. After actual device name is detected and paths
updated, the data was never reloaded from the correct file, leaving
incoming_paths and echo_counts empty. This caused missing path info
for all messages older than the current bridge session.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Same 1-hour cleanup issue as incoming_paths: sent messages lost
their analyzer links after ~1 hour because echo_counts was pruned
on every new send. Now matches .echoes.jsonl 7-day retention.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The attempt loop (0-3) for matching incoming echo paths left
computed_payload at attempt=3 when no match was found, producing
wrong analyzer hashes. Combined with 1-hour incoming_paths cleanup
in bridge (vs 7-day .echoes.jsonl retention), this caused older
messages to lose both path info and correct analyzer links.
Two fixes:
- Compute base_payload at attempt=0 upfront for analyzer URL
- Extend incoming_paths memory cleanup from 1h to 7 days
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MeshCore encodes lat/lon as little-endian signed int32 divided by 1e6,
not as IEEE 754 floats. This caused all map pins to show at (0,0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Discard NaN, Infinity, and out-of-range lat/lon values from
struct.unpack to prevent JSON parse errors in browser.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract lat/lon from advert payloads (struct unpack from binary)
- Store type_label and lat/lon in cache from device seed and adverts
- Show Map button for cache contacts with GPS coordinates
- Show colored type badge (CLI/REP/ROOM/SENS) for typed cache contacts
- Type filter now works for cache contacts with known type
- Change counter label from "known" to "cached"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces two sort toggle buttons with a single <select> dropdown (e-commerce style)
so all 3 filter/sort controls fit on mobile screens.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Contacts cache accumulates all known node names from device contacts
and adverts into a JSONL file, so @mentions work even after contacts
are removed from the device. Background thread scans adverts every
45s and parses advert payloads to extract public keys and node names.
Existing Contacts page now shows merged view with "Cache" badge for
contacts not on device, plus source filter (All/On device/Cache only).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
bi-flask was added in Bootstrap Icons 1.12+, but project uses 1.11.2.
Replace with bi-clipboard-data which is available and conveys
"data analysis".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generate analyzer_url from computed pkt_payload for all incoming
channel messages, not just those with echo path matches. This means
the analyzer button appears even when no route paths were captured.
Also change analyzer button icon from bi-search (magnifying glass)
to bi-flask (lab flask) to better convey "analysis/inspection".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The parser's .strip() was removing trailing whitespace from message
text, but the encrypted radio payload includes those trailing spaces.
This caused pkt_payload mismatches for messages ending with spaces
(e.g., "Dzień dobry "). Use original unstripped text for raw_text.
Also add debug logging for unmatched messages to help diagnose
remaining edge cases.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace unreliable timestamp-based heuristic (±10s window) with exact
cryptographic matching for incoming channel message routes. Compute
pkt_payload by reconstructing the AES-128-ECB encrypted packet from
message data (sender_timestamp, txt_type, text) + channel secret, then
match against echo data by exact key lookup.
Also accumulate ALL route paths per message (previously only last path
was kept due to dict overwrite), and display them in a multi-path popup
showing SNR and hops for each route.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>