The pywebpush version bump alone didn't fix WNS: even the latest
release (2.4.0) has no WNS-specific header handling in its own
source, confirmed by inspecting the installed package directly.
Adds the required X-WNS-Cache-Policy header ourselves via
webpush()'s own headers= param, gated to *.notify.windows.com
endpoints.
Also: subscribeToPush()'s permission request and service-worker-ready
wait had no timeout, so a browser that never settles either (seen
live on a fresh Windows/Edge install -- greyed out, no prompt, no
error) left the toggle stuck forever with no feedback. Both now time
out after 20s with an actionable message instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WNS (Windows/Edge push) has required an X-WNS-Cache-Policy header
since April 2024; the production venv was likely still on the old
pywebpush 2.0.x installed when the app was first deployed, which
predates the library's fix. Floored the dependency at 2.4.0 (current
latest) so the next deploy picks it up.
Also logs response body and headers on any non-410/404 push failure,
not just body text -- WNS's own 400s carry their actual reason in a
header, which the old body-only logging would still have missed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Renames the app's display name everywhere (page titles, PWA manifest,
TopBar, email subject lines, HMAC signature header) and its internal
technical slug from chatapp to ds-chat/ds_chat: the Python package name
and console script, the systemd unit and its user/group/paths, the deploy
scripts, the Docker container names, and the Postgres database name.
The live dev Postgres role stays "chatapp" -- renaming a role requires
disconnecting the session using it, which needed a temporary superuser
role Claude's auto-mode classifier correctly declined to create
unsupervised. Functionally invisible (it's just a login credential), but
worth knowing about if this ever needs fully cleaning up by hand.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend: PushSubscription model/migration, VAPID config + `cli.py
generate-vapid-keys`, push_service.send_push_to_user (upsert-by-endpoint
subscribe/unsubscribe, auto-cleanup of expired 404/410 subscriptions),
/api/push/* router, and ConnectionManager now tracks connected user IDs
per room so chat.py can push only to offline members after broadcasting
to online ones.
Two test-infra bugs found and fixed along the way: send_push_to_user
takes the caller's AsyncSession and is awaited inline rather than fired
via asyncio.create_task with its own session (background tasks were
outliving the test event loop); and the ws_client fixture now uses
NullPool to eliminate a connection-pool checkout race that was failing
WS tests intermittently.
Frontend: service worker rebuilt with vite-plugin-pwa's injectManifest
strategy (custom src/sw.ts) so it can add push/notificationclick
handlers alongside the existing precaching and StaleWhileRevalidate
routes ported over from generateSW. New subscribe/unsubscribe flow
(lib/push.ts, api/push.ts) with a toggle in the account menu.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>