User reported that outgoing direct calls from macOS show up in the
history list as "missed" even when the call completes successfully.
Adds two changes to fix / diagnose:
1. history::log now dedupes by call_id. If an entry for this call_id
already exists in the store, it updates the existing row's
direction + timestamp in place instead of appending a duplicate.
Protects against double-emit (caller side adding Missed on top of
Placed, or any future signal loop that fires twice). One row per
call_id, which matches what the user intuitively expects.
2. history::log now logs every write with tracing::info — call_id,
peer_fp, direction, alias. Plus an extra line when we replace
an existing entry: "history::log replacing existing entry
from=Placed to=Missed" etc. Makes it easy to see in the desktop
stderr which side is writing what, so we can find the outgoing =>
missed regression immediately if it recurs.
3. main.ts now renders an explicit text label next to the direction
arrow: "Outgoing", "Incoming", or "Missed" instead of just the ↗
↙ ✗ icons. Removes any ambiguity about what the icon means so
future users can't misread a Placed entry as Missed based on icon
shape alone.
Side fix for scripts/build-windows-cloud.sh:
- die() and the do_full ERR trap now respect WZP_KEEP_VM=1 so a failed
build doesn't auto-destroy the debug VM (previously the trap fired
before the KEEP_VM check and tore down the VM on any error).
- Bump default server type cx23 → cx33. 4GB RAM is not enough for a
cold tauri + rustls + quinn + wzp-client cross-compile — the cx23
run got "Read from remote host ... Connection reset by peer"
partway through rustc, which is the classic signature of an OOM
kill on the SSH session. cx33 has 8GB RAM and 8 vCPU which should
comfortably fit the build.
Persistent JSON-backed call history for the direct-call screen so users
can see what they've placed / received / missed and dial back with one
click. Also fixes two small latent UX issues reported alongside.
Backend (Rust)
- new crate/module desktop/src-tauri/src/history.rs: thread-safe in-
process store (OnceLock<RwLock<Vec<CallHistoryEntry>>>) backed by
<APP_DATA_DIR>/call_history.json. Atomic writes via temp+rename. Max
200 entries, FIFO pruning. CallDirection { Placed, Received, Missed }.
- Log hooks in the signal loop + commands:
* place_call → Placed entry (with target fingerprint)
* DirectCallOffer → Missed entry up front; upgraded to Received
inside answer_call when accept_mode != Reject
via history::mark_received_if_pending(call_id).
If user rejects or never answers, it stays Missed.
- New Tauri commands:
* get_call_history() → all entries, newest first
* get_recent_contacts() → unique peers by fp, newest interaction first
* clear_call_history() → wipes JSON + in-memory
* deregister() → tears down signal transport + endpoint
Backend emits `history-changed` events so the UI can live-refresh
without polling.
Frontend (main.ts + index.html + style.css)
- Direct-call panel now has:
* Recent contacts chip row (top 6 unique peers). Click a chip → dial.
* Call history list (up to 50 rows). Direction icon (↗ placed, ↙
received, ✗ missed), peer alias/fp, relative timestamp, callback
button. Both click handlers populate target-fp and fire place_call.
* Deregister button in the "registered" header — calls the new
deregister command, tears down the signal transport, returns the
UI to the pre-register state.
* Clear-history link in the history header.
- Subscribes to `history-changed` events so the list updates the moment
the backend logs a new entry. Also refreshed on register + after a
clear.
- Nothing is rendered until there is data — empty sections stay hidden.
Tasks #20 + #21 (small UX items bundled in)
- Default room "general" for new installations: the html input value
attribute is now "general" and loadSettings() defaults match. Existing
users' localStorage still wins.
- Random alias on desktop: already latent but confirmed working — the
startup IIFE at main.ts:374 calls get_app_info() and prefills the
alias input from derive_alias(seed) when the input is empty. No code
change needed, just verified it flows through the same path as the
Android client.
Known follow-ups (deferred to step 6 polish)
- Call duration tracking (currently all entries have no duration field)
- Hangup signal from an unanswered incoming should emit history-changed
so the missed state is visible even when the user never tapped accept
- Android UI layout fit-check on the smaller Nothing screen