1
0
mirror of https://github.com/jcwimer/wrestlingApp synced 2026-03-25 01:14:43 +00:00

61 Commits

Author SHA1 Message Date
dependabot[bot]
e3fb68a932 Bump bcrypt from 3.1.21 to 3.1.22
Bumps [bcrypt](https://github.com/bcrypt-ruby/bcrypt-ruby) from 3.1.21 to 3.1.22.
- [Release notes](https://github.com/bcrypt-ruby/bcrypt-ruby/releases)
- [Changelog](https://github.com/bcrypt-ruby/bcrypt-ruby/blob/master/CHANGELOG)
- [Commits](https://github.com/bcrypt-ruby/bcrypt-ruby/compare/v3.1.21...v3.1.22)

---
updated-dependencies:
- dependency-name: bcrypt
  dependency-version: 3.1.22
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-19 18:58:35 +00:00
d359be3ea1 Fixed deprecations 2026-02-13 18:02:04 -05:00
e97aa0d680 Fixed N+1 on up_matches and added html cache. 2026-02-13 18:02:04 -05:00
ae8d995b2c Added a QR code page that generates a QR code for tournament directors to print out. 2026-02-11 18:23:14 -05:00
d57aaac09d Hide ads on schools#show, wrestlers#new, wrestlers#edit, and mats#show 2026-02-11 07:55:49 -05:00
fcc8a9b9a9 Updated to ruby 4.0.1 2026-02-10 17:58:22 -05:00
b51866e9d8 Added tests for hiding ads on lineup submission. 2026-02-09 18:36:56 -05:00
07d43e7720 Hide ads for coaches when submitting lineups 2026-02-08 18:59:42 -05:00
d8b6cfa8ac Added bundle audit to pipeline. 2026-02-05 18:40:38 -05:00
5d674f894f Added round number and bracket position under the bout number 2026-02-04 18:16:19 -05:00
25df2a7280 Updated to rails 8.1.2. 2026-02-04 18:16:19 -05:00
2767274066 Added queues for mats and provided a way for tournament directors to move matches to a mat. 2026-02-03 17:50:52 -05:00
a2f8c7bced Stats page should auto push stats when it reconnects to the websocket. Spectate page should auto pull when it reconnects to the websocket. 2026-01-29 17:28:14 -05:00
9c2a9d62ad Fixed random seeding for double elimination. Since bracket positions are already evenly distributed on top half and bottom half of the bracket, I only need to pick odd or even bracket line numbers. 2026-01-23 17:35:16 -05:00
556090c16b Fixed double elimination generate loser names for a 6 man bracket when we're placing top 8 2026-01-23 17:35:16 -05:00
86f9c03991 Fixed double elim match generation errors and added tests 2026-01-23 17:35:16 -05:00
c8764c149b Added back tournament import text for the development environment 2026-01-23 17:35:16 -05:00
fe9a9c628c Fix arguements for the tournament backup and import jobs 2026-01-22 16:59:44 -05:00
7e4b6d8fc8 Fix round 1 bracket name when the first round of the bracket is not the first round of the tournament 2026-01-19 23:25:15 -05:00
940f7b1d00 Job concurrency per tournament is 1 so we don't have to scale too much on active queue. Pages no longer refresh automatically after navigating away from the bout board. Tournament backups are no longer deleted when restoring from a backup. Cloudflare is blocking manual backup imports so I have deleted that form on the backups page. 2026-01-16 18:21:17 -05:00
52df73d14f Fixed random double elimination seeding to avoid double byes in round 1 and evenly distribute the number of round 1 matches from the top and bottom half of the bracket 2026-01-14 19:00:35 -05:00
8b03a74b1e Fixed the save seeds button on weights#show to work on mobile. Fixed the trashcan and edit icons on tournaments#show schools#show and weights#show to work on mobile. Destroy all tournament backups on tournament cleanup. Added bracket position to bout board. 2026-01-13 17:02:59 -05:00
b4bca8f10a Fixed calculate team scores button, fixed import button, fixed deleting a mat causing match deletes 2026-01-10 23:39:23 -05:00
af1f8df4b6 Fix print views 2026-01-09 23:06:24 -05:00
3576445a1c Added a turbo stream for the current and next match on mat stats page. 2026-01-09 18:37:01 -05:00
8c2ddf55ed Increased solid queue arguments limit to support tournament backups 2026-01-09 00:49:32 -05:00
cfd3e7aecd Fixed create new backup link syntax for turbo_method and made the assign_next_match button a turbo_method 2026-01-08 23:59:33 -05:00
608999cb51 Fixed create new backup link as a turbo_method and hid the baumspage importer 2026-01-08 23:51:16 -05:00
6b5308360e Fixed a bug where logged in users could not access a school with a school permission key 2026-01-06 17:24:45 -05:00
9ca6572d9b Need to bring services down before bringing them back up on deploy test 2025-12-11 14:17:27 -05:00
61dc5e3cdd Added mission control for solid queue ui. 2025-11-21 15:43:05 +05:30
af2fc3feba Fixed a test after changing links to turbo 2025-11-11 21:55:36 -05:00
793a9e3ecc All links with a confirm now use turbo 2025-11-11 21:09:24 -05:00
f73e9bfc4e Fix the reset bout board link 2025-11-11 20:55:34 -05:00
92bd06fe3c No longer using memcached. Replication settings for standalong mariadb. Use --single-transaction in mariadb replica-watcher so mysqldump does not lock tables. Added horizontal pod autoscaler to the app statefulset 2025-10-30 08:50:31 -04:00
6e9554be55 Fixed the JSON 3 deprecation in the backup and import service 2025-10-08 13:54:38 -04:00
34f1783031 Upgraded to rails 8.0.2 2025-10-08 11:35:44 -04:00
bbd2bd9b44 Fixed ads.txt 2025-10-07 15:31:04 -04:00
6ecebba70d Updated gems 2025-10-07 15:30:47 -04:00
e64751e471 Fixed name of db to replicate 2025-10-03 08:50:34 -04:00
d0f19e855f Added a mariadb replica watcher to fix replication issues 2025-09-30 16:31:43 -04:00
3e1ae22b6b Added pagination for the tournaments index page 2025-09-26 12:31:37 -04:00
15f85e439c Fixed finish_tournament_204.rake 2025-09-15 18:57:29 -04:00
c5b9783853 Fixed logout link 2025-09-15 18:32:55 -04:00
cd77268070 Trying to make the finish_tournament_204 rake job reliable 2025-09-15 18:28:35 -04:00
d61ed80287 Reload last_match and wrestler in advanced_wrestler. Moved calculate team score to the end of advance_wrestler. 2025-09-15 18:22:42 -04:00
dd5ce9bd60 Fixed my ads.txt to contain my publisher id for adsense 2025-09-15 17:31:04 -04:00
9a4e6f6597 Dynamic double elim match generation and loser name generation 2025-09-02 22:10:55 -04:00
782baedcfe Added support for 64 man brackets 2025-08-28 10:32:35 -04:00
Your Name
53e16952bf Added Stimulus and moved the matstats vanilla js to stimulus controllers. Same with the spectate page. 2025-05-20 17:22:48 -04:00
Your Name
0326d87261 Migrated from Sprockets to Propshaft. 2025-05-16 17:14:05 -04:00
5296b71bb9 Downloaded fontawesome locally instead of using CDN 2025-05-15 17:09:24 -04:00
58be6b8074 gitignore .DS_Store and update Gemfile 2025-05-15 13:58:02 -04:00
4accedbb43 Added a daily recurring job to cleanup tournaments. Fixed final score fields not loading without page refresh on mat stats page and added a cypress test for it. 2025-05-07 16:01:48 -04:00
2856060b11 Added cypress tests for mat stats javascript. 2025-05-05 19:57:03 -04:00
68a7b214c9 Fixed match stats when localstorage is empty but server data is not. 2025-04-28 16:35:24 -04:00
1fcaec876f Refactored match winner and wrestlers matches and fixed slow tests. 2025-04-25 15:59:35 -04:00
3e4317dbc5 Using action cable to send stats updates to the client and made a spectate page. 2025-04-21 17:12:54 -04:00
44fb5388b4 Made the all results page grouping better and fixed the advance wrestler job. 2025-04-17 17:34:34 -04:00
ed7186e5ce Moved the tournament navbar to the bottom of the page and made site responsive. Fixed puma solid queue in development. Added a note about clobbering assets in the README. Fixed the ad blocker check due to turbolinks it had to be idempotent. Added migrations for all dbs in the rails-dev-db-create.sh script. 2025-04-16 16:19:29 -04:00
6e61a7245a Added a separate table to record background job status for tournaments and fixed migrations/schemas for solid dbs. Foreign key constraints are now added to the migrations where we do belongs_to. 2025-04-15 16:16:44 -04:00
238 changed files with 29954 additions and 14439 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

11
.cursorrules Normal file
View File

@@ -0,0 +1,11 @@
- If rails isn't installed use docker: docker run -it -v $(pwd):/rails wrestlingdev-dev <rails command>
- If the docker image doesn't exist, use the build command: docker build -t wrestlingdev-dev -f deploy/rails-dev-Dockerfile .
- If the Gemfile changes, you need to rebuild the docker image: docker build -t wrestlingdev-dev -f deploy/rails-dev-Dockerfile.
- Do not add unnecessary comments to the code where you remove things.
- Cypress tests are created for js tests. They can be found in cypress-tests/cypress
- Cypress tests can be run with docker: bash cypress-tests/run-cypress-tests.sh
- Rails tests can be run with docker: docker run -it -v $(pwd):/rails wrestlingdev-dev rake test
- Write as little code as possible. I do not want crazy non standard rails implementations.
- This project is using propshaft and importmap.
- Stimulus is used for javascript.
- use context7

12
.gitignore vendored
View File

@@ -22,3 +22,15 @@ tmp
deploy/prod.env
frontend/node_modules
.aider*
# Ignore cypress test results
cypress-tests/cypress/screenshots
cypress-tests/cypress/videos
.DS_Store
# generated with npx repomix
# repomix-output.xml
# generated by cine mcp settings
~/

View File

@@ -1 +1 @@
wrestlingdev
wrestlingdev

View File

@@ -1 +1 @@
ruby-3.2.0
ruby-4.0.1

39
Gemfile
View File

@@ -1,14 +1,15 @@
source 'https://rubygems.org'
ruby '3.2.0'
ruby '4.0.1'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '8.0.2'
gem 'rails', '8.1.2'
# Added in rails 7.1
gem 'rails-html-sanitizer'
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem "sprockets-rails"
# Asset Management: Propshaft for serving, Importmap for JavaScript
gem "propshaft"
gem "importmap-rails"
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false
@@ -17,17 +18,19 @@ gem "bootsnap", require: false
# Use sqlite3 version compatible with Rails 8
gem 'sqlite3', ">= 2.1", :group => :development
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier'
# Use CoffeeScript for .js.coffee assets and views
gem 'coffee-rails'
# JavaScript and CSS related gems
# Uglifier is not used with Propshaft by default
# CoffeeScript (.js.coffee) files need to be converted to .js as Propshaft doesn't compile them
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby
# Use jquery as the JavaScript library
gem 'jquery-rails'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
gem 'turbolinks'
# Turbo for modern page interactions
gem 'turbo-rails'
# Stimulus for JavaScript behaviors
gem 'stimulus-rails'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder'
# bundle exec rake doc:rails generates the API under doc/api.
@@ -37,7 +40,7 @@ gem 'sdoc', :group => :doc
gem 'spring', :group => :development
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
gem 'bcrypt', '~> 3.1.22'
# Use unicorn as the app server
# gem 'unicorn'
@@ -64,25 +67,24 @@ gem 'influxdb-rails'
gem 'cancancan'
gem 'round_robin_tournament'
gem 'rb-readline'
gem 'rqrcode'
# Replacing Delayed Job with Solid Queue
# gem 'delayed_job_active_record'
gem 'solid_queue'
gem 'solid_cable'
gem 'puma'
gem 'passenger'
gem 'tzinfo-data'
gem 'daemons'
# Interface for viewing and managing background jobs
# gem 'delayed_job_web'
# Note: solid_queue-ui is not compatible with Rails 8.0 yet
# We'll create a custom UI or wait for compatibility updates
# gem 'solid_queue_ui', '~> 0.1.1'
# Solid Queue UI
gem "mission_control-jobs"
group :development do
# gem 'rubocop'
gem 'bullet'
gem 'brakeman'
gem 'bundler-audit'
gem 'rubocop'
end
group :development, :test do
@@ -90,6 +92,3 @@ group :development, :test do
# rails-controller-testing is needed for assert_template
gem 'rails-controller-testing'
end
gem 'font-awesome-sass'

View File

@@ -1,29 +1,31 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
action_text-trix (2.1.16)
railties
actioncable (8.1.2)
actionpack (= 8.1.2)
activesupport (= 8.1.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
actionmailbox (8.1.2)
actionpack (= 8.1.2)
activejob (= 8.1.2)
activerecord (= 8.1.2)
activestorage (= 8.1.2)
activesupport (= 8.1.2)
mail (>= 2.8.0)
actionmailer (8.0.2)
actionpack (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activesupport (= 8.0.2)
actionmailer (8.1.2)
actionpack (= 8.1.2)
actionview (= 8.1.2)
activejob (= 8.1.2)
activesupport (= 8.1.2)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.2)
actionview (= 8.0.2)
activesupport (= 8.0.2)
actionpack (8.1.2)
actionview (= 8.1.2)
activesupport (= 8.1.2)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@@ -31,130 +33,136 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.2)
actionpack (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
actiontext (8.1.2)
action_text-trix (~> 2.1.15)
actionpack (= 8.1.2)
activerecord (= 8.1.2)
activestorage (= 8.1.2)
activesupport (= 8.1.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.2)
activesupport (= 8.0.2)
actionview (8.1.2)
activesupport (= 8.1.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.2)
activesupport (= 8.0.2)
activejob (8.1.2)
activesupport (= 8.1.2)
globalid (>= 0.3.6)
activemodel (8.0.2)
activesupport (= 8.0.2)
activerecord (8.0.2)
activemodel (= 8.0.2)
activesupport (= 8.0.2)
activemodel (8.1.2)
activesupport (= 8.1.2)
activerecord (8.1.2)
activemodel (= 8.1.2)
activesupport (= 8.1.2)
timeout (>= 0.4.0)
activestorage (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activesupport (= 8.0.2)
activestorage (8.1.2)
actionpack (= 8.1.2)
activejob (= 8.1.2)
activerecord (= 8.1.2)
activesupport (= 8.1.2)
marcel (~> 1.0)
activesupport (8.0.2)
activesupport (8.1.2)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
json
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
base64 (0.2.0)
bcrypt (3.1.20)
benchmark (0.4.0)
bigdecimal (3.1.9)
bootsnap (1.18.4)
ast (2.4.3)
base64 (0.3.0)
bcrypt (3.1.22)
bigdecimal (4.0.1)
bootsnap (1.23.0)
msgpack (~> 1.2)
brakeman (7.0.2)
brakeman (8.0.2)
racc
builder (3.3.0)
bullet (8.0.3)
bullet (8.1.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundler-audit (0.9.2)
bundler (>= 1.2.0, < 3)
bundler-audit (0.9.3)
bundler (>= 1.2.0)
thor (~> 1.0)
cancancan (3.6.1)
coffee-rails (5.0.0)
coffee-script (>= 2.2.0)
railties (>= 5.2.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
chunky_png (1.4.0)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
crass (1.0.6)
daemons (1.4.1)
date (3.4.1)
drb (2.2.1)
date (3.5.1)
drb (2.2.3)
erb (6.0.1)
erubi (1.13.1)
et-orbi (1.2.11)
et-orbi (1.4.0)
tzinfo
execjs (2.10.0)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm-linux-gnu)
ffi (1.17.1-arm-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
font-awesome-sass (6.7.2)
sassc (~> 2.0)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
fugit (1.12.1)
et-orbi (~> 1.4)
raabro (~> 1.4)
globalid (1.2.1)
globalid (1.3.0)
activesupport (>= 6.1)
i18n (1.14.7)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
importmap-rails (2.2.3)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
influxdb (0.8.1)
influxdb-rails (1.0.3)
influxdb (~> 0.6, >= 0.6.4)
railties (>= 5.0)
io-console (0.8.0)
irb (1.15.2)
io-console (0.8.2)
irb (1.17.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jquery-rails (4.6.0)
jbuilder (2.14.1)
actionview (>= 7.0.0)
activesupport (>= 7.0.0)
jquery-rails (4.6.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.18.1)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
loofah (2.24.0)
loofah (2.25.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
mail (2.9.0)
logger
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.0.4)
marcel (1.1.0)
mini_mime (1.1.5)
minitest (5.25.5)
mocha (2.7.1)
minitest (6.0.1)
prism (~> 1.5)
mission_control-jobs (1.1.0)
actioncable (>= 7.1)
actionpack (>= 7.1)
activejob (>= 7.1)
activerecord (>= 7.1)
importmap-rails (>= 1.2.1)
irb (~> 1.13)
railties (>= 7.1)
stimulus-rails
turbo-rails
mocha (3.0.2)
ruby2_keywords (>= 0.0.5)
msgpack (1.8.0)
mysql2 (0.5.6)
net-imap (0.5.6)
mysql2 (0.5.7)
bigdecimal
net-imap (0.6.3)
date
net-protocol
net-pop (0.1.2)
@@ -163,64 +171,69 @@ GEM
timeout
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.7-aarch64-linux-gnu)
nio4r (2.7.5)
nokogiri (1.19.0-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.7-aarch64-linux-musl)
nokogiri (1.19.0-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.7-arm-linux-gnu)
nokogiri (1.19.0-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.7-arm-linux-musl)
nokogiri (1.19.0-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.7-arm64-darwin)
nokogiri (1.19.0-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.7-x86_64-darwin)
nokogiri (1.19.0-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.7-x86_64-linux-gnu)
nokogiri (1.19.0-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.7-x86_64-linux-musl)
nokogiri (1.19.0-x86_64-linux-musl)
racc (~> 1.4)
passenger (6.0.27)
rack (>= 1.6.13)
rackup (>= 1.0.1)
rake (>= 12.3.3)
pp (0.6.2)
parallel (1.27.0)
parser (3.3.10.1)
ast (~> 2.4.1)
racc
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
psych (5.2.3)
prism (1.9.0)
propshaft (1.3.1)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
psych (5.3.1)
date
stringio
puma (6.6.0)
puma (7.2.0)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.1.12)
rack-session (2.1.0)
rack (3.2.4)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.2.1)
rackup (2.3.1)
rack (>= 3)
rails (8.0.2)
actioncable (= 8.0.2)
actionmailbox (= 8.0.2)
actionmailer (= 8.0.2)
actionpack (= 8.0.2)
actiontext (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activemodel (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
rails (8.1.2)
actioncable (= 8.1.2)
actionmailbox (= 8.1.2)
actionmailer (= 8.1.2)
actionpack (= 8.1.2)
actiontext (= 8.1.2)
actionview (= 8.1.2)
activejob (= 8.1.2)
activemodel (= 8.1.2)
activerecord (= 8.1.2)
activestorage (= 8.1.2)
activesupport (= 8.1.2)
bundler (>= 1.15.0)
railties (= 8.0.2)
railties (= 8.1.2)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.2.0)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
@@ -232,79 +245,98 @@ GEM
rails_stdout_logging
rails_serve_static_assets (0.0.5)
rails_stdout_logging (0.0.5)
railties (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
railties (8.1.2)
actionpack (= 8.1.2)
activesupport (= 8.1.2)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6)
rake (13.2.1)
rainbow (3.1.1)
rake (13.3.1)
rb-readline (0.5.5)
rdoc (6.13.1)
rdoc (7.2.0)
erb
psych (>= 4.0.0)
reline (0.6.1)
tsort
regexp_parser (2.11.3)
reline (0.6.3)
io-console (~> 0.5)
round_robin_tournament (0.1.2)
rqrcode (3.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 2.0)
rqrcode_core (2.1.0)
rubocop (1.84.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.0)
parser (>= 3.3.7.2)
prism (~> 1.7)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
sassc (2.4.0)
ffi (~> 1.9)
sdoc (2.6.1)
sdoc (2.6.5)
rdoc (>= 5.0)
securerandom (0.4.1)
solid_cable (3.0.7)
solid_cable (3.0.12)
actioncable (>= 7.2)
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_cache (1.0.7)
solid_cache (1.0.10)
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_queue (1.1.4)
solid_queue (1.3.1)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
fugit (~> 1.11.0)
fugit (~> 1.11)
railties (>= 7.1)
thor (~> 1.3.1)
spring (4.3.0)
sprockets (4.2.1)
concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4)
sprockets-rails (3.5.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0)
sqlite3 (2.6.0-aarch64-linux-gnu)
sqlite3 (2.6.0-aarch64-linux-musl)
sqlite3 (2.6.0-arm-linux-gnu)
sqlite3 (2.6.0-arm-linux-musl)
sqlite3 (2.6.0-arm64-darwin)
sqlite3 (2.6.0-x86_64-darwin)
sqlite3 (2.6.0-x86_64-linux-gnu)
sqlite3 (2.6.0-x86_64-linux-musl)
stringio (3.1.6)
thor (1.3.2)
timeout (0.4.3)
turbolinks (5.2.1)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
thor (>= 1.3.1)
spring (4.4.2)
sqlite3 (2.9.0-aarch64-linux-gnu)
sqlite3 (2.9.0-aarch64-linux-musl)
sqlite3 (2.9.0-arm-linux-gnu)
sqlite3 (2.9.0-arm-linux-musl)
sqlite3 (2.9.0-arm64-darwin)
sqlite3 (2.9.0-x86_64-darwin)
sqlite3 (2.9.0-x86_64-linux-gnu)
sqlite3 (2.9.0-x86_64-linux-musl)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.2.0)
thor (1.5.0)
timeout (0.6.0)
tsort (0.2.0)
turbo-rails (2.0.23)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2025.2)
tzinfo-data (1.2025.3)
tzinfo (>= 1.0.0)
uglifier (4.2.1)
execjs (>= 0.3.0, < 3)
uniform_notifier (1.16.0)
uri (1.0.3)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
uniform_notifier (1.18.0)
uri (1.1.1)
useragent (0.16.11)
websocket-driver (0.7.7)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.7.2)
zeitwerk (2.7.4)
PLATFORMS
aarch64-linux-gnu
@@ -317,41 +349,42 @@ PLATFORMS
x86_64-linux-musl
DEPENDENCIES
bcrypt (~> 3.1.7)
bcrypt (~> 3.1.22)
bootsnap
brakeman
bullet
bundler-audit
cancancan
coffee-rails
daemons
font-awesome-sass
importmap-rails
influxdb-rails
jbuilder
jquery-rails
mission_control-jobs
mocha
mysql2
passenger
propshaft
puma
rails (= 8.0.2)
rails (= 8.1.2)
rails-controller-testing
rails-html-sanitizer
rails_12factor
rb-readline
round_robin_tournament
rqrcode
rubocop
sdoc
solid_cable
solid_cache
solid_queue
spring
sprockets-rails
sqlite3 (>= 2.1)
turbolinks
stimulus-rails
turbo-rails
tzinfo-data
uglifier
RUBY VERSION
ruby 3.2.0p0
ruby 4.0.1p0
BUNDLED WITH
2.6.7
4.0.3

View File

@@ -7,11 +7,13 @@ This application is being created to run a wrestling tournament.
**Public Production Url:** [https://wrestlingdev.com](https://wrestlingdev.com)
**App Info**
* Ruby 3.2.0
* Rails 8.0.0
* Ruby 4.0.1
* Rails 8.1.2
* DB MySQL/MariaDB
* Memcached
* Solid Queue for background job processing
* Solid Cache -> MySQL/MariaDB for html partial caching
* Solid Queue -> MySQL/MariaDB for background job processing
* Solid Cable -> MySQL/MariaDB for websocket channels
* Hotwired Stimulus for client-side JavaScript
# Development
@@ -32,11 +34,14 @@ In development environments, background jobs run inline (synchronously) by defau
To run a single test file:
1. Get a shell with ruby and rails: `bash bin/rails-dev-run.sh wrestlingdev-development`
2. `rake test TEST=test/models/match_test.rb`
2. `rake test TEST=test/models/match_test.rb` OR `rails test test/models/match_test.rb`
To run a single test inside a file:
1. Get a shell with ruby and rails: `bash bin/rails-dev-run.sh wrestlingdev-development`
2. `rake test TEST=test/models/match_test.rb TESTOPTS="--name='/test_Match_should_not_be_valid_if_an_incorrect_win_type_is_given/'"`
2. `rake test TEST=test/models/match_test.rb TESTOPTS="--name='/test_Match_should_not_be_valid_if_an_incorrect_win_type_is_given/'"` OR `rails test test/models/match_test.rb --name=/test_Match_should_not_be_valid_if_an_incorrect_win_type_is_given/`
To run tests in verbose mode (outputs the time for each test file and the test file name)
`rails test -v`
## Develop with rvm
With rvm installed, run `rvm install ruby-3.2.0`
@@ -77,6 +82,7 @@ Whether you have a shell from docker or are using rvm you can now run normal rai
* ` rails s -b 0.0.0.0` port 3000 is exposed. You can open [http://localhost:3000](http://localhost:3000) after running that command
* etc.
* `rake finish_seed_tournaments` will complete all matches from the seed data. This command takes about 5 minutes to execute
* `rake assets:clobber` - removes previously compiled assets stored in `public/assets` forcing Rails to recompile them from scratch the next time they are requested.
## Testing Job Status
@@ -105,9 +111,24 @@ See `SOLID_QUEUE.md` for more details about the job system.
Note: If updating rails, do not change the version in `Gemfile` until after you run `bash bin/rails-dev-run.sh wrestlingdev-dev`. Creating the container will fail due to a mismatch in Gemfile and Gemfile.lock.
Then run `rails app:update` to update rails.
## Stimulus Controllers
The application uses Hotwired Stimulus for client-side JavaScript interactivity. Controllers can be found in `app/asssets/javascripts/controllers`
### Testing Stimulus Controllers
The Stimulus controllers are tested using Cypress end-to-end tests:
```bash
# Run Cypress tests in headless mode
bash cypress-tests/run-cypress-tests.sh
```
# Deployment
The production version of this is currently deployed in Kubernetes. See [Deploying with Kubernetes](deploy/kubernetes/README.md)
The production version of this is currently deployed in Kubernetes (via K3s). See [Deploying with Kubernetes](deploy/kubernetes/README.md)
I'm using a Hetzner dedicated server with an i7-8700, 500GB NVME (RAID1), and 64GB ECC RAM. I have a hot standby (SQL read only replication) in my homelab.
## Server Configuration
@@ -121,11 +142,6 @@ The application uses an intelligent auto-scaling configuration for Puma (the web
- **SolidQueue Integration**: When `SOLID_QUEUE_IN_PUMA=true`, background jobs run within the Puma process.
- **Database Connection Pool**: Automatically sized based on the maximum number of threads across all workers.
The configuration is designed to adapt to different environments:
- Small servers: Uses fewer workers to avoid memory exhaustion
- Large servers: Scales up to utilize available CPU cores
- Development: Uses a single worker for simplicity
All of these settings can be overridden with environment variables if needed.
To see the current configuration in the logs, look for these lines on startup:
@@ -135,6 +151,9 @@ Available system resources: X CPU(s), YMMMB RAM
SolidQueue plugin enabled in Puma
```
I have deployed Mission Control as a UI for SolidQueue. The uri for mission control is `/jobs`.
For the development environment, the user/password is dev/secret. For the production environment, it is defined by environment variables WRESTLINGDEV_MISSION_CONTROL_USER/WRESTLINGDEV_MISSION_CONTROL_PASSWORD. You can see this in `config/environments/production.rb` and `config/environments/development.rb`.
## Environment Variables
### Required Environment Variables
@@ -147,6 +166,8 @@ SolidQueue plugin enabled in Puma
* `WRESTLINGDEV_SECRET_KEY_BASE` - Rails application secret key (can be generated with `rake secret`)
* `WRESTLINGDEV_EMAIL` - Email address (currently must be a Gmail account)
* `WRESTLINGDEV_EMAIL_PWD` - Email password
* `WRESTLINGDEV_MISSION_CONTROL_USER` - mission control username
* `WRESTLINGDEV_MISSION_CONTROL_PASSWORD` - mission control password
### Optional Environment Variables
* `SOLID_QUEUE_IN_PUMA` - Set to "true" to run Solid Queue workers inside Puma (default in development)
@@ -171,26 +192,29 @@ SolidQueue plugin enabled in Puma
* `WRESTLINGDEV_INFLUXDB_USERNAME` - InfluxDB username (optional)
* `WRESTLINGDEV_INFLUXDB_PASSWORD` - InfluxDB password (optional)
See `SOLID_QUEUE.md` for details about the job system configuration.
This project provides multiple ways to develop and deploy, with Docker being the primary method.
# AI Assistant Note
# Frontend Assets
<!--
This section contains information specifically for AI code assistants to help understand the codebase structure:
## Sprockets to Propshaft Migration
1. Project type: Rails 8 application for managing wrestling tournaments
2. Key components:
- Database: MySQL/MariaDB in production, SQLite in development
- Background jobs: SolidQueue running in Puma (SOLID_QUEUE_IN_PUMA=true)
- Multiple databases: One each for main app, queue, cache, and cable
3. Development paths:
- Docker-based: Primary method using deploy/rails-dev-Dockerfile
- RVM-based: Alternative for local development
4. Important services:
- Tournament management (tournaments, matches, wrestlers)
- Background job processing (SolidQueue)
- User authentication (Devise)
5. Deployment: Kubernetes-based with environment variables
-->
- Propshaft will automatically include in its search paths the folders vendor/assets, lib/assets and app/assets of your project and of all the gems in your Gemfile. You can see all included files by using the reveal rake task: `rake assets:reveal`. When importing you'll use the relative path from this command.
- All css files are imported via `app/assets/stylesheets/application.css`. This is imported on `app/views/layouts/application.html.erb`.
- Bootstrap and fontawesome have been downloaded locally to `vendor/`
- All js files are imported with a combination of "pinning" with `config/importmaps.rb` and `app/assets/javascript/application.js` and imported to `app/views/layouts/application.html.erb`
- Jquery, bootstrap, datatables have been downloaded locally to `vendor/`
- Turbo and action cable are gems and get pathed properly by propshaft.
- development is "nobuild" with `config.assets.build_assets = false` in `config/environments/development.rb`
- production needs to run rake assets:precompile. This is done in the `deploy/rails-prod-Dockerfile`.
This project provides multiple ways to develop and deploy, with Docker being the primary method.
## Stimulus Implementation
The application has been migrated from using vanilla JavaScript to Hotwired Stimulus. The Stimulus controllers are organized in:
- `app/assets/javascripts/controllers/` - Contains all Stimulus controllers
- `app/assets/javascripts/application.js` - Registers and loads all controllers
The importmap configuration in `config/importmap.rb` handles the loading of all JavaScript dependencies including Stimulus controllers.
# Using Repomix with LLMs
`npx repomix app test`

View File

@@ -1,288 +0,0 @@
# SolidQueue, SolidCache, and SolidCable Setup
This application uses Rails 8's built-in background job processing, caching, and ActionCable features with separate dedicated databases.
## Database Configuration
We use separate databases for the main application, SolidQueue, SolidCache, and SolidCable. This ensures complete separation and avoids any conflicts or performance issues.
In `config/database.yml`, we have the following setup:
```yaml
development:
primary:
database: db/development.sqlite3
queue:
database: db/development-queue.sqlite3
cache:
database: db/development-cache.sqlite3
cable:
database: db/development-cable.sqlite3
test:
primary:
database: db/test.sqlite3
queue:
database: db/test-queue.sqlite3
cache:
database: db/test-cache.sqlite3
cable:
database: db/test-cable.sqlite3
production:
primary:
database: <%= ENV['WRESTLINGDEV_DB_NAME'] %>
queue:
database: <%= ENV['WRESTLINGDEV_DB_NAME'] %>-queue
cache:
database: <%= ENV['WRESTLINGDEV_DB_NAME'] %>-cache
cable:
database: <%= ENV['WRESTLINGDEV_DB_NAME'] %>-cable
```
## Migration Structure
Migrations for each database are stored in their respective directories:
- Main application migrations: `db/migrate/`
- SolidQueue migrations: `db/queue/migrate/`
- SolidCache migrations: `db/cache/migrate/`
- SolidCable migrations: `db/cable/migrate/`
## Running Migrations
When deploying the application, you need to run migrations for each database separately:
```bash
# Run main application migrations
rails db:migrate
# Run SolidQueue migrations
rails db:migrate:queue
# Run SolidCache migrations
rails db:migrate:cache
# Run SolidCable migrations
rails db:migrate:cable
```
## Environment Configuration
In the environment configuration files (`config/environments/*.rb`), we've configured the paths for migrations and set up the appropriate adapters:
```ruby
# SolidCache configuration
config.cache_store = :solid_cache_store
config.paths["db/migrate"] << "db/cache/migrate"
# SolidQueue configuration
config.active_job.queue_adapter = :solid_queue
config.paths["db/migrate"] << "db/queue/migrate"
# ActionCable configuration
config.paths["db/migrate"] << "db/cable/migrate"
```
The database connections are configured in their respective YAML files:
### config/cache.yml
```yaml
production:
database: cache
# other options...
```
### config/queue.yml
```yaml
production:
database: queue
# other options...
```
### config/cable.yml
```yaml
production:
adapter: solid_cable
database: cable
# other options...
```
## SolidQueue Configuration
SolidQueue is used for background job processing in all environments except test. The application is configured to run jobs as follows:
### Development and Production
In both development and production environments, SolidQueue is configured to process jobs asynchronously. This provides consistent behavior across environments while maintaining performance.
### Test
In the test environment only, jobs are executed synchronously using the inline adapter. This makes testing more predictable and avoids the need for separate worker processes during tests.
Configuration is in `config/initializers/solid_queue.rb`:
```ruby
# Configure ActiveJob queue adapter based on environment
if Rails.env.test?
# In test, use inline adapter for simplicity and predictability
Rails.application.config.active_job.queue_adapter = :inline
else
# In development and production, use solid_queue with async execution
Rails.application.config.active_job.queue_adapter = :solid_queue
# Configure for regular async processing
Rails.application.config.active_job.queue_adapter_options = {
execution_mode: :async,
logger: Rails.logger
}
end
```
## Running with Puma
By default, the application is configured to run SolidQueue workers within the Puma processes. This is done by setting the `SOLID_QUEUE_IN_PUMA` environment variable to `true` in the production Dockerfile, which enables the Puma plugin for SolidQueue.
This means you don't need to run separate worker processes in production - the same Puma processes that handle web requests also handle background jobs. This simplifies deployment and reduces resource requirements.
The application uses an intelligent auto-scaling configuration for SolidQueue when running in Puma:
1. **Auto Detection**: The Puma configuration automatically detects available CPU cores and memory
2. **Worker Scaling**: Puma workers are calculated based on available memory and CPU cores
3. **SolidQueue Integration**: When enabled, SolidQueue simply runs within the Puma process
You can enable SolidQueue in Puma by setting:
```bash
SOLID_QUEUE_IN_PUMA=true
```
In `config/puma.rb`:
```ruby
# Run the Solid Queue supervisor inside of Puma for single-server deployments
if ENV["SOLID_QUEUE_IN_PUMA"] == "true" && !Rails.env.test?
# Simply load the SolidQueue plugin with default settings
plugin :solid_queue
# Log that SolidQueue is enabled
puts "SolidQueue plugin enabled in Puma"
end
```
On startup, you'll see a log message confirming that SolidQueue is enabled in Puma.
## Job Owner Tracking
Jobs in this application include metadata about their owner (typically a tournament) to allow tracking and displaying job status to tournament directors. Each job includes:
- `job_owner_id`: Usually the tournament ID
- `job_owner_type`: A description of the job (e.g., "Create a backup")
This information is serialized with the job and can be used to filter and display jobs on tournament pages.
## Job Pattern: Simplified Job Enqueuing
Our job classes follow a simplified pattern that works consistently across all environments:
1. Service classes always use `perform_later` to enqueue jobs
2. The execution mode is determined centrally by the ActiveJob adapter configuration
3. Each job finds the needed records and calls the appropriate method on the service class or model
Example service class method:
```ruby
def create_backup
# Set up job owner information for tracking
job_owner_id = @tournament.id
job_owner_type = "Create a backup"
# Use perform_later which will execute based on centralized adapter config
TournamentBackupJob.perform_later(@tournament.id, @reason, job_owner_id, job_owner_type)
end
```
Example job class:
```ruby
class TournamentBackupJob < ApplicationJob
queue_as :default
# For storing job owner metadata
attr_accessor :job_owner_id, :job_owner_type
# For execution via job queue with IDs
def perform(tournament_id, reason, job_owner_id = nil, job_owner_type = nil)
# Store job owner metadata
self.job_owner_id = job_owner_id
self.job_owner_type = job_owner_type
# Find the record
tournament = Tournament.find_by(id: tournament_id)
return unless tournament
# Create the service class and call the raw method
TournamentBackupService.new(tournament, reason).create_backup_raw
end
end
```
## Job Classes
The following job classes are available:
- `AdvanceWrestlerJob`: For advancing wrestlers in brackets
- `TournamentBackupJob`: For creating tournament backups
- `WrestlingdevImportJob`: For importing from wrestlingdev
- `GenerateTournamentMatchesJob`: For generating tournament matches
- `CalculateSchoolScoreJob`: For calculating school scores
## Job Status
Jobs in this application can have the following statuses:
1. **Running**: Job is currently being executed. This is determined by checking if a record exists in the `solid_queue_claimed_executions` table for the job.
2. **Scheduled**: Job is scheduled to run at a future time. This is determined by checking if `scheduled_at` is in the future.
3. **Error**: Job has failed. This is determined by:
- Checking if a record exists in the `solid_queue_failed_executions` table for the job
- Checking if `failed_at` is present
4. **Completed**: Job has finished successfully. This is determined by checking if `finished_at` is present and no error records exist.
5. **Pending**: Job is waiting to be picked up by a worker. This is the default status when none of the above conditions are met.
## Testing Job Status
To help with testing the job status display in the UI, several rake tasks are provided:
```bash
# Create a test "Running" job for the first tournament
rails jobs:create_running
# Create a test "Completed" job for the first tournament
rails jobs:create_completed
# Create a test "Error" job for the first tournament
rails jobs:create_failed
```
## Troubleshooting
If you encounter issues with SolidQueue or the separate databases:
1. Make sure all databases exist:
```sql
CREATE DATABASE IF NOT EXISTS wrestlingtourney;
CREATE DATABASE IF NOT EXISTS wrestlingtourney-queue;
CREATE DATABASE IF NOT EXISTS wrestlingtourney-cache;
CREATE DATABASE IF NOT EXISTS wrestlingtourney-cable;
```
2. Ensure all migrations have been run for each database.
3. Check that environment configurations properly connect to the right databases.
4. Verify that the database user has appropriate permissions for all databases.
5. If jobs aren't processing in production, check that `SOLID_QUEUE_IN_PUMA` is set to `true` in your environment.
## References
- [SolidQueue README](https://github.com/rails/solid_queue)
- [Rails Multiple Database Configuration](https://guides.rubyonrails.org/active_record_multiple_databases.html)

View File

@@ -1,3 +1,11 @@
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
// Link all .js files from app/javascript for importmap, and vendor/javascript if needed directly
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/assets/javascripts .js
// Explicitly link all .css files from app/assets/stylesheets and vendor/assets/stylesheets
//= link_tree ../stylesheets .css
//= link_tree ../../../vendor/assets/stylesheets .css
//= link_tree ../../../vendor/assets/webfonts

View File

@@ -1,3 +0,0 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -1,3 +0,0 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -1,20 +1,47 @@
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require_tree .
//= require jquery
//= require jquery_ujs
// Bootstrap 3.3.6 in vendor/assets/javascripts
//= require bootstrap.min.js
// Data Tables 1.10.6 in vendor/assets/javascripts
//= require jquery.dataTables.min.js
// Entry point for your JavaScript application
// These are pinned in config/importmap.rb
import "@hotwired/turbo-rails";
import { createConsumer } from "@rails/actioncable"; // Import createConsumer directly
import "jquery";
import "bootstrap";
import "datatables.net";
// Stimulus setup
import { Application } from "@hotwired/stimulus";
// Initialize Stimulus application
const application = Application.start();
window.Stimulus = application;
// Load all controllers from app/assets/javascripts/controllers
// Import controllers manually
import WrestlerColorController from "controllers/wrestler_color_controller";
import MatchScoreController from "controllers/match_score_controller";
import MatchDataController from "controllers/match_data_controller";
import MatchSpectateController from "controllers/match_spectate_controller";
// Register controllers
application.register("wrestler-color", WrestlerColorController);
application.register("match-score", MatchScoreController);
application.register("match-data", MatchDataController);
application.register("match-spectate", MatchSpectateController);
// Your existing Action Cable consumer setup
(function() {
try {
window.App || (window.App = {});
window.App.cable = createConsumer(); // Use the imported createConsumer
console.log('Action Cable Consumer Created via app/assets/javascripts/application.js');
} catch (e) {
console.error('Error creating ActionCable consumer:', e);
console.error('ActionCable not loaded or createConsumer failed, App.cable not created.');
}
}).call(this);
console.log("Propshaft/Importmap application.js initialized with jQuery, Bootstrap, Stimulus, and DataTables.");
// If you have custom JavaScript files in app/javascript/ that were previously
// handled by Sprockets `require_tree`, you'll need to import them here explicitly.
// For example:
// import "./my_custom_logic";

View File

@@ -0,0 +1,420 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [
"w1Stat", "w2Stat", "statusIndicator"
]
static values = {
tournamentId: Number,
boutNumber: Number,
matchId: Number
}
connect() {
console.log("Match data controller connected")
this.isConnected = false
this.pendingLocalSync = { w1: false, w2: false }
this.w1 = {
name: "w1",
stats: "",
updated_at: null,
timers: {
"injury": { time: 0, startTime: null, interval: null },
"blood": { time: 0, startTime: null, interval: null }
}
}
this.w2 = {
name: "w2",
stats: "",
updated_at: null,
timers: {
"injury": { time: 0, startTime: null, interval: null },
"blood": { time: 0, startTime: null, interval: null }
}
}
// Initial values
this.updateJsValues()
// Set up debounced handlers for text areas
this.debouncedW1Handler = this.debounce((el) => this.handleTextAreaInput(el, this.w1), 400)
this.debouncedW2Handler = this.debounce((el) => this.handleTextAreaInput(el, this.w2), 400)
// Set up text area event listeners
this.w1StatTarget.addEventListener('input', (event) => this.debouncedW1Handler(event.target))
this.w2StatTarget.addEventListener('input', (event) => this.debouncedW2Handler(event.target))
// Initialize from local storage
this.initializeFromLocalStorage()
// Setup ActionCable
if (this.matchIdValue) {
this.setupSubscription(this.matchIdValue)
}
}
disconnect() {
this.cleanupSubscription()
}
// Match stats core functionality
updateStats(wrestler, text) {
if (!wrestler) {
console.error("updateStats called with undefined wrestler")
return
}
wrestler.stats += text + " "
wrestler.updated_at = new Date().toISOString()
this.updateHtmlValues()
this.saveToLocalStorage(wrestler)
if (!this.isConnected) this.pendingLocalSync[wrestler.name] = true
// Send the update via Action Cable if subscribed
if (this.matchSubscription) {
let payload = {}
if (wrestler.name === 'w1') payload.new_w1_stat = wrestler.stats
else if (wrestler.name === 'w2') payload.new_w2_stat = wrestler.stats
if (Object.keys(payload).length > 0) {
console.log('[ActionCable] updateStats performing send_stat:', payload)
this.matchSubscription.perform('send_stat', payload)
}
} else {
console.warn('[ActionCable] updateStats called but matchSubscription is null.')
}
}
// Specific methods for updating each wrestler
updateW1Stats(event) {
const text = event.currentTarget.dataset.matchDataText || ''
this.updateStats(this.w1, text)
}
updateW2Stats(event) {
const text = event.currentTarget.dataset.matchDataText || ''
this.updateStats(this.w2, text)
}
// End period action
endPeriod() {
this.updateStats(this.w1, '|End Period|')
this.updateStats(this.w2, '|End Period|')
}
handleTextAreaInput(textAreaElement, wrestler) {
const newValue = textAreaElement.value
console.log(`Text area input detected for ${wrestler.name}:`, newValue.substring(0, 50) + "...")
// Update the internal JS object
wrestler.stats = newValue
wrestler.updated_at = new Date().toISOString()
if (!this.isConnected) this.pendingLocalSync[wrestler.name] = true
// Save to localStorage
this.saveToLocalStorage(wrestler)
// Send the update via Action Cable if subscribed
if (this.matchSubscription) {
let payload = {}
if (wrestler.name === 'w1') {
payload.new_w1_stat = wrestler.stats
} else if (wrestler.name === 'w2') {
payload.new_w2_stat = wrestler.stats
}
if (Object.keys(payload).length > 0) {
console.log('[ActionCable] Performing send_stat from textarea with payload:', payload)
this.matchSubscription.perform('send_stat', payload)
}
}
}
// Timer functions
startTimer(wrestler, timerKey) {
const timer = wrestler.timers[timerKey]
if (timer.interval) return // Prevent multiple intervals
timer.startTime = Date.now()
timer.interval = setInterval(() => {
const elapsedSeconds = Math.floor((Date.now() - timer.startTime) / 1000)
this.updateTimerDisplay(wrestler, timerKey, timer.time + elapsedSeconds)
}, 1000)
}
stopTimer(wrestler, timerKey) {
const timer = wrestler.timers[timerKey]
if (!timer.interval || !timer.startTime) return
clearInterval(timer.interval)
const elapsedSeconds = Math.floor((Date.now() - timer.startTime) / 1000)
timer.time += elapsedSeconds
timer.interval = null
timer.startTime = null
this.saveToLocalStorage(wrestler)
this.updateTimerDisplay(wrestler, timerKey, timer.time)
this.updateStatsBox(wrestler, timerKey, elapsedSeconds)
}
resetTimer(wrestler, timerKey) {
const timer = wrestler.timers[timerKey]
this.stopTimer(wrestler, timerKey)
timer.time = 0
this.updateTimerDisplay(wrestler, timerKey, 0)
this.saveToLocalStorage(wrestler)
}
// Timer control methods for W1
startW1InjuryTimer() {
this.startTimer(this.w1, 'injury')
}
stopW1InjuryTimer() {
this.stopTimer(this.w1, 'injury')
}
resetW1InjuryTimer() {
this.resetTimer(this.w1, 'injury')
}
startW1BloodTimer() {
this.startTimer(this.w1, 'blood')
}
stopW1BloodTimer() {
this.stopTimer(this.w1, 'blood')
}
resetW1BloodTimer() {
this.resetTimer(this.w1, 'blood')
}
// Timer control methods for W2
startW2InjuryTimer() {
this.startTimer(this.w2, 'injury')
}
stopW2InjuryTimer() {
this.stopTimer(this.w2, 'injury')
}
resetW2InjuryTimer() {
this.resetTimer(this.w2, 'injury')
}
startW2BloodTimer() {
this.startTimer(this.w2, 'blood')
}
stopW2BloodTimer() {
this.stopTimer(this.w2, 'blood')
}
resetW2BloodTimer() {
this.resetTimer(this.w2, 'blood')
}
updateTimerDisplay(wrestler, timerKey, totalTime) {
const elementId = `${wrestler.name}-${timerKey}-time`
const element = document.getElementById(elementId)
if (element) {
element.innerText = `${Math.floor(totalTime / 60)}m ${totalTime % 60}s`
}
}
updateStatsBox(wrestler, timerKey, elapsedSeconds) {
const timerType = timerKey.includes("injury") ? "Injury Time" : "Blood Time"
const formattedTime = `${Math.floor(elapsedSeconds / 60)}m ${elapsedSeconds % 60}s`
this.updateStats(wrestler, `${timerType}: ${formattedTime}`)
}
// Utility functions
generateKey(wrestler_name) {
return `${wrestler_name}-${this.tournamentIdValue}-${this.boutNumberValue}`
}
loadFromLocalStorage(wrestler_name) {
const key = this.generateKey(wrestler_name)
const data = localStorage.getItem(key)
return data ? JSON.parse(data) : null
}
saveToLocalStorage(person) {
const key = this.generateKey(person.name)
const data = {
stats: person.stats,
updated_at: person.updated_at,
timers: person.timers
}
localStorage.setItem(key, JSON.stringify(data))
}
updateHtmlValues() {
if (this.w1StatTarget) this.w1StatTarget.value = this.w1.stats
if (this.w2StatTarget) this.w2StatTarget.value = this.w2.stats
}
updateJsValues() {
if (this.w1StatTarget) this.w1.stats = this.w1StatTarget.value
if (this.w2StatTarget) this.w2.stats = this.w2StatTarget.value
}
debounce(func, wait) {
let timeout
return (...args) => {
clearTimeout(timeout)
timeout = setTimeout(() => func(...args), wait)
}
}
initializeTimers(wrestler) {
for (const timerKey in wrestler.timers) {
this.updateTimerDisplay(wrestler, timerKey, wrestler.timers[timerKey].time)
}
}
initializeFromLocalStorage() {
const w1Data = this.loadFromLocalStorage('w1')
if (w1Data) {
this.w1.stats = w1Data.stats || ''
this.w1.updated_at = w1Data.updated_at
if (w1Data.timers) this.w1.timers = w1Data.timers
this.initializeTimers(this.w1)
}
const w2Data = this.loadFromLocalStorage('w2')
if (w2Data) {
this.w2.stats = w2Data.stats || ''
this.w2.updated_at = w2Data.updated_at
if (w2Data.timers) this.w2.timers = w2Data.timers
this.initializeTimers(this.w2)
}
this.updateHtmlValues()
}
cleanupSubscription() {
if (this.matchSubscription) {
console.log(`[Stats AC Cleanup] Unsubscribing from match channel.`)
try {
this.matchSubscription.unsubscribe()
} catch (e) {
console.error(`[Stats AC Cleanup] Error during unsubscribe:`, e)
}
this.matchSubscription = null
}
}
setupSubscription(matchId) {
this.cleanupSubscription()
console.log(`[Stats AC Setup] Attempting subscription for match ID: ${matchId}`)
// Update status indicator
if (this.statusIndicatorTarget) {
this.statusIndicatorTarget.innerText = "Connecting to server for real-time stat updates..."
this.statusIndicatorTarget.classList.remove('alert-success', 'alert-warning', 'alert-danger')
this.statusIndicatorTarget.classList.add('alert-info')
}
// Exit if we don't have App.cable
if (!window.App || !window.App.cable) {
console.error(`[Stats AC Setup] Error: App.cable is not available.`)
if (this.statusIndicatorTarget) {
this.statusIndicatorTarget.innerText = "Error: WebSockets unavailable. Stats won't update in real-time."
this.statusIndicatorTarget.classList.remove('alert-info', 'alert-success', 'alert-warning')
this.statusIndicatorTarget.classList.add('alert-danger')
}
return
}
this.matchSubscription = App.cable.subscriptions.create(
{
channel: "MatchChannel",
match_id: matchId
},
{
connected: () => {
console.log(`[Stats AC] Connected to MatchStatsChannel for match ID: ${matchId}`)
this.isConnected = true
if (this.statusIndicatorTarget) {
this.statusIndicatorTarget.innerText = "Connected: Stats will update in real-time."
this.statusIndicatorTarget.classList.remove('alert-info', 'alert-warning', 'alert-danger')
this.statusIndicatorTarget.classList.add('alert-success')
}
this.sendCurrentStatsOnReconnect()
},
disconnected: () => {
console.log(`[Stats AC] Disconnected from MatchStatsChannel`)
this.isConnected = false
if (this.statusIndicatorTarget) {
this.statusIndicatorTarget.innerText = "Disconnected: Stats updates paused."
this.statusIndicatorTarget.classList.remove('alert-info', 'alert-success', 'alert-danger')
this.statusIndicatorTarget.classList.add('alert-warning')
}
},
received: (data) => {
console.log(`[Stats AC] Received data:`, data)
// Update w1 stats
if (data.w1_stat !== undefined && this.w1StatTarget) {
console.log(`[Stats AC] Updating w1_stat: ${data.w1_stat.substring(0, 30)}...`)
if (!this.pendingLocalSync.w1 || data.w1_stat === this.w1.stats) {
this.w1.stats = data.w1_stat
this.w1StatTarget.value = data.w1_stat
this.pendingLocalSync.w1 = false
} else {
console.log('[Stats AC] Skipping w1_stat overwrite due to pending local changes.')
}
}
// Update w2 stats
if (data.w2_stat !== undefined && this.w2StatTarget) {
console.log(`[Stats AC] Updating w2_stat: ${data.w2_stat.substring(0, 30)}...`)
if (!this.pendingLocalSync.w2 || data.w2_stat === this.w2.stats) {
this.w2.stats = data.w2_stat
this.w2StatTarget.value = data.w2_stat
this.pendingLocalSync.w2 = false
} else {
console.log('[Stats AC] Skipping w2_stat overwrite due to pending local changes.')
}
}
},
receive_error: (error) => {
console.error(`[Stats AC] Error:`, error)
this.matchSubscription = null
if (this.statusIndicatorTarget) {
this.statusIndicatorTarget.innerText = "Error: Connection issue. Stats won't update in real-time."
this.statusIndicatorTarget.classList.remove('alert-info', 'alert-success', 'alert-warning')
this.statusIndicatorTarget.classList.add('alert-danger')
}
}
}
)
}
sendCurrentStatsOnReconnect() {
if (!this.matchSubscription) return
const payload = {}
if (typeof this.w1?.stats === 'string' && this.w1.stats.length > 0) {
payload.new_w1_stat = this.w1.stats
this.pendingLocalSync.w1 = true
}
if (typeof this.w2?.stats === 'string' && this.w2.stats.length > 0) {
payload.new_w2_stat = this.w2.stats
this.pendingLocalSync.w2 = true
}
if (Object.keys(payload).length > 0) {
console.log('[ActionCable] Reconnect sync: sending current stats payload:', payload)
this.matchSubscription.perform('send_stat', payload)
} else {
console.log('[ActionCable] Reconnect sync: no local stats to send.')
}
}
}

View File

@@ -0,0 +1,237 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [
"winType", "winnerSelect", "submitButton", "dynamicScoreInput",
"finalScoreField", "validationAlerts", "pinTimeTip"
]
static values = {
winnerScore: { type: String, default: "0" },
loserScore: { type: String, default: "0" }
}
connect() {
console.log("Match score controller connected")
// Use setTimeout to ensure the DOM is fully loaded
setTimeout(() => {
this.updateScoreInput()
this.validateForm()
}, 50)
}
winTypeChanged() {
this.updateScoreInput()
this.validateForm()
}
winnerChanged() {
this.validateForm()
}
updateScoreInput() {
const winType = this.winTypeTarget.value
this.dynamicScoreInputTarget.innerHTML = ""
// Add section header
const header = document.createElement("h5")
header.innerText = `Score Input for ${winType}`
header.classList.add("mt-2", "mb-3")
this.dynamicScoreInputTarget.appendChild(header)
if (winType === "Pin") {
this.pinTimeTipTarget.style.display = "block"
const minuteInput = this.createTextInput("minutes", "Minutes (MM)", "Pin Time Minutes")
const secondInput = this.createTextInput("seconds", "Seconds (SS)", "Pin Time Seconds")
this.dynamicScoreInputTarget.appendChild(minuteInput)
this.dynamicScoreInputTarget.appendChild(secondInput)
// Add event listeners to the new inputs
const inputs = this.dynamicScoreInputTarget.querySelectorAll("input")
inputs.forEach(input => {
input.addEventListener("input", () => {
this.updatePinTimeScore()
this.validateForm()
})
})
this.updatePinTimeScore()
} else if (["Decision", "Major", "Tech Fall"].includes(winType)) {
this.pinTimeTipTarget.style.display = "none"
const winnerScoreInput = this.createTextInput(
"winner-score",
"Winner's Score",
"Enter the winner's score"
)
const loserScoreInput = this.createTextInput(
"loser-score",
"Loser's Score",
"Enter the loser's score"
)
this.dynamicScoreInputTarget.appendChild(winnerScoreInput)
this.dynamicScoreInputTarget.appendChild(loserScoreInput)
// Restore stored values
const winnerInput = winnerScoreInput.querySelector("input")
const loserInput = loserScoreInput.querySelector("input")
winnerInput.value = this.winnerScoreValue
loserInput.value = this.loserScoreValue
// Add event listeners to the new inputs
winnerInput.addEventListener("input", (event) => {
this.winnerScoreValue = event.target.value || "0"
this.updatePointScore()
this.validateForm()
})
loserInput.addEventListener("input", (event) => {
this.loserScoreValue = event.target.value || "0"
this.updatePointScore()
this.validateForm()
})
this.updatePointScore()
} else {
// For other types (forfeit, etc.), clear the input and hide pin time tip
this.pinTimeTipTarget.style.display = "none"
this.finalScoreFieldTarget.value = ""
// Show message for non-score win types
const message = document.createElement("p")
message.innerText = `No score required for ${winType} win type.`
message.classList.add("text-muted")
this.dynamicScoreInputTarget.appendChild(message)
}
this.validateForm()
}
updatePinTimeScore() {
const minuteInput = this.dynamicScoreInputTarget.querySelector("#minutes")
const secondInput = this.dynamicScoreInputTarget.querySelector("#seconds")
if (minuteInput && secondInput) {
const minutes = (minuteInput.value || "0").padStart(2, "0")
const seconds = (secondInput.value || "0").padStart(2, "0")
this.finalScoreFieldTarget.value = `${minutes}:${seconds}`
// Validate after updating pin time
this.validateForm()
}
}
updatePointScore() {
const winnerScore = this.winnerScoreValue || "0"
const loserScore = this.loserScoreValue || "0"
this.finalScoreFieldTarget.value = `${winnerScore}-${loserScore}`
// Validate immediately after updating scores
this.validateForm()
}
validateForm() {
const winType = this.winTypeTarget.value
const winner = this.winnerSelectTarget?.value
let isValid = true
let alertMessage = ""
let winTypeShouldBe = "Decision"
// Clear previous validation messages
this.validationAlertsTarget.innerHTML = ""
this.validationAlertsTarget.style.display = "none"
this.validationAlertsTarget.classList.remove("alert", "alert-danger", "p-3")
if (["Decision", "Major", "Tech Fall"].includes(winType)) {
// Get scores and ensure they're valid numbers
const winnerScore = parseInt(this.winnerScoreValue || "0", 10)
const loserScore = parseInt(this.loserScoreValue || "0", 10)
console.log(`Validating scores: winner=${winnerScore}, loser=${loserScore}, type=${winType}`)
// Check if winner score > loser score
if (winnerScore <= loserScore) {
isValid = false
alertMessage += "<strong>Error:</strong> Winner's score must be higher than loser's score.<br>"
} else {
// Calculate score difference and determine correct win type
const scoreDifference = winnerScore - loserScore
if (scoreDifference < 8) {
winTypeShouldBe = "Decision"
} else if (scoreDifference >= 8 && scoreDifference < 15) {
winTypeShouldBe = "Major"
} else if (scoreDifference >= 15) {
winTypeShouldBe = "Tech Fall"
}
// Check if selected win type matches the correct one based on score difference
if (winTypeShouldBe !== winType) {
isValid = false
alertMessage += `
<strong>Win Type Error:</strong> Win type should be <strong>${winTypeShouldBe}</strong>.<br>
<ul>
<li>Decisions are wins with a score difference less than 8.</li>
<li>Majors are wins with a score difference between 8 and 14.</li>
<li>Tech Falls are wins with a score difference of 15 or more.</li>
</ul>
`
}
}
}
// Check if a winner is selected
if (!winner) {
isValid = false
alertMessage += "<strong>Error:</strong> Please select a winner.<br>"
}
// Display validation messages if any
if (alertMessage) {
this.validationAlertsTarget.innerHTML = alertMessage
this.validationAlertsTarget.style.display = "block"
this.validationAlertsTarget.classList.add("alert", "alert-danger", "p-3")
}
// Enable/disable submit button based on validation result
this.submitButtonTarget.disabled = !isValid
// Return validation result for potential use elsewhere
return isValid
}
createTextInput(id, placeholder, label) {
const container = document.createElement("div")
container.classList.add("form-group", "mb-2")
const inputLabel = document.createElement("label")
inputLabel.innerText = label
inputLabel.classList.add("form-label")
inputLabel.setAttribute("for", id)
const input = document.createElement("input")
input.type = "text"
input.id = id
input.placeholder = placeholder
input.classList.add("form-control")
input.style.width = "100%"
input.style.maxWidth = "400px"
container.appendChild(inputLabel)
container.appendChild(input)
return container
}
confirmWinner(event) {
const winnerSelect = this.winnerSelectTarget;
const selectedOption = winnerSelect.options[winnerSelect.selectedIndex];
if (!confirm('Is the name of the winner ' + selectedOption.text + '?')) {
event.preventDefault();
}
}
}

View File

@@ -0,0 +1,139 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [
"w1Stats", "w2Stats", "winner", "winType",
"score", "finished", "statusIndicator"
]
static values = {
matchId: Number
}
connect() {
console.log("Match spectate controller connected")
// Setup ActionCable connection if match ID is available
if (this.matchIdValue) {
this.setupSubscription(this.matchIdValue)
} else {
console.warn("No match ID provided for spectate controller")
}
}
disconnect() {
this.cleanupSubscription()
}
// Clean up the existing subscription
cleanupSubscription() {
if (this.matchSubscription) {
console.log('[Spectator AC Cleanup] Unsubscribing...')
this.matchSubscription.unsubscribe()
this.matchSubscription = null
}
}
// Set up the Action Cable subscription for a given matchId
setupSubscription(matchId) {
this.cleanupSubscription() // Ensure clean state
console.log(`[Spectator AC Setup] Attempting subscription for match ID: ${matchId}`)
if (typeof App === 'undefined' || typeof App.cable === 'undefined') {
console.error("[Spectator AC Setup] Action Cable consumer not found.")
if (this.hasStatusIndicatorTarget) {
this.statusIndicatorTarget.textContent = "Error: AC Not Loaded"
this.statusIndicatorTarget.classList.remove('text-dark', 'text-success')
this.statusIndicatorTarget.classList.add('alert-danger', 'text-danger')
}
return
}
// Set initial connecting state for indicator
if (this.hasStatusIndicatorTarget) {
this.statusIndicatorTarget.textContent = "Connecting to backend for live updates..."
this.statusIndicatorTarget.classList.remove('alert-danger', 'alert-success', 'text-danger', 'text-success')
this.statusIndicatorTarget.classList.add('alert-secondary', 'text-dark')
}
// Assign to the instance property
this.matchSubscription = App.cable.subscriptions.create(
{ channel: "MatchChannel", match_id: matchId },
{
initialized: () => {
console.log(`[Spectator AC Callback] Initialized: ${matchId}`)
// Set connecting state again in case of retry
if (this.hasStatusIndicatorTarget) {
this.statusIndicatorTarget.textContent = "Connecting to backend for live updates..."
this.statusIndicatorTarget.classList.remove('alert-danger', 'alert-success', 'text-danger', 'text-success')
this.statusIndicatorTarget.classList.add('alert-secondary', 'text-dark')
}
},
connected: () => {
console.log(`[Spectator AC Callback] CONNECTED: ${matchId}`)
if (this.hasStatusIndicatorTarget) {
this.statusIndicatorTarget.textContent = "Connected to backend for live updates..."
this.statusIndicatorTarget.classList.remove('alert-danger', 'alert-secondary', 'text-danger', 'text-dark')
this.statusIndicatorTarget.classList.add('alert-success')
}
try {
this.matchSubscription.perform('request_sync')
} catch (e) {
console.error('[Spectator AC] request_sync perform failed:', e)
}
},
disconnected: () => {
console.log(`[Spectator AC Callback] Disconnected: ${matchId}`)
if (this.hasStatusIndicatorTarget) {
this.statusIndicatorTarget.textContent = "Disconnected from backend for live updates. Retrying..."
this.statusIndicatorTarget.classList.remove('alert-success', 'alert-secondary', 'text-success', 'text-dark')
this.statusIndicatorTarget.classList.add('alert-danger')
}
},
rejected: () => {
console.error(`[Spectator AC Callback] REJECTED: ${matchId}`)
if (this.hasStatusIndicatorTarget) {
this.statusIndicatorTarget.textContent = "Connection to backend rejected"
this.statusIndicatorTarget.classList.remove('alert-success', 'alert-secondary', 'text-success', 'text-dark')
this.statusIndicatorTarget.classList.add('alert-danger')
}
this.matchSubscription = null
},
received: (data) => {
console.log("[Spectator AC Callback] Received:", data)
this.updateDisplayElements(data)
}
}
)
}
// Update UI elements with received data
updateDisplayElements(data) {
// Update display elements if they exist and data is provided
if (data.w1_stat !== undefined && this.hasW1StatsTarget) {
this.w1StatsTarget.textContent = data.w1_stat
}
if (data.w2_stat !== undefined && this.hasW2StatsTarget) {
this.w2StatsTarget.textContent = data.w2_stat
}
if (data.score !== undefined && this.hasScoreTarget) {
this.scoreTarget.textContent = data.score || '-'
}
if (data.win_type !== undefined && this.hasWinTypeTarget) {
this.winTypeTarget.textContent = data.win_type || '-'
}
if (data.winner_name !== undefined && this.hasWinnerTarget) {
this.winnerTarget.textContent = data.winner_name || (data.winner_id ? `ID: ${data.winner_id}` : '-')
} else if (data.winner_id !== undefined && this.hasWinnerTarget) {
this.winnerTarget.textContent = data.winner_id ? `ID: ${data.winner_id}` : '-'
}
if (data.finished !== undefined && this.hasFinishedTarget) {
this.finishedTarget.textContent = data.finished ? 'Yes' : 'No'
}
}
}

View File

@@ -0,0 +1,68 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [
"w1Takedown", "w1Escape", "w1Reversal", "w1Nf2", "w1Nf3", "w1Nf4", "w1Nf5", "w1Penalty", "w1Penalty2",
"w1Top", "w1Bottom", "w1Neutral", "w1Defer", "w1Stalling", "w1Caution", "w1ColorSelect",
"w2Takedown", "w2Escape", "w2Reversal", "w2Nf2", "w2Nf3", "w2Nf4", "w2Nf5", "w2Penalty", "w2Penalty2",
"w2Top", "w2Bottom", "w2Neutral", "w2Defer", "w2Stalling", "w2Caution", "w2ColorSelect"
]
connect() {
console.log("Wrestler color controller connected")
this.initializeColors()
}
initializeColors() {
// Set initial colors based on select values
this.changeW1Color({ preventRecursion: true })
}
changeW1Color(options = {}) {
const color = this.w1ColorSelectTarget.value
this.setElementsColor("w1", color)
// Update w2 color to the opposite color unless we're already in a recursive call
if (!options.preventRecursion) {
const oppositeColor = color === "green" ? "red" : "green"
this.w2ColorSelectTarget.value = oppositeColor
this.setElementsColor("w2", oppositeColor)
}
}
changeW2Color(options = {}) {
const color = this.w2ColorSelectTarget.value
this.setElementsColor("w2", color)
// Update w1 color to the opposite color unless we're already in a recursive call
if (!options.preventRecursion) {
const oppositeColor = color === "green" ? "red" : "green"
this.w1ColorSelectTarget.value = oppositeColor
this.setElementsColor("w1", oppositeColor)
}
}
setElementsColor(wrestler, color) {
// Define which targets to update for each wrestler
const targetSuffixes = [
"Takedown", "Escape", "Reversal", "Nf2", "Nf3", "Nf4", "Nf5", "Penalty", "Penalty2",
"Top", "Bottom", "Neutral", "Defer", "Stalling", "Caution"
]
// For each target type, update the class
targetSuffixes.forEach(suffix => {
const targetName = `${wrestler}${suffix}Target`
if (this[targetName]) {
// Remove existing color classes
this[targetName].classList.remove("btn-success", "btn-danger")
// Add new color class
if (color === "green") {
this[targetName].classList.add("btn-success")
} else if (color === "red") {
this[targetName].classList.add("btn-danger")
}
}
})
}
}

View File

@@ -1,4 +0,0 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -1,3 +0,0 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -1,3 +0,0 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -1,3 +0,0 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -1,3 +0,0 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -1,3 +0,0 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -1,3 +0,0 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -1,3 +0,0 @@
// Place all the styles related to the Admin controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -1,4 +0,0 @@
/*
Place all the styles related to the matching controller here.
They will automatically be included in application.css.
*/

View File

@@ -1,19 +1,17 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any styles
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
* file per style scope.
*
*= require_tree .
*= require_self
* Bootstrap 3.3.6 in vendor/assets/stylesheets
*= require bootstrap.min.css
*= require bootstrap-theme.min.css
/* relative pathing from /vender/assets/stylesheets = / */
@import url("/bootstrap.min.css");
@import url("/bootstrap-theme.min.css");
@import url("/fontawesome/all.css");
@import url("/custom.css");
@import url("/scaffolds.css");
*/
@font-face {
font-family: 'Font Awesome 5 Brands';
/* relative pathing from /vender/assets/stylesheets = / */
src: url("/webfonts/fa-brands-400.eot");
src: url("/webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"),
url("/webfonts/fa-brands-400.woff2") format("woff2"),
url("/webfonts/fa-brands-400.woff") format("woff"),
url("/webfonts/fa-brands-400.ttf") format("truetype"),
url("/webfonts/fa-brands-400.svg#fontawesome") format("svg");
}

View File

@@ -0,0 +1,17 @@
.navbar-inverse.navbar-fixed-top {
z-index: 1040; /* Ensure main navbar is above tournament navbar */
}
#tournament-navbar {
top: 50px; /* Position below the first fixed navbar */
z-index: 1030; /* Explicitly set standard fixed navbar z-index */
}
/* Make desktop navbar dropdowns scrollable if they overflow */
@media (min-width: 768px) {
/* Target dropdowns in main nav and tournament nav specifically */
.navbar-fixed-top .dropdown-menu {
max-height: 70vh; /* Adjust as needed - 70% of viewport height */
overflow-y: auto;
}
}

View File

@@ -1,3 +0,0 @@
// Place all the styles related to the Matches controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -1,3 +0,0 @@
// Place all the styles related to the Mats controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -1,3 +0,0 @@
// Place all the styles related to the Schools controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -1,3 +0,0 @@
// Place all the styles related to the StaticPages controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -1,3 +0,0 @@
// Place all the styles related to the tournaments controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -1,3 +0,0 @@
// Place all the styles related to the Weights controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -1,3 +0,0 @@
// Place all the styles related to the Wrestlers controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end

View File

@@ -0,0 +1,88 @@
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
# Called when client wants the latest stats immediately after reconnect
def request_sync
unless @match
Rails.logger.error "[MatchChannel] Error: request_sync called but @match is nil. Client params on sub: #{params[:match_id]}"
return
end
payload = {
w1_stat: @match.w1_stat,
w2_stat: @match.w2_stat,
score: @match.score,
win_type: @match.win_type,
winner_name: @match.winner&.name,
winner_id: @match.winner_id,
finished: @match.finished
}.compact
if payload.present?
Rails.logger.info "[MatchChannel] request_sync transmit for match #{@match.id} with payload: #{payload.inspect}"
transmit(payload)
else
Rails.logger.info "[MatchChannel] request_sync payload empty for match #{@match.id}, not transmitting."
end
end
end

View File

@@ -14,9 +14,9 @@ class ApiController < ApplicationController
end
def tournament
@tournament = Tournament.where(:id => params[:tournament]).includes(:schools,:weights,:mats,:matches,:user,:wrestlers).first
@schools = @tournament.schools.includes(:wrestlers)
@weights = @tournament.weights.includes(:wrestlers)
@tournament = Tournament.where(:id => params[:tournament]).includes(:user, :mats, :schools, :weights, :matches, wrestlers: [:school, :weight, :matches_as_w1, :matches_as_w2]).first
@schools = @tournament.schools.includes(wrestlers: [:weight, :matches_as_w1, :matches_as_w2])
@weights = @tournament.weights.includes(wrestlers: [:school, :matches_as_w1, :matches_as_w2])
@matches = @tournament.matches.includes(:wrestlers,:schools)
@mats = @tournament.mats.includes(:matches)
end

View File

@@ -1,6 +1,6 @@
class MatchesController < ApplicationController
before_action :set_match, only: [:show, :edit, :update, :stat]
before_action :check_access, only: [:edit,:update, :stat]
before_action :set_match, only: [:show, :edit, :update, :stat, :spectate, :edit_assignment, :update_assignment]
before_action :check_access, only: [:edit, :update, :stat, :edit_assignment, :update_assignment]
# GET /matches/1
# GET /matches/1.json
@@ -21,7 +21,7 @@ class MatchesController < ApplicationController
session[:return_path] = "/tournaments/#{@match.tournament.id}/matches"
end
def stat
def stat
# @show_next_bout_button = false
if params[:match]
@match = Match.where(:id => params[:match]).includes(:wrestlers).first
@@ -50,16 +50,102 @@ class MatchesController < ApplicationController
end
@tournament = @match.tournament
end
session[:return_path] = "/tournaments/#{@tournament.id}/matches"
session[:error_return_path] = "/matches/#{@match.id}/stat"
if @match&.mat
@mat = @match.mat
queue_position = @mat.queue_position_for_match(@match)
@next_match = queue_position == 1 ? @mat.queue2_match : nil
@show_next_bout_button = queue_position == 1
if request.referer&.include?("/tournaments/#{@tournament.id}/matches")
session[:return_path] = "/tournaments/#{@tournament.id}/matches"
else
session[:return_path] = mat_path(@mat)
end
session[:error_return_path] = "/matches/#{@match.id}/stat"
else
session[:return_path] = "/tournaments/#{@tournament.id}/matches"
session[:error_return_path] = "/matches/#{@match.id}/stat"
end
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
# GET /matches/1/edit_assignment
def edit_assignment
@tournament = @match.tournament
@mats = @tournament.mats.sort_by(&:name)
@current_mat = @match.mat
@current_queue_position = @current_mat&.queue_position_for_match(@match)
session[:return_path] = "/tournaments/#{@tournament.id}/matches"
end
# PATCH /matches/1/update_assignment
def update_assignment
@tournament = @match.tournament
mat_id = params.dig(:match, :mat_id)
queue_position = params.dig(:match, :queue_position)
if mat_id.blank?
Mat.where("queue1 = :match_id OR queue2 = :match_id OR queue3 = :match_id OR queue4 = :match_id", match_id: @match.id)
.find_each { |mat| mat.remove_match_from_queue_and_collapse!(@match.id) }
@match.update(mat_id: nil)
redirect_to session.delete(:return_path) || "/tournaments/#{@tournament.id}/matches", notice: "Match assignment cleared."
return
end
if queue_position.blank?
redirect_to edit_assignment_match_path(@match), alert: "Queue position is required when selecting a mat."
return
end
unless %w[1 2 3 4].include?(queue_position.to_s)
redirect_to edit_assignment_match_path(@match), alert: "Queue position must be between 1 and 4."
return
end
mat = @tournament.mats.find_by(id: mat_id)
unless mat
redirect_to edit_assignment_match_path(@match), alert: "Selected mat was not found."
return
end
mat.assign_match_to_queue!(@match, queue_position)
redirect_to session.delete(:return_path) || "/tournaments/#{@tournament.id}/matches", notice: "Match assignment updated."
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.' }

View File

@@ -10,13 +10,13 @@ class MatsController < ApplicationController
if bout_number_param
@show_next_bout_button = false
@match = @mat.unfinished_matches.find { |m| m.bout_number == bout_number_param.to_i }
@match = @mat.queue_matches.compact.find { |m| m.bout_number == bout_number_param.to_i }
else
@show_next_bout_button = true
@match = @mat.unfinished_matches.first
@match = @mat.queue1_match
end
@next_match = @mat.unfinished_matches.second # Second unfinished match on the mat
@next_match = @mat.queue2_match # Second match on the mat
@wrestlers = []
if @match
@@ -82,8 +82,8 @@ class MatsController < ApplicationController
def assign_next_match
@tournament = @mat.tournament_id
respond_to do |format|
if @mat.assign_next_match
format.html { redirect_to "/tournaments/#{@mat.tournament.id}", notice: "Next Match on Mat #{@mat.name} successfully completed." }
if @mat.advance_queue!
format.html { redirect_to "/tournaments/#{@mat.tournament.id}", notice: "Mat #{@mat.name} queue advanced." }
format.json { head :no_content }
else
format.html { redirect_to "/tournaments/#{@mat.tournament.id}", alert: "There was an error." }

View File

@@ -12,7 +12,7 @@ class SchoolsController < ApplicationController
# GET /schools/1.json
def show
session.delete(:return_path)
@wrestlers = @school.wrestlers.includes(:deductedPoints,:matches,:weight,:school)
@wrestlers = @school.wrestlers.includes(:deductedPoints, :weight, :school, :matches_as_w1, :matches_as_w2)
@tournament = @school.tournament
end
@@ -84,7 +84,7 @@ class SchoolsController < ApplicationController
private
# Use callbacks to share common setup or constraints between actions.
def set_school
@school = School.where(:id => params[:id]).includes(:tournament,:wrestlers,:deductedPoints,:delegates).first
@school = School.includes(:tournament, :delegates, :deductedPoints, wrestlers: [:weight, :deductedPoints, :matches_as_w1, :matches_as_w2]).find_by(id: params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.

View File

@@ -1,6 +1,6 @@
class TournamentsController < ApplicationController
before_action :set_tournament, only: [:all_results, :delete_school_keys, :generate_school_keys,:reset_bout_board,:calculate_team_scores,:bout_sheets,:swap,:weigh_in_sheet,:error,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:remove_delegate,:school_delegate,:delegate,:matches,:weigh_in,:weigh_in_weight,:create_custom_weights,:show,:edit,:update,:destroy,:up_matches,:no_matches,:team_scores,:generate_matches,:bracket,:all_brackets]
before_action :check_access_manage, only: [:delete_school_keys, :generate_school_keys,:reset_bout_board,:calculate_team_scores,:swap,:weigh_in_sheet,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:school_delegate,:weigh_in,:weigh_in_weight,:create_custom_weights,:update,:edit,:generate_matches,:matches]
before_action :set_tournament, only: [:all_results, :delete_school_keys, :generate_school_keys,:reset_bout_board,:calculate_team_scores,:bout_sheets,:swap,:weigh_in_sheet,:error,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:remove_delegate,:school_delegate,:delegate,:matches,:weigh_in,:weigh_in_weight,:create_custom_weights,:show,:edit,:update,:destroy,:up_matches,:no_matches,:team_scores,:generate_matches,:bracket,:all_brackets,:qrcode]
before_action :check_access_manage, only: [:delete_school_keys, :generate_school_keys,:reset_bout_board,:calculate_team_scores,:swap,:weigh_in_sheet,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:school_delegate,:weigh_in,:weigh_in_weight,:create_custom_weights,:update,:edit,:generate_matches,:matches,:qrcode]
before_action :check_access_destroy, only: [:destroy,:delegate,:remove_delegate]
before_action :check_tournament_errors, only: [:generate_matches]
before_action :check_for_matches, only: [:all_results,:up_matches,:bracket,:all_brackets]
@@ -164,15 +164,16 @@ class TournamentsController < ApplicationController
end
def bracket
if params[:weight]
@weight = Weight.where(:id => params[:weight]).includes(:matches,:wrestlers).first
@matches = @weight.matches.includes(:schools,:wrestlers)
@wrestlers = @weight.wrestlers.includes(:school)
if @tournament.tournament_type == "Pool to bracket"
@pools = @weight.pool_rounds(@matches)
@bracketType = @weight.pool_bracket_type
end
if params[:weight]
@weight = Weight.includes(:matches, wrestlers: [:school, :matches_as_w1, :matches_as_w2]).find_by(id: params[:weight])
@matches = @weight.matches
@wrestlers = @weight.wrestlers
if @tournament.tournament_type == "Pool to bracket"
@pools = @weight.pool_rounds(@matches)
@bracketType = @weight.pool_bracket_type
end
end
end
def all_results
@@ -195,6 +196,11 @@ class TournamentsController < ApplicationController
end
def qrcode
@tournament_url = tournament_url(@tournament)
@qrcode = RQRCode::QRCode.new(@tournament_url)
end
def up_matches
# .where.not(loser1_name: 'BYE') won't return matches with NULL loser1_name
@@ -220,12 +226,26 @@ class TournamentsController < ApplicationController
end
def index
if params[:search]
# @tournaments = Tournament.limit(200).search(params[:search]).order("date DESC")
@tournaments = Tournament.limit(200).search_date_name(params[:search]).order("date DESC")
# Simple manual pagination to avoid introducing a gem.
per_page = 20
page = params[:page].to_i > 0 ? params[:page].to_i : 1
offset = (page - 1) * per_page
if params[:search].present?
tournaments = Tournament.search_date_name(params[:search]).to_a
else
@tournaments = Tournament.all.sort_by{|t| t.days_until_start}.first(20)
tournaments = Tournament.all.to_a
end
# Sort by distance from today (closest first)
today = Date.today
tournaments.sort_by! { |t| (t.date - today).abs }
@total_count = tournaments.size
@total_pages = (@total_count / per_page.to_f).ceil
@page = page
@per_page = per_page
@tournaments = tournaments.slice(offset, per_page) || []
end
def show
@@ -285,7 +305,7 @@ class TournamentsController < ApplicationController
def reset_bout_board
@tournament.reset_and_fill_bout_board
redirect_to tournament_path(@tournament), notice: "Successfully reset the bout board."
redirect_to tournament_path(@tournament), notice: "Successfully reset the bout board. Please have all mat table workers refresh their page."
end
def generate_school_keys
@@ -303,7 +323,7 @@ class TournamentsController < ApplicationController
private
# Use callbacks to share common setup or constraints between actions.
def set_tournament
@tournament = Tournament.where(:id => params[:id]).includes(:schools,:weights,:mats,:matches,:user,:wrestlers).first
@tournament = Tournament.includes(:user, :mats, :schools, :weights, :matches, wrestlers: [:school, :weight, :matches_as_w1, :matches_as_w2]).find_by(id: params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.

View File

@@ -98,7 +98,8 @@ class WeightsController < ApplicationController
private
# Use callbacks to share common setup or constraints between actions.
def set_weight
@weight = Weight.where(:id => params[:id]).includes(:tournament,:wrestlers).first
# Add nested includes for wrestlers
@weight = Weight.includes(:tournament, wrestlers: [:school, :matches_as_w1, :matches_as_w2]).find_by(id: params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.

View File

@@ -86,7 +86,11 @@ class WrestlersController < ApplicationController
private
def set_wrestler
@wrestler = Wrestler.includes(:school, :weight, :tournament, :matches).find_by(id: params[:id])
@wrestler = Wrestler.includes(:school, :weight, :tournament, :matches_as_w1, :matches_as_w2).find_by(id: params[:id])
if @wrestler.nil?
redirect_to root_path, alert: "Wrestler not found"
end
end
def wrestler_params

View File

@@ -1,4 +1,20 @@
module ApplicationHelper
def hide_ads?
case controller_name
when "schools"
action_name == "show" && (user_signed_in? || school_permission_key_present?)
when "wrestlers"
%w[new edit].include?(action_name) && (user_signed_in? || school_permission_key_present?)
when "mats"
action_name == "show" && user_signed_in?
else
false
end
end
def school_permission_key_present?
@school_permission_key.present? ||
params[:school_permission_key].present? ||
params.dig(:school, :school_permission_key).present?
end
end

View File

@@ -1,16 +1,34 @@
class AdvanceWrestlerJob < ApplicationJob
queue_as :default
# associations are not available here so we had to pass tournament_id when creating the job
limits_concurrency to: 1, key: ->(_wrestler, _match, tournament_id) { "tournament:#{tournament_id}" }
# Class method for direct execution in test environment
def self.perform_sync(wrestler, match)
# Execute directly on provided objects
service = AdvanceWrestler.new(wrestler, match)
service.advance_raw
def perform(wrestler, match, tournament_id)
# Get tournament from wrestler
tournament = wrestler.tournament
# Create job status record
job_name = "Advancing wrestler #{wrestler.name}"
job_status = TournamentJobStatus.create!(
tournament: tournament,
job_name: job_name,
status: "Running",
details: "Match ID: #{match&.bout_number || 'No match'} Wrestler Name #{wrestler&.name || 'No Wrestler'}"
)
begin
# Execute the job
service = AdvanceWrestler.new(wrestler, match)
service.advance_raw
# Remove the job status record on success
TournamentJobStatus.complete_job(tournament.id, job_name)
rescue => e
# Update status to errored
job_status.update(status: "Errored", details: "Error: #{e.message}")
# Re-raise the error for SolidQueue to handle
raise e
end
end
def perform(wrestler, match)
# Execute the job
service = AdvanceWrestler.new(wrestler, match)
service.advance_raw
end
end
end

View File

@@ -1,7 +1,8 @@
class CalculateSchoolScoreJob < ApplicationJob
queue_as :default
limits_concurrency to: 1, key: ->(school) { "tournament:#{school.tournament_id}" }
# Class method for direct execution in test environment
# Need for TournamentJobStatusIntegrationTest
def self.perform_sync(school)
# Execute directly on provided objects
school.calculate_score_raw
@@ -11,7 +12,28 @@ class CalculateSchoolScoreJob < ApplicationJob
# Log information about the job
Rails.logger.info("Calculating score for school ##{school.id} (#{school.name})")
# Execute the calculation
school.calculate_score_raw
# Create job status record
tournament = school.tournament
job_name = "Calculating team score for #{school.name}"
job_status = TournamentJobStatus.create!(
tournament: tournament,
job_name: job_name,
status: "Running",
details: "School ID: #{school.id}"
)
begin
# Execute the calculation
school.calculate_score_raw
# Remove the job status record on success
TournamentJobStatus.complete_job(tournament.id, job_name)
rescue => e
# Update status to errored
job_status.update(status: "Errored", details: "Error: #{e.message}")
# Re-raise the error for SolidQueue to handle
raise e
end
end
end
end

View File

@@ -1,12 +1,6 @@
class GenerateTournamentMatchesJob < ApplicationJob
queue_as :default
# Class method for direct execution in test environment
def self.perform_sync(tournament)
# Execute directly on provided objects
generator = GenerateTournamentMatches.new(tournament)
generator.generate_raw
end
limits_concurrency to: 1, key: ->(tournament) { "tournament:#{tournament.id}" }
def perform(tournament)
# Log information about the job
@@ -24,4 +18,4 @@ class GenerateTournamentMatchesJob < ApplicationJob
raise # Re-raise the error so it's properly recorded
end
end
end
end

View File

@@ -1,19 +1,33 @@
class TournamentBackupJob < ApplicationJob
queue_as :default
# Class method for direct execution in test environment
def self.perform_sync(tournament, reason = nil)
# Execute directly on provided objects
service = TournamentBackupService.new(tournament, reason)
service.create_backup_raw
end
limits_concurrency to: 1, key: ->(tournament, *) { "tournament:#{tournament.id}" }
def perform(tournament, reason = nil)
# Log information about the job
Rails.logger.info("Creating backup for tournament ##{tournament.id} (#{tournament.name}), reason: #{reason || 'manual'}")
# Execute the backup
service = TournamentBackupService.new(tournament, reason)
service.create_backup_raw
# Create job status record
job_name = "Backing up tournament"
job_status = TournamentJobStatus.create!(
tournament: tournament,
job_name: job_name,
status: "Running",
details: "Reason: #{reason || 'manual'}"
)
begin
# Execute the backup
service = TournamentBackupService.new(tournament, reason)
service.create_backup_raw
# Remove the job status record on success
TournamentJobStatus.complete_job(tournament.id, job_name)
rescue => e
# Update status to errored
job_status.update(status: "Errored", details: "Error: #{e.message}")
# Re-raise the error for SolidQueue to handle
raise e
end
end
end
end

View File

@@ -0,0 +1,37 @@
class TournamentCleanupJob < ApplicationJob
queue_as :default
def perform
# Remove or clean up tournaments based on age and match status
process_old_tournaments
end
private
def process_old_tournaments
# Get all tournaments older than 1 week that have a user_id
old_tournaments = Tournament.where('date < ? AND user_id IS NOT NULL', 1.week.ago.to_date)
old_tournaments.each do |tournament|
# Check if it has any non-BYE finished matches
has_real_matches = tournament.matches.where(finished: 1).where.not(win_type: 'BYE').exists?
if has_real_matches
tournament.tournament_backups.destroy_all
# 1. Remove all school delegates
tournament.schools.each do |school|
school.delegates.destroy_all
end
# 2. Remove all tournament delegates
tournament.delegates.destroy_all
# 3. Set user_id to null
tournament.update(user_id: nil)
else
tournament.destroy
end
end
end
end

View File

@@ -1,21 +1,34 @@
class WrestlingdevImportJob < ApplicationJob
queue_as :default
# Class method for direct execution in test environment
def self.perform_sync(tournament, import_data = nil)
# Execute directly on provided objects
importer = WrestlingdevImporter.new(tournament)
importer.import_data = import_data if import_data
importer.import_raw
end
limits_concurrency to: 1, key: ->(tournament, *) { "tournament:#{tournament.id}" }
def perform(tournament, import_data = nil)
# Log information about the job
Rails.logger.info("Starting import for tournament ##{tournament.id} (#{tournament.name})")
# Execute the import
importer = WrestlingdevImporter.new(tournament)
importer.import_data = import_data if import_data
importer.import_raw
# Create job status record
job_name = "Importing tournament"
job_status = TournamentJobStatus.create!(
tournament: tournament,
job_name: job_name,
status: "Running",
details: "Processing backup data"
)
begin
# Execute the import
importer = WrestlingdevImporter.new(tournament)
importer.import_data = import_data if import_data
importer.import_raw
# Remove the job status record on success
TournamentJobStatus.complete_job(tournament.id, job_name)
rescue => e
# Update status to errored
job_status.update(status: "Errored", details: "Error: #{e.message}")
# Re-raise the error for SolidQueue to handle
raise e
end
end
end
end

View File

@@ -1,6 +1,20 @@
class Ability
include CanCan::Ability
def school_permission_key_check(school_permission_key)
# Can read school if tournament is public or a valid school permission key is provided
can :read, School do |school|
school.tournament.is_public ||
(school_permission_key.present? && school.permission_key == school_permission_key)
end
# Can manage school if a valid school permission key is provided
# school_permission_key comes from app/controllers/application_controller.rb
can :manage, School do |school|
(school_permission_key.present? && school.permission_key == school_permission_key)
end
end
def initialize(user, school_permission_key = nil)
if user
# LOGGED IN USER PERMISSIONS
@@ -46,6 +60,8 @@ class Ability
school.tournament.delegates.map(&:user_id).include?(user.id) ||
school.tournament.user_id == user.id
end
school_permission_key_check(school_permission_key)
else
# NON LOGGED IN USER PERMISSIONS
@@ -58,18 +74,7 @@ class Ability
# SCHOOL PERMISSIONS
# wrestler permissions are included with school permissions
# Can read school if tournament is public or a valid school permission key is provided
can :read, School do |school|
school.tournament.is_public ||
(school_permission_key.present? && school.permission_key == school_permission_key)
end
# Can read school if a valid school permission key is provided
# school_permission_key comes from app/controllers/application_controller.rb
can :manage, School do |school|
(school_permission_key.present? && school.permission_key == school_permission_key)
end
school_permission_key_check(school_permission_key)
end
end
end

View File

@@ -1,53 +1,52 @@
class Mat < ApplicationRecord
include ActionView::RecordIdentifier
belongs_to :tournament
has_many :matches
has_many :matches, dependent: :nullify
has_many :mat_assignment_rules, dependent: :destroy
validates :name, presence: true
before_destroy do
if tournament.matches.size > 0
tournament.reset_mats
matsToAssign = tournament.mats.select{|m| m.id != self.id}
tournament.assign_mats(matsToAssign)
end
end
QUEUE_SLOTS = %w[queue1 queue2 queue3 queue4].freeze
after_create do
if tournament.matches.size > 0
tournament.reset_mats
matsToAssign = tournament.mats
tournament.assign_mats(matsToAssign)
end
end
after_save :clear_queue_matches_cache
def assign_next_match
slot = first_empty_queue_slot
return true unless slot
match = next_eligible_match
self.matches.reload
if match and self.unfinished_matches.size < 4
match.mat_id = self.id
if match.save
# Invalidate any wrestler caches
if match.w1
match.wrestler1.touch
match.wrestler1.school.touch
return false unless match
place_match_in_empty_slot!(match, slot)
true
end
def advance_queue!(finished_match = nil)
self.class.transaction do
if finished_match
position = queue_position_for_match(finished_match)
if position == 1
shift_queue_forward!
fill_queue_slots!
elsif position
remove_match_from_queue_and_collapse!(finished_match.id)
else
fill_queue_slots!
end
if match.w2
match.wrestler2.touch
match.wrestler2.school.touch
end
return true
else
return false
if queue1_match&.finished == 1
shift_queue_forward!
end
fill_queue_slots!
end
else
return true
end
broadcast_current_match
true
end
def next_eligible_match
# Start with all matches that are either unfinished (nil or 0), have a bout number, and are ordered by bout_number
filtered_matches = tournament.matches
filtered_matches = Match.where(tournament_id: tournament_id)
.where(finished: [nil, 0]) # finished is nil or 0
.where(mat_id: nil) # mat_id is nil
.where.not(bout_number: nil) # bout_number is not nil
@@ -57,6 +56,11 @@ class Mat < ApplicationRecord
filtered_matches = filtered_matches
.where("loser1_name != ? OR loser1_name IS NULL", "BYE")
.where("loser2_name != ? OR loser2_name IS NULL", "BYE")
# Filter out matches without a wrestlers
filtered_matches = filtered_matches
.where("w1 IS NOT NULL")
.where("w2 IS NOT NULL")
# Apply mat assignment rules
mat_assignment_rules.each do |rule|
@@ -80,9 +84,194 @@ class Mat < ApplicationRecord
filtered_matches.first
end
def queue_match_ids
QUEUE_SLOTS.map { |slot| public_send(slot) }
end
# used to prevent N+1 query on each mat
def queue_matches
slot_ids = queue_match_ids
if @queue_matches.nil? || @queue_match_slot_ids != slot_ids
ids = slot_ids.compact
@queue_matches = if ids.empty?
[nil, nil, nil, nil]
else
matches_by_id = Match.where(id: ids).index_by(&:id)
slot_ids.map { |match_id| match_id ? matches_by_id[match_id] : nil }
end
@queue_match_slot_ids = slot_ids
end
@queue_matches
end
def queue1_match
queue_match_at(1)
end
def queue2_match
queue_match_at(2)
end
def queue3_match
queue_match_at(3)
end
def queue4_match
queue_match_at(4)
end
def queue_position_for_match(match)
return nil unless match
return 1 if queue1 == match.id
return 2 if queue2 == match.id
return 3 if queue3 == match.id
return 4 if queue4 == match.id
nil
end
def remove_match_from_queue_and_collapse!(match_id)
queue_ids = queue_match_ids
return if queue_ids.none? { |id| id == match_id }
queue_ids.map! { |id| id == match_id ? nil : id }
queue_ids = queue_ids.compact
queue_ids += [nil] * (4 - queue_ids.size)
update!(
queue1: queue_ids[0],
queue2: queue_ids[1],
queue3: queue_ids[2],
queue4: queue_ids[3]
)
fill_queue_slots!
broadcast_current_match
end
def assign_match_to_queue!(match, position)
position = position.to_i
raise ArgumentError, "Queue position must be 1-4" unless (1..4).cover?(position)
self.class.transaction do
match.update!(mat_id: id)
remove_match_from_other_mats!(match.id)
queue_ids = queue_match_ids.map { |id| id == match.id ? nil : id }
queue_ids = queue_ids.compact
queue_ids.insert(position - 1, match.id)
bumped_match_id = queue_ids.length > 4 ? queue_ids.pop : nil
queue_ids += [nil] * (4 - queue_ids.length)
update!(
queue1: queue_ids[0],
queue2: queue_ids[1],
queue3: queue_ids[2],
queue4: queue_ids[3]
)
bumped_match = Match.find_by(id: bumped_match_id)
if bumped_match && bumped_match.finished != 1
bumped_match.update!(mat_id: nil)
end
end
broadcast_current_match
end
def clear_queue!
update!(queue1: nil, queue2: nil, queue3: nil, queue4: nil)
end
def unfinished_matches
matches.select{|m| m.finished != 1}.sort_by{|m| m.bout_number}
end
private
def clear_queue_matches_cache
@queue_matches = nil
@queue_match_slot_ids = nil
end
def queue_match_at(position)
queue_matches[position - 1]
end
def first_empty_queue_slot
QUEUE_SLOTS.each_with_index do |slot, index|
return index + 1 if public_send(slot).nil?
end
nil
end
def shift_queue_forward!
update!(
queue1: queue2,
queue2: queue3,
queue3: queue4,
queue4: nil
)
end
def fill_queue_slots!
queue_ids = queue_match_ids
updated = false
QUEUE_SLOTS.each_with_index do |_slot, index|
next if queue_ids[index].present?
match = next_eligible_match
break unless match
queue_ids[index] = match.id
match.update!(mat_id: id)
updated = true
end
if updated
update!(
queue1: queue_ids[0],
queue2: queue_ids[1],
queue3: queue_ids[2],
queue4: queue_ids[3]
)
end
end
def remove_match_from_other_mats!(match_id)
self.class.where.not(id: id)
.where("queue1 = :match_id OR queue2 = :match_id OR queue3 = :match_id OR queue4 = :match_id", match_id: match_id)
.find_each do |mat|
mat.remove_match_from_queue_and_collapse!(match_id)
end
end
def place_match_in_empty_slot!(match, slot)
self.class.transaction do
match.update!(mat_id: id)
remove_match_from_other_mats!(match.id)
update!(slot_key(slot) => match.id)
end
broadcast_current_match
end
def slot_key(slot)
"queue#{slot}"
end
def broadcast_current_match
Turbo::StreamsChannel.broadcast_update_to(
self,
target: dom_id(self, :current_match),
partial: "mats/current_match",
locals: {
mat: self,
match: queue1_match,
next_match: queue2_match,
show_next_bout_button: true
}
)
end
end

View File

@@ -1,7 +1,10 @@
class Match < ApplicationRecord
include ActionView::RecordIdentifier
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
@@ -9,12 +12,21 @@ class Match < ApplicationRecord
# Callback to update finished_at when a match is finished
before_save :update_finished_at
after_update :after_finished_actions, if: -> {
saved_change_to_finished? ||
saved_change_to_winner_id? ||
saved_change_to_win_type? ||
saved_change_to_score? ||
saved_change_to_overtime_type?
# update mat show with correct match if bout board is reset
# this is done with a turbo stream
after_commit :broadcast_mat_assignment_change, if: :saved_change_to_mat_id?, on: [:create, :update]
# Enqueue advancement and related actions after the DB transaction has committed.
# Using after_commit ensures any background jobs enqueued inside these callbacks
# will see the committed state of the match (e.g. finished == 1). Enqueuing
# jobs from after_update can cause jobs to run before the transaction commits,
# which leads to jobs observing stale data and not performing advancement.
after_commit :after_finished_actions, on: :update, if: -> {
saved_change_to_finished? ||
saved_change_to_winner_id? ||
saved_change_to_win_type? ||
saved_change_to_score? ||
saved_change_to_overtime_type?
}
def after_finished_actions
@@ -25,11 +37,14 @@ class Match < ApplicationRecord
wrestler2.touch
end
if self.finished == 1 && self.winner_id != nil
if self.mat
self.mat.assign_next_match
end
advance_wrestlers
calculate_school_points
if self.mat
self.mat.advance_queue!(self)
end
self.tournament.refill_open_bout_board_queues
# School point calculation has move to the end of advance wrestler
# calculate_school_points
self.update(mat_id: nil)
end
end
@@ -43,7 +58,7 @@ class Match < ApplicationRecord
errors.add(:winner_id, "cannot be blank")
end
if win_type == "Pin" and ! score.match(/^[0-5]?[0-9]:[0-5][0-9]/)
errors.add(:score, "needs to be in time format MM:SS when win type is Pin example: 1:23 or 10:03")
errors.add(:score, "needs to be in time format MM:SS when win type is Pin example: 2:23, 0:25, 10:03")
end
if win_type == "Decision" or win_type == "Tech Fall" or win_type == "Major" and ! score.match(/^[0-9]?[0-9]-[0-9]?[0-9]/)
errors.add(:score, "needs to be in Number-Number format when win type is Decision, Tech Fall, and Major example: 10-2")
@@ -188,6 +203,7 @@ class Match < ApplicationRecord
end
def w1_bracket_name
first_round = self.weight.matches.sort_by{|m| m.round}.first.round
return_string = ""
return_string_ending = ""
if self.w1 and self.winner_id == self.w1
@@ -195,7 +211,7 @@ class Match < ApplicationRecord
return_string_ending = return_string_ending + "</strong>"
end
if self.w1 != nil
if self.round == 1
if self.round == first_round
return_string = return_string + "#{wrestler1.long_bracket_name}"
else
return_string = return_string + "#{wrestler1.short_bracket_name}"
@@ -207,6 +223,7 @@ class Match < ApplicationRecord
end
def w2_bracket_name
first_round = self.weight.matches.sort_by{|m| m.round}.first.round
return_string = ""
return_string_ending = ""
if self.w2 and self.winner_id == self.w2
@@ -214,7 +231,7 @@ class Match < ApplicationRecord
return_string_ending = return_string_ending + "</strong>"
end
if self.w2 != nil
if self.round == 1
if self.round == first_round
return_string = return_string + "#{wrestler2.long_bracket_name}"
else
return_string = return_string + "#{wrestler2.short_bracket_name}"
@@ -229,10 +246,10 @@ class Match < ApplicationRecord
if self.finished != 1
return ""
end
if self.winner_id == self.w1
if self.winner == self.wrestler1
return self.w1_name
end
if self.winner_id == self.w2
if self.winner == self.wrestler2
return self.w2_name
end
end
@@ -241,20 +258,28 @@ class Match < ApplicationRecord
if self.finished != 1
return ""
end
if self.winner_id == self.w1
winning_wrestler = self.wrestler1
winning_wrestler = self.winner
if winning_wrestler == self.wrestler1
losing_wrestler = self.wrestler2
end
if self.winner_id == self.w2
winning_wrestler = self.wrestler2
elsif winning_wrestler == self.wrestler2
losing_wrestler = self.wrestler1
else
# Handle cases where winner is not w1 or w2 (e.g., BYE, DQ where opponent might be nil)
# Or maybe the match hasn't been fully populated yet after a win?
# Returning an empty string for now, but this might need review based on expected scenarios.
return ""
end
return "#{self.weight.max} lbs - #{winning_wrestler.name} (#{winning_wrestler.school.name}) #{self.win_type} #{losing_wrestler.name} (#{losing_wrestler.school.name}) #{self.score}"
# Ensure losing_wrestler is not nil before accessing its properties
losing_wrestler_name = losing_wrestler ? losing_wrestler.name : "Unknown"
losing_wrestler_school = losing_wrestler ? losing_wrestler.school.name : "Unknown"
return "#{self.weight.max} lbs - #{winning_wrestler.name} (#{winning_wrestler.school.name}) #{self.win_type} #{losing_wrestler_name} (#{losing_wrestler_school}) #{self.score}"
end
def bracket_winner_name
if winner_name != ""
return "#{winner_name} (#{Wrestler.find(winner_id).school.abbreviation})"
# Use the winner association directly
if self.winner
return "#{self.winner.name} (#{self.winner.school.abbreviation})"
else
""
end
@@ -319,4 +344,26 @@ class Match < ApplicationRecord
self.finished_at = Time.current.utc
end
end
def broadcast_mat_assignment_change
old_mat_id, new_mat_id = saved_change_to_mat_id || previous_changes["mat_id"]
return unless old_mat_id || new_mat_id
[old_mat_id, new_mat_id].compact.uniq.each do |mat_id|
mat = Mat.find_by(id: mat_id)
next unless mat
Turbo::StreamsChannel.broadcast_update_to(
mat,
target: dom_id(mat, :current_match),
partial: "mats/current_match",
locals: {
mat: mat,
match: mat.queue1_match,
next_match: mat.queue2_match,
show_next_bout_button: true
}
)
end
end
end

View File

@@ -1,8 +1,8 @@
class School < ApplicationRecord
belongs_to :tournament, touch: true
has_many :wrestlers, dependent: :destroy
has_many :deductedPoints, class_name: "Teampointadjust"
has_many :delegates, class_name: "SchoolDelegate"
has_many :deductedPoints, class_name: "Teampointadjust", dependent: :destroy
has_many :delegates, class_name: "SchoolDelegate", dependent: :destroy
validates :name, presence: true

View File

@@ -6,9 +6,10 @@ class Tournament < ApplicationRecord
has_many :mats, dependent: :destroy
has_many :wrestlers, through: :weights
has_many :matches, dependent: :destroy
has_many :delegates, class_name: "TournamentDelegate"
has_many :delegates, class_name: "TournamentDelegate", dependent: :destroy
has_many :mat_assignment_rules, dependent: :destroy
has_many :tournament_backups, dependent: :destroy
has_many :tournament_job_statuses, dependent: :destroy
validates :date, :name, :tournament_type, :address, :director, :director_email , presence: true
@@ -81,23 +82,18 @@ class Tournament < ApplicationRecord
matches.maximum(:round) || 0 # Return 0 if no matches or max round is nil
end
def assign_mats(mats_to_assign)
if mats_to_assign.count > 0
until mats_to_assign.sort_by{|m| m.id}.last.matches.count == 4
mats_to_assign.sort_by{|m| m.id}.each do |m|
m.assign_next_match
end
end
end
end
def reset_mats
matches.reload
mats.reload
matches_to_reset = matches.select{|m| m.mat_id != nil}
# matches_to_reset.update_all( {:mat_id => nil } )
matches_to_reset.each do |m|
m.mat_id = nil
m.save
end
mats.each do |mat|
mat.clear_queue!
end
end
def pointAdjustments
@@ -155,14 +151,14 @@ class Tournament < ApplicationRecord
def double_elim_number_of_wrestlers_error
error_string = ""
if self.tournament_type == "Double Elimination 1-6" or self.tournament_type == "Double Elimination 1-8"
weights_with_too_many_wrestlers = weights.select{|w| w.wrestlers.size > 32}
weight_with_too_few_wrestlers = weights.select{|w| w.wrestlers.size < 4}
if self.tournament_type == "Regular Double Elimination 1-6" or self.tournament_type == "Regular Double Elimination 1-8"
weights_with_too_many_wrestlers = weights.select{|w| w.wrestlers.size > 64}
weight_with_too_few_wrestlers = weights.select{|w| w.wrestlers.size < 2}
weights_with_too_many_wrestlers.each do |weight|
error_string = error_string + " The weight class #{weight.max} has more than 16 wrestlers."
error_string = error_string + " The weight class #{weight.max} has more than 64 wrestlers."
end
weight_with_too_few_wrestlers.each do |weight|
error_string = error_string + " The weight class #{weight.max} has less than 4 wrestlers."
error_string = error_string + " The weight class #{weight.max} has less than 2 wrestlers."
end
end
return error_string
@@ -227,19 +223,24 @@ class Tournament < ApplicationRecord
def reset_and_fill_bout_board
reset_mats
if mats.any?
4.times do
# Iterate over each mat and assign the next available match
mats.each do |mat|
match_assigned = mat.assign_next_match
# If no more matches are available, exit early
unless match_assigned
puts "No more eligible matches to assign."
return
end
matches.reload
refill_open_bout_board_queues
end
def refill_open_bout_board_queues
return unless mats.any?
loop do
assigned_any = false
# Fill in round-robin order by queue depth:
# all mats queue1 first, then queue2, then queue3, then queue4.
(1..4).each do |slot|
mats.reload.each do |mat|
next unless mat.public_send("queue#{slot}").nil?
assigned_any ||= mat.assign_next_match
end
end
end
break unless assigned_any
end
end
@@ -263,9 +264,19 @@ class Tournament < ApplicationRecord
return error_string.blank?
end
# Check if there are any active jobs for this tournament
def has_active_jobs?
tournament_job_statuses.active.exists?
end
# Get all active jobs for this tournament
def active_jobs
tournament_job_statuses.active
end
private
def connection_adapter
ActiveRecord::Base.connection.adapter_name
end
end
end

View File

@@ -0,0 +1,22 @@
class TournamentJobStatus < ApplicationRecord
belongs_to :tournament, optional: false
# Validations
validates :job_name, presence: true
validates :status, presence: true
validates_inclusion_of :status, in: ["Queued", "Running", "Errored"], allow_nil: false
validates :tournament, presence: true
# Scopes
scope :active, -> { where.not(status: "Errored") }
# Class methods to find jobs for a tournament
def self.for_tournament(tournament)
where(tournament_id: tournament.id)
end
# Clean up completed jobs (should be called when job finishes successfully)
def self.complete_job(tournament_id, job_name)
where(tournament_id: tournament_id, job_name: job_name).destroy_all
end
end

View File

@@ -4,8 +4,8 @@ class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
has_many :tournaments
has_many :delegated_tournament_permissions, class_name: "TournamentDelegate"
has_many :delegated_school_permissions, class_name: "SchoolDelegate"
has_many :delegated_tournament_permissions, class_name: "TournamentDelegate", dependent: :destroy
has_many :delegated_school_permissions, class_name: "SchoolDelegate", dependent: :destroy
# Replace Devise with has_secure_password
has_secure_password

View File

@@ -2,8 +2,13 @@ class Wrestler < ApplicationRecord
belongs_to :school, touch: true
belongs_to :weight, touch: true
has_one :tournament, through: :weight
has_many :matches, through: :weight
has_many :deductedPoints, class_name: "Teampointadjust"
has_many :deductedPoints, class_name: "Teampointadjust", dependent: :destroy
## Matches association
# Rails associations expect only a single column so we cannot do a w1 OR w2
# So we have to create two associations and combine them with the all_matches method
has_many :matches_as_w1, ->(wrestler){ where(weight_id: wrestler.weight_id) }, class_name: 'Match', foreign_key: 'w1'
has_many :matches_as_w2, ->(wrestler){ where(weight_id: wrestler.weight_id) }, class_name: 'Match', foreign_key: 'w2'
##
attr_accessor :poolAdvancePoints, :originalId, :swapId
validates :name, :weight_id, :school_id, presence: true
@@ -59,7 +64,7 @@ class Wrestler < ApplicationRecord
end
def winner_of_last_match?
if last_match.winner_id == self.id
if last_match && last_match.winner == self # Keep winner association change
return true
else
return false
@@ -87,28 +92,28 @@ class Wrestler < ApplicationRecord
end
def result_by_bout(bout)
bout_match = all_matches.select{|m| m.bout_number == bout and m.finished == 1}
if bout_match.size == 0
bout_match_results = all_matches.select{|m| m.bout_number == bout and m.finished == 1}
if bout_match_results.empty?
return ""
end
if bout_match.first.winner_id == self.id
return "W #{bout_match.first.bracket_score_string}"
end
if bout_match.first.winner_id != self.id
return "L #{bout_match.first.bracket_score_string}"
bout_match = bout_match_results.first
if bout_match.winner == self # Keep winner association change
return "W #{bout_match.bracket_score_string}"
else
return "L #{bout_match.bracket_score_string}"
end
end
def result_by_id(id)
bout_match = all_matches.select{|m| m.id == id and m.finished == 1}
if bout_match.size == 0
bout_match_results = all_matches.select{|m| m.id == id and m.finished == 1}
if bout_match_results.empty?
return ""
end
if bout_match.first.winner_id == self.id
return "W #{bout_match.first.bracket_score_string}"
end
if bout_match.first.winner_id != self.id
return "L #{bout_match.first.bracket_score_string}"
bout_match = bout_match_results.first
if bout_match.winner == self # Keep winner association change
return "W #{bout_match.bracket_score_string}"
else
return "L #{bout_match.bracket_score_string}"
end
end
@@ -120,7 +125,8 @@ class Wrestler < ApplicationRecord
if all_matches.blank?
return false
else
return true
# Original logic checked blank?, not specific round. Reverting to that.
return true
end
end
@@ -142,8 +148,12 @@ class Wrestler < ApplicationRecord
end
end
# Restore all_matches method
def all_matches
return matches.select{|m| m.w1 == self.id or m.w2 == self.id}
# Combine the two specific associations.
# This returns an Array, similar to the previous select method.
# Add .uniq for safety and sort for consistent order.
(matches_as_w1 + matches_as_w2).uniq.sort_by(&:bout_number)
end
def pool_matches
@@ -152,7 +162,9 @@ class Wrestler < ApplicationRecord
end
def has_a_pool_bye
if weight.pool_rounds(matches) > pool_matches.size
# Revert back to using all_matches here too? Seems complex.
# Sticking with original: uses `matches` (all weight) and `pool_matches` (derived from all_matches)
if weight.pool_rounds(all_matches) > pool_matches.size
return true
else
return false
@@ -188,7 +200,8 @@ class Wrestler < ApplicationRecord
end
def matches_won
all_matches.select{|m| m.winner_id == self.id}
# Revert, but keep using winner association check
all_matches.select{|m| m.winner == self}
end
def pool_wins
@@ -268,11 +281,17 @@ class Wrestler < ApplicationRecord
def season_win_percentage
win = self.season_win.to_f
loss = self.season_loss.to_f
# Revert to original logic
if win > 0 and loss != nil
match_total = win + loss
percentage_dec = win / match_total
percentage = percentage_dec * 100
return percentage.to_i
if match_total > 0
percentage_dec = win / match_total
percentage = percentage_dec * 100
return percentage.to_i
else
# Avoid division by zero if somehow win > 0 but total <= 0
return 0
end
elsif self.season_win == 0
return 0
elsif self.season_win == nil or self.season_loss == nil
@@ -281,6 +300,7 @@ class Wrestler < ApplicationRecord
end
def long_bracket_name
# Revert to original logic
return_string = ""
if self.original_seed
return_string = return_string + "[#{self.original_seed}] "
@@ -293,10 +313,12 @@ class Wrestler < ApplicationRecord
end
def short_bracket_name
# Revert to original logic
return "#{self.name} (#{self.school.abbreviation})"
end
def name_with_school
# Revert to original logic
return "#{self.name} - #{self.school.name}"
end
end

View File

@@ -8,16 +8,18 @@ class AdvanceWrestler
def advance
# Use perform_later which will execute based on centralized adapter config
# This will be converted to inline execution in test environment by ActiveJob
AdvanceWrestlerJob.perform_later(@wrestler, @last_match)
AdvanceWrestlerJob.perform_later(@wrestler, @last_match, @tournament.id)
end
def advance_raw
@last_match.reload
@wrestler.reload
if @last_match && @last_match.finished?
pool_to_bracket_advancement if @tournament.tournament_type == "Pool to bracket"
ModifiedDoubleEliminationAdvance.new(@wrestler, @last_match).bracket_advancement if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
DoubleEliminationAdvance.new(@wrestler, @last_match).bracket_advancement if @tournament.tournament_type.include? "Regular Double Elimination"
end
@wrestler.school.calculate_score
end
def pool_to_bracket_advancement
@@ -27,4 +29,4 @@ class AdvanceWrestler
PoolAdvance.new(@wrestler).advanceWrestler
end
end
end

View File

@@ -7,17 +7,22 @@ class DoubleEliminationAdvance
end
def bracket_advancement
if @last_match.winner_id == @wrestler.id
winner_advance
end
if @last_match.winner_id != @wrestler.id
loser_advance
end
advance_wrestler
advance_double_byes
set_bye_for_placement
end
def winner_advance
def advance_wrestler
# Advance winner
if @last_match.winner == @wrestler
winners_bracket_advancement
# Advance loser
elsif @last_match.winner != @wrestler
losers_bracket_advancement
end
end
def winners_bracket_advancement
if (@last_match.loser1_name == "BYE" or @last_match.loser2_name == "BYE") and @last_match.is_championship_match
update_consolation_bye
end
@@ -77,7 +82,7 @@ class DoubleEliminationAdvance
end
end
def loser_advance
def losers_bracket_advancement
bout = @last_match.bout_number
next_match = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?", "Loser of #{bout}", "Loser of #{bout}", @wrestler.weight_id).first

View File

@@ -7,17 +7,20 @@ class ModifiedDoubleEliminationAdvance
end
def bracket_advancement
if @last_match.winner_id == @wrestler.id
winner_advance
end
if @last_match.winner_id != @wrestler.id
loser_advance
end
advance_wrestler
advance_double_byes
set_bye_for_placement
end
def winner_advance
def advance_wrestler
if @last_match.winner == @wrestler
winners_bracket_advancement
elsif @last_match.winner != @wrestler
losers_bracket_advancement
end
end
def winners_bracket_advancement
if (@last_match.loser1_name == "BYE" or @last_match.loser2_name == "BYE") and @last_match.is_championship_match
update_consolation_bye
end
@@ -69,7 +72,7 @@ class ModifiedDoubleEliminationAdvance
end
end
def loser_advance
def losers_bracket_advancement
bout = @last_match.bout_number
next_match = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?", "Loser of #{bout}", "Loser of #{bout}", @wrestler.weight_id).first

View File

@@ -30,15 +30,20 @@ class PoolAdvance
end
def bracketAdvancment
if @last_match.winner_id == @wrestler.id
winnerAdvance
end
if @last_match.winner_id != @wrestler.id
loserAdvance
end
advance_wrestlers
end
def winnerAdvance
def advance_wrestlers
# Advance winner
if @last_match.winner == @wrestler
winner_advance
# Advance loser
elsif @last_match.winner != @wrestler
loser_advance
end
end
def winner_advance
if @wrestler.last_match.bracket_position == "Quarter"
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","Semis",@wrestler.next_match_position_number.ceil,@wrestler.weight_id).first
updateNewMatch(new_match)
@@ -65,7 +70,7 @@ class PoolAdvance
end
end
def loserAdvance
def loser_advance
bout = @wrestler.last_match.bout_number
next_match = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?","Loser of #{bout}","Loser of #{bout}",@wrestler.weight_id)
if next_match.size > 0

View File

@@ -29,7 +29,7 @@ class PoolOrder
def setOriginalPoints
@wrestlers.each do |w|
matches = w.matches.reload
matches = w.reload.all_matches
w.pool_placement_tiebreaker = nil
w.pool_placement = nil
w.poolAdvancePoints = w.pool_wins.size
@@ -80,10 +80,13 @@ class PoolOrder
def headToHead(wrestlers_with_same_points)
wrestlers_with_same_points.each do |wr|
otherWrestler = wrestlers_with_same_points.select{|w| w.id != wr.id}.first
if otherWrestler and wr.match_against(otherWrestler).select{|match| match.bracket_position == "Pool"}.first.winner_id == wr.id
addPointsToWrestlersAhead(wr)
wr.pool_placement_tiebreaker = "Head to Head"
if otherWrestler
matches = wr.match_against(otherWrestler).select { |match| match.bracket_position == "Pool" }
if matches.any? && matches.first.winner == wr
addPointsToWrestlersAhead(wr)
wr.pool_placement_tiebreaker = "Head to Head"
addPoints(wr)
end
end
end
end

View File

@@ -3,130 +3,206 @@ class DoubleEliminationGenerateLoserNames
@tournament = tournament
end
# Entry point: assign loser placeholders and advance any byes
def assign_loser_names
@tournament.weights.each do |weight|
assign_loser_names_for_weight(weight)
advance_bye_matches_championship(weight.matches.reload)
# only assign loser names if there's conso matches to be had
if weight.calculate_bracket_size > 2
assign_loser_names_for_weight(weight)
advance_bye_matches_championship(weight)
advance_bye_matches_consolation(weight)
end
end
end
def define_losername_championship_mappings(bracket_size)
# Use hashes instead of arrays for mappings
case bracket_size
when 4
[
{ conso_bracket_position: "3/4", championship_bracket_position: "Semis", cross_bracket: false, both_wrestlers: true }
]
when 8
[
{ conso_bracket_position: "Conso Quarter", championship_bracket_position: "Quarter", cross_bracket: false, both_wrestlers: true },
{ conso_bracket_position: "Conso Semis", championship_bracket_position: "Semis", cross_bracket: true, both_wrestlers: false }
]
when 16
[
{ conso_bracket_position: "Conso Round of 8.1", championship_bracket_position: "Bracket Round of 16", cross_bracket: false, both_wrestlers: true },
{ conso_bracket_position: "Conso Round of 8.2", championship_bracket_position: "Quarter", cross_bracket: true, both_wrestlers: false },
{ conso_bracket_position: "Conso Semis", championship_bracket_position: "Semis", cross_bracket: false, both_wrestlers: false }
]
when 32
[
{ conso_bracket_position: "Conso Round of 16.1", championship_bracket_position: "Bracket Round of 32", cross_bracket: false, both_wrestlers: true },
{ conso_bracket_position: "Conso Round of 16.2", championship_bracket_position: "Bracket Round of 16", cross_bracket: true, both_wrestlers: false },
{ conso_bracket_position: "Conso Round of 8.2", championship_bracket_position: "Quarter", cross_bracket: false, both_wrestlers: false },
{ conso_bracket_position: "Conso Semis", championship_bracket_position: "Semis", cross_bracket: true, both_wrestlers: false },
]
else
nil
end
end
private
# Assign loser names for a single weight bracket
def assign_loser_names_for_weight(weight)
number_of_placers = @tournament.number_of_placers
bracket_size = weight.calculate_bracket_size
matches_by_weight = weight.matches.reload
matches = weight.matches.reload
num_placers = @tournament.number_of_placers
loser_name_championship_mappings = define_losername_championship_mappings(bracket_size)
# Build dynamic round definitions
champ_rounds = dynamic_championship_rounds(bracket_size)
conso_rounds = dynamic_consolation_rounds(bracket_size)
first_round = { bracket_position: first_round_label(bracket_size) }
champ_full = [first_round] + champ_rounds
loser_name_championship_mappings.each do |mapping|
conso_bracket_position = mapping[:conso_bracket_position]
championship_bracket_position = mapping[:championship_bracket_position]
cross_bracket = mapping[:cross_bracket]
both_wrestlers = mapping[:both_wrestlers]
# Map championship losers into consolation slots
mappings = []
champ_full[0...-1].each_with_index do |champ_info, i|
map_idx = i.zero? ? 0 : (2 * i - 1)
next if map_idx < 0 || map_idx >= conso_rounds.size
conso_matches = matches_by_weight.select do |match|
match.bracket_position == conso_bracket_position && match.bracket_position == conso_bracket_position
end.sort_by(&:bracket_position_number)
mappings << {
championship_bracket_position: champ_info[:bracket_position],
consolation_bracket_position: conso_rounds[map_idx][:bracket_position],
both_wrestlers: i.zero?,
champ_round_index: i
}
end
championship_matches = matches_by_weight.select do |match|
match.bracket_position == championship_bracket_position && match.bracket_position == championship_bracket_position
end.sort_by(&:bracket_position_number)
# Apply loser-name mappings
mappings.each do |map|
champ = matches.select { |m| m.bracket_position == map[:championship_bracket_position] }
.sort_by(&:bracket_position_number)
conso = matches.select { |m| m.bracket_position == map[:consolation_bracket_position] }
.sort_by(&:bracket_position_number)
current_champ_round_index = map[:champ_round_index]
if current_champ_round_index.odd?
conso.reverse!
end
conso_matches.reverse! if cross_bracket
idx = 0
# Determine if this mapping is for losers from the first championship round
is_first_champ_round_feed = map[:champ_round_index].zero?
championship_bracket_position_number = 1
conso_matches.each do |match|
bout_number1 = championship_matches.find do |bout_match|
bout_match.bracket_position_number == championship_bracket_position_number
end.bout_number
match.loser1_name = "Loser of #{bout_number1}"
if both_wrestlers
championship_bracket_position_number += 1
bout_number2 = championship_matches.find do |bout_match|
bout_match.bracket_position_number == championship_bracket_position_number
end.bout_number
match.loser2_name = "Loser of #{bout_number2}"
conso.each do |cm|
champ_match1 = champ[idx]
if champ_match1
if is_first_champ_round_feed && ((champ_match1.w1 && champ_match1.w2.nil?) || (champ_match1.w1.nil? && champ_match1.w2))
cm.loser1_name = "BYE"
else
cm.loser1_name = "Loser of #{champ_match1.bout_number}"
end
else
cm.loser1_name = nil # Should not happen if bracket generation is correct
end
championship_bracket_position_number += 1
if map[:both_wrestlers] # This is true only if is_first_champ_round_feed
idx += 1 # Increment for the second championship match
champ_match2 = champ[idx]
if champ_match2
# BYE check is only relevant for the first championship round feed
if is_first_champ_round_feed && ((champ_match2.w1 && champ_match2.w2.nil?) || (champ_match2.w1.nil? && champ_match2.w2))
cm.loser2_name = "BYE"
else
cm.loser2_name = "Loser of #{champ_match2.bout_number}"
end
else
cm.loser2_name = nil # Should not happen
end
end
idx += 1 # Increment for the next consolation match or next pair from championship
end
end
conso_semi_matches = matches_by_weight.select { |match| match.bracket_position == "Conso Semis" }
conso_quarter_matches = matches_by_weight.select { |match| match.bracket_position == "Conso Quarter" }
if number_of_placers >= 6 && weight.wrestlers.size >= 5
five_six_match = matches_by_weight.find { |match| match.bracket_position == "5/6" }
bout_number1 = conso_semi_matches.find { |match| match.bracket_position_number == 1 }.bout_number
bout_number2 = conso_semi_matches.find { |match| match.bracket_position_number == 2 }.bout_number
five_six_match.loser1_name = "Loser of #{bout_number1}"
five_six_match.loser2_name = "Loser of #{bout_number2}"
# 5th/6th place
if bracket_size >= 5 && num_placers >= 6 && weight.wrestlers.size > 4
conso_semis = matches.select { |m| m.bracket_position == "Conso Semis" }
.sort_by(&:bracket_position_number)
if conso_semis.size >= 2
m56 = matches.find { |m| m.bracket_position == "5/6" }
m56.loser1_name = "Loser of #{conso_semis[0].bout_number}"
m56.loser2_name = "Loser of #{conso_semis[1].bout_number}" if m56
end
end
if number_of_placers >= 8 && weight.wrestlers.size >= 7
seven_eight_match = matches_by_weight.find { |match| match.bracket_position == "7/8" }
bout_number1 = conso_quarter_matches.find { |match| match.bracket_position_number == 1 }.bout_number
bout_number2 = conso_quarter_matches.find { |match| match.bracket_position_number == 2 }.bout_number
seven_eight_match.loser1_name = "Loser of #{bout_number1}"
seven_eight_match.loser2_name = "Loser of #{bout_number2}"
# 7th/8th place
if bracket_size >= 7 && num_placers >= 8 && weight.wrestlers.size > 6
conso_quarters = matches.select { |m| m.bracket_position == "Conso Quarter" }
.sort_by(&:bracket_position_number)
if conso_quarters.size >= 2
m78 = matches.find { |m| m.bracket_position == "7/8" }
m78.loser1_name = "Loser of #{conso_quarters[0].bout_number}"
m78.loser2_name = "Loser of #{conso_quarters[1].bout_number}" if m78
end
end
save_matches(matches_by_weight)
end
def save_matches(matches)
matches.each(&:save!)
end
def advance_bye_matches_championship(matches)
first_round = matches.sort_by{|m| m.round}.first.round
matches.select do |m|
m.round == first_round
end.sort_by(&:bracket_position_number).each do |match|
next unless match.w1.nil? || match.w2.nil?
# Advance first-round byes in championship bracket
def advance_bye_matches_championship(weight)
matches = weight.matches.reload
first_round = matches.map(&:round).min
matches.select { |m| m.round == first_round }
.sort_by(&:bracket_position_number)
.each { |m| handle_bye(m) }
end
# Advance first-round byes in consolation bracket
def advance_bye_matches_consolation(weight)
matches = weight.matches.reload
bracket_size = weight.calculate_bracket_size
first_conso = dynamic_consolation_rounds(bracket_size).first
matches.select { |m| m.round == first_conso[:round] && m.bracket_position == first_conso[:bracket_position] }
.sort_by(&:bracket_position_number)
.each { |m| handle_bye(m) }
end
# Mark bye match, set finished, and advance
def handle_bye(match)
if [match.w1, match.w2].compact.size == 1
match.finished = 1
match.win_type = "BYE"
match.win_type = 'BYE'
if match.w1
match.winner_id = match.w1
match.loser2_name = "BYE"
elsif match.w2
match.winner_id = match.w2
match.loser1_name = "BYE"
match.winner_id = match.w1
match.loser2_name = 'BYE'
else
match.winner_id = match.w2
match.loser1_name = 'BYE'
end
match.score = ""
match.save
match.score = ''
match.save!
match.advance_wrestlers
end
end
# Helpers for dynamic bracket labels
def first_round_label(size)
case size
when 2 then 'Final'
when 4 then 'Semis'
when 8 then 'Quarter'
else "Bracket Round of #{size}"
end
end
def dynamic_championship_rounds(size)
total = Math.log2(size).to_i
(1...total).map do |i|
participants = size / (2**i)
{ bracket_position: bracket_label(participants), round: i + 1 }
end
end
def dynamic_consolation_rounds(size)
total_log2 = Math.log2(size).to_i
return [] if total_log2 <= 1
max_j_val = (2 * (total_log2 - 1) - 1)
(1..max_j_val).map do |j|
current_participants = size / (2**((j.to_f / 2).ceil))
{
bracket_position: consolation_label(current_participants, j, size),
round: j
}
end
end
def bracket_label(participants)
case participants
when 2 then '1/2'
when 4 then 'Semis'
when 8 then 'Quarter'
else "Bracket Round of #{participants}"
end
end
def consolation_label(participants, j, bracket_size)
max_j_for_bracket = (2 * (Math.log2(bracket_size).to_i - 1) - 1)
if participants == 2 && j == max_j_for_bracket
return '3/4'
elsif participants == 4
return j.odd? ? 'Conso Quarter' : 'Conso Semis'
else
suffix = j.odd? ? ".1" : ".2"
return "Conso Round of #{participants}#{suffix}"
end
end
end

View File

@@ -27,10 +27,10 @@ class DoubleEliminationMatchGeneration
# 1) Round one matchups
bracket_info[:round_one_matchups].each_with_index do |matchup, idx|
seed1, seed2 = matchup[:seeds]
bracket_position = matchup[:bracket_position]
bracket_pos_number = idx + 1
round_number = matchup[:round] # Use the round from our definition
seed1, seed2 = matchup[:seeds]
bracket_position = matchup[:bracket_position]
bracket_pos_number = idx + 1
round_number = matchup[:round]
create_matchup_from_seed(
seed1,
@@ -77,167 +77,112 @@ class DoubleEliminationMatchGeneration
)
end
#
# 5/6, 7/8 placing logic
#
if weight.wrestlers.size >= 5
if @tournament.number_of_placers >= 6 && matches_this_round == 1
create_matchup(nil, nil, "5/6", 1, round_number, weight)
end
if weight.wrestlers.size >= 5 && @tournament.number_of_placers >= 6 && matches_this_round == 1
create_matchup(nil, nil, "5/6", 1, round_number, weight)
end
if weight.wrestlers.size >= 7
if @tournament.number_of_placers >= 8 && matches_this_round == 1
create_matchup(nil, nil, "7/8", 1, round_number, weight)
end
if weight.wrestlers.size >= 7 && @tournament.number_of_placers >= 8 && matches_this_round == 1
create_matchup(nil, nil, "7/8", 1, round_number, weight)
end
end
end
#
# Single bracket definition that includes both bracket_position and round.
# If you later decide to tweak round numbering, you do it in ONE place.
#
# Single bracket definition dynamically generated for any power-of-two bracket size.
# Returns a hash with :round_one_matchups, :championship_rounds, and :consolation_rounds.
def define_bracket_matches(bracket_size)
case bracket_size
when 4
{
round_one_matchups: [
# First round is Semis => round=1
{ seeds: [1, 4], bracket_position: "Semis", round: 1 },
{ seeds: [2, 3], bracket_position: "Semis", round: 1 }
],
championship_rounds: [
# Final => round=2
{ bracket_position: "1/2", number_of_matches: 1, round: 2 }
],
consolation_rounds: [
# 3rd place => round=2
{ bracket_position: "3/4", number_of_matches: 1, round: 2 }
]
}
# Only support brackets that are powers of two
return nil unless (bracket_size & (bracket_size - 1)).zero?
when 8
{
round_one_matchups: [
# Quarter => round=1
{ seeds: [1, 8], bracket_position: "Quarter", round: 1 },
{ seeds: [4, 5], bracket_position: "Quarter", round: 1 },
{ seeds: [3, 6], bracket_position: "Quarter", round: 1 },
{ seeds: [2, 7], bracket_position: "Quarter", round: 1 }
],
championship_rounds: [
# Semis => round=2, Final => round=4
{ bracket_position: "Semis", number_of_matches: 2, round: 2 },
{ bracket_position: "1/2", number_of_matches: 1, round: 4 }
],
consolation_rounds: [
# Conso Quarter => round=2, Conso Semis => round=3, 3/4 => round=4
{ bracket_position: "Conso Quarter", number_of_matches: 2, round: 2 },
{ bracket_position: "Conso Semis", number_of_matches: 2, round: 3 },
{ bracket_position: "3/4", number_of_matches: 1, round: 4 }
]
}
# 1) Generate the seed sequence (e.g., [1,8,5,4,...] for size=8)
seeds = generate_seed_sequence(bracket_size)
when 16
# 2) Pair seeds into first-round matchups, sorting so lower seed is w1
round_one = seeds.each_slice(2).map.with_index do |(s1, s2), idx|
a, b = [s1, s2].sort
{
round_one_matchups: [
{ seeds: [1,16], bracket_position: "Bracket Round of 16", round: 1 },
{ seeds: [8,9], bracket_position: "Bracket Round of 16", round: 1 },
{ seeds: [5,12], bracket_position: "Bracket Round of 16", round: 1 },
{ seeds: [4,13], bracket_position: "Bracket Round of 16", round: 1 },
{ seeds: [3,14], bracket_position: "Bracket Round of 16", round: 1 },
{ seeds: [6,11], bracket_position: "Bracket Round of 16", round: 1 },
{ seeds: [7,10], bracket_position: "Bracket Round of 16", round: 1 },
{ seeds: [2,15], bracket_position: "Bracket Round of 16", round: 1 }
],
championship_rounds: [
# Quarter => round=2, Semis => round=4, Final => round=6
{ bracket_position: "Quarter", number_of_matches: 4, round: 2 },
{ bracket_position: "Semis", number_of_matches: 2, round: 4 },
{ bracket_position: "1/2", number_of_matches: 1, round: 6 }
],
consolation_rounds: [
# Just carry over your standard numbering
{ bracket_position: "Conso Round of 8.1", number_of_matches: 4, round: 2 },
{ bracket_position: "Conso Round of 8.2", number_of_matches: 4, round: 3 },
{ bracket_position: "Conso Quarter", number_of_matches: 2, round: 4 },
{ bracket_position: "Conso Semis", number_of_matches: 2, round: 5 },
{ bracket_position: "3/4", number_of_matches: 1, round: 6 }
]
seeds: [a, b],
bracket_position: first_round_label(bracket_size),
round: 1
}
when 32
{
round_one_matchups: [
{ seeds: [1,32], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [16,17], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [9,24], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [8,25], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [5,28], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [12,21], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [13,20], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [4,29], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [3,30], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [14,19], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [11,22], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [6,27], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [7,26], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [10,23], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [15,18], bracket_position: "Bracket Round of 32", round: 1 },
{ seeds: [2,31], bracket_position: "Bracket Round of 32", round: 1 }
],
championship_rounds: [
{ bracket_position: "Bracket Round of 16", number_of_matches: 8, round: 2 },
{ bracket_position: "Quarter", number_of_matches: 4, round: 4 },
{ bracket_position: "Semis", number_of_matches: 2, round: 6 },
{ bracket_position: "1/2", number_of_matches: 1, round: 8 }
],
consolation_rounds: [
{ bracket_position: "Conso Round of 16.1", number_of_matches: 8, round: 2 },
{ bracket_position: "Conso Round of 16.2", number_of_matches: 8, round: 3 },
{ bracket_position: "Conso Round of 8.1", number_of_matches: 4, round: 4 },
{ bracket_position: "Conso Round of 8.2", number_of_matches: 4, round: 5 },
{ bracket_position: "Conso Quarter", number_of_matches: 2, round: 6 },
{ bracket_position: "Conso Semis", number_of_matches: 2, round: 7 },
{ bracket_position: "3/4", number_of_matches: 1, round: 8 }
]
}
else
nil
end
# 3) Build full structure, including dynamic championship & consolation rounds
{
round_one_matchups: round_one,
championship_rounds: dynamic_championship_rounds(bracket_size),
consolation_rounds: dynamic_consolation_rounds(bracket_size)
}
end
# Returns a human-readable label for the first round based on bracket size.
def first_round_label(bracket_size)
case bracket_size
when 2 then "1/2"
when 4 then "Semis"
when 8 then "Quarter"
else "Bracket Round of #{bracket_size}"
end
end
# Dynamically generate championship rounds for any power-of-two bracket size.
def dynamic_championship_rounds(bracket_size)
rounds = []
num_rounds = Math.log2(bracket_size).to_i
# i: 1 -> first post-initial round, up to num_rounds-1 (final)
(1...num_rounds).each do |i|
participants = bracket_size / (2**i)
number_of_matches = participants / 2
bracket_position = case participants
when 2 then "1/2"
when 4 then "Semis"
when 8 then "Quarter"
else "Bracket Round of #{participants}"
end
round_number = i * 2
rounds << { bracket_position: bracket_position,
number_of_matches: number_of_matches,
round: round_number }
end
rounds
end
# Dynamically generate consolation rounds for any power-of-two bracket size.
def dynamic_consolation_rounds(bracket_size)
rounds = []
num_rounds = Math.log2(bracket_size).to_i
total_conso = 2 * (num_rounds - 1) - 1
(1..total_conso).each do |j|
participants = bracket_size / (2**((j.to_f / 2).ceil))
number_of_matches = participants / 2
bracket_position = case participants
when 2 then "3/4"
when 4
j.odd? ? "Conso Quarter" : "Conso Semis"
else
suffix = j.odd? ? ".1" : ".2"
"Conso Round of #{participants}#{suffix}"
end
round_number = j + 1
rounds << { bracket_position: bracket_position,
number_of_matches: number_of_matches,
round: round_number }
end
rounds
end
###########################################################################
# PHASE 2: Overwrite rounds in all smaller brackets to match the largest one.
###########################################################################
def align_all_rounds_to_largest_bracket
#
# 1) Find the bracket size that is largest
#
largest_weight = @tournament.weights.max_by { |w| w.calculate_bracket_size }
return unless largest_weight
#
# 2) Gather all matches for that bracket. Build a map from bracket_position => round
#
# We assume "largest bracket" is the single weight with the largest bracket_size.
#
largest_bracket_size = largest_weight.calculate_bracket_size
largest_matches = largest_weight.tournament.matches.where(weight_id: largest_weight.id)
position_to_round = {}
largest_matches.each do |m|
# In case multiple matches have the same bracket_position but different rounds
# (like "3/4" might appear more than once), you can pick the first or max.
position_to_round[m.bracket_position] ||= m.round
largest_weight.tournament.matches.where(weight_id: largest_weight.id).each do |m|
position_to_round[m.bracket_position] ||= m.round
end
#
# 3) For every other match in the entire tournament (including possibly the largest bracket, if you want),
# overwrite the round to match this map.
#
@tournament.matches.find_each do |match|
# If there's a known round for this bracket_position, use it
if position_to_round.key?(match.bracket_position)
match.update(round: position_to_round[match.bracket_position])
end
@@ -272,4 +217,23 @@ class DoubleEliminationMatchGeneration
bracket_position_number: bracket_position_number
)
end
# Calculates the sequence of seeds for the first round of a power-of-two bracket.
def generate_seed_sequence(n)
raise ArgumentError, "Bracket size must be a power of two" unless (n & (n - 1)).zero?
return [1, 2] if n == 2
half = n / 2
prev = generate_seed_sequence(half)
comp = prev.map { |s| n + 1 - s }
result = []
(0...prev.size).step(2) do |k|
result << prev[k]
result << comp[k]
result << comp[k + 1]
result << prev[k + 1]
end
result
end
end

View File

@@ -37,11 +37,19 @@ class TournamentBackupService
attributes: @tournament.attributes,
schools: @tournament.schools.map(&:attributes),
weights: @tournament.weights.map(&:attributes),
mats: @tournament.mats.map(&:attributes),
mats: @tournament.mats.map do |mat|
mat.attributes.merge(
"queue_bout_numbers" => mat.queue_matches.map { |match| match&.bout_number }
)
end,
mat_assignment_rules: @tournament.mat_assignment_rules.map do |rule|
rule.attributes.merge(
mat: Mat.find_by(id: rule.mat_id)&.attributes.slice("name"),
weight_classes: rule.weight_classes.map do |weight_id|
# Emit the human-readable max values under a distinct key to avoid
# colliding with the raw DB-backed "weight_classes" attribute (which
# is stored as a comma-separated string). Using a different key
# prevents duplicate JSON keys when symbols and strings are both present.
"weight_class_maxes" => rule.weight_classes.map do |weight_id|
Weight.find_by(id: weight_id)&.max
end
)
@@ -54,11 +62,11 @@ class TournamentBackupService
end,
matches: @tournament.matches.sort_by(&:bout_number).map do |match|
match.attributes.merge(
w1_name: Wrestler.find_by(id: match.w1)&.name,
w2_name: Wrestler.find_by(id: match.w2)&.name,
winner_name: Wrestler.find_by(id: match.winner_id)&.name,
weight: Weight.find_by(id: match.weight_id)&.attributes,
mat: Mat.find_by(id: match.mat_id)&.attributes
w1_name: match.wrestler1&.name,
w2_name: match.wrestler2&.name,
winner_name: match.winner&.name,
weight: match.weight&.attributes,
mat: match.mat&.attributes
)
end
}

View File

@@ -18,15 +18,15 @@ class TournamentSeeding
def random_seeding(wrestlers, bracket_size)
half_of_bracket = bracket_size / 2
available_bracket_lines = (1..bracket_size).to_a
first_half_available_bracket_lines = (1..half_of_bracket).to_a
# remove bracket lines that are taken from available_bracket_lines
wrestlers_with_bracket_lines = wrestlers.select{|w| w.bracket_line != nil }
wrestlers_with_bracket_lines.each do |wrestler|
available_bracket_lines.delete(wrestler.bracket_line)
first_half_available_bracket_lines.delete(wrestler.bracket_line)
end
available_bracket_lines_to_use = set_random_seeding_bracket_line_order(available_bracket_lines)
wrestlers_without_bracket_lines = wrestlers.select{|w| w.bracket_line == nil }
if @tournament.tournament_type == "Pool to bracket"
wrestlers_without_bracket_lines.shuffle.each do |wrestler|
@@ -38,15 +38,10 @@ class TournamentSeeding
else
# Iterrate over the list randomly
wrestlers_without_bracket_lines.shuffle.each do |wrestler|
if first_half_available_bracket_lines.size > 0
random_available_bracket_line = first_half_available_bracket_lines.sample
wrestler.bracket_line = random_available_bracket_line
available_bracket_lines.delete(random_available_bracket_line)
first_half_available_bracket_lines.delete(random_available_bracket_line)
else
random_available_bracket_line = available_bracket_lines.sample
wrestler.bracket_line = random_available_bracket_line
available_bracket_lines.delete(random_available_bracket_line)
if available_bracket_lines_to_use.size > 0
bracket_line_to_use = available_bracket_lines_to_use.first
wrestler.bracket_line = bracket_line_to_use
available_bracket_lines_to_use.delete(bracket_line_to_use)
end
end
end
@@ -81,4 +76,24 @@ class TournamentSeeding
end
return wrestlers
end
private
def set_random_seeding_bracket_line_order(available_bracket_lines)
# This method prevents double BYEs in round 1
# It also evenly distributes matches from the top half of the bracket to the bottom half
# It does both of these while keeping the randomness of the line assignment
odd_or_even = [0, 1]
odd_or_even_sample = odd_or_even.sample
# sort by odd or even based on the sample above
if odd_or_even_sample == 1
# odd numbers first
result = available_bracket_lines.sort_by { |n| n.even? ? 1 : 0 }
else
# even numbers first
result = available_bracket_lines.sort_by { |n| n.odd? ? 1 : 0 }
end
result
end
end

View File

@@ -31,25 +31,30 @@ class WrestlingdevImporter
end
def destroy_all
@tournament.mat_assignment_rules.destroy_all
@tournament.mats.destroy_all
# These depend directly on @tournament and will cascade deletes
# due to `dependent: :destroy` in the Tournament model
@tournament.schools.destroy_all # Cascades to Wrestlers, Teampointadjusts, SchoolDelegates
@tournament.weights.destroy_all # Cascades to Wrestlers, Matches
@tournament.mats.destroy_all # Cascades to Matches, MatAssignmentRules
# Explicitly destroy matches again just in case some aren't linked via mats/weights? Unlikely but safe.
# Also handles matches linked directly to tournament if that's possible.
@tournament.matches.destroy_all
@tournament.schools.each do |school|
school.wrestlers.destroy_all
school.destroy
end
@tournament.weights.destroy_all
@tournament.mat_assignment_rules.destroy_all # Explicitly destroy rules (might be redundant if Mat cascades)
@tournament.delegates.destroy_all
@tournament.tournament_job_statuses.destroy_all
# Note: Teampointadjusts are deleted via School/Wrestler cascade
end
def parse_data
parse_tournament(@import_data["tournament"]["attributes"])
parse_schools(@import_data["tournament"]["schools"])
parse_weights(@import_data["tournament"]["weights"])
parse_mats(@import_data["tournament"]["mats"])
parse_wrestlers(@import_data["tournament"]["wrestlers"])
parse_matches(@import_data["tournament"]["matches"])
parse_mat_assignment_rules(@import_data["tournament"]["mat_assignment_rules"])
end
def parse_data
parse_tournament(@import_data["tournament"]["attributes"])
parse_schools(@import_data["tournament"]["schools"])
parse_weights(@import_data["tournament"]["weights"])
parse_mats(@import_data["tournament"]["mats"])
parse_wrestlers(@import_data["tournament"]["wrestlers"])
parse_matches(@import_data["tournament"]["matches"])
apply_mat_queues
parse_mat_assignment_rules(@import_data["tournament"]["mat_assignment_rules"])
end
def parse_tournament(attributes)
attributes.except!("id")
@@ -70,28 +75,47 @@ class WrestlingdevImporter
end
end
def parse_mats(mats)
mats.each do |mat_attributes|
mat_attributes.except!("id")
Mat.create(mat_attributes.merge(tournament_id: @tournament.id))
end
end
def parse_mats(mats)
@mat_queue_bout_numbers = {}
mats.each do |mat_attributes|
mat_name = mat_attributes["name"]
queue_bout_numbers = mat_attributes["queue_bout_numbers"]
mat_attributes.except!("id", "queue1", "queue2", "queue3", "queue4", "queue_bout_numbers", "tournament_id")
Mat.create(mat_attributes.merge(tournament_id: @tournament.id))
if mat_name && queue_bout_numbers
@mat_queue_bout_numbers[mat_name] = queue_bout_numbers
end
end
end
def parse_mat_assignment_rules(mat_assignment_rules)
mat_assignment_rules.each do |rule_attributes|
mat_name = rule_attributes.dig("mat", "name")
mat = Mat.find_by(name: mat_name, tournament_id: @tournament.id)
# Map max values of weight_classes to their new IDs
new_weight_classes = rule_attributes["weight_classes"].map do |max_value|
Weight.find_by(max: max_value, tournament_id: @tournament.id)&.id
end.compact
# Extract bracket_positions and rounds
# Prefer the new "weight_class_maxes" key emitted by backups (human-readable
# max values). If not present, fall back to the legacy "weight_classes"
# value which may be a comma-separated string or an array of IDs.
if rule_attributes.key?("weight_class_maxes") && rule_attributes["weight_class_maxes"].respond_to?(:map)
new_weight_classes = rule_attributes["weight_class_maxes"].map do |max_value|
Weight.find_by(max: max_value, tournament_id: @tournament.id)&.id
end.compact
elsif rule_attributes["weight_classes"].is_a?(Array)
# Already an array of IDs
new_weight_classes = rule_attributes["weight_classes"].map(&:to_i)
elsif rule_attributes["weight_classes"].is_a?(String)
# Comma-separated IDs stored in the DB column; split into integers.
new_weight_classes = rule_attributes["weight_classes"].to_s.split(",").map(&:strip).reject(&:empty?).map(&:to_i)
else
new_weight_classes = []
end
# Extract bracket_positions and rounds (leave as-is; model will coerce if needed)
bracket_positions = rule_attributes["bracket_positions"]
rounds = rule_attributes["rounds"]
rule_attributes.except!("id", "mat", "tournament_id", "weight_classes")
# Remove any keys we don't want to mass-assign (including both old/new weight keys)
rule_attributes.except!("id", "mat", "tournament_id", "weight_classes", "weight_class_maxes")
MatAssignmentRule.create(
rule_attributes.merge(
@@ -117,9 +141,9 @@ class WrestlingdevImporter
end
end
def parse_matches(matches)
matches.each do |match_attributes|
next unless match_attributes # Skip if match_attributes is nil
def parse_matches(matches)
matches.each do |match_attributes|
next unless match_attributes # Skip if match_attributes is nil
weight = Weight.find_by(max: match_attributes.dig("weight", "max"), tournament_id: @tournament.id)
mat = Mat.find_by(name: match_attributes.dig("mat", "name"), tournament_id: @tournament.id)
@@ -138,6 +162,53 @@ class WrestlingdevImporter
w2: w2&.id,
winner_id: winner&.id
))
end
end
end
end
end
def apply_mat_queues
if @mat_queue_bout_numbers.blank?
Mat.where(tournament_id: @tournament.id).find_each do |mat|
match_ids = mat.matches.where(finished: [nil, 0]).order(:bout_number).limit(4).pluck(:id)
mat.update(
queue1: match_ids[0],
queue2: match_ids[1],
queue3: match_ids[2],
queue4: match_ids[3]
)
end
return
end
@mat_queue_bout_numbers.each do |mat_name, bout_numbers|
mat = Mat.find_by(name: mat_name, tournament_id: @tournament.id)
next unless mat
matches = Array(bout_numbers).map do |bout_number|
Match.find_by(bout_number: bout_number, tournament_id: @tournament.id)
end
mat.update(
queue1: matches[0]&.id,
queue2: matches[1]&.id,
queue3: matches[2]&.id,
queue4: matches[3]&.id
)
matches.compact.each do |match|
match.update(mat_id: mat.id)
end
end
Mat.where(tournament_id: @tournament.id)
.where(queue1: nil, queue2: nil, queue3: nil, queue4: nil)
.find_each do |mat|
match_ids = mat.matches.where(finished: [nil, 0]).order(:bout_number).limit(4).pluck(:id)
mat.update(
queue1: match_ids[0],
queue2: match_ids[1],
queue3: match_ids[2],
queue4: match_ids[3]
)
end
end
end

View File

@@ -5,7 +5,7 @@ class CalculateWrestlerTeamScore
end
def totalScore
if @wrestler.extra or @wrestler.matches.count == 0
if @wrestler.extra or @wrestler.all_matches.count == 0
return 0
else
earnedPoints - deductedPoints

View File

@@ -27,10 +27,22 @@ class DoubleEliminationPlacementPoints
end
def bracket_position_size(bracket_position_name)
@wrestler.all_matches.select{|m| m.bracket_position == bracket_position_name}.size
@wrestler.all_matches.select{|m| m.bracket_position == bracket_position_name}.size
end
def won_bracket_position_size(bracket_position_name)
@wrestler.matches_won.select{|m| m.bracket_position == bracket_position_name}.size
end
def bracket_placement_points(bracket_position_name)
if bracket_position_name == "Did not place"
return 0
end
if @wrestler.participating_matches.where(bracket_position: bracket_position_name).count > 0
points = Teampointadjust.find_by(tournament_id: @wrestler.tournament.id, points_for_placement: bracket_position_name)
if points
# ... existing code ...
end
end
end
end

View File

@@ -28,7 +28,7 @@ json.cache! ["api_tournament", @tournament] do
json.mats @mats do |mat|
json.name mat.name
json.unfinished_matches mat.unfinished_matches do |match|
json.unfinished_matches mat.queue_matches.compact do |match|
json.bout_number match.bout_number
json.w1_name match.w1_name
json.w2_name match.w2_name

View File

@@ -1,6 +1,3 @@
<!-- Fontawesome CDN -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.1/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<!-- Fontawesome Modifications -->
<style>
.fa {

View File

@@ -10,7 +10,7 @@
<a class="navbar-brand" href="/">WrestlingDev</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<ul class="nav navbar-nav navbar-right">
<li><%= link_to "Browse Tournaments", "/tournaments/" %></li>
<li><%= link_to "About", "/static_pages/about" %></li>
<li><%= link_to "Tutorials", "/static_pages/tutorials" %></li>
@@ -19,9 +19,9 @@
<a class="dropdown-toggle" data-toggle="dropdown" href="#"><%= current_user.email %>
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><%= link_to "Log out", logout_path, method: :delete %></li>
<li><%= button_to "Log out", logout_path, method: :delete, class: 'btn btn-link', form: { class: 'navbar-logout-form' } %></li>
<li><%= link_to "Edit profile", edit_user_path(current_user) %></li>
<li><%= link_to "My tournaments and schools", "/static_pages/my_tournaments" %></li>
<li><%= link_to "My tournaments and schools", "/static_pages/my_tournaments" %></li>
</ul>
</li>
<% else %>

View File

@@ -1,13 +1,21 @@
<% if @tournament and @tournament.id %>
<h2><%= @tournament.name %> Links</h2>
<nav class="navbar navbar-default navbar-static-top" role="navigation" id="tournament-navbar">
<nav class="navbar navbar-default navbar-fixed-top" role="navigation" id="tournament-navbar">
<div class="container">
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><%= link_to " Tournament Home" , "/tournaments/#{@tournament.id}", class: "fas fa-home" %></li>
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#tournament-navbar-collapse" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<%= link_to "Tournament Menu", "/tournaments/#{@tournament.id}", class: "navbar-brand" %>
</div>
<div id="tournament-navbar-collapse" class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li><%= link_to " Home" , "/tournaments/#{@tournament.id}", class: "fas fa-home" %></li>
<% if can? :read, @tournament %>
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#"><i class="fas fa-poll-h"> Tournament Results/Brackets</i>
<a class="dropdown-toggle" data-toggle="dropdown" href="#"><i class="fas fa-poll-h"> Results/Brackets</i>
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><strong>Results</strong></li>
@@ -24,18 +32,19 @@
<% end %>
<% if can? :manage, @tournament %>
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#director"><i class="fas fa-tools"> Tournament Director Links</i>
<a class="dropdown-toggle" data-toggle="dropdown" href="#director"><i class="fas fa-tools"> Director Links</i>
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><strong>Pages</strong></li>
<li></span> <%= link_to "Edit Tournament Info", edit_tournament_path(@tournament) %></li>
<li><%= link_to "Weigh In Page" , "/tournaments/#{@tournament.id}/weigh_in" %></li>
<li><%= link_to "All Matches" , "/tournaments/#{@tournament.id}/matches" %></li>
<li><%= link_to "Full Screen Bout Board" , "/tournaments/#{@tournament.id}/up_matches?print=true" , target: :_blank %></li>
<li><%= link_to "Deduct Team Points" , "/tournaments/#{@tournament.id}/teampointadjust" %></li>
<li><%= link_to "All Matches" , "/tournaments/#{@tournament.id}/matches" %></li>
<li><%= link_to "Full Screen Bout Board" , "/tournaments/#{@tournament.id}/up_matches?print=true" , target: :_blank %></li>
<li><%= link_to "QR Code (Full Screen)" , "/tournaments/#{@tournament.id}/qrcode?print=true" , target: :_blank %></li>
<li><%= link_to "Deduct Team Points" , "/tournaments/#{@tournament.id}/teampointadjust" %></li>
<li><%= link_to "View All Mat Assignment Rules", tournament_mat_assignment_rules_path(@tournament) %></li>
<li><%= link_to 'Manage Backups', tournament_tournament_backups_path(@tournament) %></li>
<li><%= link_to "Reset Bout Board", reset_bout_board_tournament_path(@tournament), method: :post, data: { confirm: "Are you sure you want to reset the bout board?" } %></li>
<li><%= link_to "Reset Bout Board", reset_bout_board_tournament_path(@tournament), data: { turbo_method: :post, turbo_confirm: "Are you sure you want to reset the bout board?" } %></li>
<% if can? :destroy, @tournament %>
<li><%= link_to "Tournament Delegation" , "/tournaments/#{@tournament.id}/delegate" %></li>
<% end %>
@@ -47,13 +56,13 @@
<% end %>
<% end %>
<li><strong>Time Savers</strong></li>
<li><%= link_to "Create Boys High School Weights (106-285)" , "/tournaments/#{@tournament.id}/create_custom_weights?customValue=#{Weight::HS_WEIGHT_CLASSES}",data: { confirm: 'Are you sure? This will delete all current weights.' } %></li>
<li><%= link_to "Create Girls High School Weights (100-235)" , "/tournaments/#{@tournament.id}/create_custom_weights?customValue=#{Weight::HS_GIRLS_WEIGHT_CLASSES}",data: { confirm: 'Are you sure? This will delete all current weights.' } %></li>
<li><%= link_to "Create Boys Middle School Weights (80-245)" , "/tournaments/#{@tournament.id}/create_custom_weights?customValue=#{Weight::MS_WEIGHT_CLASSES}",data: { confirm: 'Are you sure? This will delete all current weights.' } %></li>
<li><%= link_to "Create Girls Middle School Weights (72-235)" , "/tournaments/#{@tournament.id}/create_custom_weights?customValue=#{Weight::MS_GIRLS_WEIGHT_CLASSES}",data: { confirm: 'Are you sure? This will delete all current weights.' } %></li>
<li><%= link_to "Create Boys High School Weights (106-285)" , "/tournaments/#{@tournament.id}/create_custom_weights?customValue=#{Weight::HS_WEIGHT_CLASSES}",data: { turbo_method: :get, turbo_confirm: 'Are you sure? This will delete all current weights.' } %></li>
<li><%= link_to "Create Girls High School Weights (100-235)" , "/tournaments/#{@tournament.id}/create_custom_weights?customValue=#{Weight::HS_GIRLS_WEIGHT_CLASSES}",data: { turbo_method: :get, turbo_confirm: 'Are you sure? This will delete all current weights.' } %></li>
<li><%= link_to "Create Boys Middle School Weights (80-245)" , "/tournaments/#{@tournament.id}/create_custom_weights?customValue=#{Weight::MS_WEIGHT_CLASSES}",data: { turbo_method: :get, turbo_confirm: 'Are you sure? This will delete all current weights.' } %></li>
<li><%= link_to "Create Girls Middle School Weights (72-235)" , "/tournaments/#{@tournament.id}/create_custom_weights?customValue=#{Weight::MS_GIRLS_WEIGHT_CLASSES}",data: { turbo_method: :get, turbo_confirm: 'Are you sure? This will delete all current weights.' } %></li>
<li><strong>Tournament Actions</strong></li>
<li><%= link_to "Calculate Team Scores" , "/tournaments/#{@tournament.id}/calculate_team_scores", :method => :put %></li>
<li><%= link_to "Generate Brackets" , "/tournaments/#{@tournament.id}/generate_matches", data: { confirm: 'Are you sure? This will delete all current matches.' } %></li>
<li><%= link_to "Calculate Team Scores" , "/tournaments/#{@tournament.id}/calculate_team_scores", data: { turbo_method: :put } %></li>
<li><%= link_to "Generate Brackets" , "/tournaments/#{@tournament.id}/generate_matches", data: { turbo_method: :get, turbo_confirm: 'Are you sure? This will delete all current matches.' } %></li>
<li><%= link_to "Export Data" , "/tournaments/#{@tournament.id}/export?print=true", target: :_blank %></li>
</ul>
<% end %>
@@ -61,4 +70,4 @@
</div>
</div>
</nav>
<% end %>
<% end %>

View File

@@ -14,21 +14,37 @@
<p>We've detected that you have an ad blocker enabled! Please consider disabling it for <strong>wrestlingdev.com</strong>. This site is free for users and supported by ads. Ad blockers also block performance monitoring that help us with user experience.</p>
</div>
<script>
let fakeAd = document.createElement("div");
fakeAd.className = "textads banner-ads banner_ads ad-unit ad-zone ad-space adsbox"
// Only run ad blocker check if fakeAd hasn't been checked yet in this context
if (typeof window.adBlockerCheckExecuted === 'undefined') {
let fakeAd = document.createElement("div");
fakeAd.className = "textads banner-ads banner_ads ad-unit ad-zone ad-space adsbox"
fakeAd.style.height = "1px"
fakeAd.style.position = "absolute"; // Prevent potential layout shift
fakeAd.style.top = "-10px";
fakeAd.style.left = "-10px";
fakeAd.style.height = "1px"
document.body.appendChild(fakeAd)
let x_width = fakeAd.offsetHeight;
let msg = document.getElementById("msg")
document.body.appendChild(fakeAd)
if(x_width){
console.log("No AdBlocker Detected")
}else{
console.log("AdBlocker detected")
document.getElementById("blocked_message").style.display = 'block';
}
// Use requestAnimationFrame to ensure the element is rendered before checking offsetHeight
requestAnimationFrame(() => {
let x_width = fakeAd.offsetHeight;
// let msg = document.getElementById("msg") // msg variable wasn't used
if(x_width){
console.log("No AdBlocker Detected")
}else{
console.log("AdBlocker detected")
const blockedMessage = document.getElementById("blocked_message");
if (blockedMessage) {
blockedMessage.style.display = 'block';
}
}
// Clean up the fake element
document.body.removeChild(fakeAd);
});
// Mark check as executed
window.adBlockerCheckExecuted = true;
}
</script>

View File

@@ -3,10 +3,10 @@
<% if params[:print] %>
<head>
<%= csrf_meta_tags %>
<%= action_cable_meta_tag %>
<title>WrestlingDev</title>
<%= stylesheet_link_tag "application", media: "all",
"data-turbolinks-track" => true %>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
<%= stylesheet_link_tag "application", media: "all", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
<%= render 'layouts/cdn' %>
<%= render 'layouts/shim' %>
</head>
@@ -16,27 +16,31 @@
<% else %>
<head>
<title>WrestlingDev</title>
<%= action_cable_meta_tag %>
<meta name="viewport" content="width=device-width, initial-scale=1">
<% if Rails.env.production? %>
<%= render 'layouts/analytics' %>
<% end %>
<%= stylesheet_link_tag "application", media: "all",
"data-turbolinks-track" => true %>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
<%= stylesheet_link_tag "application", media: "all", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
<%= csrf_meta_tags %>
<%= render 'layouts/cdn' %>
<%= render 'layouts/shim' %>
</head>
<body style="padding-top: 70px;">
<body style="padding-top: 100px;">
<%= render 'layouts/header' %>
<%= render 'layouts/tournament-navbar' %>
<div class="container">
<div class="navbar-roof"></div>
<%= render 'layouts/header' %>
<div id="page-content">
<div class="row">
<%= render 'layouts/tournament-navbar' %>
</div>
<div class="row">
<div class="col-md-12"><%= render 'layouts/underheader' %></div>
<div class="col-md-12">
<% unless hide_ads? %>
<%= render 'layouts/underheader' %>
<% end %>
</div>
</div>
<div class="row no-margin">
<div class="col-md-12" style="padding-left: 2%;">
@@ -46,7 +50,10 @@
<% if alert %>
<p id="alert" class="alert alert-danger alert-dismissible"><a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a><%= alert %></p>
<% end %>
<div id="view"><%= yield %></div>
<div id="view" style="overflow-x: auto; overflow-y: hidden;"> <%# Horizontal scroll only %>
<br><br>
<%= yield %>
</div>
</div>
</div>
</div>
@@ -55,4 +62,3 @@
</body>
<% end %>
</html>

View File

@@ -23,7 +23,7 @@
<td><%= Array(rule.rounds).join(", ") %></td>
<td>
<%= link_to '', edit_tournament_mat_assignment_rule_path(@tournament, rule), class: "fas fa-edit" %>
<%= link_to '', tournament_mat_assignment_rule_path(@tournament, rule), method: :delete, data: { confirm: "Are you sure?" }, class: "fas fa-trash-alt" %>
<%= link_to '', tournament_mat_assignment_rule_path(@tournament, rule), data: { turbo_method: :delete, turbo_confirm: "Are you sure?" }, class: "fas fa-trash-alt" %>
</td>
</tr>
<% end %>

View File

@@ -10,127 +10,146 @@
</ul>
</div>
<% end %>
<div data-controller="match-data"
data-match-data-tournament-id-value="<%= @match.tournament.id %>"
data-match-data-bout-number-value="<%= @match.bout_number %>"
data-match-data-match-id-value="<%= @match.id %>">
<div id="cable-status-indicator" data-match-data-target="statusIndicator" class="alert alert-secondary" style="padding: 5px; margin-bottom: 10px; border-radius: 4px;"></div>
<h4>Bout <strong><%= @match.bout_number %></strong></h4>
<% 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" %>
<% end %>
<h4>Bracket Position: <strong><%= @match.bracket_position %></strong></h4>
<table class="table">
<thead>
<tr>
<th>Name: <%= @wrestler1_name %> <select id="w1-color" onchange="changeW1Color(this)">
<option value="green">Green</option>
<option value="red">Red</option>
</select>
<br>School: <%= @wrestler1_school_name %>
<br>Last Match: <%= @wrestler1_last_match && @wrestler1_last_match.finished_at ? time_ago_in_words(@wrestler1_last_match.finished_at) : "N/A" %></th>
<th>Name: <%= @wrestler2_name %> <select id="w2-color" onchange="changeW2Color(this)">
<option value="red">Red</option>
<option value="green">Green</option>
</select>
<br>School: <%= @wrestler2_school_name %>
<br>Last Match: <%= @wrestler2_last_match && @wrestler2_last_match.finished_at ? time_ago_in_words(@wrestler2_last_match.finished_at) : "N/A" %></th>
</tr>
</thead>
<tbody>
<tr>
<td><%= @wrestler1_name %> Stats: <br><%= f.text_area :w1_stat, cols: "30", rows: "10" %></td>
<td><%= @wrestler2_name %> Stats: <br><%= f.text_area :w2_stat, cols: "30", rows: "10" %></td>
</tr>
<tr>
<td><%= @wrestler1_name %> Scoring <br><button id="w1-takedown" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'T3');updateStats(w2,'__');">T3</button>
<button id="w1-escape" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'E1');updateStats(w2,'__');">E1</button>
<button id="w1-reversal" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'R2');updateStats(w2,'__');">R2</button>
<button id="w1-nf2" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'N2');updateStats(w2,'__');">N2 </button>
<button id="w1-nf3" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'N3');updateStats(w2,'__');">N3</button>
<button id="w1-nf4" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'N4');updateStats(w2,'__');">N4</button>
<button id="w1-nf5" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'N5');updateStats(w2,'__');">N5</button>
<button id="w1-penalty" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'P1');updateStats(w2,'__');">P1</button>
<button id="w1-penalty2" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'P2');updateStats(w2,'__');">P2</button></td>
<td><%= @wrestler2_name %> Scoring <br><button id="w2-takedown" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'T3');updateStats(w1,'__');">T3</button>
<button id="w2-escape" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'E1');updateStats(w1,'__');">E1</button>
<button id="w2-reversal" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'R2');updateStats(w1,'__');">R2</button>
<button id="w2-nf2" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'N2');updateStats(w1,'__');">N2</button>
<button id="w2-nf3" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'N3');updateStats(w1,'__');">N3</button>
<button id="w2-nf4" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'N4');updateStats(w1,'__');">N4</button>
<button id="w2-nf5" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'N5');updateStats(w1,'__');">N5</button>
<button id="w2-penalty" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'P1');updateStats(w1,'__');">P1</button>
<button id="w2-penalty2" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'P2');updateStats(w1,'__');">P2</button></td>
</tr>
<tr>
<td><%= @wrestler1_name %> Choice <br><button id="w1-top" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'|Chose Top|')">Chose Top</button>
<button id="w1-bottom" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'|Chose Bottom|')">Chose Bottom</button>
<button id="w1-neutral" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'|Chose Neutral|')">Chose Neutral</button>
<button id="w1-defer" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'|Deferred|')">Deferred</button></td>
<td><%= @wrestler2_name %> Choice <br><button id="w2-top" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'|Chose Top|')">Chose Top</button>
<button id="w2-bottom" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'|Chose Bottom|')">Chose Bottom</button>
<button id="w2-neutral" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'|Chose Neutral|')">Chose Neutral</button>
<button id="w2-defer" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'|Deferred|')">Deferred</button></td>
</tr>
<tr>
<td><%= @wrestler1_name %> Warnings <br><button id="w1-stalling" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'S');updateStats(w2,'_');">Stalling</button>
<button id="w1-caution" type="button" class="btn btn-success btn-sm" onclick="updateStats(w1,'C');updateStats(w2,'_');">Caution</button></td>
<td><%= @wrestler2_name %> Warnings <br><button id="w2-stalling" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'S');updateStats(w1,'_');">Stalling</button>
<button id="w2-caution" type="button" class="btn btn-danger btn-sm" onclick="updateStats(w2,'C');updateStats(w1,'_');">Caution</button></td>
</tr>
<tr>
<td>Match Options <br><button type="button" class="btn btn-primary btn-sm" onclick="updateStats(w2,'|End Period|'); updateStats(w1,'|End Period|');">End Period</button></td>
<td></td>
</tr>
<tr>
<td>
<h5><%= @wrestler1_name %> Timer Controls</h5>
Injury Time (90 second max): <span id="w1-injury-time">0 sec</span>
<button type="button" onclick="startTimer(w1, 'injury')" class="btn btn-primary btn-sm">Start</button>
<button type="button" onclick="stopTimer(w1, 'injury')" class="btn btn-primary btn-sm">Stop</button>
<button type="button" onclick="resetTimer(w1, 'injury')" class="btn btn-primary btn-sm">Reset</button>
<br><br>
Blood Time (600 second max): <span id="w1-blood-time">0 sec</span>
<button type="button" onclick="startTimer(w1, 'blood')" class="btn btn-primary btn-sm">Start</button>
<button type="button" onclick="stopTimer(w1, 'blood')" class="btn btn-primary btn-sm">Stop</button>
<button type="button" onclick="resetTimer(w1, 'blood')" class="btn btn-primary btn-sm">Reset</button>
</td>
<td>
<h5><%= @wrestler2_name %> Timer Controls</h5>
Injury Time (90 second max): <span id="w2-injury-time">0 sec</span>
<button type="button" onclick="startTimer(w2, 'injury')" class="btn btn-primary btn-sm">Start</button>
<button type="button" onclick="stopTimer(w2, 'injury')" class="btn btn-primary btn-sm">Stop</button>
<button type="button" onclick="resetTimer(w2, 'injury')" class="btn btn-primary btn-sm">Reset</button>
<br><br>
Blood Time (600 second max): <span id="w2-blood-time">0 sec</span>
<button type="button" onclick="startTimer(w2, 'blood')" class="btn btn-primary btn-sm">Start</button>
<button type="button" onclick="stopTimer(w2, 'blood')" class="btn btn-primary btn-sm">Stop</button>
<button type="button" onclick="resetTimer(w2, 'blood')" class="btn btn-primary btn-sm">Reset</button>
</td>
</tr>
</tbody>
</table>
<br>
<br>
<br>
<h4>Match Results</h4>
<br>
<div class="field">
<%= f.label "Win Type" %><br>
<%= f.select(:win_type, Match::WIN_TYPES) %>
</div>
<div data-controller="wrestler-color">
<table class="table">
<thead>
<tr>
<th>Name: <%= @wrestler1_name %> <select id="w1-color" data-wrestler-color-target="w1ColorSelect" data-action="change->wrestler-color#changeW1Color">
<option value="green">Green</option>
<option value="red">Red</option>
</select>
<br>School: <%= @wrestler1_school_name %>
<br>Last Match: <%= @wrestler1_last_match && @wrestler1_last_match.finished_at ? time_ago_in_words(@wrestler1_last_match.finished_at) : "N/A" %></th>
<th>Name: <%= @wrestler2_name %> <select id="w2-color" data-wrestler-color-target="w2ColorSelect" data-action="change->wrestler-color#changeW2Color">
<option value="red">Red</option>
<option value="green">Green</option>
</select>
<br>School: <%= @wrestler2_school_name %>
<br>Last Match: <%= @wrestler2_last_match && @wrestler2_last_match.finished_at ? time_ago_in_words(@wrestler2_last_match.finished_at) : "N/A" %></th>
</tr>
</thead>
<tbody>
<tr>
<td><%= @wrestler1_name %> Stats: <br><%= f.text_area :w1_stat, cols: "30", rows: "10", data: { match_data_target: "w1Stat" } %></td>
<td><%= @wrestler2_name %> Stats: <br><%= f.text_area :w2_stat, cols: "30", rows: "10", data: { match_data_target: "w2Stat" } %></td>
</tr>
<tr>
<td><%= @wrestler1_name %> Scoring <br><button id="w1-takedown" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Takedown" data-action="click->match-data#updateW1Stats" data-match-data-text="T3">T3</button>
<button id="w1-escape" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Escape" data-action="click->match-data#updateW1Stats" data-match-data-text="E1">E1</button>
<button id="w1-reversal" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Reversal" data-action="click->match-data#updateW1Stats" data-match-data-text="R2">R2</button>
<button id="w1-nf2" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Nf2" data-action="click->match-data#updateW1Stats" data-match-data-text="N2">N2 </button>
<button id="w1-nf3" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Nf3" data-action="click->match-data#updateW1Stats" data-match-data-text="N3">N3</button>
<button id="w1-nf4" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Nf4" data-action="click->match-data#updateW1Stats" data-match-data-text="N4">N4</button>
<button id="w1-nf5" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Nf5" data-action="click->match-data#updateW1Stats" data-match-data-text="N5">N5</button>
<button id="w1-penalty" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Penalty" data-action="click->match-data#updateW1Stats" data-match-data-text="P1">P1</button>
<button id="w1-penalty2" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Penalty2" data-action="click->match-data#updateW1Stats" data-match-data-text="P2">P2</button></td>
<td><%= @wrestler2_name %> Scoring <br><button id="w2-takedown" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Takedown" data-action="click->match-data#updateW2Stats" data-match-data-text="T3">T3</button>
<button id="w2-escape" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Escape" data-action="click->match-data#updateW2Stats" data-match-data-text="E1">E1</button>
<button id="w2-reversal" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Reversal" data-action="click->match-data#updateW2Stats" data-match-data-text="R2">R2</button>
<button id="w2-nf2" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Nf2" data-action="click->match-data#updateW2Stats" data-match-data-text="N2">N2</button>
<button id="w2-nf3" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Nf3" data-action="click->match-data#updateW2Stats" data-match-data-text="N3">N3</button>
<button id="w2-nf4" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Nf4" data-action="click->match-data#updateW2Stats" data-match-data-text="N4">N4</button>
<button id="w2-nf5" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Nf5" data-action="click->match-data#updateW2Stats" data-match-data-text="N5">N5</button>
<button id="w2-penalty" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Penalty" data-action="click->match-data#updateW2Stats" data-match-data-text="P1">P1</button>
<button id="w2-penalty2" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Penalty2" data-action="click->match-data#updateW2Stats" data-match-data-text="P2">P2</button></td>
</tr>
<tr>
<td><%= @wrestler1_name %> Choice <br><button id="w1-top" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Top" data-action="click->match-data#updateW1Stats" data-match-data-text="|Chose Top|">Chose Top</button>
<button id="w1-bottom" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Bottom" data-action="click->match-data#updateW1Stats" data-match-data-text="|Chose Bottom|">Chose Bottom</button>
<button id="w1-neutral" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Neutral" data-action="click->match-data#updateW1Stats" data-match-data-text="|Chose Neutral|">Chose Neutral</button>
<button id="w1-defer" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Defer" data-action="click->match-data#updateW1Stats" data-match-data-text="|Deferred|">Deferred</button></td>
<td><%= @wrestler2_name %> Choice <br><button id="w2-top" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Top" data-action="click->match-data#updateW2Stats" data-match-data-text="|Chose Top|">Chose Top</button>
<button id="w2-bottom" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Bottom" data-action="click->match-data#updateW2Stats" data-match-data-text="|Chose Bottom|">Chose Bottom</button>
<button id="w2-neutral" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Neutral" data-action="click->match-data#updateW2Stats" data-match-data-text="|Chose Neutral|">Chose Neutral</button>
<button id="w2-defer" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Defer" data-action="click->match-data#updateW2Stats" data-match-data-text="|Deferred|">Deferred</button></td>
</tr>
<tr>
<td><%= @wrestler1_name %> Warnings <br><button id="w1-stalling" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Stalling" data-action="click->match-data#updateW1Stats" data-match-data-text="S">Stalling</button>
<button id="w1-caution" type="button" class="btn btn-success btn-sm" data-wrestler-color-target="w1Caution" data-action="click->match-data#updateW1Stats" data-match-data-text="C">Caution</button></td>
<td><%= @wrestler2_name %> Warnings <br><button id="w2-stalling" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Stalling" data-action="click->match-data#updateW2Stats" data-match-data-text="S">Stalling</button>
<button id="w2-caution" type="button" class="btn btn-danger btn-sm" data-wrestler-color-target="w2Caution" data-action="click->match-data#updateW2Stats" data-match-data-text="C">Caution</button></td>
</tr>
<tr>
<td>Match Options <br><button type="button" class="btn btn-primary btn-sm" data-action="click->match-data#endPeriod">End Period</button></td>
<td></td>
</tr>
<tr>
<td>
<h5><%= @wrestler1_name %> Timer Controls</h5>
Injury Time (90 second max): <span id="w1-injury-time">0 sec</span>
<button type="button" data-action="click->match-data#startW1InjuryTimer" class="btn btn-primary btn-sm">Start</button>
<button type="button" data-action="click->match-data#stopW1InjuryTimer" class="btn btn-primary btn-sm">Stop</button>
<button type="button" data-action="click->match-data#resetW1InjuryTimer" class="btn btn-primary btn-sm">Reset</button>
<br><br>
Blood Time (600 second max): <span id="w1-blood-time">0 sec</span>
<button type="button" data-action="click->match-data#startW1BloodTimer" class="btn btn-primary btn-sm">Start</button>
<button type="button" data-action="click->match-data#stopW1BloodTimer" class="btn btn-primary btn-sm">Stop</button>
<button type="button" data-action="click->match-data#resetW1BloodTimer" class="btn btn-primary btn-sm">Reset</button>
</td>
<td>
<h5><%= @wrestler2_name %> Timer Controls</h5>
Injury Time (90 second max): <span id="w2-injury-time">0 sec</span>
<button type="button" data-action="click->match-data#startW2InjuryTimer" class="btn btn-primary btn-sm">Start</button>
<button type="button" data-action="click->match-data#stopW2InjuryTimer" class="btn btn-primary btn-sm">Stop</button>
<button type="button" data-action="click->match-data#resetW2InjuryTimer" class="btn btn-primary btn-sm">Reset</button>
<br><br>
Blood Time (600 second max): <span id="w2-blood-time">0 sec</span>
<button type="button" data-action="click->match-data#startW2BloodTimer" class="btn btn-primary btn-sm">Start</button>
<button type="button" data-action="click->match-data#stopW2BloodTimer" class="btn btn-primary btn-sm">Stop</button>
<button type="button" data-action="click->match-data#resetW2BloodTimer" class="btn btn-primary btn-sm">Reset</button>
</td>
</tr>
</tbody>
</table>
</div><!-- End of wrestler-color controller div -->
<br>
<div class="field">
<%= f.label "Overtime Type" %> Leave blank if not overtime. For High School the 1st overtime is SV-1, second overtime is TB-1, third overtime is UTB.<br>
<%= f.select(:overtime_type, Match::OVERTIME_TYPES) %>
</div>
<br>
<div class="field">
<%= f.label "Winner" %> Please choose the winner<br>
<%= f.collection_select :winner_id, @wrestlers, :id, :name_with_school, include_blank: true %>
</div>
<br>
<% if @match.finished && @match.finished == 1 %>
<h4>Match Results</h4>
<br>
<div data-controller="match-score">
<div class="field">
<%= f.label "Final Score" %> For decision, major, or tech fall put the score here in Number-Number format. If pin, put the accumulated pin time in the format MM:SS. If default, injury default, dq, bye, or forfeit, leave blank. Examples: 7-2, 17-2, 0:30, or 2:34.<br>
<%= f.text_field :score %>
<%= f.label "Win type" %><br>
<%= f.select :win_type, Match::WIN_TYPES, { include_blank: false }, {
data: {
match_score_target: "winType",
action: "change->match-score#winTypeChanged"
}
} %>
</div>
<% else %>
<br>
<div class="field">
<%= f.label "Overtime Type" %> Leave blank if not overtime. For High School the 1st overtime is SV-1, second overtime is TB-1, third overtime is UTB.<br>
<%= f.select(:overtime_type, Match::OVERTIME_TYPES) %>
</div>
<br>
<div class="field">
<%= f.label "Winner" %> Please choose the winner<br>
<%= f.collection_select :winner_id, @wrestlers, :id, :name_with_school,
{ include_blank: true },
{
data: {
match_score_target: "winnerSelect",
action: "change->match-score#winnerChanged"
}
}
%>
</div>
<br>
<div class="field">
<%= f.label "Final Score" %>
<br>
@@ -138,196 +157,27 @@
The input will adjust based on the selected win type.
</span>
<br>
<div id="dynamic-score-input"></div>
<p id="pin-time-tip" class="text-muted mt-2" style="display: none;">
<div id="dynamic-score-input" data-match-score-target="dynamicScoreInput"></div>
<p id="pin-time-tip" class="text-muted mt-2" style="display: none;" data-match-score-target="pinTimeTip">
<strong>Tip:</strong> Pin time is an accumulation over the match, not how much time was left in the current period.
<br>For example, if all 3 periods are 2 minutes and a pin happened with 1:27 left in the second period,
the pin time would be <strong>2:33</strong> (2 minutes for the first period + 33 seconds elapsed in the second period).
</p>
<div id="validation-alerts" class="text-danger mt-2"></div>
<%= f.hidden_field :score, id: "final-score-field" %>
<div id="validation-alerts" class="text-danger mt-2" data-match-score-target="validationAlerts"></div>
<%= f.hidden_field :score, id: "final-score-field", data: { match_score_target: "finalScoreField" } %>
<br>
<%= f.submit "Update Match", id: "update-match-btn",
data: {
match_score_target: "submitButton",
action: "click->match-score#confirmWinner"
},
class: "btn btn-success" %>
</div>
<%= render 'matches/matchstats_variable_score_input' %>
<% end %>
<br>
</div><!-- End of match-score controller -->
</div><!-- End of match-data controller div -->
<br>
<%= f.hidden_field :finished, :value => 1 %>
<%= f.hidden_field :round, :value => @match.round %>
<br>
<div class="actions">
<%= f.submit "Update Match", id: "update-match-btn", onclick: "return confirm('Is the name of the winner ' + document.getElementById('match_winner_id').options[document.getElementById('match_winner_id').selectedIndex].text + '?')", class: "btn btn-success" %>
</div>
<% end %>
<%= render 'matches/matchstats_color_change' %>
<script>
// Get variables
var tournament = <%= @match.tournament.id %>;
var bout = <%= @match.bout_number %>;
var match_finsihed = "<%= @match.finished %>";
// ############### STATS
// Create person object
function Person(stats, name) {
this.name = name;
this.stats = stats;
this.updated_at = null; // Track last updated timestamp
this.timers = {
"injury": { time: 0, startTime: null, interval: null },
"blood": { time: 0, startTime: null, interval: null },
};
}
// Declare variables
var w1 = new Person("", "w1");
var w2 = new Person("", "w2");
updateJsValues();
// Generate unique localStorage key
function generateKey(wrestler_name) {
return `${wrestler_name}-${tournament}-${bout}`;
}
// Load values from localStorage
function loadFromLocalStorage(wrestler_name) {
const key = generateKey(wrestler_name);
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
}
// Save values to localStorage
function saveToLocalStorage(person) {
const key = generateKey(person.name);
const data = {
stats: person.stats,
updated_at: person.updated_at,
timers: person.timers, // Save all timers
};
localStorage.setItem(key, JSON.stringify(data));
}
// Update HTML values
function updateHtmlValues() {
document.getElementById("match_w1_stat").value = w1.stats;
document.getElementById("match_w2_stat").value = w2.stats;
}
// Update JS object values from HTML
function updateJsValues() {
w1.stats = document.getElementById("match_w1_stat").value;
w2.stats = document.getElementById("match_w2_stat").value;
}
// Update stats and persist to localStorage
function updateStats(wrestler, text) {
updateJsValues();
wrestler.stats += text + " ";
wrestler.updated_at = new Date().toISOString(); // Update timestamp
updateHtmlValues();
// Save to localStorage
if (wrestler === w1) {
saveToLocalStorage(w1);
} else if (wrestler === w2) {
saveToLocalStorage(w2);
}
}
// Initialize data on page load
function initializeTimers(wrestler) {
// Iterate over each timer in the wrestler object
Object.keys(wrestler.timers).forEach((timerKey) => {
const savedData = loadFromLocalStorage(wrestler.name);
if (savedData && savedData.timers && savedData.timers[timerKey]) {
wrestler.timers[timerKey].time = savedData.timers[timerKey].time || 0;
updateTimerDisplay(wrestler, timerKey, wrestler.timers[timerKey].time);
}
});
}
function initialize() {
const localW1 = loadFromLocalStorage("w1");
const localW2 = loadFromLocalStorage("w2");
if (localW1) {
w1.stats = localW1.stats || "";
w1.updated_at = localW1.updated_at || null;
w1.timers = localW1.timers || w1.timers; // Load timer data
// set localStorage values to html
updateHtmlValues();
}
if (localW2) {
w2.stats = localW2.stats || "";
w2.updated_at = localW2.updated_at || null;
w2.timers = localW2.timers || w2.timers; // Load timer data
// set localStorage values to html
updateHtmlValues();
}
initializeTimers(w1);
initializeTimers(w2);
updateJsValues()
}
document.addEventListener("DOMContentLoaded", function () {
initialize();
});
// ############### Blood and Injury time timers
// Timer storage and interval references
// Start a timer for a wrestler
function startTimer(wrestler, timerKey) {
const timer = wrestler.timers[timerKey];
if (timer.interval) return; // Prevent multiple intervals
timer.startTime = Date.now(); // Record the start time
timer.interval = setInterval(() => {
const elapsedSeconds = Math.floor((Date.now() - timer.startTime) / 1000);
updateTimerDisplay(wrestler, timerKey, timer.time + elapsedSeconds); // Show total time
}, 1000);
}
// Stop a timer for a wrestler
function stopTimer(wrestler, timerKey) {
const timer = wrestler.timers[timerKey];
if (!timer.interval || !timer.startTime) return; // Timer not running
clearInterval(timer.interval);
const elapsedSeconds = Math.floor((Date.now() - timer.startTime) / 1000); // Calculate elapsed time
timer.time += elapsedSeconds; // Add elapsed time to total
timer.interval = null;
timer.startTime = null;
saveToLocalStorage(wrestler); // Save wrestler data
updateTimerDisplay(wrestler, timerKey, timer.time); // Update final display
updateStatsBox(wrestler, timerKey, elapsedSeconds); // Update wrestler stats
}
// Reset a timer for a wrestler
function resetTimer(wrestler, timerKey) {
const timer = wrestler.timers[timerKey];
stopTimer(wrestler, timerKey); // Stop if running
timer.time = 0; // Reset time
updateTimerDisplay(wrestler, timerKey, 0); // Update display
saveToLocalStorage(wrestler); // Save wrestler data
}
// Update the timer display
function updateTimerDisplay(wrestler, timerKey, totalTime) {
const elementId = `${wrestler.name}-${timerKey}-time`; // Construct element ID
const element = document.getElementById(elementId);
if (element) {
element.innerText = `${Math.floor(totalTime / 60)}m ${totalTime % 60}s`;
}
}
// Update wrestler stats box with elapsed timer information
function updateStatsBox(wrestler, timerKey, elapsedSeconds) {
const timerType = timerKey.includes("injury") ? "Injury Time" : "Blood Time";
const formattedTime = `${Math.floor(elapsedSeconds / 60)}m ${elapsedSeconds % 60}s`;
updateStats(wrestler, `${timerType}: ${formattedTime}`);
}
</script>
<% end %><!-- End of form_for -->

