Add Map button between Copy Key and Delete buttons on existing contacts cards.
Button appears only when contact has GPS coordinates (adv_lat/adv_lon != 0).
Opens contact location in Google Maps in new tab.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Move toast container from bottom-right to top-left corner
- Reduce notification display time from 3-5s to 1.5s
- Prevents notifications from blocking message input area
- Applied consistently across all pages (main, DM, contacts)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Problem: List was too tall causing the entire page to scroll,
hiding navigation buttons at the top when scrolling to bottom.
Solution:
- Changed from max-height to height for #existingList
- Desktop: calc(100vh - 260px) - slightly smaller to fit all elements
- Mobile: calc(100vh - 300px) - adjusted for mobile layout
Now only the contact list scrolls internally, while navigation
buttons remain visible at the top.
Problem: #existingList ID selector had higher specificity than
.contacts-list-fullscreen class, and max-height values were limiting
the list height regardless of the class height setting.
Changes:
- Desktop: #existingList max-height calc(100vh - 400px) → calc(100vh - 240px)
- Mobile: #existingList max-height calc(100vh - 450px) → calc(100vh - 280px)
This should now properly utilize available screen space.
Changed height calculation:
- Desktop: calc(100vh - 300px) → calc(100vh - 240px)
- Mobile: calc(100vh - 340px) → calc(100vh - 280px)
This should eliminate the ~20% unused whitespace at the bottom
of the contact lists.
Adjusted .contacts-list-fullscreen height to better utilize available
screen space by increasing the subtracted value from 200px to 300px
on desktop and from 150px to 340px on mobile.
This accounts for:
- Navbar (~56px)
- Main container padding (~48px)
- Back buttons (~60px)
- Search toolbar (~50px)
- Filter/sort toolbar (~60px)
- Margins between elements (~26px)
Fixes blank space at bottom of Existing Contacts and Pending Contacts pages.
Testing revealed that meshcli's remove_contact command only works
with contact names, not with public keys (neither prefix nor full key).
Example test results:
- remove_contact bd030a71e091 → Unknown contact
- remove_contact bd030a71e091b14e...f7a → Unknown contact
- remove_contact "Progres-SLU-Lubsza-test" → SUCCESS
Changed frontend to send contact name as selector.
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.
Added logging to see exactly what meshcli returns when removing a contact.
This will help diagnose why contacts are not being deleted despite
returning success status.
The cleanupBtn element only exists on the contact management page,
not on the main chat page. This was causing JavaScript to crash
with 'cannot access property addEventListener of null' error,
preventing messages from loading.
Added null check before attaching event listener.
- Add CSS override to enable scrolling on Contact Management pages
(override global overflow: hidden from style.css)
- Move cleanup description to tooltip on info icon for consistency
with Manual approval toggle pattern
- Add flexbox layout to cleanup section h6 for proper icon alignment
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Previous line-based parsing failed (0 contacts parsed despite receiving data).
New approach:
- Use brace-matching algorithm to find complete JSON objects {...}
- Works for both single-line and prettified (multi-line) JSON
- Added logging of first 500 chars of output for debugging
This handles the case where bridge may return prettified JSON instead
of newline-delimited JSON.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Root cause: apply_to contact_info returns NDJSON (newline-delimited JSON),
not a JSON array. Each contact is a separate JSON object on its own line.
Changes:
- Call apply_to separately for each type (t=1, t=2, t=3, t=4) instead of
using comma-separated list which returns 0 matches through bridge
- Parse NDJSON format: each line is a separate JSON object
- Skip non-JSON lines (prompt echoes "MarWoj|*", summary "> N matches")
- Collect all contacts from all types into single dictionary
- Add detailed logging for each type and total count
This matches the actual meshcli output format observed in interactive mode.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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>
Compact Manual Approval and Pending Contacts sections to give more
space for Existing Contacts list, which is the primary focus.
Changes to Manual Approval section:
- Convert from full section to single-line compact control
- Replace description text with tooltip icon (hover for info)
- Reduce vertical space by ~70px
Changes to Pending Contacts section:
- Reduce header size from h5 to h6
- Compact empty state (1rem padding vs 3rem)
- Reduce icon size (1.5rem vs 3rem)
- Limit list height to 200px (was 600px)
- Remove "Refresh" text from button (icon only)
Changes to Existing Contacts section:
- Dynamic height: calc(100vh - 400px) with 300px minimum
- Adapts to viewport height automatically
- On mobile: calc(100vh - 450px) for better fit
- More contacts visible without scrolling
Other improvements:
- Initialize Bootstrap tooltips in contacts.js
- Smaller fonts and margins throughout
- Better vertical space distribution
Result: ~150px more space for main contacts list on desktop,
~200px more on mobile. Tooltip provides same info without clutter.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fix scrolling issue where contact lists with many items (263/350)
couldn't be scrolled on desktop or mobile devices.
Changes:
- Add max-height: 600px to #existingList and #pendingList
- Enable vertical scrolling with overflow-y: auto
- Add smooth scrolling for mobile with -webkit-overflow-scrolling
- Style custom scrollbar (8px, rounded, gray) for better UX
Tested: Lists now show ~8-10 contact cards at once with scrollbar
appearing when needed. Works with mouse wheel and touch gestures.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Issue: Messages sent to Public channel had visible double quotes around
multi-word text (e.g., "Hello world" appeared as "Hello world" in chat).
Root cause: In interactive mode, meshcli's 'public' command treats quotes
literally as part of message content, while 'chan' command correctly parses
them as argument delimiters.
Solution: Use 'chan 0' for Public channel instead of 'public' command.
This ensures consistent quote handling across all channels.
Before:
- Public (ch 0): public "message" → quotes visible in output
- Other channels: chan <nb> "message" → quotes correctly parsed ✓
After:
- All channels: chan <nb> "message" → consistent behavior ✓
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Two improvements to DM functionality:
1. Removed DM button from message blocks in channel view
- Users should use the DM page directly instead
- Cleaner UI without redundant buttons
2. Filter only CLI (client) contacts in DM dropdown
- Added filter_types parameter to parse_contacts()
- get_contacts_list() now returns only CLI contacts
- Repeaters (REP), rooms (ROOM), and sensors (SENS) are excluded
- You can't send DMs to repeaters anyway!
Updated README.md to reflect these changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changed DM approach from conditional button visibility to showing all
available contacts directly in the DM page dropdown. This provides better
UX and performance.
Changes:
- Reverted conditional DM button visibility in app.js (button always shows)
- Removed contacts loading from main page (app.js)
- Added loadContacts() function to dm.js to fetch contacts from API
- Modified populateConversationSelector() to show:
1. Existing conversations (with history) first
2. Separator: "--- Available contacts ---"
3. All contacts from device who aren't in conversations yet
- Users can now start new DM conversations with any contact
- Updated README.md with new DM workflow description
Benefits:
- Simpler and more intuitive UX
- Better performance (no checks on every message)
- Users can proactively start conversations
- Clear visibility of who's available for DM
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Previous regex stopped at first space, causing names like "daniel5120 🔫"
to be parsed as just "daniel5120", breaking DM button visibility checks.
Changed parsing logic to split by 2+ consecutive spaces (column separator
in meshcli output) and extract full contact name before type column.
🤖 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>
Fix channel message sender name display. In meshcore-cli 1.3.12, the 'name'
field in SENT_CHAN entries now contains the channel name instead of the
sender's device name. Update parser to use the 'sender' field instead.
Before: Sent messages showed "channel 0" as sender
After: Sent messages correctly show "MarWoj" (actual sender name)
This change is consistent with the SENT_MSG parsing for Direct Messages.
🤖 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>
- Change DM menu link from <a> to <button> with onclick for better
mobile compatibility (fixes link not working on some devices)
- Add proper flexbox layout styles for DM page viewport height
- Use 100dvh (dynamic viewport height) for mobile browsers
- Add flex-shrink: 0 to form and status bar to prevent overflow
- Reduce padding in form and status bar for more compact mobile view
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix duplicate conversations in dropdown by merging pk_ and name_ IDs
- Add intelligent refresh (only reload when new messages arrive)
- Fix message alignment (own messages right, others left)
- Change sent message status from 'timeout' to 'pending'
- Add emoji picker button to DM page
- Change message limit from 200 to 140 bytes (consistent with channels)
- Update README.md with corrected DM documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 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>
- Change message byte limit from 200 to 140 bytes
- Remove keyboard hint text to save space
- Simplify navbar to show only notification bell and channel selector
- Add slide-out offcanvas menu for less-used options:
* Refresh Messages
* Manage Channels
* Message History (archive selector)
* Settings
- Increase font sizes in dropdowns and menu for better readability
- Add icons and text labels to all menu items
- Auto-close menu after selecting options
- Update clipboard API to modern navigator.clipboard with fallback
🤖 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>
- Add timeout protection (10s for channels, 15s for updates)
- Implement fallback mechanism to ensure Public channel always exists
- Fix race condition by loading channels before setupAutoRefresh()
- Add proper validation for API responses and channel data
- Improve error handling with detailed logging
- Prevent checkForUpdates() from running before channels are loaded
This fixes the recurring issue where the channel dropdown would
randomly become empty, especially visible on mobile devices.
🤖 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>
BREAKING CHANGE: Switched from single-container to multi-container setup
This commit introduces a meshcore-bridge service that isolates USB device
access from the main application, resolving persistent USB timeout and
deadlock issues in Docker + VM environments.
Changes:
- Add meshcore-bridge/ - Lightweight HTTP API wrapper for meshcli
- Flask server exposes /cli endpoint (port 5001, internal only)
- Exclusive USB device access via --device flag
- Health check endpoint at /health
- Refactor app/meshcore/cli.py
- Replace subprocess calls with HTTP requests to bridge
- Add requests library dependency
- Better error handling for bridge communication
- Update docker-compose.yml
- Define meshcore-bridge and mc-webui services
- Create meshcore-net Docker network
- Add depends_on with health check condition
- Bridge gets USB device, main app uses HTTP only
- Modify Dockerfile
- Remove meshcore-cli installation from main app
- Lighter image without gcc dependencies
- Update config.py
- Add MC_BRIDGE_URL environment variable
- Remove meshcli_command property (no longer needed)
- Update documentation (README.md, .claude/instructions.md)
- Document 2-container architecture
- Add troubleshooting section for bridge
- Update prerequisites (no host meshcore-cli needed)
- Add architecture diagram in project structure
Benefits:
✅ Solves USB device locking after container restarts
✅ Restartable main app without USB reset
✅ Better separation of concerns
✅ Easier debugging (isolated meshcli logs)
✅ No manual USB recovery scripts needed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Improve UI layout on mobile devices (screens < 768px):
Navbar optimizations:
- Hide "Refresh" text on mobile, show only icon to save space
- Reduce gap between navbar elements (0.25rem instead of 0.5rem)
- Decrease min-width for channel/date selectors (80px vs 120px/150px)
- Smaller font-size (0.75rem) and padding for select elements
- Compact button sizes (0.25rem padding)
Modal optimizations:
- Reduce modal margins (0.5rem) for better mobile fit
- Smaller modal-body padding (0.75rem)
- Compact channel keys with smaller font (0.65rem) and word-break
- Reduce list-group-item padding (0.5rem)
- Stack "Add New" and "Join Existing" buttons vertically on very small screens (<400px)
Files changed:
- app/static/css/style.css: Added mobile-specific CSS rules in @media query
- app/templates/base.html: Wrapped "Refresh" text in <span class="btn-text">
Testing: Verified on mobile viewport (iPhone, Android simulators)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fix critical JavaScript error that prevented channel selector from working:
- Add null check for selectedOption before accessing .text property
- Prevents "can't access property 'text', undefined" error at line 106
- This error occurred when selector options were being rebuilt
Additional cleanup:
- Remove duplicate escapeHtml() function definition
- Add detailed console logging to loadChannels() for debugging
Root cause: When populateChannelSelector() clears and rebuilds options,
the change event can fire with an invalid selectedIndex, causing
e.target.options[e.target.selectedIndex] to return undefined.
Bug reported by user: TypeError at app.js:106 when switching channels
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fix issue where channel selector would show empty or missing channels
after auto-refresh. The populateChannelSelector() function was clearing
all options but not rebuilding them from API data correctly.
Changes:
- Rebuild entire dropdown from API response (including Public channel)
- Use currentChannelIdx from global state for selection
- Fallback to Public (index 0) if saved channel no longer exists
- Remove unused variable (IDE warning fix)
Bug: Channel selector showing empty after first auto-refresh
Root cause: API returns ALL channels including Public at index 0,
but the function was clearing all options and not adding them back.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added favicon support with multiple sizes for different platforms (16x16, 32x32, 180x180 for Apple, 192x192 and 512x512 for Android). Includes PWA manifest.json for standalone app display with Bootstrap primary theme color.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added a professional emoji picker widget to make it easier to insert emoji on desktop browsers where native emoji input is not readily available.
Changes:
- Added emoji-picker-element library from CDN (~50KB)
- New emoji button (😊) next to the Send button
- Full emoji picker with categories and search functionality
- Emoji inserted at cursor position in textarea
- Automatic byte counter update after insertion
- Mobile responsive: 6 columns on mobile, 8 on desktop
- Click outside to close picker
Benefits:
- Easy emoji access on Windows/Linux desktop browsers
- No need to use Win+. shortcut or copy-paste
- Professional UI with search and categories
- Still works with native emoji keyboards on mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>