Skip to content

Conversation

@CodeMaverick-143
Copy link

@CodeMaverick-143 CodeMaverick-143 commented Nov 13, 2025

Bug Fixes

Error Boundary Improvements

  • Fixed handleAsyncError return value issue: The async error handler was not returning the result from wrapped functions, causing callers to receive undefined instead of expected data
  • Enhanced TypeScript typing: Added generic type parameters to preserve return types and improve type safety
  • Improved error handling: Better error propagation while maintaining proper error boundary functionality

Tailwind Configuration Fix

  • Resolved ES module compatibility issue: tailwindcss-animate plugin doesn't support ES module imports
  • Converted to CommonJS format: Renamed tailwind.config.js to tailwind.config.cjs and updated to use require() syntax
  • Eliminated build warnings: Removed problematic tailwind-scrollbar-hide plugin that caused ES module loading warnings
  • Maintained functionality: Kept essential tailwindcss-animate plugin working correctly

Technical Changes

  • Fixed async function wrapper in useErrorBoundary.ts to return awaited results
  • Added generic type constraints for better TypeScript inference
  • Updated Tailwind configuration to use proper CommonJS module system
  • Ensured build process runs without module compatibility warnings

Testing

  • Build passes without errors or warnings
  • TypeScript compilation successful
  • Error boundaries maintain proper functionality
  • Tailwind animations continue to work as expected

Related Issues

Addresses code review feedback regarding:

  • Async function return value handling in error boundaries
  • Module system compatibility issues in Tailwind configuration

Impact

  • Improved reliability: Error boundaries now properly handle async operations without breaking data flow
  • Better developer experience: Enhanced TypeScript support with proper type inference
  • Cleaner builds: Eliminated module compatibility warnings and build issues
  • Maintained functionality: All existing features continue to work as expected

Summary by CodeRabbit

Release Notes

  • New Features
    • Team debate system with team creation, joining, management, and captain controls
    • Live spectator features including polls, Q&A, reactions, and presence tracking
    • Team matchmaking system to automatically pair teams for debates
    • Speaking order management with token-based turn allocation for fair participation
    • Enhanced judgment system with detailed rating feedback and opponent tracking
    • Real-time live transcripts and caption display during debates

CodeMaverick-143 and others added 12 commits October 12, 2025 23:12
…ents

- Updated Go version to 1.24 and added toolchain specification.
- Updated dependencies in go.mod and go.sum, including cloud.google.com/go to v0.116.0.
- Added Redis configuration to the backend config.
- Removed unnecessary log statements and improved code readability across various backend controllers and services.
- Enhanced frontend components by adding new routes for viewing debates and integrating new libraries like fuse.js and zustand.
- Cleaned up unused console logs and improved error handling in frontend components.
- Removed unused routes related to pros and cons in the server setup.
- Simplified the WebSocket client handling by updating user details retrieval to include avatar URL and elo rating.
- Enhanced the transcript controller by cleaning up unused code and ensuring proper error handling.
- Adjusted phase durations in the frontend debate room component for a more streamlined experience.
- Cleaned up unnecessary state variables in the OnlineDebateRoom component for improved performance.
…sues

- Fix handleAsyncError to return the result from async functions
- Improve TypeScript typing with generics for better type safety
- Convert tailwind.config.js to tailwind.config.cjs for CommonJS compatibility
- Use require() for tailwindcss-animate plugin (CommonJS)
- Remove tailwind-scrollbar-hide to eliminate ES module warnings
@coderabbitai
Copy link

coderabbitai bot commented Nov 13, 2025

Caution

Review failed

Failed to post review comments

Walkthrough

This PR integrates Redis for debate event streaming with poll and rate-limiting infrastructure, implements comprehensive team management including creation, joining, matchmaking, and turn-based debates, adds WebSocket-based spectator support for individual and team debates, introduces global error boundaries and specialized error handling, refactors services to remove logging and add opponent debate tracking, and expands the frontend with new pages for team building and team debates alongside spectator viewing capabilities.

Changes