View File

@@ -1,108 +0,0 @@
<script>
// ############### Button color change red/green
function changeW1Color(color){
if (color.value == "red") {
w1Red();
w2Green();
document.getElementById("w2-color").value = "green";
}
if (color.value == "green") {
w1Green();
w2Red();
document.getElementById("w2-color").value = "red";
}
}
function changeW2Color(color){
if (color.value == "red") {
w2Red();
w1Green();
document.getElementById("w1-color").value = "green";
}
if (color.value == "green") {
w2Green();
w1Red();
document.getElementById("w1-color").value = "red";
}
}
function redColor(id){
document.getElementById(id).className = "btn btn-danger btn-sm";
}
function greenColor(id){
document.getElementById(id).className = "btn btn-success btn-sm";
}
function w1Red(){
redColor("w1-takedown");
redColor("w1-escape");
redColor("w1-reversal");
redColor("w1-penalty");
redColor("w1-penalty2");
redColor("w1-nf5");
redColor("w1-nf4");
redColor("w1-nf3");
redColor("w1-nf2");
redColor("w1-top");
redColor("w1-bottom");
redColor("w1-neutral");
redColor("w1-defer");
redColor("w1-stalling");
redColor("w1-caution");
}
function w1Green(){
greenColor("w1-takedown");
greenColor("w1-escape");
greenColor("w1-reversal");
greenColor("w1-penalty");
greenColor("w1-penalty2");
greenColor("w1-nf5");
greenColor("w1-nf4");
greenColor("w1-nf3");
greenColor("w1-nf2");
greenColor("w1-top");
greenColor("w1-bottom");
greenColor("w1-neutral");
greenColor("w1-defer");
greenColor("w1-stalling");
greenColor("w1-caution");
}
function w2Red(){
redColor("w2-takedown");
redColor("w2-escape");
redColor("w2-reversal");
redColor("w2-penalty");
redColor("w2-penalty2");
redColor("w2-nf5");
redColor("w2-nf4");
redColor("w2-nf3");
redColor("w2-nf2");
redColor("w2-top");
redColor("w2-bottom");
redColor("w2-neutral");
redColor("w2-defer");
redColor("w2-stalling");
redColor("w2-caution");
}
function w2Green(){
greenColor("w2-takedown");
greenColor("w2-escape");
greenColor("w2-reversal");
greenColor("w2-penalty");
greenColor("w2-penalty2");
greenColor("w2-nf5");
greenColor("w2-nf4");
greenColor("w2-nf3");
greenColor("w2-nf2");
greenColor("w2-top");
greenColor("w2-bottom");
greenColor("w2-neutral");
greenColor("w2-defer");
greenColor("w2-stalling");
greenColor("w2-caution");
}
</script>

