solid_queue_monitor 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2597291a879f3f6720e783c6aae3fa203434a67652ba24c5e65a6fdd87081120
4
- data.tar.gz: d01e77ac3c412bf2fcdab1c4f1a02aa072786f7d88ec42798758615159ddc67c
3
+ metadata.gz: 385e4d7ff7dc636b9448cefef256495f8c7aefd2e3318c57a6637c69d4cb5c1e
4
+ data.tar.gz: c4f7d1ee427a0cd5255b345bcff159fc88f3120b893fe103fb94701c8a33f446
5
5
  SHA512:
6
- metadata.gz: 9f95324749998dd5c7a6e0598a25edd14f82bbc32610499dcf688592729f4cbf805bf3ac8da8ad3f776c0dc3231ebaa6fa710fac5537806553cc3cc50f3a7d66
7
- data.tar.gz: be751aaa21b617d1ba1f09eb473a9bbd83eeccfbe79c918040fe2df420c76258b012fad5c26eefa1aaaafe50e62fdf6d566b08469042006a501d9f354cd49658
6
+ metadata.gz: 29410fb9b7d8dfba02eb65f673462c822fdef643701c637fa7531aa11cac3e5ce9530146127b32a68f2128666e71f23fd9ecd2df8de924752193274393b03b18
7
+ data.tar.gz: c01b1718f58067a0488d75f359d33c6ef1802105f528410f03ac52b17f1944adcc767fc7fb46500a5e88f33004497ab980328f5c948eb25d2b477c44e6c19cdf
data/README.md CHANGED
@@ -16,6 +16,8 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
16
16
  ## Features
17
17
 
18
18
  - **Dashboard Overview**: Get a quick snapshot of your queue's health with statistics on all job types
19
+ - **Job Activity Chart**: Visual line chart showing jobs created, completed, and failed over time with 9 time range options (15m to 1 week)
20
+ - **Dark Theme**: Toggle between light and dark themes with system preference detection and localStorage persistence
19
21
  - **Ready Jobs**: View jobs that are ready to be executed
20
22
  - **In Progress Jobs**: Monitor jobs currently being processed by workers
21
23
  - **Scheduled Jobs**: See upcoming jobs scheduled for future execution with ability to execute immediately or reject permanently
@@ -33,9 +35,13 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
33
35
 
34
36
  ## Screenshots
35
37
 
36
- ### Dashboard Overview
38
+ ### Dashboard Overview (Light Theme)
37
39
 
38
- ![Dashboard Overview](screenshots/dashboard-3.png)
40
+ ![Dashboard Overview - Light Theme](screenshots/dashboard-light.png)
41
+
42
+ ### Dashboard Overview (Dark Theme)
43
+
44
+ ![Dashboard Overview - Dark Theme](screenshots/dashboard-dark.png)
39
45
 
40
46
  ### Failed Jobs
41
47
 
@@ -46,7 +52,7 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
46
52
  Add this line to your application's Gemfile:
47
53
 
48
54
  ```ruby
49
- gem 'solid_queue_monitor', '~> 0.4.0'
55
+ gem 'solid_queue_monitor', '~> 0.6.0'
50
56
  ```
51
57
 
52
58
  Then execute:
@@ -4,6 +4,7 @@ module SolidQueueMonitor
4
4
  class OverviewController < BaseController
5
5
  def index
6
6
  @stats = SolidQueueMonitor::StatsCalculator.calculate
7
+ @chart_data = SolidQueueMonitor::ChartDataService.new(time_range: time_range_param).calculate
7
8
 
8
9
  recent_jobs_query = SolidQueue::Job.order(created_at: :desc).limit(100)
9
10
  @recent_jobs = paginate(filter_jobs(recent_jobs_query))
@@ -13,10 +14,20 @@ module SolidQueueMonitor
13
14
  render_page('Overview', generate_overview_content)
14
15
  end
15
16
 
17
+ def chart_data
18
+ chart_data = SolidQueueMonitor::ChartDataService.new(time_range: time_range_param).calculate
19
+ render json: chart_data
20
+ end
21
+
16
22
  private
17
23
 
24
+ def time_range_param
25
+ params[:time_range] || ChartDataService::DEFAULT_TIME_RANGE
26
+ end
27
+
18
28
  def generate_overview_content
19
29
  SolidQueueMonitor::StatsPresenter.new(@stats).render +
