Skip to content

Commit bba4d63

Browse files
authored
feat: configurable base controller class (#39)
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.
1 parent fbb65cb commit bba4d63

8 files changed

Lines changed: 174 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Changelog
22

3+
## [2.1.0] - 2026-05-13
4+
5+
### Added
6+
7+
- `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.
8+
- README "Custom Authentication" section documenting the integration pattern with minimal and role-gated examples.
9+
10+
### Changed
11+
12+
- 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`.
13+
14+
### Migration
15+
16+
`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`).
17+
318
## [2.0.0] - 2026-05-12
419

520
### Changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
solid_queue_monitor (2.0.0)
4+
solid_queue_monitor (2.1.0)
55
rails (>= 7.0)
66
solid_queue (>= 0.1.0)
77

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ SolidQueueMonitor.setup do |config|
124124
# Disable the chart on the overview page to skip chart queries entirely
125125
# config.show_chart = true
126126
end
127+
128+
# Optional: inherit from a host-app controller to plug into your existing auth.
129+
# See "Custom Authentication" below. Defaults to "ActionController::Base".
130+
# SolidQueueMonitor.base_controller_class = 'AdminController'
127131
```
128132

129133
### Performance at Scale
@@ -159,6 +163,48 @@ config.username = -> { Rails.application.credentials.dig(:solid_queue_monitor, :
159163
config.password = -> { Rails.application.credentials.dig(:solid_queue_monitor, :password) }
160164
```
161165

166+
### Custom Authentication
167+
168+
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:
169+
170+
```ruby
171+
# config/initializers/solid_queue_monitor.rb
172+
SolidQueueMonitor.setup do |config|
173+
config.authentication_enabled = false # disable HTTP Basic
174+
end
175+
176+
# Inherit from your own controller so its before_actions, rescue_froms,
177+
# layout, and current_user helper cascade into the engine.
178+
SolidQueueMonitor.base_controller_class = 'AdminController'
179+
```
180+
181+
**Minimal example — just authenticate:**
182+
183+
```ruby
184+
class AdminController < ApplicationController
185+
before_action :authenticate_user! # Devise (or your equivalent)
186+
end
187+
```
188+
189+
**Richer example — require an admin role:**
190+
191+
```ruby
192+
class AdminController < ApplicationController
193+
before_action :authenticate_user!
194+
before_action :require_admin
195+
196+
private
197+
198+
def require_admin
199+
redirect_to root_path, alert: 'Not authorized' unless current_user&.admin?
200+
end
201+
end
202+
```
203+
204+
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.
205+
206+
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.
207+
162208
## Usage
163209

164210
After installation, visit `/solid_queue` in your browser to access the dashboard.

app/controllers/solid_queue_monitor/application_controller.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
# frozen_string_literal: true
22

33
module SolidQueueMonitor
4-
class ApplicationController < ActionController::Base
4+
class ApplicationController < SolidQueueMonitor.base_controller_class.safe_constantize || ActionController::Base
55
include ActionController::HttpAuthentication::Basic::ControllerMethods
66
include ActionController::Flash
77

8+
# Explicitly include the engine's helpers so they remain available when the
9+
# host configures a custom base_controller_class. Rails auto-includes engine
10+
# helpers only when the parent is ActionController::Base; inheriting from a
11+
# host controller short-circuits that, breaking view methods like render_chart.
12+
helper SolidQueueMonitor::Engine.helpers
13+
814
before_action :authenticate, if: -> { SolidQueueMonitor::AuthenticationService.authentication_required? }
915
layout 'solid_queue_monitor/application'
1016
skip_before_action :verify_authenticity_token

lib/solid_queue_monitor.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55

66
module SolidQueueMonitor
77
class Error < StandardError; end
8+
9+
DEFAULT_BASE_CONTROLLER_CLASS = 'ActionController::Base'
10+
811
class << self
9-
attr_writer :username, :password
12+
attr_writer :username, :password, :base_controller_class
1013
attr_accessor :jobs_per_page, :authentication_enabled,
1114
:auto_refresh_enabled, :auto_refresh_interval, :show_chart
1215

@@ -18,6 +21,10 @@ def password
1821
resolve_value(@password)
1922
end
2023

24+
def base_controller_class
25+
@base_controller_class || DEFAULT_BASE_CONTROLLER_CLASS
26+
end
27+
2128
private
2229

2330
def resolve_value(value)

