# Changelog All notable changes to this project are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ### Added — Phase 2 guild member list + identity export/import + channel rate limit (fourth batch) #### Phase 2 guild member list with kick / role-assign UI (Phase E4) - **New PacketIds:** `GuildMemberListFetch` (0x010E), `GuildMemberList` (0x010F), `GuildRoleAssign` (0x0110), `GuildRoleUnassign` (0x0111), `GuildRoleListFetch` (0x0112), `GuildRoleList` (0x0113). - **New storage methods:** - `Storage::list_guild_members(guild_id)` — returns each member's nickname, joined_at, highest-role color + name (via SQL subqueries on `member_roles`/`roles`). - `Storage::assign_role(guild_id, user_id, role_id)` — idempotent insert into `member_roles`. - `Storage::remove_role_from_user(guild_id, user_id, role_id)`. - `Storage::list_guild_roles(guild_id)` — full role rows with permissions + position. - **New gateway handlers:** `handle_member_list_fetch`, `handle_role_assign`, `handle_role_unassign`, `handle_role_list_fetch`. - Role assign/unassign require `MANAGE_ROLES` permission (or owner bypass) and append to the audit log. - **Client wiring:** `NetCommand::{GuildMemberListFetch, GuildRoleAssign, GuildRoleUnassign, GuildRoleListFetch}` + `NetEvent::{GuildMemberList, GuildRoleList}` + dispatch + UI handling. - **👥 button in guild bar** — opens the guild members modal (disabled when no guild is selected). On open, fetches both member list and role list in parallel. - **Guild members modal:** - Member rows with role-colored avatar ring + nickname + role name + truncated user ID. - 👑 crown indicator for the guild owner. - 👢 kick button (hidden for owner) — calls `GuildMemberKick` and auto-refreshes the list after 200 ms. - "+ role" combo — lists all guild roles; clicking a role name calls `GuildRoleAssign`. - ↻ refresh button. - "N members" header with close button. #### Phase 2 server-side channel rate limit (Phase E6) - `handle_channel_create` now consumes a token from the per-session rate limiter (same bucket as chat/DM messages). - When exceeded, replies `ErrorPayload{ RateLimited }` with message "slow down — too many channel operations". - Bumps `rate_limited_events_total` counter. - Default channels ("general", "voice") remain protected from deletion (already in place from previous batch). #### Phase 2 identity export / import to keyfile (Phase E7) - **`identity::export_keyfile(identity, passphrase)`** — serializes the identity to JSON, seals it with the same Argon2id + ChaCha20-Poly1305 scheme as the on-disk vault, returns the keyfile JSON string. Passphrase is optional (empty = plain JSON). - **`identity::import_keyfile(keyfile_json, passphrase)`** — opens a keyfile and returns the deserialized identity (without persisting — caller calls `save()` separately). - **Export keyfile modal** in Settings → Identity: - Optional passphrase + confirm field (with mismatch check). - "Generate keyfile" button produces the JSON output. - Output shown in a scrollable multiline text box. - "⧉ Copy" button to clipboard. - "Save to disk" button writes to `~/Documents/vnox--.vnoxkey`. - **Import keyfile modal:** - Multiline paste area for the keyfile JSON. - Optional passphrase field. - "Import & Replace Identity" button — calls `import_keyfile` + `save(identity, None)`. - Warning that the current identity will be replaced; restart required. - New `UiState` fields: `export_keyfile_open`, `export_keyfile_pass`, `export_keyfile_confirm`, `export_keyfile_output`, `export_keyfile_error`, `import_keyfile_open`, `import_keyfile_input`, `import_keyfile_pass`, `import_keyfile_error`. ### Build verification - `cargo clippy --workspace` — 0 warnings. - `cargo test --workspace` — 41/41 tests pass. ### Added — Phase 2 server-side channels + identity vault UI + audit log (third batch, kept for history) #### Phase 2 server-side channel management - **`ChannelCreate` packet (0x0033)** — register a new channel in the gateway's channel store. - Validates `kind` ("text" or "voice"); rejects empty `channel_id`. - Returns `ChannelState` to the creator and broadcasts `ChannelCreate` to all other sessions. - Returns `ErrorPayload{ ChannelNotFound }` (reused code) if a channel with the same id exists. - **`ChannelDelete` packet (0x0034)** — remove a channel from the store. - Protects default channels ("general", "voice") with `PermissionDenied`. - Broadcasts `ChannelDelete` to all sessions; client removes it from sidebar and clears cached messages. - **`ChannelList` packet (0x0035)** — fetch all known channels. Client replaces its local list with the server-authoritative one (preserving member lists for channels already joined). - New `channels::create` / `channels::delete` / `channels::list` ops on the gateway. - Client wiring: `NetCommand::{ChannelCreate, ChannelDelete, ChannelList}` + `NetEvent::{ChannelCreated, ChannelDeleted, ChannelListEvent}` + dispatch + UI handling. - **Channel create popup** now sends `ChannelCreate` to the gateway instead of adding locally — sidebar updates come back through the broadcast. - **Channel delete context menu** — right-click any non-default channel in the sidebar to delete it. #### Phase 2 identity vault UI - **Set-passphrase modal** in Settings → Identity: - Two passphrase fields with match check + min-8-char warning. - Calls `identity::save(identity, Some(passphrase))` to encrypt the keypair at rest with Argon2id + ChaCha20-Poly1305. - Shows error messages on save failure. - **Remove-passphrase modal** — type `REMOVE` to confirm; calls `identity::save(identity, None)` to revert to plain JSON. - **Vault status block** — shows current encryption state (🔒 encrypted / 🔓 plain) with explanatory text. - **Copy pubkey button** — copies the hex pubkey to clipboard via `egui::Context::copy_text`. - New `UiState` fields: `vault_set_open`, `vault_remove_open`, `vault_passphrase_input`, `vault_passphrase_confirm`, `vault_error`. #### Phase 2 audit log viewer - **`GuildAuditLogFetch` packet (0x010C)** — admin-only endpoint; requires `MANAGE_GUILD` permission (or owner). - Returns last 50 audit log entries (clamped 1..=200) via `GuildAuditLogPayload`. - New storage method `Storage::get_audit_log(guild_id, limit)` — newest first. - Client wiring: `NetCommand::GuildAuditLogFetch`, `NetEvent::GuildAuditLog`, dispatch, `UiState.audit_log_entries`. - **📋 button in guild bar** — opens the audit log modal for the active guild (disabled when no guild is selected). - **Audit log modal** — scrollable list of entries with: - Action-specific icons (🏗 create, 🗑 delete, 👢 kick, 🏷 role, 🔗 invite, ✓ accept). - Actor ID (truncated to 8 hex chars), target ID, reason (italic). - Relative timestamp ("2m ago", "3h ago", "5d ago"). - Empty state with explanation. ### Fixed - Renumbered `PID_FRIEND_EVENT` from 0x0155 to 0x0158 to avoid collision with server `BlockUser=0x0155` (already done in previous batch, re-confirmed here). ### Build verification (third batch) - `cargo clippy --workspace` — 0 warnings. - `cargo test --workspace` — 41/41 tests pass. ### Added — Phase 2 hardening + Phase 1.3 polish (second batch, kept for history) #### Phase 2 server hardening - **Admin HTTP server (axum) on gateway.** Default bind `0.0.0.0:7601`, configurable via `[gateway] admin_bind`. - `GET /health` — JSON `{status, uptime_seconds, node, address, private_mode}` for orchestrator liveness probes. - `GET /version` — `{name, version, lnex_version}`. - `GET /metrics` — Prometheus text exposition format v0.0.4 with counters/gauges for messages, DMs, voice packets, connections, auth failures, rate-limited events, errors, guilds, friends requests, sessions, channels, uptime. - **Per-session token-bucket rate limiting** on chat and DM messages. - Configurable via `[gateway] message_rate_per_sec` (default 5) and `message_rate_burst` (default 10). - When a session exceeds the limit, server replies `ErrorPayload{ code: RateLimited }` and bumps `rate_limited_events_total` counter. - Idle buckets are pruned on session disconnect via `RateLimiter::remove()`. - **Prometheus metrics module** (`gateway/src/admin/metrics.rs`) with lock-free atomic counters shared across all session tasks. - **`ensure_column` idempotent migration helper** — adds new SQLite columns on existing databases without dropping data. Used to add `messages.reply_to` for the reply feature. #### Phase 2 identity security - **Encrypted identity vault** (`client/src/identity_vault.rs`) — Argon2id passphrase + ChaCha20-Poly1305 AEAD. - Vault format: `{version, scheme, salt, nonce, ciphertext, plaintext}` (JSON). - When passphrase is set, identity is encrypted at rest with Argon2id (m=64 MiB, t=3, p=4) for KDF + ChaCha20-Poly1305 AEAD. - When passphrase is empty, falls back to plain JSON (legacy behavior preserved). - Backward-compatible: legacy `identity.json` is auto-migrated to `identity.vault.json` on next save. - `is_vault_encrypted()` for UI to show lock state. - Unit tests: roundtrip encrypted, wrong passphrase fails, plaintext roundtrip, empty passphrase treated as plain. #### Phase 1.3 chat UX - **Message context menu** (right-click on any message row): - Quick-reactions row (👍 ❤️ 😂 🎉 👀 🤔) — toggles reaction on click. - Reply — sets `replying_to` state, preview bar shown above chat input. - Copy text — copies message content to clipboard via `egui::Context::copy_text`. - Edit (own messages only) — fills chat input with current content, switches to edit mode. - Delete (own messages only) — sends MessageDelete to gateway. - **Reply feature** end-to-end: - Wire protocol: `ChatMessagePayload.reply_to: Option` (optional message_id). - DB schema: `messages.reply_to TEXT` column (added via `ensure_column` migration). - `NetCommand::SendChat { reply_to: Option }` carries the reference through the network layer. - UI: italic "↳ alice: " preview above the message body when `reply_to` is set. - **Custom status + activity status UI**: - Right-click on user-bar status label opens a context menu. - Status picker: Online / Idle / DND / Invisible (with color dots). - Custom status text editor (Discord-style "what's on your mind?"). - Activity type combo (Playing / Listening / Watching / Streaming) + activity text input. - All changes sync to gateway via `PresenceUpdate` with `activity_type`/`activity_text`/`custom_status` fields. - User bar displays: voice state → custom status → activity (icon + text) → online/latency. #### Phase 1.3 block list (was: placeholder) - **Block / Unblock commands** wired end-to-end: - Client: `NetCommand::BlockUser`, `UnblockUser`, `BlockList` + payloads + PID constants (0x0155-0x0157). - Server handlers (already present) now reachable. - Client events: `NetEvent::BlockList`, `BlockedUser`, `UnblockedUser` dispatched to `social::handle`. - `blocked_users: Vec` field on `UiState`. - Friends panel "Blocked" tab now shows: input row to block by user ID, list of blocked users with "Unblock" button. - Auto-fetches block list on first open of the Blocked tab. #### Phase 1.3 per-user speaking attribution - **Voice packet protocol extended** — plaintext now includes `[sender_id:32]` (raw Ed25519 pubkey) between channel_id and opus data. - `voice::build_packet()` accepts `sender_pubkey: &[u8; 32]`. - `voice::spawn_recv()` parses sender_id and emits hex-encoded `sender_id` in `NetEvent::VoicePacket`. - Legacy packets (no sender_id) gracefully handled — empty `sender_id` string. - Unit test `build_packet_header_layout` updated to verify sender_id roundtrip. - **Per-user speaking indicator in voice panel**: - `last_remote_speaker_id` field on `UiState`, updated on every received voice packet. - Voice panel matches `last_remote_speaker_id` against channel members and highlights only the speaking member (green border, name, 🔊 emoji). - Voice activity banner now shows the speaker's nickname: "🔊 alice is talking" instead of generic "someone is talking". - Speaker ID decays after 500 ms of silence (driven by `remote_speaking(500)`). #### Phase 1.3 channel creation UI - **"Create a Channel" popup** — accessible from the channel list header via the "+" button. - Input: channel name (free text). - Type selector: Text (`#`) or Voice (`🔊`). - Creates the channel in the local `s.channels` list so it appears in the sidebar immediately. - Future JoinChannel attempt will register with the gateway (server-side channel-create is Phase 2 backend work). ### Fixed - **NetEvent dispatch bug (from previous commit):** `GuildMemberKicked`, `InviteCreated/Accepted/Deleted`, `RoleCreated/Deleted` were silently dropped — now routed to `social::handle`. - **`FriendAccepted` handler (from previous commit):** now uses `user_id` and adds the friend to the list. - **PID collision:** `PID_FRIEND_EVENT` was 0x0155, conflicting with server `BlockUser=0x0155`. Renumbered: `BlockUser=0x0155`, `UnblockUser=0x0156`, `BlockList=0x0157`, `FriendEvent=0x0158`. - **PresenceUpdate wire format mismatch:** client was sending `{custom_status, activity}` but server expected `{activity_type, activity_text}`. Now the client serializes `activity_type`/`activity_text`/`custom_status` correctly, matching the gateway's `PresenceUpdatePayload`. ### Documentation - `docs/00-status.md` and `docs/06-roadmap.md` updated in previous commit — Phase 1.1/1.2/1.3 marked DONE where implemented. - `CHANGELOG.md` expanded with all new entries. ### Build verification (second batch) - `cargo clippy --workspace` — 0 warnings. - `cargo test --workspace` — 41/41 tests pass (22 in client lib incl. new rate_limit + vault tests, 10 in gateway, 9 in voice-node). ## [0.1.0] - 2025-05-21 ### Added - Initial workspace: gateway, voice-node, client, sdk stub, federation stub. - LNEx Phase 1 over TCP (JSON payloads): auth, channels, text chat. - UDP voice relay between clients in the same channel. - SQLite message history and user records. - Desktop client shell with egui UI and net layer. [Unreleased]: https://github.com/loki5512344/Vnox/compare/v0.1.0...HEAD [0.1.0]: https://github.com/loki5512344/Vnox/releases/tag/v0.1.0