Nudge badges lingered in the lobby on games that ended by turn-timeout,
resignation or forfeit: those paths commit the finish directly, bypassing
the move path's per-mover NudgeClearer, so the awaited seat's nudge was
never marked read. A finished game's nudges are stale, so clear them all.
Add a wired NudgeExpirer (social.ExpireNudges) called from the shared
commit finish block — covering every completion path — and from the
voidGame recovery path. It clears every seat's nudge bits for the game
and leaves chat messages unread; unlike ClearNudges it records no
publish-to-read latency, since a completion is an expiry, not a read.
An integration test reproduces the timeout case (nudge cleared, chat
kept). Docs: ARCHITECTURE §9.1, FUNCTIONAL (+_ru), backend/README.
Three owner-requested polish changes:
- robot: replace the lengthening 60-90 min -> 6 h proactive-nudge ramp with a
flat uniform 9-12 h wait before every nudge; the existing sleep-window gate
still skips and defers a nudge that would land in the robot's night.
- ui: colour the lobby/in-game unread dot by type -- the regular danger colour
when a chat message is unread, a softer amber (--warn) when only nudges are.
Adds a per-viewer unread_messages flag (chat_messages.kind='message') across
the backend DTO, FlatBuffers wire, gateway transcode and the UI store.
- ui: float games with any unread notification to the top of the lobby's
your-turn and opponent-turn sections (finished keeps its order), reusing the
existing unread_chat flag.
Docs (ARCHITECTURE 7, FUNCTIONAL + _ru) updated. No DB migration; the new wire
field is backward-compatible.
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.