30
+ SolidQueueMonitor::ChartPresenter.new(@chart_data).render +
20
31
  SolidQueueMonitor::JobsPresenter.new(@recent_jobs[:records],
21
32
  current_page: @recent_jobs[:current_page],
22
33
  total_pages: @recent_jobs[:total_pages],
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueMonitor
4
+ class ChartDataService
5
+ TIME_RANGES = {
6
+ '15m' => { duration: 15.minutes, buckets: 15, label_format: '%H:%M', label: 'Last 15 minutes' },
7
+ '30m' => { duration: 30.minutes, buckets: 15, label_format: '%H:%M', label: 'Last 30 minutes' },
8
+ '1h' => { duration: 1.hour, buckets: 12, label_format: '%H:%M', label: 'Last 1 hour' },
9
+ '3h' => { duration: 3.hours, buckets: 18, label_format: '%H:%M', label: 'Last 3 hours' },
10
+ '6h' => { duration: 6.hours, buckets: 24, label_format: '%H:%M', label: 'Last 6 hours' },
11
+ '12h' => { duration: 12.hours, buckets: 24, label_format: '%H:%M', label: 'Last 12 hours' },
12
+ '1d' => { duration: 1.day, buckets: 24, label_format: '%H:%M', label: 'Last 24 hours' },
13
+ '3d' => { duration: 3.days, buckets: 36, label_format: '%m/%d %H:%M', label: 'Last 3 days' },
14
+ '1w' => { duration: 7.days, buckets: 28, label_format: '%m/%d', label: 'Last 7 days' }
15
+ }.freeze
16
+
17
+ DEFAULT_TIME_RANGE = '1d'
18
+
19
+ def initialize(time_range: DEFAULT_TIME_RANGE)
20
+ @time_range = TIME_RANGES.key?(time_range) ? time_range : DEFAULT_TIME_RANGE
21
+ @config = TIME_RANGES[@time_range]
22
+ end
23
+
24
+ def calculate
25
+ end_time = Time.current
26
+ start_time = end_time - @config[:duration]
27
+ bucket_duration = @config[:duration] / @config[:buckets]
28
+
29
+ buckets = build_buckets(start_time, bucket_duration)
30
+
31
+ created_counts = fetch_created_counts(start_time, end_time)
32
+ completed_counts = fetch_completed_counts(start_time, end_time)
33
+ failed_counts = fetch_failed_counts(start_time, end_time)
34
+
35
+ created_data = assign_to_buckets(created_counts, buckets, bucket_duration)
36
+ completed_data = assign_to_buckets(completed_counts, buckets, bucket_duration)
37
+ failed_data = assign_to_buckets(failed_counts, buckets, bucket_duration)
38
+
39
+ {
40
+ labels: buckets.map { |b| b[:label] }, # rubocop:disable Rails/Pluck
41
+ created: created_data,
42
+ completed: completed_data,
43
+ failed: failed_data,
44
+ totals: {
45
+ created: created_data.sum,
46
+ completed: completed_data.sum,
47
+ failed: failed_data.sum
48
+ },
49
+ time_range: @time_range,
50
+ time_range_label: @config[:label],
51
+ available_ranges: TIME_RANGES.transform_values { |v| v[:label] }
52
+ }
53
+ end
54
+
55
+ private
56
+
57
+ def build_buckets(start_time, bucket_duration)
58
+ @config[:buckets].times.map do |i|
59
+ bucket_start = start_time + (i * bucket_duration)
60
+ {
61
+ start: bucket_start,
62
+ end: bucket_start + bucket_duration,
63
+ label: bucket_start.strftime(@config[:label_format])
64
+ }
65
+ end
66
+ end
67
+
68
+ def fetch_created_counts(start_time, end_time)
69
+ SolidQueue::Job
70
+ .where(created_at: start_time..end_time)
71
+ .pluck(:created_at)
72
+ end
73
+
74
+ def fetch_completed_counts(start_time, end_time)
75
+ SolidQueue::Job
76
+ .where(finished_at: start_time..end_time)
77
+ .where.not(finished_at: nil)
78
+ .pluck(:finished_at)
79
+ end
80
+
81
+ def fetch_failed_counts(start_time, end_time)
82
+ SolidQueue::FailedExecution
83
+ .where(created_at: start_time..end_time)
84
+ .pluck(:created_at)
85
+ end
86
+
87
+ def assign_to_buckets(timestamps, buckets, _bucket_duration)
88
+ counts = Array.new(buckets.size, 0)
89
+
90
+ timestamps.each do |timestamp|
91
+ bucket_index = buckets.find_index do |bucket|
92
+ timestamp >= bucket[:start] && timestamp < bucket[:end]
93
+ end
94
+ counts[bucket_index] += 1 if bucket_index
95
+ end
96
+
97
+ counts
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueMonitor
4
+ class ChartPresenter
5
+ CHART_WIDTH = 1200
6
+ CHART_HEIGHT = 280
7
+ PADDING = { top: 40, right: 30, bottom: 60, left: 60 }.freeze
8
+ COLORS = {
9
+ created: '#3b82f6', # Blue
10
+ completed: '#10b981', # Green
11
+ failed: '#ef4444' # Red
12
+ }.freeze
13
+
14
+ def initialize(chart_data)
15
+ @data = chart_data
16
+ @plot_width = CHART_WIDTH - PADDING[:left] - PADDING[:right]
17
+ @plot_height = CHART_HEIGHT - PADDING[:top] - PADDING[:bottom]
18
+ end
19
+
20
+ def render
21
+ <<-HTML
22
+ <div class="chart-section" id="chart-section">
23
+ <div class="chart-header">
24
+ <div class="chart-header-left">
25
+ <button class="chart-toggle-btn" id="chart-toggle-btn" title="Toggle chart">
26
+ <svg class="chart-toggle-icon" id="chart-toggle-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
27
+ <polyline points="6 9 12 15 18 9"></polyline>
28
+ </svg>
29
+ </button>
30
+ <h3>Job Activity</h3>
31
+ #{render_summary}
32
+ </div>
33
+ #{render_time_select}
34
+ </div>
35
+ <div class="chart-collapsible" id="chart-collapsible">
36
+ <div class="chart-container">
37
+ #{render_svg}
38
+ </div>
39
+ #{render_legend}
40
+ </div>
41
+ </div>
42
+ #{render_tooltip}
43
+ HTML
44
+ end
45
+
46
+ private
47
+
48
+ def render_summary
49
+ totals = @data[:totals] || { created: 0, completed: 0, failed: 0 }
50
+ <<-HTML
51
+ <span class="chart-summary">
52
+ <span class="summary-item summary-created">#{totals[:created]} created</span>
53
+ <span class="summary-separator">·</span>
54
+ <span class="summary-item summary-completed">#{totals[:completed]} completed</span>
55
+ <span class="summary-separator">·</span>
56
+ <span class="summary-item summary-failed">#{totals[:failed]} failed</span>
57
+ </span>
58
+ HTML
59
+ end
60
+
61
+ def render_time_select
62
+ options = @data[:available_ranges].map do |key, label|
63
+ selected = key == @data[:time_range] ? 'selected' : ''
64
+ "<option value=\"#{key}\" #{selected}>#{label}</option>"
65
+ end.join
66
+
67
+ <<-HTML
68
+ <div class="chart-time-select-wrapper">
69
+ <select class="chart-time-select" id="chart-time-select" onchange="window.location.href='?time_range=' + this.value">
70
+ #{options}
71
+ </select>
72
+ </div>
73
+ HTML
74
+ end
75
+
76
+ def render_svg
77
+ return render_empty_state if all_series_empty?
78
+
79
+ max_value = calculate_max_value
80
+ max_value = 10 if max_value.zero?
81
+
82
+ <<-SVG
83
+ <svg viewBox="0 0 #{CHART_WIDTH} #{CHART_HEIGHT}" class="job-activity-chart" preserveAspectRatio="xMidYMid meet">
84
+ #{render_grid_lines(max_value)}
85
+ #{render_axes}
86
+ #{render_x_labels}
87
+ #{render_y_labels(max_value)}
88
+ #{render_series_line(:failed, max_value)}
89
+ #{render_series_line(:completed, max_value)}
90
+ #{render_series_line(:created, max_value)}
91
+ #{render_series_points(:failed, max_value)}
92
+ #{render_series_points(:completed, max_value)}
93
+ #{render_series_points(:created, max_value)}
94
+ </svg>
95
+ SVG
96
+ end
97
+
98
+ def all_series_empty?
99
+ %i[created completed failed].all? { |series| series_empty?(series) }
100
+ end
101
+
102
+ def series_empty?(series)
103
+ @data[series].nil? || @data[series].all?(&:zero?)
104
+ end
105
+
106
+ def render_empty_state
107
+ <<-HTML
108
+ <div class="chart-empty">
109
+ <span>No job activity in this time range</span>
110
+ </div>
111
+ HTML
112
+ end
113
+
114
+ def render_series_line(series, max_value)
115
+ return '' if series_empty?(series)
116
+
117
+ render_line(series, max_value)
118
+ end
119
+
120
+ def render_series_points(series, max_value)
121
+ return '' if series_empty?(series)
122
+
123
+ render_data_points(series, max_value)
124
+ end
125
+
126
+ def calculate_max_value
127
+ all_values = @data[:created] + @data[:completed] + @data[:failed]
128
+ max = all_values.max || 0
129
+ # Round up to nice number
130
+ return 10 if max <= 10
131
+
132
+ magnitude = 10**Math.log10(max).floor
133
+ ((max.to_f / magnitude).ceil * magnitude)
134
+ end
135
+
136
+ def render_grid_lines(_max_value)
137
+ lines = []
138
+ 5.times do |i|
139
+ y = PADDING[:top] + (@plot_height * i / 4.0)
140
+ lines << "<line x1=\"#{PADDING[:left]}\" y1=\"#{y}\" x2=\"#{CHART_WIDTH - PADDING[:right]}\" y2=\"#{y}\" class=\"grid-line\"/>"
141
+ end
142
+ lines.join("\n")
143
+ end
144
+
145
+ def render_axes
146
+ <<-SVG
147
+ <line x1="#{PADDING[:left]}" y1="#{PADDING[:top]}" x2="#{PADDING[:left]}" y2="#{CHART_HEIGHT - PADDING[:bottom]}" class="axis-line"/>
148
+ <line x1="#{PADDING[:left]}" y1="#{CHART_HEIGHT - PADDING[:bottom]}" x2="#{CHART_WIDTH - PADDING[:right]}" y2="#{CHART_HEIGHT - PADDING[:bottom]}" class="axis-line"/>
149
+ SVG
150
+ end
151
+
152
+ def render_x_labels
153
+ labels = @data[:labels]
154
+ return '' if labels.empty?
155
+
156
+ # Show fewer labels if too many
157
+ step = labels.size > 12 ? (labels.size / 6.0).ceil : 1
158
+
159
+ label_elements = labels.each_with_index.map do |label, i|
160
+ next unless (i % step).zero? || i == labels.size - 1
161
+
162
+ x = PADDING[:left] + (@plot_width * i / (labels.size - 1).to_f)
163
+ "<text x=\"#{x}\" y=\"#{CHART_HEIGHT - PADDING[:bottom] + 20}\" class=\"axis-label x-label\">#{label}</text>"
164
+ end.compact
165
+
166
+ label_elements.join("\n")
167
+ end
168
+
169
+ def render_y_labels(max_value)
170
+ labels = []
171
+ 5.times do |i|
172
+ value = (max_value * (4 - i) / 4.0).round
173
+ y = PADDING[:top] + (@plot_height * i / 4.0)
174
+ labels << "<text x=\"#{PADDING[:left] - 10}\" y=\"#{y + 4}\" class=\"axis-label y-label\">#{value}</text>"
175
+ end
176
+ labels.join("\n")
177
+ end
178
+
179
+ def render_line(series, max_value)
180
+ points = calculate_points(series, max_value)
181
+ return '' if points.empty?
182
+
183
+ points_str = points.map { |p| "#{p[:x]},#{p[:y]}" }.join(' ')
184
+
185
+ "<polyline points=\"#{points_str}\" class=\"chart-line chart-line-#{series}\" fill=\"none\" stroke=\"#{COLORS[series]}\" stroke-width=\"2\"/>"
186
+ end
187
+
188
+ def render_data_points(series, max_value)
189
+ points = calculate_points(series, max_value)
190
+ values = @data[series]
191
+
192
+ points.each_with_index.map do |point, i|
193
+ <<-SVG
194
+ <circle cx="#{point[:x]}" cy="#{point[:y]}" r="4" class="data-point data-point-#{series}" fill="#{COLORS[series]}"
195
+ data-series="#{series}" data-label="#{@data[:labels][i]}" data-value="#{values[i]}"/>
196
+ SVG
197
+ end.join("\n")
198
+ end
199
+
200
+ def calculate_points(series, max_value)
201
+ values = @data[series]
202
+ return [] if values.blank?
203
+
204
+ values.each_with_index.map do |value, i|
205
+ x = PADDING[:left] + (@plot_width * i / (values.size - 1).to_f)
206
+ y = CHART_HEIGHT - PADDING[:bottom] - (@plot_height * value / max_value.to_f)
207
+ { x: x.round(2), y: y.round(2) }
208
+ end
209
+ end
210
+
211
+ def render_legend
212
+ <<-HTML
213
+ <div class="chart-legend">
214
+ <span class="legend-item">
215
+ <span class="legend-color" style="background-color: #{COLORS[:created]}"></span>
216
+ Created
217
+ </span>
218
+ <span class="legend-item">
219
+ <span class="legend-color" style="background-color: #{COLORS[:completed]}"></span>
220
+ Completed
221
+ </span>
222
+ <span class="legend-item">
223
+ <span class="legend-color" style="background-color: #{COLORS[:failed]}"></span>
224
+ Failed
225
+ </span>
226
+ </div>
227
+ HTML
228
+ end
229
+
230
+ def render_tooltip
231
+ <<-HTML
232
+ <div id="chart-tooltip" class="chart-tooltip" style="display: none;">
233
+ <div class="tooltip-label"></div>
234
+ <div class="tooltip-value"></div>
235
+ </div>
236
+ HTML
237
+ end
238
+ end
239
+ end
@@ -51,6 +51,7 @@ module SolidQueueMonitor
51
51
  #{generate_footer}
52
52
  </div>
53
53
  #{generate_auto_refresh_script}
54
+ #{generate_chart_script}
54
55
  HTML
55
56
  end
56
57
 
@@ -87,20 +88,32 @@ module SolidQueueMonitor
87
88
  end
88
89
 
89
90
  def generate_header
91
+ nav_items = [
92
+ { path: root_path, label: 'Overview', match: 'Overview' },
93
+ { path: ready_jobs_path, label: 'Ready Jobs', match: 'Ready Jobs' },
94
+ { path: in_progress_jobs_path, label: 'In Progress Jobs', match: 'In Progress' },
95
+ { path: scheduled_jobs_path, label: 'Scheduled Jobs', match: 'Scheduled Jobs' },
96
+ { path: recurring_jobs_path, label: 'Recurring Jobs', match: 'Recurring Jobs' },
97
+ { path: failed_jobs_path, label: 'Failed Jobs', match: 'Failed Jobs' },
98
+ { path: queues_path, label: 'Queues', match: 'Queues' }
99
+ ]
100
+
101
+ nav_links = nav_items.map do |item|
102
+ active_class = @title&.include?(item[:match]) ? 'active' : ''
103
+ "<a href=\"#{item[:path]}\" class=\"nav-link #{active_class}\">#{item[:label]}</a>"
104
+ end.join("\n ")
105
+
90
106
  <<-HTML
91
107
  <header>
92
108
  <div class="header-top">
93
109
  <h1>Solid Queue Monitor</h1>
94
- #{generate_auto_refresh_controls}
110
+ <div class="header-controls">
111
+ #{generate_auto_refresh_controls}
112
+ #{generate_theme_toggle}
113
+ </div>
95
114
  </div>
96
115
  <nav class="navigation">
97
- <a href="#{root_path}" class="nav-link">Overview</a>
98
- <a href="#{ready_jobs_path}" class="nav-link">Ready Jobs</a>
99
- <a href="#{in_progress_jobs_path}" class="nav-link">In Progress Jobs</a>
100
- <a href="#{scheduled_jobs_path}" class="nav-link">Scheduled Jobs</a>
101
- <a href="#{recurring_jobs_path}" class="nav-link">Recurring Jobs</a>
102
- <a href="#{failed_jobs_path}" class="nav-link">Failed Jobs</a>
103
- <a href="#{queues_path}" class="nav-link">Queues</a>
116
+ #{nav_links}
104
117
  </nav>
105
118
  </header>
106
119
  HTML
@@ -135,6 +148,27 @@ module SolidQueueMonitor
135
148
  HTML
136
149
  end
137
150
 
151
+ def generate_theme_toggle
152
+ <<-HTML
153
+ <button class="theme-toggle-btn" id="theme-toggle-btn" title="Toggle dark mode">
154
+ <svg class="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
155
+ <circle cx="12" cy="12" r="5"></circle>
156
+ <line x1="12" y1="1" x2="12" y2="3"></line>
157
+ <line x1="12" y1="21" x2="12" y2="23"></line>
158
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
159
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
160
+ <line x1="1" y1="12" x2="3" y2="12"></line>
161
+ <line x1="21" y1="12" x2="23" y2="12"></line>
162
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
163
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
164
+ </svg>
165
+ <svg class="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
166
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
167
+ </svg>
168
+ </button>
169
+ HTML
170
+ end
171
+
138
172
  def generate_auto_refresh_script
139
173
  return '' unless SolidQueueMonitor.auto_refresh_enabled
140
174
 
@@ -212,6 +246,132 @@ module SolidQueueMonitor
212
246
  JS
213
247
  end
214
248
 
249
+ def generate_chart_script
250
+ <<-HTML
251
+ <script>
252
+ #{theme_toggle_javascript}
253
+ #{chart_tooltip_javascript}
254
+ </script>
255
+ HTML
256
+ end
257
+
258
+ def theme_toggle_javascript
259
+ <<-JS
260
+ (function() {
261
+ var body = document.body;
262
+ var themeBtn = document.getElementById('theme-toggle-btn');
263
+ var storageKey = 'sqm_dark_theme';
264
+
265
+ // Check for saved preference or system preference
266
+ function getPreferredTheme() {
267
+ var saved = localStorage.getItem(storageKey);
268
+ if (saved !== null) {
269
+ return saved === 'true';
270
+ }
271
+ // Check system preference
272
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
273
+ }
274
+
275
+ function setTheme(isDark) {
276
+ if (isDark) {
277
+ body.classList.add('dark-theme');
278
+ } else {
279
+ body.classList.remove('dark-theme');
280
+ }
281
+ localStorage.setItem(storageKey, isDark ? 'true' : 'false');
282
+ }
283
+
284
+ // Initialize theme
285
+ setTheme(getPreferredTheme());
286
+
287
+ // Toggle on button click
288
+ if (themeBtn) {
289
+ themeBtn.addEventListener('click', function() {
290
+ var isDark = body.classList.contains('dark-theme');
291
+ setTheme(!isDark);
292
+ });
293
+ }
294
+
295
+ // Listen for system preference changes
296
+ if (window.matchMedia) {
297
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
298
+ // Only auto-switch if user hasn't manually set a preference
299
+ if (localStorage.getItem(storageKey) === null) {
300
+ setTheme(e.matches);
301
+ }
302
+ });
303
+ }
304
+ })();
305
+ JS
306
+ end
307
+
308
+ def chart_tooltip_javascript
309
+ <<-JS
310
+ (function() {
311
+ // Chart collapse/expand functionality
312
+ var chartSection = document.getElementById('chart-section');
313
+ var toggleBtn = document.getElementById('chart-toggle-btn');
314
+
315
+ if (chartSection && toggleBtn) {
316
+ var isCollapsed = localStorage.getItem('sqm_chart_collapsed') === 'true';
317
+
318
+ if (isCollapsed) {
319
+ chartSection.classList.add('collapsed');
320
+ }
321
+
322
+ toggleBtn.addEventListener('click', function() {
323
+ chartSection.classList.toggle('collapsed');
324
+ var collapsed = chartSection.classList.contains('collapsed');
325
+ localStorage.setItem('sqm_chart_collapsed', collapsed ? 'true' : 'false');
326
+ });
327
+ }
328
+
329
+ // Chart tooltip functionality
330
+ var tooltip = document.getElementById('chart-tooltip');
331
+ if (!tooltip) return;
332
+
333
+ var dataPoints = document.querySelectorAll('.data-point');
334
+ var seriesNames = { created: 'Created', completed: 'Completed', failed: 'Failed' };
335
+
336
+ dataPoints.forEach(function(point) {
337
+ point.addEventListener('mouseenter', function(e) {
338
+ var series = this.getAttribute('data-series');
339
+ var label = this.getAttribute('data-label');
340
+ var value = this.getAttribute('data-value');
341
+
342
+ tooltip.querySelector('.tooltip-label').textContent = label;
343
+ tooltip.querySelector('.tooltip-value').textContent = seriesNames[series] + ': ' + value;
344
+ tooltip.style.display = 'block';
345
+ positionTooltip(e);
346
+ });
347
+
348
+ point.addEventListener('mousemove', function(e) {
349
+ positionTooltip(e);
350
+ });
351
+
352
+ point.addEventListener('mouseleave', function() {
353
+ tooltip.style.display = 'none';
354
+ });
355
+ });
356
+
357
+ function positionTooltip(e) {
358
+ var x = e.clientX + 10;
359
+ var y = e.clientY - 30;
360
+
361
+ if (x + tooltip.offsetWidth > window.innerWidth) {
362
+ x = e.clientX - tooltip.offsetWidth - 10;
363
+ }
364
+ if (y < 0) {
365
+ y = e.clientY + 10;
366
+ }
367
+
368
+ tooltip.style.left = x + 'px';
369
+ tooltip.style.top = y + 'px';
370
+ }
371
+ })();
372
+ JS
373
+ end
374
+
215
375
  def default_url_options
