solid_queue_monitor 0.1.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.
Files changed (29) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +128 -0
  3. data/Rakefile +23 -0
  4. data/app/controllers/solid_queue_monitor/monitor_controller.rb +250 -0
  5. data/app/presenters/solid_queue_monitor/base_presenter.rb +136 -0
  6. data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +70 -0
  7. data/app/presenters/solid_queue_monitor/jobs_presenter.rb +100 -0
  8. data/app/presenters/solid_queue_monitor/queues_presenter.rb +62 -0
  9. data/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb +72 -0
  10. data/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb +77 -0
  11. data/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb +114 -0
  12. data/app/presenters/solid_queue_monitor/stats_presenter.rb +35 -0
  13. data/app/services/solid_queue_monitor/authentication_service.rb +14 -0
  14. data/app/services/solid_queue_monitor/execute_job_service.rb +28 -0
  15. data/app/services/solid_queue_monitor/html_generator.rb +88 -0
  16. data/app/services/solid_queue_monitor/pagination_service.rb +31 -0
  17. data/app/services/solid_queue_monitor/stats_calculator.rb +15 -0
  18. data/app/services/solid_queue_monitor/status_calculator.rb +14 -0
  19. data/app/services/solid_queue_monitor/stylesheet_generator.rb +395 -0
  20. data/config/initializers/solid_queue_monitor.rb +5 -0
  21. data/config/routes.rb +11 -0
  22. data/lib/generators/solid_queue_monitor/install_generator.rb +23 -0
  23. data/lib/generators/solid_queue_monitor/templates/README.md +23 -0
  24. data/lib/generators/solid_queue_monitor/templates/initializer.rb +14 -0
  25. data/lib/solid_queue_monitor/engine.rb +14 -0
  26. data/lib/solid_queue_monitor/version.rb +5 -0
  27. data/lib/solid_queue_monitor.rb +24 -0
  28. data/lib/tasks/app.rake +135 -0
  29. metadata +240 -0
