Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,4 @@ TEST_PASSWORD=Test123!@#

PANDASCORE_API_KEY=your_pandascore_api_key_here
PANDASCORE_BASE_URL=https://api.pandascore.co
PANDASCORE_CACHE_TTL=3600
PANDASCORE_CACHE_TTL=3600
2 changes: 1 addition & 1 deletion .env.production.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ SECRET_KEY_BASE=CHANGE_ME_GENERATE_WITH_rails_secret
DEVISE_JWT_SECRET_KEY=CHANGE_ME_GENERATE_WITH_rails_secret

# CORS
CORS_ORIGINS=https://prostaff.gg,https://api.prostaff.gg,https://www.prostaff.gg
CORS_ORIGINS=https://prostaff.gg,https://www.prostaff.gg,https://prostaffgg.netlify.app

# External APIs
RIOT_API_KEY=RGAPI-YOUR-PRODUCTION-KEY-HERE
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,9 @@ TEST_ANALYSIS_REPORT.md
MODULAR_MIGRATION_PHASE1_SUMMARY.md
MODULAR_MONOLITH_MIGRATION_PLAN.md
app/modules/players/README.md
/League-Data-Scraping-And-Analytics-master/jsons
/League-Data-Scraping-And-Analytics-master/Pro/game
/League-Data-Scraping-And-Analytics-master/Pro/timeline
League-Data-Scraping-And-Analytics-master/ProStaff-Scraper/
DOCS/ELASTICSEARCH_SETUP.md
DOCS/deployment/QUICK_DEPLOY_VPS.md
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ gem 'rswag'
gem 'rswag-api'
gem 'rswag-ui'

# Elasticsearch client (for analytics queries)
gem 'elasticsearch', '~> 9.1', '>= 9.1.3'

group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem 'debug', platforms: %i[mri mingw x64_mingw]
Expand Down
10 changes: 10 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ GEM
dotenv (= 3.1.8)
railties (>= 6.1)
drb (2.2.3)
elastic-transport (8.4.1)
faraday (< 3)
multi_json
elasticsearch (9.2.0)
elastic-transport (~> 8.3)
elasticsearch-api (= 9.2.0)
elasticsearch-api (9.2.0)
multi_json
erb (5.0.3)
erubi (1.13.1)
et-orbi (1.4.0)
Expand Down Expand Up @@ -172,6 +180,7 @@ GEM
mini_mime (1.1.5)
minitest (5.26.0)
msgpack (1.8.0)
multi_json (1.18.0)
net-http (0.6.0)
uri
net-imap (0.5.12)
Expand Down Expand Up @@ -371,6 +380,7 @@ DEPENDENCIES
database_cleaner-active_record
debug
dotenv-rails
elasticsearch (~> 9.1, >= 9.1.3)
factory_bot_rails
faker
faraday
Expand Down
9 changes: 3 additions & 6 deletions app/controllers/api/v1/analytics/performance_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,9 @@ def apply_date_filters(matches)
# @param period [String] Time period (week, month, season)
# @return [Integer] Number of days
def time_period_to_days(period)
case period
when 'week' then 7
when 'month' then 30
when 'season' then 90
else 30
end
return 7 if period == 'week'
return 90 if period == 'season'
30
end

# Legacy method - kept for backwards compatibility
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/api/v1/dashboard_controller_optimized.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ def calculate_win_rate_fast(wins, total)
((wins.to_f / total) * 100).round(1)
end

def calculate_recent_form(matches)
matches.map { |m| m.victory? ? 'W' : 'L' }.join('')
def calculate_recent_form(matches)
matches.map { |m| m.victory? ? 'W' : 'L' }.join
end

def calculate_average_kda_fast(kda_result)
Expand Down
13 changes: 5 additions & 8 deletions app/controllers/api/v1/scrims/opponent_teams_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,13 @@ def destroy
# only modify teams they have scrims with.
# Read operations (index/show) are allowed for all teams to enable discovery.
#
# SECURITY: Unscoped find is intentional here. OpponentTeam is a global
# resource visible to all organizations for discovery. Authorization is
# handled by verify_team_usage! for modifications.
# rubocop:disable Rails/FindById
def set_opponent_team
@opponent_team = OpponentTeam.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: 'Opponent team not found' }, status: :not_found
id = Integer(params[:id]) rescue nil
return render json: { error: 'Opponent team not found' }, status: :not_found unless id

