@@ -47,7 +47,7 @@ def index
4747 def fetch_scrim_entries ( game :, region :)
4848 scrims = Scrim . unscoped
4949 . eager_load ( :organization )
50- . includes ( :opponent_team , organization : :players )
50+ . includes ( :opponent_team )
5151 . where ( scrims : { visibility : 'public' } )
5252 . where ( organizations : { is_public : true } )
5353 . where ( 'scrims.scheduled_at >= ?' , Time . current )
@@ -58,7 +58,9 @@ def fetch_scrim_entries(game:, region:)
5858 scrims = scrims . where ( organizations : { region : region } ) if region
5959 scrims = filter_by_tier ( scrims , params [ :tier ] ) if params [ :tier ] . present?
6060
61- scrims . map { |s | serialize_lobby_scrim ( s ) }
61+ records = scrims . to_a
62+ players_by_org = load_public_players ( records . map { |s | s . organization_id } )
63+ records . map { |s | serialize_lobby_scrim ( s , players_by_org ) }
6264 end
6365
6466 # ── Source 2: AvailabilityWindow records → next occurrence ───────────────
@@ -69,18 +71,20 @@ def fetch_window_entries(game:, region:, exclude_org_ids:)
6971 . joins ( :organization )
7072 . where ( organizations : { is_public : true } )
7173 . where . not ( organization_id : exclude_org_ids . to_a )
72- . includes ( organization : :players )
74+ . includes ( :organization )
7375 . limit ( WINDOW_CAP )
7476
7577 windows = windows . where ( availability_windows : { game : game } ) if game
7678 windows = windows . where ( availability_windows : { region : region } ) if region
7779
78- windows . filter_map { |w | serialize_lobby_window ( w ) }
80+ records = windows . to_a
81+ players_by_org = load_public_players ( records . map { |w | w . organization_id } )
82+ records . filter_map { |w | serialize_lobby_window ( w , players_by_org ) }
7983 end
8084
8185 # ── Serializers ───────────────────────────────────────────────────────────
8286
83- def serialize_lobby_scrim ( scrim )
87+ def serialize_lobby_scrim ( scrim , players_by_org )
8488 org = scrim . organization
8589 {
8690 id : scrim . id ,
@@ -90,15 +94,16 @@ def serialize_lobby_scrim(scrim)
9094 games_planned : scrim . games_planned ,
9195 status : scrim . status ,
9296 source : scrim . try ( :source ) || 'internal' ,
93- organization : serialize_org ( org )
97+ organization : serialize_org ( org , players_by_org [ org . id ] || [ ] )
9498 }
9599 end
96100
97101 # Returns nil if next_occurrence cannot be computed — filter_map drops nils.
98- def serialize_lobby_window ( window )
102+ def serialize_lobby_window ( window , players_by_org )
99103 occurs_at = next_occurrence ( window )
100104 return nil unless occurs_at
101105
106+ org = window . organization
102107 {
103108 id : "window-#{ window . id } " , # namespaced to avoid collision with Scrim IDs
104109 scheduled_at : occurs_at ,
@@ -107,13 +112,13 @@ def serialize_lobby_window(window)
107112 games_planned : 3 ,
108113 status : 'open' ,
109114 source : 'availability_window' ,
110- organization : serialize_org ( window . organization )
115+ organization : serialize_org ( org , players_by_org [ org . id ] || [ ] )
111116 }
112117 end
113118
114119 # Only expose fields safe for public consumption.
115120 # Notably absent: email, subscription_plan, is_public, internal config.
116- def serialize_org ( org )
121+ def serialize_org ( org , players )
117122 {
118123 id : org . id ,
119124 name : org . name ,
@@ -122,18 +127,18 @@ def serialize_org(org)
122127 tier : org . try ( :tier ) ,
123128 public_tagline : org . try ( :public_tagline ) ,
124129 discord_invite_url : org . try ( :discord_invite_url ) ,
125- roster : serialize_org_roster ( org )
130+ roster : serialize_org_roster ( players )
126131 }
127132 end
128133
129- # Returns the org's active players sorted by role, already preloaded via includes .
134+ # Players are preloaded via load_public_players — no association traversal here .
130135 # Capped at 10 to keep the response lean.
131- def serialize_org_roster ( org )
136+ def serialize_org_roster ( players )
132137 role_sort = %w[ top jungle mid adc support ]
133- players = org . players . select ( & : active? )
134- players . sort_by { |p | [ role_sort . index ( p . role ) || 99 , p . summoner_name ] }
135- . first ( 10 )
136- . map do |p |
138+ active = players . select { | p | p . status == ' active' && p . deleted_at . nil? }
139+ active . sort_by { |p | [ role_sort . index ( p . role ) || 99 , p . summoner_name . to_s ] }
140+ . first ( 10 )
141+ . map do |p |
137142 {
138143 summoner_name : p . summoner_name ,
139144 role : p . role ,
@@ -145,6 +150,19 @@ def serialize_org_roster(org)
145150
146151 # ── Helpers ───────────────────────────────────────────────────────────────
147152
153+ # Loads players for the given org_ids bypassing OrganizationScoped, since
154+ # this is a public endpoint with no authenticated user. Returns a Hash
155+ # keyed by organization_id (UUID string) for O(1) lookup in serializers.
156+ def load_public_players ( org_ids )
157+ return { } if org_ids . empty?
158+
159+ Player . unscoped
160+ . where ( organization_id : org_ids , deleted_at : nil )
161+ . select ( :id , :organization_id , :summoner_name , :role ,
162+ :solo_queue_tier , :solo_queue_rank , :status , :deleted_at )
163+ . group_by ( &:organization_id )
164+ end
165+
148166 def filter_by_tier ( scrims , tier )
149167 tier_plans = case tier
150168 when 'professional' then %w[ professional enterprise ]
0 commit comments