From 3e4317dbc50f4e8c38e8b1907a0f5a7b647dce79 Mon Sep 17 00:00:00 2001 From: Jacob Cody Wimer Date: Mon, 21 Apr 2025 17:12:54 -0400 Subject: [PATCH] Using action cable to send stats updates to the client and made a spectate page. --- app/assets/javascripts/application.js | 10 + app/channels/application_cable/channel.rb | 4 + app/channels/application_cable/connection.rb | 4 + app/channels/match_channel.rb | 63 ++++ app/controllers/matches_controller.rb | 32 ++- app/models/match.rb | 1 + app/views/layouts/application.html.erb | 2 + app/views/matches/_matchstats.html.erb | 269 +++++++++++++----- app/views/matches/spectate.html.erb | 222 +++++++++++++++ app/views/tournaments/_bracket_final.html.erb | 2 +- .../tournaments/_bracket_partial.html.erb | 11 + app/views/tournaments/_bracket_round.html.erb | 2 +- config/cable.yml | 11 +- config/routes.rb | 12 +- test/channels/match_channel_test.rb | 8 + 15 files changed, 565 insertions(+), 88 deletions(-) create mode 100644 app/channels/application_cable/channel.rb create mode 100644 app/channels/application_cable/connection.rb create mode 100644 app/channels/match_channel.rb create mode 100644 app/views/matches/spectate.html.erb create mode 100644 test/channels/match_channel_test.rb diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e0eecee..38834d7 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -18,5 +18,15 @@ //= require jquery.dataTables.min.js //= require turbolinks // +//= require actioncable +//= require_self //= require_tree . +// Create the Action Cable consumer instance +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer(); + +}).call(this); + diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..d672697 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..0ff5442 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/channels/match_channel.rb b/app/channels/match_channel.rb new file mode 100644 index 0000000..0ae50b3 --- /dev/null +++ b/app/channels/match_channel.rb @@ -0,0 +1,63 @@ +class MatchChannel < ApplicationCable::Channel + def subscribed + @match = Match.find_by(id: params[:match_id]) + Rails.logger.info "[MatchChannel] Client subscribed with match_id: #{params[:match_id]}. Match found: #{@match.present?}" + if @match + stream_for @match + else + Rails.logger.warn "[MatchChannel] Match not found for ID: #{params[:match_id]}. Subscription may fail." + # You might want to reject the subscription if the match isn't found + # reject + end + end + + def unsubscribed + Rails.logger.info "[MatchChannel] Client unsubscribed for match #{@match&.id}" + end + + # Called when client sends data with action: 'send_stat' + def send_stat(data) + # Explicit check for @match at the start + unless @match + Rails.logger.error "[MatchChannel] Error: send_stat called but @match is nil. Client params on sub: #{params[:match_id]}" + return # Stop if no match context + end + + Rails.logger.info "[MatchChannel] Received send_stat for match #{@match.id} with data: #{data.inspect}" + + # Prepare attributes to update + attributes_to_update = {} + attributes_to_update[:w1_stat] = data['new_w1_stat'] if data.key?('new_w1_stat') + attributes_to_update[:w2_stat] = data['new_w2_stat'] if data.key?('new_w2_stat') + + if attributes_to_update.present? + # Persist the changes to the database + # Note: Consider background job or throttling for very high frequency updates + begin + if @match.update(attributes_to_update) + Rails.logger.info "[MatchChannel] Updated match #{@match.id} stats in DB: #{attributes_to_update.keys.join(', ')}" + + # Prepare payload for broadcast (using potentially updated values from @match) + payload = { + w1_stat: @match.w1_stat, + w2_stat: @match.w2_stat + }.compact + + if payload.present? + Rails.logger.info "[MatchChannel] Broadcasting DB-persisted stats to match #{@match.id} with payload: #{payload.inspect}" + MatchChannel.broadcast_to(@match, payload) + else + Rails.logger.info "[MatchChannel] Payload empty after DB update for match #{@match.id}, not broadcasting." + end + else + Rails.logger.error "[MatchChannel] Failed to update match #{@match.id} stats in DB: #{@match.errors.full_messages.join(', ')}" + end + rescue => e + Rails.logger.error "[MatchChannel] Exception during match update for #{@match.id}: #{e.message}" + Rails.logger.error e.backtrace.join("\n") + end + else + Rails.logger.info "[MatchChannel] No new stat data provided in send_stat for match #{@match.id}, not updating DB or broadcasting." + end + end +end diff --git a/app/controllers/matches_controller.rb b/app/controllers/matches_controller.rb index df848e0..1375f1b 100644 --- a/app/controllers/matches_controller.rb +++ b/app/controllers/matches_controller.rb @@ -1,5 +1,5 @@ class MatchesController < ApplicationController - before_action :set_match, only: [:show, :edit, :update, :stat] + before_action :set_match, only: [:show, :edit, :update, :stat, :spectate] before_action :check_access, only: [:edit,:update, :stat] # GET /matches/1 @@ -54,12 +54,42 @@ class MatchesController < ApplicationController session[:error_return_path] = "/matches/#{@match.id}/stat" end + # GET /matches/:id/spectate + def spectate + # Similar to stat, but potentially simplified for read-only view + # We mainly need @match for the view to get the ID + # and maybe initial wrestler names/schools + if @match + @wrestler1_name = @match.w1 ? @match.wrestler1.name : "Not assigned" + @wrestler1_school_name = @match.w1 ? @match.wrestler1.school.name : "N/A" + @wrestler2_name = @match.w2 ? @match.wrestler2.name : "Not assigned" + @wrestler2_school_name = @match.w2 ? @match.wrestler2.school.name : "N/A" + @tournament = @match.tournament + else + # Handle case where match isn't found, perhaps redirect or render error + redirect_to root_path, alert: "Match not found." + end + end # PATCH/PUT /matches/1 # PATCH/PUT /matches/1.json def update respond_to do |format| if @match.update(match_params) + # Broadcast the update + MatchChannel.broadcast_to( + @match, + { + w1_stat: @match.w1_stat, + w2_stat: @match.w2_stat, + score: @match.score, + win_type: @match.win_type, + winner_id: @match.winner_id, + winner_name: @match.winner&.name, + finished: @match.finished + } + ) + if session[:return_path] sanitized_return_path = sanitize_return_path(session[:return_path]) format.html { redirect_to sanitized_return_path, notice: 'Match was successfully updated.' } diff --git a/app/models/match.rb b/app/models/match.rb index 71de101..8c571c1 100644 --- a/app/models/match.rb +++ b/app/models/match.rb @@ -2,6 +2,7 @@ class Match < ApplicationRecord belongs_to :tournament, touch: true belongs_to :weight, touch: true belongs_to :mat, touch: true, optional: true + belongs_to :winner, class_name: 'Wrestler', foreign_key: 'winner_id', optional: true has_many :wrestlers, :through => :weight has_many :schools, :through => :wrestlers validate :score_validation, :win_type_validation, :bracket_position_validation, :overtime_type_validation diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index b4719d7..d3cf640 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -3,6 +3,7 @@ <% if params[:print] %> <%= csrf_meta_tags %> + <%= action_cable_meta_tag %> WrestlingDev <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> @@ -16,6 +17,7 @@ <% else %> WrestlingDev + <%= action_cable_meta_tag %> <% if Rails.env.production? %> <%= render 'layouts/analytics' %> diff --git a/app/views/matches/_matchstats.html.erb b/app/views/matches/_matchstats.html.erb index cbc2cad..845dd73 100644 --- a/app/views/matches/_matchstats.html.erb +++ b/app/views/matches/_matchstats.html.erb @@ -10,6 +10,7 @@ <% end %> +

