Skip to content

Video calling, screen sharing, call page, tabbed settings, auto-reconnect#6

Merged
intendednull merged 19 commits into
mainfrom
feat/video-screen-sharing-call-page
Mar 27, 2026
Merged

Video calling, screen sharing, call page, tabbed settings, auto-reconnect#6
intendednull merged 19 commits into
mainfrom
feat/video-screen-sharing-call-page

Conversation

@intendednull
Copy link
Copy Markdown
Owner

Summary

Video / Call Page

  • Full call page with participant grid/focus layout, control strip, duration timer
  • Camera video (getUserMedia) and screen sharing (getDisplayMedia), mutually exclusive
  • Speaking detection via AudioContext + AnalyserNode polling
  • Peer-ID-derived gradient avatars for audio-only participants
  • Perfect negotiation pattern for WebRTC offer collision handling
  • VoiceManager refactored for connection reuse across renegotiations

Network Resilience

  • Auto-reconnect with exponential backoff (1s → 30s cap) when WebSocket drops
  • Topic re-subscription on reconnect via connected_subscribed flag reset
  • Commands queue during backoff and are delivered to the new node

Settings

  • Unified tabbed settings panel (Profile / Server / Roles) with breadcrumb
  • server_settings.rs deleted and merged into settings.rs

Bug Fixes (11 issues found and fixed across 2 deep-review rounds)

  • Reconnect re-subscribes topics (was permanently deaf after reconnect)
  • Participant count no longer double-counts local user
  • Voice channel switch properly cleans up old channel connections
  • Screen share uses object-fit: contain for remote video
  • close_all() removes audio elements from DOM
  • SpeakingDetector recreated after disconnect (was permanently broken)
  • Timer cleaned up on component unmount (no more stacking)
  • Audio source nodes properly tracked instead of mem::forget leak
  • Dead code removed (show_server_settings signal, unused clones)

Tests

  • Cross-browser E2E test (mobile Chrome ↔ desktop Firefox)

Test plan

  • just check passes (fmt + clippy + test + WASM)
  • All 116 client tests pass
  • Cross-browser E2E tests pass
  • 2 rounds of deep review — 0 remaining issues
  • Deployed and manually tested

🤖 Generated with Claude Code

intendednull and others added 19 commits March 26, 2026 03:28
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rack management

- Connections reused on renegotiation instead of recreated
- Perfect negotiation: polite/impolite by peer ID comparison
- onnegotiationneeded handler for addTrack/removeTrack flows
- Unified video management: start_video/stop_video_share
- ontrack redesign: routes audio to <audio> elements, video to callback
- Camera and screen share mutually exclusive via VideoSource enum

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…trols

Full call page replaces chat when in voice channel.
Participant tiles show peer-ID-derived gradient avatars or video.
Grid and focus layout modes with click-to-focus.
Control strip: mute, deafen, camera, screen share, disconnect.
Frosted glass controls, speaking glow, staggered tile animations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SpeakingDetector polls AnalyserNodes at 60ms intervals.
Participant tiles show green pulse glow when peer is speaking.
Local mic also analysed for self-feedback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused `wasm_bindgen::JsCast` import from participant_tile.rs
- Remove stale `roles::*` re-export from components/mod.rs (server_settings module is not declared)
- Prefix unused `roles` variable with `_` in app.rs
- Add `#[allow(dead_code)]` to `RoleManager` component props (Leptos codegen artifact)
- Add `#[allow(dead_code)]` to `UiState::show_server_settings` and `UiState::settings_tab` fields
- Add `#[allow(clippy::too_many_arguments)]` to `render_tile` in call_page.rs
- Replace `let _ = state.pc.remove_track(...)` with a bare call (let_unit_value)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The local user's camera/screen share stream was passed to VoiceManager
for remote peers but never shown on their own participant tile. Add a
local_video_stream signal inside CallPage, set it when camera or screen
share starts, clear it on all stop paths, and pass it to the local
tile's render_tile calls. Also add a track.onended listener for when
the browser's "Stop sharing" button is used.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two issues fixed:
1. set_src_object received &SendWrapper<MediaStream> instead of
   &MediaStream — explicit deref now ensures correct type
2. play() called before DOM element fully mounted — deferred to
   next microtask via set_timeout(Duration::ZERO)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s, navigation

1. Screen share onended now calls stop_video_share on VoiceManager
2. create_offer sets making_offer flag to prevent onnegotiationneeded race
3. Audio elements tracked per-peer and removed from DOM on disconnect
4. Remove dead acquire_microphone methods
5. Text channel click closes call page
6. local_video_stream moved to global VoiceState (survives component remount)
7. voice_participants_map cleared on disconnect
8. Remote video defaults to object-fit:cover (not contain)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs:
1. ontrack handler silently dropped video tracks when ev.streams()
   was empty (common during renegotiation when SDP lacks a=msid).
   Now creates a MediaStream from the track as fallback.