216
376
  { only_path: true }
217
377
  end
@@ -8,9 +8,35 @@ module SolidQueueMonitor
8
8
  --primary-color: #3b82f6;
9
9
  --success-color: #10b981;
10
10
  --error-color: #ef4444;
11
+ --warning-color: #f59e0b;
11
12
  --text-color: #1f2937;
13
+ --text-muted: #6b7280;
12
14
  --border-color: #e5e7eb;
13
15
  --background-color: #f9fafb;
16
+ --card-background: #ffffff;
17
+ --card-shadow: 0 1px 3px rgba(0,0,0,0.1);
18
+ --input-background: #ffffff;
19
+ --input-border: #d1d5db;
20
+ --hover-background: #f3f4f6;
21
+ --code-background: #f5f5f5;
22
+ }
23
+
24
+ /* Dark theme */
25
+ .solid_queue_monitor.dark-theme {
26
+ --primary-color: #60a5fa;
27
+ --success-color: #34d399;
28
+ --error-color: #f87171;
29
+ --warning-color: #fbbf24;
30
+ --text-color: #f9fafb;
31
+ --text-muted: #9ca3af;
32
+ --border-color: #2d2d2d;
33
+ --background-color: #000000;
34
+ --card-background: #121212;
35
+ --card-shadow: 0 1px 3px rgba(0,0,0,0.5);
36
+ --input-background: #1e1e1e;
37
+ --input-border: #3d3d3d;
38
+ --hover-background: #1e1e1e;
39
+ --code-background: #1e1e1e;
14
40
  }
