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>
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>
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>
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>
Bridge captures ACK packets from meshcli stdout (json_log_rx),
persists to .acks.jsonl, and exposes /ack_status endpoint.
Delivery status is merged server-side into DM messages and
displayed as a green checkmark with SNR/route tooltip.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Compute packet_hash from pkt_payload (SHA-256 of type byte + payload)
and generate analyzer.letsmesh.net links. Button appears on both sent
and received messages when echo data is available.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Deduplicate 2-char repeater codes in echo badge (same repeater via
different routes was shown twice, e.g., "3 (d1, 5e, 5e)")
- Use deduplicated count for unique repeaters, not unique full paths
- Improve incoming path correlation: widen window to 10s, prefer
path_len match but fall back to timestamp-only if needed
- Add debug logging for incoming path correlation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Show repeater path codes in sent message echo badge (e.g., "2 (5e, d1)")
- Capture and display route path for incoming messages in message meta
- Persist all echo data to .echoes.jsonl (survives container restarts)
- Load echo data from disk on startup with 7-day retention and compaction
- Combine sent echo and incoming path data in single /echo_counts response
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Track how many repeaters heard and forwarded sent channel messages,
similar to the Meshcore mobile app's "Heard X repeats" feature.
- Bridge: Detect GRP_TXT echoes from stdout, track unique paths
- Bridge: New /register_echo and /echo_counts API endpoints
- API: Register messages for echo tracking after send
- API: Merge echo counts into /api/messages response
- Frontend: Display green badge with broadcast icon next to Resend button
- CSS: Echo badge styling with dark mode support
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add server-side API for console history (GET/POST/DELETE)
- Add history dropdown button with clock icon
- Save commands to server after execution
- Load history from server on page load
- History persists between sessions and works across devices
- Max 50 commands stored, duplicates moved to end
- Dropdown shows most recent commands first
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The scheduler now uses the timezone configured in .env (TZ variable)
instead of hardcoded UTC:
- Add get_local_timezone_name() helper to manager.py
- BackgroundScheduler uses system local timezone
- API returns timezone field in cleanup-settings response
- Frontend displays timezone next to hour selector and in status text
- Updated documentation to reflect timezone behavior
This makes the cleanup hour more intuitive for users in non-UTC timezones.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Allow users to select the hour (0-23 UTC) when automatic contact
cleanup runs:
- Add hour selector dropdown in Advanced Filters (disabled until enabled)
- Hour field saved to .webui_settings.json with cleanup_settings
- API validates hour (0-23), scheduler uses CronTrigger with hour param
- Status text shows configured hour (e.g., "Enabled (runs daily at 03:00 UTC)")
- Documentation updated in user-guide.md
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add auto-cleanup feature that runs daily at 01:00 UTC using APScheduler:
- New cleanup settings stored in .webui_settings.json (enabled, types, date_field, days, name_filter)
- API endpoints: GET/POST /api/contacts/cleanup-settings
- Scheduler functions: _cleanup_job(), schedule_cleanup(), init_cleanup_schedule()
- UI toggle in Advanced Filters with validation (requires days > 0)
- Debounced auto-save for filter criteria changes
- Protected contacts are excluded from auto-cleanup
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add ability to protect contacts from accidental deletion via Contact
Cleanup tool. Protected contacts are stored locally in settings file.
Changes:
- Add GET /api/contacts/protected endpoint
- Add POST /api/contacts/<key>/protect toggle endpoint
- Add is_protected field to /api/contacts/detailed response
- Exclude protected contacts from cleanup preview and deletion
- Add Protect toggle button to Existing Contacts list
- Show lock icon for protected contacts
- Disable Delete button for protected contacts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds webhook-based update system that allows triggering updates
directly from the mc-webui menu. Includes:
- Webhook server (updater.py) on port 5050
- Systemd service and install script
- API proxy endpoints for container-to-host communication
- Update modal with progress tracking and auto-reload
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- version.py now captures and exports GIT_BRANCH
- Display branch badge next to version in menu (e.g., "2026.01.20+abc1234 [dev]")
- /api/version now returns branch field
- /api/check-update uses frozen branch instead of hardcoded "dev"
- Allows proper update checking for both dev and main branches
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add /api/check-update endpoint that queries GitHub API
- Compare current commit hash with latest on dev branch
- Add check button next to version in menu
- Show spinning icon during check, green checkmark when done
- Display "Update available" link when newer version exists
- Handle rate limits and network errors gracefully
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 60s cache for /api/contacts/detailed (reduces 4 USB calls to 0)
- Optimize /api/messages/updates to read file once instead of N×2 times
- Parallelize initialization: timestamps loaded together, messages and
geo cache loaded in parallel
- Defer checkForUpdates() to after messages are displayed
- Remove blocking checkForUpdates() from loadChannels()
- Add cache invalidation for contacts on add/delete operations
- Add performance timing logs to browser console
Expected improvement: ~10-20s → ~2-3s initial load time
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bridge now detects device name from meshcli prompt ("DeviceName|*")
and exposes it via /health endpoint. mc-webui fetches this at startup
and uses RuntimeConfig for dynamic device name throughout the app.
Fallback chain: prompt detection → .infos command → MC_DEVICE_NAME env var
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Changed hard limit from 8 to 40 channels (most LoRa devices support up to 40)
- Added soft warning when exceeding 7 channels (some devices may have lower limits)
- Warning displayed in UI after successful channel creation/join
- Updated error message to reflect new 40-channel maximum
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add query parameter 'types' to GET /api/contacts/pending endpoint
- Save user's type filter selection to localStorage (pendingContactsTypeFilter)
- Restore filter on page reload (Pending Contacts page)
- Contact Management badge shows count based on saved filter
- Default filter: CLI only (type=1)
Frontend changes:
- Add localStorage functions (save/load/set checkboxes)
- Modify loadPendingContacts() to use types from checkboxes
- Modify loadContactCounts() to use filter from localStorage
- Checkbox changes trigger save to localStorage + API reload
Backend changes:
- Add 'types' query parameter to GET /api/contacts/pending
- Filter pending contacts by type before returning
- Validate types parameter (must be 1-4)
This allows users to customize which contact types they want to see
in pending contacts list, and the selection persists across sessions.
The same filter is used consistently across all pages (Pending Contacts
page and Contact Management page).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Removed Node Discovery feature that was experiencing persistent timeout
issues. Feature attempted to scan mesh network for nearby repeaters but
consistently failed due to bridge timing constraints.
Changes:
- Remove node_discover() function from cli.py
- Remove 'node_discover' from SPECIAL_COMMANDS in api.py
- Remove Discover Nodes button and modal from base.html
- Remove discoverNodes() and displayNodeDiscoveryResults() from app.js
- Remove Discover Nodes documentation from README.md
IMPORTANT: Advert message cleanup ("Advert sent") is preserved and
working correctly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Integrate pending contacts refactoring with JSON format
- Resolve conflict in special commands handler
- Keep node_discover functionality from main
- Add filtering and batch approval from dev
- Change backend from 'pending_contacts' to '.pending_contacts' command
- Parse JSON response with enriched contact data (type, GPS, timestamps)
- Add type badges (CLI/REP/ROOM/SENS) with color coding
- Add Map button for contacts with GPS coordinates
- Add type filter (checkboxes, default: CLI only) and name search
- Add batch approval with confirmation modal
- Follow existing contacts UI pattern for consistency
- Mobile-first design with touch-friendly controls
Breaking change: /api/contacts/pending response format changed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implemented new "Discover Nodes" feature in Network Commands menu:
- Added .node_discover command to meshcli wrapper (cli.py)
- Created interactive modal with sortable table showing nearby repeaters
- Displays SNR, RSSI, path length, and signal quality indicators
- Added refresh functionality to rescan for nodes
Fixed advert notification to show clean "Advert sent" message
instead of full meshcli output.
Technical changes:
- app/meshcore/cli.py: Added node_discover() function with JSON parsing
- app/routes/api.py: Updated SPECIAL_COMMANDS and execute_special_command()
to handle node_discover return type and clean advert message
- app/templates/base.html: Added "Discover Nodes" menu button and modal
- app/static/js/app.js: Added discoverNodes() and displayNodeDiscoveryResults()
- README.md: Added documentation for Discover Nodes feature
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace localStorage-based message read tracking with server-side storage
to enable unread badge synchronization across all devices and browsers.
Changes:
- Add read_status.py module for server-side read status management
- Add GET /api/read_status endpoint to fetch read status
- Add POST /api/read_status/mark_read endpoint to update read status
- Update app.js to load/save read status from server instead of localStorage
- Update dm.js to load/save DM read status from server instead of localStorage
- Read status stored in MC_CONFIG_DIR/.read_status.json for persistence
Benefits:
- Unread badges sync across all devices (phone, computer, tablet)
- Read status persists across browser sessions
- No more duplicate unread notifications when switching devices
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Remove impractical path length filter from contact cleanup feature.
The out_path_len parameter is rarely useful as most contacts have
value -1 (no route), making this filter unpractical for real-world use.
Changes:
- Remove path_len parameter from backend API endpoints
- Remove path length input field from HTML template
- Remove path_len collection from JavaScript code
- Update documentation (CLAUDE.md, README.md)
- Simplify cleanup filter to: name, types, date field, and days
Backend changes:
- Update _filter_contacts_by_criteria() to remove path_len logic
- Remove path_len validation from both endpoints
- Update API documentation in docstrings
Frontend changes:
- Remove Path Length input section from contacts-manage.html
- Remove path_len from collectCleanupCriteria() function
Documentation changes:
- Update API endpoint descriptions in CLAUDE.md
- Update cleanup instructions in README.md
- Remove path_len from example use cases
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Problem:
- meshcli's remove_contact command requires full public key
- We were sending only 12-char prefix, causing 'Unknown contact' errors
- Contacts appeared in list but couldn't be deleted
Solution:
- API now includes full_public_key in /api/contacts/detailed response
- Frontend uses full_public_key when available, falls back to prefix
- Added detailed logging to track deletion attempts
This fixes the issue where contacts (especially with last_seen=4)
could not be removed from the device.
Add comprehensive debug logging to trace last_seen data flow:
cli.py (get_contacts_with_last_seen):
- Log apply_to command success/failure with error details
- Log number of bytes returned by command
- Log number of contacts parsed from JSON
- Log sample contact with public_key prefix and last_advert value
- Log raw output on JSON parse errors
api.py (get_contacts_detailed_api):
- Log number of detailed contacts retrieved
- Log number of matched contacts after merging
- Fix case sensitivity: normalize both prefix and full_key to lowercase
This will help identify why all contacts show "Last seen: Unknown".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add comprehensive "last seen" tracking for all contact types:
Backend (cli.py):
- New function get_contacts_with_last_seen() using 'apply_to t=1,t=2,t=3,t=4 contact_info'
- Fetches detailed contact metadata including last_advert timestamps
- Returns dictionary indexed by full public_key for efficient lookup
API (api.py):
- Enhanced /api/contacts/detailed endpoint to merge last_seen data
- Matches contacts by public_key_prefix (first 12 chars)
- Graceful fallback if detailed fetch fails (contacts still displayed without last_seen)
Frontend (contacts.js):
- formatRelativeTime() - converts Unix timestamps to human-readable format
("5 minutes ago", "2 hours ago", "3 days ago")
- getActivityStatus() - returns status indicator based on recency:
🟢 Active (< 5 min), 🟡 Recent (< 1 hour), 🔴 Inactive (> 1 hour)
- Contact cards now display "Last seen" with status icon and relative time
- Clean handling of missing last_seen data (shows "Unknown")
This feature helps users identify active vs. inactive contacts at a glance,
using the last_advert field from meshcli's contact_info command.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The DM button is now only shown for users who are in the device's contacts
list, ensuring that direct messages will actually be delivered. This prevents
users from attempting to send DMs to recipients who cannot receive them.
Changes:
- Added parse_contacts() and get_contacts_list() functions to cli.py for parsing
meshcli contacts output
- Created /api/contacts endpoint to retrieve contact names from device
- Modified frontend app.js to fetch and cache contacts list on page load
- Updated createMessageElement() to conditionally render DM button only when
sender is in contacts list
- Updated README.md with note about DM button visibility requirement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Remove workaround for meshcore-cli 1.3.11 bug where SENT_MSG contained
sender name instead of recipient. Now using native SENT_MSG entries from
.msgs file with correct recipient and sender fields (requires meshcore-cli >= 1.3.12).
Changes:
- Add _parse_sent_msg() to parse SENT_MSG from .msgs file
- Update read_dm_messages() to process both PRIV and SENT_MSG from .msgs
- Remove save_sent_dm(), _read_sent_dm_log(), _parse_sent_dm_entry()
- Remove dm_sent_log_path property from config
- Add _cleanup_old_dm_sent_log() to remove obsolete log file
- Update comments and documentation
Benefits:
- Single source of truth (.msgs file only)
- Simpler codebase (-95 lines)
- No custom workarounds
- Better data consistency
Related: meshcore-cli update to 1.3.12 (commit ad4a7b3)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Replace DM modal with full-page view at /dm route for better mobile UX
- Add workaround for meshcore-cli bug where SENT_MSG contains sender's
name instead of recipient - now saving sent DMs to separate log file
- Fix DM button styling to match Reply button (btn-outline-secondary)
- Add dm.js for DM page functionality
- Add dm.html template with green navbar for visual distinction
- Update menu link to navigate to /dm instead of opening modal
- Remove unused DM modal functions from app.js
- Update documentation with new DM workflow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Parse PRIV (incoming) and SENT_MSG (outgoing) message types
- Add DM API endpoints: conversations, messages, updates
- Implement conversation grouping by pubkey_prefix or name
- Add timeout-based delivery status (pending → timeout)
- Add DM modal with conversation list and thread views
- Add dual notification badge (blue=channels, green=DM)
- Add DM button next to Reply on channel messages
- Include message deduplication for both incoming and outgoing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add new menu section "Network Commands" with two special commands:
- Send Advert: sends single advertisement (recommended for normal use)
- Flood Advert: floods network with advertisement (for recovery only)
Changes:
- cli.py: Add advert() and floodadv() functions
- api.py: Add POST /api/device/command and GET /api/device/commands endpoints
- base.html: Add Network Commands section to slide-out menu
- app.js: Add JavaScript handlers with confirmation for floodadv
- README.md: Document new Network Commands feature
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Allow joining public channels (starting with #) without encryption key
- Frontend: Make key field optional with validation for # channels
- Backend: Update API to accept optional key parameter
- CLI wrapper: Build meshcli command dynamically based on key presence
- Implement automatic message cleanup when deleting channels
- Add delete_channel_messages() function to remove channel history
- Integrate cleanup into DELETE /api/channels endpoint
- Prevents message leakage when reusing channel slots
- Update documentation with new features and usage instructions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Root cause: Multiple concurrent meshcli calls were fighting for USB access,
causing "Protocol error" and 504 Gateway Timeouts.
Changes to meshcore-bridge:
- Add threading.Lock to serialize meshcli subprocess calls
- Prevent concurrent USB access that causes OSError [Errno 71]
- Reduce DEFAULT_TIMEOUT from 30s to 10s
- Add detailed logging for lock acquisition and release
Changes to main API:
- Implement 30s cache for get_channels() to reduce USB calls
- Cache invalidation after channel create/join/delete operations
- Use cached channels in /api/channels and /api/messages/updates
- Reduce HTTP timeout from 30s to 12s (10s bridge + 2s buffer)
Impact:
- Eliminates race conditions when page loads (multiple API calls)
- Prevents USB port conflicts and protocol errors
- Faster response times due to caching
- No need for manual USB resets after container restarts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replaces the blind 60-second refresh with a smart polling system that only updates the UI when new messages actually arrive.
Key improvements:
- Lightweight update checks every 10 seconds (vs full refresh every 60s)
- Chat view refreshes only when new messages appear on active channel
- Notification bell with global unread count across all channels
- Per-channel unread badges in channel selector (e.g., "Malopolska (3)")
- Last-seen timestamp tracking per channel with localStorage persistence
- Bell ring animation when new messages arrive
Backend changes:
- New /api/messages/updates endpoint for efficient update polling
- Returns per-channel update status and unread counts
Frontend changes:
- Smart auto-refresh mechanism with conditional UI updates
- Unread message tracking system with localStorage
- Notification bell UI component with badge
- Channel selector badges for unread messages
- CSS animations for bell ring effect
This dramatically reduces network traffic and server load while providing better UX through instant notifications about activity on other channels.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>