Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 8 additions & 1 deletion lib/solid_queue_monitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/solid_queue_monitor/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module SolidQueueMonitor
VERSION = '2.0.0'
VERSION = '2.1.0'
end
64 changes: 64 additions & 0 deletions spec/lib/solid_queue_monitor_spec.rb
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions spec/services/solid_queue_monitor/chart_data_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand Down Expand Up @@ -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
Expand Down
Loading