2cd44dc3f73b52b5001c1af64f37fd5fd1ff8eeb
Root cause of "still not getting push notifications while backgrounded" despite #31: joinRoom() was made to send unconditionally on the theory that opening a room always implies genuine visibility (a real click can't happen on a truly hidden tab). That's wrong for one real case -- mobile Chrome can silently discard and later reload a long-backgrounded tab from memory, which re-mounts the room and calls joinRoom() again with nobody actually looking at the screen. Each such reload re-joined the room's presence with no matching "leave" (a discard skips normal unmount cleanup), so a room could accumulate a stuck presence entry that permanently suppressed push notifications for it -- confirmed live via a user's server logs (repeated silent WS reconnects, and their account still showing present in the room's Redis presence hash while genuinely backgrounded). joinRoom() and the reconnect replay now check document.visibilityState live instead of trusting a cached ref or sending unconditionally: a still-hidden reload correctly stays "left" (the room stays in desiredRoomsRef, so the next genuine foreground transition still joins it, just deferred instead of wrongly immediate), while a real user-driven open still joins immediately as before. Verified both directions in the browser: mounting a room while genuinely hidden leaves the room's presence hash empty; a subsequent real visibility transition to visible correctly triggers the deferred join. Note: this prevents new stuck entries but doesn't retroactively clear any that already exist -- an affected user needs one real close (not just backgrounding) to send a clean disconnect and reset the stuck refcount. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
DS Chat
A web-based team chat service (Mattermost-style, no threaded conversations), invite-only. See ARCHITECTURE.md for the full system design and phased build plan.
Phase 1: auth, open-room CRUD, and single-instance WebSocket chat, backend
- a minimal frontend. Phase 2: private rooms, room roles (owner/admin/ member), and room invites — backend only, see below. Later phases (push notifications, Redis fan-out, the admin portal, the bot/extension system, and production deployment) are tracked as issues in the repo's issue tracker, prioritized.
Structure
backend/— FastAPI + SQLAlchemy 2.0 (async) + PostgreSQL. Seebackend/README.mdfor local setup, migrations, how to create a user (site registration is invite-only — no public sign-up endpoint), and the Phase 2 room-roles/invites API.frontend/— React + Vite PWA (login, room list, chat view). Still Phase-1-only: it doesn't yet call any of the Phase 2 endpoints. A UI redesign is happening separately; frontend work resumes once that lands.
Quickstart
# 1. Postgres (see backend/README.md for details)
docker run -d --name ds-chat-postgres \
-e POSTGRES_USER=ds_chat -e POSTGRES_PASSWORD=ds_chat -e POSTGRES_DB=ds_chat \
-p 5432:5432 postgres:16-alpine
# 2. Backend
cd backend
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
cp .env.example .env # then set SESSION_SECRET
.venv/bin/alembic upgrade head
.venv/bin/python -m app.cli create-user alice alice@example.com "some-password"
.venv/bin/uvicorn app.main:app --reload &
# 3. Frontend (in another shell)
cd frontend
npm install
npm run dev
Then open http://localhost:5173 and log in with the account created above.
The Vite dev server proxies /api and /ws to the backend on :8000, so no
CORS configuration is needed in development.
Deployment
See DEPLOYMENT.md for the full production runbook — two Debian 13 servers, no containers, matching ARCHITECTURE.md §9.