15
41
 
16
42
  .solid_queue_monitor * { box-sizing: border-box; margin: 0; padding: 0; }
@@ -23,7 +49,8 @@ module SolidQueueMonitor
23
49
  }
24
50
 
25
51
  .solid_queue_monitor .container {
26
- max-width: 1200px;
52
+ width: 95%;
53
+ max-width: 1800px;
27
54
  margin: 0 auto;
28
55
  padding: 2rem;
29
56
  }
@@ -52,8 +79,8 @@ module SolidQueueMonitor
52
79
  color: var(--text-color);
53
80
  padding: 0.5rem 1rem;
54
81
  border-radius: 0.375rem;
55
- background: white;
56
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
82
+ background: var(--card-background);
83
+ box-shadow: var(--card-shadow);
57
84
  transition: all 0.2s;
58
85
  }
59
86
 
@@ -89,15 +116,15 @@ module SolidQueueMonitor
89
116
  .solid_queue_monitor .stat-card {
90
117
  flex: 1 1 0;
91
118
  min-width: 150px;
92
- background: white;
119
+ background: var(--card-background);
93
120
  padding: 1.5rem 1rem;
94
121
  border-radius: 0.5rem;
95
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
122
+ box-shadow: var(--card-shadow);
96
123
  text-align: center;
97
124
  }