View File

@@ -1,229 +0,0 @@
<script>
// ############### Score field changer and form validation
document.addEventListener("DOMContentLoaded", () => {
const winTypeSelect = document.getElementById("match_win_type");
const winnerSelect = document.getElementById("match_winner_id");
const submitButton = document.getElementById("update-match-btn");
const dynamicScoreInput = document.getElementById("dynamic-score-input");
const finalScoreField = document.getElementById("final-score-field");
const validationAlerts = document.getElementById("validation-alerts");
const pinTimeTip = document.getElementById("pin-time-tip");
// Variables to persist scores across win type changes
let storedScores = {
winnerScore: "0",
loserScore: "0",
};
function updateScoreInput() {
const winType = winTypeSelect.value;
if (winType === "Pin") {
// Clear existing validation state and stored scores
dynamicScoreInput.innerHTML = "";
pinTimeTip.style.display = "block";
const minuteInput = createTextInput("minutes", "Minutes (MM)", "Pin Time Minutes");
const secondInput = createTextInput("seconds", "Seconds (SS)", "Pin Time Seconds");
dynamicScoreInput.appendChild(minuteInput);
dynamicScoreInput.appendChild(secondInput);
const updateFinalScore = () => {
// Ensure inputs are defined and have valid values
const minutes = (minuteInput.value || "0").padStart(2, "0");
const seconds = (secondInput.value || "0").padStart(2, "0");
finalScoreField.value = `${minutes}:${seconds}`;
validateForm();
};
[minuteInput, secondInput].forEach((input) => {
input.addEventListener("input", updateFinalScore);
});
// Safely initialize the final score
updateFinalScore(); // Set initial value
validateForm(); // Trigger validation
return;
}
if (
winType === "Decision" ||
winType === "Major" ||
winType === "Tech Fall"
) {
if (
dynamicScoreInput.querySelector("#winner-score") &&
dynamicScoreInput.querySelector("#loser-score")
) {
validateForm(); // Trigger validation
return;
}
// Clear existing form and create Score inputs
dynamicScoreInput.innerHTML = "";
pinTimeTip.style.display = "none";
const winnerScoreInput = createTextInput(
"winner-score",
"Winner's Score",
"Enter the winner's score"
);
const loserScoreInput = createTextInput(
"loser-score",
"Loser's Score",
"Enter the loser's score"
);
dynamicScoreInput.appendChild(winnerScoreInput);
dynamicScoreInput.appendChild(loserScoreInput);
// Restore stored values
winnerScoreInput.value = storedScores.winnerScore;
loserScoreInput.value = storedScores.loserScore;
const updateFinalScore = () => {
const winnerScore = winnerScoreInput.value || "0";
const loserScore = loserScoreInput.value || "0";
finalScoreField.value = `${winnerScore}-${loserScore}`;
validateForm();
};
[winnerScoreInput, loserScoreInput].forEach((input) => {
input.addEventListener("input", (event) => {
storedScores[event.target.id === "winner-score" ? "winnerScore" : "loserScore"] =
event.target.value || "0";
updateFinalScore();
});
});
updateFinalScore(); // Set initial value
validateForm(); // Trigger validation
return;
}
// For other types, clear everything
dynamicScoreInput.innerHTML = "";
pinTimeTip.style.display = "none";
finalScoreField.value = ""; // Clear the final score for other win types
validateForm(); // Trigger validation
}
function validateForm() {
const winType = winTypeSelect.value;
const winner = winnerSelect.value;
let isValid = true;
let alertMessage = "";
let winTypeShouldBe = "Decision";
if (winType === "Decision" || winType === "Major" || winType === "Tech Fall") {
const winnerScoreInput = document.getElementById("winner-score");
const loserScoreInput = document.getElementById("loser-score");
if (!winnerScoreInput || !loserScoreInput) return;
const winnerScore = parseInt(winnerScoreInput.value || "0", 10);
const loserScore = parseInt(loserScoreInput.value || "0", 10);
const scoreDifference = winnerScore - loserScore;
if (winnerScore <= loserScore) {
isValid = false;
alertMessage += "Winner's score must be higher than loser's score.<br>";
}
if (scoreDifference < 8) {
winTypeShouldBe = "Decision";
} else if (scoreDifference >= 8 && scoreDifference < 15) {
winTypeShouldBe = "Major";
} else if (scoreDifference >= 15) {
winTypeShouldBe = "Tech Fall";
}
if (winTypeShouldBe !== winType) {
isValid = false;
alertMessage += `
Win type should be <strong>${winTypeShouldBe}</strong>.
Decisions are wins with a score difference less than 8.
Majors are wins with a score difference between 8 and 14.
Tech Falls are wins with a score difference of 15 or more.<br>
`;
}
}
if (!winner) {
isValid = false;
alertMessage += "Please select a winner.<br>";
}
if (!isValid) {
validationAlerts.innerHTML = alertMessage;
validationAlerts.style.display = "block";
} else {
validationAlerts.innerHTML = ""; // Clear alerts
validationAlerts.style.display = "none";
}
submitButton.disabled = !isValid;
}
document.querySelector("form").addEventListener("submit", (event) => {
const winType = winTypeSelect.value;
if (winType === "Pin") {
const minuteInput = document.getElementById("minutes");
const secondInput = document.getElementById("seconds");
if (minuteInput && secondInput) {
const minutes = minuteInput.value.padStart(2, "0");
const seconds = secondInput.value.padStart(2, "0");
finalScoreField.value = `${minutes}:${seconds}`;
} else {
finalScoreField.value = ""; // Clear if no inputs
}
} else if (
winType === "Decision" ||
winType === "Major" ||
winType === "Tech Fall"
) {
const winnerScoreInput = document.getElementById("winner-score");
const loserScoreInput = document.getElementById("loser-score");
if (winnerScoreInput && loserScoreInput) {
const winnerScore = winnerScoreInput.value || "0";
const loserScore = loserScoreInput.value || "0";
finalScoreField.value = `${winnerScore}-${loserScore}`;
} else {
finalScoreField.value = ""; // Clear if no inputs
}
} else {
finalScoreField.value = ""; // Reset for other win types
}
});
winTypeSelect.addEventListener("change", updateScoreInput);
winnerSelect.addEventListener("change", validateForm);
updateScoreInput();
validateForm();
});
// Helper function to create text inputs
function createTextInput(id, placeholder, label) {
const container = document.createElement("div");
container.classList.add("form-group");
const inputLabel = document.createElement("label");
inputLabel.innerText = label;
const input = document.createElement("input");
input.type = "text";
input.id = id;
input.placeholder = placeholder;
input.classList.add("form-control", "form-control-sm");
container.appendChild(inputLabel);
container.appendChild(input);
return container;
}
</script>

