feat(chat): unread read-receipts — lobby/game dot, history-open ack, admin + metrics #77

Merged
developer merged 3 commits from feature/chat-read-receipts into development 2026-06-17 09:52:23 +00:00
Owner

In-game chat read-receipts: a durable unread model replacing the old in-memory client count.

What

  • Schema (migration 00008): chat_messages.unread_seats smallint bitmask — a set bit per recipient seat that has not read the entry; a text message seeds every recipient's bit, a nudge only the awaited seat's. Inverted so unread_seats <> 0 = unread (partial index).
  • Read triggers: a seat clears its bit on POST /games/:id/chat/read (sent only when the client holds unread, so a history open is not a constant backend call) — fired when the player opens the move history (even without entering chat) or the chat. A nudge additionally clears when its recipient moves (a wired game.NudgeClearer, dependency-inverted).
  • UI: a per-viewer unread dot in the lobby (next to the opponent) and the game score bar — seeded from the authoritative REST views (unread_chat on the game view), raised by live chat/nudge events. Opening the history fade-blinks the 💬 twice then acks.
  • Admin: an unread-only filter, a read/unread column (+ CSV), and a per-message card with the per-seat read breakdown (sender / read / unread).
  • Observability: histogram chat_read_duration (publish→read latency by kind) + observable gauge chat_unread_messages + social-domain tracing spans. Grafana dashboard intentionally deferred (owner decision).

Owner decisions baked in

  • nudge participates in unread and is cleared by the recipient's move; one dot per game (no count); metrics now, dashboard later; single feature branch.

Tests

  • Backend integration (real Postgres): bitmask insert, MarkRead, UnreadGames/HasUnread, nudge multiplayer targeting (only the to-move seat), nudge-cleared-by-move (real SubmitPlay), admin filter + per-seat detail. Full suite green.
  • Wire round-trip both sides: gateway transcode unread_chat + chat.read op; TS codec unreadChat.
  • UI: 272 vitest + 140 Playwright e2e (Chromium + WebKit) green — no regression.

Docs

  • docs/ARCHITECTURE.md §8/§9/§11, docs/FUNCTIONAL.md (+_ru), PRERELEASE.md (row CR), gateway/README, backend/README, Go Doc.

Notes

  • Additive migration + trailing fbs field → goose migrates on deploy; no contour wipe expected.
  • The 💬 blink / dot are best verified visually on the test contour.
In-game chat **read-receipts**: a durable unread model replacing the old in-memory client count. ## What - **Schema (migration `00008`)**: `chat_messages.unread_seats smallint` bitmask — a set bit per recipient seat that has **not** read the entry; a text message seeds every recipient's bit, a nudge only the awaited seat's. Inverted so `unread_seats <> 0` = unread (partial index). - **Read triggers**: a seat clears its bit on `POST /games/:id/chat/read` (sent **only when the client holds unread**, so a history open is not a constant backend call) — fired when the player opens the **move history** (even without entering chat) or the chat. A **nudge additionally clears when its recipient moves** (a wired `game.NudgeClearer`, dependency-inverted). - **UI**: a per-viewer unread **dot** in the lobby (next to the opponent) and the game score bar — seeded from the authoritative REST views (`unread_chat` on the game view), raised by live chat/nudge events. Opening the history **fade-blinks the 💬 twice** then acks. - **Admin**: an **unread-only filter**, a read/unread column (+ CSV), and a per-message **card** with the per-seat read breakdown (sender / read / unread). - **Observability**: histogram `chat_read_duration` (publish→read latency by kind) + observable gauge `chat_unread_messages` + social-domain tracing spans. Grafana dashboard intentionally **deferred** (owner decision). ## Owner decisions baked in - nudge participates in unread and is cleared by the recipient's move; one **dot per game** (no count); metrics now, dashboard later; single feature branch. ## Tests - Backend integration (real Postgres): bitmask insert, `MarkRead`, `UnreadGames`/`HasUnread`, **nudge multiplayer targeting** (only the to-move seat), nudge-cleared-by-move (real `SubmitPlay`), admin filter + per-seat detail. Full suite green. - Wire round-trip both sides: gateway transcode `unread_chat` + `chat.read` op; TS codec `unreadChat`. - UI: 272 vitest + 140 Playwright e2e (Chromium + WebKit) green — no regression. ## Docs - `docs/ARCHITECTURE.md` §8/§9/§11, `docs/FUNCTIONAL.md` (+`_ru`), `PRERELEASE.md` (row **CR**), `gateway/README`, `backend/README`, Go Doc. ## Notes - Additive migration + trailing fbs field → goose migrates on deploy; **no contour wipe expected**. - The 💬 blink / dot are best verified visually on the test contour.
developer added 1 commit 2026-06-17 09:13:01 +00:00
feat(chat): unread read-receipts with lobby/game dot and history-open ack
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 49s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m16s
aaac816dc2
Persist per-message read state as a chat_messages.unread_seats bitmask
(migration 00008): a text message seeds every recipient seat's bit, a nudge
only the awaited seat's. A seat's bit clears when the player opens the move
history or chat (POST /games/:id/chat/read, sent only when something is
unread), and a nudge additionally clears when its recipient answers by moving
(a wired game NudgeClearer, dependency-inverted so game keeps off social).

UI shows a per-viewer unread dot in the lobby (next to the opponent) and the
game score bar — the unread_chat game-view flag seeds it from authoritative
REST views, live chat/nudge events raise it. Opening the move history counts
as reading (even without entering chat): the 💬 fade-blinks twice and the
client acks. Admin Messages gains an unread-only filter, a read/unread column,
and a per-message card with the per-seat read breakdown. Observability:
chat_read_duration histogram + chat_unread_messages gauge + social tracing.
developer added 1 commit 2026-06-17 09:14:27 +00:00
fix(chat): don't restore unread on a failed read-ack (would loop the in-game effect; REST re-seed self-heals)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 49s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 59s
a7f3df9346
owner approved these changes 2026-06-17 09:39:43 +00:00
Dismissed
developer added 1 commit 2026-06-17 09:47:01 +00:00
feat(chat): a message to a disguised robot opponent is born read
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 49s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m21s
20f2a5a011
A pooled robot substituted into an ordinary (non-AI) game never opens the
chat, so a text message to it would linger unread forever — skewing the unread
count and the publish-to-read metric. Clear its recipient bit at PostMessage
time (robotRecipients via account.IsRobot), so the message is born read. The
human sender never had their own message unread, so this is invisible to them;
a nudge to a robot already self-clears when the robot answers by moving.
developer dismissed owner's review 2026-06-17 09:47:01 +00:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

owner approved these changes 2026-06-17 09:51:25 +00:00
developer merged commit b71f006ddf into development 2026-06-17 09:52:23 +00:00
developer deleted branch feature/chat-read-receipts 2026-06-17 09:52:23 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: developer/scrabble-game#77