98
125
 
99
126
  .solid_queue_monitor .stat-card h3 {
100
- color: #6b7280;
127
+ color: var(--text-muted);
101
128
  font-size: 0.875rem;
102
129
  text-transform: uppercase;
103
130
  letter-spacing: 0.05em;
@@ -137,11 +164,12 @@ module SolidQueueMonitor
137
164
  }
138
165
 
139
166
  .solid_queue_monitor th {
140
- background: var(--background-color);
167
+ background: var(--hover-background);
141
168
  font-weight: 500;
142
169
  font-size: 0.875rem;
143
170
  text-transform: uppercase;
144
171
  letter-spacing: 0.05em;
172
+ color: var(--text-muted);
145
173
  }
146
174
 
147
175
  .solid_queue_monitor .status-badge {
@@ -242,7 +270,7 @@ module SolidQueueMonitor
242
270
  .solid_queue_monitor footer {
243
271
  text-align: center;
244
272
  padding: 2rem 0;
245
- color: #6b7280;
273
+ color: var(--text-muted);
246
274
  }
247
275
 
248
276
  .solid_queue_monitor .pagination {
@@ -289,7 +317,7 @@ module SolidQueueMonitor
289
317
  }
290
318
 
291
319
  .solid_queue_monitor .pagination-link {
292
- background: white;
320
+ background: var(--card-background);
293
321
  color: var(--text-color);
294
322
  border: 1px solid var(--border-color);
295
323
  }
@@ -320,7 +348,7 @@ module SolidQueueMonitor
320
348
  max-height: 100px;
321
349
  overflow-y: auto;
322
350
  padding: 8px;
323
- background: #f5f5f5;
351
+ background: var(--code-background);
324
352
  border-radius: 4px;
325
353
  font-size: 0.9em;
326
354
  }
