# Phase 06.6: UAT accounts, integer viewport scaling, chat region clamp, right-click menu - Context

**Gathered:** 2026-05-16
**Status:** Ready for planning
**Revisions:** 2026-05-16 (post-research) — see <revisions> block below; supersedes D-02, D-03, D-04, D-15, D-18

<revisions>
## Post-Research Revisions (AUTHORITATIVE — override conflicting D-XX above)

### R-01 (supersedes D-02) — Password source: migration-generated random, printed once to stdout
The seed migration generates a strong random password (≥24 chars, cryptographically random via `crypto.randomBytes`) on first run, argon2id-hashes it with shared `ARGON2_OPTS`, persists the hash, and prints the plaintext to stdout EXACTLY ONCE for operator capture. Subsequent runs detect existing rows and no-op silently (no password regeneration, no stdout). **Zero env vars. Zero stored secrets. Zero plaintext at rest.** Operator captures the printed password on first staging deploy (or first local run) and stores it in their personal password manager.

### R-02 (supersedes D-03) — Playwright stays on dev-bypass; new accounts are operator-only
Playwright `cli-08.e2e` and CI smoke continue to use `dev-bypass:uat_a` / `dev-bypass:uat_b` synthetic tokens unchanged. `dunsen_uat` / `rebbie_uat` are **operator-manual-UAT-only accounts**. Operator uses them to manually test against staging or local in parallel with Playwright automation (no collision because Playwright uses different account IDs). **No Playwright fixture changes. No CI secret setup. No GitHub Actions changes. No `.github/workflows/deploy-staging.yml` edits.**

### R-03 (supersedes D-04 account_id semantics) — `account.account_id = user.id` (UUID)
Match the canonical pattern from `apps/server/src/legacy-login.ts:177-247` exactly. Set `account.account_id = user.id` (UUID). `user.username` (UNIQUE) is the real lookup column for Better-Auth canonical username sign-in. Idempotency check uses `SELECT … WHERE username = ?` against `user` table.

### R-04 (supersedes D-15) — Contextmenu suppression via Phaser canonical API
Use `this.input.mouse.disableContextMenu()` in `GameScene.create()`. This is Phaser 3's canonical API; it auto-scopes to `game.canvas` (NOT global `document`), automatically preserves copy/paste contextmenu on `#dom-overlay` children (chat input, EscMenu DOM). Replaces D-15's hand-rolled `document.addEventListener('contextmenu', …)` proposal. No manual teardown needed — Phaser's input plugin handles lifecycle.

### R-05 (clarifies D-18) — No Playwright test updates required
Because Playwright keeps `dev-bypass:uat_a/uat_b` (R-02), the cli-08 smoke does NOT need real-login flow updates. The right-click migration (D-14) affects only operator-driven manual UAT and `esc-menu.test.ts` jsdom unit tests. Playwright tests that simulate "click to open menu" are: **none** today (cli-08 smoke does not interact with EscMenu). Only the jsdom unit test in `apps/client/src/__test__/esc-menu.test.ts` needs the left→right migration. `06-HUMAN-UAT.md` operator script updated for right-click + new credential capture step.

### Out-of-scope additions captured by revisions
- No new GitHub Actions secrets.
- No new Fly.io secrets.
- No new env vars in `fly.staging.toml` or `fly.toml`.
- No changes to `apps/client/test/e2e/fixtures.ts` UAT_ACCOUNT_*/UAT_PASSWORD_* logic.
- No changes to `.github/workflows/deploy-staging.yml`.

</revisions>


<domain>
## Phase Boundary

Four small, independent quality-of-life fixes to unblock parallel UAT (operator + Playwright simultaneously on staging) and tighten the desktop client presentation before Phase 7 full-parity work begins:

