diff --git a/.claude/skills/resolving-issues/SKILL.md b/.claude/skills/resolving-issues/SKILL.md index c12c1537..2627a6ef 100644 --- a/.claude/skills/resolving-issues/SKILL.md +++ b/.claude/skills/resolving-issues/SKILL.md @@ -84,6 +84,8 @@ Fresh agent per issue, scoped to one issue + master branch ref. Steps: 1. Read the issue. Decide if more context needed. 2. **Research (optional, parallel OK):** spawn research subagents for codebase grep, related-file reads, spec lookups. Synthesize before coding. Re-grep cited line numbers / LOC counts at HEAD before working from issue-body literal positions — they drift fast across refactors and may be off by hundreds of lines after a recent file move. + + **Verify the audit's claimed mechanism, not just its line numbers.** Audits sometimes prescribe a fix that depends on a stated mechanism — "log via `tracing::warn!`", "the rebuild Effect recovers dropped inserts", "the existing CSP test would catch this". That stated mechanism may (a) violate a module-local constraint (e.g. a privacy contract in the module doc forbidding `tracing::*`), or (b) describe code that doesn't actually exist as cited (e.g. a "rebuild Effect" referenced in a doc-comment but not present in any `app.rs` Effect — the actual recovery path is a different shape with the same fragility). **Pre-flight read** the cited file's module-doc + the cited recovery code at HEAD before dispatching. If either check fails, the issue is an **ambiguous-fix-path** (see step 6 of the Core Loop): coordinator-skip without close, note in run-end Lessons Learned, leave for a future session with fresh eyes. Saves a wasted dispatch + a likely rejected commit. Examples surfaced this way: `crates/client/src/search/handle.rs` module-doc forbids `tracing::*` (privacy contract) — kills audit's "log the drop" prescription; the "rebuild Effect" cited in `insert`'s doc-comment is itself misleading (only an event-loop subscription exists, sharing the same `do_send` fragility). 3. **Complexity gate — automated brainstorm + plan when warranted:** - **Trigger any of:** issue spans > 1 crate, fix touches state machine / wire format / migration paths, ≥ 2 reasonable approaches exist, root cause not obvious from issue text, fix likely > 5 files OR > 200 LOC, "it depends" question on scope. - **Skip when:** issue is a one-liner / config swap / typo / clearly mechanical (single rg-pattern site) / has explicit "Suggested fix" the implementer can follow verbatim. diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 5a3016d7..00000000 --- a/PLAN.md +++ /dev/null @@ -1,209 +0,0 @@ -# Willow — P2P Chat Platform - -A decentralized Discord replacement for friend groups. No central servers, no -accounts, no middlemen. Built with Rust, libp2p, and Leptos. - -## Vision - -A web app where you and your friends can: -- Text chat with channels, threads, reactions, pins, and emoji -- Voice call, video call, and screen share -- Share files and images peer-to-peer -- Create servers and channels with role-based permissions -- Discover each other on the local network or via relay nodes -- Own your data — everything is end-to-end encrypted - -## Architecture - -``` -┌─────────────────────────────────────────────────┐ -│ Leptos Web App (willow-web) │ -│ Sidebar │ Chat │ Settings │ Files │ Servers │ -│ Welcome │ Pinned │ Members │ Roles │ Typing │ -├─────────────────────────────────────────────────┤ -│ Client Library (willow-client) │ -│ Event-sourced state │ DisplayMessage view-model│ -│ Server management │ Profiles │ Sync │ Typing │ -├─────────────────────────────────────────────────┤ -│ State Machine (willow-state) │ -│ Pure apply() │ 21 EventKind variants │ Merge │ -│ StateHash │ Permissions │ Pins │ Reactions │ -├─────────────────────────────────────────────────┤ -│ Application Layer │ -│ willow-channel │ willow-messaging │ willow-files│ -├─────────────────────────────────────────────────┤ -│ Crypto Layer │ -│ willow-crypto (ChaCha20-Poly1305, X25519) │ -├─────────────────────────────────────────────────┤ -│ Network Layer │ -│ willow-network (libp2p + tokio / wasm-bindgen) │ -│ GossipSub │ Kademlia │ mDNS │ Relay │ Chunks │ -├─────────────────────────────────────────────────┤ -│ Foundation │ -│ willow-identity │ willow-transport │ -│ Ed25519 signing │ Protocol framing │ -└─────────────────────────────────────────────────┘ -``` - -## Crates - -| Crate | Purpose | Status | -|-------|---------|--------| -| `willow-transport` | Binary serialization, protocol versioning, message framing | Done | -| `willow-identity` | Ed25519 keypairs, signed messages, user profiles, PeerId extraction | Done | -| `willow-messaging` | Chat messages, HLC ordering, message store, SealedContent | Done | -| `willow-crypto` | E2E encryption, key ratchet, X25519 key exchange | Done | -| `willow-channel` | Servers, channels, roles, permissions, invites, key rotation | Done | -| `willow-files` | Content-addressed file chunking, SHA-256 hashing, ChunkStore | Done | -| `willow-network` | libp2p networking, chunk transfer protocol (native + WASM) | Done | -| `willow-state` | Pure event-sourced state machine, 21 EventKind variants | Done | -| `willow-client` | UI-agnostic client library, DisplayMessage, server management | Done | -| `willow-web` | Leptos web UI (primary frontend) | Done | -| `willow-relay` | Relay server bridging TCP and WebSocket peers | Done | - -## Roadmap - -### Phase 1 — Foundation (COMPLETE) -- [x] Transport layer with versioned envelopes -- [x] Cryptographic identity with Ed25519 signing -- [x] Message model with Hybrid Logical Clock ordering -- [x] Channel/server/role/permission model -- [x] libp2p networking with GossipSub, Kademlia, mDNS - -### Phase 2 — Working Chat (COMPLETE) -- [x] Wire up network ↔ messaging over GossipSub -- [x] Text input, message rendering with timestamps and author names -- [x] Channel switching, creation, deletion -- [x] Full integration tests with real libp2p network nodes - -### Phase 3 — E2E Encryption (COMPLETE) -- [x] ChaCha20-Poly1305 content encryption (seal/open) -- [x] Per-channel symmetric keys with ChannelKey -- [x] X25519 key exchange (Ed25519 → X25519 conversion) -- [x] Encrypted key distribution via invite system -- [x] Transparent encrypt-on-send, decrypt-on-receive - -### Phase 4 — Forward Secrecy & Key Rotation (COMPLETE) -- [x] KeyRatchet: per-message key derivation via HKDF -- [x] Key rotation on member removal (re-key all channels) - -### Phase 5 — Persistence & Profiles (COMPLETE) -- [x] Identity persistence (Ed25519 keypair saved to disk / localStorage) -- [x] Full ServerState persistence (save/load entire state, no replay needed) -- [x] Per-server profiles with display names -- [x] Profile broadcasting over gossipsub - -### Phase 6 — File Sharing (COMPLETE) -- [x] Content-addressed chunking (SHA-256, 256 KiB) -- [x] Inline file sharing via gossipsub (<256KB) -- [x] Image/GIF embedding (URL detection + uploaded file embeds) -- [x] File cards with download buttons - -### Phase 7 — Server State Sync (COMPLETE) -- [x] Event-based wire format (WireMessage with Events) -- [x] Catch-up on connect: exchange missing events via state hash -- [x] Relay stores events in SQLite, serves to new peers -- [x] Multi-peer state verification (StateVerification + hash tracking) - -### Phase 8 — Event-Sourced State Machine (COMPLETE) -- [x] willow-state crate: pure deterministic state machine (zero I/O) -- [x] 21 EventKind variants (channels, roles, permissions, messages, - edits, deletes, reactions, pins, profiles, server metadata) -- [x] StateHash (SHA-256) for divergence detection -- [x] apply() with permission enforcement and dedup -- [x] Fine-grained Permission model -- [x] merge() for divergent history resolution -- [x] DisplayMessage view-model computed from event_state (never stored) -- [x] Single source of truth: UI reads directly from ServerState -- [x] Legacy Op system fully removed — Events only - -### Phase 9 — Voice & Video (IN PROGRESS) -- [ ] WebRTC-like media transport -- [ ] Voice channels with Opus audio -- [ ] Video with VP8/VP9 -- [ ] Screen sharing - -### Phase 10 — Polish & UX (COMPLETE) -- [x] Discord-style dark/light theme with CSS variables -- [x] IBM Plex typography, refined design -- [x] Welcome/onboarding screen (no default server) -- [x] Create server + join server flows -- [x] Per-server profile editing -- [x] Server settings page (invites, roles, separate from profile) -- [x] Message editing and deletion -- [x] Reply threads with parent preview and jump-to-parent -- [x] Mention highlighting (replies to you) -- [x] Emoji reactions (toggle on click, Discord-style) -- [x] Pinned messages with clickable URLs and jump-to-message -- [x] Typing indicators with debounce and auto-expiry -- [x] Single action dropdown menu (Reply, React, Edit, Delete, Pin, Download) -- [x] Time-gap grouping (5min threshold re-shows author/timestamp) -- [x] URL detection and clickable links in messages -- [x] Member list with online/offline status and role badges -- [x] Mobile member list slide-out panel -- [x] Mobile responsive layout with touch targets -- [x] Auto-focus input on reply/edit -- [x] Notification sounds (Web Audio API) -- [x] PWA manifest + service worker -- [x] Message deduplication (msg_id across sessions) -- [x] Clipboard fallback for non-HTTPS - -### Infrastructure (COMPLETE) -- [x] WASM dual-target support (all crates compile for wasm32) -- [x] Relay server with display name (TCP + WebSocket) -- [x] Cross-platform storage (filesystem / localStorage) -- [x] Cross-platform clipboard (web_sys + textarea fallback) -- [x] Deploy skill for automated builds and deployment -- [x] justfile with all build/test/serve commands - -## Data Flow - -``` -Action → Create Event → apply_event() → ServerState mutated → save_server_state() - → broadcast_event() → peers receive → apply to their state - -UI calls client.messages(channel) → computed Vec from event_state - → display names resolved fresh (never stale) - → reactions, edits, deletes, pins all from authoritative state - → no stored intermediate copies, no manual merging -``` - -## Test Coverage - -353 tests across 7 tiers: - -| Tier | Tests | Scope | -|------|-------|-------| -| Pure state machine | 82 | Determinism, permissions, merges, pins, reactions, replay | -| Client library | 114 | DisplayMessage, server management, typing, dedup, profiles | -| Network integration | 14 | Real libp2p, 3-node topology, file chunks | -| Scaling | 7 | 5/10/20 peers, message flood, 532k events/sec | -| E2E flow | 9 | State machine + network: invite, chat, sync, merge | -| Relay history | 3 | Store events, serve to new peers, multi-peer | -| Browser (Leptos) | 39 | Real DOM, signals, events, all components | -| Web component units | 11 | URL extraction, image detection, MIME types | -| Other crates | ~74 | Transport, identity, crypto, channel, files | - -Run: `just test` (cargo), `just test-browser` (headless Firefox), -`just test-all` (both), `just test-scale` (with output), -`just test-e2e` (end-to-end flow) - -## Key Technical Decisions - -| Decision | Choice | Rationale | -|----------|--------|-----------| -| Language | Rust | Memory safety, performance, async ecosystem | -| Web UI | Leptos 0.7 CSR | Reactive, WASM-native, component-based | -| Networking | libp2p 0.53 | Battle-tested P2P, NAT traversal | -| State | Event-sourced | Deterministic, mergeable, auditable | -| Encryption | ChaCha20-Poly1305 + X25519 | AEAD + DH key exchange | -| Signing | Ed25519 | Fast, small keys, message authenticity | -| Serialization | bincode | Fast, compact binary format | -| Message Ordering | Hybrid Logical Clocks | No central coordination needed | -| Persistence | Full ServerState save | Instant load, no replay needed | - -## Deployed - -- **Relay**: `172.234.217.219` ports 9090 (TCP) + 9091 (WebSocket) -- **Web app**: `172.234.217.219` via nginx -- **PeerId**: `12D3KooWMBmUF1rHYG5CneKi8JZfKdMAciJd4oCgknTJkbwCUurd` diff --git a/crates/web/manifest.json b/crates/web/manifest.json index 14c3488b..6f6e0216 100644 --- a/crates/web/manifest.json +++ b/crates/web/manifest.json @@ -2,6 +2,8 @@ "name": "Willow", "short_name": "Willow", "description": "P2P encrypted chat", + "id": "/", + "scope": "/", "start_url": "/", "display": "standalone", "background_color": "#1a1a1e", @@ -10,12 +12,14 @@ { "src": "/icon-192.svg", "sizes": "192x192", - "type": "image/svg+xml" + "type": "image/svg+xml", + "purpose": "any maskable" }, { "src": "/icon-512.svg", "sizes": "512x512", - "type": "image/svg+xml" + "type": "image/svg+xml", + "purpose": "any maskable" } ] } diff --git a/crates/web/src/components/member_list.rs b/crates/web/src/components/member_list.rs index ebdc6ec5..8a669c67 100644 --- a/crates/web/src/components/member_list.rs +++ b/crates/web/src/components/member_list.rs @@ -381,11 +381,13 @@ pub fn MemberList( pid.chars().take(6).collect(); let name = format!("side-{short}"); wasm_bindgen_futures::spawn_local(async move { - let _ = h.create_ephemeral_channel( + if let Err(e) = h.create_ephemeral_channel( &name, willow_state::EphemeralKind::Channel, willow_state::DEFAULT_CHANNEL_THRESHOLD_MS, - ).await; + ).await { + tracing::warn!(?e, "create_ephemeral_channel failed"); + } }); } > diff --git a/crates/web/src/main.rs b/crates/web/src/main.rs index 107a9872..7e763b29 100644 --- a/crates/web/src/main.rs +++ b/crates/web/src/main.rs @@ -13,10 +13,19 @@ fn main() { } } - // Register the service worker for PWA support. - let _ = js_sys::eval( - "if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').catch(function() {}); }", - ); + // Register the service worker for PWA support. Logs failures (HTTPS + // misconfiguration, MIME mismatch, parse errors, scope violations) so + // they surface instead of being silently swallowed (issue #606). + if let Some(window) = web_sys::window() { + let sw_container = window.navigator().service_worker(); + let promise = sw_container.register("/sw.js"); + wasm_bindgen_futures::spawn_local(async move { + if let Err(e) = wasm_bindgen_futures::JsFuture::from(promise).await { + let msg = e.as_string().unwrap_or_else(|| format!("{e:?}")); + tracing::warn!("service worker registration failed: {msg}"); + } + }); + } // Wire navigator.serviceWorker.onmessage so pushes forwarded by the // service worker (focused-client path) reach the in-app Notifier.