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
| # | Finding | Layer | Severity | Batch |
|---|---|---|---|---|
| 1 | Admin dashboard NO auth at /admin | Server | crit | 1 |
| 2 | /record-ai-game trusts client results | Server | crit | 1 |
| 3 | Dice modulo bias on rated play | Server | crit | 1 |
| 4 | GameRoom setTimeout (not DO alarms) — hung games on eviction | Server | high | 5 |
| 5 | hibernate: false — wasting CF billing | Server | high | 5 |
| 6 | timingSafeEqual leaks token length | Server | high | 1 |
| 7 | Player token in WebSocket query string | Server | high | 5 |
| 8 | RoomRegistry in-memory only (lost on eviction) | Server | high | 5 |
| 9 | No persistent navigation | UX | high | 3 |
| 10 | Profile accessible 5 ways, inconsistent labels | UX | high | 3 |
| 11 | Leaderboard buried in Lobby sub-mode | UX | high | 3 |
| 12 | 100% inline styles, 0 Tailwind in JSX | Frontend | high | 2 |
| 13 | shadcn installed, completely unused | Frontend | high | 2 |
| 14 | 3 duplicate glassmorphism functions | Frontend | med | 4 |
| 15 | 3 duplicate Kicker components | Frontend | med | 4 |
| 16 | 4 duplicate buttonStyle variants | Frontend | med | 4 |
| 17 | JetBrains Mono referenced but never imported | Frontend | med | 2 |
| 18 | CSS tokens orphaned (--rc-*, --primary etc.) | Frontend | med | 2 |
| 19 | GameOverOverlay / AuthModal: no dialog role, no focus trap | A11y | med | 4 |
| 20 | Form labels not associated with inputs | A11y | med | 4 |
| 21 | 7+ duplicated server functions across 4 files | Server | med | 1 |
| 22 | Chat messages not sanitized for XSS | Server | med | 1 |
| 23 | 8+ "Back Home" buttons across Lobby | UX | med | 3 |
| 24 | Quick Match 2x duplicate entry points | UX | med | 3 |
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
| Risk | Impact | Mitigation |
|---|---|---|
| shadcn radix-nova tokens are light-theme defaults — npx shadcn add will generate bright white components | high | Fix dark-mode tokens in Batch 2 BEFORE any shadcn component generation |
| Bolt converts inline styles to Tailwind mid-task if Tailwind mentioned | med | Keep style migration and component work in separate prompts. Never mention Tailwind in nav/cleanup prompts |
| Phipps invents new color values not in the palette | med | Every prompt includes exact hex/rgba values from theme.js |
| App.jsx extraction breaks subtle state dependencies | med | Deferred to future session. Not in this 2-3hr window. |
| BoardSVG 46-prop memo boundary bypass | low | Deferred. Game rendering untouched in this plan. |
| DO hibernation changes WebSocket lifecycle | med | Bessemer 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