@opponent_team = OpponentTeam.find_by(id: id)
return render json: { error: 'Opponent team not found' }, status: :not_found unless @opponent_team
end
# rubocop:enable Rails/FindById

# Verifies that current organization has used this opponent team
# Prevents organizations from modifying/deleting teams they haven't interacted with
Expand Down
109 changes: 56 additions & 53 deletions app/jobs/sync_player_from_riot_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,69 +6,26 @@ class SyncPlayerFromRiotJob < ApplicationJob
def perform(player_id)
player = Player.find(player_id)

unless player.riot_puuid.present? || player.summoner_name.present?
player.update(sync_status: 'error', last_sync_at: Time.current)
Rails.logger.error "Player #{player_id} missing Riot info"
return
end
return mark_error(player, "Player #{player_id} missing Riot info") unless player.riot_puuid.present? || player.summoner_name.present?

riot_api_key = ENV['RIOT_API_KEY']
unless riot_api_key.present?
player.update(sync_status: 'error', last_sync_at: Time.current)
Rails.logger.error 'Riot API key not configured'
return
end
return mark_error(player, 'Riot API key not configured') unless riot_api_key.present?

region = player.region.presence&.downcase || 'br1'

begin
region = player.region.presence&.downcase || 'br1'

summoner_data = if player.riot_puuid.present?
fetch_summoner_by_puuid(player.riot_puuid, region, riot_api_key)
else
fetch_summoner_by_name(player.summoner_name, region, riot_api_key)
end

# Use PUUID for league endpoint (workaround for Riot API bug where summoner_data['id'] is nil)
# See: https://github.com/RiotGames/developer-relations/issues/1092
ranked_data = fetch_ranked_stats_by_puuid(player.riot_puuid, region, riot_api_key)

update_data = {
riot_puuid: summoner_data['puuid'],
riot_summoner_id: summoner_data['id'],
summoner_level: summoner_data['summonerLevel'],
profile_icon_id: summoner_data['profileIconId'],
sync_status: 'success',
last_sync_at: Time.current
}

solo_queue = ranked_data.find { |q| q['queueType'] == 'RANKED_SOLO_5x5' }
if solo_queue
update_data.merge!({
solo_queue_tier: solo_queue['tier'],
solo_queue_rank: solo_queue['rank'],
solo_queue_lp: solo_queue['leaguePoints'],
solo_queue_wins: solo_queue['wins'],
solo_queue_losses: solo_queue['losses']
})
end

flex_queue = ranked_data.find { |q| q['queueType'] == 'RANKED_FLEX_SR' }
if flex_queue
update_data.merge!({
flex_queue_tier: flex_queue['tier'],
flex_queue_rank: flex_queue['rank'],
flex_queue_lp: flex_queue['leaguePoints']
})
end
summoner_data = fetch_summoner(player, region, riot_api_key)
ranked_data = fetch_ranked_stats_by_puuid(summoner_data['puuid'], region, riot_api_key)

player.update!(update_data)
update_data = build_update_data(summoner_data)
update_data.merge!(extract_queue_updates(ranked_data))

player.update!(update_data)
Rails.logger.info "Successfully synced player #{player_id} from Riot API"
rescue StandardError => e
Rails.logger.error "Failed to sync player #{player_id}: #{e.message}"
Rails.logger.error e.backtrace.join("\n")

player.update(sync_status: 'error', last_sync_at: Time.current)
mark_error(player)
end
end

Expand Down Expand Up @@ -154,3 +111,49 @@ def fetch_ranked_stats_by_puuid(puuid, region, api_key)
JSON.parse(response.body)
end
end
def fetch_summoner(player, region, api_key)
return fetch_summoner_by_puuid(player.riot_puuid, region, api_key) if player.riot_puuid.present?
fetch_summoner_by_name(player.summoner_name, region, api_key)
end

def build_update_data(summoner_data)
{
riot_puuid: summoner_data['puuid'],
riot_summoner_id: summoner_data['id'],
summoner_level: summoner_data['summonerLevel'],
profile_icon_id: summoner_data['profileIconId'],
sync_status: 'success',
last_sync_at: Time.current
}
end

def extract_queue_updates(ranked_data)
updates = {}

solo = ranked_data.find { |q| q['queueType'] == 'RANKED_SOLO_5x5' }
if solo
updates.merge!({
solo_queue_tier: solo['tier'],
solo_queue_rank: solo['rank'],
solo_queue_lp: solo['leaguePoints'],
solo_queue_wins: solo['wins'],
solo_queue_losses: solo['losses']
})
end