@@ -328,7 +356,7 @@ module SolidQueueMonitor
328
356
  .solid_queue_monitor .args-single-line {
329
357
  display: inline-block;
330
358
  padding: 4px 8px;
331
- background: #f5f5f5;
359
+ background: var(--code-background);
332
360
  border-radius: 4px;
333
361
  font-size: 0.9em;
334
362
  }
@@ -399,10 +427,10 @@ module SolidQueueMonitor
399
427
  }
400
428
 
401
429
  .solid_queue_monitor .filter-form-container {
402
- background: white;
430
+ background: var(--card-background);
403
431
  padding: 1rem;
404
432
  border-radius: 0.5rem;
405
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
433
+ box-shadow: var(--card-shadow);
406
434
  flex: 3;
407
435
  }
408
436
 
@@ -411,9 +439,9 @@ module SolidQueueMonitor
411
439
  flex-direction: row;
412
440
  gap: 0.75rem;
413
441
  padding: 1rem;
414
- background: white;
442
+ background: var(--card-background);
415
443
  border-radius: 0.5rem;
416
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
444
+ box-shadow: var(--card-shadow);
417
445
  flex: 2;
418
446
  align-items: center;
419
447
  justify-content: center;
@@ -453,16 +481,18 @@ module SolidQueueMonitor
453
481
  margin-bottom: 0.5rem;
454
482
  font-size: 0.875rem;
455
483
  font-weight: 500;
456
- color: #4b5563;
484
+ color: var(--text-muted);
457
485
  }
458
486
 
459
487
  .solid_queue_monitor .filter-group input,
460
488
  .solid_queue_monitor .filter-group select {
461
489
  width: 100%;
462
490
  padding: 0.5rem;
463
- border: 1px solid #d1d5db;
491
+ border: 1px solid var(--input-border);
464
492
  border-radius: 0.375rem;
465
493
  font-size: 0.875rem;
494
+ background: var(--input-background);
495
+ color: var(--text-color);
466
496
  }
467
497
 
468
498
  .solid_queue_monitor .filter-actions {
@@ -486,9 +516,9 @@ module SolidQueueMonitor
486
516
  }
487
517
 
488
518
  .solid_queue_monitor .reset-button {
489
- background: #f3f4f6;
490
- color: #4b5563;
491
- border: 1px solid #d1d5db;
519
+ background: var(--hover-background);
520
+ color: var(--text-muted);
521
+ border: 1px solid var(--border-color);
492
522
  padding: 0.5rem 1rem;
493
523
  border-radius: 0.375rem;
494
524
  font-size: 0.875rem;
@@ -498,7 +528,7 @@ module SolidQueueMonitor
498
528
  }
499
529
 
500
530
  .solid_queue_monitor .reset-button:hover {
501
- background: #e5e7eb;
531
+ background: var(--border-color);
502
532
  }
503
533
 
504
534
  .solid_queue_monitor .action-button {
@@ -560,7 +590,7 @@ module SolidQueueMonitor
560
590
  white-space: pre-wrap;
561
591
  max-height: 200px;
562
592
  overflow-y: auto;
563
- background: #f3f4f6;
593
+ background: var(--code-background);
564
594
  padding: 0.5rem;
565
595
  border-radius: 0.25rem;
566
596
  margin-top: 0.5rem;
@@ -572,12 +602,12 @@ module SolidQueueMonitor
572
602
 
573
603
  .solid_queue_monitor summary {
574
604
  cursor: pointer;
575
- color: #6b7280;
605
+ color: var(--text-muted);
576
606
  font-size: 0.75rem;
577
607
  }
578
608
 
579
609
  .solid_queue_monitor summary:hover {
580
- color: #4b5563;
610
+ color: var(--text-color);
581
611
  }
582
612
 
583
613
  .solid_queue_monitor .job-checkbox,
@@ -590,10 +620,10 @@ module SolidQueueMonitor
590
620
  display: flex;
591
621
  gap: 0.75rem;
592
622
  margin: 1rem 0;
593
- background: white;
623
+ background: var(--card-background);
594
624
  padding: 0.75rem;
595
625
  border-radius: 0.5rem;
596
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
626
+ box-shadow: var(--card-shadow);
597
627
  }
598
628
 
599
629
  .solid_queue_monitor .bulk-actions-bar .action-button {
@@ -625,11 +655,11 @@ module SolidQueueMonitor
625
655
  align-items: center;
626
656
  gap: 0.5rem;
627
657
  padding: 0.375rem 0.625rem;
628
- background: white;
658
+ background: var(--card-background);
629
659
  border-radius: 2rem;
630
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
660
+ box-shadow: var(--card-shadow);
631
661
  font-size: 0.75rem;
632
- color: #6b7280;
662
+ color: var(--text-muted);
633
663
  cursor: default;
634
664
  }
635
665
 
@@ -682,7 +712,7 @@ module SolidQueueMonitor
682
712
  width: 6px;
683
713
  height: 6px;
684
714
  border-radius: 50%;
685
- background: #d1d5db;
715
+ background: var(--border-color);
686
716
  flex-shrink: 0;
687
717
  }
688
718
 
@@ -726,7 +756,7 @@ module SolidQueueMonitor
726
756
  left: 0;
727
757
  right: 0;
728
758
  bottom: 0;
729
- background-color: #d1d5db;
759
+ background-color: var(--border-color);
730
760
  transition: 0.2s;
731
761
  border-radius: 18px;
732
762
  }
@@ -738,7 +768,7 @@ module SolidQueueMonitor
738
768
  width: 14px;
739
769
  left: 2px;
740
770
  bottom: 2px;
741
- background-color: white;
771
+ background-color: var(--card-background);
742
772
  transition: 0.2s;
743
773
  border-radius: 50%;
744
774
  box-shadow: 0 1px 2px rgba(0,0,0,0.2);
@@ -761,7 +791,7 @@ module SolidQueueMonitor
761
791
  padding: 0.25rem;
762
792
  border-radius: 0.25rem;
763
793
  cursor: pointer;
