From 5c7436ddfcad1e21003582e8480ad28af9140cc7 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 12 Jan 2026 07:07:42 -0600 Subject: [PATCH] Commit changes --- admin/app/admin/ad_campaigns.rb | 2 +- admin/app/admin/affiliates.rb | 10 +- admin/app/admin/connections.rb | 6 +- admin/app/admin/email_batch.rb | 10 +- admin/app/admin/icecast_bootstrap.rb | 4 +- admin/app/admin/icecast_server_group.rb | 1 + admin/app/admin/jam_class_knobs.rb | 2 +- admin/app/admin/jam_ruby_artifact_updates.rb | 2 +- admin/app/admin/jam_ruby_users.rb | 2 +- .../app/admin/jammers_subscription_cohorts.rb | 4 +- .../admin/jamtrack_subscription_cohorts.rb | 2 +- admin/app/admin/score_history.rb | 2 +- admin/app/admin/subscription_override.rb | 2 + admin/app/assets/config/manifest.js | 4 + .../assets/javascripts/active_admin.js.coffee | 10 -- .../app/assets/stylesheets/active_admin.scss | 17 --- .../app/controllers/application_controller.rb | 2 +- admin/app/models/cohort.rb | 4 +- admin/app/uploaders/image_uploader.rb | 4 +- admin/config/application.rb | 20 +++- admin/config/boot.rb | 25 ++-- admin/config/environment.rb | 2 +- admin/config/environments/development.rb | 2 +- admin/config/environments/test.rb | 2 +- admin/config/initializers/active_admin.rb | 25 ++++ .../active_admin_rails8_patches.rb | 53 +++++++++ admin/config/initializers/bugsnag.rb | 2 + admin/config/initializers/influxdb-rails.rb | 2 +- .../initializers/jam_ruby/promotional.rb | 2 + admin/config/initializers/long_running_fix.rb | 2 + .../config/initializers/ransack_allow_all.rb | 17 +++ admin/config/initializers/schools.rb | 4 +- admin/spec/features/menu_click_spec.rb | 58 +++++++++ features/conversion-reporting.md | 112 ++++++++++++++++++ ...000_add_reset_password_sent_at_to_users.rb | 5 + ruby/spec/support/bugsnag_stub.rb | 6 + 36 files changed, 354 insertions(+), 75 deletions(-) create mode 100644 admin/app/assets/config/manifest.js delete mode 100644 admin/app/assets/javascripts/active_admin.js.coffee delete mode 100644 admin/app/assets/stylesheets/active_admin.scss create mode 100644 admin/config/initializers/active_admin_rails8_patches.rb create mode 100644 admin/config/initializers/long_running_fix.rb create mode 100644 admin/config/initializers/ransack_allow_all.rb create mode 100644 admin/spec/features/menu_click_spec.rb create mode 100644 features/conversion-reporting.md create mode 100644 ruby/db/migrate/20260111000000_add_reset_password_sent_at_to_users.rb create mode 100644 ruby/spec/support/bugsnag_stub.rb diff --git a/admin/app/admin/ad_campaigns.rb b/admin/app/admin/ad_campaigns.rb index dd5b0f390..8260c76c7 100644 --- a/admin/app/admin/ad_campaigns.rb +++ b/admin/app/admin/ad_campaigns.rb @@ -28,7 +28,7 @@ end ActiveAdmin.register JamRuby::AdCampaign, as: 'AdCampaign' do menu :label => 'Ad Campaigns', :parent => 'Reports' - before_filter :skip_sidebar!, :only => :index + before_action :skip_sidebar!, :only => :index config.batch_actions = false config.clear_action_items! config.sort_order = "users.origin_utm_campaign DESC" diff --git a/admin/app/admin/affiliates.rb b/admin/app/admin/affiliates.rb index 8bfc083bb..4845ff13a 100644 --- a/admin/app/admin/affiliates.rb +++ b/admin/app/admin/affiliates.rb @@ -136,10 +136,12 @@ ActiveAdmin.register JamRuby::AffiliatePartner, :as => 'Affiliates' do end - action_item :only => [:show] do - link_to("Mark Paid", - mark_paid_admin_affiliate_path(resource.id), - :confirm => "Mark this affiliate as PAID?") if resource.unpaid + action_item :mark_paid, only: :show do + if params[:id] && resource.unpaid + link_to("Mark Paid", + mark_paid_admin_affiliate_path(resource.id), + :confirm => "Mark this affiliate as PAID?") + end end member_action :mark_paid, :method => :get do diff --git a/admin/app/admin/connections.rb b/admin/app/admin/connections.rb index c21633588..9bd754581 100644 --- a/admin/app/admin/connections.rb +++ b/admin/app/admin/connections.rb @@ -29,7 +29,7 @@ ActiveAdmin.register JamRuby::Connection, :as => 'Connection' do end action_item :only => [:show] do - link_to('Reload', reload_admin_connection_path(resource.id), class: 'confirm') + link_to('Reload', reload_admin_connection_path(resource.id), class: 'confirm') if params[:id] end member_action :reload, :method => :get do connection = Connection.find(params[:id]) @@ -38,7 +38,7 @@ ActiveAdmin.register JamRuby::Connection, :as => 'Connection' do end action_item :only => [:show] do - link_to('Restart Client', restart_admin_connection_path(resource.id), class: 'confirm') if resource.client_type == 'client' + link_to('Restart Client', restart_admin_connection_path(resource.id), class: 'confirm') if params[:id] && resource.client_type == 'client' end member_action :restart, :method => :get do connection = Connection.find(params[:id]) @@ -47,7 +47,7 @@ ActiveAdmin.register JamRuby::Connection, :as => 'Connection' do end action_item :only => [:show] do - link_to('Shutdown Client', stop_admin_connection_path(resource.id), class: 'confirm') if resource.client_type == 'client' + link_to('Shutdown Client', stop_admin_connection_path(resource.id), class: 'confirm') if params[:id] && resource.client_type == 'client' end member_action :stop, :method => :get do connection = Connection.find(params[:id]) diff --git a/admin/app/admin/email_batch.rb b/admin/app/admin/email_batch.rb index 4c5150dd5..9831c920d 100644 --- a/admin/app/admin/email_batch.rb +++ b/admin/app/admin/email_batch.rb @@ -10,26 +10,26 @@ ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do form :partial => 'form' action_item :only => [:show] do - link_to('Edit Batch Email', edit_admin_batch_email_path(resource.id)) if resource.can_run_batch? + link_to('Edit Batch Email', edit_admin_batch_email_path(resource.id)) if params[:id] && resource.can_run_batch? end action_item :only => [:show] do link_to("Test Batch (#{resource.test_count})", batch_test_admin_batch_email_path(resource.id), - :confirm => "Run test batch with #{resource.test_count} emails?") if resource.can_run_test? + :confirm => "Run test batch with #{resource.test_count} emails?") if params[:id] && resource.can_run_test? end action_item :only => [:show] do link_to("Deliver Batch (#{User.email_opt_in.count})", batch_send_admin_batch_email_path(resource.id), - :confirm => "Run LIVE batch with #{User.email_opt_in.count} emails?") if resource.can_run_batch? + :confirm => "Run LIVE batch with #{User.email_opt_in.count} emails?") if params[:id] && resource.can_run_batch? end action_item :only => [:show, :edit] do - link_to('Clone Batch Email', batch_clone_admin_batch_email_path(resource.id)) + link_to('Clone Batch Email', batch_clone_admin_batch_email_path(resource.id)) if params[:id] end - action_item do + action_item :new_batch_email do link_to('New Batch Email', new_admin_batch_email_path) end diff --git a/admin/app/admin/icecast_bootstrap.rb b/admin/app/admin/icecast_bootstrap.rb index 6367fff89..d5d19b0f0 100644 --- a/admin/app/admin/icecast_bootstrap.rb +++ b/admin/app/admin/icecast_bootstrap.rb @@ -205,11 +205,11 @@ ActiveAdmin.register_page "Bootstrap" do end - action_item do + action_item :create_brew_template do link_to "Create MacOSX (Brew) Template", admin_bootstrap_brew_template_path, :method => :post end - action_item do + action_item :create_ubuntu_template do link_to "Create Ubuntu 12.04 Template", admin_bootstrap_ubuntu_template_path, :method => :post end diff --git a/admin/app/admin/icecast_server_group.rb b/admin/app/admin/icecast_server_group.rb index 2c560996d..185086bf2 100644 --- a/admin/app/admin/icecast_server_group.rb +++ b/admin/app/admin/icecast_server_group.rb @@ -1,3 +1,4 @@ ActiveAdmin.register JamRuby::IcecastServerGroup, :as => 'IcecastServerGroup' do menu :parent => 'Icecast' + index download_links: false end diff --git a/admin/app/admin/jam_class_knobs.rb b/admin/app/admin/jam_class_knobs.rb index f27e3f0e0..c7747b4e9 100644 --- a/admin/app/admin/jam_class_knobs.rb +++ b/admin/app/admin/jam_class_knobs.rb @@ -8,7 +8,7 @@ ActiveAdmin.register_page "Jam Class Knobs" do end - action_item do + action_item :force_hourly do link_to "Force Hourly Background Job", admin_jam_class_knobs_force_hourly_path, :method => :post end end \ No newline at end of file diff --git a/admin/app/admin/jam_ruby_artifact_updates.rb b/admin/app/admin/jam_ruby_artifact_updates.rb index de0a4f65b..f92d3b859 100644 --- a/admin/app/admin/jam_ruby_artifact_updates.rb +++ b/admin/app/admin/jam_ruby_artifact_updates.rb @@ -21,7 +21,7 @@ ActiveAdmin.register JamRuby::ArtifactUpdate, :as => 'Artifacts' do end action_item :only => [:show] do - link_to('Send Client Update Notice', send_client_update_notice_admin_artifact_path(resource.id)) + link_to('Send Client Update Notice', send_client_update_notice_admin_artifact_path(resource.id)) if params[:id] end member_action :send_client_update_notice, :method => :get do diff --git a/admin/app/admin/jam_ruby_users.rb b/admin/app/admin/jam_ruby_users.rb index d65f2bc39..043bc7169 100644 --- a/admin/app/admin/jam_ruby_users.rb +++ b/admin/app/admin/jam_ruby_users.rb @@ -28,7 +28,7 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do end action_item :latency, only: :show do - link_to 'Show Latency', latency_admin_user_path(resource) + link_to 'Show Latency', latency_admin_user_path(resource) if params[:id] end collection_action :add_school_user, method: [:get, :post] do diff --git a/admin/app/admin/jammers_subscription_cohorts.rb b/admin/app/admin/jammers_subscription_cohorts.rb index 336e86ae9..8185d23f7 100644 --- a/admin/app/admin/jammers_subscription_cohorts.rb +++ b/admin/app/admin/jammers_subscription_cohorts.rb @@ -54,9 +54,9 @@ ActiveAdmin.register_page "Jammers Subscription Cohorts" do j.created_at LIMIT 1 -- Select only that single row ) j ON TRUE }) - .group("date_trunc('month', users.created_at)") + .group(Arel.sql("date_trunc('month', users.created_at)")) .where("j.created_at IS NULL OR (j.created_at - users.created_at) >= INTERVAL '2 hours'") - .order("date_trunc('month', users.created_at) DESC") do |row| + .order(Arel.sql("date_trunc('month', users.created_at) DESC")) do |row| column "Month", Proc.new { |user| user.month.strftime('%B %Y') } column "Total", :total column "Subscribed", Proc.new { |user| raw(Spacer.spacer(user.subscribed, user)) } diff --git a/admin/app/admin/jamtrack_subscription_cohorts.rb b/admin/app/admin/jamtrack_subscription_cohorts.rb index 5b78f4f20..070c3cbfc 100644 --- a/admin/app/admin/jamtrack_subscription_cohorts.rb +++ b/admin/app/admin/jamtrack_subscription_cohorts.rb @@ -54,7 +54,7 @@ ActiveAdmin.register_page "JamTrack Subscription Cohorts" do j.created_at LIMIT 1 -- Select only that single row ) j ON (j.created_at - users.created_at) < INTERVAL '2 hours' }) - .group("date_trunc('month', users.created_at)").order("date_trunc('month', users.created_at) DESC") do |row| + .group(Arel.sql("date_trunc('month', users.created_at)")).order(Arel.sql("date_trunc('month', users.created_at) DESC")) do |row| column "Month", Proc.new { |user| user.month.strftime('%B %Y') } column "Total", :total column "Subscribed", Proc.new { |user| raw(Spacer.spacer(user.subscribed, user)) } diff --git a/admin/app/admin/score_history.rb b/admin/app/admin/score_history.rb index 7786b4c29..bd50b96c5 100644 --- a/admin/app/admin/score_history.rb +++ b/admin/app/admin/score_history.rb @@ -37,7 +37,7 @@ ActiveAdmin.register JamRuby::ScoreHistory, :as => 'Score History' do filter :to_latitude filter :to_longitude - before_filter only: :index do + before_action only: :index do @per_page = 1_000_000 if request.format == 'text/csv' end diff --git a/admin/app/admin/subscription_override.rb b/admin/app/admin/subscription_override.rb index 8831c66cf..eaf571f47 100644 --- a/admin/app/admin/subscription_override.rb +++ b/admin/app/admin/subscription_override.rb @@ -2,6 +2,8 @@ ActiveAdmin.register User, :as => 'User_Override' do menu :label => 'Subscription', :parent => 'Users' + config.filters = false + form title: 'Use This To Give a No-Payment Subscription' do |f| inputs 'Details' do input :admin_override_touch, :as => :hidden, value: 'true' diff --git a/admin/app/assets/config/manifest.js b/admin/app/assets/config/manifest.js new file mode 100644 index 000000000..fb27c7262 --- /dev/null +++ b/admin/app/assets/config/manifest.js @@ -0,0 +1,4 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css +//= link jquery-ui/theme.css \ No newline at end of file diff --git a/admin/app/assets/javascripts/active_admin.js.coffee b/admin/app/assets/javascripts/active_admin.js.coffee deleted file mode 100644 index 7d9d7acbd..000000000 --- a/admin/app/assets/javascripts/active_admin.js.coffee +++ /dev/null @@ -1,10 +0,0 @@ -#= require active_admin/base -#= require jquery3 -#= require best_in_place -#= require jquery.purr -#= require best_in_place.purr - -$(document).ready -> - # IS NOT HAPPENING: USE ACTIVE_ADMIN.JS - console.log("DAT COFFEE INIT") - jQuery(".best_in_place").best_in_place() \ No newline at end of file diff --git a/admin/app/assets/stylesheets/active_admin.scss b/admin/app/assets/stylesheets/active_admin.scss deleted file mode 100644 index d3c61f387..000000000 --- a/admin/app/assets/stylesheets/active_admin.scss +++ /dev/null @@ -1,17 +0,0 @@ -// SASS variable overrides must be declared before loading up Active Admin's styles. -// -// To view the variables that Active Admin provides, take a look at -// `app/assets/stylesheets/active_admin/mixins/_variables.scss` in the -// Active Admin source. -// -// For example, to change the sidebar width: -// $sidebar-width: 242px; - -// Active Admin's got SASS! -@import "active_admin/mixins"; -@import "active_admin/base"; - -// Overriding any non-variable SASS must be done after the fact. -// For example, to change the default status-tag color: -// -// .status_tag { background: #6090DB; } diff --git a/admin/app/controllers/application_controller.rb b/admin/app/controllers/application_controller.rb index d3e00deb6..a77ddbfee 100644 --- a/admin/app/controllers/application_controller.rb +++ b/admin/app/controllers/application_controller.rb @@ -3,7 +3,7 @@ class ApplicationController < ActionController::Base protect_from_forgery - before_filter :prepare_gon + before_action :prepare_gon def prepare_gon @olark_enabled = false diff --git a/admin/app/models/cohort.rb b/admin/app/models/cohort.rb index dcf7e51f2..dd605d05c 100644 --- a/admin/app/models/cohort.rb +++ b/admin/app/models/cohort.rb @@ -74,7 +74,7 @@ class Cohort < ActiveRecord::Base end def self.generate_monthly_cohorts(monthly_start, monthly_end) - Cohort.delete_all(['all_time = ?',false]) + Cohort.where(all_time: false).delete_all self.cohort_group_ranges.collect do |range| next if range.first > monthly_end cc = Cohort.new @@ -89,7 +89,7 @@ class Cohort < ActiveRecord::Base end def self.generate_all_time_cohorts - Cohort.delete_all("all_time = 't'") + Cohort.where(all_time: true).delete_all self.cohort_group_ranges.collect do |range| unless cc = Cohort.where(group_start: range.first).where(all_time: true).limit(1).first cc = Cohort.new diff --git a/admin/app/uploaders/image_uploader.rb b/admin/app/uploaders/image_uploader.rb index f094084d4..68495db4f 100644 --- a/admin/app/uploaders/image_uploader.rb +++ b/admin/app/uploaders/image_uploader.rb @@ -2,8 +2,8 @@ class ImageUploader < CarrierWave::Uploader::Base include CarrierWaveDirect::Uploader - include CarrierWave::MimeTypes - process :set_content_type + # include CarrierWave::MimeTypes + # process :set_content_type def initialize(*) diff --git a/admin/config/application.rb b/admin/config/application.rb index 2f1330667..a6655f7b7 100644 --- a/admin/config/application.rb +++ b/admin/config/application.rb @@ -1,6 +1,22 @@ require File.expand_path('../boot', __FILE__) -require 'rails/all' +# require 'rails/all' +require "rails" +# Pick the frameworks you want: +require "active_model/railtie" +require "active_job/railtie" +require "active_record/railtie" +# require "active_storage/engine" +require "action_controller/railtie" +require "action_mailer/railtie" +# require "action_mailbox/engine" +# require "action_text/engine" +require "action_view/railtie" +# require "action_cable/engine" +require "sprockets/railtie" +require "rails/test_unit/railtie" + +require 'inherited_resources' # initialize ActiveRecord's db connection # why? Because user.rb uses validates :acceptance, which needs a connection to the database. if there is better way... @@ -35,7 +51,7 @@ module JamAdmin # config.plugins = [ :exception_notification, :ssl_requirement, :all ] # Activate observers that should always be running. - config.active_record.observers = "JamRuby::InvitedUserObserver" + # config.active_record.observers = "JamRuby::InvitedUserObserver" config.assets.paths << "#{Rails.root}/app/assets/csvs" config.assets.prefix = "#{ENV['RAILS_RELATIVE_URL_ROOT']}/assets" diff --git a/admin/config/boot.rb b/admin/config/boot.rb index 56e91c277..7cbc2c0e2 100644 --- a/admin/config/boot.rb +++ b/admin/config/boot.rb @@ -3,18 +3,17 @@ require 'rubygems' # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) -# change default port of jam-admin so that it's easy to run both -require 'rails/commands/server' +# require 'rails/commands/server' -module Rails - class Server - alias :default_options_alias :default_options - def default_options - default_options_alias.merge!( - :Port => 3333 + ENV['JAM_INSTANCE'].to_i, - :pid => File.expand_path("tmp/pids/server-#{ENV['JAM_INSTANCE'].to_i}.pid")) - end - end -end \ No newline at end of file +# module Rails +# class Server +# alias :default_options_alias :default_options +# def default_options +# default_options_alias.merge!( +# :Port => 3333 + ENV['JAM_INSTANCE'].to_i, +# :pid => File.expand_path("tmp/pids/server-#{ENV['JAM_INSTANCE'].to_i}.pid")) +# end +# end +# end \ No newline at end of file diff --git a/admin/config/environment.rb b/admin/config/environment.rb index 92e6d1539..640a5a47c 100644 --- a/admin/config/environment.rb +++ b/admin/config/environment.rb @@ -1,7 +1,7 @@ # Load the rails application require File.expand_path('../application', __FILE__) -APP_CONFIG = Rails.application.config +APP_CONFIG = Rails.application.config unless defined?(APP_CONFIG) Stats.client = InfluxDB::Rails.client # Initialize the rails application diff --git a/admin/config/environments/development.rb b/admin/config/environments/development.rb index 2d0595cd7..651ecaaa4 100644 --- a/admin/config/environments/development.rb +++ b/admin/config/environments/development.rb @@ -26,7 +26,7 @@ JamAdmin::Application.configure do config.action_dispatch.best_standards_support = :builtin # Raise exception on mass assignment protection for Active Record models - config.active_record.mass_assignment_sanitizer = :strict + # config.active_record.mass_assignment_sanitizer = :strict # Do not compress assets diff --git a/admin/config/environments/test.rb b/admin/config/environments/test.rb index f8700bd0f..d7c8deccc 100644 --- a/admin/config/environments/test.rb +++ b/admin/config/environments/test.rb @@ -30,7 +30,7 @@ JamAdmin::Application.configure do config.action_mailer.delivery_method = :test # Raise exception on mass assignment protection for Active Record models - config.active_record.mass_assignment_sanitizer = :strict + # config.active_record.mass_assignment_sanitizer = :strict # Print deprecation notices to the stderr config.active_support.deprecation = :stderr diff --git a/admin/config/initializers/active_admin.rb b/admin/config/initializers/active_admin.rb index 8f5e04739..dffcfb1a7 100644 --- a/admin/config/initializers/active_admin.rb +++ b/admin/config/initializers/active_admin.rb @@ -1,3 +1,25 @@ +module DeviseHelper + def devise_error_messages! + return "" if resource.errors.empty? + + messages = resource.errors.full_messages.map { |msg| content_tag(:li, msg) }.join + html = <<-HTML +
+ +
+ HTML + + html.html_safe + end +end + +require File.expand_path('../../app/helpers/active_admin/subscription_helper', __dir__) +Dir[File.expand_path('../../app/helpers/*.rb', __dir__)].each do |file| + require file +end +require File.expand_path('../../app/controllers/application_controller', __dir__) +require 'inherited_resources/base' + class Footer < ActiveAdmin::Component def build(namespace) super(id: "footer") @@ -131,6 +153,9 @@ ActiveAdmin.setup do |config| # config.batch_actions = true + # Disable download links globally to prevent UrlGenerationError in Rails 8 + config.download_links = false + # == Controller Filters # diff --git a/admin/config/initializers/active_admin_rails8_patches.rb b/admin/config/initializers/active_admin_rails8_patches.rb new file mode 100644 index 000000000..e5a69b5c4 --- /dev/null +++ b/admin/config/initializers/active_admin_rails8_patches.rb @@ -0,0 +1,53 @@ +# Patches for ActiveAdmin 3.4.0 compatibility with Rails 8.0 +# Fixes ActionController::UrlGenerationError caused by passing `params:` to `url_for`. + +Rails.logger.info "Applying ActiveAdmin Rails 8 Patches..." + +require 'active_admin/views/components/scopes' + +# Patch 1: Scopes - Class override (Fixes UrlGenerationError) +ActiveAdmin::Views::Scopes.class_eval do + def build_scope(scope, options) + li class: classes_for_scope(scope) do + begin + p = params.respond_to?(:to_unsafe_h) ? params.to_unsafe_h : params.to_h + current_params = p.symbolize_keys.except(:page, :scope, :commit, :format, :_method, :authenticity_token, :utf8) + link_options = current_params.merge(scope: scope.id) + + a href: controller.url_for(link_options), class: "table_tools_button" do + text_node scope_name(scope) + span class: "count" do + "(#{get_scope_count(scope)})" + end if options[:scope_count] && get_scope_count(scope) + end + rescue => e + Rails.logger.error "ActiveAdmin Scope Patch Error: #{e.message}" + a href: "#", class: "table_tools_button error" do + text_node scope_name(scope) + end + end + end + end +end + +# Patch 2: Global UrlHelper patch for ArgumentError +# Fixes "arguments passed to url_for can't be handled" which happens in ActiveAdmin download links +module ActionView + module Helpers + module UrlHelper + alias_method :original_url_for_before_patch, :url_for + + def url_for(options = nil) + begin + original_url_for_before_patch(options) + rescue ArgumentError => e + if e.message.include?("arguments passed to url_for can't be handled") + Rails.logger.error "UrlHelper Patch rescued ArgumentError for #{options.inspect}" + return "#" + end + raise e + end + end + end + end +end \ No newline at end of file diff --git a/admin/config/initializers/bugsnag.rb b/admin/config/initializers/bugsnag.rb index 6f5093939..87d4ed626 100644 --- a/admin/config/initializers/bugsnag.rb +++ b/admin/config/initializers/bugsnag.rb @@ -1,3 +1,5 @@ +require 'jam_admin/version' + Bugsnag.configure do |config| config.api_key = "fa0e229f687bcb2c8711fcb80223744e" #config.use_ssl = false diff --git a/admin/config/initializers/influxdb-rails.rb b/admin/config/initializers/influxdb-rails.rb index 5657f7d42..036840db2 100644 --- a/admin/config/initializers/influxdb-rails.rb +++ b/admin/config/initializers/influxdb-rails.rb @@ -7,7 +7,7 @@ InfluxDB::Rails.configure do |config| config.ignored_environments = Rails.application.config.influxdb_ignored_environments config.async = true config.debug = false - config.logger = Logging.logger['InfluxDB'] + # config.logger = Logging.logger['InfluxDB'] config.series_name_for_controller_runtimes = "admin.rails.controller" config.series_name_for_view_runtimes = "admin.rails.view" diff --git a/admin/config/initializers/jam_ruby/promotional.rb b/admin/config/initializers/jam_ruby/promotional.rb index 7d58bf09d..cb935a936 100644 --- a/admin/config/initializers/jam_ruby/promotional.rb +++ b/admin/config/initializers/jam_ruby/promotional.rb @@ -1,3 +1,5 @@ +require File.expand_path('../../../app/uploaders/image_uploader', __dir__) + class JamRuby::PromoBuzz < JamRuby::Promotional mount_uploader :image, ImageUploader end diff --git a/admin/config/initializers/long_running_fix.rb b/admin/config/initializers/long_running_fix.rb new file mode 100644 index 000000000..231e22389 --- /dev/null +++ b/admin/config/initializers/long_running_fix.rb @@ -0,0 +1,2 @@ +# Fix for LongRunning job class missing model_name for ActiveAdmin forms +JamRuby::LongRunning.include ActiveModel::Model if defined?(JamRuby::LongRunning) diff --git a/admin/config/initializers/ransack_allow_all.rb b/admin/config/initializers/ransack_allow_all.rb new file mode 100644 index 000000000..07a782cab --- /dev/null +++ b/admin/config/initializers/ransack_allow_all.rb @@ -0,0 +1,17 @@ +# Allow Ransack to search all attributes and associations by default. +# Required for Ransack 4.0+ upgrade. +# Ideally, this should be done per-model for security, but for legacy app upgrade, we enable all. + +module RansackAllowAll + def ransackable_attributes(auth_object = nil) + column_names + _ransackers.keys + end + + def ransackable_associations(auth_object = nil) + reflect_on_all_associations.map { |a| a.name.to_s } + end +end + +ActiveSupport.on_load(:active_record) do + extend RansackAllowAll +end diff --git a/admin/config/initializers/schools.rb b/admin/config/initializers/schools.rb index ab01fcbf0..0b0279528 100644 --- a/admin/config/initializers/schools.rb +++ b/admin/config/initializers/schools.rb @@ -1,7 +1,7 @@ class JamRuby::School # add a custom validation - :photo_url, :original_fpfile, :cropped_fpfile, :cropped_s3_path, :crop_selection, :large_photo_url, - :cropped_large_s3_path, :cropped_large_fpfile, :jamkazam_rate, :second_ed, :education, :special, :base_rate, as: :admin + # attr_accessible :photo_url, :original_fpfile, :cropped_fpfile, :cropped_s3_path, :crop_selection, :large_photo_url, + # :cropped_large_s3_path, :cropped_large_fpfile, :jamkazam_rate, :second_ed, :education, :special, :base_rate, as: :admin end diff --git a/admin/spec/features/menu_click_spec.rb b/admin/spec/features/menu_click_spec.rb new file mode 100644 index 000000000..8810560ae --- /dev/null +++ b/admin/spec/features/menu_click_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe 'Admin Menu', :type => :feature do + before do + # Ensure dependencies for factory are present + unless JamRuby::Instrument.exists?('electric guitar') + JamRuby::Instrument.create(id: 'electric guitar', description: 'Electric Guitar') + end + end + + let(:admin) { FactoryBot.create(:admin) } + + before do + sign_in(admin) + end + + it 'allows clicking through all menu items' do + visit root_path + + # Collect links + links = [] + + # ActiveAdmin menu structure selectors + # Top level links + all('#header ul#tabs > li > a', visible: :all).each do |a| + href = a[:href] + links << href if href && href != '#' && !href.start_with?('javascript:') + end + + # Submenu links + all('#header ul#tabs > li > ul > li > a', visible: :all).each do |a| + href = a[:href] + links << href if href && href != '#' && !href.start_with?('javascript:') + end + + links.uniq! + # Remove logout to avoid ending session + links.reject! { |l| l.include?('logout') || l.include?('sign_out') } + + # Ignore pages referring to Gift Cards, Lessons, Students, or Teachers + links.reject! { |l| l =~ /gift_card|lesson|student|teacher|slow_responses|jam_class|jamclass|posa|onboarder|schooluserupload/i } + + puts "Testing #{links.size} menu links: #{links.inspect}" + + links.each do |link| + visit link + + # Check for common Rails/ActiveAdmin error indicators + if page.has_content?("We're sorry, but something went wrong") || + page.has_content?("Exception") || + (page.respond_to?(:status_code) && page.status_code == 500) + fail "Failed to load #{link}" + end + + expect(page.status_code).to eq(200) if page.respond_to?(:status_code) + end + end +end diff --git a/features/conversion-reporting.md b/features/conversion-reporting.md new file mode 100644 index 000000000..a32e95cf6 --- /dev/null +++ b/features/conversion-reporting.md @@ -0,0 +1,112 @@ +Add a filter to the current report (link below) that lets us view: All, Organic, Advertising, or Campaign. And if Campaign is selected, add a drop down list box with Campaign strings. + +https://www.jamkazam.com/admin/admin/jammers_subscription_cohorts + +Add a field to the user record in the database that records if a user signup was Organic or Advertising, and if Advertising, add a field to capture/store the Campaign string. + +Implement the tech to enable data-driven analysis and advertising optimization for the Meta ad network, as described in the Claude AI notes below. Basically, if we place an ads on Facebook/Instagram/etc, that ad will cause a campaign attribution string to be included in the URL a visitor uses to come to the JamKazam website (typically the Wordpress marketing site). If the user signs up for a JamKazam account, we should tag their account as described above, so that we know this account was generated from Advertising, and from the specific Campaign string in the URL for attribution. Importantly, we should also implement a conversion indicator that reports back to Meta when a user hits the conversion funnel step of Gear Setup (i.e. they have downloaded the desktop app AND then run the FTUE gear setup wizard to set up some gear in the app). We will consider this a “conversion” for the purposes of evaluating and optimizing Meta ads (and Meta will itself use this data to optimize our ROI on ad placements, given that these days AI is being used to make ads more efficient than we can manually accomplish via more traditional targeting methodologies). + +Here’s my prompt to Claude: “I'm interested in testing the use of Facebook ads to promote the use of my company's app. It's a desktop app, not a mobile app. It looks like online advertising tech has changed since I last used it, and there is automated optimization for targeting the best prospective users with the best ROI on ad spend. To take advantage of this new optimization, I've read that a good way to use it with an app is to identify a success benchmark and report that benchmark back to the Facebook ads platform for each referred user that "converts" - where converts is defined as hitting that benchmark or milestone. In my case, conversion would equate to having the user download and install the app, and then complete one specified action in the app. This is more complex and time-consuming than, for example, just following an ad link to a particular page, and this process will not live within a single browser session. So I think I would need to somehow call a Facebook Ads API (or something like that) to report back a conversion success on a referred user. To do this, I think I would need some kind of unique ID for each user referred from a Facebook ad, and an understanding of the API (or URL or other tech) used to report this conversion success. Can you please help me by describing how to implement this process technically?” + +And below is Claude’s answer: + +You're on the right track! What you're describing is Facebook's Conversions API (formerly called Server-Side API), which is designed exactly for this scenario. Here's how to implement it: + +Technical Overview +Capture Facebook Click ID (fbclid) at Landing + +When someone clicks your Facebook ad, Facebook appends a fbclid parameter to the URL (e.g., yoursite.com?fbclid=ABC123). You need to: + +Capture this fbclid from the URL when the user lands on your download page + +Store it in a cookie or local storage (Facebook recommends a cookie named _fbc with format fb.1.timestamp.fbclid) + +Also capture the _fbp cookie (Facebook Pixel browser ID) if you're using the Facebook Pixel + +Pass the Click ID Through Your Download Flow + +When the user downloads your app installer, you need to pass this tracking data to the app. Common approaches: + +URL parameters in download link: Embed the fbclid/fbc in the download URL itself + +Server-side tracking: Store the fbclid associated with the download session on your server + +Installer metadata: Some installers can accept custom parameters + +Store the Click ID in Your App + +When your desktop app first runs after installation, capture and store the tracking identifier locally (registry, local file, or app database). + +Report Conversion via Conversions API + +When the user completes your target action, make an HTTPS POST request to Facebook's Conversions API: + + + +POST https://graph.facebook.com/v18.0/{PIXEL_ID}/events +Required parameters: + +access_token: Your Conversions API access token + +data: JSON array containing event data + +Example payload: + +json + + + +{ + "data": [ + { + "event_name": "CompleteAction", + "event_time": 1702345678, + "action_source": "app", + "user_data": { + "fbc": "fb.1.1702345600.ABC123", + "fbp": "fb.1.1702340000.987654321", + "em": "hashed_email@example.com", // SHA256 hashed + "client_user_agent": "Mozilla/5.0...", + "client_ip_address": "192.168.1.1" + }, + "event_source_url": "https://yoursite.com/download" + } + ] +} +Implementation Steps +Set up Facebook Pixel on your landing/download page (optional but recommended for attribution) + +Get Conversions API credentials from Facebook Business Manager: + +Your Pixel ID + +Generate an access token with ads_management permission + +Capture tracking data on your website: + +javascript + + + +// Capture fbclid from URL +const urlParams = new URLSearchParams(window.location.search); +const fbclid = urlParams.get('fbclid'); +// Store in cookie for 90 days +if (fbclid) { + const timestamp = Math.floor(Date.now() / 1000); + document.cookie = `_fbc=fb.1.${timestamp}.${fbclid}; max-age=7776000; path=/`; +} +Associate tracking data with download - perhaps by generating a unique download token that includes the fbc/fbp values + +In your desktop app, when the conversion action occurs, make the API call (from your backend server, not directly from the app for security) + +Important Considerations +Privacy/GDPR: Ensure you have proper consent and privacy policies + +Hash PII: Email addresses and phone numbers must be SHA256 hashed + +Event deduplication: Use event_id to prevent duplicate reporting + +Test events: Use the Test Events feature in Facebook Events Manager during development + +Attribution window: Facebook typically has a 7-day click attribution window diff --git a/ruby/db/migrate/20260111000000_add_reset_password_sent_at_to_users.rb b/ruby/db/migrate/20260111000000_add_reset_password_sent_at_to_users.rb new file mode 100644 index 000000000..0fe0d558f --- /dev/null +++ b/ruby/db/migrate/20260111000000_add_reset_password_sent_at_to_users.rb @@ -0,0 +1,5 @@ +class AddResetPasswordSentAtToUsers < ActiveRecord::Migration[4.2] + def change + add_column :users, :reset_password_sent_at, :datetime + end +end diff --git a/ruby/spec/support/bugsnag_stub.rb b/ruby/spec/support/bugsnag_stub.rb new file mode 100644 index 000000000..aafd1a825 --- /dev/null +++ b/ruby/spec/support/bugsnag_stub.rb @@ -0,0 +1,6 @@ + +class Bugsnag + def self.notify(e, &block) + # no-op + end +end