Bout <%= @match.bout_number %>

<% if @show_next_bout_button && @next_match %> <%= link_to "Skip to Next Match for Mat #{@mat.name}", mat_path(@mat, bout_number: @next_match.bout_number), class: "btn btn-primary" %> @@ -165,13 +166,11 @@ <%= render 'matches/matchstats_color_change' %> diff --git a/app/views/matches/spectate.html.erb b/app/views/matches/spectate.html.erb new file mode 100644 index 0000000..6fe59c1 --- /dev/null +++ b/app/views/matches/spectate.html.erb @@ -0,0 +1,222 @@ +

Spectating Match: Bout <%= @match.bout_number %>

+

<%= @match.weight.max %> lbs

+

<%= @tournament.name %>

+ +
+ +
+
+

<%= @wrestler1_name %> (<%= @wrestler1_school_name %>)

+
+ Stats: +
<%= @match.w1_stat %>
+
+
+ +
+

<%= @wrestler2_name %> (<%= @wrestler2_school_name %>)

+
+ Stats: +
<%= @match.w2_stat %>
+
+
+ +
+

Result

+

Winner: <%= @match.winner_id ? @match.winner.name : '-' %>

+

Win Type: <%= @match.win_type || '-' %>

+

Score: <%= @match.score || '-' %>

