Commit Graph
5 Commits
Author SHA1 Message Date
ksmithandClaude Sonnet 5 1d322d9516 Fix DM recipient never seeing the new conversation (#52 follow-up)
start_dm_endpoint created the room and membership correctly but never
sent the room_added signal every other "you're now in a room" path
(add_member) already sends -- without it, GET /rooms/mine is only
fetched once at app mount, so a DM started against an already-open
client stayed completely invisible until a manual reload. The
recipient still got an offline push/desktop notification (that path
is independent, via _notify_offline_members), just nothing to
actually open when they went looking in an already-loaded session.

Added a test mirroring the existing add_member broadcast test exactly
(recipient connected but never joined any room channel, proving the
signal alone is what tells their client the room exists) -- it failed
before this fix and passes now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 16:22:40 -06:00
ksmithandClaude Sonnet 5 7ef6cfca65 Add presence indicators and a manual "appear offline" override (#36)
Every avatar in the app (chat messages, room member list, your own
avatar in the top bar/profile, the admin user list, the room-invite
search) now shows a green/red presence dot. Also adds a global
"Appear offline" toggle in the account menu, letting a user lurk in a
room undetected -- it overrides the real connection state everywhere,
not per-room.

Backend: new GlobalPresence (backend/app/ws/global_presence.py), a
cross-instance Redis-backed connection tracker parallel to the
existing per-room Presence, incremented/decremented on WS connect/
disconnect. A new users.appear_offline column (migration
f0f6e494454a) always wins over actual connection state when computing
displayed status. RoomMemberRead gained a computed `status` field;
add_member/change_member_role/list_room_members all compute it via a
shared _member_status() helper. Connect/disconnect and profile
updates (display_name, avatar, appear_offline) all broadcast
member_updated to every room the user belongs to, reusing the
broadcast infrastructure from the earlier avatar-staleness fix, so
chat surfaces update live with no new WS envelope type needed. A new
GET /api/users/online gives the admin list and user-search a snapshot
(deliberately not live -- see backend/app/routers/users.py) for
surfaces where "accurate as of page load" is good enough.

Frontend: UserAvatar renders an optional status dot; every call site
threads status/appear_offline through from whichever data source it
already has (room members, the current user, or the new online-ids
snapshot for admin/search).

4 new backend tests (backend/tests/test_presence.py); existing
broadcast-adjacent WS tests updated to tolerate the new member_updated
noise on connect. Verified end-to-end in the browser with two real
users: presence dot flips live on connect/disconnect via the existing
room-broadcast channel, and the lurk toggle correctly forces offline
while still connected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 15:39:56 -06:00
ksmithandClaude Sonnet 5 1b4d681ad0 Broadcast profile updates so member lists stay live (no reload needed)
Another user's new display name or avatar didn't show up until you
reloaded -- update_profile/upload_avatar/remove_avatar never told
anyone. Same root cause and fix shape as #26 (room_added): the
frontend's already-fetched member list had no way to hear about a
change, since nothing ever pushed one.

Reuses the existing per-room broadcast channel (not the per-user one
#26 added, since this only matters for rooms the affected user shares
with someone currently looking at them) -- publishes member_updated to
every room the user belongs to; ChatShellPage refetches members when
it arrives for the currently open room.

Verified end-to-end in the browser: one user's room-info member list
updated live when another user changed their display name from a
separate session, no reload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 11:08:36 -06:00
ksmithandClaude Sonnet 5 7dcc7104df Push a live signal when a user is added to a room (#26)
Previously GET /api/rooms/mine was only ever fetched once at app mount,
so a room added mid-session stayed invisible until a full page reload
-- add_member had no way to reach an already-open client at all.

Backend: ConnectionManager and Broadcaster (renamed from RoomBroadcaster)
now support per-user channels alongside the existing per-room ones, so a
signal can reach a user's socket even for a room they haven't joined
(and by definition can't have, until this fires). add_member publishes
a room_added event on the target user's channel.

Frontend: the WebSocket connection is no longer scoped to whichever
room is open -- ChatShellPage now owns one persistent connection for
the whole session (including while no room is open, which is exactly
when this bug showed), and ChatPane joins/leaves rooms on top of it.
A room_added event triggers a room-list refetch with no reload needed.

Verified end-to-end in the browser: a user sitting on the empty room
list saw a newly-added room appear live, then chatted in it normally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 10:29:06 -06:00
ksmithandClaude Sonnet 5 0b995ef75f Phase 5: Redis pub/sub for horizontal scaling
Splits the WebSocket layer into three pieces so one app instance and many
behave identically: ConnectionManager stays a purely local socket registry;
RoomBroadcaster publishes chat messages to a per-room Redis channel and
every instance (including the publisher) forwards received messages to its
own local sockets via a single psubscribe("room:*") listener started in
main.py's lifespan; Presence is a Redis-backed refcounted hash per room
tracking who's connected across all instances.

Presence replaces the old process-local connected_user_ids check that
Phase 4's offline-push logic used -- without it, a user connected on a
different instance would look offline and get a redundant push. Fixing
this was scoped in beyond the issue's literal ask (message fan-out only)
since it's a real correctness gap in a phase specifically about running
more than one instance; a known limitation (no heartbeat/TTL, so a hard
crash leaks a presence increment) is documented in the README instead of
solved here.

New tests/test_broadcast.py spins up two independent app instances sharing
one Postgres + Redis to prove delivery and presence both actually cross
the Redis boundary, not just work in-process. Manually verified the same
thing against two real uvicorn processes on different ports.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 07:13:06 -06:00