Add While You Were Away session summary modal and indexer endpoint #511

Merged
PlasticDigits merged 3 commits from issue-338-while-you-were-away into main 2026-06-19 08:22:54 +00:00
PlasticDigits commented 2026-06-19 06:38:47 +00:00 (Migrated from gitlab.com)

Summary

Adds the While You Were Away absent-session flow for play / (#338):

  • Indexer GET /v1/arena/session-summary?since_ms=&wallet= (schema 2.18.0) — stateless aggregates since a browser close timestamp: buys, unique players, podium epoch endings (winners + 4∶2∶1 prizes), optional wallet rank delta / wins / buy count. since_ms clamped to 30d; future skew → 400. Podium ingest now stores block_timestamp on idx_arena_podium_epoch.
  • Frontend — per-browser localStorage close time (yieldomega.arena.lastClosedAt.v1), visibilitychange / pagehide persistence, glass WhileYouWereAwayModal on TimeArenaPage, indexer-first fetch via fetchArenaSessionSummary. Skips first visit, <60s absence, indexer offline, and empty summaries.

Acceptance checklist

Criterion Verification Result
Browser persists last-close timestamp cd frontend && npm test -- src/lib/arenaSessionClose.test.ts PASS
Reopen after activity shows populated modal WhileYouWereAwayModal.test.tsx + Playwright screenshots spec PASS
First-time visitors not interrupted useWhileYouWereAway skips when lastClosedAt null; unit tests PASS
Connected winner congratulations WhileYouWereAwayModal.test.tsx wywa-congrats PASS
Glass modal + desktop/tablet/mobile screenshots docs/testing/screenshots/issue-338/*.png via CI=1 npm run test:e2e -- --workers=1 e2e/wywa-modal-screenshots.spec.ts PASS
Indexer + frontend tests; schema documented See verification checklist below PASS

Verification checklist (third parties)

Indexer

cd indexer && cargo test
YIELDOMEGA_PG_TEST_URL=postgres://yieldomega:password@127.0.0.1:5433/yieldomega_indexer_test \
  cargo test --test integration_stage2 arena_session_summary_fixture_since_activity
curl -sf 'http://127.0.0.1:3100/v1/arena/session-summary?since_ms=1700000000000' | jq keys

Frontend

cd frontend && npm run typecheck && npm run lint && npm test
CI=1 npm run test:e2e -- --workers=1 e2e/wywa-modal-screenshots.spec.ts

Live smoke

bash scripts/verify-podium-live-anvil.sh

Manual reopen flow

  1. Open play / with indexer + Anvil stack; note localStorage['yieldomega.arena.lastClosedAt.v1'] after tab hide.
  2. Run buys (bot swarm or manual); reload after >60s → modal shows buy count > 0.

Docs / invariants: INV-INDEXER-338-SESSION-SUMMARY, INV-FRONTEND-338-WYWA-MODAL in docs/testing/invariants-and-business-logic.md; API in docs/indexer/design.md and indexer/README.md; UI in docs/frontend/arena-views.md.

Screenshots: docs/testing/screenshots/issue-338/

## Summary Adds the **While You Were Away** absent-session flow for play `/` ([#338](https://gitlab.com/PlasticDigits/yieldomega/-/issues/338)): - **Indexer** `GET /v1/arena/session-summary?since_ms=&wallet=` (schema **2.18.0**) — stateless aggregates since a browser close timestamp: buys, unique players, podium epoch endings (winners + 4∶2∶1 prizes), optional wallet rank delta / wins / buy count. `since_ms` clamped to 30d; future skew → **400**. Podium ingest now stores `block_timestamp` on `idx_arena_podium_epoch`. - **Frontend** — per-browser `localStorage` close time (`yieldomega.arena.lastClosedAt.v1`), `visibilitychange` / `pagehide` persistence, glass `WhileYouWereAwayModal` on `TimeArenaPage`, indexer-first fetch via `fetchArenaSessionSummary`. Skips first visit, <60s absence, indexer offline, and empty summaries. ## Acceptance checklist | Criterion | Verification | Result | |-----------|--------------|--------| | Browser persists last-close timestamp | `cd frontend && npm test -- src/lib/arenaSessionClose.test.ts` | PASS | | Reopen after activity shows populated modal | `WhileYouWereAwayModal.test.tsx` + Playwright screenshots spec | PASS | | First-time visitors not interrupted | `useWhileYouWereAway` skips when `lastClosedAt` null; unit tests | PASS | | Connected winner congratulations | `WhileYouWereAwayModal.test.tsx` `wywa-congrats` | PASS | | Glass modal + desktop/tablet/mobile screenshots | `docs/testing/screenshots/issue-338/*.png` via `CI=1 npm run test:e2e -- --workers=1 e2e/wywa-modal-screenshots.spec.ts` | PASS | | Indexer + frontend tests; schema documented | See verification checklist below | PASS | ## Verification checklist (third parties) **Indexer** ```bash cd indexer && cargo test YIELDOMEGA_PG_TEST_URL=postgres://yieldomega:password@127.0.0.1:5433/yieldomega_indexer_test \ cargo test --test integration_stage2 arena_session_summary_fixture_since_activity curl -sf 'http://127.0.0.1:3100/v1/arena/session-summary?since_ms=1700000000000' | jq keys ``` **Frontend** ```bash cd frontend && npm run typecheck && npm run lint && npm test CI=1 npm run test:e2e -- --workers=1 e2e/wywa-modal-screenshots.spec.ts ``` **Live smoke** ```bash bash scripts/verify-podium-live-anvil.sh ``` **Manual reopen flow** 1. Open play `/` with indexer + Anvil stack; note `localStorage['yieldomega.arena.lastClosedAt.v1']` after tab hide. 2. Run buys (bot swarm or manual); reload after >60s → modal shows buy count > 0. **Docs / invariants:** `INV-INDEXER-338-SESSION-SUMMARY`, `INV-FRONTEND-338-WYWA-MODAL` in `docs/testing/invariants-and-business-logic.md`; API in `docs/indexer/design.md` and `indexer/README.md`; UI in `docs/frontend/arena-views.md`. **Screenshots:** [docs/testing/screenshots/issue-338/](docs/testing/screenshots/issue-338/)
PlasticDigits commented 2026-06-19 06:38:58 +00:00 (Migrated from gitlab.com)

mentioned in issue #338

mentioned in issue #338
PlasticDigits commented 2026-06-19 06:42:15 +00:00 (Migrated from gitlab.com)

Security review — MR !511

Commit reviewed: eba1e722610acff70edd8f35b3384b608b99d094
Scope: GET /v1/arena/session-summary indexer handler + ingest block_timestamp on idx_arena_podium_epoch; frontend While You Were Away modal, localStorage close persistence, and indexer client wiring.

Outcome: FINDINGS: 0 medium+

Inline threads: none (no medium+ findings to anchor on the diff)

Summary

Reviewed attacker-controlled inputs (since_ms, optional wallet, indexer JSON rendered in the modal, localStorage close timestamp) through SQL sinks, HTTP validation, and React rendering.

  • SQL injection: All new queries use sqlx parameter binding; wallet is normalized and gated by valid_0x_address20 before reaching $1/$2 binds. since_ms is clamped (30d lookback, future skew → 400) and converted to a bound float timestamp — no string concatenation.
  • Authn/authz: Stateless public read aggregate over already-indexed on-chain data, consistent with existing /v1/arena/* endpoints; optional wallet stats mirror data available via /v1/arena/wallet/{address}/stats.
  • XSS / HTML injection: Modal renders via React text nodes; addresses pass through AddressInline (isAddress gate) and amounts through AmountDisplay; podium labels are server-side enum mapping with safe fallback.
  • DoS: Response work is bounded (30d window, 20 podium rows). Rank aggregation runs twice when wallet is set but follows the same unauthenticated read model and DB access patterns as other arena aggregate routes; no new amplification beyond direct API use.
  • EVM / contracts: No smart-contract changes in this diff.

Security review: no medium+ findings on this diff.

## Security review — MR !511 **Commit reviewed:** `eba1e722610acff70edd8f35b3384b608b99d094` **Scope:** `GET /v1/arena/session-summary` indexer handler + ingest `block_timestamp` on `idx_arena_podium_epoch`; frontend While You Were Away modal, localStorage close persistence, and indexer client wiring. **Outcome:** `FINDINGS: 0` medium+ **Inline threads:** none (no medium+ findings to anchor on the diff) ### Summary Reviewed attacker-controlled inputs (`since_ms`, optional `wallet`, indexer JSON rendered in the modal, `localStorage` close timestamp) through SQL sinks, HTTP validation, and React rendering. - **SQL injection:** All new queries use sqlx parameter binding; `wallet` is normalized and gated by `valid_0x_address20` before reaching `$1`/`$2` binds. `since_ms` is clamped (30d lookback, future skew → 400) and converted to a bound float timestamp — no string concatenation. - **Authn/authz:** Stateless public read aggregate over already-indexed on-chain data, consistent with existing `/v1/arena/*` endpoints; optional wallet stats mirror data available via `/v1/arena/wallet/{address}/stats`. - **XSS / HTML injection:** Modal renders via React text nodes; addresses pass through `AddressInline` (`isAddress` gate) and amounts through `AmountDisplay`; podium labels are server-side enum mapping with safe fallback. - **DoS:** Response work is bounded (30d window, 20 podium rows). Rank aggregation runs twice when `wallet` is set but follows the same unauthenticated read model and DB access patterns as other arena aggregate routes; no new amplification beyond direct API use. - **EVM / contracts:** No smart-contract changes in this diff. **Security review: no medium+ findings on this diff.**
PlasticDigits commented 2026-06-19 07:52:22 +00:00 (Migrated from gitlab.com)

added 5 commits

  • eba1e722...e1ebea74 - 4 commits from branch main
  • a7d3b64e - Merge origin/main into issue-338-while-you-were-away

Compare with previous version

added 5 commits <ul><li>eba1e722...e1ebea74 - 4 commits from branch <code>main</code></li><li>a7d3b64e - Merge origin/main into issue-338-while-you-were-away</li></ul> [Compare with previous version](/PlasticDigits/yieldomega/-/merge_requests/141/diffs?diff_id=1860574184&start_sha=eba1e722610acff70edd8f35b3384b608b99d094)
PlasticDigits commented 2026-06-19 07:52:27 +00:00 (Migrated from gitlab.com)

Resolved merge conflict with main in docs/testing/invariants-and-business-logic.md by keeping both invariant sets:

  • #337 (INV-FRONTEND-337-BUY-EFFECT-TOASTS) from main
  • #338 (INV-INDEXER-338-SESSION-SUMMARY, INV-FRONTEND-338-WYWA-MODAL) from this branch

Sanity checks: frontend typecheck/lint + WYWA tests PASS; indexer arena_session_summary unit + integration tests PASS.

Resolved merge conflict with `main` in `docs/testing/invariants-and-business-logic.md` by keeping both invariant sets: - **#337** (`INV-FRONTEND-337-BUY-EFFECT-TOASTS`) from `main` - **#338** (`INV-INDEXER-338-SESSION-SUMMARY`, `INV-FRONTEND-338-WYWA-MODAL`) from this branch Sanity checks: frontend typecheck/lint + WYWA tests PASS; indexer `arena_session_summary` unit + integration tests PASS.
PlasticDigits commented 2026-06-19 07:55:47 +00:00 (Migrated from gitlab.com)

Security review — MR !511

Commit reviewed: a7d3b64ee4981a8d619944f0c829f9e6d1933273
Scope: GET /v1/arena/session-summary indexer handler + ingest block_timestamp on idx_arena_podium_epoch; frontend While You Were Away modal, localStorage close persistence, and indexer client wiring.

Outcome: FINDINGS: 0 medium+

Inline threads: none (no medium+ findings to anchor on the diff)

Summary

Re-reviewed attacker-controlled inputs (since_ms, optional wallet, indexer JSON rendered in the modal, localStorage close timestamp) through SQL sinks, HTTP validation, and React rendering on the post-merge-conflict head.

  • SQL injection: All new queries use sqlx parameter binding; wallet is normalized and gated by valid_0x_address20 before reaching $1/$2 binds. since_ms is clamped (30d lookback, future skew → 400) and converted to a bound float timestamp — no string concatenation.
  • Authn/authz: Stateless public read aggregate over already-indexed on-chain data, consistent with existing /v1/arena/* endpoints; optional wallet stats mirror data available via /v1/arena/wallet/{address}/stats.
  • XSS / HTML injection: Modal renders via React text nodes; addresses pass through AddressInline (isAddress gate) and amounts through AmountDisplay; podium labels are server-side enum mapping with safe fallback.
  • DoS: Response work is bounded (30d window, 20 podium rows). Rank aggregation runs twice when wallet is set but follows the same unauthenticated read model and DB access patterns as other arena aggregate routes; no new amplification beyond direct API use.
  • EVM / contracts: No smart-contract changes in this diff.

Security review: no medium+ findings on this diff.

## Security review — MR !511 **Commit reviewed:** `a7d3b64ee4981a8d619944f0c829f9e6d1933273` **Scope:** `GET /v1/arena/session-summary` indexer handler + ingest `block_timestamp` on `idx_arena_podium_epoch`; frontend While You Were Away modal, `localStorage` close persistence, and indexer client wiring. **Outcome:** `FINDINGS: 0` medium+ **Inline threads:** none (no medium+ findings to anchor on the diff) ### Summary Re-reviewed attacker-controlled inputs (`since_ms`, optional `wallet`, indexer JSON rendered in the modal, `localStorage` close timestamp) through SQL sinks, HTTP validation, and React rendering on the post-merge-conflict head. - **SQL injection:** All new queries use sqlx parameter binding; `wallet` is normalized and gated by `valid_0x_address20` before reaching `$1`/`$2` binds. `since_ms` is clamped (30d lookback, future skew → 400) and converted to a bound float timestamp — no string concatenation. - **Authn/authz:** Stateless public read aggregate over already-indexed on-chain data, consistent with existing `/v1/arena/*` endpoints; optional wallet stats mirror data available via `/v1/arena/wallet/{address}/stats`. - **XSS / HTML injection:** Modal renders via React text nodes; addresses pass through `AddressInline` (`isAddress` gate) and amounts through `AmountDisplay`; podium labels are server-side enum mapping with safe fallback. - **DoS:** Response work is bounded (30d window, 20 podium rows). Rank aggregation runs twice when `wallet` is set but follows the same unauthenticated read model and DB access patterns as other arena aggregate routes; no new amplification beyond direct API use. - **EVM / contracts:** No smart-contract changes in this diff. **Security review: no medium+ findings on this diff.**
PlasticDigits commented 2026-06-19 08:19:55 +00:00 (Migrated from gitlab.com)

added 4 commits

  • a7d3b64e...1411dc49 - 3 commits from branch main
  • 0fdf405b - Merge origin/main into issue-338-while-you-were-away

Compare with previous version

added 4 commits <ul><li>a7d3b64e...1411dc49 - 3 commits from branch <code>main</code></li><li>0fdf405b - Merge origin/main into issue-338-while-you-were-away</li></ul> [Compare with previous version](/PlasticDigits/yieldomega/-/merge_requests/141/diffs?diff_id=1860625354&start_sha=a7d3b64ee4981a8d619944f0c829f9e6d1933273)
PlasticDigits commented 2026-06-19 08:20:01 +00:00 (Migrated from gitlab.com)

Resolved merge conflicts with main (merge commit 0fdf405b).

Conflict: frontend/src/index.css — both branches added modal CSS at the same location (#338 While You Were Away vs #335 level-up celebration).

Resolution: Kept both style blocks in order (WYWA modal, then level-up celebration popover). Completed the truncated WYWA @media (max-width: 520px) rule.

Sanity: cd frontend && npm run typecheck — PASS.

Resolved merge conflicts with `main` (merge commit `0fdf405b`). **Conflict:** `frontend/src/index.css` — both branches added modal CSS at the same location (#338 While You Were Away vs #335 level-up celebration). **Resolution:** Kept both style blocks in order (WYWA modal, then level-up celebration popover). Completed the truncated WYWA `@media (max-width: 520px)` rule. **Sanity:** `cd frontend && npm run typecheck` — PASS.
PlasticDigits commented 2026-06-19 08:22:29 +00:00 (Migrated from gitlab.com)

Security review — MR !511

Commit reviewed: 0fdf405b4e9b7ecc0400980cfe1ccabb798e0c32
Scope: GET /v1/arena/session-summary indexer handler + ingest block_timestamp on idx_arena_podium_epoch; frontend While You Were Away modal, localStorage close persistence, and indexer client wiring (post-merge-conflict head including index.css resolution with #335).

Outcome: FINDINGS: 0 medium+

Inline threads: none (no medium+ findings to anchor on the diff)

Summary

Re-reviewed attacker-controlled inputs (since_ms, optional wallet, indexer JSON rendered in the modal, localStorage close timestamp) through SQL sinks, HTTP validation, and React rendering on the latest MR head.

  • SQL injection: All new queries use sqlx parameter binding; wallet is normalized and gated by valid_0x_address20 before reaching $1/$2 binds. since_ms is clamped (30d lookback, future skew → 400) and converted to a bound float timestamp — no string concatenation.
  • Authn/authz: Stateless public read aggregate over already-indexed on-chain data, consistent with existing /v1/arena/* endpoints; optional wallet stats mirror data available via /v1/arena/wallet/{address}/stats.
  • XSS / HTML injection: Modal renders via React text nodes; addresses pass through AddressInline (isAddress gate) and amounts through AmountDisplay; podium labels are server-side enum mapping with safe fallback.
  • DoS: Response work is bounded (30d window, 20 podium rows). Rank aggregation runs twice when wallet is set but follows the same unauthenticated read model and DB access patterns as other arena aggregate routes; no new amplification beyond direct API use.
  • EVM / contracts: No smart-contract changes in this diff.

Security review: no medium+ findings on this diff.

## Security review — MR !511 **Commit reviewed:** `0fdf405b4e9b7ecc0400980cfe1ccabb798e0c32` **Scope:** `GET /v1/arena/session-summary` indexer handler + ingest `block_timestamp` on `idx_arena_podium_epoch`; frontend While You Were Away modal, `localStorage` close persistence, and indexer client wiring (post-merge-conflict head including `index.css` resolution with #335). **Outcome:** `FINDINGS: 0` medium+ **Inline threads:** none (no medium+ findings to anchor on the diff) ### Summary Re-reviewed attacker-controlled inputs (`since_ms`, optional `wallet`, indexer JSON rendered in the modal, `localStorage` close timestamp) through SQL sinks, HTTP validation, and React rendering on the latest MR head. - **SQL injection:** All new queries use sqlx parameter binding; `wallet` is normalized and gated by `valid_0x_address20` before reaching `$1`/`$2` binds. `since_ms` is clamped (30d lookback, future skew → 400) and converted to a bound float timestamp — no string concatenation. - **Authn/authz:** Stateless public read aggregate over already-indexed on-chain data, consistent with existing `/v1/arena/*` endpoints; optional wallet stats mirror data available via `/v1/arena/wallet/{address}/stats`. - **XSS / HTML injection:** Modal renders via React text nodes; addresses pass through `AddressInline` (`isAddress` gate) and amounts through `AmountDisplay`; podium labels are server-side enum mapping with safe fallback. - **DoS:** Response work is bounded (30d window, 20 podium rows). Rank aggregation runs twice when `wallet` is set but follows the same unauthenticated read model and DB access patterns as other arena aggregate routes; no new amplification beyond direct API use. - **EVM / contracts:** No smart-contract changes in this diff. **Security review: no medium+ findings on this diff.**
PlasticDigits commented 2026-06-19 08:22:54 +00:00 (Migrated from gitlab.com)

mentioned in commit 8821b2dfba

mentioned in commit 8821b2dfba632ee18c61af771c302f421f1b429a
PlasticDigits (Migrated from gitlab.com) merged commit 8821b2dfba into main 2026-06-19 08:22:54 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/yieldomega!511
No description provided.