View File

@@ -0,0 +1,34 @@
<h1>Assign Mat/Queue for Match <%= @match.bout_number %></h1>
<% if @current_mat %>
<p>Current Assignment: Mat <%= @current_mat.name %><%= @current_queue_position ? " (Queue #{@current_queue_position})" : "" %></p>
<% else %>
<p>Current Assignment: Unassigned</p>
<% end %>
<%= form_with model: @match, url: update_assignment_match_path(@match), method: :patch do |f| %>
<div class="field">
<%= f.label :mat_id, "Mat" %><br>
<%= f.collection_select :mat_id, @mats, :id, :name, { include_blank: "Unassigned" } %>
</div>
<br>
<div class="field">
<%= f.label :queue_position, "Queue Position" %><br>
<%= f.select :queue_position,
options_for_select(
[
["On Mat (Queue 1)", 1],
["On Deck (Queue 2)", 2],
["In The Hole (Queue 3)", 3],
["Warm Up (Queue 4)", 4]
],
@current_queue_position
),
include_blank: "Select position"
%>
</div>
<br>
<div class="actions">
<%= f.submit "Update Assignment", class: "btn btn-success" %>
</div>
<% end %>

View File

@@ -0,0 +1,86 @@
<h1>Spectating Match: Bout <%= @match.bout_number %></h1>
<h2><%= @match.weight.max %> lbs</h2>
<h3><%= @tournament.name %></h3>
<div data-controller="match-spectate"
data-match-spectate-match-id-value="<%= @match.id %>">
<div id="cable-status-indicator"
data-match-spectate-target="statusIndicator"
class="alert alert-secondary"
style="padding: 5px; margin-bottom: 10px; border-radius: 4px;"></div>
<div class="match-details">
<div class="wrestler-info wrestler1">
<h4><%= @wrestler1_name %> (<%= @wrestler1_school_name %>)</h4>
<div class="stats">
<strong>Stats:</strong>
<pre data-match-spectate-target="w1Stats"><%= @match.w1_stat %></pre>
</div>
</div>
<div class="wrestler-info wrestler2">
<h4><%= @wrestler2_name %> (<%= @wrestler2_school_name %>)</h4>
<div class="stats">
<strong>Stats:</strong>
<pre data-match-spectate-target="w2Stats"><%= @match.w2_stat %></pre>
</div>
</div>
<div class="match-result">
<h4>Result</h4>
<p><strong>Winner:</strong> <span data-match-spectate-target="winner"><%= @match.winner_id ? @match.winner.name : '-' %></span></p>
<p><strong>Win Type:</strong> <span data-match-spectate-target="winType"><%= @match.win_type || '-' %></span></p>
<p><strong>Score:</strong> <span data-match-spectate-target="score"><%= @match.score || '-' %></span></p>
<p><strong>Finished:</strong> <span data-match-spectate-target="finished"><%= @match.finished ? 'Yes' : 'No' %></span></p>
</div>
</div>
</div>
<style>
.match-details {
display: flex;
justify-content: space-around;
margin-top: 20px;
}
.wrestler-info {
border: 1px solid #ccc;
padding: 15px;
width: 40%;
}
.wrestler-info pre {
background-color: #f8f8f8;
border: 1px solid #eee;
padding: 10px;
white-space: pre-wrap; /* Allow text wrapping */
word-wrap: break-word; /* Break long words */
max-height: 300px; /* Optional: limit height */
overflow-y: auto; /* Optional: add scroll if needed */
}
.match-result {
border: 1px solid #ccc;
padding: 15px;
width: 15%;
}
.match-result span {
font-weight: normal;
}
/* REMOVE Status Indicator Background Styles
#cable-status-indicator {
transition: background-color 0.5s ease, color 0.5s ease;
}
#cable-status-indicator.status-connecting {
background-color: #ffc107;
color: #333;
}
#cable-status-indicator.status-connected {
background-color: #28a745;
color: white;
}
#cable-status-indicator.status-disconnected {
background-color: #dc3545;
color: white;
}
*/
</style>