flex = ranked_data.find { |q| q['queueType'] == 'RANKED_FLEX_SR' }
if flex
updates.merge!({
flex_queue_tier: flex['tier'],
flex_queue_rank: flex['rank'],
flex_queue_lp: flex['leaguePoints']
})
end

updates
end

def mark_error(player, message = nil)
Rails.logger.error(message) if message
player.update(sync_status: 'error', last_sync_at: Time.current)
end
11 changes: 4 additions & 7 deletions app/models/audit_log.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,10 @@ def time_ago
end

def risk_level
case action
when 'delete' then 'high'
when 'update' then 'medium'
when 'create' then 'low'
when 'login', 'logout' then 'info'
else 'medium'
end
return 'high' if action == 'delete'
return 'low' if action == 'create'
return 'info' if %w[login logout].include?(action)
'medium'
end

def risk_color
Expand Down
2 changes: 1 addition & 1 deletion app/models/schedule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def participant_overlap?(other)
our_participants = required_players + optional_players
other_participants = other.required_players + other.optional_players

(our_participants & other_participants).any?
our_participants.intersect?(other_participants)
end

def log_audit_trail
Expand Down
3 changes: 2 additions & 1 deletion app/models/team_goal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ def update_progress!(new_current_value)
end

def assigned_to_name
assigned_to&.full_name || assigned_to&.email&.split('@')&.first || 'Unassigned'
return 'Unassigned' unless assigned_to
assigned_to.full_name || (assigned_to.email&.split('@')&.first) || 'Unassigned'
end

def player_name
Expand Down
9 changes: 3 additions & 6 deletions app/models/vod_review.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,9 @@ def duration_formatted
end

def status_color
case status
when 'draft' then 'yellow'
when 'published' then 'green'
when 'archived' then 'gray'
else 'gray'
end
return 'yellow' if status == 'draft'
return 'green' if status == 'published'
'gray'
end

def can_be_edited_by?(user)
Expand Down
11 changes: 4 additions & 7 deletions app/models/vod_timestamp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,10 @@ def timestamp_formatted
end

def importance_color
case importance
when 'low' then 'gray'
when 'normal' then 'blue'
when 'high' then 'orange'
when 'critical' then 'red'
else 'gray'
end
return 'blue' if importance == 'normal'
return 'orange' if importance == 'high'
return 'red' if importance == 'critical'
'gray'
end

def category_color
Expand Down
22 changes: 22 additions & 0 deletions app/modules/analytics/services/elasticsearch_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Analytics
module Services
class ElasticsearchClient
def initialize(url: ENV.fetch('ELASTICSEARCH_URL', 'http://localhost:9200'))
@client = Elasticsearch::Client.new(url: url)
end

def ping
@client.ping
rescue StandardError => e
Rails.logger.error("Elasticsearch ping failed: #{e.message}")
false
end

def search(index:, body: {})
@client.search(index: index, body: body)
end
end
end
end
6 changes: 1 addition & 5 deletions app/modules/matches/jobs/sync_match_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,7 @@ def create_player_match_stats(match, participants, organization)
end

def determine_match_type(game_mode)
case game_mode.upcase
when 'CLASSIC' then 'official'
when 'ARAM' then 'scrim'
else 'scrim'
end
game_mode.to_s.upcase == 'CLASSIC' ? 'official' : 'scrim'
end

def determine_team_victory(participants, organization)
Expand Down
13 changes: 5 additions & 8 deletions app/modules/scrims/controllers/opponent_teams_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,13 @@ def destroy
# only modify teams they have scrims with.
# Read operations (index/show) are allowed for all teams to enable discovery.
#
# SECURITY: Unscoped find is intentional here. OpponentTeam is a global
# resource visible to all organizations for discovery. Authorization is
# handled by verify_team_usage! for modifications.
# rubocop:disable Rails/FindById
def set_opponent_team
@opponent_team = OpponentTeam.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: 'Opponent team not found' }, status: :not_found
id = Integer(params[:id]) rescue nil
return render json: { error: 'Opponent team not found' }, status: :not_found unless id

@opponent_team = OpponentTeam.find_by(id: id)
return render json: { error: 'Opponent team not found' }, status: :not_found unless @opponent_team
end
# rubocop:enable Rails/FindById

# Verifies that current organization has used this opponent team
# Prevents organizations from modifying/deleting teams they haven't interacted with
Expand Down
Loading