764
- color: #9ca3af;
794
+ color: var(--text-muted);
765
795
  transition: all 0.2s;
766
796
  }
767
797
 
@@ -786,6 +816,344 @@ module SolidQueueMonitor
786
816
  display: none;
787
817
  }
788
818
  }
819
+
820
+ /* Navigation active state */
821
+ .solid_queue_monitor .nav-link.active {
822
+ background: var(--primary-color);
823
+ color: white;
824
+ border-left: 3px solid #1d4ed8;
825
+ }
826
+
827
+ /* Chart styles */
828
+ .solid_queue_monitor .chart-section {
829
+ background: var(--card-background);
830
+ border-radius: 0.5rem;
831
+ box-shadow: var(--card-shadow);
832
+ padding: 1rem 1.5rem;
833
+ margin-bottom: 2rem;
834
+ }
835
+
836
+ .solid_queue_monitor .chart-section.collapsed {
837
+ padding-bottom: 1rem;
838
+ }
839
+
840
+ .solid_queue_monitor .chart-header {
841
+ display: flex;
842
+ justify-content: space-between;
843
+ align-items: center;
844
+ flex-wrap: wrap;
845
+ gap: 0.75rem;
846
+ }
847
+
848
+ .solid_queue_monitor .chart-header-left {
849
+ display: flex;
850
+ align-items: center;
851
+ gap: 0.5rem;
852
+ flex-wrap: wrap;
853
+ }
854
+
855
+ .solid_queue_monitor .chart-header h3 {
856
+ font-size: 1rem;
857
+ font-weight: 600;
858
+ color: var(--text-color);
859
+ margin: 0;
860
+ }
861
+
862
+ .solid_queue_monitor .chart-toggle-btn {
863
+ display: flex;
864
+ align-items: center;
865
+ justify-content: center;
866
+ width: 28px;
867
+ height: 28px;
868
+ background: var(--hover-background);
869
+ border: 1px solid var(--border-color);
870
+ border-radius: 0.375rem;
871
+ cursor: pointer;
872
+ color: var(--text-muted);
873
+ transition: all 0.2s;
874
+ }
875
+
876
+ .solid_queue_monitor .chart-toggle-btn:hover {
877
+ background: var(--border-color);
878
+ color: var(--text-color);
879
+ }
880
+
881
+ .solid_queue_monitor .chart-toggle-icon {
882
+ transition: transform 0.2s;
883
+ }
884
+
885
+ .solid_queue_monitor .chart-section.collapsed .chart-toggle-icon {
886
+ transform: rotate(-90deg);
887
+ }
888
+
889
+ .solid_queue_monitor .chart-summary {
890
+ display: flex;
891
+ align-items: center;
892
+ gap: 0.5rem;
893
+ font-size: 0.8rem;
894
+ color: var(--text-muted);
895
+ margin-left: 0.5rem;
896
+ padding-left: 0.75rem;
897
+ border-left: 1px solid var(--border-color);
898
+ }
899
+
900
+ .solid_queue_monitor .summary-item {
901
+ white-space: nowrap;
902
+ }
903
+
904
+ .solid_queue_monitor .summary-created {
905
+ color: #3b82f6;
906
+ }
907
+
908
+ .solid_queue_monitor .summary-completed {
909
+ color: #10b981;
910
+ }
911
+
912
+ .solid_queue_monitor .summary-failed {
913
+ color: #ef4444;
914
+ }
915
+
916
+ .solid_queue_monitor .summary-separator {
917
+ color: var(--border-color);
918
+ }
919
+
920
+ .solid_queue_monitor .chart-time-select-wrapper {
921
+ position: relative;
922
+ }
923
+
924
+ .solid_queue_monitor .chart-time-select {
925
+ appearance: none;
926
+ padding: 0.5rem 2rem 0.5rem 0.75rem;
927
+ font-size: 0.8rem;
928
+ color: var(--text-color);
929
+ background: var(--input-background);
930
+ border: 1px solid var(--border-color);
931
+ border-radius: 0.375rem;
932
+ cursor: pointer;
933
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
934
+ background-repeat: no-repeat;
935
+ background-position: right 0.5rem center;
936
+ background-size: 14px;
937
+ min-width: 140px;
938
+ }
939
+
940
+ .solid_queue_monitor .chart-time-select:hover {
941
+ border-color: var(--text-muted);
942
+ }
943
+
944
+ .solid_queue_monitor .chart-time-select:focus {
945
+ outline: none;
946
+ border-color: var(--primary-color);
947
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
948
+ }
949
+
950
+ .solid_queue_monitor .chart-collapsible {
951
+ overflow: hidden;
952
+ transition: max-height 0.3s ease-out, opacity 0.2s ease-out, margin-top 0.3s ease-out;
953
+ max-height: 500px;
954
+ opacity: 1;
955
+ margin-top: 1rem;
956
+ }
957
+
958
+ .solid_queue_monitor .chart-section.collapsed .chart-collapsible {
959
+ max-height: 0;
960
+ opacity: 0;
961
+ margin-top: 0;
962
+ }
963
+
964
+ .solid_queue_monitor .chart-container {
965
+ width: 100%;
966
+ overflow-x: auto;
967
+ overflow-y: hidden;
968
+ }
969
+
970
+ .solid_queue_monitor .chart-container svg {
971
+ display: block;
972
+ width: 100%;
973
+ height: auto;
974
+ }
975
+
976
+ .solid_queue_monitor .job-activity-chart {
977
+ width: 100%;
978
+ height: auto;
979
+ min-height: 250px;
980
+ }
981
+
982
+ .solid_queue_monitor .grid-line {
983
+ stroke: var(--border-color);
984
+ stroke-width: 1;
985
+ stroke-dasharray: 4 4;
986
+ }
987
+
988
+ .solid_queue_monitor .axis-line {
989
+ stroke: var(--border-color);
990
+ stroke-width: 1;
991
+ }
992
+
993
+ .solid_queue_monitor .axis-label {
994
+ font-size: 11px;
995
+ fill: var(--text-muted);
996
+ }
997
+
998
+ .solid_queue_monitor .x-label {
999
+ text-anchor: middle;
1000
+ }
1001
+
1002
+ .solid_queue_monitor .y-label {
1003
+ text-anchor: end;
1004
+ }
1005
+
1006
+ .solid_queue_monitor .chart-line {
1007
+ stroke-linecap: round;
1008
+ stroke-linejoin: round;
1009
+ }
1010
+
1011
+ .solid_queue_monitor .data-point {
1012
+ cursor: pointer;
1013
+ transition: r 0.2s;
1014
+ }
1015
+
1016
+ .solid_queue_monitor .data-point:hover {
1017
+ r: 6;
1018
+ }
1019
+
1020
+ .solid_queue_monitor .chart-legend {
1021
+ display: flex;
1022
+ justify-content: center;
1023
+ gap: 1.5rem;
1024
+ margin-top: 1rem;
1025
+ flex-wrap: wrap;
1026
+ }
1027
+
1028
+ .solid_queue_monitor .legend-item {
1029
+ display: flex;
1030
+ align-items: center;
1031
+ gap: 0.375rem;
1032
+ font-size: 0.875rem;
1033
+ color: var(--text-muted);
1034
+ }
1035
+
1036
+ .solid_queue_monitor .legend-color {
1037
+ width: 12px;
1038
+ height: 12px;
1039
+ border-radius: 2px;
1040
+ }
1041
+
1042
+ .solid_queue_monitor .chart-tooltip {
1043
+ position: fixed;
1044
+ background: #1f2937;
1045
+ color: white;
1046
+ padding: 0.5rem 0.75rem;
1047
+ border-radius: 0.375rem;
1048
+ font-size: 0.75rem;
1049
+ pointer-events: none;
1050
+ z-index: 1000;
1051
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
1052
+ }
1053
+
1054
+ .solid_queue_monitor .tooltip-label {
1055
+ font-weight: 500;
1056
+ margin-bottom: 0.25rem;
1057
+ }
1058
+
1059
+ .solid_queue_monitor .tooltip-value {
1060
+ color: #d1d5db;
1061
+ }
1062
+
1063
+ .solid_queue_monitor .chart-empty {
1064
+ display: flex;
1065
+ align-items: center;
1066
+ justify-content: center;
1067
+ height: 200px;
1068
+ color: var(--text-muted);
1069
+ font-size: 0.875rem;
1070
+ }
1071
+
1072
+ @media (max-width: 768px) {
1073
+ .solid_queue_monitor .chart-section {
1074
+ padding: 1rem;
1075
+ }
1076
+
1077
+ .solid_queue_monitor .chart-header {
1078
+ flex-direction: column;
1079
+ align-items: flex-start;
1080
+ }
1081
+
1082
+ .solid_queue_monitor .chart-header-left {
1083
+ width: 100%;
1084
+ flex-wrap: wrap;
1085
+ }
1086
+
1087
+ .solid_queue_monitor .chart-summary {
1088
+ margin-left: 0;
1089
+ padding-left: 0;
1090
+ border-left: none;
1091
+ margin-top: 0.5rem;
1092
+ width: 100%;
1093
+ }
1094
+
1095
+ .solid_queue_monitor .chart-time-select {
1096
+ width: 100%;
1097
+ }
1098
+
1099
+ .solid_queue_monitor .job-activity-chart {
1100
+ min-height: 200px;
1101
+ }
1102
+
1103
+ .solid_queue_monitor .chart-legend {
1104
+ gap: 1rem;
1105
+ }
1106
+ }
1107
+
1108
+ /* Theme toggle button */
1109
+ .solid_queue_monitor .theme-toggle-btn {
1110
+ display: flex;
1111
+ align-items: center;
1112
+ justify-content: center;
1113
+ width: 36px;
1114
+ height: 36px;
1115
+ background: var(--card-background);
1116
+ border: 1px solid var(--border-color);
1117
+ border-radius: 50%;
1118
+ cursor: pointer;
1119
+ color: var(--text-muted);
1120
+ transition: all 0.2s;
1121
+ box-shadow: var(--card-shadow);
1122
+ }
1123
+
1124
+ .solid_queue_monitor .theme-toggle-btn:hover {
1125
+ color: var(--text-color);
1126
+ border-color: var(--text-muted);
1127
+ }
1128
+
1129
+ .solid_queue_monitor .theme-toggle-btn svg {
1130
+ width: 18px;
1131
+ height: 18px;
1132
+ }
1133
+
1134
+ /* Hide moon icon in light mode, show sun icon */
1135
+ .solid_queue_monitor .theme-icon-moon {
1136
+ display: none;
1137
+ }
1138
+
1139
+ .solid_queue_monitor .theme-icon-sun {
1140
+ display: block;
1141
+ }
1142
+
1143
+ /* In dark mode, show moon icon, hide sun icon */
1144
+ .solid_queue_monitor.dark-theme .theme-icon-moon {
1145
+ display: block;
1146
+ }
1147
+
1148
+ .solid_queue_monitor.dark-theme .theme-icon-sun {
1149
+ display: none;
1150
+ }
1151
+
1152
+ .solid_queue_monitor .header-controls {
1153
+ display: flex;
1154
+ align-items: center;
1155
+ gap: 0.75rem;
1156
+ }
789
1157
  CSS
790
1158
  end
791
1159
  end
data/config/routes.rb CHANGED
@@ -6,6 +6,8 @@ SolidQueueMonitor::Engine.routes.draw do
6
6
 
7
7
  root to: 'overview#index'
8
8
 
9
+ get 'chart_data', to: 'overview#chart_data', as: :chart_data
10
+
9
11
  resources :ready_jobs, only: [:index]
10
12
  resources :scheduled_jobs, only: [:index]
11
13
  resources :recurring_jobs, only: [:index]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueueMonitor
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue_monitor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vishal Sadriya
@@ -67,6 +67,8 @@ files:
67
67
  - app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb
68
68
  - app/presenters/solid_queue_monitor/stats_presenter.rb
69
69
  - app/services/solid_queue_monitor/authentication_service.rb
70
+ - app/services/solid_queue_monitor/chart_data_service.rb
71
+ - app/services/solid_queue_monitor/chart_presenter.rb
70
72
  - app/services/solid_queue_monitor/execute_job_service.rb
71
73
  - app/services/solid_queue_monitor/failed_job_service.rb
72
74
  - app/services/solid_queue_monitor/html_generator.rb