Roll-Cube Master Plan

comprehensive audit • 4 agents • 6 batches • 2–3 hour window

Dashboard
Total Issues
38
Critical
3
High
10
Medium
14
Batches
6
Tests
70
Criticals
Admin dashboard unprotected. AI game results client-reported (rating manipulation). Dice modulo bias.
Frontend Debt
100% inline styles. shadcn installed but unused. 3 parallel color systems. No a11y infrastructure. App.jsx = 1924 lines.
What's Working
Game logic solid. 70 tests passing. Tailwind + shadcn plumbing in place (tokens, CSS classes). Glass panel classes defined. Build clean.
Audit Summary
#FindingLayerSeverityBatch
1Admin dashboard NO auth at /adminServercrit1
2/record-ai-game trusts client resultsServercrit1
3Dice modulo bias on rated playServercrit1
4GameRoom setTimeout (not DO alarms) — hung games on evictionServerhigh5
5hibernate: false — wasting CF billingServerhigh5
6timingSafeEqual leaks token lengthServerhigh1
7Player token in WebSocket query stringServerhigh5
8RoomRegistry in-memory only (lost on eviction)Serverhigh5
9No persistent navigationUXhigh3
10Profile accessible 5 ways, inconsistent labelsUXhigh3
11Leaderboard buried in Lobby sub-modeUXhigh3
12100% inline styles, 0 Tailwind in JSXFrontendhigh2
13shadcn installed, completely unusedFrontendhigh2
143 duplicate glassmorphism functionsFrontendmed4
153 duplicate Kicker componentsFrontendmed4
164 duplicate buttonStyle variantsFrontendmed4
17JetBrains Mono referenced but never importedFrontendmed2
18CSS tokens orphaned (--rc-*, --primary etc.)Frontendmed2
19GameOverOverlay / AuthModal: no dialog role, no focus trapA11ymed4
20Form labels not associated with inputsA11ymed4
217+ duplicated server functions across 4 filesServermed1
22Chat messages not sanitized for XSSServermed1
238+ "Back Home" buttons across LobbyUXmed3
24Quick Match 2x duplicate entry pointsUXmed3
Architecture Decision
The Styling Source-of-Truth Problem
Three parallel color systems exist and none talk to each other: 1. theme.js U object — 18 hardcoded hex values, used in 15+ components via inline styles 2. index.css CSS vars — full shadcn oklch tokens (--primary, --muted) + --rc-* bridge. ALL ORPHANED. 3. ui-kit.js / styles.js — style factory functions returning JS objects Decision: Keep theme.js U for game-specific colors (felt, pips, board chrome). Remap --rc-* CSS vars to match U values. Use .glass-panel CSS classes (already defined, never used) for all UI chrome. Kill styles.js entirely — merge into ui-kit.js. Do NOT adopt shadcn stone base tokens — they’re light-theme defaults that don’t match this app. This foundation MUST land before any component extraction or visual work.
Second opinion confirmed: “If you extract components while they still use inline styles with U.panel and U.border, you’ll bake in the old pattern and have to touch every extracted file again.” Style foundation first, then structure.
Batch 0 — Cleanup
BOLT 5 min zero risk
Delete all .bak files. Delete orphaned backup directories. Zero functional risk — these are Bolt’s own snapshots. Files to delete: • src/App.jsx.growth.bak • src/components/FrontDoor.jsx.bak- • src/components/FrontDoor.jsx.growth.bak • src/components/Lobby.jsx.growth.bak • src/components/OnlineGame.jsx.growth.bak • backups/ directory (entire tree)
Batch 1 — Security + Server Cleanup
PHIPPS 30 min low risk
Security before polish.
These are live vulnerabilities. The admin dashboard is public. Anyone with curl can inflate their rating. Fix before touching CSS. 1a. Gate /admin behind auth — server/index.js lines 100-107 Check for ROLL_CUBE_ADMIN_TOKEN env var in cookie or Authorization header before serving admin HTML. Return 401 with a login form if missing. 1b. Fix /record-ai-game — server/playerRegistry.js lines 1096-1172 Add server-side validation: require a signed game-completion token from the game logic, not just client-reported won/lost. Minimum viable: add a nonce issued at game start, verified at submission. 1c. Fix dice modulo bias — server/gameCore.js lines 193-206 Replace (buf[0] % 6) + 1 with rejection sampling. Only accept values < 4294967292. 1d. Fix timingSafeEqual — server/index.js line 423, server/playerRegistry.js line 82 Pad shorter string to match length before comparing. Or use crypto.timingSafeEqual from CF Workers runtime. 1e. Sanitize chat messages — server/gameRoom.js line 293 Strip HTML tags from chat text server-side before broadcasting. 1f. Extract server/shared.js Move sanitizeName, isAllowedOrigin, normalizeRoomCode, ALLOWED_ORIGINS, MAX_NAME_LENGTH, corsHeaders into server/shared.js. Update imports in index.js, gameRoom.js, matchmakingQueue.js, playerRegistry.js.
Batch 2 — Style Foundation
BOLT 45 min medium risk
Wire the plumbing that’s already there.
The shadcn/Tailwind infrastructure exists. The glass-panel CSS classes are defined. The --rc-* vars are set. Nothing uses them. This batch connects the dots. 2a. Import JetBrains Mono npm install @fontsource-variable/jetbrains-mono Add import to src/index.css: @import "@fontsource-variable/jetbrains-mono"; Update --font-mono in @theme inline to: 'JetBrains Mono Variable', monospace 2b. Fix dark-mode CSS tokens The .dark {} block in index.css has oklch values that don’t match the actual app colors. Remap key tokens to match theme.js U values: --background → oklch from #111 (U.bg) --card → oklch from #1a1a1a (U.panel) --border → oklch from #2a2a2a (U.border) --foreground → oklch from #ccc (U.text) --muted-foreground → oklch from #8a8a8a (U.textDim) --primary → oklch from #4a9aca (U.select) --destructive → oklch from #c44 (U.red) 2c. Merge styles.js into ui-kit.js Move buttonStyle and compactButtonStyle from styles.js into ui-kit.js. Delete styles.js. Update all imports (FrontDoor, Lobby, GameLog — grep for from "../styles.js"). 2d. Consolidate glassmorphism Delete surface() from FrontDoor.jsx (lines 14-24). Delete surfaceStyle() from Lobby.jsx (lines 55-65). Make glassPanel() in ui-kit.js the ONLY glass function. Ensure glassPanel() signature covers all variants: glassPanel(tint, extra) 2e. Add cn() utility Create src/lib/utils.js (needed for any future shadcn component use): import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs) { return twMerge(clsx(inputs)); }
Batch 3 — Navigation + UX
BOLT 40 min medium risk
Add persistent nav. Kill redundant access points.
3a. Create TopNav.jsx New file: src/components/TopNav.jsx Fixed position, 48px height, glass surface background (use glassPanel from ui-kit.js). Left: “ROLL CUBE” in JetBrains Mono 600 weight, 14px, letter-spacing 3px. Center: Home | Play | Leaderboard | Profile — text buttons, active = bottom border in U.cyan. Right: mute toggle (use Lucide Volume2/VolumeX icons). Mobile: convert to 56px bottom tab bar with icons + labels. Prop: hideNav (boolean) — render null during active gameplay. Style with glassPanel + inline styles matching existing codebase pattern. 3b. Wire TopNav in App.jsx Import TopNav (NOT lazy — always visible). Render above the view switch ternary. Pass: view, onNavigate, auth, isMobile, muted, toggleMute, hideNav. hideNav = true when in active local game or active online game (mp.connected + mp.phase === “playing”). Add padding-top 48px on desktop content, 0 on mobile (nav at bottom). 3c. Clean FrontDoor Delete QuickMatchCard component + its render (duplicate entry point). Delete ReturningPlayerCard component + its render (profile now in nav). Delete “Profile benefits” section (~40 lines around line 471). Make MiniStats clickable (add onClick prop, render as button when provided). 3d. Clean Lobby Delete all “Back Home” buttons (8+ instances). Delete AccountCard from every mode except the auth flow. Delete “What this screen is for” and “Best default” InfoCards. Add initialMode prop to Lobby — useState(initialMode || null).
Batch 4 — Component Dedup + A11y
PHIPPS 25 min low risk
Merge duplicates, add a11y basics.
4a. Extract shared Kicker component Create shared Kicker in ui-kit.js (or new src/components/shared/Kicker.jsx). Signature: Kicker({ children, color = “rgba(255,255,255,0.58)” }) Delete local Kicker from FrontDoor.jsx (line 26), Lobby.jsx (line 80). 4b. Collapse GameLog button variants GameLog.jsx defines buttonStyle, compactButtonStyle, singleButtonStyle (lines 94-136). Replace all 3 with the consolidated uiButton from ui-kit.js. Pass size/active/color params instead of calling different factory functions. 4c. Add dialog a11y to GameOverOverlay Add role=“dialog” aria-modal=“true” to overlay container. Add Escape key handler (onKeyDown on the wrapper, call onClose). Move focus into overlay on mount (useEffect + ref.focus()). 4d. Add dialog a11y to AuthModal Same as above: role=“dialog”, aria-modal, Escape key, focus on mount. 4e. Fix form label associations in Lobby FieldLabel uses <label> but InputField has no id. Add id prop to InputField. Add htmlFor matching id to FieldLabel. Apply to ALL form fields in create/join modes. 4f. Add aria-live to error states AuthModal error div (line 174): add role=“alert” StatusBar: add aria-live=“polite”
Batch 5 — Backend Ops
BESSEMER 30 min medium risk
DO hibernation + alarms. Bessemer handles this directly.
This batch requires understanding Cloudflare Durable Object lifecycle. Too nuanced for Bolt/Phipps. 5a. Enable hibernation on GameRoom Change static options = { hibernate: false } to { hibernate: true } in server/gameRoom.js. This requires migrating onConnect/onMessage/onClose to the hibernation API (webSocketMessage, webSocketClose handlers on the DO class). 5b. Replace setTimeout with DO alarms Turn timer: replace setTimeout(90s) with this.ctx.storage.setAlarm(Date.now() + 90000) Disconnect grace: replace setTimeout(15s) with alarm. Implement alarm() method on GameRoom class to handle timer expiry. 5c. Persist RoomRegistry to SQLite RoomRegistry currently uses in-memory Map only. Add this.ctx.storage.sql calls to persist room data. Read from SQL on DO initialization, update on changes. 5d. Move player token out of WebSocket URL Currently pt is passed as a query parameter. Change to: connect with no auth, send auth message after connection established. Update useMultiplayer.js client-side to send token as first message.
Agent Assignments
BOLT
Batch 0 — Cleanup (5m) Batch 2 — Style foundation (45m) Batch 3 — Nav + UX (40m) Total: ~90 min Bolt gets the structural frontend work. Precise file creation, component wiring, import management. All prompts include exact file names, line numbers, and DO NOT lists.
PHIPPS
Batch 1 — Security + server (30m) Batch 4 — Component dedup + a11y (25m) Total: ~55 min Phipps gets the server fixes and mechanical dedup. All prompts include exact hex values and function signatures — Phipps invents colors if you don’t specify.
BESSEMER
Batch 5 — Backend ops (30m) Audit loop — review every push Total: ~30 min + audit Bessemer handles DO lifecycle changes (hibernation, alarms) — too nuanced for the other agents. Also runs the audit loop after every push.
Execution order: Batch 0 (Bolt, 5m) → Batch 1 (Phipps) + Batch 2 (Bolt) in parallel → Batch 3 (Bolt, depends on B2) → Batch 4 (Phipps) + Batch 5 (Bessemer) in parallel Parallel lanes: Server work (Phipps) never blocks frontend work (Bolt). Bessemer’s DO work is independent. Max 2 agents coding simultaneously.
Risk Register
RiskImpactMitigation
shadcn radix-nova tokens are light-theme defaults — npx shadcn add will generate bright white componentshighFix dark-mode tokens in Batch 2 BEFORE any shadcn component generation
Bolt converts inline styles to Tailwind mid-task if Tailwind mentionedmedKeep style migration and component work in separate prompts. Never mention Tailwind in nav/cleanup prompts
Phipps invents new color values not in the palettemedEvery prompt includes exact hex/rgba values from theme.js
App.jsx extraction breaks subtle state dependenciesmedDeferred to future session. Not in this 2-3hr window.
BoardSVG 46-prop memo boundary bypasslowDeferred. Game rendering untouched in this plan.
DO hibernation changes WebSocket lifecyclemedBessemer handles directly. Test with cold-start script.
Prompting Guide
BOLT Rules
• Never say “refactor X.jsx” — say “extract lines N-M into NewComponent.jsx with these props” • Always give exact import statements to add/remove • Never mention Tailwind in a structural prompt (Bolt will start converting everything) • Give the test command: npx playwright test • Bolt needs the exact file path for every file touched • Keep each prompt to ONE batch — never combine batches
PHIPPS Rules
• Always provide exact hex/rgba values — Phipps WILL invent colors • Specify “do not add any new DOM elements” to prevent wrapper div proliferation • Give exact function signatures to produce, not “merge these functions” • Phipps handles server files well — assign security and dedup work • Use MiniMax M2.7’s strength: structured code following a template • If Phipps rate-limits, fall back to Codex Spark for the same prompt
Both agents: • The @ alias in vite.config.js resolves to ./src • src/components/ui/ does NOT exist yet — npx shadcn add creates it • src/lib/utils.js with cn() must exist before any shadcn component import • Worker deploy is separate from Pages deploy — specify which when asking to deploy • Run npx playwright test after every change