View File

@@ -0,0 +1,37 @@
<% @mat = mat %>
<% @match = local_assigns[:match] || mat.queue1_match %>
<% @next_match = local_assigns[:next_match] || mat.queue2_match %>
<% @show_next_bout_button = local_assigns.key?(:show_next_bout_button) ? local_assigns[:show_next_bout_button] : true %>
<% @wrestlers = [] %>
<% if @match %>
<% if @match.w1 %>
<% @wrestler1_name = @match.wrestler1.name %>
<% @wrestler1_school_name = @match.wrestler1.school.name %>
<% @wrestler1_last_match = @match.wrestler1.last_match %>
<% @wrestlers.push(@match.wrestler1) %>
<% else %>
<% @wrestler1_name = "Not assigned" %>
<% @wrestler1_school_name = "N/A" %>
<% @wrestler1_last_match = nil %>
<% end %>
<% if @match.w2 %>
<% @wrestler2_name = @match.wrestler2.name %>
<% @wrestler2_school_name = @match.wrestler2.school.name %>
<% @wrestler2_last_match = @match.wrestler2.last_match %>
<% @wrestlers.push(@match.wrestler2) %>
<% else %>
<% @wrestler2_name = "Not assigned" %>
<% @wrestler2_school_name = "N/A" %>
<% @wrestler2_last_match = nil %>
<% end %>
<% @tournament = @match.tournament %>
<% end %>
<% if @match %>
<%= render "matches/matchstats" %>
<% else %>
<p>No matches assigned to this mat.</p>
<% end %>