2. onnegotiationneeded handler didn't guard against duplicate offers
   when making_offer was already true. Added early return check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Rewrite SettingsPanel as tabbed (Profile/Server/Roles) with breadcrumb
2. Delete server_settings.rs (merged into settings.rs)
3. Seed voice_participants_map from client state on rejoin
4. Add local user to participants map on voice join

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The component renders .tab-btn but CSS targeted .settings-tab.
Fixed selector, added accent glow on active underline, proper spacing,
hover state, and consistent breadcrumb header styling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two tests launching different browser engines to verify P2P sync
works across browser types. Both pass — confirms the relay handles
cross-browser WebSocket connections correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the WebSocket connection to the relay drops (low battery, network
switch, tab backgrounding), the network layer now automatically retries
with exponential backoff (1s → 2s → 4s → ... → 30s cap). Backoff resets
on successful node start and on peer connection. Commands queue during
reconnect and are processed on the next successful connection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the WebSocket connection to the relay drops (low battery, network
switch, tab backgrounding), the network layer now automatically retries
with exponential backoff (1s → 2s → 4s → ... → 30s cap). Backoff resets
on successful peer connection. Commands queue during reconnect and are
processed on the next successful connection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… cleanup, screen-share styling

1. Reset connected_subscribed on 'reconnecting' sentinel so topics
   are re-subscribed after network reconnect
2. Filter local peer from remote_participants to prevent double tile
3. Full cleanup of old voice channel before joining a new one
4. Default remote video to screen-share styling (object-fit: contain)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The grid rendering filter was fixed in the previous commit but the
header count display still had +1. Removed since local is already
in voice_participants_map.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…imer leak, dead code

1. close_all() now calls close_connection() per peer to remove <audio> elements
2. SpeakingDetector recreated after close_all() so speaking works on rejoin
3. set_interval timer cleaned up with on_cleanup to prevent stacking
4. Removed dead _handle_camera clone
5. Removed dead show_server_settings signal
6. Audio source nodes stored in shared map instead of leaked via mem::forget

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@intendednull intendednull merged commit 6bb1144 into main Mar 27, 2026
intendednull pushed a commit that referenced this pull request Apr 5, 2026
Fixes from three-agent deep review (PR comments, research papers,
plan analysis):

**Critical: Equivocation detection**
- Added ChainStatus::Forked variant for same-seq-different-hash
- Added Equivocation Detection section in Section 4 with freeze/
  propagate/alert response protocol
- Referenced 2P-BFT-Log paper (arXiv:2307.08381)