lib/solid_queue_monitor/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module SolidQueueMonitor
4-
VERSION = '2.0.0'
4+
VERSION = '2.1.0'
55
end
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
RSpec.describe SolidQueueMonitor do
6+
describe '.base_controller_class' do
7+
after { described_class.base_controller_class = nil }
8+
9+
it 'defaults to "ActionController::Base"' do
10+
described_class.base_controller_class = nil
11+
expect(described_class.base_controller_class).to eq('ActionController::Base')
12+
end
13+
14+
it 'returns the configured class name when set' do
15+
described_class.base_controller_class = 'AdminController'
16+
expect(described_class.base_controller_class).to eq('AdminController')
17+
end
18+
19+
it 'falls back to the default when reset to nil' do
20+
described_class.base_controller_class = 'AdminController'
21+
described_class.base_controller_class = nil
22+
expect(described_class.base_controller_class).to eq('ActionController::Base')
23+
end
24+
end
25+
26+
describe 'ApplicationController parent class' do
27+
it 'inherits from ActionController::Base by default' do
28+
expect(SolidQueueMonitor::ApplicationController.ancestors).to include(ActionController::Base)
29+
end
30+
31+
it 'resolves the configured class via safe_constantize at load time' do
32+
# The application_controller.rb file uses
33+
# SolidQueueMonitor.base_controller_class.safe_constantize || ActionController::Base
34+
# at class-definition time. Re-evaluate the same expression here to confirm
35+
# the resolution logic works as documented.
36+
expect(described_class.base_controller_class.safe_constantize).to eq(ActionController::Base)
37+
end
38+
39+
it 'falls back to ActionController::Base when the configured name does not resolve' do
40+
described_class.base_controller_class = 'NotAClass::ThatExists'
41+
resolved = described_class.base_controller_class.safe_constantize || ActionController::Base
42+
expect(resolved).to eq(ActionController::Base)
43+
ensure
44+
described_class.base_controller_class = nil
45+
end
46+
end
47+
48+
describe 'engine helper wiring' do
49+
# Regression guard: when ApplicationController inherits from a non-AC::Base
50+
# parent (custom base_controller_class), Rails will NOT auto-include the
51+
# engine's helpers. ApplicationController must include them explicitly via
52+
# `helper SolidQueueMonitor::Engine.helpers` so views keep working.
53+
let(:helper_methods) { SolidQueueMonitor::ApplicationController._helpers.instance_methods }
54+
55+
it 'exposes ChartHelper#render_chart on the controller helper module' do
56+
expect(helper_methods).to include(:render_chart)
57+
end
58+
59+
it 'exposes the rest of the engine helpers' do
60+
# One method per engine helper module, to catch any missing wiring.
61+
expect(helper_methods).to include(:sortable_header, :visible_pages)
62+
end
63+
end
64+
end

spec/services/solid_queue_monitor/chart_data_service_spec.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
require 'spec_helper'
44

55
RSpec.describe SolidQueueMonitor::ChartDataService do
6+
include ActiveSupport::Testing::TimeHelpers
7+
68
describe '#calculate' do
79
let(:service) { described_class.new(time_range: time_range) }
810
let(:time_range) { '1d' }
@@ -100,6 +102,36 @@
100102
expect(service.calculate[:created].sum).to eq(0)
101103
end
102104
end
105+
106+
context 'when the host application configures a non-UTC time zone' do
107+
# 2026-05-13 17:00 UTC == 2026-05-13 10:00 America/Los_Angeles (PDT).
108+
let(:frozen_utc) { Time.utc(2026, 5, 13, 17, 0, 0) }
109+
110+
around { |example| Time.use_zone('America/Los_Angeles') { example.run } }
111+
before { travel_to(frozen_utc) }
112+
113+
it 'formats 1d x-axis labels in the host time zone' do
114+
labels = described_class.new(time_range: '1d').calculate[:labels]
115+
# 24 hourly buckets walking forward from (now - 1 day) at 10:00 PT
116+
# back to now at 09:00 PT (23 hours later).
117+
expect(labels.first).to eq('10:00')
118+
expect(labels.last).to eq('09:00')
119+
end
120+
121+
it 'does not format 1d labels in UTC' do
122+
labels = described_class.new(time_range: '1d').calculate[:labels]
123+
# In UTC the same range would start at 17:00 and end at 16:00.
124+
expect(labels.first).not_to eq('17:00')
125+
expect(labels.last).not_to eq('16:00')
126+
end
127+
128+
it 'formats fine-grained 1h labels in the host time zone' do
129+
labels = described_class.new(time_range: '1h').calculate[:labels]
130+
# 12 buckets, 5 minutes each, walking from 09:00 PT to 09:55 PT.
131+
expect(labels.first).to eq('09:00')
132+
expect(labels.last).to eq('09:55')
133+
end
134+
end
103135
end
104136

105137
describe 'constants' do

0 commit comments

Comments
 (0)