+

Finished: <%= @match.finished ? 'Yes' : 'No' %>

+
+
+ + + + \ No newline at end of file diff --git a/app/views/tournaments/_bracket_final.html.erb b/app/views/tournaments/_bracket_final.html.erb index 67f8080..e5fa9ce 100644 --- a/app/views/tournaments/_bracket_final.html.erb +++ b/app/views/tournaments/_bracket_final.html.erb @@ -2,7 +2,7 @@
<%= match.w1_bracket_name.html_safe %>
-

<%= match.bout_number %> <%= match.bracket_score_string %>

<%= @winner_place %> Place Winner

+

<%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %>

<%= @winner_place %> Place Winner

<%= match.w2_bracket_name.html_safe %>
diff --git a/app/views/tournaments/_bracket_partial.html.erb b/app/views/tournaments/_bracket_partial.html.erb index a276604..e2ec0fd 100644 --- a/app/views/tournaments/_bracket_partial.html.erb +++ b/app/views/tournaments/_bracket_partial.html.erb @@ -65,6 +65,17 @@ table.smallText tr td { font-size: 10px; } text-align: center; /*padding-top: 15px;*/ } + +/* Style links within bout-number like default links */ +.bout-number a { + color: #007bff; /* Or your preferred link color */ + text-decoration: underline; +} +.bout-number a:hover { + color: #0056b3; /* Darker color on hover */ + text-decoration: underline; +} + .bracket-winner { border-bottom:1px solid #ddd; padding: 2px; diff --git a/app/views/tournaments/_bracket_round.html.erb b/app/views/tournaments/_bracket_round.html.erb index 826785a..1263b98 100644 --- a/app/views/tournaments/_bracket_round.html.erb +++ b/app/views/tournaments/_bracket_round.html.erb @@ -2,7 +2,7 @@ <% @round_matches.sort_by{|m| m.bracket_position_number}.each do |match| %>
<%= match.w1_bracket_name.html_safe %>
-
<%= match.bout_number %> <%= match.bracket_score_string %> 
+
<%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %> 
<%= match.w2_bracket_name.html_safe %>
<% end %> diff --git a/config/cable.yml b/config/cable.yml index e981a1c..b9adc5a 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -3,16 +3,15 @@ # not a terminal started via bin/rails console! Add "console" to any action or any ERB template view # to make the web console appear. development: - adapter: solid_cable - database: cable - polling_interval: 0.1.seconds - message_retention: 1.day + adapter: async test: adapter: test production: adapter: solid_cable - database: cable + connects_to: + database: + writing: cable polling_interval: 0.1.seconds - message_retention: 1.day \ No newline at end of file + message_retention: 1.day diff --git a/config/routes.rb b/config/routes.rb index b9d36a1..c5a6147 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,8 +1,16 @@ Wrestling::Application.routes.draw do + # Mount Action Cable server + mount ActionCable.server => '/cable' + resources :mats post "mats/:id/assign_next_match" => "mats#assign_next_match", :as => :assign_next_match - resources :matches + resources :matches do + member do + get :stat + get :spectate + end + end # Replace devise_for :users with custom routes get '/login', to: 'sessions#new' @@ -89,8 +97,6 @@ Wrestling::Application.routes.draw do get "/api/index" => "api#index" post "/api/tournaments/new" => "newTournament" - get "/matches/:id/stat" => "matches#stat", :as => :stat_match_path - resources :tournaments do member do post :generate_school_keys diff --git a/test/channels/match_channel_test.rb b/test/channels/match_channel_test.rb new file mode 100644 index 0000000..0aa4b00 --- /dev/null +++ b/test/channels/match_channel_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +class MatchChannelTest < ActionCable::Channel::TestCase + # test "subscribes" do + # subscribe + # assert subscription.confirmed? + # end +end