Cohort / File(s) Summary
Backend Configuration & Initialization
backend/.env.example, backend/cmd/server/main.go, backend/config/config.go
Added .env template with Redis/server configs; extended main.go with Redis initialization, WebSocket debate/team spectator routes, and team route setup; added Redis config struct with URL, password, DB fields.
Redis Infrastructure
backend/internal/debate/redis_client.go, backend/internal/debate/stream_consumer.go, backend/internal/debate/poll_store.go, backend/internal/debate/rate_limiter.go, backend/internal/debate/events.go
New Redis client wrapper with initialization and accessors; StreamConsumer for Redis stream-based debate events with consumer group and replay logic; PollStore for vote tracking via Redis sets and counters; RateLimiter with per-spectator question/reaction windows; Event/Payload types for Redis stream serialization.
Team Models & Database
backend/models/team.go, backend/models/user.go, backend/models/debate.go, backend/models/coach.go, backend/models/transcript.go
New Team, TeamMember, TeamDebate, TeamDebateMessage, TeamChatMessage models with custom JSON marshaling for hex-encoded IDs; minor formatting/newline fixes to existing models.
Team Management
backend/controllers/team_controller.go, backend/controllers/team_debate_controller.go, backend/controllers/team_matchmaking.go
Full team CRUD, join/leave, member management with captain-only operations; TeamDebate creation with alternating stances and matchmaking cleanup; Matchmaking endpoints for pool management and status polling.
Team Services
backend/services/team_matchmaking.go, backend/services/team_turn_service.go
In-memory team matchmaking pool with thread-safe entry management and Elo-based matching; TokenBucketService and TeamTurnManager for fair speaking time and turn order.
Debate WebSocket Handlers
backend/cmd/server/ws_debate_handler.go, backend/websocket/team_debate_handler.go, backend/websocket/team_websocket.go
New DebateHub for spectator connections with poll snapshots, presence tracking, vote/question/reaction handling; TeamDebateHub for team-based debates with bi-directional team messaging; TeamRoom/TeamClient for individual debate participants with token/turn management.
Service Refactoring
backend/services/rating_service.go, backend/services/debatevsbot.go, backend/services/coach.go, backend/services/pros_cons.go, backend/services/transcriptservice.go, backend/services/gemini.go
Updated UpdateRatings to return both user and opponent debates; enhanced transcript submission with opponent metadata; refactored AI service calls to use unified generateDefaultModelText; removed logging throughout; added FormatHistory and fallback judgment generation.
Utility & API Updates
backend/db/db.go, backend/utils/populate.go, backend/utils/auth.go, backend/utils/debate.go, backend/utils/user.go, backend/routes/debate.go, backend/routes/team.go, backend/routes/rooms.go, backend/go.mod
Added GetCollection helper; removed logging from populate/auth; updated UpdateRatings signature and debate route response with ratingSummary; added team route setup functions; extended Room/Participant models with owner and avatar fields; bumped Go to 1.24, added redis/go-redis and google.golang.org/genai dependencies.
Controller & Middleware Updates
backend/controllers/auth.go, backend/controllers/profile_controller.go, backend/controllers/leaderboard.go, backend/controllers/debatevsbot_controller.go, backend/controllers/debate_controller.go, backend/controllers/transcript_controller.go, backend/middlewares/auth.go
Unified user response building and stats normalization; added id field to profile; removed logging across multiple controllers; extended SubmitTranscriptsRequest with opponent fields; AuthMiddleware now enriches context with database-backed user data.
Test & Server Utilities
backend/cmd/test_judge/main.go, backend/test_server.go, backend/websocket/matchmaking.go, backend/websocket/handler.go, backend/websocket/websocket.go
New test_judge entry point for evaluation testing; removed logging from test_server; enhanced matchmaking with error messaging and pool status tracking; added spectator mode, avatar/Elo handling, and getUserDetails signature update in websocket; removed debug logs from handlers.
Frontend Configuration
frontend/package.json, frontend/index.html
Added dependencies: react-dnd, react-dnd-html5-backend, react-spring, reconnecting-websocket, fuse.js; updated esbuild; changed HTML title to "DebateAI".
Frontend Pages
frontend/src/Pages/TeamBuilder.tsx, frontend/src/Pages/TeamDebateRoom.tsx, frontend/src/Pages/ViewDebate.tsx, frontend/src/Pages/OnlineDebateRoom.tsx
New TeamBuilder for team CRUD and matchmaking; new TeamDebateRoom for live team debates with phase-driven flow and speech recognition; new ViewDebate for spectator viewing with polls/Q&A/reactions and WebRTC; enhanced OnlineDebateRoom with opponent tracking, rating summaries, and live transcripts.
Frontend Error Handling
frontend/src/components/ErrorBoundary/*, frontend/src/App.tsx
New GlobalErrorBoundary, RouteErrorBoundary, DebateRoomErrorBoundary classes with fallback UI; new useErrorBoundary hook family for error dispatch/handling; App.tsx now wraps routes with error boundaries.
Frontend Components
frontend/src/components/ReactionBar.tsx, frontend/src/components/AnonymousQA.tsx, frontend/src/components/MatchmakingPool.tsx, frontend/src/components/TeamChatSidebar.tsx, frontend/src/components/TeamMatchmaking.tsx, frontend/src/components/JudgementPopup.tsx
New ReactionBar with animated emoji reactions; new AnonymousQA with Fuse.js search; new MatchmakingPool display; new TeamChatSidebar with live messaging; new TeamMatchmaking pool manager; enhanced JudgementPopup with rating summaries and nested score structures.
Frontend State & Hooks
frontend/src/atoms/debateAtoms.ts, frontend/src/hooks/useDebateWS.ts, frontend/src/hooks/useUser.ts, frontend/src/context/authContext.tsx, frontend/src/context/theme-provider.tsx
New debateAtoms with jotai for WebSocket, poll state, questions, reactions, presence; new useDebateWS hook managing ReconnectingWebSocket with poll/question/reaction handlers; useUser refactored for profile-first data extraction; removed logging from auth/theme contexts.
Frontend UI & Utilities
frontend/src/components/Layout.tsx, frontend/src/components/Sidebar.tsx, frontend/src/components/ChatRoom.tsx, frontend/src/components/Chatbox.tsx, frontend/src/components/Game.tsx, frontend/src/components/UserCamera.tsx, frontend/src/components/PlayerCard.tsx, frontend/src/components/SavedTranscripts.tsx, frontend/src/components/RoomBrowser.tsx, frontend/src/Pages/*
Updated Sidebar with Team Debates link; removed React import across files per new JSX runtime; removed debug logging; enhanced Game with typing indicators and speaking status; updated RoomBrowser route to /view-debate; extended ChatRoom with typed Message/FloatingEmoji structures; various console.log/console.error removals.
Frontend Styling
frontend/src/index.css
Added @layer base scope for Tailwind customizations; added pulse and floatUp animations with @keyframes.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Spectator Client
    participant WS as WebSocket<br/>(DebateHub)
    participant Redis as Redis<br/>(Stream)
    participant DB as MongoDB

    Client->>WS: Join (spectatorId)
    WS->>WS: Hash spectatorId<br/>Register in room
    WS->>Redis: StartConsumerGroup()
    WS->>DB: LoadPollSnapshot()
    WS->>Client: Send poll_snapshot
    WS->>Client: Send presence

    Note over Client,Redis: Spectator Actions
    Client->>WS: Vote payload
    WS->>Redis: Vote(pollID, option)<br/>Track voter
    WS->>Redis: PublishEvent()
    WS->>Redis: XAdd to stream
    
    loop Consumer Loop
        Redis-->>WS: XReadGroup()
        WS->>WS: ProcessMessage()
        WS->>Client: BroadcastToDebate()
    end

    Client->>WS: Disconnect
    WS->>WS: Unregister client
    WS->>Client: BroadcastPresence()
Loading
sequenceDiagram
    participant Captain as Team Captain
    participant API as Matchmaking API
    participant Pool as Matchmaking<br/>Pool
    participant DB as MongoDB

    Captain->>API: JoinMatchmaking(teamId)
    API->>DB: Fetch team
    API->>Pool: StartTeamMatchmaking(teamId)
    Pool->>Pool: Store entry
    
    Note over Captain,Pool: Waiting for Match...
    
    Captain->>API: GetMatchmakingStatus(teamId)
    API->>Pool: FindMatchingTeam(teamId)
    Pool->>Pool: Scan pool for<br/>same size & Elo range
    
    alt Match Found
        API->>Captain: matched: true<br/>(opponent team data)
        Captain->>API: CreateTeamDebate()
        API->>DB: Insert TeamDebate
        API->>Pool: RemoveFromMatchmaking()
        API->>Captain: Redirect to debate
    else No Match
        API->>Captain: matched: false
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Areas requiring extra attention:

  • Redis integration & stream consumer logic (backend/internal/debate/stream_consumer.go, backend/internal/debate/poll_store.go, backend/internal/debate/rate_limiter.go) — concurrent message processing, consumer group management, and idempotency guarantees need careful verification
  • UpdateRatings signature change (backend/services/rating_service.go) — now returns two debate records; verify all call sites handle new return value correctly and opponent debate persistence logic
  • Team matchmaking algorithm (backend/services/team_matchmaking.go) — thread-safe pool mutation, Elo range matching (200-point window), and concurrent access patterns
  • SubmitTranscripts expansion (backend/services/transcriptservice.go) — complex multi-participant flow with opponent resolution, transcript persistence, and judgment aggregation
  • WebSocket spectator handlers (backend/cmd/server/ws_debate_handler.go, backend/websocket/team_websocket.go) — connection lifecycle, broadcast routing, and state synchronization across multiple rooms
  • Frontend error boundaries — verify error capture, logging, and fallback rendering across all route types
  • OnlineDebateRoom refactor (frontend/src/Pages/OnlineDebateRoom.tsx) — speech recognition retry logic, phase-driven state management, opponent transcript handling, and rating summary computation

Possibly related PRs

Suggested reviewers

  • bhavik-mangla

Poem

🐇 Hop, hop, the teams debate in hubs so bright,
With Redis streams and spectators watching tight,
Error bounds catch falls, matchmaking finds a mate,
Speech flows live, transcripts celebrate—
A warren full of wit and real-time delight! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix Critical Error Boundary Issues and Tailwind Configuration' accurately summarizes the main changes: error boundary fixes and Tailwind configuration corrections.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 57

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
frontend/src/components/RoomBrowser.tsx (1)

21-52: Critical: Empty catch block silently swallows all fetch errors.

The empty catch block at line 48 provides no error handling, user feedback, or recovery mechanism. Combined with the silent failures at lines 31-34 and 36-39, users cannot distinguish between legitimate empty rooms, network failures, or backend errors—they simply see "No rooms available."

Consider one of these approaches:

Option 1: Add user-visible error state

 const RoomBrowser: React.FC = () => {
   const [rooms, setRooms] = useState<Room[]>([]);
   const [loading, setLoading] = useState(true);
+  const [error, setError] = useState<string | null>(null);
   const navigate = useNavigate();
 
   const fetchRooms = async () => {
     const token = localStorage.getItem('token');
     try {
       const response = await fetch('http://localhost:1313/rooms', {
         // ...
       });
       if (!response.ok) {
+        setError('Failed to fetch rooms. Please try again.');
         setLoading(false);
         return;
       }
       const data = await response.json();
       if (!data || !Array.isArray(data)) {
+        setError('Invalid room data received.');
         setRooms([]);
         return;
       }
       // ...
+      setError(null);
       setRooms(normalizedRooms);
     } catch (error) {
+      setError('Unable to connect to the server.');
     } finally {
       setLoading(false);
     }
   };

Then display the error in the UI:

{loading ? (
  <p>Loading rooms...</p>
) : error ? (
  <p className='text-center text-destructive'>{error}</p>
) : rooms.length === 0 ? (
  <p>No rooms available at the moment.</p>
) : (
  // table
)}

Option 2: Let error boundaries handle it

     } catch (error) {
+      throw error; // Let error boundary catch and display
     } finally {
frontend/src/Pages/Profile.tsx (1)

283-332: Critical: Rollback logic doesn't preserve the previous avatar URL.

The optimistic update on Line 294 mutates dashboard.profile.avatarUrl to the new value. When the API call fails, the rollback attempts to revert using dashboard.profile.avatarUrl (lines 317 and 328), but this already contains the new URL from the optimistic update. This means the UI incorrectly displays the new avatar even when the backend update fails.

Apply this diff to capture the previous value before the optimistic update:

 const handleAvatarSelect = async (avatarUrl: string) => {
   if (!dashboard?.profile) return;
   const token = getAuthToken();
   if (!token) {
     setErrorMessage("Authentication token is missing.");
     return;
   }
   try {
+    // Capture the previous avatar URL before optimistic update
+    const previousAvatarUrl = dashboard.profile.avatarUrl;
+
     // Optimistically update the local state
     setDashboard({
       ...dashboard,
       profile: { ...dashboard.profile, avatarUrl },
     });

     const response = await updateProfile(
       token,
       dashboard.profile.displayName,
       dashboard.profile.bio,
       dashboard.profile.twitter,
       dashboard.profile.instagram,
       dashboard.profile.linkedin,
       avatarUrl
     );

     if (response.success) {
       setSuccessMessage("Avatar updated successfully!");
       setErrorMessage("");
     } else {
       setErrorMessage(response.error?.message || "Failed to update avatar.");
       // Revert state on failure
       setDashboard({
         ...dashboard,
         profile: {
           ...dashboard.profile,
-          avatarUrl: dashboard.profile.avatarUrl,
+          avatarUrl: previousAvatarUrl,
         },
       });
     }
   } catch (err) {
     setErrorMessage("Failed to update avatar.");
-    // Optionally revert state on failure
+    // Revert state on failure
     setDashboard({
       ...dashboard,
       profile: {
         ...dashboard.profile,
-        avatarUrl: dashboard.profile.avatarUrl,
+        avatarUrl: previousAvatarUrl,
       },
     });
   }
 };
frontend/src/Pages/TournamentHub.tsx (1)

1-299: Verify PR scope: changes appear unrelated to stated objectives.

The PR objectives describe fixes to error boundaries (useErrorBoundary.ts) and Tailwind configuration (tailwind.config.cjs), but this file contains unrelated changes:

  • Loading state implementation
  • Skeleton loader UI
  • Quote style changes

These appear to be feature additions or UX improvements rather than the critical error boundary and Tailwind fixes described in the PR summary. Please verify whether:

  1. These changes should be in a separate PR
  2. The PR description needs updating
  3. The intended error boundary/Tailwind config files are missing from this review
frontend/src/Pages/OnlineDebateRoom.tsx (2)

626-653: Replace hardcoded URL with environment variable.

Line 631 uses hardcoded http://localhost:1313 instead of import.meta.env.VITE_BASE_URL.

Apply this diff:

-      const response = await fetch(`http://localhost:1313/rooms`, {
+      const response = await fetch(`${import.meta.env.VITE_BASE_URL}/rooms`, {
         method: "POST",

844-1047: Replace hardcoded WebSocket URL and remove debug logging.

  1. Line 849 uses hardcoded ws://localhost:1313 - derive from environment variable:
    const wsUrl = import.meta.env.VITE_BASE_URL.replace(/^http/, 'ws');
  2. Lines 876, 939 contain console.debug calls that should be removed for production

Apply this diff:

+    const wsUrl = import.meta.env.VITE_BASE_URL.replace(/^http/, 'ws');
     const ws = new WebSocket(
-      `ws://localhost:1313/ws?room=${roomId}&token=${token}`
+      `${wsUrl}/ws?room=${roomId}&token=${token}`
     );
backend/controllers/debatevsbot_controller.go (2)

162-163: Critical: Database error silently ignored.

The error from SaveDebateVsBot is caught but not handled. This can mask database failures and lead to data inconsistency issues where the debate state is not persisted.

Handle the error appropriately:

 if err := db.SaveDebateVsBot(debate); err != nil {
+  log.Printf("Failed to save debate: %v", err)
+  // Consider whether to return an error response to the client
 }

207-208: Critical: Database error silently ignored.

The error from UpdateDebateVsBotOutcome is ignored, which means the debate result might not be persisted. This could lead to incorrect user statistics and rating calculations.

Log or handle the error:

 if err := db.UpdateDebateVsBotOutcome(email, result); err != nil {
+  log.Printf("Failed to update debate outcome: %v", err)
 }
backend/websocket/matchmaking.go (1)

119-126: Clean up the WebSocket client when AddToPool fails.

We upgrade to WebSocket, stash the client in matchmakingRoom.clients, and then call AddToPool. On failure we return immediately via c.String(...), but the connection stays open, the client remains in the map, and no read/write pump ever runs. That leaves a dangling connection/channel and leaks the entry, so the next broadcast can block once the buffered channel fills. Close the socket, drop the client from matchmakingRoom.clients, and shut down client.send (and send the error over the socket before closing) before returning.

Apply this diff:

-	err = matchmakingService.AddToPool(user.ID.Hex(), user.DisplayName, userRating)
-	if err != nil {
-		c.String(http.StatusInternalServerError, "Failed to join matchmaking")
-		return
-	}
+	err = matchmakingService.AddToPool(user.ID.Hex(), user.DisplayName, userRating)
+	if err != nil {
+		errorPayload, _ := json.Marshal(MatchmakingMessage{
+			Type:  "error",
+			Error: "Failed to join matchmaking",
+		})
+		matchmakingRoom.mutex.Lock()
+		delete(matchmakingRoom.clients, client)
+		matchmakingRoom.mutex.Unlock()
+		close(client.send)
+		_ = conn.WriteMessage(websocket.TextMessage, errorPayload)
+		_ = conn.Close()
+		return
+	}
🧹 Nitpick comments (32)
frontend/src/Pages/StrengthenArgument.tsx (1)

134-142: Consider preserving error logging for debugging purposes.

While the user-facing error handling via setError and notify is appropriate, completely removing console logging may hinder debugging and monitoring. Consider adding conditional logging based on environment:

     } catch (err: any) {
+      if (process.env.NODE_ENV === 'development') {
+        console.error('Error fetching weak statement:', err);
+      }
       const errorMessage = err.message.includes("invalid character")
         ? "Server returned invalid data. Please try again or contact support."
         : err.message;
       setError(errorMessage);
       notify("Error", errorMessage, "destructive");
     } finally {

Apply similar changes to the handleSubmit catch block:

     } catch (err: any) {
+      if (process.env.NODE_ENV === 'development') {
+        console.error('Error submitting argument:', err);
+      }
       setError(err.message);
       notify("Error", err.message, "destructive");
     } finally {

This approach maintains clean production logs while preserving valuable debugging information during development.

Also applies to: 178-183

frontend/src/Pages/TournamentHub.tsx (2)

16-46: Consider renaming initialTournaments for clarity.

The constant initialTournaments is no longer used to initialize state (line 46 now uses an empty array). The name is misleading since it's only referenced inside the useEffect to simulate API data. Consider renaming to mockTournamentData or tournamentFixtures to better reflect its purpose.

Apply this diff:

-  const initialTournaments: Tournament[] = [
+  const mockTournamentData: Tournament[] = [
     {
       id: "1",
       name: "Spring Showdown",
       ...
     },
   ];

   const [tournaments, setTournaments] = useState<Tournament[]>([]);
   const [isLoading, setIsLoading] = useState(true);
   ...
   useEffect(() => {
     const timer = setTimeout(() => {
-      setTournaments(initialTournaments);
+      setTournaments(mockTournamentData);
       setIsLoading(false);
     }, 1000);

55-63: Consider extracting the hardcoded timeout value.

The 1000ms delay is a magic number. While acceptable for a simulated API call, extracting it to a named constant would improve maintainability if you need to adjust the delay later.

+  const MOCK_API_DELAY = 1000;
+
   useEffect(() => {
     // Simulate API call
     const timer = setTimeout(() => {
       setTournaments(initialTournaments);
       setIsLoading(false);
-    }, 1000);
+    }, MOCK_API_DELAY);

     return () => clearTimeout(timer);
   }, []);
frontend/src/Pages/MatchLogs.tsx (2)

110-112: Hardcoded player names reduce maintainability.

The isFirstRoundMatch3 flag checks for an exact match string including specific player names. This approach is brittle and won't work for:

  • Different tournaments or matches
  • Dynamic data from a backend
  • Any tiebreaker scenario with different players

If this is production code, consider making the tiebreaker logic data-driven:

+interface MatchLog {
+  match: string;
+  score?: Score;
+  timestamp: string;
+  duration: string;
+  viewers: number;
+  isTiebreaker?: boolean;
+}

Then check the flag instead of hardcoding the match string:

-const isFirstRoundMatch3 = log.match.startsWith(
-  "First Round Match 3: Ayaan Khanna vs Vivaan Sharma"
-);
+const isTiebreaker = log.isTiebreaker || false;

227-231: Logic fix corrects broken tiebreaker display.

The previous condition stage === "First Round Match 3" would never be true since stage is only set to "First Round", "Semifinal", or "Final" (lines 105-109). The new isFirstRoundMatch3 flag correctly displays the tiebreaker note.

However, this fix is limited to the hardcoded match string and won't generalize to other tiebreaker scenarios.

frontend/src/Pages/TournamentBracketPage.tsx (2)

14-32: Mock data implementation needs API integration path.

The simulated API call with hard-coded data is fine for development, but consider:

  • No error handling for when this becomes a real API call
  • External dependency on pravatar.cc may fail in production
  • Missing loading error states

Consider adding error handling now to make the transition to real API smoother:

 useEffect(() => {
-  // Simulate API call
   const timer = setTimeout(() => {
-    const data: Participant[] = [
-      { id: 1, name: "Rishit Tiwari", avatar: `https://i.pravatar.cc/32?u=1` },
-      // ... rest of mock data
-    ];
-    setParticipants(data);
-    setIsLoading(false);
+    try {
+      // TODO: Replace with actual API call
+      const data: Participant[] = [
+        { id: 1, name: "Rishit Tiwari", avatar: `https://i.pravatar.cc/32?u=1` },
+        // ... rest of mock data
+      ];
+      setParticipants(data);
+    } catch (error) {
+      console.error('Failed to load tournament data:', error);
+      // TODO: Set error state
+    } finally {
+      setIsLoading(false);
+    }
   }, 1000);

   return () => clearTimeout(timer);
 }, []);

96-156: Rendering structure is well-implemented.

The semifinals, first round, and match labels are properly structured with:

  • Correct grid layouts for each round
  • Proper participant pairing logic using arithmetic
  • Appropriate winner highlighting
  • Well-positioned bracket connectors

Consider making match labels dynamic if match metadata (e.g., match IDs, times, or scores) becomes available:

  {/* Match Labels */}
  <div className="w-full grid grid-cols-4 gap-2 mt-4">
