Server:
- GET /v1/whoami returns client IP, IPv4/IPv6 classification
- Detects proxy via X-Forwarded-For, X-Real-IP, Via headers
- Shows proxy details when behind reverse proxy (Caddy etc)
- ConnectInfo enabled for direct socket address
Web client:
- /myip, /whatsmyip, /ip — shows your IP + proxy info
- Useful for testing IPv4/IPv6 connectivity
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
wzp-web --relay only accepts IP:port, not Docker hostnames.
Fixed IP 172.28.0.10 on backend network with explicit subnet.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- FC-P5-T5 added to task plan with full design notes
- OpenMLS/TreeKEM approach: O(log n) key rotation, forward secrecy
- Current group calls marked as transport-encrypted only (QUIC)
- UI warning shown when starting group call
- Updated completed task statuses (read receipts, wrapping, tab complete)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server:
- POST /v1/groups/:name/signal — broadcast plaintext JSON to all
online group members via WS push (for call signals, typing, etc.)
- No auth required (membership validated from 'from' field)
Web client:
- Group call signals now use /signal endpoint (was broken with /send)
- WS handler detects group_call JSON signals early
- Auto-join #ops now properly registers presence + fetches members
- /gmembers resolves ETH addresses via /v1/resolve
- /admin-calls — list all active calls in the system
- /admin-help — list all admin/debug commands
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Direct call ringing:
- Incoming: oscillator ring tone (440/480Hz, on/off pattern)
- Outgoing: ringback tone (2s on, 4s off) while waiting for answer
- Browser Notification API for background incoming calls
- All tones stop on answer/reject/hangup
- Notification permission requested on first click
Group calls:
- /gcall — start a group call (notifies all group members)
- /gjoin — join an active group call
- /gleave-call — leave group call
- /gmute — toggle per-group call notification mute (localStorage)
- Participant tracking (joined/left messages with count)
- Group call signal via group message broadcast (JSON type: group_call)
- Call bar shows "Join Call" button when group call is active
- Audio via same WZP bridge (room = gc-<groupname>)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Web UI:
- Peer input Enter key now resolves ETH/@alias (like /peer command)
- ETH address stored and shown everywhere instead of raw fingerprint
- Call UI shows ETH address: "Calling 0x0021...", "In call with 0x9D70..."
- Server URL color: #444 → #666 (readable on dark background)
- Peer input placeholder: "ETH address, fingerprint, or @alias"
- peerEthAddr persisted in localStorage across sessions
Server:
- WS binary header: strip zero-padding from 64-char to 32-char fingerprint
- Call routing now works (was failing due to padded fingerprint lookup)
- startCall() resolves ETH/alias before sending CallSignal::Offer
- Audio bridge sends auth token to wzp-web as first WS message
- Deterministic room name: sorted fingerprint pair (both peers same room)
Docs updated:
- SERVER.md: WZP integration section (components, running, TLS, auth flow)
- USAGE.md: voice call usage for web and TUI
- LLM_HELP.md: call architecture, key files, environment vars
- LLM_BOT_DEV.md: note that bots cannot participate in calls
- TESTING_E2E.md: updated WZP prerequisites with correct flags
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Web client:
- After call goes "active", connects to WZP web bridge WS
- Mic capture: getUserMedia → ScriptProcessor → PCM int16 frames → WS
- Playback: WS → PCM int16 → Float32 → AudioContext.createBufferSource
- Room name derived from peer fingerprint (deterministic)
- Relay address fetched from /v1/wzp/relay-config
- Audio auto-starts on accept/answer, auto-stops on hangup/reject
- startAudio()/stopAudio() manage full lifecycle
TUI:
- /call shows "Audio: use web client for voice (TUI audio coming soon)"
- Signaling works, audio requires web client for now
This completes the last critical task — voice calls work end-to-end:
User A calls → signaling via featherChat WS → User B accepts →
both connect to WZP relay → audio flows
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- **bold**, *italic*, \`code\` rendered with ratatui styles
- # headers, > blockquotes, - bullet lists
- Multi-line messages split and indented per line
- Code spans: cyan bold, headers: white bold, quotes: gray italic
- No external dependency (custom md_to_spans parser)
- tui-markdown had ratatui version mismatch, built our own
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ChatLine gains sender_fp field for tracking who sent each message
- App gains read_receipts_sent HashSet to avoid duplicate receipts
- After each draw(), visible received messages get a Read receipt sent
- Only fires once per message_id, skips system/self messages
- Sender sees blue ✓✓ (existing display logic already handles Read)
- All ChatLine literals across 6 files updated with sender_fp field
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Call state reload on restart:
- Loads Ringing/Active calls from sled into active_calls on startup
- Expires calls older than 24h automatically
TUI sender ETH cache prefill:
- prefill_eth_cache() resolves all known contacts on poll_loop start
- First message from known contacts now shows ETH address immediately
Server integration tests (10 new):
- push_to_client offline/online
- register_ws + connection cap (5 max)
- is_online + device_count
- kick_device + revoke_all_except
- deliver_or_queue offline/online
- call state lifecycle
- list_devices
155 tests passing (was 135)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FC-P2-T6: /contacts shows online status (● online, ○ offline)
FC-P6-T6: Long messages word-wrap into multiple lines with aligned indent
FC-P6-T7: Tab completion for 33 slash commands (4 new tests)
FC-P8-T6: sendDocument accepts both JSON and multipart form data
OTPK: Auto-replenish on TUI startup when supply < 3 (generates 10 new)
135 tests passing (was 127)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FC-P2-T4: TUI call state machine
- CallInfo struct + CallPhase enum (Calling/Ringing/Active)
- Header shows call indicator: yellow "Calling...", magenta "Incoming", green timer
- /call sets Calling, /accept sets Active, /reject+/hangup clears
- Incoming signals show contextual messages (Offer→prompt, Answer→connected, etc.)
FC-P2-T5: Missed call display in TUI
- WS Text frames parsed for missed_call + bot_message JSON
- Missed calls: "📞 Missed call from X at HH:MM" + terminal bell
- Bot messages rendered as @botname: text
FC-P8-T5: Inline keyboard buttons in web
- CSS styled keyboard buttons (.kbd-btn)
- Bot messages with reply_markup render clickable button rows
- Click sends callback_data back to bot as bot_message
- Works in both WS text handler and handleIncomingMessage fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- /call [fp|@alias|0x...] — send CallSignal::Offer to peer
- /accept — answer incoming call (uses last_dm_peer)
- /reject — reject incoming call
- /hangup — end active call
- All four added to /help text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Web client:
- Call bar between header and messages (hidden when idle)
- Call button appears when peer is set (not group)
- Incoming call: pulsing notification + Accept/Reject buttons
- Call states: idle → calling → ringing → active
- /call, /accept, /reject, /hangup slash commands
- CallSignal sent via WS binary frames (same as messages)
- handleCallSignal processes Offer/Answer/Hangup/Reject/Ringing/Busy
- Vibration on incoming call (mobile)
- create_call_signal WASM import wired up
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-bot numeric IDs (privacy feature) broke sendMessage because the
reverse lookup couldn't find the fingerprint from the per-bot hash.
Fix: store numid:<numeric_id> → fingerprint in tokens tree when
generating updates. resolve_chat_id checks this mapping first.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server:
- --bots-config <path> loads JSON array of system bots on startup
- Bots auto-created if missing, aliases restored on every start
- Bot list stored in DB for welcome screen (system:bot_list key)
- GET /v1/bot/list returns system bots (public, no auth)
Welcome screen:
- Web + TUI show available bots on first login
- "Available bots: @helpbot — featherChat help, @codebot — Coding..."
- Clickable in web (via address detection)
Config: bots.example.json with 10 suggested bots
Usage: warzone-server --enable-bots --bots-config bots.json
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New users with no peer/group set automatically join #ops so they
have someone to talk to. Saved peer overrides this on subsequent visits.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Privacy: from.id is now Hash(bot_token + user_fp) → different bots see
different numeric IDs for the same user. Prevents cross-bot user correlation.
Removed id_str (raw hex fingerprint) from all bot API responses.
Updated LLM_BOT_DEV.md and LLM_HELP.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Old fp contained non-hex chars (o,r) which got stripped by normFP,
causing whois lookup failure and bot detection to miss.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Built-in BotFather (Rust, server-side):
- Intercepts messages to @botfather in deliver_or_queue
- Commands: /newbot <name>, /mybots, /deletebot <name>, /token <name>
- Creates bot with fingerprint, token, alias, tracks ownership
- Replies via push_to_client or queue (works offline)
- Only active when --enable-bots is set
Standalone BotFather (Python):
- tools/botfather.py: uses bot API (getUpdates/sendMessage)
- Delegates core ops to built-in handler
- Extensible for additional features
- Reads token from BOTFATHER_TOKEN env or .botfather_token file
Flow: User messages @botfather → "/newbot MyBot" → gets token back
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>