@@ -0,0 +1,395 @@
1
+ module SolidQueueMonitor
2
+ class StylesheetGenerator
3
+ def generate
4
+ <<-CSS
5
+ :root {
6
+ --primary-color: #3b82f6;
7
+ --success-color: #10b981;
8
+ --error-color: #ef4444;
9
+ --text-color: #1f2937;
10
+ --border-color: #e5e7eb;
11
+ --background-color: #f9fafb;
12
+ }
13
+
14
+ * { box-sizing: border-box; margin: 0; padding: 0; }
15
+
16
+ body {
17
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
18
+ line-height: 1.5;
19
+ color: var(--text-color);
20
+ background: var(--background-color);
21
+ }
22
+
23
+ .container {
24
+ max-width: 1200px;
25
+ margin: 0 auto;
26
+ padding: 2rem;
27
+ }
28
+
29
+ header {
30
+ margin-bottom: 2rem;
31
+ text-align: center;
32
+ }
33
+
34
+ h1 {
35
+ font-size: 2rem;
36
+ font-weight: 600;
37
+ margin-bottom: 0.5rem;
38
+ }
39
+
40
+ .navigation {
41
+ display: flex;
42
+ flex-wrap: wrap;
43
+ justify-content: center;
44
+ gap: 0.5rem;
45
+ padding: 0.5rem;
46
+ }
47
+
48
+ .nav-link {
49
+ text-decoration: none;
50
+ color: var(--text-color);
51
+ padding: 0.5rem 1rem;
52
+ border-radius: 0.375rem;
53
+ background: white;
54
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
55
+ transition: all 0.2s;
56
+ }
57
+
58
+ .nav-link:hover {
59
+ background: var(--primary-color);
60
+ color: white;
61
+ }
62
+
63
+ .section-wrapper {
64
+ margin-top: 2rem;
65
+ }
66
+
67
+
68
+ .section h2 {
69
+ padding: 1rem;
70
+ border-bottom: 1px solid var(--border-color);
71
+ font-size: 1.25rem;
72
+ background: var(--background-color);
73
+ }
74
+
75
+ .stats-container {
76
+ margin-bottom: 2rem;
77
+ }
78
+
79
+ .stats {
80
+ display: flex;
81
+ flex-direction: row;
82
+ flex-wrap: wrap;
83
+ gap: 1rem;
84
+ margin: 0 -0.5rem;
85
+ }
86
+
87
+ .stat-card {
88
+ flex: 1 1 0;
89
+ min-width: 150px;
90
+ background: white;
91
+ padding: 1.5rem 1rem;
92
+ border-radius: 0.5rem;
93
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
94
+ text-align: center;
95
+ }
96
+
97
+ .stat-card h3 {
98
+ color: #6b7280;
99
+ font-size: 0.875rem;
100
+ text-transform: uppercase;
101
+ letter-spacing: 0.05em;
102
+ margin-bottom: 0.5rem;
103
+ }
104
+
105
+ .stat-card p {
106
+ font-size: 1.5rem;
107
+ font-weight: 600;
108
+ color: var(--primary-color);
109
+ }
110
+
111
+ .section h2 {
112
+ padding: 1rem;
113
+ border-bottom: 1px solid var(--border-color);
114
+ font-size: 1.25rem;
115
+ }
116
+
117
+ .table-container {
118
+ width: 100%;
119
+ overflow-x: auto;
120
+ -webkit-overflow-scrolling: touch;
121
+ }
122
+
123
+ table {
124
+ width: 100%;
125
+ min-width: 800px; /* Ensures table doesn't get too squeezed */
126
+ border-collapse: collapse;
127
+ white-space: nowrap;
128
+ }
129
+
130
+ th, td {
131
+ padding: 0.75rem 1rem;
132
+ text-align: left;
133
+ border-bottom: 1px solid var(--border-color);
134
+ }
135
+
136
+ th {
137
+ background: var(--background-color);
138
+ font-weight: 500;
139
+ font-size: 0.875rem;
140
+ text-transform: uppercase;
141
+ letter-spacing: 0.05em;
142
+ }
143
+
144
+ .status-badge {
145
+ display: inline-block;
146
+ padding: 0.25rem 0.5rem;
147
+ border-radius: 9999px;
148
+ font-size: 0.75rem;
149
+ font-weight: 500;
150
+ }
151
+
152
+ .table-actions {
153
+ display: flex;
154
+ justify-content: space-between;
155
+ align-items: center;
156
+ padding: 1rem;
157
+ border-top: 1px solid var(--border-color);
158
+ }
159
+
160
+ .select-all {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 0.5rem;
164
+ cursor: pointer;
165
+ }
166
+
167
+ .execute-btn:disabled {
168
+ opacity: 0.5;
169
+ cursor: not-allowed;
170
+ }
171
+
172
+ input[type="checkbox"] {
173
+ width: 1rem;
174
+ height: 1rem;
175
+ cursor: pointer;
176
+ }
177
+
178
+ .status-completed { background: #d1fae5; color: #065f46; }
179
+ .status-failed { background: #fee2e2; color: #991b1b; }
180
+ .status-scheduled { background: #dbeafe; color: #1e40af; }
181
+ .status-pending { background: #f3f4f6; color: #374151; }
182
+
183
+ .execute-btn {
184
+ background: var(--primary-color);
185
+ color: white;
186
+ border: none;
187
+ padding: 0.5rem 1rem;
188
+ border-radius: 0.375rem;
189
+ font-size: 0.875rem;
190
+ cursor: pointer;
191
+ transition: background-color 0.2s;
192
+ }
193
+
194
+ .execute-btn:hover {
195
+ background: #2563eb;
196
+ }
197
+
198
+ .message {
199
+ padding: 1rem;
200
+ margin-bottom: 1rem;
201
+ border-radius: 0.375rem;
202
+ }
203
+
204
+ .message-success {
205
+ background: #d1fae5;
206
+ color: #065f46;
207
+ }
208
+
209
+ .message-error {
210
+ background: #fee2e2;
211
+ color: #991b1b;
212
+ }
213
+
214
+ footer {
215
+ text-align: center;
216
+ padding: 2rem 0;
217
+ color: #6b7280;
218
+ }
219
+
220
+ .pagination {
221
+ display: flex;
222
+ justify-content: center;
223
+ gap: 0.5rem;
224
+ margin-top: 1rem;
225
+ padding: 1rem;
226
+ }
227
+
228
+ .pagination-nav {
229
+ padding: 0.5rem 1rem;
230
+ font-size: 0.875rem;
231
+ }
232
+
233
+ .pagination-gap {
234
+ display: inline-flex;
235
+ align-items: center;
236
+ justify-content: center;
237
+ min-width: 2rem;
238
+ height: 2rem;
239
+ padding: 0 0.5rem;
240
+ color: var(--text-color);
241
+ }
242
+
243
+ .pagination-link.disabled {
244
+ opacity: 0.5;
245
+ cursor: not-allowed;
246
+ pointer-events: none;
247
+ }
248
+
249
+ .pagination-link,
250
+ .pagination-current {
251
+ display: inline-flex;
252
+ align-items: center;
253
+ justify-content: center;
254
+ min-width: 2rem;
255
+ height: 2rem;
256
+ padding: 0 0.5rem;
257
+ border-radius: 0.375rem;
258
+ font-size: 0.875rem;
259
+ text-decoration: none;
260
+ transition: all 0.2s;
261
+ }
262
+
263
+ .pagination-link {
264
+ background: white;
265
+ color: var(--text-color);
266
+ border: 1px solid var(--border-color);
267
+ }
268
+
269
+ .pagination-link:hover {
270
+ background: var(--primary-color);
271
+ color: white;
272
+ border-color: var(--primary-color);
273
+ }
274
+
275
+ .pagination-current {
276
+ background: var(--primary-color);
277
+ color: white;
278
+ font-weight: 500;
279
+ }
280
+
281
+ @media (max-width: 768px) {
282
+ .container {
283
+ padding: 0.5rem;
284
+ }
285
+
286
+ .stats {
287
+ margin: 0;
288
+ }
289
+
290
+ .stat-card {
291
+ flex: 1 1 calc(33.333% - 1rem);
292
+ min-width: 120px;
293
+ }
294
+
295
+ .section {
296
+ margin: 0.5rem 0;
297
+ border-radius: 0.375rem;
298
+ }
299
+
300
+ .table-container {
301
+ width: 100%;
302
+ overflow-x: auto;
303
+ }
304
+ }
305
+
306
+ @media (max-width: 480px) {
307
+ .stat-card {
308
+ flex: 1 1 calc(50% - 1rem);
309
+ }
310
+
311
+ .nav-link {
312
+ width: 100%;
313
+ text-align: center;
314
+ }
315
+ .pagination-nav {
316
+ display: none;
317
+ }
318
+ }
319
+
320
+ .filter-form-container {
321
+ background: white;
322
+ padding: 1rem;
323
+ border-radius: 0.5rem;
324
+ margin-bottom: 1rem;
325
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
326
+ }
327
+
328
+ .filter-form {
329
+ display: flex;
330
+ flex-wrap: wrap;
331
+ gap: 1rem;
332
+ align-items: flex-end;
333
+ }
334
+
335
+ .filter-group {
336
+ flex: 1;
337
+ min-width: 200px;
338
+ }
339
+
340
+ .filter-group label {
341
+ display: block;
342
+ margin-bottom: 0.5rem;
343
+ font-size: 0.875rem;
344
+ font-weight: 500;
345
+ color: #4b5563;
346
+ }
347
+
348
+ .filter-group input,
349
+ .filter-group select {
350
+ width: 100%;
351
+ padding: 0.5rem;
352
+ border: 1px solid #d1d5db;
353
+ border-radius: 0.375rem;
354
+ font-size: 0.875rem;
355
+ }
356
+
357
+ .filter-actions {
358
+ display: flex;
359
+ gap: 0.5rem;
360
+ }
361
+
362
+ .filter-button {
363
+ background: var(--primary-color);
364
+ color: white;
365
+ border: none;
366
+ padding: 0.5rem 1rem;
367
+ border-radius: 0.375rem;
368
+ font-size: 0.875rem;
369
+ cursor: pointer;
370
+ transition: background-color 0.2s;
371
+ }
372
+
373
+ .filter-button:hover {
374
+ background: #2563eb;
375
+ }
376
+
377
+ .reset-button {
378
+ background: #f3f4f6;
379
+ color: #4b5563;
380
+ border: 1px solid #d1d5db;
381
+ padding: 0.5rem 1rem;
382
+ border-radius: 0.375rem;
383
+ font-size: 0.875rem;
384
+ text-decoration: none;
385
+ cursor: pointer;
386
+ transition: background-color 0.2s;
387
+ }
388
+
389
+ .reset-button:hover {
390
+ background: #e5e7eb;
391
+ }
392
+ CSS
393
+ end
394
+ end
395
+ end
@@ -0,0 +1,5 @@
1
+ SolidQueueMonitor.setup do |config|
2
+ config.username = 'admin' # Change this in your application
3
+ config.password = 'password' # Change this in your application
4
+ config.jobs_per_page = 25
5
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,11 @@
1
+ SolidQueueMonitor::Engine.routes.draw do
2
+ root to: 'monitor#index'
3
+
4
+ get 'ready_jobs', to: 'monitor#ready_jobs'
5
+ get 'scheduled_jobs', to: 'monitor#scheduled_jobs'
6
+ get 'failed_jobs', to: 'monitor#failed_jobs'
7
+ get 'recurring_jobs', to: 'monitor#recurring_jobs'
8
+ get 'queues', to: 'monitor#queues'
9
+
10
+ post 'execute_jobs', to: 'monitor#execute_jobs'
11
+ end
@@ -0,0 +1,23 @@
1
+ require 'rails/generators/base'
2
+
3
+ module SolidQueueMonitor
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('templates', __dir__)
7
+
8
+ def copy_initializer
9
+ template "initializer.rb", "config/initializers/solid_queue_monitor.rb"
10
+ end
11
+
12
+ def add_routes
13
+ prepend_to_file "config/routes.rb", "require 'solid_queue_monitor'\n\n"
14
+
15
+ route "mount SolidQueueMonitor::Engine => '/solid_queue'"
16
+ end
17
+
18
+ def show_readme
19
+ readme "README.md" if behavior == :invoke
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # SolidQueueMonitor Installation
2
+
3
+ The SolidQueueMonitor has been installed.
4
+
5
+ ## Next Steps
6
+
7
+ 1. Configure your settings in `config/initializers/solid_queue_monitor.rb`
8
+
9
+ 2. Access your dashboard at: http://your-app-url/solid_queue
10
+
11
+ 3. Authentication:
12
+ - Authentication is disabled by default for ease of setup
13
+ - To enable authentication, set `config.authentication_enabled = true` in the initializer
14
+ - Default credentials (when authentication is enabled):
15
+ - Username: admin
16
+ - Password: password
17
+
18
+ ## Security Note
19
+
20
+ For production environments, it's strongly recommended to:
21
+
22
+ 1. Enable authentication
23
+ 2. Change the default credentials to secure values
@@ -0,0 +1,14 @@
1
+ SolidQueueMonitor.setup do |config|
2
+ # Enable or disable authentication
3
+ # When disabled, no authentication is required to access the monitor
4
+ config.authentication_enabled = false
5
+
6
+ # Set the username for HTTP Basic Authentication (only used if authentication is enabled)
7
+ # config.username = 'admin'
8
+
9
+ # Set the password for HTTP Basic Authentication (only used if authentication is enabled)
10
+ # config.password = 'password'
11
+
12
+ # Number of jobs to display per page
13
+ # config.jobs_per_page = 25
14
+ end
@@ -0,0 +1,14 @@
1
+ module SolidQueueMonitor
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace SolidQueueMonitor
4
+
5
+ config.autoload_paths << root.join('app', 'services')
6
+
7
+ # Optional: Add eager loading for production
8
+ config.eager_load_paths << root.join('app', 'services')
9
+
10
+ initializer "solid_queue_monitor.assets" do |app|
11
+ # Optional: Add assets if needed
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueMonitor
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "solid_queue_monitor/version"
4
+ require_relative "solid_queue_monitor/engine"
5
+
6
+ module SolidQueueMonitor
7
+ class Error < StandardError; end
8
+ # Configuration options
9
+ mattr_accessor :username
10
+ @@username = 'admin'
11
+
12
+ mattr_accessor :password
13
+ @@password = 'password'
14
+
15
+ mattr_accessor :jobs_per_page
16
+ @@jobs_per_page = 25
17
+
18
+ mattr_accessor :authentication_enabled
19
+ @@authentication_enabled = false
20
+
21
+ def self.setup
22
+ yield self
23
+ end
24
+ end
@@ -0,0 +1,135 @@
1
+ namespace :app do
2
+ desc "Setup the dummy app for testing"
3
+ task :setup do
4
+ require 'fileutils'
5
+
6
+ # Create dummy app directories
7
+ dummy_app_path = File.expand_path("../../spec/dummy", __FILE__)
8
+
9
+ # Ensure directories exist
10
+ %w[
11
+ app/controllers
12
+ app/models
13
+ app/views
14
+ config/environments
15
+ config/initializers
16
+ db
17
+ lib
18
+ log
19
+ ].each do |dir|
20
+ FileUtils.mkdir_p(File.join(dummy_app_path, dir))
21
+ end
22
+
23
+ # Create necessary files if they don't exist
24
+ unless File.exist?(File.join(dummy_app_path, "config/boot.rb"))
25
+ File.write(File.join(dummy_app_path, "config/boot.rb"), <<~RUBY)
26
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
27
+ require 'bundler/setup'
28
+ RUBY
29
+ end
30
+
31
+ unless File.exist?(File.join(dummy_app_path, "config/application.rb"))
32
+ File.write(File.join(dummy_app_path, "config/application.rb"), <<~RUBY)
33
+ require_relative "boot"
34
+
35
+ require "rails"
36
+ require "active_model/railtie"
37
+ require "active_record/railtie"
38
+ require "action_controller/railtie"
39
+ require "action_view/railtie"
40
+ require "rails/test_unit/railtie"
41
+ require "solid_queue"
42
+ require "solid_queue_monitor"
43
+
44
+ module Dummy
45
+ class Application < Rails::Application
46
+ config.load_defaults Rails::VERSION::STRING.to_f
47
+
48
+ # Settings in config/environments/* take precedence over those specified here.
49
+ # Application configuration can go into files in config/initializers
50
+ # -- all .rb files in that directory are automatically loaded after loading
51
+ # the framework and any gems in your application.
52
+
53
+ # Only loads a smaller set of middleware suitable for API only apps.
54
+ # Middleware like session, flash, cookies can be added back manually.
55
+ config.api_only = true
56
+
57
+ # Don't generate system test files.
58
+ config.generators.system_tests = nil
59
+ end
60
+ end
61
+ RUBY
62
+ end
63
+
64
+ unless File.exist?(File.join(dummy_app_path, "config/environment.rb"))
65
+ File.write(File.join(dummy_app_path, "config/environment.rb"), <<~RUBY)
66
+ # Load the Rails application.
67
+ require_relative 'application'
68
+
69
+ # Initialize the Rails application.
70
+ Rails.application.initialize!
71
+ RUBY
72
+ end
73
+
74
+ unless File.exist?(File.join(dummy_app_path, "config/environments/test.rb"))
75
+ File.write(File.join(dummy_app_path, "config/environments/test.rb"), <<~RUBY)
76
+ Rails.application.configure do
77
+ # Settings specified here will take precedence over those in config/application.rb.
78
+
79
+ # The test environment is used exclusively to run your application's
80
+ # test suite. You never need to work with it otherwise. Remember that
81
+ # your test database is "scratch space" for the test suite and is wiped
82
+ # and recreated between test runs. Don't rely on the data there!
83
+ config.cache_classes = true
84
+
85
+ # Do not eager load code on boot. This avoids loading your whole application
86
+ # just for the purpose of running a single test. If you are using a tool that
87
+ # preloads Rails for running tests, you may have to set it to true.
88
+ config.eager_load = false
89
+
90
+ # Configure public file server for tests with Cache-Control for performance.
91
+ config.public_file_server.enabled = true
92
+ config.public_file_server.headers = {
93
+ 'Cache-Control' => "public, max-age=\#{1.hour.to_i}"
94
+ }
95
+
96
+ # Show full error reports and disable caching.
97
+ config.consider_all_requests_local = true
98
+ config.action_controller.perform_caching = false
99
+
100
+ # Raise exceptions instead of rendering exception templates.
101
+ config.action_dispatch.show_exceptions = false
102
+
103
+ # Disable request forgery protection in test environment.
104
+ config.action_controller.allow_forgery_protection = false
105
+
106
+ # Print deprecation notices to the stderr.
107
+ config.active_support.deprecation = :stderr
108
+
109
+ # Raises error for missing translations.
110
+ # config.action_view.raise_on_missing_translations = true
111
+ end
112
+ RUBY
113
+ end
114
+
115
+ unless File.exist?(File.join(dummy_app_path, "config/database.yml"))
116
+ File.write(File.join(dummy_app_path, "config/database.yml"), <<~YAML)
117
+ test:
118
+ adapter: sqlite3
119
+ database: ":memory:"
120
+ pool: 5
121
+ timeout: 5000
122
+ YAML
123
+ end
124
+
125
+ unless File.exist?(File.join(dummy_app_path, "config/routes.rb"))
126
+ File.write(File.join(dummy_app_path, "config/routes.rb"), <<~RUBY)
127
+ Rails.application.routes.draw do
128
+ mount SolidQueueMonitor::Engine => "/solid_queue"
129
+ end
130
+ RUBY
131
+ end
132
+
133
+ puts "Dummy app setup complete!"
134
+ end
135
+ end