- Stack room code and share link vertically on left
- Place square QR button on right, spanning both rows
- Show mini QR code (40px) in button instead of emoji
- Fix popover z-index to appear above dropdown menu (z: 10000)
- Reduce button padding for more compact appearance
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated the game theme system and game cards to have vibrant, eye-catching
gradients and consistent heights:
Theme System Changes:
- Updated all game themes from pastel to vibrant gradients
- Blue: Vibrant cyan (#4facfe to #00f2fe)
- Purple: Vibrant purple (#667eea to #764ba2)
- Pink: Vibrant pink (#f093fb to #f5576c)
- Green: Vibrant green/teal (#43e97b to #38f9d7)
- Plus updated indigo, teal, orange, yellow, red, gray themes
Game Manifest Updates:
- Memory Lightning: now uses purple theme
- Matching Pairs: now uses pink theme
- Complement Race: continues using blue (cyan) theme
- Card Sorting: now uses green theme
Homepage Game Cards:
- Added height: '100%' and flexbox to make all cards equal height
- Cards now stretch uniformly regardless of content length
- Maintains responsive hover effects and text readability overlays
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Change <p> tag to <div> tag to fix HTML nesting violation. The
<p> tag was containing <DecompositionWithReasons> which renders
a <div>, causing a hydration error. In HTML, <p> cannot contain
block-level elements like <div>.
Fixed: apps/web/src/components/tutorial/TutorialPlayer.tsx:1386
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Create a centralized theme system to prevent inconsistent game card
styling. All games now use standard color presets instead of manually
specifying gradients.
## Changes
**New Theme System:**
- Created `/src/lib/arcade/game-themes.ts` with 10 standard themes
- All themes use Tailwind 100-200 colors for consistent pastel appearance
- Exported `getGameTheme()` helper and `GAME_THEMES` constants via SDK
- Added comprehensive documentation in `.claude/GAME_THEMES.md`
**Migrated All Games:**
- card-sorting: Uses `getGameTheme('teal')`
- memory-quiz: Uses `getGameTheme('blue')`
- matching: Uses `getGameTheme('purple')`
- complement-race: Uses `getGameTheme('blue')`
**Benefits:**
- ✅ Prevents future styling inconsistencies
- ✅ One-line theme setup instead of three properties
- ✅ TypeScript autocomplete for available themes
- ✅ Centralized maintenance - update all games by changing theme definition
- ✅ Clear documentation prevents mistakes
**Before:**
```typescript
color: 'teal',
gradient: 'linear-gradient(135deg, #99f6e4, #5eead4)', // Manual, error-prone
borderColor: 'teal.200',
```
**After:**
```typescript
...getGameTheme('teal') // Simple, consistent, discoverable
```
This fixes the root cause where card-sorting needed manual gradient
adjustments - now all games automatically get professional styling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace outdated "flashcard generator" landing page with comprehensive
platform showcase highlighting all three pillars: arcade games,
interactive learning, and flashcard creation.
**New Home Page Structure:**
- Compact hero with 3 CTAs: Play Games, Learn, Create
- 4 arcade game cards with player counts and mode tags
- Two-column feature sections for Learning & Flashcards
- Multiplayer features grid (4 cards)
- Stats banner: 4 games, 8 max players, 3 learning modes, 4+ formats
**Visual Design:**
- Smaller, denser components to fit more content
- Information-rich showcase vs marketing fluff
- Purple gradient hero matching guide branding
- Responsive grid layouts for all screen sizes
**Result:**
Home page now accurately represents the full platform:
multiplayer arcade games + interactive tutorials + flashcard tools.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Resolves the state structure incompatibility between single-player and
multiplayer implementations by creating a compatibility transformation layer.
## Problem
The existing beautiful UI components (train animations, railroad tracks,
passenger mechanics) were built for single-player state structure, but the
new multiplayer system uses a different state shape (per-player data, nested
config, different gamePhase values).
## Solution: State Adapter Pattern
Created a transformation layer in Provider that:
- Maps multiplayer state to look like single-player state
- Extracts local player data from `players[localPlayerId]`
- Transforms `currentQuestions[playerId]` → `currentQuestion`
- Maps gamePhase enum values (`setup`/`lobby` → `controls`)
- Separates local UI state (currentInput, isPaused) from server state
- Provides compatibility dispatch mapping old actions to new action creators
## Key Changes
- Added `CompatibleGameState` interface matching old single-player shape
- Implemented state transformation in `compatibleState` useMemo hook
- Enhanced dispatch compatibility for local UI state management
- Updated all component imports to use new Provider
- Preserved ALL existing UI components without modification
## Verification
- ✅ TypeScript: Zero errors in new code
- ✅ Format: Biome formatting passes
- ✅ Lint: No new warnings
- ✅ All existing UI components preserved
See `.claude/COMPLEMENT_RACE_STATE_ADAPTER.md` for technical documentation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements "Option A: Single Source of Truth" from type audit recommendations.
**Phase 1: Consolidate GameValidator**
- Remove redundant GameValidator re-declaration from SDK types
- SDK now properly re-exports GameValidator from validation types
- Eliminates confusion about which validator interface to use
**Phase 2: Eliminate Move Type Duplication**
- Remove duplicate game-specific move interfaces from validation/types.ts
- Add re-exports of game move types from their source modules
- Maintains single source of truth (game types) while providing convenient access
**Changes:**
- `src/lib/arcade/game-sdk/types.ts`: Import & re-export GameValidator instead of re-declaring
- `src/lib/arcade/validation/types.ts`: Replace duplicate move interfaces with re-exports
- `__tests__/room-realtime-updates.e2e.test.ts`: Fix socket-server import path
**Impact:**
- Zero new type errors introduced
- All existing functionality preserved
- Clear ownership: game types are source of truth
- Improved maintainability: changes in one place
**Verification:**
- TypeScript compilation: ✅ No new errors
- Server build: ✅ Successful
- All pre-existing errors unchanged (AbacusReact module resolution, etc.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Completes the Matching Pairs Battle migration from legacy dual-location
architecture to the unified modular game SDK system.
## Summary of Changes
### Phase 1-4: Core Infrastructure
- Created modular game definition with `defineGame()` in `src/arcade-games/matching/index.ts`
- Registered game in arcade registry with proper type inference
- Consolidated types into SDK-compatible `MatchingConfig`, `MatchingState`, and `MatchingMove`
- Migrated and updated validator with new import paths
### Phase 5-6: Provider and Components
- Created unified `MatchingProvider` with proper context and `useMatching` hook
- Moved all 7 components from arcade location to `src/arcade-games/matching/components/`
- Updated all component imports to use absolute paths (@/) where applicable
- Fixed styled-system import paths for new directory structure
### Phase 7-8: Utilities and Cleanup
- Migrated utility functions (cardGeneration, matchValidation, gameScoring)
- **Deleted 32 legacy files** from `/src/app/arcade/matching/` and `/src/app/games/matching/`
- Updated room page to use registry pattern exclusively
- Fixed all import references across the codebase
## Breaking Changes
- Old routes `/arcade/matching` and `/games/matching` no longer exist
- Game now accessed exclusively through arcade room system at `/arcade/room`
- Legacy providers and contexts removed
## Migration Verification
- All TypeScript errors in new code resolved
- Only remaining errors are pre-existing (@soroban/abacus-react, complement-race)
- Components successfully moved and imports updated
- Game registry integration working correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Issue: game-config-helpers.ts was importing game-registry.ts which loads
game definitions including React components. This caused server startup to
fail with MODULE_NOT_FOUND for GameModeContext.
Solution: Lazy-load game registry only in browser environment.
On server, getGame() returns undefined and validation falls back to
switch statement for legacy games.
Changes:
- game-config-helpers.ts: Add conditional getGame() that checks typeof window
- Only requires game-registry in browser environment
- Server uses switch statement fallback for validation
- Browser uses game.validateConfig() when available
This maintains the architectural improvement (games own validation)
while keeping server-side code working.
Test: Dev server starts successfully, no MODULE_NOT_FOUND errors
BREAKING CHANGE: Database schemas now accept any string for game names
This implements Critical Fix#1 from AUDIT_2_ARCHITECTURE_QUALITY.md
Changes:
- Remove hardcoded enums from all database schemas
- arcade-rooms.ts: gameName now accepts any string
- arcade-sessions.ts: currentGame now accepts any string
- room-game-configs.ts: gameName now accepts any string
Runtime Validation:
- Add isValidGameName() helper to validate against registry
- Add assertValidGameName() helper for fail-fast validation
- Update settings API to use runtime validation instead of hardcoded array
Benefits:
✅ No schema migration needed when adding new games
✅ No TypeScript compilation errors for new games
✅ Single source of truth: validator registry
✅ "Just register and go" - no database changes required
Migration Impact:
- Existing data is compatible (strings remain strings)
- No data migration needed
- TypeScript will now allow any string, but runtime validation enforces correctness
This eliminates the most critical architectural issue identified in the audit.
Future games can be added by:
1. Register validator in validators.ts
2. Register game in game-registry.ts
That's it! No database schema changes needed.
Change weighted selection from 70/20/10 to 50/25/25:
- Emoji-specific: 70% → 50%
- Category-specific: 20% → 25%
- Global abacus: 10% → 25%
This increases abacus-themed words by 2.5x, ensuring stronger
presence of core abacus vocabulary (Calculator, Abacist, Counter)
while still maintaining emoji personality theming.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add comprehensive emoji-themed player name generation system:
- 150+ emoji-specific word lists (10 adjectives + 10 nouns each)
- 45+ category-themed word lists as fallback
- Generic abacus-themed words as ultimate fallback
Probabilistic tier selection for variety:
- 70% emoji-specific (e.g., "Grinning Grinner" for 😀)
- 20% category-specific (e.g., "Cheerful Optimist" for happy faces)
- 10% global abacus theme (e.g., "Lightning Calculator")
Cross-tier mixing for abacus flavor infusion:
- 60% pure themed words
- 30% themed adjective + abacus noun
- 10% abacus adjective + themed noun
Updated all name generation call sites to pass player emoji:
- PlayerConfigDialog.tsx: Pass emoji when generating name
- GameModeContext.tsx: Theme names for default players and new players
This creates ultra-personalized, variety-rich names while maintaining
abacus theme presence across all generated names.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add defensive state corruption checks to RoomMemoryPairsProvider
- Update test fixtures to include userId field in GameMove objects
- Add git restore to allowed bash commands in local settings
These changes improve robustness when game state becomes corrupted
(e.g., from game type mismatches between room sessions).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The GAMES_CONFIG was changed from 'memory-lightning' to 'memory-quiz'
but the GAME_TYPE_TO_NAME mapping in room/page.tsx still used the old key.
This caused the handleGameSelect function to fail silently when users
clicked on Memory Lightning in the "Change Game" screen, as it couldn't
find the mapping for 'memory-quiz'.
Also added debug logging to GameCard component to help diagnose issues.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Root Cause:
- GAMES_CONFIG used 'memory-lightning' as key but validator was registered as 'memory-quiz'
- When rooms were created with gameName 'memory-lightning', getValidator() couldn't find the validator
- This caused all move validations to fail, breaking configuration changes and guess validation
Key Changes:
1. Fixed game identifier mismatch:
- Changed GAMES_CONFIG key from 'memory-lightning' to 'memory-quiz'
- Updated games/page.tsx to use 'memory-quiz' for routing
2. Completed Memory Quiz room-based multiplayer implementation:
- Added MemoryQuizGameValidator with all 9 move types (START_QUIZ, NEXT_CARD, SHOW_INPUT_PHASE, ACCEPT_NUMBER, REJECT_NUMBER, SET_INPUT, SHOW_RESULTS, RESET_QUIZ, SET_CONFIG)
- Created RoomMemoryQuizProvider for network-synchronized gameplay
- Implemented optimistic client-side updates with server validation
- Added proper serialization handling (send numbers instead of React components)
- Split memory-quiz/page.tsx into modular components (SetupPhase, DisplayPhase, InputPhase, ResultsPhase)
3. Updated socket-server:
- Fixed to use getValidator() instead of hardcoded matchingGameValidator
- Added game-specific initial state handling for both 'matching' and 'memory-quiz'
4. Fixed test failures from arcade_sessions schema changes:
- Updated arcade-session-validation.e2e.test.ts to create rooms before sessions (roomId is now primary key)
- Added missing playerMetadata and playerHovers fields to arcade-session-integration.test.ts
- Skipped obsolete test in orphaned-session-cleanup.test.ts (roomId can't be null as it's the primary key)
5. Code quality fixes:
- Removed unused type imports from room-moderation.ts
- Changed to optional chain in MemoryQuizGameValidator.ts
- Removed unnecessary fragment in MemoryQuizGame.tsx
Testing:
- All modified tests updated to match new schema requirements
- TypeScript errors resolved (excluding pre-existing @soroban/abacus-react issues)
- Lint passes with 0 errors and 0 warnings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add success/error message UI component to ModerationPanel
- Replace all browser alert() calls with inline React-based feedback
- Add displayPassword field to arcade_rooms schema for plain text storage
- Create migration to add display_password column
- Update settings PATCH route to store both hashed and display passwords
- Update room GET route to return displayPassword only to room creator
- Update ModerationPanel to populate password field when loading settings
- Fix room-manager test to include displayPassword field
Password field now persists and displays correctly when reloading the page
for room owners in password-protected rooms.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
When users click share links for approval-only rooms, they now:
- See a prompt to request approval from the room moderator
- Can send a join request with one click
- Get a waiting screen showing their request is pending
Room moderators now see:
- A prominent blue badge showing pending join request count
- Combined count of join requests + reports in the badge
- Badge turns blue when join requests exist (vs red for reports only)
- Detailed tooltip showing breakdown of pending items
- Real-time polling (30s intervals) for new join requests
Also includes improvements to room display names using emoji prefixes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add comprehensive access control system for arcade rooms with 6 modes:
- open: Anyone can join (default)
- locked: Only current members allowed
- retired: Room no longer functions
- password: Requires password to join
- restricted: Only users with pending invitations can join
- approval-only: Requires host approval via join request system
Database Changes:
- Add accessMode field to arcade_rooms (replaces isLocked boolean with enum)
- Add password field to arcade_rooms (hashed with bcrypt)
- Create room_join_requests table for approval-only mode
New API Endpoints:
- PATCH /api/arcade/rooms/:roomId/settings - Update room access mode and password (host only)
- POST /api/arcade/rooms/:roomId/transfer-ownership - Transfer ownership to another member (host only)
- POST /api/arcade/rooms/:roomId/join-request - Request to join approval-only room
- GET /api/arcade/rooms/:roomId/join-requests - Get pending join requests (host only)
- POST /api/arcade/rooms/:roomId/join-requests/:requestId/approve - Approve join request (host only)
- POST /api/arcade/rooms/:roomId/join-requests/:requestId/deny - Deny join request (host only)
Updated Endpoints:
- POST /api/arcade/rooms/:roomId/join - Now validates access modes before allowing join:
* locked: Rejects all joins
* retired: Rejects all joins (410 Gone)
* password: Requires password validation
* restricted: Requires valid pending invitation
* approval-only: Requires approved join request
* open: Allows anyone (existing behavior)
Libraries:
- Add room-join-requests.ts for managing join request lifecycle
- Ownership transfer updates room.createdBy and member.isCreator flags
- Socket.io events for join request notifications and ownership transfers
Migration: 0007_access_modes.sql
Next Steps (UI not included in this commit):
- RoomSettingsModal for configuring access mode and password
- Join request approval UI in ModerationPanel
- Ownership transfer UI in ModerationPanel
- Password input in join flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update local settings for development workflow.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The previous fix only cleared hover on turn switch, but the actual turn
transition happens during CLEAR_MISMATCH (when cards flip back). This
was causing stale hover avatars to persist at the start of new turns.
Changes:
- **MatchingGameValidator.ts**: Clear non-current players' hovers in
validateClearMismatch to ensure clean state when cards are cleared
- **RoomMemoryPairsProvider.tsx**: Mirror the server logic in optimistic
CLEAR_MISMATCH handling
- **LocalMemoryPairsProvider.tsx**: Same fix for local-only games
Now when CLEAR_MISMATCH fires (after 1.5s mismatch timeout), we clear
hover state for all players except the current player, ensuring:
- No stale hovers from previous turns
- Only active player's current hover shows
- Clean transition between turns
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Problem:**
- player-ownership.ts imported drizzle-orm and @/db at top level
- When RoomMemoryPairsProvider imported client-safe utilities, Webpack bundled ALL imports including database code
- This caused hydration error: "The 'original' argument must be of type Function"
- Node.js util.promisify was being called in browser context
**Solution:**
1. Created player-ownership.client.ts with ONLY client-safe utilities
- No database imports
- Safe to import from 'use client' components
- Contains: buildPlayerOwnershipFromRoomData(), buildPlayerMetadata(), helper functions
2. Updated player-ownership.ts to re-export client utilities and add server-only functions
- Re-exports everything from .client.ts
- Adds buildPlayerOwnershipMap() (async, database-backed)
- Safe to import from server components/API routes
3. Updated RoomMemoryPairsProvider to import from .client.ts
**Result:**
- No more hydration errors on /arcade/room
- Client bundle doesn't include database code
- Server code can still use both client and server utilities
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace arcade session-based provider with pure client-side reducer for
local-only games at /arcade/matching. This eliminates "No active session"
errors when starting non-networked games.
Changes:
- Replace useArcadeSession with useReducer for local state management
- Implement complete game logic client-side (match validation, turn switching)
- Remove dependency on socket/server for local games
- Add MATCH_FOUND, MATCH_FAILED, SWITCH_PLAYER actions to reducer
- Auto-detect matches and switch players using useEffect hooks
Local games now run entirely in the browser without requiring an arcade
session or network connection.
Fixes: Starting game at /arcade/matching no longer shows session errors
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add networked hover presence feature showing player avatars over cards
they're considering. Avatars smoothly glide between cards using react-spring
physics-based animations.
Key features:
- Fixed positioning with getBoundingClientRect for accurate placement
- Component keyed by playerId to persist across card changes
- Spring animations for smooth position transitions
- Only shows remote players' avatars (filters out local player)
- Only sends hover events when it's the player's turn
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Exports MemoryPairsContext from MemoryPairsContext.tsx so that
RoomMemoryPairsProvider can properly provide to it. Also re-exports
useMemoryPairs hook from RoomMemoryPairsProvider for convenience.
This fixes the runtime error:
"useMemoryPairs must be used within a MemoryPairsProvider"
Changes:
- Export MemoryPairsContext in MemoryPairsContext.tsx
- Re-export useMemoryPairs from RoomMemoryPairsProvider.tsx
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace basic HTML title tooltips with Radix UI Tooltip components
that display comprehensive player information:
New PlayerTooltip component features:
- Player name with color accent dot
- Visual badge distinguishing local vs network players
- Player color indicator with glow effect
- "Joined X ago" timestamp (formatted as days/hours/minutes)
- For network players: shows controlling member name
- Glassmorphic dark design with backdrop blur
- Smooth fade-in animation
Updated components:
- ActivePlayersList: Wraps player avatars with PlayerTooltip
- NetworkPlayerIndicator: Shows "Controlled by [member]" in tooltip
- PageWithNav: Passes full player objects (including color, createdAt)
instead of just {id, name, emoji}
Technical improvements:
- Added @radix-ui/react-tooltip dependency
- Removed all basic `title` attributes
- Type-safe player data passing through component tree
- Handles Date | number types for createdAt properly
The new tooltips provide much richer context for understanding
network opponents and collaborators in multiplayer games.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add tests to prevent regression of the enabled flag bug and verify
room navigation behavior with active game sessions.
New tests:
- useArcadeGuard with enabled=false blocks HTTP redirects
- useArcadeGuard with enabled=false blocks WebSocket redirects
- useArcadeGuard with enabled=true still allows redirects
- Room browser renders without redirect when user has active session
- Room navigation works with active sessions
- No redirect loops during rapid navigation
- Users can browse rooms during active gameplay
Fixes in existing tests:
- Updated mock response format to match API (wrapped in { session })
- All 16 useArcadeGuard tests passing
- All 5 room navigation tests passing
This ensures the /arcade-rooms pages remain accessible during
active game sessions, preventing the bug where users were
immediately redirected to /arcade/room.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
When a user is in a room, GameModeContext now merges players from all
room members to create a unified player set for gameplay. This enables
true multiplayer where all participants' active players participate
together in the game.
Key changes:
- Added useRoomData and useViewerId to GameModeContext
- Local players (from DB) are marked with isLocal: true
- Remote players (from other room members) are marked with isLocal: false
- Players map merges local + remote players when in a room
- activePlayers set includes all active players from all room members
- Edit operations (update/remove/setActive) only work on local players
- Socket broadcast when local players change to notify room members
- When not in a room, behavior is unchanged (solo/local multiplayer)
This implements the "players is the union of all active players for all
members of the room" requirement for room-based gameplay.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add socket.io broadcasts to join and leave API routes to notify
all users in a room immediately when someone joins or leaves,
without waiting for socket connection.
Previously:
- API added member to database
- No socket event until user's browser connected
- Other users didn't see new member until page reload
- Players showed up but member didn't (timing race condition)
Now:
- API broadcasts `member-joined` immediately after adding member
- API broadcasts `member-left` immediately after removing member
- All connected users get real-time updates instantly
- Both members list and players list update simultaneously
Changes:
- Export `getSocketIO()` from socket-server.ts for API access
- Join route broadcasts member-joined with updated members/players
- Leave route broadcasts member-left with updated members/players
- Socket broadcasts are non-blocking (won't fail the request)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement hybrid database + application-level enforcement to ensure users
can only be in one room at a time, with graceful auto-leave behavior and
clear error messaging.
## Changes
### Database Layer
- Add unique index on `room_members.user_id` to enforce one room per user
- Migration includes cleanup of any existing duplicate memberships
- Constraint provides safety net if application logic fails
### Application Layer
- Auto-leave logic: when joining a room, automatically remove user from
all other rooms first
- Return `AutoLeaveResult` with metadata about rooms that were left
- Idempotent rejoining: rejoining the same room just updates status
### API Layer
- Join route returns auto-leave information in response
- Catches and handles constraint violations with 409 Conflict
- User-friendly error messages when conflicts occur
### Frontend
- Room list and detail pages handle ROOM_MEMBERSHIP_CONFLICT errors
- Show alerts when user needs to leave current room
- Refresh room list after conflicts to show current state
### Testing
- 7 integration tests for modal room behavior
- Tests cover: first join, auto-leave, rejoining, multi-user scenarios,
constraint enforcement, and metadata accuracy
- Updated existing unit tests for new return signature
## Technical Details
- `addRoomMember()` now returns `{ member, autoLeaveResult? }`
- Auto-leave happens before new room join, preventing race conditions
- Database unique constraint as ultimate safety net
- Socket events remain status-only (joining goes through API)
## Testing
- ✅ All modal room tests pass (7/7)
- ✅ All room API e2e tests pass (12/12)
- ✅ Format and lint checks pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Run Biome formatter on all files to ensure consistent code style:
- Single quotes for JS/TS
- Double quotes for JSX
- 2-space indentation
- 100 character line width
- Semicolons as needed
- ES5 trailing commas
This is the result of running: npx @biomejs/biome format . --write
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>