-   <div className="text-center text-[10px] font-medium text-primary-foreground">Match 1</div>
-   <div className="text-center text-[10px] font-medium text-primary-foreground">Match 2</div>
-   <div className="text-center text-[10px] font-medium text-primary-foreground">Match 3</div>
-   <div className="text-center text-[10px] font-medium text-primary-foreground">Match 4</div>
+   {[1, 2, 3, 4].map((matchNum) => (
+     <div key={matchNum} className="text-center text-[10px] font-medium text-primary-foreground">
+       Match {matchNum}
+     </div>
+   ))}
  </div>
frontend/src/Pages/OnlineDebateRoom.tsx (2)

1174-1382: LGTM: Comprehensive speech recognition error handling.

The retry logic properly handles transient network errors (lines 1291-1322) and distinguishes between recoverable and non-recoverable errors. The live transcript feature (lines 1241-1252) enhances user experience.

Consider extracting the retry logic into a separate utility function to reduce complexity of the onerror handler.


1667-1671: Remove empty useEffect hooks.

Lines 1667-1671 contain useEffect hooks with empty bodies that serve no purpose and should be removed to reduce code clutter.

Apply this diff:

-  // Debug user state changes
-  useEffect(() => {}, [currentUser]);
-
-  useEffect(() => {}, [localUser]);
-
-  useEffect(() => {}, [opponentUser]);
backend/services/gemini.go (1)

