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>
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>
- New `contact_paths` table for storing multiple user-configured paths per contact
- New `no_auto_flood` column on contacts to prevent automatic DIRECT→FLOOD reset
- Path rotation during DM retry: cycles through configured paths before optional flood fallback
- REST API for path CRUD, reorder, reset-to-flood, repeater listing
- Path management UI in Contact Info modal: add/delete/reorder paths, repeater picker with uniqueness warnings, hash size selector (1B/2B/3B)
- "No Flood" per-contact toggle in modal footer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All settings (protected_contacts, cleanup_settings, retention_settings,
manual_add_contacts) moved from .webui_settings.json file to SQLite database.
Startup migration auto-imports existing file and renames it to .json.bak.
Added safeguard in _on_new_contact: if firmware fires NEW_CONTACT for a
contact already on the device, skip pending and log a warning. Also added
diagnostic logging showing previous DB state (source, protected) when
contacts reappear as pending.
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>
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>
- 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>
- _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>
- 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>
- 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>
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>
- 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>
- 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>
- 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>
- 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>