View File

@@ -1,9 +1,12 @@
<h3>Mat <%= @mat.name %></h3>
<h3>Tournament: <%= @mat.tournament.name %></h3>
<% if @match %>
<%= render 'matches/matchstats' %>
<% else %>
<p>No matches assigned to this mat.</p>
<% end %>
<%= turbo_stream_from @mat %>
<%= turbo_frame_tag dom_id(@mat, :current_match) do %>
<%= render "mats/current_match",
mat: @mat,
match: @match,
next_match: @next_match,
show_next_bout_button: @show_next_bout_button %>
<% end %>

View File

@@ -76,8 +76,12 @@
<% delete_wrestler_path_with_key = wrestler_path(wrestler) %>
<% delete_wrestler_path_with_key += "?school_permission_key=#{params[:school_permission_key]}" if params[:school_permission_key].present? %>
<%= link_to '', edit_wrestler_path_with_key, class: "fas fa-edit" %>
<%= link_to '', delete_wrestler_path_with_key, method: :delete, data: { confirm: "Are you sure you want to delete #{wrestler.name}? This will delete all of his matches." }, class: "fas fa-trash-alt" %>
<%= link_to edit_wrestler_path_with_key, class: "text-decoration-none" do %>
<span class="fas fa-edit" aria-hidden="true"></span>
<% end %>
<%= link_to delete_wrestler_path_with_key, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{wrestler.name}? This will delete all of his matches." }, class: "text-decoration-none" do %>
<span class="fas fa-trash-alt" aria-hidden="true"></span>
<% end %>
</td>
<% end %>
</tr>
@@ -101,8 +105,12 @@
<% delete_wrestler_path_with_key = wrestler_path(wrestler) %>
<% delete_wrestler_path_with_key += "?school_permission_key=#{params[:school_permission_key]}" if params[:school_permission_key].present? %>
<%= link_to '', edit_wrestler_path_with_key, class: "fas fa-edit" %>
<%= link_to '', delete_wrestler_path_with_key, method: :delete, data: { confirm: "Are you sure you want to delete #{wrestler.name}? This will delete all of his matches." }, class: "fas fa-trash-alt" %>
<%= link_to edit_wrestler_path_with_key, class: "text-decoration-none" do %>
<span class="fas fa-edit" aria-hidden="true"></span>
<% end %>
<%= link_to delete_wrestler_path_with_key, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{wrestler.name}? This will delete all of his matches." }, class: "text-decoration-none" do %>
<span class="fas fa-trash-alt" aria-hidden="true"></span>
<% end %>
</td>
<% end %>
</tr>
@@ -111,6 +119,6 @@
</tbody>
</table>
<% if can? :manage, @school %>
<%= render 'baums_roster_import' %>
<% end %>
<%# if can? :manage, @school %>
<%#= render 'baums_roster_import' %>
<%# end %>

