644 Commits

Author SHA1 Message Date
MarekWo 2a3a48ed5f feat(contacts): add ignored and blocked contact lists
- New DB tables: ignored_contacts, blocked_contacts (keyed by pubkey)
- Ignored contacts: cached but excluded from pending/auto-add
- Blocked contacts: ignored + messages hidden from chat (stored in DB)
- Backend: filter in _on_new_contact, _on_channel_message, _on_dm_received
- API: /contacts/<pk>/ignore, /contacts/<pk>/block toggle endpoints
- API: filter blocked from /api/messages and /dm/conversations
- Frontend: Ignore/Block buttons on pending cards, existing cards, chat messages
- Frontend: source filter dropdown with Ignored/Blocked options
- Frontend: status icons (eye-slash, slash-circle) on contact cards
- Frontend: real-time blocked message filtering via socketio
- Name→pubkey mapping for chat window block/ignore buttons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:10:21 +01:00
MarekWo b709cc7b14 feat(contacts): add device/cache source icon to contact cards
Green chip icon for device contacts, grey cloud icon for cache-only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:03:46 +01:00
MarekWo 09fbc56956 feat(contacts): complete cache functionality, fix display bugs
- _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>
2026-03-09 08:51:25 +01:00
MarekWo 34b6e9b1ec fix(contacts): fallback add to mc.contacts after approve
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>
2026-03-07 13:37:51 +01:00
MarekWo 6c34ce85d8 fix(contacts): sync device↔DB contacts, restore contact cache
- 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>
2026-03-07 13:10:13 +01:00
MarekWo b516d4e370 docs: clarify DM contact issue — device table vs DB mismatch
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>
2026-03-07 12:50:01 +01:00
MarekWo 808a9a6bb3 docs: add v1 to v2 migration guide
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>
2026-03-07 12:46:10 +01:00
MarekWo 53928390c8 fix(contacts): soft-delete contacts to preserve DM history
- 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>
2026-03-07 08:29:44 +01:00
MarekWo 66fa261151 fix(dm): prevent orphaned DMs on contact deletion, improve relinking
- 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>
2026-03-07 08:09:24 +01:00
MarekWo d1ce3ceb92 fix(dm): refresh mc.contacts on approve, DB name fallback, relink orphans
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>
2026-03-07 07:41:15 +01:00
MarekWo 1a3a1e937c fix(dm): clear error when contact not on device, log error_code detail
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>
2026-03-07 07:32:56 +01:00
MarekWo 6a5fe98e32 debug(dm): add detailed logging for device send errors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:31:28 +01:00
MarekWo 94f1bd98de fix(dm): don't require contact in mc.contacts to send DM
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>
2026-03-07 07:26:24 +01:00
MarekWo 8f31c27360 fix(dm): resolve short pubkey prefix to full key on incoming DM
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>
2026-03-07 07:19:46 +01:00
MarekWo 5b757e9548 feat(dm): add delivery confirmation, retry, and receiver-side dedup
- 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>
2026-03-07 07:02:58 +01:00
MarekWo c1b0085710 feat(watchdog): skip USB reset if TCP connection is used
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.
2026-03-06 09:53:06 +00:00
MarekWo dc8c7ad1d6 fix(channels): pre-allocate reader.channels to prevent lib corruption
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>
2026-03-06 10:05:17 +01:00
MarekWo 8821892b4c fix(channels): stop channel iteration on consecutive empty slots
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>
2026-03-06 10:02:43 +01:00
MarekWo 97323649c7 feat(tcp): document TCP connection in .env.example + fix defaults
- 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>
2026-03-06 07:27:08 +01:00
MarekWo 5c47a5b617 fix(watchdog): add logical USB unbind/bind and authorized toggle
When a native USB ESP32 device freezes, ioctl reset or DTR/RTS is often ignored. This uses sysfs unbind/bind and authorized toggles to forcefully drop the device from the kernel logic, causing it to re-enumerate cleanly without physical power cycles.
2026-03-03 20:28:40 +00:00
MarekWo 02b75c167b fix(watchdog): add ESP32 hardware reset via DTR/RTS
Since a standard USB bus reset often isn't enough to revive a hung ESP32, this adds a serial DTR/RTS toggle sequence (used by esptool) to physically reset the chip before trying a USB bus reset.
2026-03-03 20:20:52 +00:00
MarekWo d079f97a38 fix(watchdog): stop container before resetting USB bus
This prevents the container from holding the serial port open during the hardware reset, which was causing the reset to fail or the device to re-enumerate on a different port.
2026-03-03 20:13:19 +00:00
MarekWo ad8c5702f9 feat(watchdog): monitor mc-webui logs for unresponsive LoRa device
The v2 branch consolidated meshcore-bridge into mc-webui. Watchdog now:
- Monitors mc-webui logs for specific device connection errors
- Automatically restarts the container when errors are detected
- Performs a hardware USB bus reset if errors persist across 3 restarts
- Updated README.md to reflect the removal of meshcore-bridge
2026-03-03 20:01:46 +00:00
MarekWo d6a7354f06 fix(channels): use device-reported max_channels instead of hardcoded 8
Firmware reports MAX_GROUP_CHANNELS (typically 40 for companion builds)
in the DEVICE_INFO response. Fetch it at startup and use it in all
channel iteration loops. Previously hardcoded range(8) prevented
channels 8+ from appearing and blocked adding new channels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 20:38:15 +01:00
MarekWo 9b206beeac fix(echo): validate channel hash before correlating echo with sent message
Pending echo correlation was assigning ANY first echo to the sent message,
even if it came from a different channel. This caused cross-channel mismatches
(e.g., Public channel echo assigned to #krakow message).

Fix: check that pkt_payload's first byte (channel_hash = sha256(secret)[0])
matches the channel we sent on before accepting correlation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 20:18:32 +01:00
MarekWo ac1667bd01 fix(ui): fall back to echo SNR when message SNR is missing
meshcore library doesn't always provide SNR in CHANNEL_MSG_RECV events.
Use the first echo path's SNR as fallback for inline display.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:25:25 +01:00
MarekWo ba990b155f fix(ui): fix leading pipe separator when SNR is missing from message
Use array-based metaParts.join(' | ') instead of string concatenation
to avoid ugly leading "| Hops: 0" when meshcore lib doesn't provide SNR.
Also revert temporary INFO-level debug logging back to DEBUG.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:19:09 +01:00
MarekWo 7bcd6bd216 debug: add INFO logging for RX_LOG_DATA events and subscriptions 2026-03-02 16:56:43 +01:00
MarekWo 9f249a4521 feat(echoes): add RX_LOG_DATA echo tracking + sent message pkt_payload correlation
- 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>
2026-03-02 16:53:12 +01:00
MarekWo 44832ada5e fix(analyzer): fix pkt_payload computation for block-aligned text + trailing whitespace
Two bugs in Analyzer URL generation:
1. Firmware omits null+padding when header+text exactly fills AES block boundary
   (len % 16 == 0), but our code always added \0+padding → wrong MAC → wrong hash
2. content.strip() in event handler removed trailing whitespace that was part of
   the original packet. Now uses raw_text from raw_json (preserves original text)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:53:59 +01:00
MarekWo 2a3a00e654 fix(contacts): refresh contacts on unknown advert + cleanup fixes
When an ADVERTISEMENT arrives for a pubkey not in mc.contacts, the
firmware has auto-added a new contact. Trigger ensure_contacts() to
refresh the contact list and get the name. Also: remove contacts from
mc.contacts cache on delete, add reject/clear pending API endpoints,
promote advert logging to INFO level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 08:30:07 +01:00
MarekWo 37694dde09 fix(contacts): remove from pending list after approve + add reject/clear
approve_contact now removes the contact from mc.pending_contacts after
successful approval. Added reject_contact (remove without adding) and
clear_pending_contacts methods with API endpoints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 08:14:10 +01:00
MarekWo 2c547ee1fc fix(perf): disable auto_update_contacts to prevent serial blocking
auto_update_contacts=True triggers ensure_contacts() on every
ADVERTISEMENT event, fetching 324+ contacts over serial (several seconds).
This blocks the serial port and delays MESSAGES_WAITING processing,
causing 10-30s message reception delays. Contacts are synced at startup
and updated individually via NEW_CONTACT events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 07:48:08 +01:00
MarekWo 63f2473933 fix(contacts): respect manual approval setting in NEW_CONTACT handler
When manual approval is enabled in settings, _on_new_contact now skips
DB upsert and emits SocketIO event for pending contacts. When auto mode
is on, contacts are added to DB immediately as before. Also enriched
approve_contact and get_pending_contacts with full contact data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 07:45:44 +01:00
MarekWo 499759931c feat(analyzer): compute pkt_payload from channel secrets for Analyzer URLs
meshcore v2 doesn't provide pkt_payload in events, so compute it
lazily in the API from channel secrets + message data. Analyzer URLs
now appear for ALL messages (own and incoming), not just own.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 07:38:44 +01:00
MarekWo e37ab4243c Revert "fix(contacts): respect manual approval + fix message delay"
This reverts commit 6bb985f9c4.
2026-03-01 21:57:37 +01:00
MarekWo 6bb985f9c4 fix(contacts): respect manual approval + fix message delay
1. Manual approval: _on_new_contact now checks self_info
   manual_add_contacts flag. When enabled, new contacts stay in
   mc.pending_contacts for UI approval instead of auto-adding to DB.

2. Message delay: disable auto_update_contacts which was triggering
   full contact list refresh (270+ records over serial) on every
   ADVERTISEMENT event, blocking message reception for seconds.
   Contact names for adverts are looked up from cached mc.contacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:46:08 +01:00
MarekWo db5aac084c fix(adverts): fix empty names + enable auto_update_contacts
- ADVERTISEMENT events only contain public_key — look up name/type/lat/lon
  from mc.contacts instead of the empty payload
- Enable mc.auto_update_contacts so meshcore refreshes contacts after adverts
- Fix NEW_CONTACT handler to store lat/lon/last_advert (was only storing
  name and type)
- Fix type field: NEW_CONTACT uses 'type' not 'adv_type'
- Graceful handling of set_manual_add_contacts firmware incompatibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:02:49 +01:00
MarekWo 1df8fa03f9 debug: add ADVERT/NEW_CONTACT payload logging 2026-03-01 18:42:29 +01:00
MarekWo b034a181ce feat(retention): add message retention scheduling (Task 2.6)
- 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>
2026-03-01 17:28:54 +01:00
MarekWo d89e276054 feat(api): add advertisement history API endpoint (Task 2.8)
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>
2026-03-01 17:26:37 +01:00
MarekWo 5df10f0ab9 feat(v2): Expand console router with status, channels, help commands
- Add 'status' command: connection, name, battery, contacts count
- Add 'channels' command: list configured channels (0-7)
- Add 'help' command: list all available commands with descriptions
- Update unknown command message to suggest 'help'

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 17:23:40 +01:00
MarekWo f8c7bfb115 feat(v2): Add SocketIO real-time push for channel messages page
- Add SocketIO /chat client to app.js for real-time message updates
- Listen for new_message (channel) events, refresh current channel
- Update unread badges for other channels in real-time
- Listen for device_status to update connection indicator
- Reduce polling interval from 10s to 60s (fallback only)
- Include socket.io.min.js in base.html template

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 17:21:56 +01:00
MarekWo ebfe383190 feat(v2): Add SocketIO real-time push for DM page
- Add SocketIO /chat client to dm.js for real-time DM and ACK updates
- Listen for new_message (dm), ack, device_status events
- Remove 5x cascading refresh after send (replaced by SocketIO ACK)
- Reduce polling interval from 10s to 60s (fallback only)
- Add data-ack attribute to status icons for real-time ACK updates
- Enrich ACK emission with snr/rssi/route_type (device_manager.py)
- Include socket.io.min.js in dm.html template

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 17:19:54 +01:00
MarekWo e18ad0f7a3 fix(v2): Fix echo enrichment bug + add analyzer URL to channel messages
Bug: echo enrichment at api.py:384 used leaked `row` variable from
previous loop — all messages got echo data from the LAST DB row.

Fix: include pkt_payload in message dict during conversion loop,
then enrich each message with its own echo data and analyzer URL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 17:16:50 +01:00
MarekWo 7f9aa4ac58 fix(v2): Truncate pubkey in DM placeholder using displayName() helper
currentRecipient keeps full value for sending, displayName() used for
all placeholder/dropdown display to truncate 64-char pubkeys to 8+...

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 16:31:21 +01:00
MarekWo 3e81eeeae7 fix(v2): Fix last_advert timestamps and DM placeholder display
- Sync last_advert from device contacts as Unix timestamp (was missing)
- Convert _on_advertisement to store Unix timestamp (was ISO string)
- Add _parse_last_advert() to handle both ISO and Unix formats in API
- Truncate full pubkey to short prefix in DM placeholder and dropdown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 16:23:45 +01:00
MarekWo c20e7c20ad fix(v2): Fix DM contact names showing as pubkey prefix
- DM handler: don't overwrite contact name with prefix when name unknown
- Migration: upsert contact with sender name from v1 PRIV entries
- Fixes conversations showing "4e45565e" instead of "demo mc-webui"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:58:15 +01:00
MarekWo 4b463fecfa fix(v2): Resolve DM sender pubkey prefix to full key from contacts
Incoming DM events only contain a short pubkey_prefix. Now resolves it
to the full public_key via mc.get_contact_by_key_prefix() so incoming
and outgoing messages end up in the same conversation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:46:10 +01:00
MarekWo 95dcf38d06 fix(v2): Handle bytes expected_ack from meshcore + DM prefix matching
- 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>
2026-03-01 13:36:37 +01:00