Stage 8: UI social/account/history surfaces
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 13s
Tests · UI / test (push) Successful in 16s

Wire the deferred Stage 7 surfaces end-to-end (UI -> gateway transcode ->
backend REST -> existing domain services): friends (incl. one-time friend
codes), per-user blocks, friend-game invitations, profile editing + email
binding, the statistics screen, and the in-game history + GCG export.

Friends gain two add paths (interview decision, a deliberate plan change):
one-time 6-digit codes (friend_codes table, 12h TTL, single-use, rate-limited
redeem); and play-gated requests (shared game required) where an explicit
decline is permanent, an ignored request lapses after 30 days, and a code
bypasses a decline. Migration 00006 widens friendships_status_chk and adds
friend_codes.

Lobby notification badge is poll + push: a new generic `notify` event drives
it live; the client polls on open/focus. Language stays a single Settings
control that writes through to the durable account's preferred_language. GCG
export is finished-only (game.ErrGameActive) and shares/downloads the .gcg file.

Tests: backend unit + inttest (friend gate/decline/code, ListInvitations,
GetStats, GCG gate), gateway transcode round-trips + notify constructor, UI
vitest (codecs, win-rate, share choice) + Playwright social specs. Docs: PLAN
(Stage 8 done + refinements + TODO-5), ARCHITECTURE, FUNCTIONAL(+ru), UI_DESIGN,
TESTING, module READMEs.
This commit is contained in:
Ilia Denisov
2026-06-03 19:47:40 +02:00
parent 539e24fba1
commit d733ce3119
114 changed files with 8210 additions and 149 deletions
+43 -2
View File
@@ -4,7 +4,17 @@
// not need to be strictly legal here — this is a visual/interaction fixture; real
// legality and scoring come from the backend.
import type { ChatMessage, GameView, MoveRecord, Profile, Seat, Session } from '../model';
import type {
AccountRef,
ChatMessage,
GameView,
Invitation,
MoveRecord,
Profile,
Seat,
Session,
Stats,
} from '../model';
export const ME = 'me';
@@ -20,12 +30,43 @@ export const PROFILE: Profile = {
displayName: 'You',
preferredLanguage: 'en',
timeZone: 'UTC',
awayStart: '00:00',
awayEnd: '07:00',
hintBalance: 3,
blockChat: false,
blockFriendRequests: false,
isGuest: true,
isGuest: false,
};
// Seed social/account data for the mock (pnpm start + Playwright). The mock profile
// is a durable account so the Stage 8 surfaces (friends, stats, history) are reachable.
export const MOCK_FRIENDS: AccountRef[] = [
{ accountId: 'ann', displayName: 'Ann' },
{ accountId: 'kaya', displayName: 'Kaya' },
];
export const MOCK_INCOMING: AccountRef[] = [{ accountId: 'rick', displayName: 'Rick' }];
export const MOCK_STATS: Stats = { wins: 7, losses: 4, draws: 1, maxGamePoints: 421, maxWordPoints: 95 };
export function mockInvitations(): Invitation[] {
return [
{
id: 'inv1',
inviter: { accountId: 'kaya', displayName: 'Kaya' },
invitees: [{ accountId: ME, displayName: 'You', seat: 1, response: 'pending' }],
variant: 'english',
turnTimeoutSecs: 86400,
hintsAllowed: true,
hintsPerPlayer: 1,
dropoutTiles: 'remove',
status: 'pending',
gameId: '',
expiresAtUnix: Math.floor(Date.now() / 1000) + 7 * 86400,
},
];
}
function seat(s: number, accountId: string, displayName: string, score: number, isWinner = false): Seat {
return { seat: s, accountId, displayName, score, hintsUsed: 0, isWinner };
}