21-30: Add context timeout to GenerateContent call.

The generateModelText function uses ctx but the Gemini API call might hang indefinitely if no timeout is set on the context.

Consider adding a timeout if the caller doesn't provide one:

 func generateModelText(ctx context.Context, modelName, prompt string) (string, error) {
   if geminiClient == nil {
     return "", errors.New("gemini client not initialized")
   }
+  // Add timeout if context doesn't have one
+  if _, hasDeadline := ctx.Deadline(); !hasDeadline {
+    var cancel context.CancelFunc
+    ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
+    defer cancel()
+  }
   resp, err := geminiClient.Models.GenerateContent(ctx, modelName, genai.Text(prompt), nil)
frontend/src/components/JudgementPopup.tsx (3)

129-182: Simplify the complex name/avatar derivation logic.

The nested ternary expressions and interleaved logic for deriving player names and avatars are difficult to follow and maintain. Consider extracting this into separate helper functions or using a lookup table approach.

Example refactor:

const getPlayerInfo = (
  isUserBotFormat: boolean,
  localRole: DebateSide | null,
  side: 'for' | 'against'
) => {
  if (isUserBotFormat) {
    return side === 'for' 
      ? { name: userName, avatar: userAvatar || localAvatar }
      : { name: botName || 'Bot', avatar: botAvatar || opponentAvatar };
  }
  
  const isLocalSide = localRole === side;
  return {
    name: isLocalSide ? resolvedLocalName : resolvedOpponentName,
    avatar: isLocalSide ? derivedLocalAvatar : derivedOpponentAvatar
  };
};

const player1Info = getPlayerInfo(isUserBotFormat, localRole, 'for');
const player2Info = getPlayerInfo(isUserBotFormat, localRole, 'against');

Additionally, add error handling for localStorage access:

-  const localAvatar =
-    localStorage.getItem('userAvatar') ||
-    'https://avatar.iran.liara.run/public/40';
+  const localAvatar = (() => {
+    try {
+      return localStorage.getItem('userAvatar') || 'https://avatar.iran.liara.run/public/40';
+    } catch {
+      return 'https://avatar.iran.liara.run/public/40';
+    }
+  })();

198-261: Consider simplifying the getScoreAndReason function.

The function contains two nearly identical switch statements with duplicate case handling. This could be simplified to reduce maintenance burden.

Example refactor using a lookup approach:

const getScoreAndReason = (
  section: string,
  player: 'player1' | 'player2'
) => {
  const data = judgment as any;
  const key = isUserBotFormat 
    ? (player === 'player1' ? 'user' : 'bot')
    : (player === 'player1' ? 'for' : 'against');
  
  // Handle special mapping for ForAgainst format
  const sectionKey = !isUserBotFormat && section === 'cross_examination'
    ? null  // ForAgainst splits this into _questions and _answers
    : section;
  
  if (!sectionKey || !data[sectionKey]) {
    return { score: 0, reason: 'Data not available' };
  }
  
  if (section === 'total') {
    return { score: data.total[key], reason: '' };
  }
  
  return {
    score: data[sectionKey][key].score,
    reason: data[sectionKey][key].reason
  };
};

Note: This refactor requires careful testing to ensure all section mappings remain correct.


1-747: Consider using automated formatting tools for style changes.

The entire file has been converted from double quotes to single quotes. While consistency is valuable, style changes like this are better handled by automated tools (Prettier, ESLint with --fix) rather than manual edits. This keeps the diff focused on functional changes and prevents review fatigue.

For future PRs, consider:

  1. Setting up Prettier/ESLint for automatic formatting
  2. Separating style changes into dedicated formatting commits
  3. Using pre-commit hooks to enforce style consistently
frontend/src/components/TeamChatSidebar.tsx (2)

8-15: Consider renaming the interface to TeamChatMessage for clarity.

The name ChatMessage conflicts with the existing interface in frontend/src/components/Chatbox.tsx (lines 4-10), which has different fields. While they serve different purposes, using distinct names prevents confusion and potential type errors during refactoring.

Apply this diff:

-interface ChatMessage {
+interface TeamChatMessage {
   id: string;
   userId: string;
   email: string;
   displayName: string;
   message: string;
   timestamp: string;
 }

And update the type references:

-  const [messages, setMessages] = useState<ChatMessage[]>([]);
+  const [messages, setMessages] = useState<TeamChatMessage[]>([]);
-        const newChatMessage: ChatMessage = {
+        const newChatMessage: TeamChatMessage = {

157-161: Reliance on external avatar service may cause issues.

Line 159 uses an external avatar service (https://avatar.iran.liara.run/public/${msg.userId}) that could be unavailable, blocked by corporate networks, or fail. While the AvatarFallback provides a fallback, consider:

  1. Using a self-hosted avatar generation library (e.g., @dicebear/core)
  2. Generating avatar URLs from a backend endpoint you control
  3. Using initials-based avatars consistently without external dependencies

Example using DiceBear (if added as dependency):

import { createAvatar } from '@dicebear/core';
import { initials } from '@dicebear/collection';

// In the component
const avatarSvg = createAvatar(initials, {
  seed: msg.userId,
}).toString();

// In the JSX
<AvatarImage src={`data:image/svg+xml;utf8,${encodeURIComponent(avatarSvg)}`} />
backend/services/rating_service.go (1)

132-145: Consider validating fallback parameters to sanitizePlayerStats.

While the function sanitizes player stats against NaN/Inf values, the fallbackRating and fallbackRD parameters themselves could theoretically be NaN or Inf if passed incorrectly. Consider adding validation:

 func sanitizePlayerStats(player *rating.Player, fallbackRating, fallbackRD float64) {
+  // Validate fallback values themselves
+  if math.IsNaN(fallbackRating) || math.IsInf(fallbackRating, 0) {
+    fallbackRating = 1200.0
+  }
+  if math.IsNaN(fallbackRD) || math.IsInf(fallbackRD, 0) {
+    fallbackRD = 350.0
+  }
+
   if math.IsNaN(player.Rating) || math.IsInf(player.Rating, 0) {
     player.Rating = fallbackRating
   }
   // ... rest of function
 }
frontend/src/components/ErrorBoundary/RouteErrorBoundary.tsx (1)

40-54: Consider removing console logging in production when error tracking is implemented.

Line 52 uses console.error to log errors in production. Once the error tracking service is integrated (as indicated by the comment on line 53), this console logging should be removed to avoid duplicate logs and potential sensitive data exposure.

   private logRouteError = (error: Error, errorInfo: ErrorInfo) => {
     const errorData = {
       type: 'route_error',
       route: this.props.routeName || 'unknown',
       message: error.message,
       stack: error.stack,
       componentStack: errorInfo.componentStack,
       timestamp: new Date().toISOString(),
       url: window.location.href,
       pathname: window.location.pathname,
     };
     
-    console.error('Route error logged:', errorData);
-    // In production, send to error tracking service
+    // Send to error tracking service (e.g., Sentry, LogRocket)
+    // errorTrackingService.logError(errorData);
   };
frontend/src/components/ReactionBar.tsx (1)

57-60: Consider removing the localStorage fallback.

The spectatorHash atom already handles localStorage retrieval and fallback generation (lines 33-48 in frontend/src/atoms/debateAtoms.ts). The fallback here duplicates that logic and could lead to inconsistencies if the atom's initialization logic changes.

Apply this diff to simplify:

   const handleReaction = (emoji: string) => {
-    const storedHash =
-      spectatorHash || localStorage.getItem('spectatorHash') || '';
-    if (!debateId || !storedHash) return;
+    if (!debateId || !spectatorHash) return;

     const payload = {
       reaction: emoji,
-      spectatorHash: storedHash,
+      spectatorHash: spectatorHash,
       timestamp: Date.now(),
     };
frontend/src/Pages/TeamBuilder.tsx (1)

291-294: Missing error handling for clipboard API.

The clipboard.writeText call can fail due to permissions or browser compatibility. Consider adding error handling to provide user feedback.

   const handleCopyTeamCode = (code: string) => {
-    navigator.clipboard.writeText(code);
-    setSuccess(`Team code ${code} copied to clipboard!`);
-    setTimeout(() => setSuccess(""), 3000);
+    navigator.clipboard.writeText(code)
+      .then(() => {
+        setSuccess(`Team code ${code} copied to clipboard!`);
+        setTimeout(() => setSuccess(""), 3000);
+      })
+      .catch(() => {
+        setError("Failed to copy to clipboard");
+        setTimeout(() => setError(""), 3000);
+      });
   };
backend/internal/debate/redis_client.go (3)

10-13: Package-level background context limits cancellation control.

The shared context.Background() at line 12 means all Redis operations use the same uncancellable context. This prevents proper request-scoped cancellation and could lead to resource leaks if long-running operations aren't cancelled when requests are abandoned.

Consider these options:

  1. Option 1: Accept context as a parameter in operations:
func GetRedisClient() *redis.Client {
    return rdb
}

// Callers pass their own context to Redis operations
// rdb.Get(ctx, key)
  1. Option 2: Remove GetContext() and document that callers should use request contexts:
-var (
-	rdb *redis.Client
-	ctx = context.Background()
-)
+var rdb *redis.Client

-// GetContext returns the default context
-func GetContext() context.Context {
-	return ctx
-}

The first option is preferred as it allows proper cancellation propagation from HTTP handlers.


34-37: GetRedisClient can return nil without indication.

If InitRedis hasn't been called or failed, GetRedisClient() returns nil without any error indication. This could cause nil pointer panics in calling code.

Consider adding nil check protection:

 // GetRedisClient returns the Redis client instance
 func GetRedisClient() *redis.Client {
+	if rdb == nil {
+		panic("Redis client not initialized. Call InitRedis first.")
+	}
 	return rdb
 }

Or change signature to return an error:

func GetRedisClient() (*redis.Client, error) {
	if rdb == nil {
		return nil, fmt.Errorf("redis client not initialized")
	}
	return rdb, nil
}

15-32: LGTM with suggestion - Consider idempotent initialization.

The Redis initialization logic is correct and includes proper connection testing with the Ping command.

For robustness, consider making InitRedis idempotent:

 func InitRedis(redisURL string, password string, db int) error {
+	if rdb != nil {
+		return nil // Already initialized
+	}
+
 	opt := &redis.Options{

This prevents accidental double-initialization and makes the function safe to call multiple times.

backend/controllers/team_matchmaking.go (1)

32-54: Consider refactoring to avoid duplicate database queries and logic.

The team is fetched from the database and its fullness is checked here (lines 35-51), but services.StartTeamMatchmaking performs the same operations again (fetching the team and checking fullness). This results in duplicate database calls and redundant validation logic.

Consider refactoring the service layer to return a more descriptive error that allows you to distinguish between "team not found," "team not full," and other failures, or pass the already-fetched team to the service to avoid the duplicate query.

Example refactor:

-	// Get team
-	collection := db.GetCollection("teams")
-	var team models.Team
-	err = collection.FindOne(context.Background(), bson.M{"_id": objectID}).Decode(&team)
-	if err != nil {
-		c.JSON(http.StatusNotFound, gin.H{"error": "Team not found"})
-		return
-	}
-
-	// Check if user is captain
-	if team.CaptainID != userID.(primitive.ObjectID) {
-		c.JSON(http.StatusForbidden, gin.H{"error": "Only the captain can join matchmaking"})
-		return
-	}
-
-	// Check if team is full
-	if len(team.Members) < team.MaxSize {
-		c.JSON(http.StatusBadRequest, gin.H{"error": "Team is not full yet"})
-		return
-	}
-
-	// Add to matchmaking
-	err = services.StartTeamMatchmaking(objectID)
+	// Add to matchmaking (validates captain, fullness, and fetches team)
+	team, err := services.StartTeamMatchmaking(objectID, userID.(primitive.ObjectID))
 	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to join matchmaking"})
+		// Handle specific error types
+		if err == services.ErrNotCaptain {
+			c.JSON(http.StatusForbidden, gin.H{"error": "Only the captain can join matchmaking"})
+		} else if err == services.ErrTeamNotFull {
+			c.JSON(http.StatusBadRequest, gin.H{"error": "Team is not full yet"})
+		} else if err == mongo.ErrNoDocuments {
+			c.JSON(http.StatusNotFound, gin.H{"error": "Team not found"})
+		} else {
+			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to join matchmaking"})
+		}
 		return
 	}
frontend/src/components/Matchmaking.tsx (2)

71-118: Consider logging errors in the catch block.

The message handling is well-structured with proper type checking and state updates. However, the catch block at line 116-117 silently swallows all errors, making debugging difficult if message parsing fails or state updates throw errors.

       } catch (error) {
+        console.error('Error processing matchmaking message:', error);
       }

143-176: Good connection state handling with retry logic.

The enhanced connection handling properly checks WebSocket readyState and provides user feedback. The retry logic for CONNECTING state is a good user experience improvement.

One minor consideration: the setTimeout at line 162 isn't cleaned up if the component unmounts during the 1-second delay. This could lead to attempting to send messages on a closed WebSocket.

If you want to make it more robust:

     } else if (readyState === WebSocket.CONNECTING) {
       // Wait a bit and try again
-      setTimeout(() => {
+      const timeoutId = setTimeout(() => {
         if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
           wsRef.current.send(JSON.stringify({ type: 'join_pool' }));
         } else {
           alert(
             'Connection is taking too long. Please check your internet connection and try again.'
           );
         }
       }, 1000);
+      // Store timeoutId if you want to clean it up on unmount
     } else {
frontend/src/Pages/ViewDebate.tsx (1)

262-271: Consider logging autoplay errors for debugging.

The empty catch blocks when calling .play() are likely intentional to handle browser autoplay restrictions, but silently swallowing these errors makes debugging difficult.

     if (video1Ref.current && remoteStream1) {
       video1Ref.current.srcObject = remoteStream1;
-      video1Ref.current.play().catch(() => {});
+      video1Ref.current.play().catch((error) => {
+        console.warn('Autoplay prevented for video 1:', error);
+      });
     }
     if (video2Ref.current && remoteStream2) {
       video2Ref.current.srcObject = remoteStream2;
-      video2Ref.current.play().catch(() => {});
+      video2Ref.current.play().catch((error) => {
+        console.warn('Autoplay prevented for video 2:', error);
+      });
     }
backend/services/team_matchmaking.go (1)

44-58: Consider checking for duplicate entries before adding to the pool.

The function doesn't check if the team is already in the matchmaking pool before adding it. While the map will simply overwrite the existing entry, this resets the timestamp, which could affect matchmaking fairness (longer-waiting teams might lose their priority).

 	teamMatchmakingMutex.Lock()
 	defer teamMatchmakingMutex.Unlock()
 
 	// Initialize pool if it doesn't exist
 	if teamMatchmakingPool == nil {
 		teamMatchmakingPool = make(map[string]*TeamMatchmakingEntry)
 	}
+
+	// Check if team is already in the pool
+	if _, exists := teamMatchmakingPool[teamID.Hex()]; exists {
+		return errors.New("team is already in matchmaking pool")
+	}
 
 	teamMatchmakingPool[teamID.Hex()] = &TeamMatchmakingEntry{
backend/controllers/team_controller.go (2)

67-71: Add type assertion safety checks.

Type assertions without checks (line 83) will panic if the auth middleware doesn't set the expected type. While unlikely, defensive programming suggests using the comma-ok idiom.

-	userID, exists := c.Get("userID")
-	if !exists {
-		c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
-		return
-	}
+	userID, exists := c.Get("userID")
+	if !exists {
+		c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
+		return
+	}
+	userObjectID, ok := userID.(primitive.ObjectID)
+	if !ok {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"})
+		return
+	}

 	// Check if user is captain
-	if team.CaptainID != userID.(primitive.ObjectID) {
+	if team.CaptainID != userObjectID {

This pattern applies throughout the file wherever type assertions are used (lines 83, 146, 199, 207-208, 282-283, 288-289, 298, 316-317, 367-368, 383, 390, 398, 421-422, 463-464, 479, 530-531, 546).

Also applies to: 83-86


383-386: Clarify captain leave/transfer captaincy flow.

The error message directs captains to "Transfer captaincy first," but no TransferCaptaincy endpoint is visible in this controller. This creates a poor UX if the captain wants to leave.

Consider one of these approaches:

  1. Allow captain to leave if they're the only member (and automatically delete the team):
 	if team.CaptainID == userID.(primitive.ObjectID) {
-		c.JSON(http.StatusBadRequest, gin.H{"error": "Captain cannot leave team. Transfer captaincy first."})
-		return
+		if len(team.Members) == 1 {
+			// Captain is the only member, delete the team
+			_, err := collection.DeleteOne(context.Background(), bson.M{"_id": objectID})
+			if err != nil {
+				c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to leave team"})
+				return
+			}
+			c.JSON(http.StatusOK, gin.H{"message": "Successfully left team (team deleted)"})
+			return
+		}
+		c.JSON(http.StatusBadRequest, gin.H{"error": "Captain cannot leave team. Transfer captaincy or delete the team."})
+		return
 	}
  1. Implement a TransferCaptaincy endpoint if not already present.

Do you want me to generate the transfer captaincy implementation?

backend/cmd/server/ws_debate_handler.go (3)

157-162: Silent error suppression may hide connection failures.

Write errors are completely ignored in the broadcast loop. While this prevents one bad connection from blocking others, it means dead connections accumulate and are never cleaned up.

Consider logging errors and/or closing failed connections:

 	// Broadcast to all clients
 	for _, client := range clients {
 		if err := client.WriteJSON(eventData); err != nil {
+			// Log the error
+			// Optionally: close and unregister the failed connection
+			go hub.Unregister(debateID, client.conn)
 		}
 	}

286-288: Empty "join" case handler.

The "join" message type is parsed but has no implementation. If this is intentional (join is implicit via connection), consider adding a comment. If not, implement the handler or remove the case.

 		switch clientMsg.Type {
 		case "join":
+			// Join is implicit via WebSocket connection; no action needed
 		case "vote":

299-358: Error handling is too silent throughout handlers.

All error paths in handleVote (and similarly in handleQuestion and handleReaction) return silently without:

  1. Logging the error for debugging
  2. Sending feedback to the client

This makes it impossible to diagnose why votes/questions/reactions fail from either the client or server side.

Add error logging at minimum:

 func handleVote(client *SpectatorClient, payloadBytes []byte) {
 	var payload debate.VotePayload
 	if err := json.Unmarshal(payloadBytes, &payload); err != nil {
+		log.Printf("Failed to unmarshal vote payload: %v", err)
 		return
 	}
 
 	// Set spectator hash and timestamp
 	payload.SpectatorHash = client.spectatorHash
 	if payload.ClientEventID == "" {
 		payload.ClientEventID = uuid.New().String()
 	}
 	payload.Timestamp = time.Now().Unix()
 
 	// Check rate limit
 	rateLimiter := debate.NewRateLimiter()
 	canVote, err := rateLimiter.CheckVoteRateLimit(client.debateID, payload.PollID, client.spectatorHash)
 	if err != nil || !canVote {
+		if err != nil {
+			log.Printf("Vote rate limit check failed: %v", err)
+		}
 		return
 	}

Consider also sending error responses to the client via WebSocket when operations fail.

Comment on lines +36 to 45
// Connect to Redis if configured
if cfg.Redis.URL != "" {
redisURL := cfg.Redis.URL
if redisURL == "" {
redisURL = "localhost:6379"
}
if err := debate.InitRedis(redisURL, cfg.Redis.Password, cfg.Redis.DB); err != nil {
panic("Failed to initialize Redis: " + err.Error())
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Redundant empty check in Redis URL fallback logic.

Lines 38-41 check if redisURL is empty after already assigning it from cfg.Redis.URL on line 38. If cfg.Redis.URL is empty, this condition (line 37) wouldn't be entered in the first place, making the fallback logic unreachable.

Remove the redundant check:

   // Connect to Redis if configured
   if cfg.Redis.URL != "" {
     redisURL := cfg.Redis.URL
-    if redisURL == "" {
-      redisURL = "localhost:6379"
-    }
     if err := debate.InitRedis(redisURL, cfg.Redis.Password, cfg.Redis.DB); err != nil {
       panic("Failed to initialize Redis: " + err.Error())
     }
   }

If you want a fallback for when cfg.Redis.URL is empty, change the logic:

-  if cfg.Redis.URL != "" {
-    redisURL := cfg.Redis.URL
-    if redisURL == "" {
-      redisURL = "localhost:6379"
-    }
+  redisURL := cfg.Redis.URL
+  if redisURL == "" {
+    redisURL = "localhost:6379"
+  }
+  if redisURL != "" {
     if err := debate.InitRedis(redisURL, cfg.Redis.Password, cfg.Redis.DB); err != nil {
       panic("Failed to initialize Redis: " + err.Error())
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Connect to Redis if configured
if cfg.Redis.URL != "" {
redisURL := cfg.Redis.URL
if redisURL == "" {
redisURL = "localhost:6379"
}
if err := debate.InitRedis(redisURL, cfg.Redis.Password, cfg.Redis.DB); err != nil {
panic("Failed to initialize Redis: " + err.Error())
}
}
// Connect to Redis if configured
if cfg.Redis.URL != "" {
redisURL := cfg.Redis.URL
if err := debate.InitRedis(redisURL, cfg.Redis.Password, cfg.Redis.DB); err != nil {
panic("Failed to initialize Redis: " + err.Error())
}
}
🤖 Prompt for AI Agents
In backend/cmd/server/main.go around lines 36 to 45, the inner empty check for
redisURL is redundant because the outer if only runs when cfg.Redis.URL != "".
Replace the current conditional with logic that sets redisURL := cfg.Redis.URL;
if redisURL == "" set redisURL = "localhost:6379"; then unconditionally call
debate.InitRedis(redisURL, cfg.Redis.Password, cfg.Redis.DB) (or alternatively
keep the outer if and simply remove the inner check and use cfg.Redis.URL
directly) so the fallback behavior is correct and the redundant branch is
removed.

Comment on lines 512 to 530
"message": "Token is valid",
"user": gin.H{
"id": user.ID.Hex(),
"email": user.Email,
"displayName": user.DisplayName,
"nickname": user.Nickname,
"bio": user.Bio,
"rating": user.Rating,
"rd": user.RD,
"volatility": user.Volatility,
"id": user.ID.Hex(),
"email": user.Email,
"displayName": user.DisplayName,
"nickname": user.Nickname,
"bio": user.Bio,
"rating": user.Rating,
"rd": user.RD,
"volatility": user.Volatility,
"lastRatingUpdate": user.LastRatingUpdate.Format(time.RFC3339),
"avatarUrl": user.AvatarURL,
"twitter": user.Twitter,
"instagram": user.Instagram,
"linkedin": user.LinkedIn,
"isVerified": user.IsVerified,
"createdAt": user.CreatedAt.Format(time.RFC3339),
"updatedAt": user.UpdatedAt.Format(time.RFC3339),
"avatarUrl": user.AvatarURL,
"twitter": user.Twitter,
"instagram": user.Instagram,
"linkedin": user.LinkedIn,
"isVerified": user.IsVerified,
"createdAt": user.CreatedAt.Format(time.RFC3339),
"updatedAt": user.UpdatedAt.Format(time.RFC3339),
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sanitize stats before responding from VerifyToken

Line [514] still returns raw rating/rd/volatility values. If any of them are NaN/Inf (the exact failure case that motivated normalizeUserStats), encoding/json rejects the payload and VerifyToken blows up with a 500. The new helpers already fix this elsewhere—please reuse them here so the token-validation path can never emit unsupported floats.

@@
-	// Return user details
-	ctx.JSON(200, gin.H{
-		"message": "Token is valid",
-		"user": gin.H{
-			"id":               user.ID.Hex(),
-			"email":            user.Email,
-			"displayName":      user.DisplayName,
-			"nickname":         user.Nickname,
-			"bio":              user.Bio,
-			"rating":           user.Rating,
-			"rd":               user.RD,
-			"volatility":       user.Volatility,
-			"lastRatingUpdate": user.LastRatingUpdate.Format(time.RFC3339),
-			"avatarUrl":        user.AvatarURL,
-			"twitter":          user.Twitter,
-			"instagram":        user.Instagram,
-			"linkedin":         user.LinkedIn,
-			"isVerified":       user.IsVerified,
-			"createdAt":        user.CreatedAt.Format(time.RFC3339),
-			"updatedAt":        user.UpdatedAt.Format(time.RFC3339),
-		},
-	})
+	if normalizeUserStats(&user)) {
+		if err := persistUserStats(dbCtx, &user); err != nil {
+		}
+	}
+
+	ctx.JSON(200, gin.H{
+		"message": "Token is valid",
+		"user":    buildUserResponse(user),
+	})

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/controllers/auth.go around lines 512 to 530, the response body in
VerifyToken is currently returning raw user stats (rating, rd, volatility) which
can be NaN/Inf and cause encoding/json to fail; call the existing
normalizeUserStats helper before building the gin.H user map and use the
sanitized values (or the normalized user copy) for "rating", "rd", and
"volatility" so the JSON encoder never sees unsupported floats; keep the rest of
the fields the same and ensure you replace the three raw fields with the
normalized equivalents.

Comment on lines +103 to +125
// GetMatchmakingPool returns the current matchmaking pool for debugging
func GetMatchmakingPool(c *gin.Context) {
pool := services.GetMatchmakingPool()

// Convert to a more readable format
var poolInfo []gin.H
for teamID, entry := range pool {
poolInfo = append(poolInfo, gin.H{
"teamId": teamID,
"teamName": entry.Team.Name,
"captainId": entry.Team.CaptainID.Hex(),
"maxSize": entry.MaxSize,
"averageElo": entry.AverageElo,
"membersCount": len(entry.Team.Members),
"timestamp": entry.Timestamp.Format("2006-01-02 15:04:05"),
})
}

c.JSON(http.StatusOK, gin.H{
"poolSize": len(pool),
"teams": poolInfo,
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add authentication or consider the privacy implications of this endpoint.

The GetMatchmakingPool endpoint has no authentication or authorization checks, allowing anyone to view all teams currently in the matchmaking pool, including their Elo ratings, captain IDs, and timestamps. This could:

  • Expose sensitive matchmaking data
  • Enable users to game the system by monitoring the pool
  • Violate user privacy expectations

Consider either:

  1. Adding authentication to restrict access to logged-in users
  2. Rate-limiting this endpoint
  3. Documenting this as a public/debug endpoint if that's the intended design
  4. Removing sensitive information from the response if it's meant to be public
 func GetMatchmakingPool(c *gin.Context) {
+	// Check authentication if this should be a protected endpoint
+	_, exists := c.Get("userID")
+	if !exists {
+		c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
+		return
+	}
+
 	pool := services.GetMatchmakingPool()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// GetMatchmakingPool returns the current matchmaking pool for debugging
func GetMatchmakingPool(c *gin.Context) {
pool := services.GetMatchmakingPool()
// Convert to a more readable format
var poolInfo []gin.H
for teamID, entry := range pool {
poolInfo = append(poolInfo, gin.H{
"teamId": teamID,
"teamName": entry.Team.Name,
"captainId": entry.Team.CaptainID.Hex(),
"maxSize": entry.MaxSize,
"averageElo": entry.AverageElo,
"membersCount": len(entry.Team.Members),
"timestamp": entry.Timestamp.Format("2006-01-02 15:04:05"),
})
}
c.JSON(http.StatusOK, gin.H{
"poolSize": len(pool),
"teams": poolInfo,
})
}
// GetMatchmakingPool returns the current matchmaking pool for debugging
func GetMatchmakingPool(c *gin.Context) {
// Check authentication if this should be a protected endpoint
_, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
return
}
pool := services.GetMatchmakingPool()
// Convert to a more readable format
var poolInfo []gin.H
for teamID, entry := range pool {
poolInfo = append(poolInfo, gin.H{
"teamId": teamID,
"teamName": entry.Team.Name,
"captainId": entry.Team.CaptainID.Hex(),
"maxSize": entry.MaxSize,
"averageElo": entry.AverageElo,
"membersCount": len(entry.Team.Members),
"timestamp": entry.Timestamp.Format("2006-01-02 15:04:05"),
})
}
c.JSON(http.StatusOK, gin.H{
"poolSize": len(pool),
"teams": poolInfo,
})
}
🤖 Prompt for AI Agents
In backend/controllers/team_matchmaking.go around lines 103 to 125, the
GetMatchmakingPool handler currently returns sensitive team data without any
authentication or authorization; update it to require an authenticated user (and
optionally an admin role) before returning the pool: apply the existing auth
middleware or check the request context for a valid user and return 401/403 when
absent/unauthorized; if this endpoint must remain public, remove or redact
sensitive fields (averageElo, captainId, exact timestamps) and/or add
rate-limiting and clear documentation that it is a debug endpoint.

Comment on lines +127 to +148
// GetMatchmakingStatus returns the matchmaking pool status
func GetMatchmakingStatus(c *gin.Context) {
teamID := c.Param("teamId")
objectID, err := primitive.ObjectIDFromHex(teamID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid team ID"})
return
}

// Try to find a match
matchingTeam, err := services.FindMatchingTeam(objectID)
if err != nil {
c.JSON(http.StatusOK, gin.H{"matched": false})
return
}

c.JSON(http.StatusOK, gin.H{
"matched": true,
"team": matchingTeam,
"matchId": matchingTeam.ID.Hex(),
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add authorization to prevent unauthorized status checks.

The GetMatchmakingStatus endpoint allows any user to check the matchmaking status of any team without authorization. This could:

  • Leak information about when matches are found
  • Allow users to monitor other teams' matchmaking activity
  • Enable strategic gaming of the matchmaking system

Consider restricting this endpoint to only the team's captain or members:

 func GetMatchmakingStatus(c *gin.Context) {
 	teamID := c.Param("teamId")
 	objectID, err := primitive.ObjectIDFromHex(teamID)
 	if err != nil {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid team ID"})
 		return
 	}
+
+	// Verify the user is authorized to check this team's status
+	userID, exists := c.Get("userID")
+	if !exists {
+		c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
+		return
+	}
+
+	// Fetch team and verify user is captain or member
+	collection := db.GetCollection("teams")
+	var team models.Team
+	if err := collection.FindOne(context.Background(), bson.M{"_id": objectID}).Decode(&team); err != nil {
+		c.JSON(http.StatusNotFound, gin.H{"error": "Team not found"})
+		return
+	}
+
+	userObjectID := userID.(primitive.ObjectID)
+	isMember := team.CaptainID == userObjectID
+	for _, member := range team.Members {
+		if member.UserID == userObjectID {
+			isMember = true
+			break
+		}
+	}
+	if !isMember {
+		c.JSON(http.StatusForbidden, gin.H{"error": "Not authorized to check this team's status"})
+		return
+	}
 
 	// Try to find a match
 	matchingTeam, err := services.FindMatchingTeam(objectID)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// GetMatchmakingStatus returns the matchmaking pool status
func GetMatchmakingStatus(c *gin.Context) {
teamID := c.Param("teamId")
objectID, err := primitive.ObjectIDFromHex(teamID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid team ID"})
return
}
// Try to find a match
matchingTeam, err := services.FindMatchingTeam(objectID)
if err != nil {
c.JSON(http.StatusOK, gin.H{"matched": false})
return
}
c.JSON(http.StatusOK, gin.H{
"matched": true,
"team": matchingTeam,
"matchId": matchingTeam.ID.Hex(),
})
}
// GetMatchmakingStatus returns the matchmaking pool status
func GetMatchmakingStatus(c *gin.Context) {
teamID := c.Param("teamId")
objectID, err := primitive.ObjectIDFromHex(teamID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid team ID"})
return
}
// Verify the user is authorized to check this team's status
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
// Fetch team and verify user is captain or member
collection := db.GetCollection("teams")
var team models.Team
if err := collection.FindOne(context.Background(), bson.M{"_id": objectID}).Decode(&team); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Team not found"})
return
}
userObjectID := userID.(primitive.ObjectID)
isMember := team.CaptainID == userObjectID
for _, member := range team.Members {
if member.UserID == userObjectID {
isMember = true
break
}
}
if !isMember {
c.JSON(http.StatusForbidden, gin.H{"error": "Not authorized to check this team's status"})
return
}
// Try to find a match
matchingTeam, err := services.FindMatchingTeam(objectID)
if err != nil {
c.JSON(http.StatusOK, gin.H{"matched": false})
return
}
c.JSON(http.StatusOK, gin.H{
"matched": true,
"team": matchingTeam,
"matchId": matchingTeam.ID.Hex(),
})
}

Comment on lines +23 to +28
Role string `json:"role" binding:"required"`
Transcripts map[string]string `json:"transcripts" binding:"required"`
OpponentRole string `json:"opponentRole"`
OpponentID string `json:"opponentId"`
OpponentEmail string `json:"opponentEmail"`
OpponentTranscripts map[string]string `json:"opponentTranscripts"`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore strict role validation to prevent orphaned transcripts

Dropping oneof=for against now lets any string through. Downstream in services.SubmitTranscripts we upsert transcripts under the raw role and later only look for "for"/"against" when deciding if both sides have submitted (see backend/services/transcriptservice.go). A bad value (e.g., "FOR" or "neutral") will never match that lookup, so the debate stays stuck at “Waiting for opponent submission” even after both sides finish. Please keep the validator (or normalize the role before passing it on) so only the expected roles persist.

🤖 Prompt for AI Agents
In backend/controllers/transcript_controller.go around lines 23 to 28, the Role
field lost its strict validation which allows arbitrary strings and leads to
orphaned transcripts; restore the validator by adding the binding tag (e.g.,
`binding:"required,oneof=for against"`) or alternatively normalize incoming role
values to the canonical "for" or "against" before forwarding to services so only
the expected roles are stored and downstream lookups succeed.

Comment on lines +648 to +649
{member.userId === team.captainId && (
<FaCrown className="text-yellow-500" />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent captain ID comparison.

Line 648 compares member.userId === team.captainId directly without the type normalization performed in the isCaptain function (lines 238-249). This could cause false negatives if ID formats are inconsistent.

Apply the isCaptain helper for consistency:

                             <div className="flex items-center gap-1.5">
-                              {member.userId === team.captainId && (
+                              {isCaptain(team) && member.userId === (typeof team.captainId === 'string' ? team.captainId : (team.captainId as any)?.$oid || String(team.captainId)) && (
                                 <FaCrown className="text-yellow-500" />
                               )}

Or better yet, add a captain flag to the member object during mapping for simpler comparisons.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/src/Pages/TeamBuilder.tsx around lines 648-649, the code compares
member.userId === team.captainId directly which is inconsistent with the
normalized comparison in the isCaptain helper (lines 238-249); change the check
to use the isCaptain helper (e.g., isCaptain(member.userId, team.captainId)) so
IDs are normalized before comparison, or alternatively set a member.isCaptain
boolean during the member mapping step and use that flag here for simpler,
consistent checks.

Comment on lines +4 to +8
interface Participant {
id: number;
name: string;
avatar: string;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Type mismatch with backend Participant struct.

The frontend Participant interface doesn't align with the backend struct. The backend uses ID (string), Username, AvatarURL, Elo, and Email, while the frontend expects id (number), name, and avatar. This mismatch will cause integration issues when replacing the mock data with real API calls.

Apply this diff to align the frontend interface with the backend:

 interface Participant {
-  id: number;
-  name: string;
-  avatar: string;
+  id: string;
+  username: string;
+  elo: number;
+  avatarUrl: string;
+  email?: string;
 }

You'll also need to update the mock data and all references to participant.name and participant.avatar throughout the component.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface Participant {
id: number;
name: string;
avatar: string;
}
interface Participant {
id: string;
username: string;
elo: number;
avatarUrl: string;
email?: string;
}
🤖 Prompt for AI Agents
In frontend/src/Pages/TournamentBracketPage.tsx around lines 4 to 8, the
Participant interface doesn't match the backend struct — change the interface to
use ID: string, Username: string, AvatarURL: string, Elo: number, Email: string;
then update the mock data to use those property names/types (IDs as strings, add
Elo and Email) and replace all usages of participant.name and participant.avatar
with participant.Username and participant.AvatarURL respectively (and update any
code assuming numeric id to use string IDs).

Comment on lines +61 to +65
// Bracket logic
const round1Winners = [participants[0], participants[2], participants[4], participants[7]];
const semiFinalWinners = [round1Winners[0], round1Winners[3]];
const champion = semiFinalWinners[1];
const winnerHighlight = "ring-4 ring-yellow-400 shadow-lg transition-all duration-300";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Hard-coded winner logic and missing array bounds validation.

The bracket logic has several serious issues:

  1. Hard-coded winners: Lines 62-64 predetermine winners using fixed array indices, making the tournament results fake and non-functional.
  2. No bounds checking: Accessing participants[7] without validating participants.length >= 8 will cause a runtime crash.
  3. No validation: Missing check that exactly 8 participants exist before proceeding.

Apply this diff to add validation and prepare for dynamic winner selection:

+ // Validate we have exactly 8 participants for single-elimination bracket
+ if (participants.length !== 8) {
+   return (
+     <div className="flex justify-center items-center w-full py-12">
+       <p className="text-destructive">Tournament requires exactly 8 participants. Currently have {participants.length}.</p>
+     </div>
+   );
+ }
+
  // Bracket logic
- const round1Winners = [participants[0], participants[2], participants[4], participants[7]];
- const semiFinalWinners = [round1Winners[0], round1Winners[3]];
- const champion = semiFinalWinners[1];
+ // TODO: Replace with actual match results from API
+ const round1Winners = [
+   participants[0], // Match 1 winner (placeholder)
+   participants[2], // Match 2 winner (placeholder)
+   participants[4], // Match 3 winner (placeholder)
+   participants[6], // Match 4 winner (placeholder - was 7, should be 6 for match 4)
+ ];
+ const semiFinalWinners = [round1Winners[0], round1Winners[2]]; // Placeholder
+ const champion = semiFinalWinners[0]; // Placeholder
  const winnerHighlight = "ring-4 ring-yellow-400 shadow-lg transition-all duration-300";

Note: The original code used participants[7] for the 4th winner, but Match 4 should be participants[6] vs participants[7], so the winner should come from one of those, not automatically be participants[7].

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Bracket logic
const round1Winners = [participants[0], participants[2], participants[4], participants[7]];
const semiFinalWinners = [round1Winners[0], round1Winners[3]];
const champion = semiFinalWinners[1];
const winnerHighlight = "ring-4 ring-yellow-400 shadow-lg transition-all duration-300";
// Validate we have exactly 8 participants for single-elimination bracket
if (participants.length !== 8) {
return (
<div className="flex justify-center items-center w-full py-12">
<p className="text-destructive">Tournament requires exactly 8 participants. Currently have {participants.length}.</p>
</div>
);
}
// Bracket logic
// TODO: Replace with actual match results from API
const round1Winners = [
participants[0], // Match 1 winner (placeholder)
participants[2], // Match 2 winner (placeholder)
participants[4], // Match 3 winner (placeholder)
participants[6], // Match 4 winner (placeholder - was 7, should be 6 for match 4)
];
const semiFinalWinners = [round1Winners[0], round1Winners[2]]; // Placeholder
const champion = semiFinalWinners[0]; // Placeholder
const winnerHighlight = "ring-4 ring-yellow-400 shadow-lg transition-all duration-300";
🤖 Prompt for AI Agents
In frontend/src/Pages/TournamentBracketPage.tsx around lines 61 to 65, the code
uses hard-coded winners and unsafe indexing (e.g., participants[7]) which both
fakes results and can crash; replace this with a runtime validation that
participants is an array of exactly 8 entries (throw or render a friendly
error/placeholder if not), remove the hard-coded winner assignments and instead
compute winners from match results (e.g., derive round1Winners from outcomes of
matches [0 vs1, 2 vs3, 4 vs5, 6 vs7]), ensure you never index out of bounds by
always checking length before accessing indices, and correct the 4th match to
use participants[6] vs participants[7] (pick the winner from those two rather
than auto-selecting participants[7]).

Comment on lines +183 to +224
} else if (data.type === "offer" && data.userId && data.offer) {
const debaterIndex = participantsRef.current.findIndex(
(participant) => participant.id === data.userId
);
let pc: RTCPeerConnection | null = null;
if (debaterIndex === 0) {
pc = pc1;
} else if (debaterIndex === 1) {
pc = pc2;
} else {
pc = pc1;
}

if (pc && data.offer) {
try {
if (pc === pc1) {
pc1UserIdRef.current = data.userId;
pc1ConnectionIdRef.current = data.connectionId || null;
} else if (pc === pc2) {
pc2UserIdRef.current = data.userId;
pc2ConnectionIdRef.current = data.connectionId || null;
}

await pc.setRemoteDescription(
new RTCSessionDescription(data.offer)
);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
// Include connectionId if provided (for spectator connections)
ws.send(
JSON.stringify({
type: "answer",
answer,
targetUserId: data.userId,
userId: data.userId,
connectionId: data.connectionId, // Include connectionId if provided
})
);
} catch (error) {
console.error("Error handling offer", error);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fallback logic may cause connection issues for unexpected debater indices.

At line 193, if the debaterIndex is neither 0 nor 1, the code falls back to using pc1. This could cause:

  • Two debaters being assigned to the same peer connection
  • Overwriting existing connections
  • Incorrect video stream routing

Consider rejecting offers from unexpected participants instead of silently falling back:

           const debaterIndex = participantsRef.current.findIndex(
             (participant) => participant.id === data.userId
           );
           let pc: RTCPeerConnection | null = null;
           if (debaterIndex === 0) {
             pc = pc1;
           } else if (debaterIndex === 1) {
             pc = pc2;
-          } else {
-            pc = pc1;
+          } else {
+            console.error('Received offer from unexpected participant:', data.userId);
+            return;
           }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (data.type === "offer" && data.userId && data.offer) {
const debaterIndex = participantsRef.current.findIndex(
(participant) => participant.id === data.userId
);
let pc: RTCPeerConnection | null = null;
if (debaterIndex === 0) {
pc = pc1;
} else if (debaterIndex === 1) {
pc = pc2;
} else {
pc = pc1;
}
if (pc && data.offer) {
try {
if (pc === pc1) {
pc1UserIdRef.current = data.userId;
pc1ConnectionIdRef.current = data.connectionId || null;
} else if (pc === pc2) {
pc2UserIdRef.current = data.userId;
pc2ConnectionIdRef.current = data.connectionId || null;
}
await pc.setRemoteDescription(
new RTCSessionDescription(data.offer)
);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
// Include connectionId if provided (for spectator connections)
ws.send(
JSON.stringify({
type: "answer",
answer,
targetUserId: data.userId,
userId: data.userId,
connectionId: data.connectionId, // Include connectionId if provided
})
);
} catch (error) {
console.error("Error handling offer", error);
}
}
} else if (data.type === "offer" && data.userId && data.offer) {
const debaterIndex = participantsRef.current.findIndex(
(participant) => participant.id === data.userId
);
let pc: RTCPeerConnection | null = null;
if (debaterIndex === 0) {
pc = pc1;
} else if (debaterIndex === 1) {
pc = pc2;
} else {
console.error('Received offer from unexpected participant:', data.userId);
return;
}
if (pc && data.offer) {
try {
if (pc === pc1) {
pc1UserIdRef.current = data.userId;
pc1ConnectionIdRef.current = data.connectionId || null;
} else if (pc === pc2) {
pc2UserIdRef.current = data.userId;
pc2ConnectionIdRef.current = data.connectionId || null;
}
await pc.setRemoteDescription(
new RTCSessionDescription(data.offer)
);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
// Include connectionId if provided (for spectator connections)
ws.send(
JSON.stringify({
type: "answer",
answer,
targetUserId: data.userId,
userId: data.userId,
connectionId: data.connectionId, // Include connectionId if provided
})
);
} catch (error) {
console.error("Error handling offer", error);
}
}
🤖 Prompt for AI Agents
In frontend/src/Pages/ViewDebate.tsx around lines 183 to 224, the current
fallback assigns pc1 when debaterIndex is neither 0 nor 1 which can cause two
debaters sharing the same RTCPeerConnection and broken streams; change the logic
to reject or ignore offers from unexpected participants: validate debaterIndex
explicitly (allow only 0 or 1), if it is out of range log a warning and send a
WS message back indicating the offer is rejected (or simply return without
assigning a peer), and do not fall back to pc1; this prevents overwriting
existing connections and ensures only known participants are handled, while
still preserving the existing flow for valid indices and sending the answer when
accepted.

Comment on lines +221 to +223
} catch (error) {
console.error("Error handling offer", error);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Error handling for WebRTC operations could mask connection failures.

The error handling for setRemoteDescription/createAnswer (lines 221-223) and addIceCandidate (lines 241-243) only logs errors without any recovery or user feedback. Failed WebRTC operations will leave users staring at blank video placeholders without understanding why.

Consider adding user-facing error feedback or recovery logic:

             } catch (error) {
               console.error("Error handling offer", error);
+              // Consider showing a toast notification or setting an error state
+              // that displays to the user that video connection failed
             }

Similarly for ICE candidate errors:

             } catch (error) {
               console.error("Error adding ICE candidate", error);
+              // ICE candidate failures might self-recover, but repeated failures
+              // could indicate network issues worth surfacing to the user
             }

Also applies to: 241-243

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants