modern_queue_dashboard 0.3.1

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 (34) hide show
  1. checksums.yaml +7 -0
  2. data/.npmignore +25 -0
  3. data/.npmrc +2 -0
  4. data/.rubocop.yml +18 -0
  5. data/CHANGELOG.md +37 -0
  6. data/CODE_OF_CONDUCT.md +16 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +168 -0
  9. data/Rakefile +70 -0
  10. data/app/assets/builds/modern_queue_dashboard.css +1 -0
  11. data/app/assets/builds/modern_queue_dashboard.js +19 -0
  12. data/app/assets/javascripts/modern_queue_dashboard.js +18 -0
  13. data/app/assets/stylesheets/modern_queue_dashboard.css +8 -0
  14. data/app/controllers/modern_queue_dashboard/application_controller.rb +7 -0
  15. data/app/controllers/modern_queue_dashboard/dashboard_controller.rb +10 -0
  16. data/app/controllers/modern_queue_dashboard/queues_controller.rb +19 -0
  17. data/app/models/modern_queue_dashboard/metrics.rb +49 -0
  18. data/app/models/modern_queue_dashboard/queue_summary.rb +52 -0
  19. data/app/views/layouts/modern_queue_dashboard/application.html.erb +15 -0
  20. data/app/views/modern_queue_dashboard/dashboard/index.html.erb +51 -0
  21. data/app/views/modern_queue_dashboard/queues/index.html.erb +35 -0
  22. data/app/views/modern_queue_dashboard/queues/show.html.erb +51 -0
  23. data/config/routes.rb +7 -0
  24. data/docs/PLAN.md +201 -0
  25. data/lib/modern_queue_dashboard/engine.rb +24 -0
  26. data/lib/modern_queue_dashboard/metrics.rb +43 -0
  27. data/lib/modern_queue_dashboard/queue_summary.rb +38 -0
  28. data/lib/modern_queue_dashboard/version.rb +5 -0
  29. data/lib/modern_queue_dashboard.rb +35 -0
  30. data/package-lock.json +1506 -0
  31. data/package.json +17 -0
  32. data/sig/modern_queue_dashboard.rbs +4 -0
  33. data/tailwind.config.js +11 -0
  34. metadata +196 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8f3b6ba3fdf6b602a2b5b6701717d6768a56ced216ec87b5f8aa36ac94a171c8
4
+ data.tar.gz: 7b96e4fd86584d611e1c1ac0b68eccc3239aecdcc1261e8bed105afd682bb17a
5
+ SHA512:
6
+ metadata.gz: 7280186443a102521984f7987e2a5664c63567cc646f7f5b1867893cdf518ac71643a5051fa40bae050be5bd752fb958ca1b01f14f305d01c643310a44b189b2
7
+ data.tar.gz: 710daee003dd37bbc49eb854d3ce106b4100660ab105038711304846271238e2058101590a9f32a4215990322dd3c40adb6b76a3056f1cc9e90ed4054e02c806
data/.npmignore ADDED
@@ -0,0 +1,25 @@
1
+ # Ruby gem files
2
+ *.gem
3
+ *.gemspec
4
+ Gemfile
5
+ Gemfile.lock
6
+ Rakefile
7
+ bin/
8
+ lib/
9
+ test/
10
+ app/
11
+ config/
12
+ docs/
13
+ .ruby-version
14
+ .yardoc
15
+ .rubocop.yml
16
+
17
+ # Other files
18
+ .git/
19
+ .github/
20
+ .gitignore
21
+ .DS_Store
22
+ .rspec_status
23
+ pkg/
24
+ tmp/
25
+ coverage/
data/.npmrc ADDED
@@ -0,0 +1,2 @@
1
+ bin-links=false
2
+ save-exact=true
data/.rubocop.yml ADDED
@@ -0,0 +1,18 @@
1
+ inherit_gem:
2
+ rubocop-rails-omakase: rubocop.yml
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 3.3.8
6
+ NewCops: enable
7
+ Exclude:
8
+ - 'test/dummy/**/*'
9
+ - 'bin/**/*'
10
+ - 'tmp/**/*'
11
+ - 'vendor/**/*'
12
+ - 'node_modules/**/*'
13
+
14
+ Style/StringLiterals:
15
+ EnforcedStyle: double_quotes
16
+
17
+ Style/StringLiteralsInInterpolation:
18
+ EnforcedStyle: double_quotes
data/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.3.1] - 2025-05-20
4
+
5
+ - Fixed compatibility with Solid Queue standard schema structure
6
+ - Updated queries to use `scheduled_at` instead of `run_at`
7
+ - Improved job status tracking via the appropriate Solid Queue tables
8
+ - Updated README to clarify that this dashboard is exclusively for Solid Queue
9
+
10
+ ## [0.3.0] - 2025-05-19
11
+
12
+ - Updated tailwindcss-rails dependency to ~> 4.0
13
+ - Updated turbo-rails dependency to ~> 2.0
14
+ - Improved compatibility with Rails 8.0.2
15
+
16
+ ## [0.2.2] - 2025-05-18
17
+
18
+ - Fixed CI workflow to run RuboCop and fail the build if there are issues
19
+ - Added a CHANGELOG.md file
20
+ - Added a Gemfile.lock file
21
+
22
+ ## [0.2.1] - 2023-07-30
23
+
24
+ - Updated UI color scheme from indigo to sky blue
25
+ - Fixed Tailwind CSS configuration and build process
26
+
27
+ ## [0.2.0] - 2023-07-28
28
+
29
+ - Added CI workflow with GitHub Actions
30
+ - Integrated RuboCop with Rails Omakase style
31
+ - Added proper test setup including unit and integration tests
32
+ - Improved engine compatibility with Rails 8 asset systems
33
+ - Added developer documentation and test helpers
34
+
35
+ ## [0.1.0] - 2023-07-25
36
+
37
+ - Initial release
@@ -0,0 +1,16 @@
1
+ # Community Guidelines for Conduct
2
+
3
+ Modern Queue Dashboard follows the Ruby on Rails guidelines for conduct in all collaborative spaces, including mailing lists, submitted patches, commit comments, and discussions:
4
+
5
+ * Participants are expected to be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free from personal attacks and disparaging remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behavior that could reasonably be considered harassment will not be tolerated.
9
+
10
+ ## Reporting
11
+
12
+ If you believe these guidelines have been violated, please contact the project maintainers.
13
+
14
+ ## Attribution
15
+
16
+ This Code of Conduct is adapted from the [Ruby on Rails Community Guidelines for Conduct](https://rubyonrails.org/conduct).
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Clayton Lengel-Zigich
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # Modern Queue Dashboard
2
+
3
+ A mountable Rails engine that provides a clean, Hotwire-powered dashboard **specifically designed for monitoring [Solid Queue](https://github.com/basecamp/solid_queue)** jobs. Built with Tailwind CSS, Turbo frames, and Stimulus controllers.
4
+
5
+ ![Dashboard Screenshot](screenshots/dashboard.png)
6
+
7
+ ## Features
8
+
9
+ * High-level metrics - counts for pending, scheduled, running, completed and failed jobs
10
+ * Per-queue statistics - job counts and latency metrics
11
+ * Job details - arguments, timestamps, state transitions, and error information
12
+ * Real-time updates - auto-refreshing metrics via Turbo Stream polling
13
+ * Clean UI - responsive interface using Tailwind CSS
14
+ * Zero setup - install, mount, and go
15
+
16
+ ## Requirements
17
+
18
+ * Rails 8.0+
19
+ * Ruby 3.3.8+
20
+ * **Solid Queue 1.1+** (this dashboard is exclusively for Solid Queue and does not support other job backends)
21
+
22
+ ## Installation
23
+
24
+ Add this line to your application's Gemfile:
25
+
26
+ ```ruby
27
+ gem "modern_queue_dashboard"
28
+ ```
29
+
30
+ And then execute:
31
+
32
+ ```bash
33
+ bundle install
34
+ ```
35
+
36
+ ## Mounting the Dashboard
37
+
38
+ Add the following to your `config/routes.rb`:
39
+
40
+ ```ruby
41
+ Rails.application.routes.draw do
42
+ # ... your other routes
43
+
44
+ # Mount the dashboard at /queue-dashboard
45
+ mount ModernQueueDashboard::Engine, at: "/queue-dashboard"
46
+ end
47
+ ```
48
+
49
+ ## Security
50
+
51
+ The dashboard doesn't include authentication by itself. You should restrict access using your application's authentication system.
52
+
53
+ ### With Devise
54
+
55
+ ```ruby
56
+ # In config/routes.rb
57
+ authenticate :user, -> { current_user.admin? } do
58
+ mount ModernQueueDashboard::Engine, at: "/queue-dashboard"
59
+ end
60
+ ```
61
+
62
+ ### With Basic Auth
63
+
64
+ ```ruby
65
+ # In config/routes.rb
66
+ require "authenticated_constraint"
67
+
68
+ constraints AuthenticatedConstraint.new do
69
+ mount ModernQueueDashboard::Engine, at: "/queue-dashboard"
70
+ end
71
+
72
+ # In lib/authenticated_constraint.rb
73
+ class AuthenticatedConstraint
74
+ def matches?(request)
75
+ return false unless request.session[:user_id]
76
+ user = User.find_by(id: request.session[:user_id])
77
+ user && user.admin?
78
+ end
79
+ end
80
+ ```
81
+
82
+ ### With HTTP Basic Auth
83
+
84
+ ```ruby
85
+ # In config/routes.rb
86
+ mount ModernQueueDashboard::Engine, at: "/queue-dashboard", constraints: lambda { |request|
87
+ ActiveSupport::SecurityUtils.secure_compare(
88
+ ::Digest::SHA256.hexdigest(request.headers["Authorization"].to_s),
89
+ ::Digest::SHA256.hexdigest("Basic #{Base64.encode64("username:password")}")
90
+ )
91
+ }
92
+ ```
93
+
94
+ ## Configuration
95
+
96
+ You can configure the dashboard by creating an initializer:
97
+
98
+ ```ruby
99
+ # config/initializers/modern_queue_dashboard.rb
100
+ ModernQueueDashboard.configure do |config|
101
+ config.refresh_interval = 5 # seconds
102
+ config.time_zone = "UTC"
103
+ end
104
+ ```
105
+
106
+ ## Development
107
+
108
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run the following:
109
+
110
+ ```bash
111
+ # Run unit tests
112
+ bundle exec rake unit_tests
113
+
114
+ # Run unit tests + RuboCop style checks
115
+ bundle exec rake ci
116
+
117
+ # Run just RuboCop
118
+ bundle exec rubocop
119
+ ```
120
+
121
+ ### Building Tailwind CSS
122
+
123
+ The dashboard uses Tailwind CSS for styling. When making changes to the styles, you need to rebuild the CSS file:
124
+
125
+ ```bash
126
+ # Install Node.js dependencies (only needed once)
127
+ npm install -D tailwindcss
128
+
129
+ # Build the CSS file
130
+ bin/build
131
+ ```
132
+
133
+ This will process the Tailwind directives in `app/assets/stylesheets/modern_queue_dashboard.css` and output the compiled CSS to `app/assets/builds/modern_queue_dashboard.css`.
134
+
135
+ ### Dummy Application
136
+
137
+ The gem includes a dummy Rails application in `test/dummy` for integration testing and development:
138
+
139
+ ```bash
140
+ # Start the dummy app server
141
+ cd test/dummy
142
+ bin/rails server
143
+
144
+ # Start the solid_queue worker
145
+ cd test/dummy
146
+ bin/rails solid_queue:process
147
+ ```
148
+
149
+ Then visit http://localhost:3000 and navigate to http://localhost:3000/queue-dashboard to see the dashboard.
150
+
151
+ ### CI Process
152
+
153
+ The CI pipeline runs on GitHub Actions and includes:
154
+ - Unit tests for the gem
155
+ - RuboCop style checks using the Rails Omakase style guide
156
+ - (Optional) Integration tests with the dummy application
157
+
158
+ ## Contributing
159
+
160
+ Bug reports and pull requests are welcome on GitHub at https://github.com/clayton/modern_queue_dashboard. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/clayton/modern_queue_dashboard/blob/main/CODE_OF_CONDUCT.md).
161
+
162
+ ## License
163
+
164
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
165
+
166
+ ## Code of Conduct
167
+
168
+ Everyone interacting in the Modern Queue Dashboard project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/clayton/modern_queue_dashboard/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ # Main test task for gem's unit tests
7
+ Rake::TestTask.new(:unit_tests) do |t|
8
+ t.libs << "test"
9
+ t.test_files = FileList["test/**/*_test.rb"].exclude("test/dummy/**/*_test.rb")
10
+ end
11
+
12
+ # Simple task that runs everything in sequence
13
+ desc "Run all tests and quality checks"
14
+ task :ci do
15
+ failed = false
16
+
17
+ # Run unit tests
18
+ puts "\n== Running unit tests =="
19
+ Rake::Task["unit_tests"].invoke
20
+
21
+ # Run dummy app tests if they exist, but don't fail the build if they fail
22
+ if File.directory?("test/dummy/test/integration")
23
+ puts "\n== Running integration tests (optional) =="
24
+ unless system("cd test/dummy && bin/rails test test/integration 2>/dev/null")
25
+ puts "Integration tests failed or could not run. This is allowed in CI."
26
+ end
27
+ end
28
+
29
+ # Always run Rubocop and fail the build if there are issues
30
+ puts "\n== Running RuboCop (Rails Omakase style) =="
31
+ unless system("bundle exec rubocop")
32
+ puts "\nRuboCop found issues - failing the build"
33
+ failed = true
34
+ end
35
+
36
+ # Fail the build if any step failed
37
+ fail "CI task failed" if failed
38
+ end
39
+
40
+ # More strict version for CI environments
41
+ desc "Run all tests and quality checks strictly (for CI)"
42
+ task :ci_strict do
43
+ failed = false
44
+
45
+ # Run unit tests
46
+ puts "\n== Running unit tests =="
47
+ Rake::Task["unit_tests"].invoke
48
+
49
+ # Run dummy app tests if they exist
50
+ if File.directory?("test/dummy/test/integration")
51
+ puts "\n== Running integration tests =="
52
+ unless system("cd test/dummy && bin/rails test test/integration")
53
+ puts "Warning: Integration tests failed or could not run"
54
+ failed = true
55
+ end
56
+ end
57
+
58
+ # Run Rubocop if available
59
+ if system("which rubocop > /dev/null 2>&1")
60
+ puts "\n== Running RuboCop =="
61
+ system("rubocop --format simple")
62
+ failed = true unless $?.success?
63
+ end
64
+
65
+ # Fail the build if any step failed
66
+ raise "CI task failed" if failed
67
+ end
68
+
69
+ # Set default task to run everything
70
+ task default: :ci
@@ -0,0 +1 @@
1
+ *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.mx-auto{margin-left:auto;margin-right:auto}.mb-4{margin-bottom:1rem}.flex{display:flex}.table{display:table}.grid{display:grid}.min-h-screen{min-height:100vh}.min-w-full{min-width:100%}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.items-center{align-items:center}.gap-4{gap:1rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity,1))}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.border{border-width:1px}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-wider{letter-spacing:.05em}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-sky-500{--tw-text-opacity:1;color:rgb(14 165 233/var(--tw-text-opacity,1))}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.jobs-container{border-radius:.25rem;--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1));padding:1rem;--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:1024px){.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}
@@ -0,0 +1,19 @@
1
+ // Modern Queue Dashboard JavaScript - This is a placeholder
2
+ // In a real application, this would be compiled
3
+ console.log('Modern Queue Dashboard loaded');
4
+
5
+ // Add auto-refresh functionality
6
+ document.addEventListener('turbo:load', () => {
7
+ const dashboardElement = document.querySelector('.modern-queue-dashboard');
8
+
9
+ if (dashboardElement) {
10
+ // Get refresh interval from data attribute (or default to 5000ms)
11
+ const refreshInterval = (parseInt(dashboardElement.dataset.refreshInterval) || 5) * 1000;
12
+
13
+ console.log(`Setting dashboard refresh interval: ${refreshInterval}ms`);
14
+
15
+ setInterval(() => {
16
+ Turbo.visit(window.location.href, { action: 'replace' });
17
+ }, refreshInterval);
18
+ }
19
+ });
@@ -0,0 +1,18 @@
1
+ // Modern Queue Dashboard JavaScript
2
+ console.log('Modern Queue Dashboard loaded');
3
+
4
+ // Add auto-refresh functionality
5
+ document.addEventListener('turbo:load', () => {
6
+ const dashboardElement = document.querySelector('.modern-queue-dashboard');
7
+
8
+ if (dashboardElement) {
9
+ // Get refresh interval from data attribute (or default to 5000ms)
10
+ const refreshInterval = (parseInt(dashboardElement.dataset.refreshInterval) || 5) * 1000;
11
+
12
+ console.log(`Setting dashboard refresh interval: ${refreshInterval}ms`);
13
+
14
+ setInterval(() => {
15
+ Turbo.visit(window.location.href, { action: 'replace' });
16
+ }, refreshInterval);
17
+ }
18
+ });
@@ -0,0 +1,8 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Custom styles for Modern Queue Dashboard */
6
+ .jobs-container {
7
+ @apply bg-white shadow rounded p-4;
8
+ }
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModernQueueDashboard
4
+ class ApplicationController < ::ApplicationController
5
+ layout "modern_queue_dashboard/application"
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModernQueueDashboard
4
+ class DashboardController < ApplicationController
5
+ def index
6
+ @metrics = Metrics.summary
7
+ @queues = QueueSummary.with_stats.limit(10)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModernQueueDashboard
4
+ class QueuesController < ApplicationController
5
+ def index
6
+ @queues = QueueSummary.with_stats
7
+ end
8
+
9
+ def show
10
+ @queue_name = params[:id]
11
+ @queue = QueueSummary.with_stats.detect { |q| q.name == @queue_name }
12
+
13
+ return if @queue
14
+
15
+ flash[:error] = "Queue not found"
16
+ redirect_to queues_path
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModernQueueDashboard
4
+ Metric = Struct.new(:key, :label, :value, keyword_init: true)
5
+
6
+ class Metrics
7
+ class << self
8
+ def summary
9
+ [
10
+ Metric.new(key: :pending, label: "Pending", value: pending_count),
11
+ Metric.new(key: :scheduled, label: "Scheduled", value: scheduled_count),
12
+ Metric.new(key: :running, label: "Running", value: running_count),
13
+ Metric.new(key: :failed, label: "Failed", value: failed_count),
14
+ Metric.new(key: :completed, label: "Completed", value: completed_count),
15
+ Metric.new(key: :latency, label: "Latency", value: latency_seconds)
16
+ ]
17
+ end
18
+
19
+ private
20
+
21
+ def pending_count
22
+ SolidQueue::Job.where("run_at <= ?", Time.current).count
23
+ end
24
+
25
+ def scheduled_count
26
+ SolidQueue::Job.where("run_at > ?", Time.current).count
27
+ end
28
+
29
+ def running_count
30
+ SolidQueue::Execution.where(finished_at: nil).count
31
+ end
32
+
33
+ def failed_count
34
+ SolidQueue::FailedExecution.count
35
+ end
36
+
37
+ def completed_count
38
+ SolidQueue::Execution.where.not(finished_at: nil).count
39
+ end
40
+
41
+ def latency_seconds
42
+ oldest = SolidQueue::Job.order(:created_at).limit(1).pick(:created_at)
43
+ return 0 unless oldest
44
+
45
+ (Time.current - oldest).to_i
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModernQueueDashboard
4
+ QueueStat = Struct.new(:name, :pending, :scheduled, :running, :failed, :latency, keyword_init: true)
5
+
6
+ class QueueSummary
7
+ class << self
8
+ def with_stats
9
+ queue_names.map { |q| stats_for(q) }.sort_by(&:name)
10
+ end
11
+
12
+ private
13
+
14
+ def queue_names
15
+ SolidQueue::Job.distinct.pluck(:queue_name)
16
+ end
17
+
18
+ def stats_for(name)
19
+ # Pending: Jobs scheduled for now or in the past but not finished
20
+ pending = SolidQueue::Job.where(queue_name: name)
21
+ .where("scheduled_at <= ? AND finished_at IS NULL", Time.current)
22
+ .count
23
+
24
+ # Scheduled: Jobs scheduled for the future
25
+ scheduled = SolidQueue::Job.where(queue_name: name)
26
+ .where("scheduled_at > ? AND finished_at IS NULL", Time.current)
27
+ .count
28
+
29
+ # Running: Jobs that are currently claimed but not finished
30
+ running = SolidQueue::ClaimedExecution.joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_claimed_executions.job_id")
31
+ .where("solid_queue_jobs.queue_name = ?", name)
32
+ .count
33
+
34
+ # Failed: Jobs that have failed executions
35
+ failed = SolidQueue::FailedExecution.joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_failed_executions.job_id")
36
+ .where("solid_queue_jobs.queue_name = ?", name)
37
+ .count
38
+
39
+ # Latency: Time since oldest unfinished job was scheduled
40
+ oldest_scheduled_at = SolidQueue::Job.where(queue_name: name)
41
+ .where(finished_at: nil)
42
+ .order(:scheduled_at)
43
+ .limit(1)
44
+ .pick(:scheduled_at)
45
+
46
+ latency = oldest_scheduled_at ? (Time.current - oldest_scheduled_at).to_i : 0
47
+
48
+ QueueStat.new(name:, pending:, scheduled:, running:, failed:, latency:)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>ModernQueueDashboard</title>
5
+ <%= csp_meta_tag %>
6
+ <%= csrf_meta_tags %>
7
+
8
+ <%= stylesheet_link_tag "modern_queue_dashboard", media: "all", "data-turbo-track": "reload" %>
9
+ <%= javascript_include_tag "modern_queue_dashboard", "data-turbo-track": "reload", defer: true %>
10
+ </head>
11
+
12
+ <body class="bg-gray-100 min-h-screen">
13
+ <%= yield %>
14
+ </body>
15
+ </html>
@@ -0,0 +1,51 @@
1
+ <div class="container mx-auto p-6 space-y-6 modern-queue-dashboard"
2
+ data-refresh-interval="<%= ModernQueueDashboard.configuration.refresh_interval %>">
3
+ <h1 class="text-3xl font-semibold">Modern Queue Dashboard</h1>
4
+
5
+ <div class="text-sm text-gray-500 mb-4">
6
+ Refresh interval: <%= ModernQueueDashboard.configuration.refresh_interval %> seconds |
7
+ Timezone: <%= ModernQueueDashboard.configuration.time_zone %>
8
+ </div>
9
+
10
+ <!-- Metric Cards -->
11
+ <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
12
+ <% @metrics.each do |metric| %>
13
+ <turbo-frame id="metric_<%= metric.key %>">
14
+ <div class="bg-white shadow-sm rounded p-4 text-center">
15
+ <p class="text-sm text-gray-500"><%= metric.label %></p>
16
+ <p class="text-2xl font-bold text-sky-500"><%= metric.value %></p>
17
+ </div>
18
+ </turbo-frame>
19
+ <% end %>
20
+ </div>
21
+
22
+ <!-- Queue Table -->
23
+ <div class="bg-white shadow rounded">
24
+ <table class="min-w-full divide-y divide-gray-200">
25
+ <thead class="bg-gray-50">
26
+ <tr>
27
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Queue</th>
28
+ <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Pending</th>
29
+ <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Scheduled</th>
30
+ <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Running</th>
31
+ <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Failed</th>
32
+ <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Latency</th>
33
+ </tr>
34
+ </thead>
35
+ <tbody class="bg-white divide-y divide-gray-200">
36
+ <% @queues.each do |queue| %>
37
+ <tr>
38
+ <td class="px-6 py-4 whitespace-nowrap font-medium text-sky-500">
39
+ <%= link_to queue.name, queue_path(queue.name), data: { turbo_frame: "_top" } %>
40
+ </td>
41
+ <td class="px-6 py-4 text-right"><%= queue.pending %></td>
42
+ <td class="px-6 py-4 text-right"><%= queue.scheduled %></td>
43
+ <td class="px-6 py-4 text-right"><%= queue.running %></td>
44
+ <td class="px-6 py-4 text-right"><%= queue.failed %></td>
45
+ <td class="px-6 py-4 text-right"><%= queue.latency %> ms</td>
46
+ </tr>
47
+ <% end %>
48
+ </tbody>
49
+ </table>
50
+ </div>
51
+ </div>