Round-6 follow-up: UX polish + client-IP fix
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 32s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s

- Client IP: the compose caddy trusts X-Forwarded-For from private-range
  upstreams (trusted_proxies private_ranges), so the real client IP survives
  the host-caddy hop (it was logging the docker caddy hop 172.18.0.x for chat
  moderation and bucketing the gateway per-IP rate limiter on it). Correct and
  spoof-safe in both contours (prod has no host caddy); peerIP unit-tested.
- Ad banner gated off behind a compile-time SHOW_AD_BANNER=false (the if-branch,
  the AdBanner import and banner.ts are tree-shaken out of the prod bundle).
- Landing: the Telegram entry is just the 64px logo (clickable, no button/text).
- TG-fullscreen header: title + menu centred as a pair (hamburger right of the
  title), pinned to the bottom of the TG nav band.
- Edge-swipe back (Screen): a left-edge rightward drag navigates to back
  (touch/pen only, armed from <=24px; skipped inside Telegram).
- Chat soft-keyboard: a bottom-sheet Modal lifted above the keyboard by a
  visualViewport-driven transform (compositor-only, no page/sheet relayout).
  iOS-specific, needs on-device tuning; native resize=none awaits Capacitor.
- Tests: e2e for the in-game '✓ in friends' item and a board→board tile
  relocation; codec units for last_activity_unix + OutgoingRequestList.

Deferred to the next PR (agreed): #4 enrich the your-turn/game-end push; #5 hide
finished games from the lobby.
This commit is contained in:
Ilia Denisov
2026-06-08 21:31:44 +02:00
parent f95a6cb9c8
commit 645df52c0b
12 changed files with 229 additions and 29 deletions
+12
View File
@@ -122,6 +122,18 @@ test('game: add-to-friends flips to a disabled "request sent"', async ({ page })
await expect(sent).toBeDisabled();
});
test('game: an opponent who is already a friend shows a disabled "in friends"', async ({ page }) => {
await loginLobby(page);
await page.getByRole('button', { name: /Kaya/ }).click(); // the finished game vs Kaya, a seeded friend
await page.locator('.burger').first().click();
// The in-game friend item is derived from the server's friend list (Stage 17): a friend reads
// a disabled "✓ in friends", not the addable "Add to friends".
const inFriends = page.getByRole('button', { name: /in friends/i });
await expect(inFriends).toBeVisible();
await expect(inFriends).toBeDisabled();
await expect(page.getByRole('button', { name: /Add to friends: Kaya/ })).toHaveCount(0);
});
test('profile edit disables Save and flags an invalid display name', async ({ page }) => {
await loginLobby(page);
await page.locator('.burger').first().click();