View File

@@ -24,7 +24,7 @@
<td>
<%= link_to '', edit_tournament_path(tournament), :class=>"fas fa-edit" %>
<% if can? :destroy, tournament %>
<%= link_to '', tournament, method: :delete, data: { confirm: "Are you sure you want to delete #{tournament.name}?" }, :class=>"fas fa-trash-alt" %>
<%= link_to '', tournament, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{tournament.name}?" }, :class=>"fas fa-trash-alt" %>
<% end %>
</td>
<% end %>
@@ -55,4 +55,4 @@
<% end %>
</tbody>
</table>
<% end %>
<% end %>

View File

@@ -8,7 +8,7 @@ and will also delete all of your current data. It's best to use the create backu
<tr>
<th>Backup Created At</th>
<th>Backup Reason</th>
<th><%= link_to ' Create New Backup', tournament_tournament_backups_path(@tournament), method: :post, class: 'fas fa-plus'%></th>
<th><%= link_to ' Create New Backup', tournament_tournament_backups_path(@tournament), data: { turbo_method: :post }, class: 'fas fa-plus'%></th>
</tr>
</thead>
<tbody>
@@ -19,20 +19,20 @@ and will also delete all of your current data. It's best to use the create backu
</td>
<td><%= backup.backup_reason.presence || 'No reason provided' %></td>
<td>
<%= link_to '', restore_tournament_tournament_backup_path(@tournament, backup), method: :post, data: { confirm: "This will restore the backup from #{backup.created_at.strftime('%Y-%m-%d %H:%M:%S')}. It will delete all current data from the tournament in order to restore the backup." }, class: 'fas fa-undo-alt text-warning', title: 'Restore Backup' %>
<%= link_to '', tournament_tournament_backup_path(@tournament, backup), method: :delete, data: { confirm: 'Are you sure you want to delete this backup?' }, class: 'fas fa-trash-alt', title: 'Delete Backup' %>
<%= link_to '', restore_tournament_tournament_backup_path(@tournament, backup), data: { turbo_method: :post, turbo_confirm: "This will restore the backup from #{backup.created_at.strftime('%Y-%m-%d %H:%M:%S')}. It will delete all current data from the tournament in order to restore the backup." }, class: 'fas fa-undo-alt text-warning', title: 'Restore Backup' %>
<%= link_to '', tournament_tournament_backup_path(@tournament, backup), data: { turbo_method: :delete, turbo_confirm: 'Are you sure you want to delete this backup?' }, class: 'fas fa-trash-alt', title: 'Delete Backup' %>
</td>
</tr>
<% end %>
</tbody>
</table>
<br><br>
<h3>Import Manual Backup</h3>
<p>Paste the backup text here. Note, if this is formatted wrong, you'll need to restore a backup from above to fix it and you'll see an error in your background jobs.</p>
<%= form_for(:tournament, url: import_manual_tournament_tournament_backups_path(@tournament)) do |f| %>
<div class="field">
<%= f.label 'Import text' %><br>
<%= f.text_area :import_text, cols: "30", rows: "20" %>
</div>
<%= submit_tag "Import", class: "btn btn-success", data: { confirm: 'Are you sure? This will delete everything for the current tournament and restore it with the backup text pasted below.' } %>
<% end %>
<% if ENV["RAILS_ENV"] == "development" %>
<%= form_for(:tournament, url: import_manual_tournament_tournament_backups_path(@tournament)) do |f| %>
<div class="field">
<%= f.label 'Import text' %><br>
<%= f.text_area :import_text, cols: "30", rows: "20" %>
</div>
<%= submit_tag "Import", class: "btn btn-success", data: { turbo_confirm: 'Are you sure? This will delete everything for the current tournament and restore it with the backup text pasted below.' } %>
<% end %>
<% end %>

View File

@@ -2,7 +2,11 @@
<div class="round">
<div class="game">
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
<div class="bout-number"><p><%= match.bout_number %> <%= match.bracket_score_string %></p><p><%= @winner_place %> Place Winner</p></div>
<% if params[:print] %>
<div class="bout-number"><p><%= match.bout_number %> <%= match.bracket_score_string %></p><p><%= @winner_place %> Place Winner</p></div>
<% else %>
<div class="bout-number"><p><%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %></p><p><%= @winner_place %> Place Winner</p></div>
<% end %>
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
</div>
</div>

View File

@@ -11,7 +11,7 @@ table.smallText tr td { font-size: 10px; }
min-width: 150px;
min-height: 50px;
/*background-color: #ddd;*/
border: 1px solid #ddd;
border: 1px solid #000; /* Dark border so boxes stay visible when printed */
margin: 5px;
}
@@ -56,7 +56,7 @@ table.smallText tr td { font-size: 10px; }
}
.game-top {
border-bottom:1px solid #ddd;
border-bottom:1px solid #000;
padding: 2px;
min-height: 12px;
}
@@ -65,14 +65,25 @@ 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;
border-bottom:1px solid #000;
padding: 2px;
min-height: 12px;
}
.game-bottom {
border-top:1px solid #ddd;
border-top:1px solid #000;
padding: 2px;
min-height: 12px;
}
@@ -120,4 +131,4 @@ table.smallText tr td { font-size: 10px; }
<% elsif @tournament.tournament_type.include? "Regular Double Elimination" %>
<%= render 'double_elimination_bracket' %>
<% end %>

View File

@@ -2,7 +2,13 @@
<% @round_matches.sort_by{|m| m.bracket_position_number}.each do |match| %>
<div class="game">
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
<div class="bout-number"><%= match.bout_number %> <%= match.bracket_score_string %>&nbsp;</div>
<% if params[:print] %>
<div class="bout-number"><%= match.bout_number %> <%= match.bracket_score_string %>&nbsp;</div>
<% else %>
<div class="bout-number"><%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %>&nbsp;</div>
<% end %>
<div class="bout-number">Round <%= match.round %></div>
<div class="bout-number"><%= match.bracket_position %></div>
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
</div>
<% end %>

View File

@@ -0,0 +1,34 @@
<% queue1_match, queue2_match, queue3_match, queue4_match = mat.queue_matches %>
<% cache ["up_matches_mat_row", mat, mat.queue1, mat.queue2, mat.queue3, mat.queue4] do %>
<tr>
<td><%= mat.name %></td>
<td>
<% if queue1_match %><strong><%= queue1_match.bout_number %></strong> (<%= queue1_match.bracket_position %>)<br>
<%= queue1_match.weight_max %> lbs
<br><%= queue1_match.w1_bracket_name %> vs. <br>
<%= queue1_match.w2_bracket_name %>
<% end %>
</td>
<td>
<% if queue2_match %><strong><%= queue2_match.bout_number %></strong> (<%= queue2_match.bracket_position %>)<br>
<%= queue2_match.weight_max %> lbs
<br><%= queue2_match.w1_bracket_name %> vs. <br>
<%= queue2_match.w2_bracket_name %>
<% end %>
</td>
<td>
<% if queue3_match %><strong><%= queue3_match.bout_number %></strong> (<%= queue3_match.bracket_position %>)<br>
<%= queue3_match.weight_max %> lbs
<br><%= queue3_match.w1_bracket_name %> vs. <br>
<%= queue3_match.w2_bracket_name %>
<% end %>
</td>
<td>
<% if queue4_match %><strong><%= queue4_match.bout_number %></strong> (<%= queue4_match.bracket_position %>)<br>
<%= queue4_match.weight_max %> lbs
<br><%= queue4_match.w1_bracket_name %> vs. <br>
<%= queue4_match.w2_bracket_name %>
<% end %>
</td>
</tr>
<% end %>

View File

@@ -1,4 +1,4 @@
<% @matches.select{|m| m.finished == 1 && m.w1 && m.w2}.sort_by{|m| [ m.bout_number, m.bracket_position, m.weight.max ]}.each do |match| %>
<% @matches.select{|m| m.finished == 1 && m.w1 && m.w2}.sort_by{|m| [ m.round, -m.bracket_position, m.weight.max ]}.each do |match| %>
<% if @round != match.round || @bracket_position != match.bracket_position %>
<p><strong><%= match.bracket_position %> Round: <%= match.round %></strong></p>
<% end %>

View File

@@ -42,10 +42,10 @@
<% @users_delegates.each do |delegate| %>
<tr>
<td><%= delegate.user.email %></td>
<td><%= link_to 'Remove permissions', "/tournaments/#{@tournament.id}/#{delegate.id}/remove_delegate", method: :delete, confirm: 'Are you sure?', :class=>"btn btn-danger btn-sm" %></td>
<td><%= link_to 'Remove permissions', "/tournaments/#{@tournament.id}/#{delegate.id}/remove_delegate", data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' }, :class=>"btn btn-danger btn-sm" %></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<% end %>

View File

@@ -3,7 +3,11 @@
"attributes": <%= @tournament.attributes.to_json %>,
"schools": <%= @tournament.schools.map(&:attributes).to_json %>,
"weights": <%= @tournament.weights.map(&:attributes).to_json %>,
"mats": <%= @tournament.mats.map(&:attributes).to_json %>,
"mats": <%= @tournament.mats.map { |mat| mat.attributes.merge(
{
"queue_bout_numbers": mat.queue_matches.map { |match| match&.bout_number }
}
) }.to_json %>,
"wrestlers": <%= @tournament.wrestlers.map { |wrestler| wrestler.attributes.merge(
{
"school": wrestler.school&.attributes,
@@ -20,4 +24,4 @@
}
) }.to_json %>
}
}
}

View File

@@ -1,42 +1,85 @@
<h1>Upcoming Tournaments</h1> <%= form_tag(tournaments_path, :method => "get", id: "search-form") do %>
<%= text_field_tag :search, params[:search], placeholder: "Search Tournaments" %>
<%= submit_tag "Search" %>
<% end %>
<p>Search by name or date YYYY-MM-DD</p>
<script>
// $(document).ready(function() {
// $('#tournamentList').dataTable();
// pagingType: "bootstrap";
// } );
</script>
<h1>Upcoming Tournaments</h1>
<%= form_tag(tournaments_path, :method => "get", id: "search-form") do %>
<%= text_field_tag :search, params[:search], placeholder: "Search Tournaments" %>
<%= submit_tag "Search" %>
<% end %>
<p>Search by name or date YYYY-MM-DD</p>
<br>
<table class="table table-hover table-condensed" id="tournamentList">
<thead>
<tr>
<th>Name</th>
<th>Date</th>
<th><% if user_signed_in? %><%= link_to ' New Tournament', new_tournament_path, :class=>"fas fa-plus" %></th><% end %>
<th>
<% if user_signed_in? %>
<%= link_to ' New Tournament', new_tournament_path, :class=>"fas fa-plus" %>
<% end %>
</th>
</tr>
</thead>
<tbody>
<% @tournaments.each do |tournament| %>
<tr>
<td><%= link_to "#{tournament.name}", tournament %></td>
<td><%= link_to tournament.name, tournament %></td>
<td><%= tournament.date %></td>
<td>
<% if can? :manage, tournament %>
<%= link_to '', edit_tournament_path(tournament), :class=>"fas fa-edit" %>
<% if can? :destroy, tournament %>
<%= link_to '', tournament, method: :delete, data: { confirm: "Are you sure you want to delete #{tournament.name}?" }, :class=>"fas fa-trash-alt" %>
<%= link_to '', tournament, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{tournament.name}?" }, :class=>"fas fa-trash-alt" %>
<% end %>
<% end %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%# Pagination controls %>
<% if @total_pages.present? && @total_pages > 1 %>
<nav aria-label="Tournaments pagination">
<ul class="pagination">
<%# Previous link %>
<% if @page > 1 %>
<li class="page-item">
<%= link_to 'Previous', tournaments_path(page: @page - 1, search: params[:search]), class: "page-link" %>
</li>
<% else %>
<li class="page-item disabled"><span class="page-link">Previous</span></li>
<% end %>
<%# Page number links (limit displayed pages for large counts) %>
<% window = 5
left = [1, @page - window/2].max
right = [@total_pages, left + window - 1].min
left = [1, right - window + 1].max
%>
<% (left..right).each do |p| %>
<% if p == @page %>
<li class="page-item active"><span class="page-link"><%= p %></span></li>
<% else %>
<li class="page-item"><%= link_to p, tournaments_path(page: p, search: params[:search]), class: "page-link" %></li>
<% end %>
<% end %>
<%# Next link %>
<% if @page < @total_pages %>
<li class="page-item">
<%= link_to 'Next', tournaments_path(page: @page + 1, search: params[:search]), class: "page-link" %>
</li>
<% else %>
<li class="page-item disabled"><span class="page-link">Next</span></li>
<% end %>
</ul>
</nav>
<p class="text-muted">
<% start_index = ((@page - 1) * @per_page) + 1
end_index = [@page * @per_page, @total_count].min
%>
Showing <%= start_index %> - <%= end_index %> of <%= @total_count %> tournaments
</p>
<% end %>

View File

@@ -28,9 +28,13 @@
<td><%= match.finished %></td>
<td><%= link_to 'Show', match, :class=>"btn btn-default btn-sm" %>
<%= link_to 'Edit Wrestlers', edit_match_path(match), :class=>"btn btn-primary btn-sm" %>
<%= link_to 'Edit Mat/Queue', edit_assignment_match_path(match), :class=>"btn btn-primary btn-sm" %>
<%= link_to 'Stat Match', "/matches/#{match.id}/stat", :class=>"btn btn-primary btn-sm" %>
</td>
</tr>
<% end %>
</tbody>
</table>
</table>
<br>
<p>Total matches without byes: <%= @matches.select{|m| m.loser1_name != 'BYE' and m.loser2_name != 'BYE'}.size %></p>
<p>Unfinished matches: <%= @matches.select{|m| m.finished != 1 and m.loser1_name != 'BYE' and m.loser2_name != 'BYE'}.size %></p>

View File

@@ -0,0 +1,50 @@
<style>
.qr-page {
min-height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 24px;
}
.qr-page h1 {
margin: 0 0 24px 0;
font-size: 40px;
font-weight: 700;
}
.qr-code {
width: 100%;
display: flex;
justify-content: center;
}
.qr-code svg {
width: min(80vmin, 720px);
height: auto;
display: block;
margin: 0;
}
@media print {
body {
margin: 0;
}
}
</style>
<div class="qr-page">
<h1><%= @tournament.name %> Brackets and Results Available Here</h1>
<div class="qr-code">
<%= raw @qrcode.as_svg(
offset: 0,
color: "000",
shape_rendering: "crispEdges",
module_size: 8,
standalone: true
) %>
</div>
</div>

View File

@@ -78,7 +78,7 @@
<tr>
<td><%= delegate.user.email %></td>
<td><%= delegate.school.name %></td>
<td><%= link_to 'Remove permissions', "/tournaments/#{@tournament.id}/#{delegate.id}/remove_school_delegate", method: :delete, confirm: 'Are you sure?', :class=>"btn btn-danger btn-sm" %></td>
<td><%= link_to 'Remove permissions', "/tournaments/#{@tournament.id}/#{delegate.id}/remove_school_delegate", data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' }, :class=>"btn btn-danger btn-sm" %></td>
</tr>
<% end %>

Some files were not shown because too many files have changed in this diff Show More