Skip to content

Conversation

@rixitgithub
Copy link
Contributor

@rixitgithub rixitgithub commented Nov 9, 2025

Summary by CodeRabbit

  • New Features

    • Full spectator mode and a ViewDebate page with live video streams
    • Audience-facing poll creation and voting, plus live poll panel
    • Anonymous Q&A/comments and live reaction bar
    • Team speaking turn management with token-based speaking limits
  • Improvements

    • More reliable real-time sync and spectator-aware signaling
    • Better transcript handling and participant state consistency
    • Camera toggle and UI refinements for team workflows

✏️ Tip: You can customize this high-level summary in your review settings.

- Added functionality to create polls via WebSocket with appropriate payload handling.
- Introduced new data structures for poll metadata and state management.
- Enhanced the `loadPollSnapshot` function to include poll metadata in the snapshot.
- Updated WebSocket message handling to support poll creation and voting events.
- Improved frontend components to display audience polls and comments in real-time.
- Refactored related atoms and hooks to manage poll state effectively.
@coderabbitai
Copy link

coderabbitai bot commented Nov 9, 2025

Walkthrough

Adds a spectator WebSocket hub, Redis-backed poll storage, an event schema for debate interactions, team token-bucket turn management, and multiple frontend spectator/room UI updates with signaling and RTC integration for live debates.

Changes

Cohort / File(s) Summary
Debate spectator hub & handler
backend/cmd/server/ws_debate_handler.go
New DebateHub/DebateRoom/SpectatorClient and WebSocket handler implementing registration, presence, read/write pumps, message handlers (vote, question, reaction, createPoll), poll snapshot loading, and broadcasting to spectators.
Debate event types & serialization
backend/internal/debate/events.go
New Event model and payload structs (VotePayload, CreatePollPayload, PollCreatedPayload, QuestionPayload, ReactionPayload, PollSnapshotPayload, PresencePayload, JoinPayload, ClientMessage) with NewEvent/MarshalEvent/UnmarshalEvent helpers.
Poll store (Redis)
backend/internal/debate/poll_store.go
New PollStore with PollMetadata, CreatePoll, Vote (duplicate prevention), GetPollState (includes metadata), HasVoted, and Redis-backed initialization/transaction logic.
Team speaking / turn service
backend/services/team_turn_service.go
New TokenBucketService and TeamTurnManager with initialization, time-based refill, Consume/TryConsumeForSpeaking, GetRemainingTokens, CanUserSpeak, GetTeamSpeakingStatus, and circular turn progression.
Team WebSocket & main websocket enhancements
backend/websocket/team_websocket.go, backend/websocket/websocket.go
Added TeamRoom/TeamClient types and TeamWebsocketHandler; main websocket extended with ConnectionID, LiveTranscript, spectator lifecycle utilities, participant snapshots, improved broadcasting and spectator-aware paths.
Transcript controller
backend/controllers/transcript_controller.go
SubmitTranscripts now requires non-empty email on token validation; UpdatePendingTranscriptsHandler discards returned email value.
Frontend: View debate & spectator pages
frontend/src/Pages/ViewDebate.tsx, frontend/src/Pages/OnlineDebateRoom.tsx, frontend/src/Pages/TeamDebateRoom.tsx
New ViewDebate spectator component with dual RTCPeerConnection handling and poll UI; OnlineDebateRoom and TeamDebateRoom updated with ReconnectingWebSocket, per-spectator RTCPeerConnection queuing, signaling paths, spectator presence/count, media init, late-join ref patterns, and camera/transcript handling.
Frontend atoms & hooks
frontend/src/atoms/debateAtoms.ts, frontend/src/hooks/useDebateWS.ts
New PollInfo interface and pollStateAtom refactor; useDebateWS extended for poll_snapshot/poll_created handling, backwards compatibility conversion, normalized PollInfo updates, and robust WebSocket URL derivation.
Frontend services & API config
frontend/src/services/teamDebateService.ts, frontend/src/services/teamService.ts
New matchmaking types (TeamMatchmakingPoolEntry/Response, MatchmakingStatusResponse); getMatchmakingPool response type updated; API base switched to VITE_API_URL with trailing-slash trimming.
Frontend minor UI changes
frontend/src/Pages/TeamBuilder.tsx, frontend/src/components/Matchmaking.tsx, frontend/src/components/ReactionBar.tsx, frontend/src/components/TeamMatchmaking.tsx
Conditional TeamMatchmaking rendering with sanitized user; reordering of state updates on close; ReactionBar spectatorId fallback for reactions; removed debug logging in TeamMatchmaking.
New/updated manifests
package.json, manifest_file
Build/runtime manifest updates referenced by changes.

Sequence Diagram(s)

sequenceDiagram
    participant Spectator as Spectator (Browser)
    participant WS as WebSocket Server
    participant Hub as DebateHub
    participant PollStore as PollStore (Redis)

    Spectator->>WS: HTTP upgrade -> DebateWebsocketHandler
    WS->>Hub: Register(debateID, conn, spectatorHash)
    Hub->>Hub: ensure DebateRoom exists
    WS->>PollStore: loadPollSnapshot(debateID)
    PollStore-->>WS: poll state snapshot
    WS->>Spectator: send initial snapshot + presence
    Spectator->>WS: send "vote"/"createPoll"/"question"/"reaction"
    WS->>WS: handleVote/handleCreatePoll/handleQuestion/handleReaction
    WS->>PollStore: (Vote/CreatePoll) persist
    PollStore-->>WS: success/failure
    WS->>Hub: BroadcastToDebate(debateID, event)
    Hub->>Spectator: event broadcast to all connected spectators
Loading
sequenceDiagram
    participant TeamMember as Team Member (Browser)
    participant WS as WebSocket Server
    participant TBS as TokenBucketService
    participant TTM as TeamTurnManager
    participant MongoDB as MongoDB

    TeamMember->>WS: connect -> TeamWebsocketHandler
    WS->>TBS: InitializeTeamBuckets(teamID)
    TBS->>MongoDB: fetch team members
    WS->>TTM: InitializeTeamTurns(teamID)
    TTM->>MongoDB: fetch team members
    TeamMember->>WS: "attempt to speak" / request
    WS->>TBS: TryConsumeForSpeaking(teamID, userID, ttm)
    TBS->>TTM: Check current turn
    TTM-->>TBS: isCurrentTurn
    TBS-->>WS: result + remaining tokens
    WS->>TeamMember: broadcast speaking status / grant
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Attention areas:
    • Concurrency and mutex usage in DebateHub and TokenBucketService
    • Redis transaction correctness and error handling in poll_store.go (CreatePoll / Vote)
    • RTCPeerConnection lifecycle, cleanup and memory/resource leaks in ViewDebate and OnlineDebateRoom
    • Message serialization/deserialization across events.go and useDebateWS compatibility paths
    • WebSocket read/write error handling and spectator registration/unregistration race conditions

Possibly related PRs

Suggested reviewers

  • bhavik-mangla

Poem

🐰 I hopped through sockets late at night,

Polls and questions shining bright,
Spectators gather, tails a-flutter,
Votes and tokens in a joyful mutter,
Webs connect — the debate takes flight!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.05% 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 pull request title clearly and accurately summarizes the main changes: implementing audience interaction features including live polls and anonymous Q&A across both backend and frontend.
✨ 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: 38

Caution

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

⚠️ Outside diff range comments (8)
frontend/src/hooks/useUser.ts (1)

45-46: Restore error reporting in fetch failure path.
The catch now swallows all errors silently, leaving auth/profile fetch failures invisible and making user-state issues nearly impossible to debug. Please put the logging (or comparable handling) back so we surface these failures.

-        } catch (error) {
-        }
+        } catch (error) {
+          console.error('Failed to fetch user profile', error);
+        }
frontend/src/Pages/DebateRoom.tsx (1)

509-510: Provide user feedback when bot turn fails.

The catch block silently swallows errors without informing the user. When sendDebateMessage fails, the user sees no bot response and no explanation, making it appear as if the bot simply skipped its turn.

Consider adding user-visible error feedback (e.g., a popup message like "Bot failed to respond, moving to next turn") so users understand what happened rather than wondering if the app is broken.

Apply this diff to add user feedback:

     } catch (error) {
+      setPopup({
+        show: true,
+        message: "Bot failed to respond. Moving to next turn.",
+      });
+      setTimeout(() => setPopup({ show: false, message: "" }), 2000);
       setNextTurnPending(true);
     } finally {
frontend/src/components/Matchmaking.tsx (1)

54-135: Restore automatic reconnect after WebSocket closes.

With the new “already connected” guard, the effect that opens the socket never runs again once the initial connection closes (server restart, network blip, room_created navigation, etc.). After ws.onclose fires, isConnected toggles, but none of the hook’s dependencies change, so no reconnect happens—wsRef.current stays locked to a CLOSED socket and all matchmaking actions start throwing “Connection is not ready”.

Please either include a reconnection trigger (e.g., reset wsRef.current and let the effect rerun, add isConnected to the dependency list, or schedule a reconnect from onclose) so the UI recovers automatically instead of forcing a full page reload.

backend/controllers/debatevsbot_controller.go (3)

162-163: Database save errors are silently ignored.

The error from SaveDebateVsBot is checked but not handled, which could lead to data loss without any indication. Errors from database operations should be logged at minimum, or returned to the caller.

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

207-208: Database update errors are silently ignored.

The error from UpdateDebateVsBotOutcome is checked but not handled. This could result in incorrect debate outcomes being persisted without any error indication.

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

266-275: Transcript save errors are deliberately discarded.

The blank identifier explicitly discards the error from SaveDebateTranscript. This prevents detection of transcript persistence failures, which could impact users expecting to review their debates later.

-	_ = services.SaveDebateTranscript(
+	if err := services.SaveDebateTranscript(
 		userID,
 		email,
 		"user_vs_bot",
 		latestDebate.Topic,
 		latestDebate.BotName,
 		resultStatus,
 		req.History,
 		nil,
-	)
+	); err != nil {
+		log.Printf("Failed to save debate transcript: %v", err)
+	}
backend/controllers/auth.go (1)

500-531: Use the sanitized user payload in VerifyToken

VerifyToken marshals the models.User fields directly, so any stored NaN/Inf ratings will make Gin’s JSON encoding fail with json: unsupported value: NaN, returning a blank response instead of a token check result.(gigi.nullneuron.net) Reuse the existing normalization flow before emitting the response and render the shared buildUserResponse map (with sanitized floats) to keep the payload safe and consistent.

@@
-	// 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(http.StatusInternalServerError, gin.H{"error": "Failed to refresh user stats"})
+			return
+		}
+	}
+
+	ctx.JSON(http.StatusOK, gin.H{
+		"message": "Token is valid",
+		"user":    buildUserResponse(user),
+	})
frontend/src/Pages/OnlineDebateRoom.tsx (1)

473-1014: Parameterize the backend host; localhost hard-coding breaks real users

New logic is scattering http://localhost:1313/... and ws://localhost:1313/... (judgment polling, room socket). Once deployed, browsers will attempt to talk to their own localhost and the whole flow fails. Please derive both the WebSocket and REST URLs from the configured API base instead of hard-coding localhost.

Illustrative fix:

-  const wsUrl = `ws://localhost:1313/ws?room=${roomId}&token=${token}`;
+  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+  const apiHost =
+    import.meta.env.VITE_API_URL?.replace(/^https?:\/\//, "") ??
+    window.location.host;
+  const wsUrl = `${protocol}//${apiHost}/ws?room=${roomId}&token=${token}`;
@@
-        const pollResponse = await fetch(
-          `http://localhost:1313/submit-transcripts`,
+        const baseUrl =
+          import.meta.env.VITE_API_URL ??
+          `${window.location.protocol}//${window.location.host}`;
+        const pollResponse = await fetch(
+          `${baseUrl}/submit-transcripts`,
@@
-      const response = await fetch(`http://localhost:1313/submit-transcripts`, {
+      const baseUrl =
+        import.meta.env.VITE_API_URL ??
+        `${window.location.protocol}//${window.location.host}`;
+      const response = await fetch(`${baseUrl}/submit-transcripts`, {

Apply the same substitution anywhere else we call back into the debate API in this file.

🧹 Nitpick comments (18)
frontend/tsconfig.app.tsbuildinfo (1)

1-1: Consider excluding .tsbuildinfo from version control.

The tsconfig.app.tsbuildinfo file is a TypeScript build artifact generated during compilation and used for incremental build optimization. It typically shouldn't be committed to version control, as it can cause unnecessary merge conflicts and increase repository size without providing value to developers.

Ensure this file is in .gitignore:

# Add to frontend/.gitignore (or root .gitignore if it covers frontend)
*.tsbuildinfo

Alternatively, if there's a specific organizational reason for tracking this artifact, document that decision.

backend/utils/user.go (1)

3-12: Reorganize imports per Go conventions.

Imports should be organized into groups: standard library, third-party, then project packages. The current order is fragmented (project → standard lib → third-party → standard lib).

Apply this diff to reorganize imports correctly:

import (
+	"context"
+	"strings"
+	"time"
+
 	"go.mongodb.org/mongo-driver/bson"
 	"go.mongodb.org/mongo-driver/bson/primitive"
 	"go.mongodb.org/mongo-driver/mongo"
+
 	"arguehub/db"
 	"arguehub/models"
-	"context"
-	"go.mongodb.org/mongo-driver/bson"
-	"go.mongodb.org/mongo-driver/bson/primitive"
-	"go.mongodb.org/mongo-driver/mongo"
-	"strings"
-	"time"
)
frontend/src/components/PlayerCard.tsx (1)

32-38: Consider making player details configurable.

The player name ("Sofia Dev"), rating ("1000"), and avatar image are currently hardcoded. Consider adding these as props to make the component more reusable across different contexts.

 interface PlayerCardProps {
   isUser: boolean;
   cameraOn?: boolean;
   micOn?: boolean;
   setCameraOn?: React.Dispatch<React.SetStateAction<boolean>>;
   setMicOn?: React.Dispatch<React.SetStateAction<boolean>>;
   isTurn: boolean;
   turnDuration?: number;
+  playerName?: string;
+  playerRating?: number;
+  avatarSrc?: string;
 }
 
 const PlayerCard: React.FC<PlayerCardProps> = ({
   isUser,
   cameraOn = true,
   micOn = true,
   setCameraOn,
   setMicOn,
   isTurn,
   turnDuration = 0,
+  playerName = "Sofia Dev",
+  playerRating = 1000,
+  avatarSrc = avatar1,
 }) => {
   return (
     <div className="flex justify-between items-center border rounded w-full p-2 ">
       {/* Player Details Section */}
       <div className="flex items-center">
         <Avatar className="bg-primary mx-2">
-          <AvatarImage src={avatar1} alt="@name" className="object-cover" />
+          <AvatarImage src={avatarSrc} alt={`@${playerName}`} className="object-cover" />
-          <AvatarFallback>SL</AvatarFallback>
+          <AvatarFallback>{playerName.split(' ').map(n => n[0]).join('')}</AvatarFallback>
         </Avatar>
-        <span>Sofia Dev</span>
+        <span>{playerName}</span>
         <Separator orientation="vertical" className="mx-2 h-5 bg-muted" />
-        <span>1000</span>
+        <span>{playerRating}</span>
       </div>
backend/routes/pros_cons_route copy.go (1)

10-21: Hard-coded skill level reduces flexibility.

The function uses a hard-coded "intermediate" skill level (line 13) rather than accepting it as a parameter. The comment claims a "default," but the implementation is inflexible. This may be intentional, but consider whether GetProsConsTopic should accept an optional skill level parameter or whether the skill level should come from user context.

Consider one of these approaches:

  • Option A (parameter-based): Accept skill level as a query parameter:

    skillLevel := c.DefaultQuery("skillLevel", "intermediate")
  • Option B (context-based): Extract skill level from authenticated user context or session.

Which approach aligns with your application design?

backend/rating/glicko2.go (1)

78-204: Optional: Consider separating formatting from functional changes.

The blank lines added throughout improve readability by separating logical sections. However, mixing formatting changes with functional changes can make it harder to review the substantive modifications.

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

210-254: Consider aligning validation strategy for consistency.

The addition of required attributes (lines 219, 232) is a good UX improvement. However, the validation approach is inconsistent:

  • Name field (line 219): has both HTML5 required and JS validation (lines 55-58)
  • Date field (line 232): has HTML5 required but no JS validation
  • Description field: has neither

Consider one of these approaches for consistency:

  1. Rely on HTML5 validation: Remove the redundant JS validation for name (lines 55-58) since the required attribute will catch empty values before form submission.

  2. Add comprehensive JS validation: Add validation for the date field to match the name validation pattern, ensuring a consistent error messaging strategy.

Example for approach 2:

 const handleCreate = (e: FormEvent) => {
   e.preventDefault();
   if (!name) {
     setError("Tournament name is required.");
     return;
   }
+  if (!date) {
+    setError("Tournament date is required.");
+    return;
+  }
frontend/src/Pages/DebateRoom.tsx (2)

252-285: Consider removing the redundant guard check.

The if (SpeechRecognition) check at line 252 is redundant since the outer condition at line 249 already verifies that SpeechRecognition or webkitSpeechRecognition exists in the window. If either exists, the assignment at lines 250-251 will always succeed with a truthy value.

Apply this diff to simplify:

     if ("SpeechRecognition" in window || "webkitSpeechRecognition" in window) {
       const SpeechRecognition =
         window.SpeechRecognition || window.webkitSpeechRecognition;
-      if (SpeechRecognition) {
-        recognitionRef.current = new SpeechRecognition();
-        recognitionRef.current.continuous = true;
-        recognitionRef.current.interimResults = true;
-        recognitionRef.current.lang = "en-US";
+      recognitionRef.current = new SpeechRecognition();
+      recognitionRef.current.continuous = true;
+      recognitionRef.current.interimResults = true;
+      recognitionRef.current.lang = "en-US";

-        recognitionRef.current.onresult = (event) => {
+      recognitionRef.current.onresult = (event) => {
           let newFinalTranscript = "";
           let newInterimTranscript = "";
           for (let i = event.resultIndex; i < event.results.length; i++) {
@@ -274,14 +271,13 @@
           } else {
             setInterimInput(newInterimTranscript);
           }
-        };
+      };

-        recognitionRef.current.onend = () => setIsRecognizing(false);
-        recognitionRef.current.onerror = () => {
-          setIsRecognizing(false);
-        };
-      }
+      recognitionRef.current.onend = () => setIsRecognizing(false);
+      recognitionRef.current.onerror = () => {
+        setIsRecognizing(false);
+      };
     }

282-283: Restore error logging for observability.

Removing all console.error statements from error handlers (speech recognition errors at lines 282-283, bot turn failures at lines 509-510, and judging failures in the catch block around lines 527-555) makes production debugging challenging. Without error logs, you cannot diagnose failure patterns or root causes when users report issues.

Consider restoring minimal error logging in catch blocks:

       recognitionRef.current.onerror = () => {
+        console.error("Speech recognition error occurred");
         setIsRecognizing(false);
       };
     } catch (error) {
+      console.error("Bot turn failed:", error);
       setNextTurnPending(true);
     } finally {
     } catch (error) {
+      console.error("Debate judging failed:", error);
       setJudgmentData({

Also applies to: 509-510, 527-555

backend/models/user.go (1)

16-18: Inconsistent field alignment.

These three fields have additional leading spaces, but the rest of the struct fields (lines 11-15, 19-29) do not. Consider using gofmt to ensure consistent formatting across the entire struct, or align all fields uniformly if manual alignment is intended.

backend/test_server.go (1)

1-50: Consider retaining logging in test/debug scripts.

All logging statements have been removed from this test server. While this aligns with the broader PR pattern of reducing console output, test scripts typically benefit from logging to verify behavior and diagnose issues. Without any output, it's difficult to confirm whether the matchmaking test is working as expected or to debug failures.

Consider keeping at least minimal logging for test/debug scripts, or rename/relocate this file if it's no longer intended for testing purposes.

frontend/src/Pages/StrengthenArgument.tsx (1)

101-143: Consider conditional logging for development environments.

Console error logging was removed from the error handlers (lines 134-143 in fetchWeakStatement and similar in handleSubmit). While user-facing error notifications remain via notify(), removing console logs can make development debugging more difficult.

Consider using conditional logging that only outputs to console in development:

if (import.meta.env.DEV) {
  console.error('Error fetching weak statement:', err);
}

This preserves debugging capability during development while keeping production console output clean.

frontend/src/components/Chatbox.tsx (1)

120-123: Preserve speech recognition error details

With the zero-arg handler we now discard the SpeechRecognitionErrorEvent, so not-allowed, audio-capture, etc. all fail silently; neither developers nor users get any hint about what went wrong. Please keep the event parameter and surface/log the error (or route it into UI state) so we can react appropriately.

-      recognition.onerror = () => {
+      recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
         setIsRecognizing(false);
         onSpeakingChangeRef.current(false);
+        console.error('Speech recognition error:', event.error);
       };
backend/websocket/matchmaking.go (1)

119-125: Close the upgraded socket on pool join failure.

Once the HTTP connection is upgraded, calling c.String won’t emit a response; Gin’s writer has already been hijacked. If AddToPool ever returns an error, the client socket stays open, the connection is still tracked in matchmakingRoom.clients, and nothing tells the browser the join failed. Please make sure you close conn, remove the client from matchmakingRoom.clients, and send an explicit error frame before returning.

frontend/src/components/Matchmaking.tsx (1)

137-169: Surface a clear retry path when the socket isn’t ready.

The alert-only flow makes the UX brittle—after a transient CLOSE/CONNECTING state, the user can’t recover because no retry is attempted and wsRef still points at a defunct socket. Consider queueing the join_pool message until OPEN, or at least prompting the reconnection logic described above before throwing the alert.

backend/services/matchmaking.go (1)

211-223: Broadcast pool updates after successful matches.

After a successful room insert we call the callback, but we never push a new pool_update. Observers keep seeing the matched players until their sockets close and the defer in readPump fires. Consider calling sendPoolStatus() here so the pool reflects the match immediately.

backend/cmd/test_judge/main.go (1)

9-12: Consider making the config path configurable.

The config path is hardcoded to ./config/config.prod.yml. For a test harness, consider accepting the path as a command-line argument or environment variable to improve flexibility across different environments.

 func main() {
-	cfg, err := config.LoadConfig("./config/config.prod.yml")
+	configPath := os.Getenv("CONFIG_PATH")
+	if configPath == "" {
+		configPath = "./config/config.prod.yml"
+	}
+	cfg, err := config.LoadConfig(configPath)
 	if err != nil {
 		panic("failed to load config: " + err.Error())
 	}
backend/controllers/debatevsbot_controller.go (1)

250-251: Simplify complex boolean expression.

The boolean logic on these lines is difficult to read and maintain. Consider extracting the compound conditions into named variables or helper functions.

-	if strings.Contains(resultLower, "user win") || strings.Contains(resultLower, "user wins") ||
-		strings.Contains(resultLower, "user") && strings.Contains(resultLower, "win") {
+	hasUserWin := strings.Contains(resultLower, "user win") || strings.Contains(resultLower, "user wins")
+	hasUserAndWin := strings.Contains(resultLower, "user") && strings.Contains(resultLower, "win")
+	if hasUserWin || hasUserAndWin {
 		resultStatus = "win"
frontend/src/hooks/useDebateWS.ts (1)

264-267: Wrap the presence case in a block
Line 265 declares const count directly under the case. Lint (noSwitchDeclarations) flagged this because lexical declarations leak across cases. Adding braces isolates the scope and quiets the warning.

-          case 'presence':
-            const count = eventData.payload.connected || 0;
-            setPresence(count);
-            break;
+          case 'presence': {
+            const count = eventData.payload.connected || 0;
+            setPresence(count);
+            break;
+          }

Based on static analysis hints

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f29be62 and b2fc4e9.

⛔ Files ignored due to path filters (2)
  • backend/go.sum is excluded by !**/*.sum
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (98)
  • backend/cmd/server/main.go (6 hunks)
  • backend/cmd/server/ws_debate_handler.go (1 hunks)
  • backend/cmd/test_judge/main.go (1 hunks)
  • backend/config/config.go (1 hunks)
  • backend/controllers/auth.go (9 hunks)
  • backend/controllers/debate_controller.go (0 hunks)
  • backend/controllers/debatevsbot_controller.go (1 hunks)
  • backend/controllers/leaderboard.go (0 hunks)
  • backend/controllers/profile_controller.go (3 hunks)
  • backend/controllers/team_controller.go (1 hunks)
  • backend/controllers/team_debate_controller.go (1 hunks)
  • backend/controllers/team_matchmaking.go (1 hunks)
  • backend/controllers/transcript_controller.go (8 hunks)
  • backend/db/db.go (2 hunks)
  • backend/go.mod (2 hunks)
  • backend/internal/debate/events.go (1 hunks)
  • backend/internal/debate/poll_store.go (1 hunks)
  • backend/internal/debate/rate_limiter.go (1 hunks)
  • backend/internal/debate/redis_client.go (1 hunks)
  • backend/internal/debate/stream_consumer.go (1 hunks)
  • backend/middlewares/auth.go (2 hunks)
  • backend/models/coach.go (1 hunks)
  • backend/models/debate.go (2 hunks)
  • backend/models/team.go (1 hunks)
  • backend/models/transcript.go (1 hunks)
  • backend/models/user.go (2 hunks)
  • backend/rating/glicko2.go (5 hunks)
  • backend/routes/debate.go (4 hunks)
  • backend/routes/leaderboard.go (1 hunks)
  • backend/routes/pros_cons_route copy.go (1 hunks)
  • backend/routes/rooms.go (9 hunks)
  • backend/routes/team.go (1 hunks)
  • backend/routes/transcriptroutes.go (1 hunks)
  • backend/services/ai.go (0 hunks)
  • backend/services/coach.go (2 hunks)
  • backend/services/debatevsbot.go (5 hunks)
  • backend/services/gemini.go (1 hunks)
  • backend/services/matchmaking.go (6 hunks)
  • backend/services/personalities.go (29 hunks)
  • backend/services/pros_cons.go (2 hunks)
  • backend/services/rating_service.go (4 hunks)
  • backend/services/team_matchmaking.go (1 hunks)
  • backend/services/team_turn_service.go (1 hunks)
  • backend/services/transcriptservice.go (13 hunks)
  • backend/test_server.go (1 hunks)
  • backend/utils/auth.go (5 hunks)
  • backend/utils/debate.go (1 hunks)
  • backend/utils/populate.go (3 hunks)
  • backend/utils/user.go (1 hunks)
  • backend/websocket/handler.go (0 hunks)
  • backend/websocket/matchmaking.go (6 hunks)
  • backend/websocket/team_debate_handler.go (1 hunks)
  • backend/websocket/team_websocket.go (1 hunks)
  • backend/websocket/websocket.go (11 hunks)
  • frontend/package.json (1 hunks)
  • frontend/src/App.tsx (4 hunks)
  • frontend/src/Pages/About.tsx (5 hunks)
  • frontend/src/Pages/Authentication/forms.tsx (0 hunks)
  • frontend/src/Pages/BotSelection.tsx (0 hunks)
  • frontend/src/Pages/DebateRoom.tsx (1 hunks)
  • frontend/src/Pages/Game.tsx (6 hunks)
  • frontend/src/Pages/MatchLogs.tsx (5 hunks)
  • frontend/src/Pages/OnlineDebateRoom.tsx (34 hunks)
  • frontend/src/Pages/Profile.tsx (0 hunks)
  • frontend/src/Pages/SpeechTest.tsx (0 hunks)
  • frontend/src/Pages/StartDebate.tsx (2 hunks)
  • frontend/src/Pages/StrengthenArgument.tsx (1 hunks)
  • frontend/src/Pages/TeamBuilder.tsx (1 hunks)
  • frontend/src/Pages/TeamDebateRoom.tsx (1 hunks)
  • frontend/src/Pages/TournamentHub.tsx (9 hunks)
  • frontend/src/Pages/ViewDebate.tsx (1 hunks)
  • frontend/src/atoms/debateAtoms.ts (1 hunks)
  • frontend/src/components/AnonymousQA.tsx (1 hunks)
  • frontend/src/components/ChatRoom.tsx (6 hunks)
  • frontend/src/components/Chatbox.tsx (1 hunks)
  • frontend/src/components/DebatePopup.tsx (0 hunks)
  • frontend/src/components/JudgementPopup.tsx (9 hunks)
  • frontend/src/components/Layout.tsx (1 hunks)
  • frontend/src/components/Matchmaking.tsx (5 hunks)
  • frontend/src/components/MatchmakingPool.tsx (1 hunks)
  • frontend/src/components/PlayerCard.tsx (1 hunks)
  • frontend/src/components/ReactionBar.tsx (1 hunks)
  • frontend/src/components/RoomBrowser.tsx (1 hunks)
  • frontend/src/components/SavedTranscripts.tsx (0 hunks)
  • frontend/src/components/Sidebar.tsx (1 hunks)
  • frontend/src/components/TeamChatSidebar.tsx (1 hunks)
  • frontend/src/components/TeamMatchmaking.tsx (1 hunks)
  • frontend/src/components/UserCamera.tsx (0 hunks)
  • frontend/src/context/authContext.tsx (0 hunks)
  • frontend/src/context/theme-provider.tsx (1 hunks)
  • frontend/src/hooks/useDebateWS.ts (1 hunks)
  • frontend/src/hooks/useUser.ts (1 hunks)
  • frontend/src/services/teamDebateService.ts (1 hunks)
  • frontend/src/services/teamService.ts (1 hunks)
  • frontend/src/services/transcriptService.ts (0 hunks)
  • frontend/src/services/vsbot.ts (1 hunks)
  • frontend/src/utils/speechTest.ts (1 hunks)
  • frontend/tsconfig.app.tsbuildinfo (1 hunks)
💤 Files with no reviewable changes (13)
  • backend/services/ai.go
  • frontend/src/components/DebatePopup.tsx
  • frontend/src/components/UserCamera.tsx
  • frontend/src/Pages/SpeechTest.tsx
  • frontend/src/services/transcriptService.ts
  • frontend/src/components/SavedTranscripts.tsx
  • frontend/src/Pages/BotSelection.tsx
  • frontend/src/Pages/Authentication/forms.tsx
  • backend/controllers/leaderboard.go
  • frontend/src/context/authContext.tsx
  • backend/websocket/handler.go
  • frontend/src/Pages/Profile.tsx
  • backend/controllers/debate_controller.go
🧰 Additional context used
🧬 Code graph analysis (48)
frontend/src/components/ChatRoom.tsx (2)
backend/websocket/websocket.go (1)
  • Message (66-89)
backend/structs/websocket.go (1)
  • Message (10-13)
backend/services/coach.go (1)
backend/models/coach.go (2)
  • WeakStatement (4-9)
  • Evaluation (18-21)
backend/controllers/debatevsbot_controller.go (1)
backend/services/transcriptservice.go (1)
  • SaveDebateTranscript (602-666)
backend/websocket/matchmaking.go (2)
backend/services/matchmaking.go (1)
  • GetMatchmakingService (39-48)
backend/db/db.go (1)
  • MongoDatabase (16-16)
backend/cmd/test_judge/main.go (3)
backend/config/config.go (1)
  • LoadConfig (58-70)
backend/services/debatevsbot.go (1)
  • InitDebateVsBotService (21-27)
backend/services/transcriptservice.go (1)
  • JudgeDebateHumanVsHuman (336-426)
frontend/src/components/TeamChatSidebar.tsx (1)
frontend/src/components/Chatbox.tsx (1)
  • ChatMessage (5-11)
frontend/src/Pages/TeamBuilder.tsx (2)
backend/models/team.go (2)
  • TeamMember (25-32)
  • Team (11-22)
frontend/src/services/teamService.ts (13)
  • TeamMember (17-24)
  • Team (4-15)
  • getAvailableTeams (142-155)
  • getUserTeams (126-139)
  • createTeam (61-77)
  • joinTeam (96-108)
  • getTeamMemberProfile (308-321)
  • removeMember (276-289)
  • deleteTeam (292-305)
  • updateTeamName (324-339)
  • updateTeamSize (358-373)
  • leaveTeam (111-123)
  • getTeamByCode (342-355)
backend/routes/team.go (4)
backend/controllers/team_controller.go (12)
  • CreateTeam (175-246)
  • GetTeam (249-270)
  • JoinTeam (273-355)
  • LeaveTeam (358-416)
  • DeleteTeam (521-559)
  • RemoveMember (447-518)
  • UpdateTeamName (49-103)
  • UpdateTeamSize (106-172)
  • GetTeamByCode (30-46)
  • GetTeamMemberProfile (562-588)
  • GetUserTeams (419-444)
  • GetAvailableTeams (591-609)
backend/controllers/team_debate_controller.go (3)
  • CreateTeamDebate (18-94)
  • GetTeamDebate (97-114)
  • GetActiveTeamDebate (117-146)
backend/controllers/team_matchmaking.go (4)
  • JoinMatchmaking (17-69)
  • LeaveMatchmaking (72-82)
  • GetMatchmakingStatus (109-129)
  • GetMatchmakingPool (85-106)
backend/services/team_matchmaking.go (1)
  • GetMatchmakingPool (111-119)
backend/middlewares/auth.go (2)
backend/models/user.go (1)
  • User (10-30)
backend/db/db.go (1)
  • MongoDatabase (16-16)
frontend/src/hooks/useDebateWS.ts (2)
backend/internal/debate/events.go (1)
  • Event (9-13)
frontend/src/atoms/debateAtoms.ts (9)
  • wsAtom (4-4)
  • debateIdAtom (7-7)
  • pollStateAtom (18-18)
  • questionsAtom (52-52)
  • reactionsAtom (55-55)
  • wsStatusAtom (58-58)
  • presenceAtom (64-64)
  • spectatorHashAtom (33-46)
  • PollInfo (9-15)
backend/rating/glicko2.go (1)
backend/config/config.go (1)
  • Config (10-55)
backend/services/rating_service.go (2)
backend/models/debate.go (1)
  • Debate (10-25)
backend/rating/glicko2.go (1)
  • Player (21-26)
frontend/src/components/MatchmakingPool.tsx (2)
backend/services/matchmaking.go (1)
  • MatchmakingPool (16-25)
frontend/src/services/teamDebateService.ts (1)
  • getMatchmakingPool (117-130)
frontend/src/App.tsx (2)
backend/websocket/team_debate_handler.go (1)
  • TeamDebateRoom (31-35)
frontend/src/Pages/ViewDebate.tsx (1)
  • ViewDebate (18-725)
backend/services/team_matchmaking.go (3)
backend/models/team.go (1)
  • Team (11-22)
backend/db/db.go (1)
  • GetCollection (20-22)
backend/controllers/team_matchmaking.go (1)
  • GetMatchmakingPool (85-106)
backend/controllers/auth.go (3)
backend/models/user.go (1)
  • User (10-30)
frontend/src/types/user.ts (1)
  • User (1-21)
backend/db/db.go (1)
  • MongoDatabase (16-16)
frontend/src/Pages/TeamDebateRoom.tsx (4)
frontend/src/services/teamService.ts (2)
  • TeamMember (17-24)
  • getTeamDebate (179-192)
frontend/src/hooks/useUser.ts (1)
  • useUser (7-59)
frontend/src/utils/auth.ts (1)
  • getAuthToken (5-7)
frontend/src/services/teamDebateService.ts (1)
  • getTeamDebate (55-68)
backend/internal/debate/rate_limiter.go (2)
backend/internal/debate/redis_client.go (2)
  • GetRedisClient (35-37)
  • GetContext (40-42)
backend/internal/debate/poll_store.go (1)
  • NewPollStore (27-32)
backend/controllers/team_debate_controller.go (3)
backend/db/db.go (1)
  • GetCollection (20-22)
backend/models/team.go (2)
  • Team (11-22)
  • TeamDebate (61-81)
backend/services/team_matchmaking.go (1)
  • RemoveFromMatchmaking (101-108)
frontend/src/components/JudgementPopup.tsx (1)
frontend/src/components/ui/button.tsx (1)
  • Button (57-57)
backend/internal/debate/stream_consumer.go (2)
backend/internal/debate/events.go (3)
  • Event (9-13)
  • UnmarshalEvent (104-110)
  • MarshalEvent (95-101)
backend/internal/debate/redis_client.go (2)
  • GetRedisClient (35-37)
  • GetContext (40-42)
backend/controllers/team_matchmaking.go (3)
backend/db/db.go (1)
  • GetCollection (20-22)
backend/models/team.go (1)
  • Team (11-22)
backend/services/team_matchmaking.go (4)
  • StartTeamMatchmaking (30-61)
  • RemoveFromMatchmaking (101-108)
  • GetMatchmakingPool (111-119)
  • FindMatchingTeam (64-98)
backend/controllers/team_controller.go (4)
backend/db/db.go (1)
  • GetCollection (20-22)
backend/models/team.go (2)
  • Team (11-22)
  • TeamMember (25-32)
frontend/src/services/teamService.ts (2)
  • Team (4-15)
  • TeamMember (17-24)
backend/models/user.go (1)
  • User (10-30)
frontend/src/Pages/ViewDebate.tsx (3)
frontend/src/atoms/debateAtoms.ts (6)
  • debateIdAtom (7-7)
  • pollStateAtom (18-18)
  • questionsAtom (52-52)
  • reactionsAtom (55-55)
  • wsStatusAtom (58-58)
  • presenceAtom (64-64)
frontend/src/hooks/useDebateWS.ts (1)
  • useDebateWS (22-305)
frontend/src/utils/auth.ts (1)
  • getAuthToken (5-7)
backend/websocket/team_debate_handler.go (2)
backend/models/team.go (2)
  • TeamDebateMessage (84-95)
  • TeamDebate (61-81)
backend/db/db.go (1)
  • GetCollection (20-22)
frontend/src/components/TeamMatchmaking.tsx (3)
backend/models/team.go (1)
  • Team (11-22)
frontend/src/services/teamService.ts (2)
  • Team (4-15)
  • createTeamDebate (158-176)
frontend/src/services/teamDebateService.ts (5)
  • getActiveTeamDebate (71-84)
  • getMatchmakingStatus (132-145)
  • joinMatchmaking (87-100)
  • leaveMatchmaking (102-115)
  • createTeamDebate (31-52)
frontend/src/services/teamDebateService.ts (3)
frontend/src/utils/auth.ts (1)
  • getAuthToken (5-7)
backend/models/team.go (1)
  • TeamDebate (61-81)
frontend/src/services/teamService.ts (3)
  • TeamDebate (26-40)
  • createTeamDebate (158-176)
  • getTeamDebate (179-192)
backend/websocket/team_websocket.go (5)
backend/services/team_turn_service.go (5)
  • TeamTurnManager (132-136)
  • TokenBucket (22-28)
  • TokenBucketService (16-19)
  • NewTeamTurnManager (139-144)
  • NewTokenBucketService (31-35)
backend/websocket/websocket.go (1)
  • Room (29-32)
backend/utils/auth.go (1)
  • ValidateTokenAndFetchEmail (127-140)
backend/db/db.go (1)
  • MongoDatabase (16-16)
backend/models/team.go (2)
  • TeamDebate (61-81)
  • Team (11-22)
backend/services/transcriptservice.go (5)
backend/controllers/transcript_controller.go (1)
  • SubmitTranscripts (42-79)
backend/db/db.go (1)
  • MongoDatabase (16-16)
backend/models/user.go (1)
  • User (10-30)
backend/models/transcript.go (1)
  • SavedDebateTranscript (25-37)
backend/services/rating_service.go (1)
  • UpdateRatings (28-106)
backend/routes/pros_cons_route copy.go (2)
backend/services/pros_cons.go (2)
  • GenerateDebateTopic (14-42)
  • EvaluateProsCons (59-120)
backend/services/coach.go (1)
  • UpdateUserPoints (117-124)
frontend/src/components/AnonymousQA.tsx (2)
frontend/src/atoms/debateAtoms.ts (4)
  • debateIdAtom (7-7)
  • spectatorHashAtom (33-46)
  • transcriptAtom (49-49)
  • questionsAtom (52-52)
frontend/src/hooks/useDebateWS.ts (1)
  • useDebateWS (22-305)
backend/cmd/server/main.go (6)
backend/db/db.go (1)
  • ConnectMongoDB (37-58)
backend/internal/debate/redis_client.go (1)
  • InitRedis (16-32)
backend/websocket/websocket.go (1)
  • WebsocketHandler (227-446)
backend/routes/team.go (4)
  • SetupTeamRoutes (10-27)
  • SetupTeamDebateRoutes (30-37)
  • SetupTeamChatRoutes (51-53)
  • SetupTeamMatchmakingRoutes (40-48)
backend/websocket/team_websocket.go (1)
  • TeamWebsocketHandler (101-380)
backend/cmd/server/ws_debate_handler.go (1)
  • DebateWebsocketHandler (196-264)
backend/test_server.go (1)
backend/services/matchmaking.go (1)
  • GetMatchmakingService (39-48)
frontend/src/components/ReactionBar.tsx (2)
frontend/src/atoms/debateAtoms.ts (2)
  • debateIdAtom (7-7)
  • spectatorHashAtom (33-46)
frontend/src/hooks/useDebateWS.ts (1)
  • useDebateWS (22-305)
backend/services/debatevsbot.go (1)
backend/db/db.go (1)
  • DebateVsBotCollection (17-17)
backend/routes/debate.go (2)
backend/services/rating_service.go (1)
  • UpdateRatings (28-106)
backend/db/db.go (1)
  • MongoDatabase (16-16)
frontend/src/services/teamService.ts (3)
backend/models/team.go (3)
  • Team (11-22)
  • TeamMember (25-32)
  • TeamDebate (61-81)
frontend/src/services/teamDebateService.ts (3)
  • TeamDebate (8-28)
  • createTeamDebate (31-52)
  • getTeamDebate (55-68)
frontend/src/utils/auth.ts (1)
  • getAuthToken (5-7)
backend/cmd/server/ws_debate_handler.go (4)
backend/internal/debate/stream_consumer.go (4)
  • StreamConsumer (18-24)
  • DebateHub (13-15)
  • NewStreamConsumer (27-45)
  • PublishEvent (169-199)
backend/internal/debate/events.go (8)
  • Event (9-13)
  • ClientMessage (75-78)
  • VotePayload (16-22)
  • NewEvent (81-92)
  • QuestionPayload (42-47)
  • ReactionPayload (50-54)
  • CreatePollPayload (25-31)
  • PollCreatedPayload (34-39)
backend/internal/debate/rate_limiter.go (2)
  • NewRateLimiter (18-23)
  • DefaultRateLimitConfig (35-43)
backend/internal/debate/poll_store.go (1)
  • NewPollStore (27-32)
frontend/src/components/Matchmaking.tsx (2)
backend/websocket/matchmaking.go (1)
  • MatchmakingMessage (48-56)
backend/services/matchmaking.go (1)
  • MatchmakingPool (16-25)
frontend/src/Pages/OnlineDebateRoom.tsx (3)
frontend/src/hooks/useDebateWS.ts (1)
  • useDebateWS (22-305)
frontend/src/atoms/debateAtoms.ts (4)
  • questionsAtom (52-52)
  • presenceAtom (64-64)
  • pollStateAtom (18-18)
  • PollInfo (9-15)
frontend/src/utils/auth.ts (1)
  • getAuthToken (5-7)
backend/services/matchmaking.go (1)
backend/db/db.go (1)
  • MongoDatabase (16-16)
backend/internal/debate/poll_store.go (1)
backend/internal/debate/redis_client.go (2)
  • GetRedisClient (35-37)
  • GetContext (40-42)
backend/services/pros_cons.go (1)
backend/models/debate.go (1)
  • ProsConsEvaluation (33-37)
backend/websocket/websocket.go (3)
backend/routes/rooms.go (1)
  • Room (21-26)
backend/structs/websocket.go (2)
  • Room (30-37)
  • Message (10-13)
backend/utils/auth.go (1)
  • ValidateTokenAndFetchEmail (127-140)
backend/controllers/profile_controller.go (1)
backend/db/db.go (1)
  • MongoDatabase (16-16)
backend/services/team_turn_service.go (2)
backend/db/db.go (1)
  • GetCollection (20-22)
backend/models/team.go (1)
  • Team (11-22)
backend/controllers/transcript_controller.go (2)
backend/services/transcriptservice.go (1)
  • SubmitTranscripts (19-252)
backend/utils/auth.go (1)
  • ValidateTokenAndFetchEmail (127-140)
backend/models/team.go (4)
frontend/src/services/teamService.ts (3)
  • TeamMember (17-24)
  • Team (4-15)
  • TeamDebate (26-40)
backend/websocket/websocket.go (1)
  • Message (66-89)
frontend/src/services/teamDebateService.ts (1)
  • TeamDebate (8-28)
backend/websocket/team_debate_handler.go (1)
  • TeamDebateMessage (37-46)
🪛 Biome (2.1.2)
frontend/src/hooks/useDebateWS.ts

[error] 265-265: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

frontend/src/Pages/TeamDebateRoom.tsx

[error] 443-443: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 444-445: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 446-446: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 512-512: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 524-524: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 525-525: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 526-526: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 552-552: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 553-553: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 567-567: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 568-570: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 571-573: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 574-574: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔇 Additional comments (22)
frontend/src/context/theme-provider.tsx (1)

26-69: Minor cleanup: console.log removal and formatting — unrelated to PR objectives.

Per the AI summary, three debug console.log statements have been removed from getInitialTheme and its conditional branches, and a trailing newline has been added at the file's end. These are cosmetic/hygiene improvements but orthogonal to the PR's core goals (audience interaction, live polls, anonymous Q&A). The theme provider logic remains unchanged and correct.

backend/utils/user.go (1)

14-28: Function logic is sound.

The GetUserIDFromEmail function correctly handles context timeouts, email normalization, and error differentiation. Proper error handling distinguishes between "document not found" and other errors.

frontend/src/utils/speechTest.ts (2)

56-57: Approved: Clean simplification of error handler.

The replacement of a complex typing workaround with a simple no-op onerror handler is a good simplification. For a test utility intentionally suppressing error logging (per PR objectives), this approach is appropriate and maintainable.


21-58: Note: Empty event handlers are intentional per PR logging cleanup.

The empty event handlers (onstart, onresult, onend) align with the PR's objective to remove runtime logging noise. Verify that this test utility is not expected to provide any result callbacks to consumers—if downstream code relies on transcript data, consider adding a callback mechanism or results getter.

frontend/src/components/Layout.tsx (1)

2-4: LGTM! Stylistic consistency improvements.

The reformatting from double quotes to single quotes in imports and className attributes maintains consistency with the rest of the codebase. No functional changes.

Also applies to: 8-12

frontend/src/Pages/StartDebate.tsx (1)

1-1: Good modernization for React 17+ JSX runtime.

Removing the default React import aligns with the automatic JSX transform introduced in React 17+, where React no longer needs to be in scope for JSX to work.

frontend/src/Pages/About.tsx (1)

11-121: Formatting changes look fine, though unrelated to PR objectives.

These changes are purely cosmetic—text reflow, adjusted line breaks, and added semicolons—with no impact on functionality or behavior. The formatting is clean and the content remains clear.

However, the About page text updates appear unrelated to this PR's core objectives (audience interaction features, live polls, Q&A). Consider keeping formatting-only changes in separate commits or PRs to maintain clearer change history.

frontend/src/components/PlayerCard.tsx (1)

19-54: Good cleanup of debug logging.

Removing the console.log statements for turn duration and isTurn is appropriate for production code and aligns with the broader effort to clean up runtime output across the codebase.

backend/utils/auth.go (1)

30-35: The JWT secret initialization is properly configured at startup.

Verified: SetJWTSecret() is called in backend/cmd/server/main.go:50 during application initialization before request handling begins. The change from log.Fatal to panic is safe and more idiomatic, allowing deferred functions to execute for cleanup.

backend/routes/pros_cons_route copy.go (2)

24-49: Good defensive validation and error handling.

The request struct uses binding constraints (max=5) that align with the service-layer validation in EvaluateProsCons, which is good defense-in-depth. Error handling is consistent and informative across both branches (JSON binding, evaluation, and points update).


42-46: Verify user context is always populated.

Line 42 extracts user_id via c.GetString("user_id") without validation. This assumes authentication middleware has set this value. Confirm that your auth middleware guarantees this is always present, or add a guard:

userID := c.GetString("user_id")
if userID == "" {
  c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
  return
}
backend/rating/glicko2.go (1)

98-99: LGTM! Timestamp updates correctly placed.

The timestamp updates ensure that the next match will properly calculate time decay. The placement is correct—after all rating calculations are complete and after updateTimeRD has used the old timestamps.

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

1-43: LGTM! Formatting changes align with style consistency.

The import formatting and conversion of string literals to double quotes improve consistency across the codebase. No functional changes detected.


45-98: LGTM! Handler logic remains intact.

Formatting adjustments to state management and event handlers maintain functional equivalence with improved readability.

frontend/src/components/Sidebar.tsx (1)

41-45: LGTM!

The new "Team Debates" navigation item is cleanly integrated, uses the already-imported Users icon, and follows the existing NavItem pattern consistently. The placement between Tournaments and Leaderboard is logical.

backend/go.mod (1)

3-9: Confirm toolchain availability in CI/CD.

Setting go 1.24 plus toolchain go1.24.4 will force every build (including CI runners and developer laptops) to download that exact toolchain. Please double-check that your build agents already support the Go toolchain directive and that the named release exists; otherwise builds will start failing with “unsupported go version”. If your infra isn’t ready yet, consider landing this change in a dedicated upgrade PR with the necessary CI updates.

backend/services/personalities.go (1)

1-1028: LGTM! Formatting improvements enhance readability.

The changes consist solely of field alignment and whitespace adjustments to improve code readability. No functional logic has been altered.

backend/config/config.go (1)

50-54: LGTM! Redis configuration follows existing patterns.

The Redis configuration block is properly structured with YAML tags and follows the existing configuration model conventions.

frontend/src/services/vsbot.ts (1)

1-121: LGTM! Debug logging cleanup.

The removal of console.log statements and addition of a trailing newline improves code cleanliness without affecting functionality.

frontend/src/Pages/MatchLogs.tsx (1)

121-244: Formatting improvements look good.

The line breaks and className adjustments improve code readability without affecting functionality.

frontend/src/components/RoomBrowser.tsx (1)

84-85: Route verification confirmed.

The /view-debate/:debateID route is properly configured in frontend/src/App.tsx (line 92) and correctly maps to the ViewDebate component. The navigation change in RoomBrowser.tsx (line 85) aligns with this routing setup.

frontend/package.json (1)

26-45: No security vulnerabilities detected in new dependencies.

The npm audit confirms "found 0 vulnerabilities" across all six newly added packages. Version checks show most dependencies are at their latest releases (@types/react-dnd is intentionally pinned to v2, likely for compatibility). No further action required—the dependencies are safe to use.

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: 13

♻️ Duplicate comments (5)
backend/websocket/websocket.go (1)

166-172: PII leak: Remove email from participant broadcasts.

The email field at line 169 is broadcast to all clients including spectators via broadcastParticipants, exposing participant emails to spectators. This was flagged in a previous review but remains unfixed.

Apply this diff to remove the email field:

 		participants = append(participants, map[string]interface{}{
 			"id":          client.UserID,
 			"displayName": client.Username,
-			"email":       client.Email,
 			"role":        client.Role,
 			"isMuted":     client.IsMuted,
 		})
frontend/src/components/TeamMatchmaking.tsx (1)

72-98: Check the actual TeamDebate payload instead of fake flags.

getActiveTeamDebate returns a TeamDebate object (id, status, …); it never sets hasActiveDebate or debateId, so this branch never sees a live debate and the notification never fires.

Apply this diff:

-        const result: any = await getActiveTeamDebate(team.id);
-        if (result.hasActiveDebate && result.debateId) {
-          if (activeDebateId !== result.debateId) {
-            setActiveDebateId(result.debateId);
+        const result = await getActiveTeamDebate(team.id);
+        if (result && result.id && result.status === "active") {
+          if (activeDebateId !== result.id) {
+            setActiveDebateId(result.id);
             setShowDebateNotification(true);
           }
         } else {
frontend/src/Pages/TeamDebateRoom.tsx (3)

256-259: Stop the permanent loading spinner.

When there’s no debateId or auth token we still bail out early, but isLoading never flips back to false, so the UI sits on “Loading debate…” forever. Clear the flag before returning.

Apply this diff:

       if (!debateId || (!token && !currentUser?.id)) {
+        setIsLoading(false);
         return;
       }

883-905: Stop posting transcripts to localhost.

The transcript submission still hits http://localhost:1313/..., so once deployed the judgment call never reaches the backend. Build the base URL from VITE_API_URL (fallback: current origin) and reuse it here.

Apply this diff:

-      const response = await fetch(`http://localhost:1313/submit-transcripts`, {
+      const rawBaseUrl =
+        import.meta.env.VITE_API_URL ??
+        `${window.location.protocol}//${window.location.host}`;
+      const baseUrl = rawBaseUrl.replace(/\/+$/, "");
+      const response = await fetch(`${baseUrl}/submit-transcripts`, {
         method: "POST",
         headers: {
           "Content-Type": "application/json",
           Authorization: `Bearer ${token}`,
         },

385-405: Use the configured API host for real-time sockets.

This WebSocket is hard-wired to ws://localhost:1313, so production clients disconnect immediately. Derive the host from VITE_API_URL (or fall back to the current origin) and encode the token before connecting.

Apply this diff:

-    const ws = new WebSocket(
-      `ws://localhost:1313/ws/team?debateId=${debateId}&token=${token}`
-    );
+    const apiUrl = import.meta.env.VITE_API_URL;
+    const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+    let host = window.location.host;
+    if (apiUrl) {
+      try {
+        host = new URL(apiUrl).host;
+      } catch {
+        host = apiUrl.replace(/^https?:\/\//, "");
+      }
+    }
+    const ws = new WebSocket(
+      `${wsProtocol}//${host}/ws/team?debateId=${debateId}&token=${encodeURIComponent(
+        token
+      )}`
+    );
🧹 Nitpick comments (2)
backend/websocket/team_websocket.go (2)

113-113: Consider making the config path configurable.

The config path is hardcoded to "./config/config.prod.yml". For better flexibility across environments (dev, staging, prod), consider passing this as a parameter or loading from an environment variable.


604-756: Consider breaking down this complex function.

handleTeamReadyStatus is 150+ lines and handles multiple concerns: ready status assignment, counting, broadcasting, and countdown logic. While the implementation is correct (mutex is held during all critical sections), splitting into smaller helper functions would improve maintainability:

  • assignReadyStatus
  • countTeamReadiness
  • broadcastReadyUpdate
  • maybeStartCountdown
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2fc4e9 and 3320615.

📒 Files selected for processing (12)
  • backend/services/team_turn_service.go (1 hunks)
  • backend/websocket/team_websocket.go (1 hunks)
  • backend/websocket/websocket.go (11 hunks)
  • frontend/src/Pages/OnlineDebateRoom.tsx (35 hunks)
  • frontend/src/Pages/TeamBuilder.tsx (1 hunks)
  • frontend/src/Pages/TeamDebateRoom.tsx (1 hunks)
  • frontend/src/Pages/ViewDebate.tsx (1 hunks)
  • frontend/src/components/ReactionBar.tsx (1 hunks)
  • frontend/src/components/TeamMatchmaking.tsx (1 hunks)
  • frontend/src/hooks/useDebateWS.ts (1 hunks)
  • frontend/src/services/teamDebateService.ts (1 hunks)
  • frontend/src/services/teamService.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/services/teamService.ts
🧰 Additional context used
🧬 Code graph analysis (11)
backend/websocket/team_websocket.go (5)
backend/services/team_turn_service.go (5)
  • TeamTurnManager (132-136)
  • TokenBucket (22-28)
  • TokenBucketService (16-19)
  • NewTeamTurnManager (139-144)
  • NewTokenBucketService (31-35)
backend/websocket/websocket.go (1)
  • Room (29-32)
backend/utils/auth.go (1)
  • ValidateTokenAndFetchEmail (127-140)
backend/db/db.go (1)
  • MongoDatabase (16-16)
backend/models/team.go (2)
  • TeamDebate (61-81)
  • Team (11-22)
frontend/src/hooks/useDebateWS.ts (2)
backend/internal/debate/events.go (1)
  • Event (9-13)
frontend/src/atoms/debateAtoms.ts (9)
  • wsAtom (4-4)
  • debateIdAtom (7-7)
  • pollStateAtom (18-18)
  • questionsAtom (52-52)
  • reactionsAtom (55-55)
  • wsStatusAtom (58-58)
  • presenceAtom (64-64)
  • spectatorHashAtom (33-46)
  • PollInfo (9-15)
backend/websocket/websocket.go (2)
backend/structs/websocket.go (2)
  • Room (30-37)
  • Message (10-13)
backend/utils/auth.go (1)
  • ValidateTokenAndFetchEmail (127-140)
backend/services/team_turn_service.go (2)
backend/db/db.go (1)
  • GetCollection (20-22)
backend/models/team.go (1)
  • Team (11-22)
frontend/src/Pages/TeamBuilder.tsx (3)
frontend/src/services/teamService.ts (13)
  • TeamMember (18-25)
  • Team (5-16)
  • getAvailableTeams (143-156)
  • getUserTeams (127-140)
  • createTeam (62-78)
  • joinTeam (97-109)
  • getTeamMemberProfile (309-322)
  • removeMember (277-290)
  • deleteTeam (293-306)
  • updateTeamName (325-340)
  • updateTeamSize (359-374)
  • leaveTeam (112-124)
  • getTeamByCode (343-356)
backend/models/team.go (2)
  • TeamMember (25-32)
  • Team (11-22)
frontend/src/state/userAtom.ts (1)
  • userAtom (4-4)
frontend/src/components/ReactionBar.tsx (2)
frontend/src/atoms/debateAtoms.ts (2)
  • debateIdAtom (7-7)
  • spectatorHashAtom (33-46)
frontend/src/hooks/useDebateWS.ts (1)
  • useDebateWS (22-305)
frontend/src/components/TeamMatchmaking.tsx (4)
frontend/src/services/teamService.ts (2)
  • Team (5-16)
  • createTeamDebate (159-177)
backend/models/team.go (1)
  • Team (11-22)
backend/models/user.go (1)
  • User (10-30)
frontend/src/services/teamDebateService.ts (5)
  • getActiveTeamDebate (95-108)
  • getMatchmakingStatus (156-171)
  • joinMatchmaking (111-124)
  • leaveMatchmaking (126-139)
  • createTeamDebate (55-76)
frontend/src/Pages/ViewDebate.tsx (5)
frontend/src/atoms/debateAtoms.ts (6)
  • debateIdAtom (7-7)
  • pollStateAtom (18-18)
  • questionsAtom (52-52)
  • reactionsAtom (55-55)
  • wsStatusAtom (58-58)
  • presenceAtom (64-64)
frontend/src/hooks/useDebateWS.ts (1)
  • useDebateWS (22-305)
frontend/src/utils/auth.ts (1)
  • getAuthToken (5-7)
frontend/src/components/AnonymousQA.tsx (1)
  • AnonymousQA (9-161)
frontend/src/components/ReactionBar.tsx (1)
  • ReactionBar (52-88)
frontend/src/Pages/TeamDebateRoom.tsx (4)
frontend/src/services/teamService.ts (2)
  • TeamMember (18-25)
  • getTeamDebate (180-193)
frontend/src/hooks/useUser.ts (1)
  • useUser (7-59)
frontend/src/utils/auth.ts (1)
  • getAuthToken (5-7)
frontend/src/services/teamDebateService.ts (1)
  • getTeamDebate (79-92)
frontend/src/services/teamDebateService.ts (3)
frontend/src/utils/auth.ts (1)
  • getAuthToken (5-7)
frontend/src/services/teamService.ts (5)
  • TeamDebate (27-41)
  • TeamMember (18-25)
  • Team (5-16)
  • createTeamDebate (159-177)
  • getTeamDebate (180-193)
backend/models/team.go (3)
  • TeamDebate (61-81)
  • TeamMember (25-32)
  • Team (11-22)
frontend/src/Pages/OnlineDebateRoom.tsx (3)
frontend/src/hooks/useDebateWS.ts (1)
  • useDebateWS (22-305)
frontend/src/atoms/debateAtoms.ts (4)
  • questionsAtom (52-52)
  • presenceAtom (64-64)
  • pollStateAtom (18-18)
  • PollInfo (9-15)
frontend/src/utils/auth.ts (1)
  • getAuthToken (5-7)
🪛 Biome (2.1.2)
frontend/src/hooks/useDebateWS.ts

[error] 265-265: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

frontend/src/Pages/TeamDebateRoom.tsx

[error] 481-481: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 482-483: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 484-484: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 550-550: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 581-581: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 582-582: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 596-596: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 597-599: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 600-602: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 603-603: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔇 Additional comments (16)
frontend/src/components/ReactionBar.tsx (3)

1-7: LGTM! Clean imports and reaction set.

The imports are appropriate and the REACTIONS array provides a good variety of emojis for user engagement.


14-50: Nice animation implementation with good accessibility.

The spring animation provides smooth visual feedback, and the aria-label ensures the buttons are accessible to screen readers.


73-87: Clean rendering with good positioning and dark mode support.

The fixed bottom-center positioning is appropriate for a reaction bar, and the dark mode classes ensure good UX across themes.

backend/services/team_turn_service.go (5)

102-108: Data race fixed—exclusive lock is now correct.

The previous critical issue where GetRemainingTokens held a read lock while mutating via refillTokens has been resolved. The function now correctly acquires bucket.Mutex.Lock() (exclusive lock) before refilling, preventing concurrent access issues.


64-88: LGTM—concurrency handling is correct.

The two-level locking (map-level RLock at line 68, then bucket-level Lock at line 76) is appropriate. Token refill and consumption are properly protected by the exclusive bucket lock.


185-220: LGTM—turn advancement logic is sound.

The circular turn advancement (line 215) is correct, and error handling for missing team/turn state returns primitive.NilObjectID as expected. Linear search through turnOrder is acceptable for typical team sizes.


111-124: LGTM—refill logic is correct.

The time-based token refill correctly caps at Capacity and only updates LastRefill when tokens are added. Note that sub-second elapsed time is truncated (line 115), so tokens refill in 1-second increments, which is acceptable for this use case.


15-35: LGTM—two-level locking design is appropriate.

The service-level mutex (line 18) protects the buckets map, while each TokenBucket has its own Mutex (line 27) for fine-grained concurrency control. This allows parallel operations on different buckets while still protecting the map structure.

backend/websocket/team_websocket.go (4)

222-231: Excellent defense-in-depth validation.

This redundant check ensures userTeamID matches one of the debate teams even if earlier logic has bugs. Good use of fail-safe validation before creating the client.


723-751: Verify goroutine safety after room deletion.

The goroutine sleeps for 3 seconds before acquiring locks. If all clients disconnect during this window, the room will be deleted (lines 322-326), but the goroutine still runs. The check at line 730 (if !stillExists) handles this, which is good.

The direct iteration over room.Clients at lines 743-747 is safe because room.Mutex is held during the loop.


401-401: Past review concern addressed.

Good fix! All handlers now use snapshotTeamRecipients to safely iterate clients instead of directly accessing room.Clients, addressing the concurrent map iteration issue from the previous review.


758-803: LGTM: Token bucket integration.

The turn request logic correctly checks CanUserSpeak before granting permission and updates the client with remaining tokens. Good integration with the token bucket service.

backend/websocket/websocket.go (4)

607-626: Past review concern addressed: double-unlock fixed.

Good fix! Using defer room.Mutex.Unlock() at line 610 ensures the mutex is released exactly once on all return paths, resolving the double-unlock panic from the previous review.


282-334: LGTM: Spectator join flow.

The spectator connection logic correctly:

  • Bypasses the 2-debater limit for spectators
  • Assigns unique ConnectionID via uuid.New()
  • Sends participant snapshot to new connection
  • Notifies other clients of spectator join

340-370: LGTM: Disconnect handling with spectator support.

The disconnect logic properly handles spectators by:

  • Detecting spectator disconnections (line 361)
  • Notifying debaters via notifySpectatorStatus (line 363)
  • Broadcasting updated participant counts (line 368)

116-224: Well-structured spectator helpers.

The new helper functions properly use mutex locking and provide clear separation of concerns for spectator vs. debater management. Good design for maintainability.

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: 5

♻️ Duplicate comments (11)
frontend/src/Pages/OnlineDebateRoom.tsx (2)

319-329: Resolved: ICE candidate payload now uses live userId.

The candidate payload reads userId from the ref-sourced local variable at send time. This fixes the stale-closure bug.


560-567: Resolved: judgment polling interval is cleared on unmount.

Cleanup added for judgePollRef prevents leaks and setState-on-unmounted warnings.

frontend/src/hooks/useDebateWS.ts (2)

39-49: Resolved: WS host now derived from VITE_API_URL/current origin.

Good fallback and protocol handling.


268-272: Resolved: presence case wrapped in a block.

Matches linter expectations and prevents scope bleed.

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

83-96: Resolved: WS host configurable and token encoded.

This unblocks deployed environments.


314-339: Resolved: send the resolved connectionId in answer payload.

Fix ensures SDP handshakes complete.

frontend/src/Pages/TeamDebateRoom.tsx (4)

257-259: Stop the endless spinner on early return.

Set isLoading to false before returning when debateId/auth is missing.

-      if (!debateId || (!token && !currentUser?.id)) {
-        return;
-      }
+      if (!debateId || (!token && !currentUser?.id)) {
+        setIsLoading(false);
+        return;
+      }

887-901: Make submit-transcripts base URL configurable.

Same localhost issue for HTTP; derive from VITE_API_URL/current origin.

-      const response = await fetch(`http://localhost:1313/submit-transcripts`, {
+      const baseUrl =
+        import.meta.env.VITE_API_URL ??
+        `${window.location.protocol}//${window.location.host}`;
+      const response = await fetch(`${baseUrl}/submit-transcripts`, {
         method: "POST",
         headers: {
           "Content-Type": "application/json",
           Authorization: `Bearer ${token}`,
         },
         body: JSON.stringify({
           roomId: debateId,
           role: localRole,
           transcripts: debateTranscripts,
         }),
       });

390-392: Don’t hard‑code WS host; use env/origin and encode params.

Use VITE_API_URL or current origin and URL‑encode debateId/token.

-    const ws = new WebSocket(
-      `ws://localhost:1313/ws/team?debateId=${debateId}&token=${token}`
-    );
+    const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+    const apiUrl = import.meta.env.VITE_API_URL;
+    let host = window.location.host;
+    if (apiUrl) {
+      try { host = new URL(apiUrl).host; } catch { host = apiUrl.replace(/^https?:\/\//, ""); }
+    }
+    const ws = new WebSocket(
+      `${protocol}//${host}/ws/team?debateId=${encodeURIComponent(debateId)}&token=${encodeURIComponent(token)}`
+    );

642-657: Use the latest team flag inside WS handler to avoid stale reads.

Replace isTeam1 with isTeam1Latest in teamStatus handling.

-          if (data.team1Members) {
-            if (isTeam1) {
+          if (data.team1Members) {
+            if (isTeam1Latest) {
               setMyTeamMembers(data.team1Members);
             } else {
               setOpponentTeamMembers(data.team1Members);
             }
           }
-          if (data.team2Members) {
-            if (isTeam1) {
+          if (data.team2Members) {
+            if (isTeam1Latest) {
               setOpponentTeamMembers(data.team2Members);
             } else {
               setMyTeamMembers(data.team2Members);
             }
           }
backend/websocket/websocket.go (1)

166-172: Stop leaking participant emails to spectators. Line 166 still injects client.Email into the participant payload that you broadcast to every connection. Because snapshotRecipients includes spectators, anyone joining the room can read every debater’s email—this is the same privacy/compliance defect flagged earlier. Strip the email before broadcasting (or send it only over a non-spectator channel) so PII never leaves authorized recipients.

@@
-		participants = append(participants, map[string]interface{}{
-			"id":          client.UserID,
-			"displayName": client.Username,
-			"email":       client.Email,
-			"role":        client.Role,
-			"isMuted":     client.IsMuted,
-		})
+		participant := map[string]interface{}{
+			"id":          client.UserID,
+			"displayName": client.Username,
+			"role":        client.Role,
+			"isMuted":     client.IsMuted,
+		}
+		participants = append(participants, participant)
🧹 Nitpick comments (4)
frontend/src/Pages/OnlineDebateRoom.tsx (2)

1320-1330: Handle rejected video.play() promises.

Avoid unhandled promise rejections when autoplay is blocked.

-      localVideoRef.current.play();
+      localVideoRef.current.play().catch(() => {});
@@
-      remoteVideoRef.current.play();
+      remoteVideoRef.current.play().catch(() => {});

599-603: Verify necessity of sending opponentEmail (PII) to backend.

Emails are PII; avoid transmitting unless strictly required server-side. If an ID suffices, drop email from the payload.

Would you like a follow-up patch that removes opponentEmail and uses opponentId only?

Also applies to: 721-723

frontend/src/hooks/useDebateWS.ts (2)

58-59: Encode path/query parts in wsUrl.

Protect against special characters in debateId/spectatorId.

-    const wsUrl = `${protocol}//${host}/ws/debate/${debateId}${spectatorId ? `?spectatorId=${spectatorId}` : ''}`;
+    const wsUrl = `${protocol}//${host}/ws/debate/${encodeURIComponent(debateId)}${
+      spectatorId ? `?spectatorId=${encodeURIComponent(spectatorId)}` : ""
+    }`;

289-297: Reset atoms on unmount to avoid stale UI after navigating away.

Clear debateId, pollState, questions, reactions, and presence in the cleanup.

     return () => {
       if (rws) {
         rws.close();
         wsRef.current = null;
         setWs(null);
         setWsStatus('disconnected');
+        setDebateId(null);
+        setPollState({});
+        setQuestions([]);
+        setReactions([]);
+        setPresence(0);
       }
     };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3320615 and b24b6f9.

📒 Files selected for processing (8)
  • backend/services/team_turn_service.go (1 hunks)
  • backend/websocket/team_websocket.go (1 hunks)
  • backend/websocket/websocket.go (11 hunks)
  • frontend/src/Pages/OnlineDebateRoom.tsx (35 hunks)
  • frontend/src/Pages/TeamDebateRoom.tsx (1 hunks)
  • frontend/src/Pages/ViewDebate.tsx (1 hunks)
  • frontend/src/components/ReactionBar.tsx (1 hunks)
  • frontend/src/hooks/useDebateWS.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/components/ReactionBar.tsx
🧰 Additional context used
🧬 Code graph analysis (7)
frontend/src/hooks/useDebateWS.ts (2)
backend/internal/debate/events.go (1)
  • Event (9-13)
frontend/src/atoms/debateAtoms.ts (9)
  • wsAtom (4-4)
  • debateIdAtom (7-7)
  • pollStateAtom (18-18)
  • questionsAtom (52-52)
  • reactionsAtom (55-55)
  • wsStatusAtom (58-58)
  • presenceAtom (64-64)
  • spectatorHashAtom (33-46)
  • PollInfo (9-15)
frontend/src/Pages/ViewDebate.tsx (5)
frontend/src/atoms/debateAtoms.ts (6)
  • debateIdAtom (7-7)
  • pollStateAtom (18-18)
  • questionsAtom (52-52)
  • reactionsAtom (55-55)
  • wsStatusAtom (58-58)
  • presenceAtom (64-64)
frontend/src/hooks/useDebateWS.ts (1)
  • useDebateWS (22-310)
frontend/src/utils/auth.ts (1)
  • getAuthToken (5-7)
frontend/src/components/AnonymousQA.tsx (1)
  • AnonymousQA (9-161)
frontend/src/components/ReactionBar.tsx (1)
  • ReactionBar (52-91)
backend/websocket/team_websocket.go (4)
backend/services/team_turn_service.go (5)
  • TeamTurnManager (155-159)
  • TokenBucket (28-34)
  • TokenBucketService (22-25)
  • NewTeamTurnManager (162-167)
  • NewTokenBucketService (37-41)
backend/utils/auth.go (1)
  • ValidateTokenAndFetchEmail (127-140)
backend/db/db.go (1)
  • MongoDatabase (16-16)
backend/models/team.go (2)
  • TeamDebate (61-81)
  • Team (11-22)
backend/websocket/websocket.go (2)
backend/structs/websocket.go (2)
  • Room (30-37)
  • Message (10-13)
backend/utils/auth.go (1)
  • ValidateTokenAndFetchEmail (127-140)
backend/services/team_turn_service.go (2)
backend/db/db.go (1)
  • GetCollection (20-22)
backend/models/team.go (1)
  • Team (11-22)
frontend/src/Pages/OnlineDebateRoom.tsx (3)
frontend/src/hooks/useDebateWS.ts (1)
  • useDebateWS (22-310)
frontend/src/atoms/debateAtoms.ts (4)
  • questionsAtom (52-52)
  • presenceAtom (64-64)
  • pollStateAtom (18-18)
  • PollInfo (9-15)
frontend/src/utils/auth.ts (1)
  • getAuthToken (5-7)
frontend/src/Pages/TeamDebateRoom.tsx (4)
frontend/src/services/teamService.ts (2)
  • TeamMember (18-25)
  • getTeamDebate (180-193)
frontend/src/hooks/useUser.ts (1)
  • useUser (7-59)
frontend/src/utils/auth.ts (1)
  • getAuthToken (5-7)
frontend/src/services/teamDebateService.ts (1)
  • getTeamDebate (79-92)
🪛 Biome (2.1.2)
frontend/src/Pages/TeamDebateRoom.tsx

[error] 487-487: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 488-489: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 490-490: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 556-556: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 587-587: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 588-588: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 602-602: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 603-605: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 606-608: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 609-609: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

Comment on lines 188 to 212
turnManager := services.NewTeamTurnManager()
tokenBucket := services.NewTokenBucketService()

// Initialize turn management for both teams
turnManager.InitializeTeamTurns(debate.Team1ID)
turnManager.InitializeTeamTurns(debate.Team2ID)

// Initialize token buckets for both teams
tokenBucket.InitializeTeamBuckets(debate.Team1ID)
tokenBucket.InitializeTeamBuckets(debate.Team2ID)

teamRooms[roomKey] = &TeamRoom{
Clients: make(map[*websocket.Conn]*TeamClient),
Team1ID: debate.Team1ID,
Team2ID: debate.Team2ID,
DebateID: debateObjectID,
TurnManager: turnManager,
TokenBucket: tokenBucket,
CurrentTopic: debate.Topic,
CurrentPhase: "setup",
Team1Role: debate.Team1Stance,
Team2Role: debate.Team2Stance,
Team1Ready: make(map[string]bool),
Team2Ready: make(map[string]bool),
}
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

Handle turn/token initialization failures. Lines 192-209 drop the errors from InitializeTeamTurns and InitializeTeamBuckets. If the DB lookup hiccups or a team has no members, you proceed with empty turn order and missing buckets, so GetCurrentTurn returns Nil and every TryConsumeForSpeaking denies the speaker. Fail fast while you still own the HTTP response instead of entering the WS loop in a broken state.

@@
-		turnManager := services.NewTeamTurnManager()
-		tokenBucket := services.NewTokenBucketService()
-
-		// Initialize turn management for both teams
-		turnManager.InitializeTeamTurns(debate.Team1ID)
-		turnManager.InitializeTeamTurns(debate.Team2ID)
-
-		// Initialize token buckets for both teams
-		tokenBucket.InitializeTeamBuckets(debate.Team1ID)
-		tokenBucket.InitializeTeamBuckets(debate.Team2ID)
+		turnManager := services.NewTeamTurnManager()
+		if err := turnManager.InitializeTeamTurns(debate.Team1ID); err != nil {
+			teamRoomsMutex.Unlock()
+			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to initialize turn order for team1"})
+			return
+		}
+		if err := turnManager.InitializeTeamTurns(debate.Team2ID); err != nil {
+			teamRoomsMutex.Unlock()
+			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to initialize turn order for team2"})
+			return
+		}
+
+		tokenBucket := services.NewTokenBucketService()
+		if err := tokenBucket.InitializeTeamBuckets(debate.Team1ID); err != nil {
+			teamRoomsMutex.Unlock()
+			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to initialize speaking tokens for team1"})
+			return
+		}
+		if err := tokenBucket.InitializeTeamBuckets(debate.Team2ID); err != nil {
+			teamRoomsMutex.Unlock()
+			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to initialize speaking tokens for team2"})
+			return
+		}
🤖 Prompt for AI Agents
In backend/websocket/team_websocket.go around lines 188 to 212, the code
currently ignores errors from InitializeTeamTurns and InitializeTeamBuckets
which can leave the room in a broken state; update those service method calls to
check returned errors (and any returned status indicating empty results), and if
any initialization fails or yields empty/no members, log the detailed error,
write an HTTP error response (e.g. 500 with a clear message) and abort creating
the TeamRoom (do not assign teamRooms[roomKey] or enter the WS loop). Ensure the
Initialize* functions return errors if they don’t already, propagate those
errors here, and only create and store the TeamRoom when all initializations
succeed and validated (non-empty turn order and token buckets present).

Comment on lines 497 to 507
const pollResponse = await fetch(
`http://localhost:1313/submit-transcripts`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ roomId, role, transcripts: {} }),
}
);
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

Make submit-transcripts base URL configurable.

Use VITE_API_URL (or current origin) instead of http://localhost:1313 and avoid plain host leakage.

-        const pollResponse = await fetch(
-          `http://localhost:1313/submit-transcripts`,
-          {
+        const baseUrl =
+          import.meta.env.VITE_API_URL ??
+          `${window.location.protocol}//${window.location.host}`;
+        const pollResponse = await fetch(`${baseUrl}/submit-transcripts`, {
             method: "POST",
             headers: {
               "Content-Type": "application/json",
               Authorization: `Bearer ${token}`,
             },
             body: JSON.stringify({ roomId, role, transcripts: {} }),
-          }
-        );
+          });
-      const response = await fetch(`http://localhost:1313/submit-transcripts`, {
+      const baseUrl =
+        import.meta.env.VITE_API_URL ??
+        `${window.location.protocol}//${window.location.host}`;
+      const response = await fetch(`${baseUrl}/submit-transcripts`, {
         method: "POST",
         headers: {
           "Content-Type": "application/json",
           Authorization: `Bearer ${token}`,
         },
         body: JSON.stringify({
           roomId,
           role,
           transcripts,
           opponentRole,
           opponentId,
           opponentEmail,
           opponentTranscripts,
         }),
       });

Also applies to: 589-606

🤖 Prompt for AI Agents
In frontend/src/Pages/OnlineDebateRoom.tsx around lines 497-507 (and likewise
update lines 589-606), the request is hardcoded to http://localhost:1313 which
leaks the host; replace the hardcoded URL with a configurable base derived from
import.meta.env.VITE_API_URL fallback to window.location.origin (or an agreed
runtime config), then build the full endpoint by concatenating the base and
"/submit-transcripts" so the fetch uses `${base}/submit-transcripts`; ensure you
handle missing trailing slashes when joining and keep using the same
headers/body logic.

Comment on lines +1018 to +1026
const wsUrl = `ws://localhost:1313/ws?room=${roomId}&token=${token}`;
const rws = new ReconnectingWebSocket(wsUrl, [], {
connectionTimeout: 4000,
maxRetries: Infinity,
maxReconnectionDelay: 10000,
minReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1.3,
});
wsRef.current = rws;
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

Stop hard-coding WS host; derive from env/origin and URL‑encode the token.

Build the ws URL from VITE_API_URL (fallback to current origin) and encode query params.

-    const wsUrl = `ws://localhost:1313/ws?room=${roomId}&token=${token}`;
-    const rws = new ReconnectingWebSocket(wsUrl, [], {
+    const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+    const apiUrl = import.meta.env.VITE_API_URL;
+    let host = window.location.host;
+    if (apiUrl) {
+      try { host = new URL(apiUrl).host; } catch { host = apiUrl.replace(/^https?:\/\//, ""); }
+    }
+    const wsUrl = `${protocol}//${host}/ws?room=${encodeURIComponent(roomId!)}&token=${encodeURIComponent(token)}`;
+    const rws = new ReconnectingWebSocket(wsUrl, [], {
       connectionTimeout: 4000,
       maxRetries: Infinity,
       maxReconnectionDelay: 10000,
       minReconnectionDelay: 1000,
       reconnectionDelayGrowFactor: 1.3,
     });
📝 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
const wsUrl = `ws://localhost:1313/ws?room=${roomId}&token=${token}`;
const rws = new ReconnectingWebSocket(wsUrl, [], {
connectionTimeout: 4000,
maxRetries: Infinity,
maxReconnectionDelay: 10000,
minReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1.3,
});
wsRef.current = rws;
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const apiUrl = import.meta.env.VITE_API_URL;
let host = window.location.host;
if (apiUrl) {
try { host = new URL(apiUrl).host; } catch { host = apiUrl.replace(/^https?:\/\//, ""); }
}
const wsUrl = `${protocol}//${host}/ws?room=${encodeURIComponent(roomId!)}&token=${encodeURIComponent(token)}`;
const rws = new ReconnectingWebSocket(wsUrl, [], {
connectionTimeout: 4000,
maxRetries: Infinity,
maxReconnectionDelay: 10000,
minReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1.3,
});
wsRef.current = rws;
🤖 Prompt for AI Agents
In frontend/src/Pages/OnlineDebateRoom.tsx around lines 1018 to 1026, the
WebSocket URL is hard-coded to ws://localhost:1313 and the token/query params
are not URL-encoded; change it to derive the base API URL from
import.meta.env.VITE_API_URL (falling back to window.location.origin), normalize
its protocol to ws or wss based on the origin (or swap http->ws, https->wss),
then build the socket URL by URL-encoding roomId and token with
encodeURIComponent when constructing the query string so no raw values or
hard-coded host/port remain.

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: 3

Caution

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

⚠️ Outside diff range comments (3)
frontend/src/Pages/OnlineDebateRoom.tsx (2)

870-880: Replace hard-coded localhost in room creation.

The room creation endpoint uses http://localhost:1313 which won't work in deployed environments.

Apply this diff:

+      const baseUrl =
+        import.meta.env.VITE_API_URL ??
+        `${window.location.protocol}//${window.location.host}`;
       const response = await fetch(
-        `http://localhost:1313/rooms`,
+        `${baseUrl}/rooms`,
         {

902-909: Replace hard-coded localhost in participants fetch.

The participants fetch endpoint uses http://localhost:1313 which won't work in deployed environments.

Apply this diff:

+        const baseUrl =
+          import.meta.env.VITE_API_URL ??
+          `${window.location.protocol}//${window.location.host}`;
         const response = await fetch(
-          `http://localhost:1313/rooms/${roomId}/participants`,
+          `${baseUrl}/rooms/${roomId}/participants`,
           {
backend/websocket/team_websocket.go (1)

188-197: Add error handling for initialization calls—currently errors are silently ignored.

Both InitializeTeamTurns and InitializeTeamBuckets return errors, but the code at lines 188–197 discards these returns. This allows the WebSocket handler to proceed with broken state if initialization fails. Check errors after each call and return early if any fail.

Example pattern:

if err := turnManager.InitializeTeamTurns(debate.Team1ID); err != nil {
    // log error and return
}
♻️ Duplicate comments (7)
frontend/src/Pages/TeamBuilder.tsx (1)

403-412: Unresolved: Feedback still hidden after joining a team.

The error/success alerts remain inside the Create Team card (which disappears when isUserInTeam is true), so feedback from rename, resize, remove member, copy code, leave, and delete operations won't be visible to users who are already in a team.

backend/internal/debate/poll_store.go (1)

121-126: Validate the option before incrementing the vote count.

HIncrBy will create a new hash field if the option doesn't exist, allowing spectators to vote for arbitrary strings. This corrupts poll results. The backend should verify the option exists in the poll's valid options before incrementing.

Apply this diff to validate the option:

+	// Verify option exists in poll metadata
+	metaKey := fmt.Sprintf("debate:%s:poll:%s:meta", debateID, pollID)
+	metaStr, err := ps.rdb.Get(ps.ctx, metaKey).Result()
+	if err != nil {
+		ps.rdb.SRem(ps.ctx, votersKey, spectatorHash)
+		return false, fmt.Errorf("failed to retrieve poll metadata: %w", err)
+	}
+	
+	var meta PollMetadata
+	if err := json.Unmarshal([]byte(metaStr), &meta); err != nil {
+		ps.rdb.SRem(ps.ctx, votersKey, spectatorHash)
+		return false, fmt.Errorf("failed to parse poll metadata: %w", err)
+	}
+	
+	// Check if option is valid
+	validOption := false
+	for _, opt := range meta.Options {
+		if opt == option {
+			validOption = true
+			break
+		}
+	}
+	if !validOption {
+		ps.rdb.SRem(ps.ctx, votersKey, spectatorHash)
+		return false, fmt.Errorf("invalid option: %s", option)
+	}
+
 	// Increment poll count atomically
 	if err := ps.rdb.HIncrBy(ps.ctx, countsKey, option, 1).Err(); err != nil {
 		// Rollback voter add
 		ps.rdb.SRem(ps.ctx, votersKey, spectatorHash)
 		return false, fmt.Errorf("failed to increment count: %w", err)
 	}

Based on learnings

frontend/src/Pages/TeamDebateRoom.tsx (1)

409-411: Replace hard-coded localhost with environment-aware WebSocket URL.

The WebSocket URL is hard-coded to ws://localhost:1313, which will fail in staging/production environments. Build the URL from VITE_API_URL (or fallback to window.location.host) and select protocol based on page protocol.

Apply this diff:

-    const ws = new WebSocket(
-      `ws://localhost:1313/ws/team?debateId=${debateId}&token=${token}`
-    );
+    const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+    const apiUrl = import.meta.env.VITE_API_URL;
+    let host = window.location.host;
+    if (apiUrl) {
+      try {
+        host = new URL(apiUrl).host;
+      } catch {
+        host = apiUrl.replace(/^https?:\/\//, "");
+      }
+    }
+    const ws = new WebSocket(
+      `${protocol}//${host}/ws/team?debateId=${debateId}&token=${token}`
+    );
frontend/src/Pages/OnlineDebateRoom.tsx (3)

511-520: Replace hard-coded localhost in judgment polling.

The polling endpoint uses http://localhost:1313 which won't work in deployed environments.

Apply this diff:

+        const baseUrl =
+          import.meta.env.VITE_API_URL ??
+          `${window.location.protocol}//${window.location.host}`;
         const pollResponse = await fetch(
-          `http://localhost:1313/submit-transcripts`,
+          `${baseUrl}/submit-transcripts`,
           {
             method: "POST",

597-615: Replace hard-coded localhost in transcript submission.

The transcript submission endpoint uses http://localhost:1313 which won't work in deployed environments.

Apply this diff:

+      const baseUrl =
+        import.meta.env.VITE_API_URL ??
+        `${window.location.protocol}//${window.location.host}`;
       const response = await fetch(
-        `http://localhost:1313/submit-transcripts`,
+        `${baseUrl}/submit-transcripts`,
         {
           method: "POST",

1087-1094: Replace hard-coded WebSocket URL with environment-aware construction.

The WebSocket URL uses ws://localhost:1313 which won't work in deployed environments. Build from VITE_API_URL or current origin, and URL-encode query parameters.

Apply this diff:

-    const wsUrl = `ws://localhost:1313/ws?room=${roomId}&token=${token}`;
+    const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+    const apiUrl = import.meta.env.VITE_API_URL;
+    let host = window.location.host;
+    if (apiUrl) {
+      try {
+        host = new URL(apiUrl).host;
+      } catch {
+        host = apiUrl.replace(/^https?:\/\//, "");
+      }
+    }
+    const wsUrl = `${protocol}//${host}/ws?room=${encodeURIComponent(
+      roomId!
+    )}&token=${encodeURIComponent(token)}`;
     const rws = new ReconnectingWebSocket(wsUrl, [], {
backend/websocket/websocket.go (1)

169-175: Remove email from participant broadcast to prevent PII leakage.

The buildParticipantsMessage function includes "email": client.Email in the participant data that is broadcast to all clients, including spectators. This exposes debater emails to anyone watching the debate, creating a privacy/compliance risk.

Apply this diff to remove the email field:

 		participants = append(participants, map[string]interface{}{
 			"id":          client.UserID,
 			"displayName": client.Username,
-			"email":       client.Email,
 			"role":        client.Role,
 			"isMuted":     client.IsMuted,
 		})
🧹 Nitpick comments (1)
frontend/src/hooks/useDebateWS.ts (1)

123-192: Consider extracting poll normalization logic to reduce duplication.

The poll normalization logic (parsing counts, deduplicating options, trimming strings) is repeated in both poll_snapshot (lines 128-167) and poll_created (lines 232-256). This duplication increases maintenance burden.

Consider extracting to a helper function:

+const normalizePollData = (poll: any): PollInfo | null => {
+  const pollId = typeof poll.pollId === 'string' ? poll.pollId : String(poll.pollId ?? '');
+  if (!pollId) return null;
+
+  const countsRaw = poll.counts || {};
+  const counts: Record<string, number> = {};
+  Object.entries(countsRaw).forEach(([option, value]) => {
+    const numericValue = typeof value === 'number' ? value : Number(value ?? 0) || 0;
+    counts[option] = numericValue;
+  });
+
+  let options: string[] = [];
+  if (Array.isArray(poll.options)) {
+    options = poll.options
+      .map((opt: unknown) => String(opt ?? '').trim())
+      .filter((opt: string) => opt.length > 0);
+  }
+  if (options.length === 0) {
+    options = Object.keys(counts);
+  }
+
+  return {
+    pollId,
+    question: typeof poll.question === 'string' ? poll.question : '',
+    options,
+    counts,
+    voters: typeof poll.voters === 'number' ? poll.voters : Number(poll.voters ?? 0) || 0,
+  };
+};

 case 'poll_snapshot': {
   const payload = eventData.payload || {};
   const pollsPayload = payload.polls;
   if (Array.isArray(pollsPayload)) {
     const nextState: Record<string, PollInfo> = {};
     pollsPayload.forEach((poll) => {
-      if (!poll) return;
-      const pollId = typeof poll.pollId === 'string' ? poll.pollId : String(poll.pollId ?? '');
-      // ... rest of normalization
+      const info = normalizePollData(poll);
+      if (info) nextState[info.pollId] = info;
     });
     setPollState(nextState);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b24b6f9 and 341a292.

📒 Files selected for processing (14)
  • backend/controllers/transcript_controller.go (2 hunks)
  • backend/internal/debate/poll_store.go (5 hunks)
  • backend/websocket/team_websocket.go (9 hunks)
  • backend/websocket/websocket.go (10 hunks)
  • frontend/src/Pages/OnlineDebateRoom.tsx (10 hunks)
  • frontend/src/Pages/TeamBuilder.tsx (1 hunks)
  • frontend/src/Pages/TeamDebateRoom.tsx (14 hunks)
  • frontend/src/atoms/debateAtoms.ts (2 hunks)
  • frontend/src/components/Matchmaking.tsx (1 hunks)
  • frontend/src/components/ReactionBar.tsx (1 hunks)
  • frontend/src/components/TeamMatchmaking.tsx (0 hunks)
  • frontend/src/hooks/useDebateWS.ts (4 hunks)
  • frontend/src/services/teamDebateService.ts (3 hunks)
  • frontend/src/services/teamService.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • frontend/src/components/TeamMatchmaking.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • frontend/src/services/teamDebateService.ts
  • frontend/src/components/ReactionBar.tsx
  • backend/controllers/transcript_controller.go
  • frontend/src/components/Matchmaking.tsx
🧰 Additional context used
🧬 Code graph analysis (4)
frontend/src/Pages/OnlineDebateRoom.tsx (2)
frontend/src/hooks/useDebateWS.ts (1)
  • useDebateWS (23-347)
frontend/src/atoms/debateAtoms.ts (4)
  • questionsAtom (63-63)
  • presenceAtom (75-75)
  • pollStateAtom (18-18)
  • PollInfo (9-15)
backend/websocket/team_websocket.go (1)
backend/services/team_turn_service.go (1)
  • TokenBucket (28-34)
frontend/src/hooks/useDebateWS.ts (1)
frontend/src/atoms/debateAtoms.ts (2)
  • lastEventIdAtom (72-72)
  • PollInfo (9-15)
backend/websocket/websocket.go (1)
backend/structs/websocket.go (1)
  • Room (30-37)
🪛 Biome (2.1.2)
frontend/src/Pages/TeamDebateRoom.tsx

[error] 997-997: Shouldn't redeclare 'toggleCamera'. Consider to delete it or rename it.

'toggleCamera' is defined here:

(lint/suspicious/noRedeclare)

🔇 Additional comments (24)
frontend/src/Pages/TeamBuilder.tsx (1)

634-645: LGTM! Good defensive check and sanitization.

The conditional rendering with user && user.id prevents potential NPEs, and the sanitized user object with fallback values ensures TeamMatchmaking receives a well-defined payload. This aligns well with the optional chaining pattern used elsewhere in the component.

frontend/src/atoms/debateAtoms.ts (1)

9-18: LGTM: Clean poll state refactor with improved type safety.

The new PollInfo interface and refactored pollStateAtom provide better structure and type safety for poll data. The fields (pollId, question, options, counts, voters) align well with the backend PollMetadata structure and support the expanded poll functionality introduced in this PR.

frontend/src/hooks/useDebateWS.ts (1)

66-76: LGTM: Proper environment-aware WebSocket URL construction.

The WebSocket URL derivation correctly uses VITE_API_URL when available and falls back to window.location.host, addressing the previous hard-coded localhost issue. The try-catch for URL parsing with fallback to string replacement is a good defensive pattern.

backend/internal/debate/poll_store.go (2)

34-99: LGTM: Robust poll creation with proper validation and atomicity.

The CreatePoll method properly validates inputs, deduplicates options, initializes counts to zero, stores metadata as JSON, and uses a pipeline for atomicity. The error handling is comprehensive and contextual.


150-174: LGTM: Non-blocking SCAN replaces blocking KEYS.

The code now uses SCAN with a bounded count (100) to iterate over poll keys instead of the blocking KEYS command. This prevents blocking Redis in production as the dataset grows.

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

356-370: LGTM: Proper ref synchronization to avoid stale closures.

The effects correctly synchronize refs (isTeam1Ref, myTeamIdRef, debatePhaseRef, currentUserIdRef) with their corresponding state values. This prevents stale closure issues in WebSocket handlers when the effect dependency array is minimized.


374-392: LGTM: Media initialization extracted to a callback.

Extracting getMedia as a useCallback allows it to be invoked from the WebSocket onopen handler without recreating the effect. The error handling properly sets mediaError on failure.

frontend/src/Pages/OnlineDebateRoom.tsx (6)

143-156: LGTM: Clean integration of spectator and audience features.

The component properly imports and uses the shared debate WebSocket hook (useDebateWS) along with Jotai atoms for presence, questions, and polls. The memoized derived values (recentQuestions, audiencePolls) are efficient.


239-279: LGTM: Comprehensive spectator connection cleanup.

The cleanup callbacks properly close peer connections, clear pending candidates, maintain base-ID tracking, and filter offer queues. The error handling silently ignores close errors, which is appropriate for cleanup paths.


293-377: LGTM: Robust spectator offer initiation with proper error handling.

The startSpectatorOffer callback validates WebSocket/stream/user readiness, normalizes connection IDs, creates RTCPeerConnection with proper event handlers, and cleans up on errors. The ICE candidate handler correctly uses the live userId from the ref.


379-417: LGTM: Queue-based spectator offer management.

The queue and flush logic ensures offers are only sent when the local stream and user ID are ready. The deduplication by connectionId prevents redundant offers.


2454-2531: LGTM: Clean audience interaction UI with polls and comments.

The Audience Polls and Audience Comments panels properly display poll results with vote counts and recent questions with timestamps. The empty states guide spectators to use the viewer portal.


2532-2541: LGTM: Clear spectator presence indicator.

The spectator count display at the bottom provides clear visibility of the current audience size.

backend/websocket/team_websocket.go (5)

256-260: Good error handling for team status fetch.

The fallback to an empty status map when GetTeamSpeakingStatus fails allows the connection to proceed gracefully rather than crashing. The error is logged for debugging. This pattern is consistently applied throughout the file.


410-411: Excellent fix for concurrent map access.

Using snapshotTeamRecipients instead of directly iterating room.Clients eliminates the race condition that could cause panics. The helper function properly holds the mutex while building the snapshot, then releases it before iterating to send messages.


769-776: Token consumption logic looks correct.

The pattern of calling TryConsumeForSpeaking to atomically check and consume tokens, then updating client.Tokens with the returned remainingTokens under mutex protection is sound. This ensures the client's local token count stays in sync with the service's token bucket.


789-795: Well-structured status broadcast.

This segment correctly combines error-safe status fetching, race-free recipient snapshotting, and team-filtered broadcasting. The pattern is consistent with other handlers in the file.


549-550: Comprehensive fix for safe broadcasting.

All broadcast handlers (handleTeamPhaseChange, handleTeamTopicChange, handleTeamRoleSelection, handleTeamTurnEnd) now consistently use snapshotTeamRecipients to avoid concurrent map access issues. This systematic approach eliminates race conditions across the entire file.

Also applies to: 567-568, 603-604, 831-832

backend/websocket/websocket.go (6)

119-154: Well-designed helper functions for spectator support.

The new helpers (nonSpectatorRecipients, countDebaters, countSpectators) provide clean abstractions for filtering and counting clients by type. Each properly holds the room mutex during access, preventing race conditions.


195-219: Good privacy boundary for spectator notifications.

The notifySpectatorStatus function correctly sends spectator join/leave events only to debaters (via nonSpectatorRecipients), preventing spectators from learning about each other while giving debaters visibility into their audience.


327-330: Proper spectator identification setup.

Using UUID for ConnectionID ensures uniqueness across spectator connections, which is essential for WebRTC signaling with multiple spectators. Setting Role = "spectator" provides a clear type distinction.


398-426: Robust disconnect handling with proper cleanup.

The enhanced error handling distinguishes normal closures from unexpected errors in logs, properly tracks the disconnected client before deletion to enable post-removal notifications, and maintains consistency by broadcasting updated participant lists to remaining clients.


475-490: Well-structured spectator offer request flow.

The handler correctly enriches spectator requestOffer messages with identifying information (connectionId, userId, displayName) and broadcasts only to debaters using broadcastRawToDebaters. The fallback to generate a ConnectionID ensures robustness.


666-682: Double unlock issue resolved.

The defer room.Mutex.Unlock() pattern correctly prevents the double unlock panic that was flagged in past reviews. The early return for spectators now safely exits without unlocking twice. The added broadcastParticipants call ensures all clients see updated role information.

Comment on lines 194 to 225
case 'vote':
setPollState((prev) => {
const newState = { ...prev };
const pollId = eventData.payload.pollId;
const option = eventData.payload.option;
if (!newState[pollId]) {
newState[pollId] = {};
const pollId = eventData.payload?.pollId;
const option = eventData.payload?.option;
if (typeof pollId !== 'string' || typeof option !== 'string') {
return prev;
}
const nextState = { ...prev };
const existing = nextState[pollId];
if (!existing) {
nextState[pollId] = {
pollId,
question: '',
options: [option],
counts: { [option]: 1 },
voters: 0,
};
return nextState;
}
newState[pollId][option] =
(newState[pollId][option] || 0) + 1;
return newState;
const nextCounts = { ...existing.counts };
nextCounts[option] = (nextCounts[option] || 0) + 1;
const nextOptions = existing.options.includes(option)
? existing.options
: [...existing.options, option];
nextState[pollId] = {
...existing,
options: nextOptions,
counts: nextCounts,
};
return nextState;
});
break;
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

Vote handler creates polls on-the-fly without validation.

The vote handler creates a new poll entry if the pollId doesn't exist (lines 204-211). This could allow votes for non-existent polls to create stub entries with incomplete metadata. Typically, only poll_created or poll_snapshot should establish poll entries.

Consider rejecting votes for unknown polls:

 case 'vote':
   setPollState((prev) => {
     const pollId = eventData.payload?.pollId;
     const option = eventData.payload?.option;
     if (typeof pollId !== 'string' || typeof option !== 'string') {
       return prev;
     }
     const nextState = { ...prev };
     const existing = nextState[pollId];
     if (!existing) {
-      nextState[pollId] = {
-        pollId,
-        question: '',
-        options: [option],
-        counts: { [option]: 1 },
-        voters: 0,
-      };
-      return nextState;
+      // Vote for unknown poll - ignore it
+      console.warn(`Received vote for unknown poll: ${pollId}`);
+      return prev;
     }
📝 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
case 'vote':
setPollState((prev) => {
const newState = { ...prev };
const pollId = eventData.payload.pollId;
const option = eventData.payload.option;
if (!newState[pollId]) {
newState[pollId] = {};
const pollId = eventData.payload?.pollId;
const option = eventData.payload?.option;
if (typeof pollId !== 'string' || typeof option !== 'string') {
return prev;
}
const nextState = { ...prev };
const existing = nextState[pollId];
if (!existing) {
nextState[pollId] = {
pollId,
question: '',
options: [option],
counts: { [option]: 1 },
voters: 0,
};
return nextState;
}
newState[pollId][option] =
(newState[pollId][option] || 0) + 1;
return newState;
const nextCounts = { ...existing.counts };
nextCounts[option] = (nextCounts[option] || 0) + 1;
const nextOptions = existing.options.includes(option)
? existing.options
: [...existing.options, option];
nextState[pollId] = {
...existing,
options: nextOptions,
counts: nextCounts,
};
return nextState;
});
break;
case 'vote':
setPollState((prev) => {
const pollId = eventData.payload?.pollId;
const option = eventData.payload?.option;
if (typeof pollId !== 'string' || typeof option !== 'string') {
return prev;
}
const nextState = { ...prev };
const existing = nextState[pollId];
if (!existing) {
// Vote for unknown poll - ignore it
console.warn(`Received vote for unknown poll: ${pollId}`);
return prev;
}
const nextCounts = { ...existing.counts };
nextCounts[option] = (nextCounts[option] || 0) + 1;
const nextOptions = existing.options.includes(option)
? existing.options
: [...existing.options, option];
nextState[pollId] = {
...existing,
options: nextOptions,
counts: nextCounts,
};
return nextState;
});
break;
🤖 Prompt for AI Agents
In frontend/src/hooks/useDebateWS.ts around lines 194 to 225, the 'vote' case
currently creates a new poll stub when pollId is unknown; instead, change the
handler to ignore/reject votes for unknown polls (do not create a new poll
object) and optionally log or trigger a fetch/request for the full poll
snapshot; specifically, inside setPollState, if pollId exists but there's no
existing entry return prev (and avoid constructing nextState with incomplete
metadata), and if desired emit a warning or enqueue a request to retrieve the
poll_snapshot/poll_created before accepting future votes.

Comment on lines 2 to +3
const API_BASE_URL =
import.meta.env.VITE_API_BASE_URL ?? "http://localhost:1313";
import.meta.env.VITE_API_URL?.replace(/\/+$/, "") ?? "http://localhost:1313";
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

🧩 Analysis chain

Verify the environment variable rename is applied consistently across the codebase.

The environment variable has been renamed from VITE_API_BASE_URL to VITE_API_URL. This is a breaking configuration change that requires:

  1. All other service files in the frontend to use the same variable name
  2. Environment files (.env, .env.example, etc.) to be updated
  3. Documentation and setup instructions to reflect the new variable name

Additionally, this change appears unrelated to the PR's stated objectives (Audience Interaction with Live Polls, Anonymous Q&A). Consider whether this refactoring should be in a separate PR for clearer change tracking.

Run the following script to verify consistency:


🏁 Script executed:

#!/bin/bash
# Description: Check for inconsistent API URL environment variable usage across frontend services

# Search for any remaining references to VITE_API_BASE_URL
echo "=== Checking for old VITE_API_BASE_URL references ==="
rg -n "VITE_API_BASE_URL" frontend/

# Search for VITE_API_URL usage to confirm consistency
echo -e "\n=== Checking VITE_API_URL usage across services ==="
rg -n "VITE_API_URL" frontend/src/services/

# Check environment files
echo -e "\n=== Checking environment configuration files ==="
fd -e env -e example . frontend/ --exec cat {}

Length of output: 400


🏁 Script executed:

#!/bin/bash
# Comprehensive verification of environment variable usage and configuration

# Check for .env files at repository root
echo "=== Environment files at repository root ==="
fd -e env -e example . . --maxdepth 1 2>/dev/null || echo "No env files found at root"

# List all service files in frontend/src/services/
echo -e "\n=== All service files in frontend/src/services/ ==="
fd '\.ts$' frontend/src/services/ 2>/dev/null

# Check each service file for API URL configuration
echo -e "\n=== API URL configuration in each service ==="
fd '\.ts$' frontend/src/services/ -x grep -n "import.meta.env.VITE_API" {} + 2>/dev/null || echo "No matches found"

# Search for documentation about environment variables
echo -e "\n=== Documentation files mentioning VITE_API ==="
fd -e md -e txt -e rst . . --exec grep -l "VITE_API" {} \; 2>/dev/null || echo "No documentation found"

# Check if there's a .env file template or example
echo -e "\n=== Checking for example/template config files ==="
find . -name ".env*" -o -name "*example*" -o -name "*.example" 2>/dev/null | head -20

Length of output: 767


🏁 Script executed:

#!/bin/bash
# Check API configuration in the four non-updated service files

echo "=== leaderboardService.ts API configuration ==="
grep -n "API\|import.meta.env\|fetch\|http" frontend/src/services/leaderboardService.ts | head -10

echo -e "\n=== profileService.ts API configuration ==="
grep -n "API\|import.meta.env\|fetch\|http" frontend/src/services/profileService.ts | head -10

echo -e "\n=== transcriptService.ts API configuration ==="
grep -n "API\|import.meta.env\|fetch\|http" frontend/src/services/transcriptService.ts | head -10

echo -e "\n=== vsbot.ts API configuration ==="
grep -n "API\|import.meta.env\|fetch\|http" frontend/src/services/vsbot.ts | head -10

Length of output: 1776


Incomplete environment variable migration creates critical inconsistency across services.

The PR updates only 2 of 6 frontend services to use VITE_API_URL, while 4 services still reference VITE_BASE_URL:

  • Updated to VITE_API_URL: teamService.ts, teamDebateService.ts
  • Still using VITE_BASE_URL: leaderboardService.ts, profileService.ts, transcriptService.ts, vsbot.ts

This creates two problems:

  1. Broken configuration: If VITE_API_URL and VITE_BASE_URL have different values, services will fail to connect to the API
  2. Inconsistent URL normalization: The VITE_API_URL services include .replace(/\/+$/, "") to remove trailing slashes, while VITE_BASE_URL services don't, leading to subtle bugs

Required fix: Either update all 6 services to use VITE_API_URL with the trailing slash normalization, or revert teamService.ts and teamDebateService.ts back to VITE_BASE_URL to maintain consistency.

🤖 Prompt for AI Agents
In frontend/src/services/teamService.ts around lines 2-3 (and apply to
teamDebateService.ts, leaderboardService.ts, profileService.ts,
transcriptService.ts, vsbot.ts), replace any usage of
import.meta.env.VITE_BASE_URL with import.meta.env.VITE_API_URL?.replace(/\/+$/,
"") ?? "http://localhost:1313" so all services use the same env var and
normalize trailing slashes; remove VITE_BASE_URL references and ensure the
fallback host remains the same across all six files.

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: 3

Caution

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

⚠️ Outside diff range comments (6)
frontend/src/services/teamDebateService.ts (1)

151-165: Fix type mismatch on line 163.

The function signature declares a return type of TeamMatchmakingPoolResponse, but line 163 references MatchmakingPoolResponse, which was removed according to the changes. This will cause a TypeScript compilation error.

Apply this diff to fix the type mismatch:

-  const data: MatchmakingPoolResponse = await response.json();
+  const data: TeamMatchmakingPoolResponse = await response.json();
backend/websocket/team_websocket.go (5)

566-577: Resolve merge conflict in phase change broadcast.

Same unresolved merge conflict. Use snapshotAllTeamClients(room) for broadcasting to all clients including the sender.

-<<<<<<< HEAD
-	recipients := snapshotTeamRecipients(room, nil)
-	for _, r := range recipients {
-=======
-	for _, r := range snapshotAllTeamClients(room) {
->>>>>>> main
+	for _, r := range snapshotAllTeamClients(room) {

589-598: Resolve merge conflict in topic change broadcast.

-<<<<<<< HEAD
-	recipients := snapshotTeamRecipients(room, nil)
-	for _, r := range recipients {
-=======
-	for _, r := range snapshotAllTeamClients(room) {
->>>>>>> main
+	for _, r := range snapshotAllTeamClients(room) {

629-641: Resolve merge conflict in role selection broadcast.

-<<<<<<< HEAD
-		recipients := snapshotTeamRecipients(room, nil)
-		for _, r := range recipients {
-=======
-		for _, r := range snapshotAllTeamClients(room) {
->>>>>>> main
+		for _, r := range snapshotAllTeamClients(room) {

819-841: Resolve merge conflict and ensure currentTurn is declared.

This merge conflict needs careful resolution. The HEAD branch has good error handling but doesn't declare currentTurn, while the main branch declares it but lacks error handling. Line 836 uses currentTurn, which would be undefined if you only keep the HEAD branch.

-<<<<<<< HEAD
-		teamStatus, statusErr := room.TokenBucket.GetTeamSpeakingStatus(client.TeamID, room.TurnManager)
-		if statusErr != nil {
-			log.Printf("failed to load team status for team %s: %v", client.TeamID.Hex(), statusErr)
-			teamStatus = map[string]interface{}{}
-		}
-		recipients := snapshotTeamRecipients(room, nil)
-		for _, r := range recipients {
-=======
-		teamStatus := room.TokenBucket.GetTeamSpeakingStatus(client.TeamID, room.TurnManager)
-		currentTurn := room.TurnManager.GetCurrentTurn(client.TeamID).Hex()
-		for _, r := range snapshotAllTeamClients(room) {
->>>>>>> main
+		teamStatus, statusErr := room.TokenBucket.GetTeamSpeakingStatus(client.TeamID, room.TurnManager)
+		if statusErr != nil {
+			log.Printf("failed to load team status for team %s: %v", client.TeamID.Hex(), statusErr)
+			teamStatus = map[string]interface{}{}
+		}
+		currentTurn := room.TurnManager.GetCurrentTurn(client.TeamID).Hex()
+		for _, r := range snapshotAllTeamClients(room) {

867-883: Resolve merge conflict in turn end broadcast.

-<<<<<<< HEAD
-	recipients := snapshotTeamRecipients(room, nil)
-	for _, r := range recipients {
-=======
-	for _, r := range snapshotAllTeamClients(room) {
->>>>>>> main
+	for _, r := range snapshotAllTeamClients(room) {
♻️ Duplicate comments (3)
backend/websocket/team_websocket.go (1)

192-198: Initialization errors are still being ignored.

The previous review flagged that InitializeTeamTurns and InitializeTeamBuckets errors should be handled to fail fast before entering the WebSocket loop. This issue remains unaddressed—if these calls fail, the room operates with empty turn order and missing buckets, causing GetCurrentTurn to return Nil and TryConsumeForSpeaking to deny all speakers.

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

1029-1029: Replace hardcoded localhost with BASE_URL.

Line 1029 still uses http://localhost:1313/submit-transcripts despite previous review feedback. This endpoint will fail in deployed environments. Use the BASE_URL constant (defined at lines 127-129) that's already being used correctly for WebSocket connections at lines 421-426.

Apply this diff:

-      const response = await fetch(`http://localhost:1313/submit-transcripts`, {
+      const response = await fetch(`${BASE_URL}/submit-transcripts`, {

1091-1099: Remove duplicate toggleCamera declaration.

toggleCamera is already defined at lines 936-954 using useCallback. This redeclaration at lines 1091-1099 will cause a runtime error and is flagged by the linter.

Apply this diff to remove the duplicate:

-  const toggleCamera = () => {
-    const stream = localStreamRef.current;
-    const videoTrack = stream?.getVideoTracks()[0];
-    const nextState = !isCameraOn;
-    if (videoTrack) {
-      videoTrack.enabled = nextState;
-    }
-    setIsCameraOn(nextState);
-  };
-
   // Manage setup popup visibility and check if debate should start
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 341a292 and d1aa334.

📒 Files selected for processing (4)
  • backend/websocket/team_websocket.go (8 hunks)
  • frontend/src/Pages/TeamDebateRoom.tsx (5 hunks)
  • frontend/src/services/teamDebateService.ts (3 hunks)
  • frontend/src/services/teamService.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/services/teamService.ts
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/services/teamDebateService.ts (2)
frontend/src/services/teamService.ts (1)
  • Team (7-18)
backend/models/team.go (1)
  • Team (11-22)
🪛 Biome (2.1.2)
frontend/src/Pages/TeamDebateRoom.tsx

[error] 1091-1091: Shouldn't redeclare 'toggleCamera'. Consider to delete it or rename it.

'toggleCamera' is defined here:

(lint/suspicious/noRedeclare)

🔇 Additional comments (8)
frontend/src/services/teamDebateService.ts (4)

2-2: LGTM!

The import correctly references the Team type needed for the MatchmakingStatusResponse interface.


43-51: LGTM!

The interface is well-structured with appropriate fields for representing a team entry in the matchmaking pool.


53-56: LGTM!

The wrapper interface correctly structures the matchmaking pool response with pool size and team entries.


58-62: LGTM!

The interface appropriately models the matchmaking status response with optional team and match ID fields.

backend/websocket/team_websocket.go (4)

257-261: Good error handling with safe fallback.

Logging the error and falling back to an empty map prevents nil-related panics downstream while still surfacing the issue in logs for debugging.


415-419: Consistent error handling pattern.


799-806: Token consumption logic looks correct.

The TryConsumeForSpeaking call properly returns both permission and remaining tokens, which are correctly used to update client state.


860-864: Consistent error handling pattern.

Comment on lines +422 to +427
<<<<<<< HEAD
recipients := snapshotTeamRecipients(room, nil)
for _, r := range recipients {
=======
for _, r := range snapshotAllTeamClients(room) {
>>>>>>> main
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

Unresolved merge conflict markers will prevent compilation.

The file contains unresolved Git merge conflict markers. This code will not compile. You need to resolve these conflicts by choosing one approach. Given that this broadcasts to all clients (including sender for sync), snapshotAllTeamClients(room) is semantically clearer.

-<<<<<<< HEAD
-	recipients := snapshotTeamRecipients(room, nil)
-	for _, r := range recipients {
-=======
-	for _, r := range snapshotAllTeamClients(room) {
->>>>>>> main
+	for _, r := range snapshotAllTeamClients(room) {
📝 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
<<<<<<< HEAD
recipients := snapshotTeamRecipients(room, nil)
for _, r := range recipients {
=======
for _, r := range snapshotAllTeamClients(room) {
>>>>>>> main
for _, r := range snapshotAllTeamClients(room) {
🤖 Prompt for AI Agents
In backend/websocket/team_websocket.go around lines 422 to 427 there are
unresolved Git merge conflict markers (<<<<<<<, =======, >>>>>>>) that will
prevent compilation; remove the conflict markers and the older branch code,
replace the recipients-based loop with the clearer call to
snapshotAllTeamClients(room) (i.e., iterate over snapshotAllTeamClients(room)
directly), delete the unused recipients variable, and ensure the final code
contains only one for-loop using snapshotAllTeamClients(room) so the file
compiles and broadcasts to all clients including the sender.

Comment on lines +368 to +382
useEffect(() => {
isTeam1Ref.current = isTeam1;
}, [isTeam1]);

useEffect(() => {
myTeamIdRef.current = myTeamId;
}, [myTeamId]);

useEffect(() => {
debatePhaseRef.current = debatePhase;
}, [debatePhase]);

useEffect(() => {
currentUserIdRef.current = currentUser?.id;
}, [currentUser?.id]);
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

Remove duplicate useEffect blocks.

Lines 368-382 contain four useEffect hooks that are exact duplicates of the ones already defined at lines 182-184, 192-194, 196-199, and 201-204. This duplication will cause each ref-sync effect to execute twice on every state change, leading to unnecessary re-renders and incorrect behavior.

Apply this diff to remove the duplicates:

-  useEffect(() => {
-    isTeam1Ref.current = isTeam1;
-  }, [isTeam1]);
-
-  useEffect(() => {
-    myTeamIdRef.current = myTeamId;
-  }, [myTeamId]);
-
-  useEffect(() => {
-    debatePhaseRef.current = debatePhase;
-  }, [debatePhase]);
-
-  useEffect(() => {
-    currentUserIdRef.current = currentUser?.id;
-  }, [currentUser?.id]);
-
   // Initialize WebSocket connection - only need token and debateId
📝 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
useEffect(() => {
isTeam1Ref.current = isTeam1;
}, [isTeam1]);
useEffect(() => {
myTeamIdRef.current = myTeamId;
}, [myTeamId]);
useEffect(() => {
debatePhaseRef.current = debatePhase;
}, [debatePhase]);
useEffect(() => {
currentUserIdRef.current = currentUser?.id;
}, [currentUser?.id]);
// Initialize WebSocket connection - only need token and debateId
🤖 Prompt for AI Agents
frontend/src/Pages/TeamDebateRoom.tsx lines 368-382: the four useEffect hooks
here are duplicates of the ref-sync effects already declared at lines ~182-204,
causing them to run twice; remove the duplicated useEffect blocks (the ones
syncing isTeam1Ref, myTeamIdRef, debatePhaseRef, and currentUserIdRef) from
lines 368-382 so each ref is updated by a single effect, and ensure no other
logic depends on the removed copies.

Comment on lines +1697 to +1701
{speechError && (
<p className="text-amber-600 mt-2 text-center max-w-6xl mx-auto">
{speechError}
</p>
)}
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

Fix undefined speechError reference.

Lines 1697-1701 reference speechError, but line 214 declares the state without capturing the value: const [, setSpeechError] = useState<string | null>(null);. The empty position before the comma means speechError is not defined in scope, causing a runtime ReferenceError.

Apply this diff to capture the state value:

-  const [, setSpeechError] = useState<string | null>(null);
+  const [speechError, setSpeechError] = useState<string | null>(null);
📝 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
{speechError && (
<p className="text-amber-600 mt-2 text-center max-w-6xl mx-auto">
{speechError}
</p>
)}
const [speechError, setSpeechError] = useState<string | null>(null);
🤖 Prompt for AI Agents
In frontend/src/Pages/TeamDebateRoom.tsx around lines 1697-1701 and the state
declaration at ~line 214, the component renders a speechError variable that was
never captured from useState (const [, setSpeechError] = ...), causing a
ReferenceError; fix it by updating the state declaration to capture both value
and setter (const [speechError, setSpeechError] = useState<string | null>(null))
so the rendered {speechError} is defined, and ensure any other local references
use the captured speechError variable.

@bhavik-mangla bhavik-mangla merged commit 655656a into AOSSIE-Org:main Dec 1, 2025
1 check passed
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