1. Seed two real Better-Auth accounts (`dunsen_uat`, `rebbie_uat`) so the Playwright two-client smoke can use real login + co-exist with operator-driven manual UAT (which uses `uat_a`/`uat_b` dev-bypass tokens) without account-id collision.
2. Replace Phaser's current `Scale.FIT + MAX_ZOOM + autoRound:true` (which produces fractional zoom in spite of the autoRound name) with explicit integer scaling — pick `Math.floor(min(winW/640, winH/480))` and re-apply on resize. Letterbox/pillarbox fills the remainder.
3. Confine the chat HUD overlay to the live canvas rect (currently it spans `position:fixed inset:0`, i.e., the full window including letterbox bars). Reposition on every resize / zoom change so it tracks the canvas.
4. Move the Esc-menu pointer-trigger from any-button `pointerdown` to right-click only, suppress the browser context menu, and reserve left-click as a no-op for future Phase 7 click-to-walk / interact work. ESC keydown trigger remains intact.

**Out of scope:** new UI features, settings persistence, mobile/touch input, fullscreen API, click-to-walk implementation (left-click stays no-op for now).
</domain>

<decisions>
## Implementation Decisions

### UAT Accounts
- **D-01:** Seed `dunsen_uat` and `rebbie_uat` as **real** Better-Auth account rows via an **idempotent migration** in `apps/server/scripts/run-migrations.ts` chain. Migration is a no-op if rows already exist (check by `username` / `account_id`). Reason: Litestream-restore-safe, zero manual step in CI/staging deploy, survives volume wipes, parallels existing migration pattern.
- **D-02:** Password sourced from a single env var (e.g. `UAT_TEST_PASSWORD`) at migration runtime. Argon2id hash computed during migration. NEVER hardcode plaintext in the repo. Same env var injected into Playwright CI so both ends agree.
- **D-03:** Playwright config + globalSetup switched from `dev-bypass:uat_a/uat_b` synthetic tokens to **real login** as `dunsen_uat` / `rebbie_uat`. Operator-driven UAT continues to use `uat_a` / `uat_b` dev-bypass — collision-free.
- **D-04:** Account flags: regular non-admin accounts. No special role bits. Username matches `account_id` (Better-Auth canonical lookup column). Display name set to `Dunsen` / `Rebbie` (capitalized).
- **D-05:** D-58c diagnostic gate at `RebnoRoom.ts:1206` (still present? — verify; freeze fix removed the spike but the per-account gate may remain in other paths) is NOT auto-extended to the new accounts. If gated diagnostics ever return, operator accounts stay opted in; Playwright accounts stay opted out.

### Integer Viewport Scaling
- **D-06:** Replace `scale.mode: Phaser.Scale.FIT` + `zoom: Phaser.Scale.MAX_ZOOM` with **manual integer zoom**: `scale.mode: Phaser.Scale.NONE` (or RESIZE — researcher to confirm which preserves canvas-pixel-perfect rendering without internal Phaser re-fits), then compute `zoom = Math.max(1, Math.floor(Math.min(winW/640, winH/480)))` and call `game.scale.setZoom(zoom)` at boot + on every `window.resize`.
- **D-07:** Letterbox/pillarbox region = canvas backgroundColor `#0A0E1A` (UI-SPEC dominant). Centered via `autoCenter: CENTER_BOTH`.
- **D-08:** Floor minimum zoom = 1x (no sub-integer fallback if window is smaller than 640×480 — canvas just clips at the edges; acceptable per project's Chrome-desktop target).
- **D-09:** `roundPixels`, `pixelArt`, `antialias: false` invariants from `main.ts:32-34` MUST stay. NaN/zero defensive guard around the resize callback (e.g., headless test environments where `window.innerWidth = 0`).

### Chat Overlay Clamp
- **D-10:** Track canvas via `game.canvas.getBoundingClientRect()` on (a) boot after Phaser READY, (b) `window.resize`, (c) the Phaser `SCALE_CHANGE` / `RESIZE` event the new manual-zoom code emits in D-06. Apply `left/top/width/height` to ChatHUD's root container (currently `position:fixed inset:0`).
- **D-11:** Maintain the HARD invariant from `ChatHUD.ts:7-14` and `ADR 0008`: ChatHUD stays under `#dom-overlay` (sibling of `#game-root`), NOT inside `#game-root`. We change ChatHUD's container CSS from `inset:0` to explicit `left/top/width/height` set by the new tracker.
- **D-12:** Same tracker applies to any sibling overlay that should be canvas-bound. EscMenu stays centered via `transform: translate(-50%, -50%)` against its own `top:50%; left:50%` and is acceptable as full-window since it is a modal-style overlay — DO NOT clamp it.
- **D-13:** Tracker debounces nothing (canvas-rect reads are cheap) but coalesces multiple events fired within the same animation frame via `requestAnimationFrame`.

### Right-Click Menu
- **D-14:** Replace `this.input.on('pointerdown', ...)` at `GameScene.ts:297` with a button-discriminating handler: open EscMenu only when `event.button === 2` (right-click). All existing D-34 suppression guards (escMenu open, pointer-lock, chat-mode, banner, force-reset) preserved.
- **D-15:** Add a `document.addEventListener('contextmenu', e => e.preventDefault())` registration **scoped to the game-root subtree** (NOT global — chat input / EscMenu DOM children must keep normal context menu so future copy-paste UX is not broken). Registration installed in GameScene `create()`, torn down in `shutdown()`.
- **D-16:** Left-click (`event.button === 0`) on the canvas = **no-op**, reserved for future Phase 7 click-to-walk / interact. NO placeholder comment in code — naming + diff context make intent obvious; a stray `// reserved` comment would rot per CLAUDE.md convention. Reservation captured here in CONTEXT.md instead.
- **D-17:** Middle-click and other buttons = no-op (no menu, no preventDefault). Wheel events untouched.
- **D-18:** Existing `06-HUMAN-UAT.md` / Playwright tests that simulate "click to open menu" must be updated to right-click (`page.click({ button: 'right' })` or `page.mouse.down({ button: 'right' })`).

### Claude's Discretion
- Migration file naming, exact env var name (likely `UAT_TEST_PASSWORD` — but researcher should grep existing CI workflow files for established convention first).
- Whether to extract a small `useCanvasRectTracker` helper for D-10 or inline it in ChatHUD. Decide based on whether any other overlay would benefit (BannerReconnect at z=9999 is window-bound on purpose; ForceResetOverlay TBD).
- Exact event for re-clamping: `game.scale.on('resize', ...)` vs `window.addEventListener('resize', ...)` — pick whichever fires reliably after our manual `setZoom` call.

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Phase / Scope
- `.planning/ROADMAP.md` §Phase 06.6 — phase entry (INSERTED marker, goal stub awaiting plan)
- `.planning/STATE.md` — current milestone progress + roadmap evolution log
- `CLAUDE.md` §Extracted Constants — 640×480 viewport, 44×40 tile, 30 Hz tick (LOCKED)

### Client Engine + Renderer
- `apps/client/src/main.ts` — current Phaser config (lines 27-47 — Scale.FIT + MAX_ZOOM to be replaced per D-06)
- `apps/client/src/scenes/GameScene.ts:297` — current canvas `pointerdown` handler (to be replaced per D-14)
- `apps/client/src/ui/EscMenu.ts:7-30` — EscMenu mount-point invariant + D-24/D-34 design notes
- `apps/client/src/ui/ChatHUD.ts:7-30` — ChatHUD HARD mount-point invariant (`#dom-overlay`, NOT inside `#game-root`) + threat model
- `docs/adr/0008-*.md` (if exists) — the mount-point ADR referenced by both overlays; researcher to locate and confirm exact ADR number

### Server / Auth / UAT Bootstrap
- `apps/server/scripts/run-migrations.ts` — existing migration runner; new UAT-seed migration appends here
- `apps/server/scripts/migrate-legacy-accounts.ts` — argon2id hash + Better-Auth row-insert reference pattern
- `apps/server/src/RebnoRoom.ts:433-435` — `dev-bypass:<account_id>` session-token short-circuit (uat_a/uat_b path stays as-is for operator UAT)
- `apps/server/src/RebnoRoom.ts:1206` — per-account gated telemetry (verify still removed post-freeze fix `29f1858`; do NOT extend to new accounts)
- `.planning/research/STACK.md` — Better-Auth + argon2id pinning

### Testing
- `apps/client/src/__test__/cli-08.e2e.test.ts` (and any sibling Playwright specs) — two-client smoke that needs to switch from dev-bypass to real `dunsen_uat`/`rebbie_uat` login
- `apps/client/src/__test__/esc-menu.test.ts` — update left-click → right-click assertions
- `apps/client/src/__test__/chat-hud.test.ts` — update for new positioning if test inspects rect
- `playwright.config.ts` / Playwright globalSetup — add UAT credentials injection from env

### Requirements Traceability
- `traceable-reqs.toml` — likely tags: `REQ-CLI-01` (Phaser config), `REQ-CLI-02`/`REQ-CLI-03` (EscMenu), `REQ-CLI-05` (chat HUD), `REQ-CLI-09` (deploy/CI account seed). Researcher to confirm and add new REQ rows if seed-script lacks a home.

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- **`apps/server/scripts/run-migrations.ts`** — idempotent migration chain; D-01 migration slots in here.
- **`apps/server/scripts/migrate-legacy-accounts.ts`** — argon2id + Better-Auth row-write pattern; D-02 hashing reuses this.
- **`apps/client/src/main.ts:49-51`** — already has `window.addEventListener('resize')` wired to `game.scale.refresh()`; D-06's manual `setZoom` calculation hooks the same listener.
- **`apps/client/src/ui/EscMenu.ts:155-167`** — `open/close` state already supports double-call no-op; D-14 right-click handler reuses unchanged.
- **`apps/client/src/scenes/GameScene.ts:297-306`** — full set of D-34 suppression guards already present; D-14 keeps them, adds button-check above.

### Established Patterns
- **HARD mount-point invariant** for DOM overlays (`#dom-overlay`, position:fixed, NOT inside Phaser DOM container). Both EscMenu and ChatHUD obey it; D-10/D-11 must NOT violate.
- **Threat model on chat input** (`textContent` only, never innerHTML — `ChatHUD.ts:15`). Repositioning DOM nodes does not affect this; preserve.
- **dev-bypass session-token short-circuit** for integration tests (`RebnoRoom.ts:433`). New real-account path uses the standard Better-Auth flow — does NOT touch this code path.
- **Atomic per-plan commits** with REQ-tag references (CLAUDE.md §Conventions).

### Integration Points
- Phaser ↔ DOM overlay coupling: Phaser `game.canvas` element rect is the source of truth for D-10 chat clamp + (potentially) any other canvas-bound HUD.
- Better-Auth ↔ Colyseus join: real login produces a real session token; Colyseus `onJoin` already accepts both real tokens and `dev-bypass:` synthetics (no server change required for D-01..D-04).
- Playwright ↔ env: credentials injected via `process.env` in CI; never committed.

</code_context>

<specifics>
## Specific Ideas

- "operator-driven tests" = manual UAT sessions that use `uat_a` / `uat_b` dev-bypass tokens. The two new accounts (`dunsen_uat`, `rebbie_uat`) exist to let **automated Playwright** run concurrently against the same staging server without account-id collision with an operator who happens to be logged in at the same time.
- "auto-pick largest that fits within the existing window dimensions" — D-06's `Math.floor(min(winW/640, winH/480))` is exactly this; phrasing locks the requirement.
- "exactly the game viewport" for chat clamp — interpret as the rendered canvas rect (post-integer-scale), NOT the 640×480 base resolution and NOT the full window. D-10 reads `getBoundingClientRect()` which yields the post-zoom rect.
- "retain ESC activation" — explicit; D-14/D-15 leave the keydown handler at `GameScene.ts:349-355` untouched.

</specifics>

<deferred>
## Deferred Ideas

- **Settings panel** in EscMenu (currently disabled placeholder per `EscMenu.ts:97-118`) — already deferred to Phase 7.
- **Click-to-walk / pathfinding** on left-click — reserved by D-16; belongs in Phase 7 (likely PAR-* row).
- **Touch / mobile input** — out of scope (Chrome desktop only per project charter).
- **Per-account UAT pool growth** (more than 2 Playwright accounts for parallel-shard tests) — defer until Playwright actually shards.
- **Smoke flake** (`waitForGameReady` 15s timeout, 10/39 fails) — pre-existing CI timing issue tracked separately under Gen-9 intentions; if D-01 real-login adds latency, may surface here — researcher should call out.

</deferred>

---

*Phase: 06.6-uat-accounts-integer-viewport-scaling-chat-region-clamp-right-click-menu*
*Context gathered: 2026-05-16*
</content>
</invoke>