├── demo
├── log
│ └── .keep
├── lib
│ └── assets
│ │ └── .keep
├── public
│ ├── favicon.ico
│ ├── icon.png
│ ├── icon.svg
│ ├── 406-unsupported-browser.html
│ ├── 500.html
│ ├── 422.html
│ └── 404.html
├── app
│ ├── assets
│ │ ├── images
│ │ │ └── .keep
│ │ ├── config
│ │ │ └── manifest.js
│ │ └── stylesheets
│ │ │ └── application.css
│ ├── models
│ │ ├── concerns
│ │ │ └── .keep
│ │ └── application_record.rb
│ ├── controllers
│ │ ├── concerns
│ │ │ └── .keep
│ │ ├── application_controller.rb
│ │ └── examples_controller.rb
│ ├── views
│ │ ├── layouts
│ │ │ ├── mailer.text.erb
│ │ │ ├── mailer.html.erb
│ │ │ └── application.html.erb
│ │ └── examples
│ │ │ ├── create.html.erb
│ │ │ ├── index.html.erb
│ │ │ └── new.html.erb
│ ├── helpers
│ │ └── application_helper.rb
│ ├── channels
│ │ └── application_cable
│ │ │ ├── channel.rb
│ │ │ └── connection.rb
│ ├── mailers
│ │ └── application_mailer.rb
│ └── jobs
│ │ └── application_job.rb
├── config
│ ├── environments
│ │ ├── demo.rb
│ │ ├── development.rb
│ │ ├── test.rb
│ │ └── production.rb
│ ├── environment.rb
│ ├── cable.yml
│ ├── boot.rb
│ ├── routes.rb
│ ├── initializers
│ │ ├── filter_parameter_logging.rb
│ │ ├── permissions_policy.rb
│ │ ├── assets.rb
│ │ ├── inflections.rb
│ │ └── content_security_policy.rb
│ ├── locales
│ │ └── en.yml
│ ├── application.rb
│ ├── database.yml
│ ├── storage.yml
│ └── puma.rb
├── bin
│ ├── rake
│ ├── rails
│ └── setup
├── config.ru
├── Rakefile
└── db
│ └── schema.rb
├── spec
├── system
│ └── .keep
├── tmp
│ └── .keep
├── app
│ ├── controllers
│ │ ├── .keep
│ │ └── events_controller_spec.rb
│ └── models
│ │ └── spectator_sport
│ │ └── event_spec.rb
├── support
│ ├── rspec_not_change.rb
│ ├── sql_helper.rb
│ ├── output_helper.rb
│ ├── logger.rb
│ ├── uuid.rb
│ ├── selenium.rb
│ ├── system.rb
│ ├── shell_out.rb
│ ├── pre_documentation_formatter.rb
│ └── example_app_helper.rb
├── lib
│ └── spectator_sport_spec.rb
├── generators
│ └── spectator_sport
│ │ └── install_generator_spec.rb
├── rails_helper.rb
└── spec_helper.rb
├── .ruby-version
├── app
├── models
│ ├── concerns
│ │ └── .keep
│ └── spectator_sport
│ │ ├── application_record.rb
│ │ ├── session.rb
│ │ ├── session_window.rb
│ │ └── event.rb
├── controllers
│ ├── concerns
│ │ └── .keep
│ └── spectator_sport
│ │ ├── application_controller.rb
│ │ ├── dashboard
│ │ ├── dashboards_controller.rb
│ │ ├── application_controller.rb
│ │ ├── session_windows_controller.rb
│ │ └── frontends_controller.rb
│ │ └── events_controller.rb
├── helpers
│ └── spectator_sport
│ │ ├── application_helper.rb
│ │ ├── dashboard
│ │ ├── application_helper.rb
│ │ └── icons_helper.rb
│ │ └── script_helper.rb
├── jobs
│ └── spectator_sport
│ │ └── application_job.rb
├── mailers
│ └── spectator_sport
│ │ └── application_mailer.rb
├── views
│ ├── spectator_sport
│ │ ├── dashboard
│ │ │ ├── session_windows
│ │ │ │ ├── _more_events_frame.erb
│ │ │ │ ├── events.erb
│ │ │ │ └── show.html.erb
│ │ │ └── dashboards
│ │ │ │ └── index.html.erb
│ │ └── shared
│ │ │ └── _navbar.erb
│ └── layouts
│ │ └── spectator_sport
│ │ └── dashboard
│ │ └── application.html.erb
└── frontend
│ └── spectator_sport
│ └── dashboard
│ ├── application.js
│ ├── modules
│ ├── theme_controller.js
│ └── player_controller.js
│ ├── style.css
│ ├── vendor
│ ├── rrweb-player
│ │ └── rrweb-player.min.css
│ └── es_module_shims.js
│ └── icons.svg
├── Procfile
├── lib
├── spectator_sport
│ ├── version.rb
│ └── engine.rb
├── tasks
│ └── spectator_sport_tasks.rake
└── spectator_sport.rb
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ └── ci.yml
├── bin
├── rspec
├── rubocop
└── rails
├── config
├── routes.rb
└── dashboard_routes.rb
├── .gitignore
├── .rubocop.yml
├── Rakefile
├── Gemfile
├── db
└── migrate
│ └── 20240923140845_create_spectator_sport_events.rb
├── spectator_sport.gemspec
├── LICENSE.txt
├── README.md
└── Gemfile.lock
/demo/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/system/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.4.5
2 |
--------------------------------------------------------------------------------
/demo/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/app/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/app/models/spectator_sport/event_spec.rb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/demo/config/environments/demo.rb:
--------------------------------------------------------------------------------
1 | require_relative "production"
2 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | release: bin/rails app:db:prepare
2 | web: bin/rails server
3 |
--------------------------------------------------------------------------------
/demo/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/lib/spectator_sport/version.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | VERSION = "0.1.0"
3 | end
4 |
--------------------------------------------------------------------------------
/demo/app/views/examples/create.html.erb:
--------------------------------------------------------------------------------
1 | <%= @resource.name %>
2 | <%= @resource.message %>
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [bensheldon]
4 |
--------------------------------------------------------------------------------
/demo/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bensheldon/spectator_sport/HEAD/demo/public/icon.png
--------------------------------------------------------------------------------
/bin/rspec:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'bundler/setup'
3 | load Gem.bin_path('rspec-core', 'rspec')
4 |
--------------------------------------------------------------------------------
/demo/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 |
--------------------------------------------------------------------------------
/demo/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | end
3 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | SpectatorSport::Engine.routes.draw do
2 | resources :events, only: [ :index, :create ]
3 | end
4 |
--------------------------------------------------------------------------------
/demo/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | primary_abstract_class
3 | end
4 |
--------------------------------------------------------------------------------
/demo/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative "../config/boot"
3 | require "rake"
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/app/helpers/spectator_sport/application_helper.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | module ApplicationHelper
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/jobs/spectator_sport/application_job.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | class ApplicationJob < ActiveJob::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/support/rspec_not_change.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec::Matchers.define_negated_matcher :not_change, :change
4 |
--------------------------------------------------------------------------------
/demo/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/lib/spectator_sport_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | describe SpectatorSport do
6 | end
7 |
--------------------------------------------------------------------------------
/lib/tasks/spectator_sport_tasks.rake:
--------------------------------------------------------------------------------
1 | # desc "Explaining what the task does"
2 | # task :spectator_sport do
3 | # # Task goes here
4 | # end
5 |
--------------------------------------------------------------------------------
/demo/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/demo/public/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/demo/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: "from@example.com"
3 | layout "mailer"
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/spectator_sport/application_controller.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | class ApplicationController < ActionController::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/lib/spectator_sport.rb:
--------------------------------------------------------------------------------
1 | require "spectator_sport/version"
2 | require "spectator_sport/engine"
3 |
4 | module SpectatorSport
5 | # Your code goes here...
6 | end
7 |
--------------------------------------------------------------------------------
/demo/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path("../config/application", __dir__)
3 | require_relative "../config/boot"
4 | require "rails/commands"
5 |
--------------------------------------------------------------------------------
/app/models/spectator_sport/application_record.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | class ApplicationRecord < ActiveRecord::Base
3 | self.abstract_class = true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/demo/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /doc/
3 | /log/*.log
4 | /pkg/
5 | /tmp/
6 | /demo/db/*.sqlite3
7 | /demo/db/*.sqlite3-*
8 | /demo/log/*.log
9 | /demo/storage/
10 | /demo/tmp/
11 |
--------------------------------------------------------------------------------
/app/models/spectator_sport/session.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | class Session < ApplicationRecord
3 | has_many :session_windows
4 | has_many :events
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/models/spectator_sport/session_window.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | class SessionWindow < ApplicationRecord
3 | belongs_to :session
4 | has_many :events
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/demo/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/app/helpers/spectator_sport/dashboard/application_helper.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | module Dashboard
3 | module ApplicationHelper
4 | include Dashboard::IconsHelper
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/mailers/spectator_sport/application_mailer.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | class ApplicationMailer < ActionMailer::Base
3 | default from: "from@example.com"
4 | layout "mailer"
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/views/spectator_sport/dashboard/session_windows/_more_events_frame.erb:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/helpers/spectator_sport/script_helper.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | module ScriptHelper
3 | def spectator_sport_script_tags
4 | tag.script defer: true, src: spectator_sport.events_path(format: :js)
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/sql_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SqlHelper
4 | def normalize_sql(sql)
5 | sql.gsub(/\s/, ' ').gsub(/([()])/, ' \1 ').squish
6 | end
7 | end
8 |
9 | RSpec.configure { |c| c.include SqlHelper }
10 |
--------------------------------------------------------------------------------
/demo/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative "config/application"
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/demo/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: spectator_sport_production
11 |
--------------------------------------------------------------------------------
/demo/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
3 |
4 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
5 | $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__)
6 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | # Omakase Ruby styling for Rails
2 | inherit_gem: { rubocop-rails-omakase: rubocop.yml }
3 |
4 | # Overwrite or add rules to create your own house style
5 | #
6 | # # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
7 | # Layout/SpaceInsideArrayLiteralBrackets:
8 | # Enabled: false
9 |
--------------------------------------------------------------------------------
/bin/rubocop:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "rubygems"
3 | require "bundler/setup"
4 |
5 | # explicit rubocop config increases performance slightly while avoiding config confusion.
6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
7 |
8 | load Gem.bin_path("rubocop", "rubocop")
9 |
--------------------------------------------------------------------------------
/demo/app/views/examples/index.html.erb:
--------------------------------------------------------------------------------
1 |
This is an example page to demonstrate Spectator Sport
2 | Your browser activity is being recorded.
3 |
4 | <%= link_to "Pretend you are doing stuff", { action: :new } %>
5 |
6 | <%= link_to "Try encountering server error", { action: :error } %>
7 |
--------------------------------------------------------------------------------
/demo/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | # Automatically retry jobs that encountered a deadlock
3 | # retry_on ActiveRecord::Deadlocked
4 |
5 | # Most jobs are safe to ignore if the underlying records are no longer available
6 | # discard_on ActiveJob::DeserializationError
7 | end
8 |
--------------------------------------------------------------------------------
/demo/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/controllers/spectator_sport/dashboard/dashboards_controller.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | module Dashboard
3 | class DashboardsController < ApplicationController
4 | def index
5 | @session_windows = SessionWindow.order(:created_at).limit(50).reverse_order
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/spectator_sport/dashboard/application_controller.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | module Dashboard
3 | class ApplicationController < ActionController::Base
4 | end
5 | end
6 | end
7 |
8 | ActiveSupport.run_load_hooks(:spectator_sport_dashboard_application_controller, SpectatorSport::Dashboard::ApplicationController)
9 |
--------------------------------------------------------------------------------
/demo/app/views/examples/new.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with model: @resource, url: { action: :create }, action: :post do |f| %>
2 | <%= f.label :name %>
3 | <%= f.text_field :name %>
4 |
5 |
6 | <%= f.label :message %>
7 | <%= f.text_area :message %>
8 |
9 |
10 |
11 | <%= f.submit "Submit" %>
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 |
3 | APP_RAKEFILE = File.expand_path("demo/Rakefile", __dir__)
4 | load "rails/tasks/engine.rake"
5 |
6 | load "rails/tasks/statistics.rake"
7 |
8 | require "bundler/gem_tasks"
9 |
10 | # assets:precompile does not exist. call app:assets:precompile is used instead
11 | task "assets:precompile" => "app:assets:precompile"
12 |
--------------------------------------------------------------------------------
/spec/support/output_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module OutputHelper
4 | def quiet(&block)
5 | if ENV['LOUD'].present?
6 | yield
7 | else
8 | expect(&block).to output(/.*/).to_stderr_from_any_process.and output(/.*/).to_stdout_from_any_process
9 | end
10 | end
11 | end
12 | RSpec.configure { |c| c.include OutputHelper }
13 |
--------------------------------------------------------------------------------
/demo/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | mount SpectatorSport::Engine => "/spectator_sport"
3 | mount SpectatorSport::Dashboard::Engine => "/spectator_sport_dashboard"
4 |
5 | root to: "examples#index"
6 |
7 | resources :examples, only: [ :index, :show, :new, :create ] do
8 | collection do
9 | get :error
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/models/spectator_sport/event.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | class Event < ApplicationRecord
3 | PAGE_LIMIT = 1_000
4 |
5 | belongs_to :session
6 | belongs_to :session_window
7 |
8 | scope :page_after, ->(event) { (event ? where("(created_at, id) > (?, ?)", event.created_at, event.id) : self).order(created_at: :asc, id: :asc).limit(PAGE_LIMIT) }
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/frontend/spectator_sport/dashboard/application.js:
--------------------------------------------------------------------------------
1 | /*jshint esversion: 6, strict: false */
2 |
3 | import "turbo";
4 | import { Application } from "stimulus";
5 |
6 | window.Stimulus = Application.start();
7 |
8 |
9 | import ThemeController from "theme_controller";
10 | import PlayerController from "player_controller";
11 | Stimulus.register("theme", ThemeController);
12 | Stimulus.register("player", PlayerController);
13 |
--------------------------------------------------------------------------------
/spec/support/logger.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.configure do |config|
4 | config.around do |example|
5 | Rails.logger.debug { "\n\n---- START EXAMPLE: #{example.full_description} (#{example.location})" }
6 | Thread.current.name = "RSpec: #{example.description}"
7 | example.run
8 | Rails.logger.debug { "---- END EXAMPLE: #{example.full_description} (#{example.location})\n\n" }
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/support/uuid.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec::Matchers.define :be_a_uuid do
4 | match do |actual|
5 | regexp = /\A\h{8}-\h{4}-(\h{4})-\h{4}-\h{12}\z/
6 | actual.is_a?(String) && actual.match?(regexp)
7 | end
8 |
9 | description { "a UUID" }
10 | failure_message { "expected #{description}" }
11 | failure_message_when_negated { "did not expect #{description}" }
12 | end
13 |
14 | RSpec::Matchers.alias_matcher :a_uuid, :be_a_uuid
15 |
--------------------------------------------------------------------------------
/demo/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
4 | # Use this to limit dissemination of sensitive information.
5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
6 | Rails.application.config.filter_parameters += [
7 | :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
8 | ]
9 |
--------------------------------------------------------------------------------
/app/views/spectator_sport/dashboard/session_windows/events.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= tag.div data: { player_target: "events", events: @events.map(&:event_data).to_json } %>
4 |
5 |
6 |
7 | <% if @events.size >= SpectatorSport::Event::PAGE_LIMIT %>
8 |
9 |
10 | <%= render "more_events_frame", events: @events %>
11 |
12 |
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/config/dashboard_routes.rb:
--------------------------------------------------------------------------------
1 | SpectatorSport::Dashboard::Engine.routes.draw do
2 | root to: "dashboards#index"
3 | resources :session_windows, only: [ :show, :destroy ] do
4 | member do
5 | get :events
6 | end
7 | end
8 |
9 | scope :frontend, controller: :frontends, defaults: { version: SpectatorSport::VERSION.tr(".", "-") } do
10 | get "modules/:version/:id", action: :module, as: :frontend_module, constraints: { format: "js" }
11 | get "static/:version/:id", action: :static, as: :frontend_static
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/helpers/spectator_sport/dashboard/icons_helper.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | module Dashboard
3 | module IconsHelper
4 | def render_icon(name, class: nil, **options)
5 | tag.svg(viewBox: "0 0 16 16", class: "svg-icon #{binding.local_variable_get(:class)}", **options) do
6 | tag.use(fill: "currentColor", href: "#{icons_path}##{name}")
7 | end
8 | end
9 |
10 | def icons_path
11 | @_icons_path ||= frontend_static_path(:icons, format: :svg, locale: nil)
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/demo/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide HTTP permissions policy. For further
4 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy
5 |
6 | # Rails.application.config.permissions_policy do |policy|
7 | # policy.camera :none
8 | # policy.gyroscope :none
9 | # policy.microphone :none
10 | # policy.usb :none
11 | # policy.fullscreen :self
12 | # policy.payment :self, "https://secure.example.com"
13 | # end
14 |
--------------------------------------------------------------------------------
/demo/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = "1.0"
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in the app/assets
11 | # folder are already added.
12 | # Rails.application.config.assets.precompile += %w[ admin.js admin.css ]
13 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails gems
3 | # installed from the root of your application.
4 |
5 | ENGINE_ROOT = File.expand_path("..", __dir__)
6 | ENGINE_PATH = File.expand_path("../lib/spectator_sport/engine", __dir__)
7 | APP_PATH = File.expand_path("../demo/config/application", __dir__)
8 |
9 | # Set up gems listed in the Gemfile.
10 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
11 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
12 |
13 | require "rails/all"
14 | require "rails/engine/commands"
15 |
--------------------------------------------------------------------------------
/app/views/spectator_sport/dashboard/session_windows/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= tag.div id: "player",
5 | data: {
6 | id: "player",
7 | controller: "player",
8 | player_events_value: @events.map(&:event_data).to_json,
9 | } do %>
10 |
11 |
12 | <% if @events.size >= SpectatorSport::Event::PAGE_LIMIT %>
13 | <%= render "more_events_frame", events: @events %>
14 | <% end %>
15 | <% end %>
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/lib/spectator_sport/engine.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | class Engine < ::Rails::Engine
3 | isolate_namespace SpectatorSport
4 |
5 | initializer "local_helper.action_controller" do
6 | ActiveSupport.on_load :action_controller do
7 | # TODO: this should probably be done manually by the client, maybe?
8 | helper SpectatorSport::ScriptHelper
9 | end
10 | end
11 | end
12 |
13 | module Dashboard
14 | class Engine < ::Rails::Engine
15 | isolate_namespace SpectatorSport::Dashboard
16 | paths.add "config/routes.rb", with: "config/dashboard_routes.rb"
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "bundler"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 | groups:
8 | bundler-lint:
9 | patterns:
10 | - brakeman
11 | - erb_lint
12 | - rubocop
13 | - rubocop-*
14 | bundler-dependencies:
15 | patterns:
16 | - "*"
17 | exclude-patterns:
18 | - puma
19 | - brakeman
20 | - erb_lint
21 | - rubocop
22 | - rubocop-*
23 | - package-ecosystem: github-actions
24 | directory: "/"
25 | schedule:
26 | interval: monthly
27 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | if File.exist?(".ruby-version")
4 | ruby file: ".ruby-version"
5 | end
6 |
7 | # Specify your gem's dependencies in spectator_sport.gemspec.
8 | gemspec
9 |
10 | gem "puma"
11 |
12 | gem "sqlite3"
13 | gem "pg"
14 |
15 | gem "sprockets-rails"
16 |
17 | # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
18 | gem "rubocop-rails-omakase", require: false
19 |
20 | # Start debugger with binding.b [https://github.com/ruby/debug]
21 | # gem "debug", ">= 1.0.0"
22 |
23 | group :test do
24 | gem "capybara"
25 | gem "rspec-rails"
26 | gem "selenium-webdriver"
27 | gem "warning"
28 | end
29 |
--------------------------------------------------------------------------------
/spec/generators/spectator_sport/install_generator_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 | # require 'generators/spectator_sport/install_generator'
5 |
6 | describe "install generator", type: :generator do
7 | around do |example|
8 | quiet { setup_example_app }
9 | example.run
10 | teardown_example_app
11 | end
12 |
13 | it 'creates a migration for spectator_sport_events table', skip: true do
14 | quiet do
15 | run_in_example_app 'rails g spectator_sport:install:migrations'
16 | end
17 |
18 | expect(Dir.glob("#{example_app_path}/db/migrate/[0-9]*_create_spectator_sport_events.rb")).not_to be_empty
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/demo/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, "\\1en"
8 | # inflect.singular /^(ox)en/i, "\\1"
9 | # inflect.irregular "person", "people"
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym "RESTful"
16 | # end
17 |
--------------------------------------------------------------------------------
/demo/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/demo/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= content_for(:title) || "SpectatorSport Demo" %>
5 |
6 | <%= csrf_meta_tags %>
7 | <%= csp_meta_tag %>
8 |
9 | <%= yield :head %>
10 |
11 |
12 |
13 |
14 | <%= stylesheet_link_tag "application" %>
15 |
16 | <%= spectator_sport_script_tags %>
17 |
18 |
19 |
20 | <%= yield %>
21 |
22 |
23 |
24 | Replay it on the <%= link_to "Spectator Sport Dashboard", spectator_sport_dashboard_path %>
25 |
26 |
27 |
--------------------------------------------------------------------------------
/db/migrate/20240923140845_create_spectator_sport_events.rb:
--------------------------------------------------------------------------------
1 | class CreateSpectatorSportEvents < ActiveRecord::Migration[7.2]
2 | def change
3 | create_table :spectator_sport_sessions do |t|
4 | t.timestamps
5 | t.string :secure_id, null: false
6 |
7 | t.index [ :secure_id, :created_at ]
8 | end
9 |
10 | create_table :spectator_sport_session_windows do |t|
11 | t.timestamps
12 | t.references :session, null: false
13 | t.string :secure_id, null: false
14 | end
15 |
16 | create_table :spectator_sport_events do |t|
17 | t.timestamps
18 | t.references :session, null: false
19 | t.references :session_window, null: true
20 | t.json :event_data, null: false # TODO: jsonb for postgres ???
21 |
22 | t.index [ :session_id, :created_at ]
23 | t.index [ :session_window_id, :created_at ]
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/demo/app/controllers/examples_controller.rb:
--------------------------------------------------------------------------------
1 | class ExamplesController < ApplicationController
2 | ExampleError = Class.new(StandardError)
3 |
4 | def index
5 | end
6 |
7 | def new
8 | @resource = FakeModel.new
9 | end
10 |
11 | def create
12 | resource_params = params.require(:resource).permit(:name, :message)
13 | @resource = FakeModel.new(resource_params)
14 | end
15 |
16 | class FakeModel
17 | include ActiveModel::Model
18 | attr_accessor :name, :message
19 |
20 | def self.model_name
21 | ActiveModel::Name.new(self, nil, "Resource")
22 | end
23 | end
24 |
25 | # This enpoints serves for testing behavior when error page is encountered
26 | def error
27 | request.env["action_dispatch.show_detailed_exceptions"] = false
28 | raise ExampleError
29 | end
30 |
31 | private
32 |
33 | def show_detailed_exceptions?
34 | action_name == "error" ? false : super
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/spec/app/controllers/events_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | describe SpectatorSport::EventsController, type: :controller do
6 | render_views
7 |
8 | before do
9 | @routes = SpectatorSport::Engine.routes
10 | end
11 |
12 | describe 'POST #create' do
13 | it 'stores events from rrweb' do
14 | payload = {
15 | "sessionId": "fb1x1aji7p5nljgnydya9kid7vncrzw48ir7d70h",
16 | "windowId": "873h0zhhw66i9f1t36rh5myu8pzwopt676v9s83z",
17 | "events": [
18 | { "type": 4, "data": { "href": "http://127.0.0.1:3000/", "width": 1512, "height": 770 }, "timestamp": 1727655888530 }
19 | ]
20 | }
21 |
22 | post :create, params: payload
23 |
24 | expect(SpectatorSport::Event.last).to have_attributes(
25 | session: SpectatorSport::Session.find_by(secure_id: payload[:sessionId])
26 | )
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spectator_sport.gemspec:
--------------------------------------------------------------------------------
1 | require_relative "lib/spectator_sport/version"
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = "spectator_sport"
5 | spec.version = SpectatorSport::VERSION
6 | spec.summary = "Summary of SpectatorSport."
7 | spec.description = "Description of SpectatorSport."
8 |
9 | spec.license = "MIT"
10 | spec.authors = [ "Ben Sheldon" ]
11 | spec.email = [ "bensheldon@gmail.com" ]
12 | spec.homepage = "https://github.com/bensheldon/spectator_sport"
13 | spec.metadata = {
14 | "homepage_uri" => spec.homepage,
15 | "source_code_uri" => "https://github.com/bensheldon/spectator_sport",
16 | "changelog_uri" => "https://github.com/bensheldon/spectator_sport/releases"
17 | }
18 |
19 | spec.files = Dir[
20 | "app/**/*",
21 | "config/**/*",
22 | "lib/**/*",
23 | "README.md",
24 | "LICENSE.txt",
25 | ]
26 |
27 | spec.add_dependency "rails", ">= 7.2.1"
28 | end
29 |
--------------------------------------------------------------------------------
/spec/support/selenium.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "action_dispatch/system_testing/browser"
4 |
5 | # Monkeypatch to quiet deprecation notice:
6 | # https://github.com/rails/rails/blob/55c4adeb36eff229972eecbb53723c1b80393091/actionpack/lib/action_dispatch/system_testing/browser.rb#L74
7 | module ActionDispatch
8 | module SystemTesting
9 | class Browser # :nodoc:
10 | silence_redefinition_of_method :resolve_driver_path
11 | def resolve_driver_path(namespace)
12 | # The path method has been deprecated in 4.20.0
13 | namespace::Service.driver_path = if Gem::Version.new(::Selenium::WebDriver::VERSION) >= Gem::Version.new("4.20.0")
14 | ::Selenium::WebDriver::DriverFinder.new(options, namespace::Service.new).driver_path
15 | else
16 | ::Selenium::WebDriver::DriverFinder.path(options, namespace::Service)
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/controllers/spectator_sport/dashboard/session_windows_controller.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | module Dashboard
3 | class SessionWindowsController < ApplicationController
4 | def show
5 | @session_window = SessionWindow.find(params[:id])
6 | @events = @session_window.events.page_after(nil)
7 | end
8 |
9 | def events
10 | response.content_type = "text/vnd.turbo-stream.html"
11 | return head(:ok) if params[:after_event_id].blank?
12 |
13 | session_window = SessionWindow.find(params[:id])
14 | previous_event = session_window.events.find_by(id: params[:after_event_id])
15 | @events = session_window.events.page_after(previous_event)
16 |
17 | render layout: false
18 | end
19 |
20 | def destroy
21 | @session_window = SessionWindow.find(params[:id])
22 | @session_window.events.delete_all
23 | @session_window.delete
24 |
25 | redirect_to root_path
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/demo/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization and
2 | # are automatically loaded by Rails. If you want to use locales other than
3 | # English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t "hello"
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t("hello") %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more about the API, please read the Rails Internationalization guide
20 | # at https://guides.rubyonrails.org/i18n.html.
21 | #
22 | # Be aware that YAML interprets the following case-insensitive strings as
23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
24 | # must be quoted to be interpreted as strings. For example:
25 | #
26 | # en:
27 | # "yes": yup
28 | # enabled: "ON"
29 |
30 | en:
31 | hello: "Hello world"
32 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2024 Ben Sheldon
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/demo/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails/all"
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module Demo
10 | class Application < Rails::Application
11 | config.load_defaults Rails::VERSION::STRING.to_f
12 |
13 | # For compatibility with applications that use this config
14 | config.action_controller.include_all_helpers = false
15 |
16 | # Please, add to the `ignore` list any other `lib` subdirectories that do
17 | # not contain `.rb` files, or that should not be reloaded or eager loaded.
18 | # Common ones are `templates`, `generators`, or `middleware`, for example.
19 | config.autoload_lib(ignore: %w[assets tasks])
20 |
21 | # Configuration for the application, engines, and railties goes here.
22 | #
23 | # These settings can be overridden in specific environments using the files
24 | # in config/environments, which are processed later.
25 | #
26 | # config.time_zone = "Central Time (US & Canada)"
27 | # config.eager_load_paths << Rails.root.join("extras")
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/demo/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy.
4 | # See the Securing Rails Applications Guide for more information:
5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header
6 |
7 | # Rails.application.configure do
8 | # config.content_security_policy do |policy|
9 | # policy.default_src :self, :https
10 | # policy.font_src :self, :https, :data
11 | # policy.img_src :self, :https, :data
12 | # policy.object_src :none
13 | # policy.script_src :self, :https
14 | # policy.style_src :self, :https
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 | #
19 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles.
20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
21 | # config.content_security_policy_nonce_directives = %w(script-src style-src)
22 | #
23 | # # Report violations without enforcing the policy.
24 | # # config.content_security_policy_report_only = true
25 | # end
26 |
--------------------------------------------------------------------------------
/demo/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | APP_ROOT = File.expand_path("..", __dir__)
5 | APP_NAME = "demo"
6 |
7 | def system!(*args)
8 | system(*args, exception: true)
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts "== Installing dependencies =="
17 | system! "gem install bundler --conservative"
18 | system("bundle check") || system!("bundle install")
19 |
20 | # puts "\n== Copying sample files =="
21 | # unless File.exist?("config/database.yml")
22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml"
23 | # end
24 |
25 | puts "\n== Preparing database =="
26 | system! "bin/rails db:prepare"
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! "bin/rails log:clear tmp:clear"
30 |
31 | puts "\n== Restarting application server =="
32 | system! "bin/rails restart"
33 |
34 | # puts "\n== Configuring puma-dev =="
35 | # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}"
36 | # system "curl -Is https://#{APP_NAME}.test/up | head -n 1"
37 | end
38 |
--------------------------------------------------------------------------------
/demo/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite. Versions 3.8.0 and up are supported.
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem "sqlite3"
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: storage/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: storage/test.sqlite3
22 |
23 |
24 | # SQLite3 write its data on the local filesystem, as such it requires
25 | # persistent disks. If you are deploying to a managed service, you should
26 | # make sure it provides disk persistence, as many don't.
27 | #
28 | # Similarly, if you deploy your application as a Docker container, you must
29 | # ensure the database is located in a persisted volume.
30 | production:
31 | <<: *default
32 | # database: path/to/persistent/storage/production.sqlite3
33 |
34 | demo:
35 | adapter: postgresql
36 | database: "spectator_sport_demo"
37 | encoding: unicode
38 | pool: 50
39 | url: <%= ENV['DATABASE_URL'] %>
40 |
--------------------------------------------------------------------------------
/demo/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket-<%= Rails.env %>
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket-<%= Rails.env %>
23 |
24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name-<%= Rails.env %>
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/app/controllers/spectator_sport/events_controller.rb:
--------------------------------------------------------------------------------
1 | module SpectatorSport
2 | class EventsController < ApplicationController
3 | skip_before_action :verify_authenticity_token
4 |
5 | def index
6 | end
7 |
8 | def create
9 | data = if params.key?(:sessionId) && params.key?(:windowId) && params.key?(:events)
10 | params.slice(:sessionId, :windowId, :events).stringify_keys
11 | else
12 | # beacon sends JSON in the request body
13 | JSON.parse(request.body.read).slice("sessionId", "windowId", "events")
14 | end
15 |
16 | session_secure_id = data["sessionId"]
17 | window_secure_id = data["windowId"]
18 | events = data["events"]
19 |
20 | session = Session.find_or_create_by(secure_id: session_secure_id)
21 | window = SessionWindow.find_or_create_by(secure_id: window_secure_id, session: session)
22 |
23 | records_data = events.map do |event|
24 | { session_id: session.id, session_window_id: window.id, event_data: event, created_at: Time.at(event["timestamp"].to_f / 1000.0) }
25 | end.to_a
26 | Event.insert_all(records_data)
27 |
28 | last_event = records_data.max_by { |data| data[:created_at] }
29 | window.update(updated_at: last_event[:created_at]) if last_event
30 |
31 | render json: { message: "ok" }
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/app/frontend/spectator_sport/dashboard/modules/theme_controller.js:
--------------------------------------------------------------------------------
1 | // hello_controller.js
2 | import { Controller } from "stimulus"
3 | export default class extends Controller {
4 | static targets = [ "dropdown", "button" ]
5 |
6 | connect() {
7 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
8 | const theme = localStorage.getItem('spectator_sport-theme');
9 | if (!["light", "dark"].includes(theme)) {
10 | this.setTheme(this.autoTheme());
11 | }
12 | });
13 |
14 | this.setTheme(this.getStoredTheme() || 'light');
15 | }
16 |
17 | change(event) {
18 | const theme = event.params.value;
19 | localStorage.setItem('spectator_sport-theme', theme);
20 | this.setTheme(theme);
21 | }
22 |
23 | setTheme(theme) {
24 | document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? this.autoTheme() : theme);
25 |
26 | this.buttonTargets.forEach((button) => {
27 | button.classList.remove('active');
28 | if (button.dataset.themeValueParam === theme) {
29 | button.classList.add('active');
30 | }
31 | });
32 |
33 | const svg = this.buttonTargets.filter(b => b.matches(".active"))[0]?.querySelector('svg');
34 | this.dropdownTarget.querySelector('svg').outerHTML = svg.outerHTML;
35 | }
36 |
37 | getStoredTheme() {
38 | return localStorage.getItem('spectator_sport-theme');
39 | }
40 |
41 | autoTheme() {
42 | return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [ main ]
7 |
8 | jobs:
9 | lint:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v4
14 |
15 | - name: Set up Ruby
16 | uses: ruby/setup-ruby@v1
17 | with:
18 | bundler-cache: true
19 |
20 | - name: Lint code for consistent style
21 | run: bin/rubocop -f github
22 |
23 | test:
24 | runs-on: ubuntu-latest
25 |
26 | # services:
27 | # redis:
28 | # image: redis
29 | # ports:
30 | # - 6379:6379
31 | # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
32 | steps:
33 | - name: Install packages
34 | run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libjemalloc2 libvips sqlite3
35 |
36 | - name: Checkout code
37 | uses: actions/checkout@v4
38 |
39 | - name: Set up Ruby
40 | uses: ruby/setup-ruby@v1
41 | with:
42 | bundler-cache: true
43 |
44 | - name: Run tests
45 | env:
46 | RAILS_ENV: test
47 | run: bin/rails db:test:prepare && bin/rspec
48 |
49 | - name: Keep screenshots from failed system tests
50 | uses: actions/upload-artifact@v4
51 | if: failure()
52 | with:
53 | name: screenshots
54 | path: ${{ github.workspace }}/tmp/screenshots
55 | if-no-files-found: ignore
56 |
57 |
--------------------------------------------------------------------------------
/app/frontend/spectator_sport/dashboard/modules/player_controller.js:
--------------------------------------------------------------------------------
1 | // hello_controller.js
2 | import { Controller } from "stimulus"
3 | import { Player } from 'rrweb-player';
4 |
5 | export default class extends Controller {
6 | static values = {
7 | events: { type: Array, default: [] },
8 | }
9 |
10 | static targets = [ "player", "events" ]
11 |
12 | connect() {
13 | this.player = new Player({
14 | target: this.playerTarget,
15 | props: {
16 | events: this.eventsValue,
17 | liveMode: true,
18 | }
19 | });
20 |
21 | if (window.location.hash) {
22 | const seconds = parseInt(window.location.hash.substring(1));
23 | this.player.getReplayer().play(seconds * 1000);
24 | }
25 |
26 | this.player.addEventListener('ui-update-current-time', (event) => {
27 | const seconds = parseInt(event.payload / 1000);
28 | window.location.hash = seconds;
29 | });
30 |
31 | const playerElement = this.playerTarget.getElementsByClassName("rr-player")[0];
32 | playerElement.style.width = "100%";
33 | playerElement.style.height = null;
34 | playerElement.style.float = "none"
35 | playerElement.style["border-radius"] = "inherit";
36 | playerElement.style["box-shadow"] = "none";
37 | }
38 |
39 | eventsTargetConnected(element) {
40 | if (!this.player) return;
41 |
42 | const events = JSON.parse(element.dataset.events);
43 | events.forEach(event => {
44 | this.player.addEvent(event);
45 | });
46 |
47 | element.remove();
48 | }
49 |
50 | disconnect() {
51 | this.player?.$destroy();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/spec/support/system.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "selenium-webdriver"
4 |
5 | Capybara.default_max_wait_time = 2
6 | Capybara.server = :puma, { Silent: true }
7 | Capybara.disable_animation = true # injects CSP-incompatible CSS and JS
8 |
9 | module SystemTestHelpers
10 | [
11 | :accept_alert,
12 | :dismiss_alert,
13 | :accept_confirm,
14 | :dismiss_confirm,
15 | :accept_prompt,
16 | :dismiss_prompt,
17 | :accept_modal,
18 | :dismiss_modal
19 | ].each do |driver_method|
20 | define_method(driver_method) do |text = nil, **options, &blk|
21 | super(text, **options, &blk)
22 | rescue Capybara::NotSupportedByDriverError
23 | blk.call
24 | end
25 | end
26 | end
27 |
28 | RSpec.configure do |config|
29 | config.include ActionView::RecordIdentifier, type: :system
30 | config.include SystemTestHelpers, type: :system
31 |
32 | config.before(:each, type: :system) do |example|
33 | ActiveRecord::Base.connection.disable_query_cache!
34 |
35 | if ENV['SHOW_BROWSER']
36 | example.metadata[:js] = true
37 | driven_by :selenium, using: :chrome, screen_size: [ 1024, 800 ]
38 | else
39 | driven_by :rack_test
40 | end
41 | end
42 |
43 | config.before(:each, :js, type: :system) do
44 | # Chrome's no-sandbox option is required for running in Docker
45 | driven_by :selenium, using: (ENV['SHOW_BROWSER'] ? :chrome : :headless_chrome), screen_size: [ 1024, 800 ] do |driver_options|
46 | driver_options.add_argument("--disable-dev-shm-usage")
47 | driver_options.add_argument("--no-sandbox")
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/app/views/layouts/spectator_sport/dashboard/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Spectator sport
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 | <%= yield :head %>
9 |
10 | <%# Do not use asset tag helpers to avoid paths being overriden by config.asset_host %>
11 | <%= tag.link rel: "stylesheet", href: frontend_static_path(:bootstrap, format: :css, locale: nil), nonce: content_security_policy_nonce %>
12 | <%= tag.link rel: "stylesheet", href: frontend_static_path(:"rrweb-player", format: :css, locale: nil), nonce: content_security_policy_nonce %>
13 | <%= tag.link rel: "stylesheet", href: frontend_static_path(:style, format: :css, locale: nil), nonce: content_security_policy_nonce %>
14 | <%= tag.script "", src: frontend_static_path(:bootstrap, format: :js, locale: nil), nonce: content_security_policy_nonce %>
15 | <%= tag.script "", src: frontend_static_path(:es_module_shims, format: :js, locale: nil), async: true, nonce: content_security_policy_nonce %>
16 | <% importmaps = SpectatorSport::Dashboard::FrontendsController.js_modules.keys.index_with { |module_name| frontend_module_path(module_name, format: :js, locale: nil) } %>
17 | <%= tag.script({ imports: importmaps }.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce) %>
18 | <%= tag.script "", type: "module", nonce: content_security_policy_nonce do %> import "application"; <% end %>
19 |
20 |
21 |
22 | <%= render "spectator_sport/shared/navbar" %>
23 | <%= yield %>
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/demo/config/puma.rb:
--------------------------------------------------------------------------------
1 | # This configuration file will be evaluated by Puma. The top-level methods that
2 | # are invoked here are part of Puma's configuration DSL. For more information
3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
4 |
5 | # Puma starts a configurable number of processes (workers) and each process
6 | # serves each request in a thread from an internal thread pool.
7 | #
8 | # The ideal number of threads per worker depends both on how much time the
9 | # application spends waiting for IO operations and on how much you wish to
10 | # to prioritize throughput over latency.
11 | #
12 | # As a rule of thumb, increasing the number of threads will increase how much
13 | # traffic a given process can handle (throughput), but due to CRuby's
14 | # Global VM Lock (GVL) it has diminishing returns and will degrade the
15 | # response time (latency) of the application.
16 | #
17 | # The default is set to 3 threads as it's deemed a decent compromise between
18 | # throughput and latency for the average Rails application.
19 | #
20 | # Any libraries that use a connection pool or another resource pool should
21 | # be configured to provide at least as many connections as the number of
22 | # threads. This includes Active Record's `pool` parameter in `database.yml`.
23 | threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
24 | threads threads_count, threads_count
25 |
26 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
27 | port ENV.fetch("PORT", 3000)
28 |
29 | # Allow puma to be restarted by `bin/rails restart` command.
30 | plugin :tmp_restart
31 |
32 | # Specify the PID file. Defaults to tmp/pids/server.pid in development.
33 | # In other environments, only set the PID file if requested.
34 | pidfile ENV["PIDFILE"] if ENV["PIDFILE"]
35 |
--------------------------------------------------------------------------------
/demo/public/406-unsupported-browser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Your browser is not supported (406)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
Your browser is not supported.
62 |
Please upgrade your browser to continue.
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/demo/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
We're sorry, but something went wrong.
64 |
65 |
If you are the application owner check the logs for more information.
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/demo/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
The change you wanted was rejected.
64 |
Maybe you tried to change something you didn't have access to.
65 |
66 |
If you are the application owner check the logs for more information.
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/app/frontend/spectator_sport/dashboard/style.css:
--------------------------------------------------------------------------------
1 | .tooltip {
2 | position: absolute;
3 | z-index: 1;
4 | padding: 5px;
5 | background: rgba(0, 0, 0, 0.3);
6 | opacity: 1;
7 | border-radius: 3px;
8 | text-align: center;
9 | pointer-events: none;
10 | color: white;
11 | transition: opacity .1s ease-out;
12 | }
13 |
14 | .tooltip.tooltip-hidden {
15 | opacity: 0;
16 | }
17 |
18 | .ct-label.ct-horizontal {
19 | white-space: nowrap;
20 | }
21 |
22 | .chart-wrapper {
23 | position: relative;
24 | height: 200px;
25 | }
26 |
27 | .legend-item-color-box {
28 | display: inline-block;
29 | flex-shrink: 0;
30 | height: 20px;
31 | width: 20px;
32 | border-style: solid;
33 | margin-right: 3px;
34 | }
35 |
36 | #chart-legend-container {
37 | height: 200px;
38 | }
39 |
40 | /* Break out of a container */
41 | .break-out {
42 | width:100vw;
43 | position:relative;
44 | left:calc(-1 * (100vw - 100%)/2);
45 | }
46 |
47 | .toast-container {
48 | z-index: 1;
49 | }
50 |
51 | .btn-outline-secondary {
52 | border-color: #ced4da; /* $gray-400 */
53 | }
54 |
55 | .min-w-auto {
56 | min-width: auto;
57 | }
58 |
59 | .w-fit-content {
60 | width: fit-content
61 | }
62 |
63 | .svg-icon {
64 | height: 1rem;
65 | width: 1rem;
66 | }
67 |
68 | /*Style table within card*/
69 | .card > table:first-child > thead > tr:first-child > th:first-child {
70 | border-top-left-radius: var(--bs-card-inner-border-radius);
71 | }
72 | .card > table:first-child > thead > tr:first-child > th:last-child {
73 | border-top-right-radius: var(--bs-card-inner-border-radius);
74 | }
75 | .card > table:last-child > tbody > tr:last-child > td:first-child {
76 | border-bottom-left-radius: var(--bs-card-inner-border-radius);
77 | }
78 | .card > table:last-child > tbody > tr:last-child > td:last-child {
79 | border-bottom-right-radius: var(--bs-card-inner-border-radius);
80 | }
81 | .card > table {
82 | margin-bottom: var(--bs-card-border-width, 1px);
83 | }
84 | .card > table > tbody > tr:last-child > td {
85 | border-bottom: none;
86 | box-shadow: none;
87 | }
88 |
--------------------------------------------------------------------------------
/demo/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
The page you were looking for doesn't exist.
64 |
You may have mistyped the address or the page may have moved.
65 |
66 |
If you are the application owner check the logs for more information.
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/app/views/spectator_sport/dashboard/dashboards/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if @session_windows.to_a.any? %>
3 |
4 |
5 | List of screen recordings
6 |
7 |
8 | Recording
9 | Session ID
10 | Updated
11 | Created
12 | Actions
13 |
14 |
15 |
16 | <% @session_windows.each do |session_window| %>
17 |
18 | <%= link_to "Session (Window ##{session_window.id})", session_window_path(session_window.id) %>
19 | <%= session_window.session_id %>
20 | <%= tag.time(time_ago_in_words(session_window.updated_at), datetime: session_window.updated_at, title: session_window.updated_at) %>
21 | <%= tag.time(time_ago_in_words(session_window.created_at), datetime: session_window.created_at, title: session_window.created_at) %>
22 |
23 |
24 | <%= render_icon "dots" %>
25 | Actions
26 |
27 |
28 |
29 | <%= link_to session_window_path(session_window.id), method: :put, class: "dropdown-item", title: "Destroy", data: { "turbo-method": :delete, "turbo-confirm": "Destroy this recording?" } do %>
30 | <%= render_icon "eject" %>
31 | Destroy
32 | <% end %>
33 |
34 |
35 |
36 |
37 | <% end %>
38 |
39 |
40 |
41 | <% else %>
42 |
No recordings found.
43 | <% end %>
44 |
45 |
--------------------------------------------------------------------------------
/spec/support/shell_out.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'open3'
4 |
5 | class ShellOut
6 | WaitTimeout = Class.new(StandardError)
7 | KILL_TIMEOUT = 5
8 | PROCESS_EXIT = "[PROCESS EXIT]"
9 |
10 | def self.command(command, env: {}, &block)
11 | new.command(command, env: env, &block)
12 | end
13 |
14 | attr_reader :output
15 |
16 | def initialize
17 | @output = Concurrent::Array.new
18 | end
19 |
20 | def command(command, env: {})
21 | all_env = ENV.to_h.merge(env)
22 | Open3.popen3(all_env, command, chdir: Rails.root) do |stdin, stdout, stderr, wait_thr|
23 | pid = wait_thr.pid
24 | stdin.close
25 |
26 | stdout_future = Concurrent::Promises.future(stdout, @output) do |fstdout, foutput|
27 | loop do
28 | line = fstdout.gets
29 | break unless line
30 |
31 | Rails.logger.debug { "STDOUT: #{line}" }
32 | foutput << line
33 | end
34 | end
35 | stderr_future = Concurrent::Promises.future(stderr, @output) do |fstderr, foutput|
36 | loop do
37 | line = fstderr.gets
38 | break unless line
39 |
40 | Rails.logger.debug { "STDERR: #{line}" }
41 | foutput << line
42 | end
43 | end
44 |
45 | begin
46 | yield(self)
47 | ensure
48 | begin
49 | Rails.logger.debug { "Sending TERM to #{pid}" }
50 | Process.kill('TERM', pid)
51 |
52 | Concurrent::Promises.future(pid, @output) do |fpid|
53 | sleep 5
54 | Process.kill('KILL', fpid)
55 | rescue Errno::ECHILD, Errno::ESRCH
56 | nil
57 | else
58 | Rails.logger.debug { "TERM unsuccessful, sent KILL to #{pid}" }
59 | end
60 |
61 | Process.wait(pid)
62 | rescue Errno::ECHILD, Errno::ESRCH
63 | @output << PROCESS_EXIT
64 | end
65 | end
66 | status = wait_thr.value
67 | stdout_future.value
68 | stderr_future.value
69 |
70 | Rails.logger.debug { "Command finished: #{status}" }
71 | @output
72 | end
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/demo/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # This file is the source Rails uses to define your schema when running `bin/rails
6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7 | # be faster and is potentially less error prone than running all of your
8 | # migrations from scratch. Old migrations may fail to apply correctly if those
9 | # migrations use external dependencies or application code.
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema[7.2].define(version: 2024_09_23_140845) do
14 | create_table "spectator_sport_events", force: :cascade do |t|
15 | t.datetime "created_at", null: false
16 | t.datetime "updated_at", null: false
17 | t.integer "session_id", null: false
18 | t.integer "session_window_id"
19 | t.json "event_data", null: false
20 | t.index [ "session_id", "created_at" ], name: "index_spectator_sport_events_on_session_id_and_created_at"
21 | t.index [ "session_id" ], name: "index_spectator_sport_events_on_session_id"
22 | t.index [ "session_window_id", "created_at" ], name: "idx_on_session_window_id_created_at_f1aab0a880"
23 | t.index [ "session_window_id" ], name: "index_spectator_sport_events_on_session_window_id"
24 | end
25 |
26 | create_table "spectator_sport_session_windows", force: :cascade do |t|
27 | t.datetime "created_at", null: false
28 | t.datetime "updated_at", null: false
29 | t.integer "session_id", null: false
30 | t.string "secure_id", null: false
31 | t.index [ "session_id" ], name: "index_spectator_sport_session_windows_on_session_id"
32 | end
33 |
34 | create_table "spectator_sport_sessions", force: :cascade do |t|
35 | t.datetime "created_at", null: false
36 | t.datetime "updated_at", null: false
37 | t.string "secure_id", null: false
38 | t.index [ "secure_id", "created_at" ], name: "index_spectator_sport_sessions_on_secure_id_and_created_at"
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/app/controllers/spectator_sport/dashboard/frontends_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SpectatorSport
4 | module Dashboard
5 | class FrontendsController < ActionController::Base # rubocop:disable Rails/ApplicationController
6 | protect_from_forgery with: :exception
7 | skip_after_action :verify_same_origin_request, raise: false
8 |
9 | def self.asset_path(path)
10 | Engine.root.join("app/frontend/spectator_sport/dashboard", path)
11 | end
12 |
13 | STATIC_ASSETS = {
14 | css: {
15 | bootstrap: asset_path("vendor/bootstrap/bootstrap.min.css"),
16 | "rrweb-player": asset_path("vendor/rrweb-player/rrweb-player.min.css"),
17 | style: asset_path("style.css")
18 | },
19 | js: {
20 | bootstrap: asset_path("vendor/bootstrap/bootstrap.bundle.min.js"),
21 | es_module_shims: asset_path("vendor/es_module_shims.js")
22 | },
23 | svg: {
24 | icons: asset_path("icons.svg")
25 | }
26 | }.freeze
27 |
28 | MODULE_OVERRIDES = {
29 | application: asset_path("application.js"),
30 | "rrweb-player": asset_path("vendor/rrweb-player/rrweb-player.min.js"),
31 | stimulus: asset_path("vendor/stimulus.js"),
32 | turbo: asset_path("vendor/turbo.js")
33 | }.freeze
34 |
35 | def self.js_modules
36 | @_js_modules ||= asset_path("modules").children.select(&:file?).each_with_object({}) do |file, modules|
37 | key = File.basename(file.basename.to_s, ".js").to_sym
38 | modules[key] = file
39 | end.merge(MODULE_OVERRIDES)
40 | end
41 |
42 | before_action do
43 | expires_in 1.year, public: true
44 | end
45 |
46 | def static
47 | file_path = STATIC_ASSETS.dig(params[:format]&.to_sym, params[:id]&.to_sym) || raise(ActionController::RoutingError, "Not Found")
48 | send_file file_path, disposition: "inline"
49 | end
50 |
51 | def module
52 | raise(ActionController::RoutingError, "Not Found") if params[:format] != "js"
53 |
54 | file_path = self.class.js_modules[params[:id]&.to_sym] || raise(ActionController::RoutingError, "Not Found")
55 | send_file file_path, disposition: "inline"
56 | end
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/spec/support/pre_documentation_formatter.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec::Support.require_rspec_core "formatters/base_text_formatter"
4 | RSpec::Support.require_rspec_core "formatters/console_codes"
5 |
6 | class PreDocumentationFormatter < RSpec::Core::Formatters::BaseTextFormatter
7 | RSpec::Core::Formatters.register self, :example_started, :example_group_started, :example_group_finished,
8 | :example_passed, :example_pending, :example_failed
9 |
10 | def initialize(output)
11 | super
12 | @group_level = 0
13 | @failed_examples = []
14 | end
15 |
16 | def example_group_started(notification)
17 | output.puts if @group_level == 0
18 | output.puts "#{current_indentation}#{notification.group.description.strip}"
19 |
20 | @group_level += 1
21 | end
22 |
23 | def example_group_finished(_notification)
24 | @group_level -= 1 if @group_level > 0
25 | end
26 |
27 | def example_passed(passed)
28 | output.puts passed_output(passed.example)
29 | end
30 |
31 | def example_pending(pending)
32 | output.puts pending_output(pending.example,
33 | pending.example.execution_result.pending_message)
34 | end
35 |
36 | def example_failed(failure)
37 | @failed_examples << failure.example
38 | output.puts failure_output(failure.example)
39 | output.puts failure.fully_formatted(@failed_examples.size)
40 | end
41 |
42 | def example_started(notification)
43 | output.puts "#{current_indentation}RUNNING: #{notification.example.description}"
44 | end
45 |
46 | private
47 |
48 | def passed_output(example)
49 | RSpec::Core::Formatters::ConsoleCodes.wrap("#{current_indentation}#{example.description.strip}", :success)
50 | end
51 |
52 | def pending_output(example, message)
53 | RSpec::Core::Formatters::ConsoleCodes.wrap("#{current_indentation}#{example.description.strip} (PENDING: #{message})", :pending)
54 | end
55 |
56 | def failure_output(example)
57 | RSpec::Core::Formatters::ConsoleCodes.wrap("#{current_indentation}#{example.description.strip} (FAILED - #{next_failure_index})", :failure)
58 | end
59 |
60 | def next_failure_index
61 | @next_failure_index ||= 0
62 | @next_failure_index += 1
63 | end
64 |
65 | def current_indentation
66 | ' ' * @group_level
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/app/views/spectator_sport/shared/_navbar.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= link_to "⚽️ Spectator Sport", root_path, class: "navbar-brand mb-0 h1" %>
4 |
5 |
6 |
7 | <%= render_icon "globe", class: "align-text-bottom" %>
8 | <%= Rails.env.capitalize %>
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | <%= render_icon "globe", class: "align-text-bottom" %>
21 | <%= Rails.env.capitalize %>
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | <%= render_icon "circle_half" %>
33 | <%= t(".theme.theme") %>
34 |
35 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file is copied to spec/ when you run 'rails generate rspec:install'
4 | require 'spec_helper'
5 | ENV['RAILS_ENV'] ||= 'test'
6 |
7 | require File.expand_path('../demo/config/environment', __dir__)
8 |
9 | # Prevent database truncation if the environment is production
10 | abort("The Rails environment is running in production mode!") if Rails.env.production?
11 | require 'rspec/rails'
12 | # Add additional requires below this line. Rails is not loaded until this point!
13 |
14 | # Requires supporting ruby files with custom matchers and macros, etc, in
15 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
16 | # run as spec files by default. This means that files in spec/support that end
17 | # in _spec.rb will both be required and run as specs, causing the specs to be
18 | # run twice. It is recommended that you do not name files matching this glob to
19 | # end with _spec.rb. You can configure this pattern with the --pattern
20 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
21 | #
22 | # The following line is provided for convenience purposes. It has the downside
23 | # of increasing the boot-up time by auto-requiring all files in the support
24 | # directory. Alternatively, in the individual `*_spec.rb` files, manually
25 | # require only the support files necessary.
26 | #
27 | Dir[File.join(File.dirname(__FILE__), 'support', '**', '*.rb')].sort.each { |f| require f }
28 |
29 | RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 10_000
30 |
31 | # Checks for pending migrations and applies them before tests are run.
32 | # If you are not using ActiveRecord, you can remove these lines.
33 | begin
34 | ActiveRecord::Migration.maintain_test_schema!
35 | rescue ActiveRecord::PendingMigrationError => e
36 | puts e.to_s.strip
37 | exit 1
38 | end
39 | RSpec.configure do |config|
40 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
41 | if config.respond_to? :fixture_paths
42 | config.fixture_paths = Rails.root.join("spec/fixtures")
43 | else
44 | config.fixture_path = Rails.root.join("spec/fixtures")
45 | end
46 |
47 | # RSpec Rails can automatically mix in different behaviours to your tests
48 | # based on their file location, for example enabling you to call `get` and
49 | # `post` in specs under `spec/controllers`.
50 | #
51 | # You can disable this behaviour by removing the line below, and instead
52 | # explicitly tag your specs with their type, e.g.:
53 | #
54 | # RSpec.describe UsersController, :type => :controller do
55 | # # ...
56 | # end
57 | #
58 | # The different available types are documented in the features, such as in
59 | # https://relishapp.com/rspec/rspec-rails/docs
60 | config.infer_spec_type_from_file_location!
61 |
62 | # Filter lines from Rails gems in backtraces.
63 | config.filter_rails_from_backtrace!
64 | # arbitrary gems may also be filtered via:
65 | # config.filter_gems_from_backtrace("gem name")
66 | end
67 |
--------------------------------------------------------------------------------
/demo/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # In the development environment your application's code is reloaded any time
7 | # it changes. This slows down response time but is perfect for development
8 | # since you don't have to restart the web server when you make code changes.
9 | config.enable_reloading = true
10 |
11 | # Do not eager load code on boot.
12 | config.eager_load = false
13 |
14 | # Show full error reports.
15 | config.consider_all_requests_local = true
16 |
17 | # Enable server timing.
18 | config.server_timing = true
19 |
20 | # Enable/disable caching. By default caching is disabled.
21 | # Run rails dev:cache to toggle caching.
22 | if Rails.root.join("tmp/caching-dev.txt").exist?
23 | config.action_controller.perform_caching = true
24 | config.action_controller.enable_fragment_cache_logging = true
25 |
26 | config.cache_store = :memory_store
27 | config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" }
28 | else
29 | config.action_controller.perform_caching = false
30 |
31 | config.cache_store = :null_store
32 | end
33 |
34 | # Store uploaded files on the local file system (see config/storage.yml for options).
35 | config.active_storage.service = :local
36 |
37 | # Don't care if the mailer can't send.
38 | config.action_mailer.raise_delivery_errors = false
39 |
40 | # Disable caching for Action Mailer templates even if Action Controller
41 | # caching is enabled.
42 | config.action_mailer.perform_caching = false
43 |
44 | config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
45 |
46 | # Print deprecation notices to the Rails logger.
47 | config.active_support.deprecation = :log
48 |
49 | # Raise exceptions for disallowed deprecations.
50 | config.active_support.disallowed_deprecation = :raise
51 |
52 | # Tell Active Support which deprecation messages to disallow.
53 | config.active_support.disallowed_deprecation_warnings = []
54 |
55 | # Raise an error on page load if there are pending migrations.
56 | config.active_record.migration_error = :page_load
57 |
58 | # Highlight code that triggered database queries in logs.
59 | config.active_record.verbose_query_logs = true
60 |
61 | # Highlight code that enqueued background job in logs.
62 | config.active_job.verbose_enqueue_logs = true
63 |
64 | # Suppress logger output for asset requests.
65 | config.assets.quiet = true
66 |
67 | # Raises error for missing translations.
68 | # config.i18n.raise_on_missing_translations = true
69 |
70 | # Annotate rendered view with file names.
71 | config.action_view.annotate_rendered_view_with_filenames = true
72 |
73 | # Uncomment if you wish to allow Action Cable access from any origin.
74 | # config.action_cable.disable_request_forgery_protection = true
75 |
76 | # Raise error when a before_action's only/except options reference missing actions.
77 | config.action_controller.raise_on_missing_callback_actions = true
78 | end
79 |
--------------------------------------------------------------------------------
/demo/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | # While tests run files are not watched, reloading is not necessary.
12 | config.enable_reloading = false
13 |
14 | # Eager loading loads your entire application. When running a single test locally,
15 | # this is usually not necessary, and can slow down your test suite. However, it's
16 | # recommended that you enable it in continuous integration systems to ensure eager
17 | # loading is working properly before deploying your code.
18 | config.eager_load = ENV["CI"].present?
19 |
20 | # Configure public file server for tests with Cache-Control for performance.
21 | config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" }
22 |
23 | # Show full error reports and disable caching.
24 | config.consider_all_requests_local = true
25 | config.action_controller.perform_caching = false
26 | config.cache_store = :null_store
27 |
28 | # Render exception templates for rescuable exceptions and raise for other exceptions.
29 | config.action_dispatch.show_exceptions = :rescuable
30 |
31 | # Disable request forgery protection in test environment.
32 | config.action_controller.allow_forgery_protection = false
33 |
34 | # Store uploaded files on the local file system in a temporary directory.
35 | config.active_storage.service = :test
36 |
37 | # Disable caching for Action Mailer templates even if Action Controller
38 | # caching is enabled.
39 | config.action_mailer.perform_caching = false
40 |
41 | # Tell Action Mailer not to deliver emails to the real world.
42 | # The :test delivery method accumulates sent emails in the
43 | # ActionMailer::Base.deliveries array.
44 | config.action_mailer.delivery_method = :test
45 |
46 | # Unlike controllers, the mailer instance doesn't have any context about the
47 | # incoming request so you'll need to provide the :host parameter yourself.
48 | config.action_mailer.default_url_options = { host: "www.example.com" }
49 |
50 | # Print deprecation notices to the stderr.
51 | config.active_support.deprecation = :stderr
52 |
53 | # Raise exceptions for disallowed deprecations.
54 | config.active_support.disallowed_deprecation = :raise
55 |
56 | # Tell Active Support which deprecation messages to disallow.
57 | config.active_support.disallowed_deprecation_warnings = []
58 |
59 | # Raises error for missing translations.
60 | # config.i18n.raise_on_missing_translations = true
61 |
62 | # Annotate rendered view with file names.
63 | # config.action_view.annotate_rendered_view_with_filenames = true
64 |
65 | # Raise error when a before_action's only/except options reference missing actions.
66 | config.action_controller.raise_on_missing_callback_actions = true
67 | end
68 |
--------------------------------------------------------------------------------
/spec/support/example_app_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'fileutils'
4 |
5 | module ExampleAppHelper
6 | def setup_example_app
7 | FileUtils.rm_rf(example_app_path)
8 |
9 | # Rails will not install within a directory containing `bin/rails`
10 | Rails.root.join("../bin/rails").rename(Rails.root.join("../bin/_rails")) if Rails.root.join("../bin/rails").exist?
11 |
12 | root_path = example_app_path.join('..')
13 | FileUtils.cd(root_path) do
14 | system("rails new #{app_name} -d postgresql --no-assets --skip-action-text --skip-action-mailer --skip-action-mailbox --skip-action-cable --skip-git --skip-sprockets --skip-listen --skip-javascript --skip-turbolinks --skip-system-test --skip-test-unit --skip-bootsnap --skip-spring --skip-active-storage")
15 | end
16 |
17 | FileUtils.rm_rf("#{example_app_path}/config/initializers/assets.rb")
18 | FileUtils.cp(::Rails.root.join('config/database.yml'), "#{example_app_path}/config/database.yml")
19 |
20 | File.open("#{example_app_path}/Gemfile", 'a') do |f|
21 | f.puts 'gem "spectator_sport", path: "#{File.dirname(__FILE__)}/../../../"' # rubocop:disable Lint/InterpolationCheck
22 | end
23 | end
24 |
25 | def teardown_example_app
26 | Rails.root.join("../bin/_rails").rename(Rails.root.join("../bin/rails"))
27 | FileUtils.rm_rf(example_app_path)
28 | end
29 |
30 | def run_in_example_app(*args)
31 | FileUtils.cd(example_app_path) do
32 | system(*args) || raise("Command #{args} failed")
33 | end
34 | end
35 |
36 | def run_in_demo_app(*args)
37 | FileUtils.cd(Rails.root) do
38 | system(*args) || raise("Command #{args} failed")
39 | end
40 | end
41 |
42 | def within_example_app
43 | # Will be running database migrations from the newly created Example App
44 | # but doing so in the existing database. This resets the database so that
45 | # newly created migrations can be run, then resets it back.
46 | #
47 | # Ideally this would happen in a different database, but that seemed like
48 | # a lot of work to do in Github Actions.
49 | models = [
50 | SpectatorSport::Event,
51 | SpectatorSport::Session,
52 | SpectatorSport::SessionWindow
53 | ]
54 | quiet do
55 | models.each do |model|
56 | table_name = model.table_name
57 | ActiveRecord::Migration.drop_table(table_name) if ActiveRecord::Base.connection.table_exists?(table_name)
58 | end
59 | ActiveRecord::Base.connection.execute("TRUNCATE schema_migrations")
60 |
61 | setup_example_app
62 | run_in_demo_app("bin/rails db:environment:set RAILS_ENV=test")
63 | models.each(&:reset_column_information)
64 | end
65 |
66 | yield
67 | ensure
68 | quiet do
69 | teardown_example_app
70 |
71 | tables.each do |table_name|
72 | ActiveRecord::Migration.drop_table(table_name) if ActiveRecord::Base.connection.table_exists?(table_name)
73 | end
74 | ActiveRecord::Base.connection.execute("TRUNCATE schema_migrations")
75 |
76 | run_in_demo_app("bin/rails db:schema:load db:environment:set RAILS_ENV=test")
77 | models.each(&:reset_column_information)
78 | end
79 | end
80 |
81 | def example_app_path
82 | Rails.root.join('../tmp', app_name)
83 | end
84 |
85 | def app_name
86 | 'example_app'
87 | end
88 | end
89 |
90 | RSpec.configure { |c| c.include ExampleAppHelper, type: :generator }
91 |
--------------------------------------------------------------------------------
/app/frontend/spectator_sport/dashboard/vendor/rrweb-player/rrweb-player.min.css:
--------------------------------------------------------------------------------
1 | // https://cdn.jsdelivr.net/npm/rrweb-player@2.0.0-alpha.18/dist/style.min.css
2 | .switch.svelte-a6h7w7.svelte-a6h7w7.svelte-a6h7w7{height:1em;display:flex;align-items:center}.switch.disabled.svelte-a6h7w7.svelte-a6h7w7.svelte-a6h7w7{opacity:.5}.label.svelte-a6h7w7.svelte-a6h7w7.svelte-a6h7w7{margin:0 8px}.switch.svelte-a6h7w7 input[type=checkbox].svelte-a6h7w7.svelte-a6h7w7{position:absolute;opacity:0}.switch.svelte-a6h7w7 label.svelte-a6h7w7.svelte-a6h7w7{width:2em;height:1em;position:relative;cursor:pointer;display:block}.switch.disabled.svelte-a6h7w7 label.svelte-a6h7w7.svelte-a6h7w7{cursor:not-allowed}.switch.svelte-a6h7w7 label.svelte-a6h7w7.svelte-a6h7w7:before{content:"";position:absolute;width:2em;height:1em;left:.1em;transition:background .1s ease;background:#4950f680;border-radius:50px}.switch.svelte-a6h7w7 label.svelte-a6h7w7.svelte-a6h7w7:after{content:"";position:absolute;width:1em;height:1em;border-radius:50px;left:0;transition:all .2s ease;box-shadow:0 2px 5px #0000004d;background:#fcfff4;animation:switch-off .2s ease-out;z-index:2}.switch.svelte-a6h7w7 input[type=checkbox].svelte-a6h7w7:checked+label.svelte-a6h7w7:before{background:#4950f6}.switch.svelte-a6h7w7 input[type=checkbox].svelte-a6h7w7:checked+label.svelte-a6h7w7:after{animation:switch-on .2s ease-out;left:1.1em}.rr-controller.svelte-189zk2r.svelte-189zk2r{width:100%;height:80px;background:#fff;display:flex;flex-direction:column;justify-content:space-around;align-items:center;border-radius:0 0 5px 5px}.rr-timeline.svelte-189zk2r.svelte-189zk2r{width:80%;display:flex;align-items:center}.rr-timeline__time.svelte-189zk2r.svelte-189zk2r{display:inline-block;width:100px;text-align:center;color:#11103e}.rr-progress.svelte-189zk2r.svelte-189zk2r{flex:1;height:12px;background:#eee;position:relative;border-radius:3px;cursor:pointer;box-sizing:border-box;border-top:solid 4px #fff;border-bottom:solid 4px #fff}.rr-progress.disabled.svelte-189zk2r.svelte-189zk2r{cursor:not-allowed}.rr-progress__step.svelte-189zk2r.svelte-189zk2r{height:100%;position:absolute;left:0;top:0;background:#e0e1fe}.rr-progress__handler.svelte-189zk2r.svelte-189zk2r{width:20px;height:20px;border-radius:10px;position:absolute;top:2px;transform:translate(-50%,-50%);background:#4950f6}.rr-controller__btns.svelte-189zk2r.svelte-189zk2r{display:flex;align-items:center;justify-content:center;font-size:13px}.rr-controller__btns.svelte-189zk2r button.svelte-189zk2r{width:32px;height:32px;display:flex;padding:0;align-items:center;justify-content:center;background:none;border:none;border-radius:50%;cursor:pointer}.rr-controller__btns.svelte-189zk2r button.svelte-189zk2r:active{background:#e0e1fe}.rr-controller__btns.svelte-189zk2r button.active.svelte-189zk2r{color:#fff;background:#4950f6}.rr-controller__btns.svelte-189zk2r button.svelte-189zk2r:disabled{cursor:not-allowed}.replayer-wrapper{position:relative}.replayer-mouse{position:absolute;width:20px;height:20px;transition:left .05s linear,top .05s linear;background-size:contain;background-position:center center;background-repeat:no-repeat;background-image:url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDUwIDUwIiB4PSIwcHgiIHk9IjBweCI+PHRpdGxlPkRlc2lnbl90bnA8L3RpdGxlPjxwYXRoIGQ9Ik00OC43MSw0Mi45MUwzNC4wOCwyOC4yOSw0NC4zMywxOEExLDEsMCwwLDAsNDQsMTYuMzlMMi4zNSwxLjA2QTEsMSwwLDAsMCwxLjA2LDIuMzVMMTYuMzksNDRhMSwxLDAsMCwwLDEuNjUuMzZMMjguMjksMzQuMDgsNDIuOTEsNDguNzFhMSwxLDAsMCwwLDEuNDEsMGw0LjM4LTQuMzhBMSwxLDAsMCwwLDQ4LjcxLDQyLjkxWm0tNS4wOSwzLjY3TDI5LDMyYTEsMSwwLDAsMC0xLjQxLDBsLTkuODUsOS44NUwzLjY5LDMuNjlsMzguMTIsMTRMMzIsMjcuNThBMSwxLDAsMCwwLDMyLDI5TDQ2LjU5LDQzLjYyWiI+PC9wYXRoPjwvc3ZnPg==);border-color:transparent}.replayer-mouse:after{content:"";display:inline-block;width:20px;height:20px;background:#4950f6;border-radius:100%;transform:translate(-50%,-50%);opacity:.3}.replayer-mouse.active:after{animation:click .2s ease-in-out 1}.replayer-mouse.touch-device{background-image:none;width:70px;height:70px;border-width:4px;border-style:solid;border-radius:100%;margin-left:-37px;margin-top:-37px;border-color:#4950f600;transition:left 0s linear,top 0s linear,border-color .2s ease-in-out}.replayer-mouse.touch-device.touch-active{border-color:#4950f6;transition:left .25s linear,top .25s linear,border-color .2s ease-in-out}.replayer-mouse.touch-device:after{opacity:0}.replayer-mouse.touch-device.active:after{animation:touch-click .2s ease-in-out 1}.replayer-mouse-tail{position:absolute;pointer-events:none}@keyframes click{0%{opacity:.3;width:20px;height:20px}50%{opacity:.5;width:10px;height:10px}}@keyframes touch-click{0%{opacity:0;width:20px;height:20px}50%{opacity:.5;width:10px;height:10px}}.rr-player{position:relative;background:#fff;float:left;border-radius:5px;box-shadow:0 24px 48px #11103e1f}.rr-player__frame{overflow:hidden}.replayer-wrapper{float:left;clear:both;transform-origin:top left;left:50%;top:50%}.replayer-wrapper>iframe{border:none}
3 |
--------------------------------------------------------------------------------
/demo/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # Code is not reloaded between requests.
7 | config.enable_reloading = false
8 |
9 | # Eager load code on boot. This eager loads most of Rails and
10 | # your application in memory, allowing both threaded web servers
11 | # and those relying on copy on write to perform better.
12 | # Rake tasks automatically ignore this option for performance.
13 | config.eager_load = true
14 |
15 | # Full error reports are disabled and caching is turned on.
16 | config.consider_all_requests_local = false
17 | config.action_controller.perform_caching = true
18 |
19 | # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment
20 | # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files).
21 | # config.require_master_key = true
22 |
23 | # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead.
24 | # config.public_file_server.enabled = false
25 |
26 | # Compress CSS using a preprocessor.
27 | # config.assets.css_compressor = :sass
28 |
29 | # Do not fall back to assets pipeline if a precompiled asset is missed.
30 | config.assets.compile = false
31 |
32 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
33 | # config.asset_host = "http://assets.example.com"
34 |
35 | # Specifies the header that your server uses for sending files.
36 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
37 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
38 |
39 | # Store uploaded files on the local file system (see config/storage.yml for options).
40 | config.active_storage.service = :local
41 |
42 | # Mount Action Cable outside main process or domain.
43 | # config.action_cable.mount_path = nil
44 | # config.action_cable.url = "wss://example.com/cable"
45 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
46 |
47 | # Assume all access to the app is happening through a SSL-terminating reverse proxy.
48 | # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies.
49 | # config.assume_ssl = true
50 |
51 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
52 | config.force_ssl = true
53 |
54 | # Skip http-to-https redirect for the default health check endpoint.
55 | # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
56 |
57 | # Log to STDOUT by default
58 | config.logger = ActiveSupport::Logger.new(STDOUT)
59 | .tap { |logger| logger.formatter = ::Logger::Formatter.new }
60 | .then { |logger| ActiveSupport::TaggedLogging.new(logger) }
61 |
62 | # Prepend all log lines with the following tags.
63 | config.log_tags = [ :request_id ]
64 |
65 | # "info" includes generic and useful information about system operation, but avoids logging too much
66 | # information to avoid inadvertent exposure of personally identifiable information (PII). If you
67 | # want to log everything, set the level to "debug".
68 | config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
69 |
70 | # Use a different cache store in production.
71 | # config.cache_store = :mem_cache_store
72 |
73 | # Use a real queuing backend for Active Job (and separate queues per environment).
74 | # config.active_job.queue_adapter = :resque
75 | # config.active_job.queue_name_prefix = "spectator_sport_production"
76 |
77 | # Disable caching for Action Mailer templates even if Action Controller
78 | # caching is enabled.
79 | config.action_mailer.perform_caching = false
80 |
81 | # Ignore bad email addresses and do not raise email delivery errors.
82 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
83 | # config.action_mailer.raise_delivery_errors = false
84 |
85 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
86 | # the I18n.default_locale when a translation cannot be found).
87 | config.i18n.fallbacks = true
88 |
89 | # Don't log any deprecations.
90 | config.active_support.report_deprecations = false
91 |
92 | # Do not dump schema after migrations.
93 | config.active_record.dump_schema_after_migration = false
94 |
95 | # Enable DNS rebinding protection and other `Host` header attacks.
96 | # config.hosts = [
97 | # "example.com", # Allow requests from example.com
98 | # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
99 | # ]
100 | # Skip DNS rebinding protection for the default health check endpoint.
101 | # config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
102 | end
103 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "warning"
4 | Warning.ignore([ :not_reached, :unused_var ], /.*lib\/mail\/parser.*/)
5 |
6 |
7 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all
8 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
9 | # The generated `.rspec` file contains `--require spec_helper` which will cause
10 | # this file to always be loaded, without a need to explicitly require it in any
11 | # files.
12 | #
13 | # Given that it is always loaded, you are encouraged to keep this file as
14 | # light-weight as possible. Requiring heavyweight dependencies from this file
15 | # will add to the boot time of your test suite on EVERY test run, even for an
16 | # individual file that may not need all of that loaded. Instead, consider making
17 | # a separate helper file that requires the additional dependencies and performs
18 | # the additional setup, and require it from the spec files that actually need
19 | # it.
20 | #
21 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
22 | RSpec.configure do |config|
23 | # rspec-expectations config goes here. You can use an alternate
24 | # assertion/expectation library such as wrong or the stdlib/minitest
25 | # assertions if you prefer.
26 | config.expect_with :rspec do |expectations|
27 | # This option will default to `true` in RSpec 4. It makes the `description`
28 | # and `failure_message` of custom matchers include text for helper methods
29 | # defined using `chain`, e.g.:
30 | # be_bigger_than(2).and_smaller_than(4).description
31 | # # => "be bigger than 2 and smaller than 4"
32 | # ...rather than:
33 | # # => "be bigger than 2"
34 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
35 | end
36 |
37 | # rspec-mocks config goes here. You can use an alternate test double
38 | # library (such as bogus or mocha) by changing the `mock_with` option here.
39 | config.mock_with :rspec do |mocks|
40 | # Prevents you from mocking or stubbing a method that does not exist on
41 | # a real object. This is generally recommended, and will default to
42 | # `true` in RSpec 4.
43 | mocks.verify_partial_doubles = true
44 | end
45 |
46 | config.warnings = true
47 |
48 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
49 | # have no way to turn it off -- the option exists only for backwards
50 | # compatibility in RSpec 3). It causes shared context metadata to be
51 | # inherited by the metadata hash of host groups and examples, rather than
52 | # triggering implicit auto-inclusion in groups with matching metadata.
53 | config.shared_context_metadata_behavior = :apply_to_host_groups
54 |
55 | # Allows RSpec to persist some state between runs in order to support
56 | # the `--only-failures` and `--next-failure` CLI options. We recommend
57 | # you configure your source control system to ignore this file.
58 | config.example_status_persistence_file_path = "tmp/rspec_examples.txt"
59 |
60 | # The settings below are suggested to provide a good initial experience
61 | # with RSpec, but feel free to customize to your heart's content.
62 | # # This allows you to limit a spec run to individual examples or groups
63 | # # you care about by tagging them with `:focus` metadata. When nothing
64 | # # is tagged with `:focus`, all examples get run. RSpec also provides
65 | # # aliases for `it`, `describe`, and `context` that include `:focus`
66 | # # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
67 | # config.filter_run_when_matching :focus
68 | #
69 | # # Limits the available syntax to the non-monkey patched syntax that is
70 | # # recommended. For more details, see:
71 | # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
72 | # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
73 | # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
74 | # config.disable_monkey_patching!
75 | #
76 | # # Many RSpec users commonly either run the entire suite or an individual
77 | # # file, and it's useful to allow more verbose output when running an
78 | # # individual spec file.
79 | # if config.files_to_run.one?
80 | # # Use the documentation formatter for detailed output,
81 | # # unless a formatter has already been configured
82 | # # (e.g. via a command-line flag).
83 | # config.default_formatter = "doc"
84 | # end
85 | #
86 | # # Print the 10 slowest examples and example groups at the
87 | # # end of the spec run, to help surface which specs are running
88 | # # particularly slow.
89 | # config.profile_examples = 10
90 | #
91 | # # Run specs in random order to surface order dependencies. If you find an
92 | # # order dependency and want to debug it, you can fix the order by providing
93 | # # the seed, which is printed after each run.
94 | # # --seed 1234
95 | # config.order = :random
96 | #
97 | # # Seed global randomization in this process using the `--seed` CLI option.
98 | # # Setting this allows you to use `--seed` to deterministically reproduce
99 | # # test failures related to randomization by passing the same `--seed` value
100 | # # as the one that triggered the failure.
101 | # Kernel.srand config.seed
102 | end
103 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spectator Sport
2 |
3 | Record and replay browser sessions in a self-hosted Rails Engine.
4 |
5 | Spectator Sport uses the [`rrweb` library](https://www.rrweb.io/) to create recordings of your website's DOM as your users interact with it. These recordings are stored in your database for replay by developers and administrators to analyze user behavior, reproduce bugs, and make building for the web more fun.
6 |
7 | 🚧 🚧 _This gem is very early in its development lifecycle and will undergo significant changes on its journey to v1.0. I would love your feedback and help in co-developing it, just fyi that it's going to be so much better than it is right now._
8 |
9 | 🚧 🚧 **Future Roadmap:**
10 |
11 | - ✅ Proof of concept and technical demo
12 | - ✅ Running in production on Ben Sheldon's personal business websites
13 | - ✅ Publish [manifesto of principles and intent](https://github.com/bensheldon/spectator_sport/discussions/6)
14 | - ◻️ Reliable and efficient event stream transport
15 | - ✅ Player dashboard design using Bootstrap and Turbo ([#20](https://github.com/bensheldon/spectator_sport/pull/20))
16 | - ◻️ Automatic cleanup of old recordings to minimize database space
17 | - ◻️ Identity methods for linking application users to recordings
18 | - ◻️ Privacy controls with masked recording by default
19 | - ◻️ Automated installation process with Rails generators
20 | - ◻️ Fully documented installation process
21 | - 🏁 Release v1.0 🎉
22 | - ◻️ Live streaming replay of recordings
23 | - ◻️ Searching / filtering of recordings, including navigation and 404s/500s, button clicks, rage clicks, dead clicks, etc.
24 | - ◻️ Custom events
25 | - 💖 Your feedback and ideas. Please open an Issue or Discussion or even a PR modifying this Roadmap. I'd love to chat!
26 |
27 | ## Installation
28 |
29 | The Spectator Sport gem is conceptually two parts packaged together in this single gem and mounted in your application:
30 |
31 | 1. The Recorder, including javascript that runs in the client browser and produces a stream of events, an API endpoint to receive those events, and database migrations and models to store the events as a cohesive recording.
32 | 2. The Player Dashboard, an administrative dashboard to view and replay stored recordings
33 |
34 | To install Spectator Sport in your Rails application:
35 |
36 | 1. Add `spectator_sport` to your application's Gemfile and install the gem:
37 | ```bash
38 | bundle add spectator_sport
39 | ```
40 | 2. Install Spectator Sport in your application. _🚧 This will change on the path to v1._ Explore the `/demo` app as live example:
41 | - Create database migrations with `bin/rails g spectator_sport:install:migrations`. Apply migrations with `bin/rails db:prepare`
42 | - Mount the recorder API in your application's routes with `mount SpectatorSport::Engine, at: "/spectator_sport, as: :spectator_sport"`
43 | - Add the `spectator_sport_script_tags` helper to the bottom of the `` of `layout/application.rb`. Example:
44 | ```erb
45 | <%# app/views/layouts/application.html.erb %>
46 | <%# ... %>
47 | <%= spectator_sport_script_tags %>
48 |
49 | ```
50 |
51 | - Add a `
56 |
57 | ```
58 | 3. To view recordings, you will want to mount the Player Dashboard in your application and set up authorization to limit access. See the section on [Dashboard authorization](#dashboard-authorization) for instructions.
59 |
60 | ## Dashboard authorization
61 |
62 | It is advisable to manually install and set up authorization for the **Player Dashboard** and refrain from making it public.
63 |
64 | If you are using Devise, the process of authorizing admins might resemble the following:
65 |
66 | ```ruby
67 | # config/routes.rb
68 | authenticate :user, ->(user) { user.admin? } do
69 | mount SpectatorSport::Dashboard::Engine, at: 'spectator_sport_dashboard', as: :spectator_sport_dashboard
70 | end
71 | ```
72 |
73 | Or set up Basic Auth:
74 |
75 | ```ruby
76 | # config/initializers/spectator_sport.rb
77 | SpectatorSport::Dashboard::Engine.middleware.use(Rack::Auth::Basic) do |username, password|
78 | ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.spectator_sport_username, username) &
79 | ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.spectator_sport_password, password)
80 | end
81 | ```
82 |
83 | If you are using an authentication method similar to the one used in ONCE products, you can utilize an auth constraint in your routes.
84 | ```ruby
85 | # config/routes.rb
86 | class AuthRouteConstraint
87 | def matches?(request)
88 | return false unless request.session[:user_id]
89 | user = User.find(request.session[:user_id])
90 |
91 | if user && user.admin?
92 | cookies = ActionDispatch::Cookies::CookieJar.build(request, request.cookies)
93 | token = cookies.signed[:session_token]
94 |
95 | return user.sessions.find_by(token: token)
96 | end
97 | end
98 | end
99 |
100 | Rails.application.routes.draw do
101 | # ...
102 | namespace :admin, constraints: AuthRouteConstraint.new do
103 | mount SpectatorSport::Dashboard::Engine, at: 'spectator_sport_dashboard', as: :spectator_sport_dashboard
104 | end
105 | end
106 | ```
107 |
108 | Or extend the `SpectatorSport::Dashboard::ApplicationController` with your own authorization logic:
109 |
110 | ```ruby
111 | # config/initializers/spectator_sport.rb
112 | ActiveSupport.on_load(:spectator_sport_dashboard_application_controller) do
113 | # context here is SpectatorSport::Dashboard::ApplicationController
114 |
115 | before_action do
116 | raise ActionController::RoutingError.new('Not Found') unless current_user&.admin?
117 | end
118 |
119 | def current_user
120 | # load current user from session, cookies, etc.
121 | end
122 | end
123 | ```
124 |
125 | ## Contributing
126 |
127 | 💖 Please don't be shy about opening an issue or half-baked PR. Your ideas and suggestions are more important to discuss than a polished/complete code change.
128 |
129 | This repository is intended to be simple and easy to run locally with a fully-featured demo application for immediately seeing the results of your proposed changes:
130 |
131 | ```bash
132 | # 1. Clone this repository via git
133 | # 2. Set it up locally
134 | bundle install
135 | # 3. Create database
136 | bin/rails db:setup
137 | # 4. Run the demo Rails application:
138 | bin/rails s
139 | # 5. Load the demo application in your browser
140 | open http://localhost:3000
141 | # 6. Make changes, see the result, commit and make a PR!
142 | ```
143 |
144 | ## License
145 |
146 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
147 |
--------------------------------------------------------------------------------
/app/frontend/spectator_sport/dashboard/icons.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | spectator_sport (0.1.0)
5 | rails (>= 7.2.1)
6 |
7 | GEM
8 | remote: https://rubygems.org/
9 | specs:
10 | actioncable (8.0.2)
11 | actionpack (= 8.0.2)
12 | activesupport (= 8.0.2)
13 | nio4r (~> 2.0)
14 | websocket-driver (>= 0.6.1)
15 | zeitwerk (~> 2.6)
16 | actionmailbox (8.0.2)
17 | actionpack (= 8.0.2)
18 | activejob (= 8.0.2)
19 | activerecord (= 8.0.2)
20 | activestorage (= 8.0.2)
21 | activesupport (= 8.0.2)
22 | mail (>= 2.8.0)
23 | actionmailer (8.0.2)
24 | actionpack (= 8.0.2)
25 | actionview (= 8.0.2)
26 | activejob (= 8.0.2)
27 | activesupport (= 8.0.2)
28 | mail (>= 2.8.0)
29 | rails-dom-testing (~> 2.2)
30 | actionpack (8.0.2)
31 | actionview (= 8.0.2)
32 | activesupport (= 8.0.2)
33 | nokogiri (>= 1.8.5)
34 | rack (>= 2.2.4)
35 | rack-session (>= 1.0.1)
36 | rack-test (>= 0.6.3)
37 | rails-dom-testing (~> 2.2)
38 | rails-html-sanitizer (~> 1.6)
39 | useragent (~> 0.16)
40 | actiontext (8.0.2)
41 | actionpack (= 8.0.2)
42 | activerecord (= 8.0.2)
43 | activestorage (= 8.0.2)
44 | activesupport (= 8.0.2)
45 | globalid (>= 0.6.0)
46 | nokogiri (>= 1.8.5)
47 | actionview (8.0.2)
48 | activesupport (= 8.0.2)
49 | builder (~> 3.1)
50 | erubi (~> 1.11)
51 | rails-dom-testing (~> 2.2)
52 | rails-html-sanitizer (~> 1.6)
53 | activejob (8.0.2)
54 | activesupport (= 8.0.2)
55 | globalid (>= 0.3.6)
56 | activemodel (8.0.2)
57 | activesupport (= 8.0.2)
58 | activerecord (8.0.2)
59 | activemodel (= 8.0.2)
60 | activesupport (= 8.0.2)
61 | timeout (>= 0.4.0)
62 | activestorage (8.0.2)
63 | actionpack (= 8.0.2)
64 | activejob (= 8.0.2)
65 | activerecord (= 8.0.2)
66 | activesupport (= 8.0.2)
67 | marcel (~> 1.0)
68 | activesupport (8.0.2)
69 | base64
70 | benchmark (>= 0.3)
71 | bigdecimal
72 | concurrent-ruby (~> 1.0, >= 1.3.1)
73 | connection_pool (>= 2.2.5)
74 | drb
75 | i18n (>= 1.6, < 2)
76 | logger (>= 1.4.2)
77 | minitest (>= 5.1)
78 | securerandom (>= 0.3)
79 | tzinfo (~> 2.0, >= 2.0.5)
80 | uri (>= 0.13.1)
81 | addressable (2.8.7)
82 | public_suffix (>= 2.0.2, < 7.0)
83 | ast (2.4.3)
84 | base64 (0.3.0)
85 | benchmark (0.4.1)
86 | bigdecimal (3.2.2)
87 | builder (3.3.0)
88 | capybara (3.40.0)
89 | addressable
90 | matrix
91 | mini_mime (>= 0.1.3)
92 | nokogiri (~> 1.11)
93 | rack (>= 1.6.0)
94 | rack-test (>= 0.6.3)
95 | regexp_parser (>= 1.5, < 3.0)
96 | xpath (~> 3.2)
97 | concurrent-ruby (1.3.5)
98 | connection_pool (2.5.3)
99 | crass (1.0.6)
100 | date (3.4.1)
101 | diff-lcs (1.6.2)
102 | drb (2.2.3)
103 | erb (5.0.2)
104 | erubi (1.13.1)
105 | globalid (1.2.1)
106 | activesupport (>= 6.1)
107 | i18n (1.14.7)
108 | concurrent-ruby (~> 1.0)
109 | io-console (0.8.1)
110 | irb (1.15.2)
111 | pp (>= 0.6.0)
112 | rdoc (>= 4.0.0)
113 | reline (>= 0.4.2)
114 | json (2.13.2)
115 | language_server-protocol (3.17.0.5)
116 | lint_roller (1.1.0)
117 | logger (1.7.0)
118 | loofah (2.24.1)
119 | crass (~> 1.0.2)
120 | nokogiri (>= 1.12.0)
121 | mail (2.8.1)
122 | mini_mime (>= 0.1.1)
123 | net-imap
124 | net-pop
125 | net-smtp
126 | marcel (1.0.4)
127 | matrix (0.4.3)
128 | mini_mime (1.1.5)
129 | mini_portile2 (2.8.9)
130 | minitest (5.25.5)
131 | net-imap (0.5.9)
132 | date
133 | net-protocol
134 | net-pop (0.1.2)
135 | net-protocol
136 | net-protocol (0.2.2)
137 | timeout
138 | net-smtp (0.5.1)
139 | net-protocol
140 | nio4r (2.7.4)
141 | nokogiri (1.18.9)
142 | mini_portile2 (~> 2.8.2)
143 | racc (~> 1.4)
144 | nokogiri (1.18.9-aarch64-linux-gnu)
145 | racc (~> 1.4)
146 | nokogiri (1.18.9-aarch64-linux-musl)
147 | racc (~> 1.4)
148 | nokogiri (1.18.9-arm-linux-gnu)
149 | racc (~> 1.4)
150 | nokogiri (1.18.9-arm-linux-musl)
151 | racc (~> 1.4)
152 | nokogiri (1.18.9-arm64-darwin)
153 | racc (~> 1.4)
154 | nokogiri (1.18.9-x86_64-darwin)
155 | racc (~> 1.4)
156 | nokogiri (1.18.9-x86_64-linux-gnu)
157 | racc (~> 1.4)
158 | nokogiri (1.18.9-x86_64-linux-musl)
159 | racc (~> 1.4)
160 | parallel (1.27.0)
161 | parser (3.3.9.0)
162 | ast (~> 2.4.1)
163 | racc
164 | pg (1.6.0)
165 | pg (1.6.0-aarch64-linux)
166 | pg (1.6.0-arm64-darwin)
167 | pg (1.6.0-x86_64-darwin)
168 | pg (1.6.0-x86_64-linux)
169 | pp (0.6.2)
170 | prettyprint
171 | prettyprint (0.2.0)
172 | prism (1.4.0)
173 | psych (5.2.6)
174 | date
175 | stringio
176 | public_suffix (6.0.2)
177 | puma (6.6.0)
178 | nio4r (~> 2.0)
179 | racc (1.8.1)
180 | rack (3.1.16)
181 | rack-session (2.1.1)
182 | base64 (>= 0.1.0)
183 | rack (>= 3.0.0)
184 | rack-test (2.2.0)
185 | rack (>= 1.3)
186 | rackup (2.2.1)
187 | rack (>= 3)
188 | rails (8.0.2)
189 | actioncable (= 8.0.2)
190 | actionmailbox (= 8.0.2)
191 | actionmailer (= 8.0.2)
192 | actionpack (= 8.0.2)
193 | actiontext (= 8.0.2)
194 | actionview (= 8.0.2)
195 | activejob (= 8.0.2)
196 | activemodel (= 8.0.2)
197 | activerecord (= 8.0.2)
198 | activestorage (= 8.0.2)
199 | activesupport (= 8.0.2)
200 | bundler (>= 1.15.0)
201 | railties (= 8.0.2)
202 | rails-dom-testing (2.3.0)
203 | activesupport (>= 5.0.0)
204 | minitest
205 | nokogiri (>= 1.6)
206 | rails-html-sanitizer (1.6.2)
207 | loofah (~> 2.21)
208 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
209 | railties (8.0.2)
210 | actionpack (= 8.0.2)
211 | activesupport (= 8.0.2)
212 | irb (~> 1.13)
213 | rackup (>= 1.0.0)
214 | rake (>= 12.2)
215 | thor (~> 1.0, >= 1.2.2)
216 | zeitwerk (~> 2.6)
217 | rainbow (3.1.1)
218 | rake (13.3.0)
219 | rdoc (6.14.2)
220 | erb
221 | psych (>= 4.0.0)
222 | regexp_parser (2.10.0)
223 | reline (0.6.2)
224 | io-console (~> 0.5)
225 | rexml (3.4.1)
226 | rspec-core (3.13.5)
227 | rspec-support (~> 3.13.0)
228 | rspec-expectations (3.13.5)
229 | diff-lcs (>= 1.2.0, < 2.0)
230 | rspec-support (~> 3.13.0)
231 | rspec-mocks (3.13.5)
232 | diff-lcs (>= 1.2.0, < 2.0)
233 | rspec-support (~> 3.13.0)
234 | rspec-rails (8.0.1)
235 | actionpack (>= 7.2)
236 | activesupport (>= 7.2)
237 | railties (>= 7.2)
238 | rspec-core (~> 3.13)
239 | rspec-expectations (~> 3.13)
240 | rspec-mocks (~> 3.13)
241 | rspec-support (~> 3.13)
242 | rspec-support (3.13.4)
243 | rubocop (1.79.0)
244 | json (~> 2.3)
245 | language_server-protocol (~> 3.17.0.2)
246 | lint_roller (~> 1.1.0)
247 | parallel (~> 1.10)
248 | parser (>= 3.3.0.2)
249 | rainbow (>= 2.2.2, < 4.0)
250 | regexp_parser (>= 2.9.3, < 3.0)
251 | rubocop-ast (>= 1.46.0, < 2.0)
252 | ruby-progressbar (~> 1.7)
253 | tsort (>= 0.2.0)
254 | unicode-display_width (>= 2.4.0, < 4.0)
255 | rubocop-ast (1.46.0)
256 | parser (>= 3.3.7.2)
257 | prism (~> 1.4)
258 | rubocop-performance (1.25.0)
259 | lint_roller (~> 1.1)
260 | rubocop (>= 1.75.0, < 2.0)
261 | rubocop-ast (>= 1.38.0, < 2.0)
262 | rubocop-rails (2.32.0)
263 | activesupport (>= 4.2.0)
264 | lint_roller (~> 1.1)
265 | rack (>= 1.1)
266 | rubocop (>= 1.75.0, < 2.0)
267 | rubocop-ast (>= 1.44.0, < 2.0)
268 | rubocop-rails-omakase (1.1.0)
269 | rubocop (>= 1.72)
270 | rubocop-performance (>= 1.24)
271 | rubocop-rails (>= 2.30)
272 | ruby-progressbar (1.13.0)
273 | rubyzip (2.4.1)
274 | securerandom (0.4.1)
275 | selenium-webdriver (4.34.0)
276 | base64 (~> 0.2)
277 | logger (~> 1.4)
278 | rexml (~> 3.2, >= 3.2.5)
279 | rubyzip (>= 1.2.2, < 3.0)
280 | websocket (~> 1.0)
281 | sprockets (4.2.2)
282 | concurrent-ruby (~> 1.0)
283 | logger
284 | rack (>= 2.2.4, < 4)
285 | sprockets-rails (3.5.2)
286 | actionpack (>= 6.1)
287 | activesupport (>= 6.1)
288 | sprockets (>= 3.0.0)
289 | sqlite3 (2.7.3-aarch64-linux-gnu)
290 | sqlite3 (2.7.3-aarch64-linux-musl)
291 | sqlite3 (2.7.3-arm-linux-gnu)
292 | sqlite3 (2.7.3-arm-linux-musl)
293 | sqlite3 (2.7.3-arm64-darwin)
294 | sqlite3 (2.7.3-x86-linux-gnu)
295 | sqlite3 (2.7.3-x86-linux-musl)
296 | sqlite3 (2.7.3-x86_64-darwin)
297 | sqlite3 (2.7.3-x86_64-linux-gnu)
298 | sqlite3 (2.7.3-x86_64-linux-musl)
299 | stringio (3.1.7)
300 | thor (1.4.0)
301 | timeout (0.4.3)
302 | tsort (0.2.0)
303 | tzinfo (2.0.6)
304 | concurrent-ruby (~> 1.0)
305 | unicode-display_width (3.1.4)
306 | unicode-emoji (~> 4.0, >= 4.0.4)
307 | unicode-emoji (4.0.4)
308 | uri (1.0.3)
309 | useragent (0.16.11)
310 | warning (1.5.0)
311 | websocket (1.2.11)
312 | websocket-driver (0.8.0)
313 | base64
314 | websocket-extensions (>= 0.1.0)
315 | websocket-extensions (0.1.5)
316 | xpath (3.2.0)
317 | nokogiri (~> 1.8)
318 | zeitwerk (2.7.3)
319 |
320 | PLATFORMS
321 | aarch64-linux
322 | aarch64-linux-gnu
323 | aarch64-linux-musl
324 | arm-linux
325 | arm-linux-gnu
326 | arm-linux-musl
327 | arm64-darwin
328 | x86-linux
329 | x86-linux-gnu
330 | x86-linux-musl
331 | x86_64-darwin
332 | x86_64-linux
333 | x86_64-linux-gnu
334 | x86_64-linux-musl
335 |
336 | DEPENDENCIES
337 | capybara
338 | pg
339 | puma
340 | rspec-rails
341 | rubocop-rails-omakase
342 | selenium-webdriver
343 | spectator_sport!
344 | sprockets-rails
345 | sqlite3
346 | warning
347 |
348 | RUBY VERSION
349 | ruby 3.4.5p51
350 |
351 | BUNDLED WITH
352 | 2.6.9
353 |
--------------------------------------------------------------------------------
/app/frontend/spectator_sport/dashboard/vendor/es_module_shims.js:
--------------------------------------------------------------------------------
1 | (function(){const e="undefined"!==typeof window;const t="undefined"!==typeof document;const noop=()=>{};const r=t?document.querySelector("script[type=esms-options]"):void 0;const s=r?JSON.parse(r.innerHTML):{};Object.assign(s,self.esmsInitOptions||{});let n=!t||!!s.shimMode;const a=globalHook(n&&s.onimport);const i=globalHook(n&&s.resolve);let c=s.fetch?globalHook(s.fetch):fetch;const f=s.meta?globalHook(n&&s.meta):noop;const te=s.mapOverrides;let se=s.nonce;if(!se&&t){const e=document.querySelector("script[nonce]");e&&(se=e.nonce||e.getAttribute("nonce"))}const ne=globalHook(s.onerror||noop);const oe=s.onpolyfill?globalHook(s.onpolyfill):()=>{console.log("%c^^ Module TypeError above is polyfilled and can be ignored ^^","font-weight:900;color:#391")};const{revokeBlobURLs:ce,noLoadEventRetriggers:le,enforceIntegrity:fe}=s;function globalHook(e){return"string"===typeof e?self[e]:e}const ue=Array.isArray(s.polyfillEnable)?s.polyfillEnable:[];const de=ue.includes("css-modules");const pe=ue.includes("json-modules");const be=!navigator.userAgentData&&!!navigator.userAgent.match(/Edge\/\d+\.\d+/);const he=t?document.baseURI:`${location.protocol}//${location.host}${location.pathname.includes("/")?location.pathname.slice(0,location.pathname.lastIndexOf("/")+1):location.pathname}`;const createBlob=(e,t="text/javascript")=>URL.createObjectURL(new Blob([e],{type:t}));let{skip:me}=s;if(Array.isArray(me)){const e=me.map((e=>new URL(e,he).href));me=t=>e.some((e=>"/"===e[e.length-1]&&t.startsWith(e)||t===e))}else if("string"===typeof me){const e=new RegExp(me);me=t=>e.test(t)}const eoop=e=>setTimeout((()=>{throw e}));const throwError=t=>{(self.reportError||e&&window.safari&&console.error||eoop)(t),void ne(t)};function fromParent(e){return e?` imported from ${e}`:""}let ke=false;function setImportMapSrcOrLazy(){ke=true}if(!n)if(document.querySelectorAll("script[type=module-shim],script[type=importmap-shim],link[rel=modulepreload-shim]").length)n=true;else{let e=false;for(const t of document.querySelectorAll("script[type=module],script[type=importmap]"))if(e){if("importmap"===t.type&&e){ke=true;break}}else"module"!==t.type||t.ep||(e=true)}const we=/\\/g;function isURL(e){if(-1===e.indexOf(":"))return false;try{new URL(e);return true}catch(e){return false}}function resolveUrl(e,t){return resolveIfNotPlainOrUrl(e,t)||(isURL(e)?e:resolveIfNotPlainOrUrl("./"+e,t))}function resolveIfNotPlainOrUrl(e,t){const r=t.indexOf("#"),s=t.indexOf("?");r+s>-2&&(t=t.slice(0,-1===r?s:-1===s||s>r?r:s));-1!==e.indexOf("\\")&&(e=e.replace(we,"/"));if("/"===e[0]&&"/"===e[1])return t.slice(0,t.indexOf(":")+1)+e;if("."===e[0]&&("/"===e[1]||"."===e[1]&&("/"===e[2]||2===e.length&&(e+="/"))||1===e.length&&(e+="/"))||"/"===e[0]){const r=t.slice(0,t.indexOf(":")+1);let s;if("/"===t[r.length+1])if("file:"!==r){s=t.slice(r.length+2);s=s.slice(s.indexOf("/")+1)}else s=t.slice(8);else s=t.slice(r.length+("/"===t[r.length]));if("/"===e[0])return t.slice(0,t.length-s.length-1)+e;const n=s.slice(0,s.lastIndexOf("/")+1)+e;const a=[];let i=-1;for(let e=0;e "${e[a]}" does not resolve`)}}let ge=!t&&(0,eval)("u=>import(u)");let ve;const ye=t&&new Promise((e=>{const t=Object.assign(document.createElement("script"),{src:createBlob("self._d=u=>import(u)"),ep:true});t.setAttribute("nonce",se);t.addEventListener("load",(()=>{if(!(ve=!!(ge=self._d))){let e;window.addEventListener("error",(t=>e=t));ge=(t,r)=>new Promise(((s,n)=>{const a=Object.assign(document.createElement("script"),{type:"module",src:createBlob(`import*as m from'${t}';self._esmsi=m`)});e=void 0;a.ep=true;se&&a.setAttribute("nonce",se);a.addEventListener("error",cb);a.addEventListener("load",cb);function cb(i){document.head.removeChild(a);if(self._esmsi){s(self._esmsi,he);self._esmsi=void 0}else{n(!(i instanceof Event)&&i||e&&e.error||new Error(`Error loading ${r&&r.errUrl||t} (${a.src}).`));e=void 0}}document.head.appendChild(a)}))}document.head.removeChild(t);delete self._d;e()}));document.head.appendChild(t)}));let $e=false;let Se=false;let Le=!(!t||!HTMLScriptElement.supports)&&HTMLScriptElement.supports("importmap");let Oe=Le;const Ce="import.meta";const Ae='import"x"assert{type:"css"}';const xe='import"x"assert{type:"json"}';const Pe=Promise.resolve(ye).then((()=>{if(ve&&(!Le||de||pe))return t?new Promise((e=>{const t=document.createElement("iframe");t.style.display="none";t.setAttribute("nonce",se);function cb({data:[r,s,n,a]}){Le=r;Oe=s;Se=n;$e=a;e();document.head.removeChild(t);window.removeEventListener("message",cb,false)}window.addEventListener("message",cb,false);const r=`