From 466c0ddb882c26462db8e1e94362c18555a2777a Mon Sep 17 00:00:00 2001 From: Vishal Sadriya Date: Thu, 14 May 2026 12:39:17 +0530 Subject: [PATCH] feat: configurable base controller class Add SolidQueueMonitor.base_controller_class so host apps can plug the dashboard into their existing auth chain (Devise, Pundit, OmniAuth, custom sessions, etc.) instead of being limited to HTTP Basic. Default is "ActionController::Base", so existing setups behave identically. Engine helpers are wired explicitly via `helper Engine.helpers` so view methods like render_chart keep working when the parent is not ActionController::Base. Chart timezone behavior (already correct via Time.current end-to-end since v2.0) is now pinned by specs running in America/Los_Angeles. Bumps version to 2.1.0. --- CHANGELOG.md | 15 +++++ Gemfile.lock | 2 +- README.md | 46 +++++++++++++ .../application_controller.rb | 8 ++- lib/solid_queue_monitor.rb | 9 ++- lib/solid_queue_monitor/version.rb | 2 +- spec/lib/solid_queue_monitor_spec.rb | 64 +++++++++++++++++++ .../chart_data_service_spec.rb | 32 ++++++++++ 8 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 spec/lib/solid_queue_monitor_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e6be7e..ee947a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [2.1.0] - 2026-05-13 + +### Added + +- `SolidQueueMonitor.base_controller_class` config option. Set it to the name of a host-app controller (e.g. `'AdminController'`) and the engine's `ApplicationController` will inherit from that class, so every `before_action`, `rescue_from`, layout, and `current_user` helper cascades into the dashboard. This unblocks integration with Devise, Pundit, OmniAuth, and custom session middleware without monkey-patching. Defaults to `'ActionController::Base'`; behaviour is unchanged when not set. +- README "Custom Authentication" section documenting the integration pattern with minimal and role-gated examples. + +### Changed + +- The activity chart's x-axis labels and bucket boundaries now consistently use the host application's `Time.zone` instead of UTC. No new config knob — set `config.time_zone` in `config/application.rb` as usual. Tests now pin this behaviour in `America/Los_Angeles`. + +### Migration + +`bundle update solid_queue_monitor` is sufficient. The default behaviour matches v2.0.0 exactly; both features are opt-in (the chart automatically picks up your existing `Time.zone`). + ## [2.0.0] - 2026-05-12 ### Changed diff --git a/Gemfile.lock b/Gemfile.lock index 40fb3bc..c1230c4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - solid_queue_monitor (2.0.0) + solid_queue_monitor (2.1.0) rails (>= 7.0) solid_queue (>= 0.1.0) diff --git a/README.md b/README.md index fbeda8a..3dba0ec 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,10 @@ SolidQueueMonitor.setup do |config| # Disable the chart on the overview page to skip chart queries entirely # config.show_chart = true end + +# Optional: inherit from a host-app controller to plug into your existing auth. +# See "Custom Authentication" below. Defaults to "ActionController::Base". +# SolidQueueMonitor.base_controller_class = 'AdminController' ``` ### Performance at Scale @@ -159,6 +163,48 @@ config.username = -> { Rails.application.credentials.dig(:solid_queue_monitor, : config.password = -> { Rails.application.credentials.dig(:solid_queue_monitor, :password) } ``` +### Custom Authentication + +By default, Solid Queue Monitor uses HTTP Basic auth with the username/password from `SolidQueueMonitor.setup`. To integrate with your app's existing auth (Devise, Pundit, OmniAuth, custom sessions, etc.), point the engine at a base controller from your host app: + +```ruby +# config/initializers/solid_queue_monitor.rb +SolidQueueMonitor.setup do |config| + config.authentication_enabled = false # disable HTTP Basic +end + +# Inherit from your own controller so its before_actions, rescue_froms, +# layout, and current_user helper cascade into the engine. +SolidQueueMonitor.base_controller_class = 'AdminController' +``` + +**Minimal example — just authenticate:** + +```ruby +class AdminController < ApplicationController + before_action :authenticate_user! # Devise (or your equivalent) +end +``` + +**Richer example — require an admin role:** + +```ruby +class AdminController < ApplicationController + before_action :authenticate_user! + before_action :require_admin + + private + + def require_admin + redirect_to root_path, alert: 'Not authorized' unless current_user&.admin? + end +end +``` + +Leave `authentication_enabled = true` if you want HTTP Basic to run *on top of* your host auth (host runs first, HTTP Basic second). Most adopters disable it. + +Restart your server after changing this config — the class hierarchy is set at load time, so config changes won't take effect on a live process. + ## Usage After installation, visit `/solid_queue` in your browser to access the dashboard. diff --git a/app/controllers/solid_queue_monitor/application_controller.rb b/app/controllers/solid_queue_monitor/application_controller.rb index 5761622..9a84c4a 100644 --- a/app/controllers/solid_queue_monitor/application_controller.rb +++ b/app/controllers/solid_queue_monitor/application_controller.rb @@ -1,10 +1,16 @@ # frozen_string_literal: true module SolidQueueMonitor - class ApplicationController < ActionController::Base + class ApplicationController < SolidQueueMonitor.base_controller_class.safe_constantize || ActionController::Base include ActionController::HttpAuthentication::Basic::ControllerMethods include ActionController::Flash + # Explicitly include the engine's helpers so they remain available when the + # host configures a custom base_controller_class. Rails auto-includes engine + # helpers only when the parent is ActionController::Base; inheriting from a + # host controller short-circuits that, breaking view methods like render_chart. + helper SolidQueueMonitor::Engine.helpers + before_action :authenticate, if: -> { SolidQueueMonitor::AuthenticationService.authentication_required? } layout 'solid_queue_monitor/application' skip_before_action :verify_authenticity_token diff --git a/lib/solid_queue_monitor.rb b/lib/solid_queue_monitor.rb index daf912b..9e32ffd 100644 --- a/lib/solid_queue_monitor.rb +++ b/lib/solid_queue_monitor.rb @@ -5,8 +5,11 @@ module SolidQueueMonitor class Error < StandardError; end + + DEFAULT_BASE_CONTROLLER_CLASS = 'ActionController::Base' + class << self - attr_writer :username, :password + attr_writer :username, :password, :base_controller_class attr_accessor :jobs_per_page, :authentication_enabled, :auto_refresh_enabled, :auto_refresh_interval, :show_chart @@ -18,6 +21,10 @@ def password resolve_value(@password) end + def base_controller_class + @base_controller_class || DEFAULT_BASE_CONTROLLER_CLASS + end + private def resolve_value(value) diff --git a/lib/solid_queue_monitor/version.rb b/lib/solid_queue_monitor/version.rb index d341a1c..d050617 100644 --- a/lib/solid_queue_monitor/version.rb +++ b/lib/solid_queue_monitor/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SolidQueueMonitor - VERSION = '2.0.0' + VERSION = '2.1.0' end diff --git a/spec/lib/solid_queue_monitor_spec.rb b/spec/lib/solid_queue_monitor_spec.rb new file mode 100644 index 0000000..703cae4 --- /dev/null +++ b/spec/lib/solid_queue_monitor_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SolidQueueMonitor do + describe '.base_controller_class' do + after { described_class.base_controller_class = nil } + + it 'defaults to "ActionController::Base"' do + described_class.base_controller_class = nil + expect(described_class.base_controller_class).to eq('ActionController::Base') + end + + it 'returns the configured class name when set' do + described_class.base_controller_class = 'AdminController' + expect(described_class.base_controller_class).to eq('AdminController') + end + + it 'falls back to the default when reset to nil' do + described_class.base_controller_class = 'AdminController' + described_class.base_controller_class = nil + expect(described_class.base_controller_class).to eq('ActionController::Base') + end + end + + describe 'ApplicationController parent class' do + it 'inherits from ActionController::Base by default' do + expect(SolidQueueMonitor::ApplicationController.ancestors).to include(ActionController::Base) + end + + it 'resolves the configured class via safe_constantize at load time' do + # The application_controller.rb file uses + # SolidQueueMonitor.base_controller_class.safe_constantize || ActionController::Base + # at class-definition time. Re-evaluate the same expression here to confirm + # the resolution logic works as documented. + expect(described_class.base_controller_class.safe_constantize).to eq(ActionController::Base) + end + + it 'falls back to ActionController::Base when the configured name does not resolve' do + described_class.base_controller_class = 'NotAClass::ThatExists' + resolved = described_class.base_controller_class.safe_constantize || ActionController::Base + expect(resolved).to eq(ActionController::Base) + ensure + described_class.base_controller_class = nil + end + end + + describe 'engine helper wiring' do + # Regression guard: when ApplicationController inherits from a non-AC::Base + # parent (custom base_controller_class), Rails will NOT auto-include the + # engine's helpers. ApplicationController must include them explicitly via + # `helper SolidQueueMonitor::Engine.helpers` so views keep working. + let(:helper_methods) { SolidQueueMonitor::ApplicationController._helpers.instance_methods } + + it 'exposes ChartHelper#render_chart on the controller helper module' do + expect(helper_methods).to include(:render_chart) + end + + it 'exposes the rest of the engine helpers' do + # One method per engine helper module, to catch any missing wiring. + expect(helper_methods).to include(:sortable_header, :visible_pages) + end + end +end diff --git a/spec/services/solid_queue_monitor/chart_data_service_spec.rb b/spec/services/solid_queue_monitor/chart_data_service_spec.rb index 2ef091a..94fec5e 100644 --- a/spec/services/solid_queue_monitor/chart_data_service_spec.rb +++ b/spec/services/solid_queue_monitor/chart_data_service_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe SolidQueueMonitor::ChartDataService do + include ActiveSupport::Testing::TimeHelpers + describe '#calculate' do let(:service) { described_class.new(time_range: time_range) } let(:time_range) { '1d' } @@ -100,6 +102,36 @@ expect(service.calculate[:created].sum).to eq(0) end end + + context 'when the host application configures a non-UTC time zone' do + # 2026-05-13 17:00 UTC == 2026-05-13 10:00 America/Los_Angeles (PDT). + let(:frozen_utc) { Time.utc(2026, 5, 13, 17, 0, 0) } + + around { |example| Time.use_zone('America/Los_Angeles') { example.run } } + before { travel_to(frozen_utc) } + + it 'formats 1d x-axis labels in the host time zone' do + labels = described_class.new(time_range: '1d').calculate[:labels] + # 24 hourly buckets walking forward from (now - 1 day) at 10:00 PT + # back to now at 09:00 PT (23 hours later). + expect(labels.first).to eq('10:00') + expect(labels.last).to eq('09:00') + end + + it 'does not format 1d labels in UTC' do + labels = described_class.new(time_range: '1d').calculate[:labels] + # In UTC the same range would start at 17:00 and end at 16:00. + expect(labels.first).not_to eq('17:00') + expect(labels.last).not_to eq('16:00') + end + + it 'formats fine-grained 1h labels in the host time zone' do + labels = described_class.new(time_range: '1h').calculate[:labels] + # 12 buckets, 5 minutes each, walking from 09:00 PT to 09:55 PT. + expect(labels.first).to eq('09:00') + expect(labels.last).to eq('09:55') + end + end end describe 'constants' do