**Governance fixes**
- Default vote threshold changed from Unanimous to Majority (PR #4)
- Bootstrap sequence updated for majority default
- Auto-apply mechanics clarified: no separate event, state change
  inferred during materialization (PR #5)
- Vote structurally references proposal via EventHash field (PR #6)
- KickMember/RevokeAdmin now clean up pending votes and re-evaluate
  all proposals (cascade thresholds) via cleanup_votes_and_reevaluate
  and reevaluate_all_proposals helpers
- SetVoteThreshold re-evaluates all pending proposals
- 5 new governance tests for vote cleanup and threshold cascading

**Permission fixes**
- GrantPermission/RevokePermission/RenameServer/SetServerDescription
  require is_admin check directly (not via ManageRoles permission)
- SendMessages permission now enforced for Message/EditMessage/
  DeleteMessage/Reaction events (was defined but never checked)
- Permission enum has doc comments explaining each variant

**Type fixes**
- Channel.pinned_messages: HashSet<String> → HashSet<EventHash>
- Event struct has explicit #[derive(Clone, Debug, Serialize, Deserialize)]
- Archival tolerance design goal reworded for append-only model

**Incremental apply safety**
- Added soft-dep arrival re-materialization note: when a previously
  missing dep arrives and creates new causal edges, re-materialize
  from the DAG to maintain consistency

**Appendix A: Known Issues and Future Work**
- Hash-grinding on governance events
- Optimized incremental apply after soft-dep arrival
- SyncProvider permission evaluation
- Versioning strategy
- Dynamic channel model

https://claude.ai/code/session_015u9tV4Abp1kKXvCVZ5iMfv
intendednull added a commit that referenced this pull request Apr 26, 2026
lifecycle, fix IrohBlobStore spec drift, track 4 new follow-ups

Round 2 review (two fresh agents) verified all 15 round-1 fixes
land cleanly with no regressions, then surfaced 8 new findings
(0 critical, 3 medium, 5 low) by widening scope to cross-component
interactions, perf, and API surface.

Fixed inline (trivial doc / spec):

- Add an "Actor coordination signal" row to the spec decision tree
  + CLAUDE.md table covering tokio::sync::watch / oneshot /
  broadcast / Notify, with the explicit rule that
  tokio::sync::Mutex is forbidden for business state on the same
  terms as std/parking_lot Mutex. Closes the spec gap that left
  contributors without guidance on async channels. (round-2 #3)
- Reconcile spec § 184 with the corrected IrohBlobStore comment
  (round-1 fixed the code, missed the spec). The blob store is
  not an iroh-callback boundary — it's an interim stub. The relay-
  status timestamp Mutex stays in the iroh boundary list. (round-2 #4)
- Document the web `_event_loop` drop pattern in `crates/web/src/app.rs`
  so future readers see explicitly that the actor System is process-
  scoped on web (page reload tears everything down) and that any
  actor needing pre-close cleanup must route via `beforeunload`,
  not Drop. (round-2 #8)

Tracked as new follow-ups in spec § Follow-up work:

- F5. SearchActor head-of-line + rebuild-storm fix. Rebuild blocks
  Query in FIFO order; the rebuild Effect has no debounce. Fix is
  chunked-Rebuild + Debounce<Rebuild> wrap. (round-2 #1, #2 — Med)
- F6. Browser-tier coverage for SearchIndexHandle consumers. The
  spawn_local + Effect path has no wasm-pack test. (round-2 #6)
- F7. Sealed ClientSpawner to narrow the system() API surface,
  rather than exposing the full SystemHandle. (round-2 #7)
- F8. Search-query debouncing-flicker fix via generation tag or
  Leptos Resource migration. (round-2 #5)

Each follow-up has a "Trigger:" line naming the dedicated PR title.

`just check` green: clippy zero warnings, 1003+ tests pass, WASM
compile clean. Loop terminates here per the user's two-round cap;
no Critical issues remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
intendednull pushed a commit that referenced this pull request Apr 28, 2026
Synthesises findings from three parallel review agents (technical,
factual research, security audit). Major changes:

Factual corrections:
- NixOS 25.11 (not 25.05); pin nixpkgs by hash, not channel name.
- Hetzner pricing updated for 2026-04-01 hike: CAX21 ARM €7.99/mo
  preferred over CPX21 AMD €11.99/mo (better $/perf, project already
  targets aarch64). Volume €0.57, Storage Box €3.20, Primary IP €0.50
  (not Floating IP, which is €3.00 and not in Phase-1 budget).
- Cost total €12.26 vs €20 cap.
- Cloudflare WSS-proxy claim corrected (proxy works fine for WSS;
  real reasons to keep relay direct are 100s idle timeout + binary
  TCP/9090).

Security additions:
- Phase 0 reframed as compromise response: leaked password is
  permanently public; auth-log audit gates whether the relay key is
  migrated at all.
- CI = root on prod stated honestly; forced-command is no defence
  against malicious closures. Required mitigations: manual approval
  gate, closure signing pinned in trusted-public-keys, action SHA
  pinning, scoped permissions.
- agenix host-key threat model documented; rekey procedure;
  bootstrap chicken-and-egg sequence; sops-nix as upgrade path for
  high-value secrets needing online revocation.
- LUKS encryption on Hetzner Volume.
- restic in append-only mode; offline DR secrets (restic password,
  Storage Box key, LUKS recovery key) listed; backup-failure
  email path.
- New "Security baseline" section: hardware-token 2FA on Hetzner +
  Cloudflare + GitHub; CAA records; OpenTofu state encryption;
  systemd hardening defaults (`systemd-analyze security` ≤ 3.0);
  break-glass key separate from CI key.

Operational additions:
- New "Host configuration baseline" section: time sync, swap, Nix
  GC, journald limits, sshd hardening, mount ordering, NixOS
  version-bump procedure.
- Caddy footguns explicit: WASM MIME directive, Brotli requires
  xcaddy build, admin API disabled.

Phase reordering:
- Phase 1 now includes backups + a verified restore drill before
  cutover (closes regression vs goal #6).
- Phase 2 cutover gates on the Phase-0 audit conclusion + atomicity
  invariant (one host holds the live key at any moment).

New runbooks/appendices:
- Appendix C: relay key migration runbook (stream through age,
  never write to laptop disk, integrity-check via SHA-256, shred
  source).

Tooling research updates:
- Cachix risk note: attic upstream stalled; celler is the
  actively-maintained fork.
- nh and clan-core added to rejected alternatives with current
  rationale.
- §10 commits to Caddy-on-VM as long-term answer (CDN via
  Cloudflare proxy toggle), drops the speculative "Phase 2 move
  to Pages" stepping-stone shortcut.

Firewall fix:
- Port 9091 dropped from edge firewall (loopback never traverses
  edge); rate limit added on TCP/9090; IPv6 rules stated.

https://claude.ai/code/session_01PPGTBSaKd4c6iPCDmFNtoM
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant