modern_queue_dashboard 0.3.1 → 0.5.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: 8f3b6ba3fdf6b602a2b5b6701717d6768a56ced216ec87b5f8aa36ac94a171c8
4
- data.tar.gz: 7b96e4fd86584d611e1c1ac0b68eccc3239aecdcc1261e8bed105afd682bb17a
3
+ metadata.gz: d7f06b4e0a9296b7bde46ce1e295a3ce73a8a719e70db335527ced702a0a01e0
4
+ data.tar.gz: 534a79e9d2b01d30dadebe9e0a7f23a7b512a3eee2f06431b84761057b593e0d
5
5
  SHA512:
6
- metadata.gz: 7280186443a102521984f7987e2a5664c63567cc646f7f5b1867893cdf518ac71643a5051fa40bae050be5bd752fb958ca1b01f14f305d01c643310a44b189b2
7
- data.tar.gz: 710daee003dd37bbc49eb854d3ce106b4100660ab105038711304846271238e2058101590a9f32a4215990322dd3c40adb6b76a3056f1cc9e90ed4054e02c806
6
+ metadata.gz: 9c8bdf048c53525cbfe2029e8f72b84ae41e0d90685fecf26a819b10897479625533bd72612f1f8f2ec4e7fd6c5b64d816ccb7d11329edd0e58c05e0c0ac6157
7
+ data.tar.gz: e9f56187f870dcb7fe98442405cb85e1c83e27f9daefcda2147c628a35c498e3675af3e784009679e1f05af3e8da4e7a943007c148010e21e492c4117df0cce9
data/CHANGELOG.md CHANGED
@@ -1,10 +1,40 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.3.1] - 2025-05-20
3
+ ## [0.5.0] - 2025-05-20
4
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
5
+ - Added job retry and discard functionality
6
+ - Implemented status filtering with pagination
7
+ - Updated job summary display with enhanced information
8
+ - Added pagination support using Pagy
9
+ - Updated queue and job models to accommodate new features
10
+ - Improved error handling throughout the application
11
+
12
+ ## [0.4.1] - 2025-05-19
13
+
14
+ - Fixed issue with jobs that have no arguments showing "Error parsing arguments"
15
+ - Improved argument handling and display in job tables
16
+
17
+ ## [0.4.0] - 2025-05-19
18
+
19
+ - Changed Tailwind's Sky color to Blue color for better contrast
20
+ - Added jobs table to queue view showing up to 50 most recent jobs with their status
21
+ - Added recent jobs table to main dashboard showing 10 most recent jobs across all queues
22
+ - Added color-coded status indicators for jobs
23
+ - Display detailed error information for failed jobs
24
+
25
+ ## [0.3.2] - 2025-05-19
26
+
27
+ - Fixed issue with load order
28
+
29
+ ## [0.3.1] - 2025-05-19
30
+
31
+ - Fixed compatibility with Solid Queue's database schema
32
+ - Updated queries to properly read from Solid Queue's execution tables:
33
+ - Ready jobs from `solid_queue_ready_executions`
34
+ - Scheduled jobs from `solid_queue_scheduled_executions`
35
+ - Running jobs from `solid_queue_claimed_executions`
36
+ - Failed jobs from `solid_queue_failed_executions`
37
+ - Added error handling for cases where tables might not be available
8
38
  - Updated README to clarify that this dashboard is exclusively for Solid Queue
9
39
 
10
40
  ## [0.3.0] - 2025-05-19
data/README.md CHANGED
@@ -1,9 +1,17 @@
1
1
  # Modern Queue Dashboard
2
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.
3
+ A mountable Rails engine that provides a clean dashboard **specifically designed for monitoring [Solid Queue](https://github.com/basecamp/solid_queue)** jobs.
4
4
 
5
5
  ![Dashboard Screenshot](screenshots/dashboard.png)
6
6
 
7
+ ## Why not use Mission Control?
8
+
9
+ [Mission Control](https://github.com/rails/mission_control-jobs) is a great tool for monitoring Active Job in Rails. I mostly forgot that it existed and built this for fun. You should probably just use that. 😄
10
+
11
+ ## Why the "Modern Queue Dashboard"?
12
+
13
+ I didn't want to give the impression that there is any association with Solid Queue and because this dashboard could be used with other job backends in the future.
14
+
7
15
  ## Features
8
16
 
9
17
  * High-level metrics - counts for pending, scheduled, running, completed and failed jobs
@@ -79,18 +87,6 @@ class AuthenticatedConstraint
79
87
  end
80
88
  ```
81
89
 
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
90
  ## Configuration
95
91
 
96
92
  You can configure the dashboard by creating an initializer:
@@ -166,3 +162,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
166
162
  ## Code of Conduct
167
163
 
168
164
  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).
165
+
166
+ ## Acknowledgements
167
+
168
+ This project was inspired by [active_storage_dashboard](https://github.com/giovapanasiti/active_storage_dashboard)
@@ -1 +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))}}
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.13 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;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;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}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-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height: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{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{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}}.sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border-width:0;white-space:nowrap}.relative{position:relative}.z-0{z-index:0}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.max-h-40{max-height:10rem}.max-h-\[500px\]{max-height:500px}.min-h-screen{min-height:100vh}.min-w-full{min-width:100%}.max-w-xs{max-width:20rem}.cursor-not-allowed{cursor:not-allowed}.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}.justify-between{justify-content:space-between}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.-space-x-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(-1px*(1 - var(--tw-space-x-reverse)));margin-right:calc(-1px*var(--tw-space-x-reverse))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.75rem*var(--tw-space-x-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - 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))}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-l-md{border-bottom-left-radius:.375rem;border-top-left-radius:.375rem}.rounded-r-md{border-bottom-right-radius:.375rem;border-top-right-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(220 252 231/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-purple-50{--tw-bg-opacity:1;background-color:rgb(250 245 255/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity))}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-4{padding-top:1rem}.pb-4,.py-4{padding-bottom:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.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}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-5{line-height:1.25rem}.tracking-wider{letter-spacing:.05em}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52/var(--tw-text-opacity))}.text-purple-700{--tw-text-opacity:1;color:rgb(126 34 206/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity))}.text-yellow-800{--tw-text-opacity:1;color:rgb(133 77 14/var(--tw-text-opacity))}.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)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-blue-300{--tw-ring-opacity:1;--tw-ring-color:rgb(147 197 253/var(--tw-ring-opacity))}.ring-blue-700\/10{--tw-ring-color:rgba(29,78,216,.1)}.ring-gray-300{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.ring-gray-500\/10{--tw-ring-color:hsla(220,9%,46%,.1)}.ring-green-600\/20{--tw-ring-color:rgba(22,163,74,.2)}.ring-purple-700\/10{--tw-ring-color:rgba(126,34,206,.1)}.ring-red-300{--tw-ring-opacity:1;--tw-ring-color:rgb(252 165 165/var(--tw-ring-opacity))}.ring-red-600\/10{--tw-ring-color:rgba(220,38,38,.1)}.ring-yellow-600\/20{--tw-ring-color:rgba(202,138,4,.2)}.transition-shadow{transition-duration:.15s;transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-200{transition-duration:.2s}.jobs-container{border-radius:.25rem;--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));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)}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.hover\:text-blue-700:hover{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.hover\:text-blue-800:hover{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity))}.hover\:text-red-700:hover{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px 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:640px){.sm\:flex{display:flex}.sm\:flex-1{flex:1 1 0%}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@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-5{grid-template-columns:repeat(5,minmax(0,1fr))}}@media (min-width:1024px){.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}
@@ -5,6 +5,7 @@ module ModernQueueDashboard
5
5
  def index
6
6
  @metrics = Metrics.summary
7
7
  @queues = QueueSummary.with_stats.limit(10)
8
+ @recent_jobs = JobSummary.all_jobs(10) # Show 10 most recent jobs across all queues
8
9
  end
9
10
  end
10
11
  end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModernQueueDashboard
4
+ class JobsController < ApplicationController
5
+ def show
6
+ @job_id = params[:id]
7
+ job = SolidQueue::Job.find_by(id: @job_id)
8
+
9
+ if job
10
+ # Check if this is a failed job and get error details
11
+ if SolidQueue::FailedExecution.exists?(job_id: job.id)
12
+ @failed_execution = SolidQueue::FailedExecution.find_by(job_id: job.id)
13
+
14
+ # Make the raw error string available to the view
15
+ @raw_error = @failed_execution.error if @failed_execution&.error.present?
16
+ end
17
+
18
+ @job_stat = JobSummary.job_details(job)
19
+ @queue_name = job.queue_name
20
+ else
21
+ flash[:error] = "Job not found"
22
+ redirect_to root_path
23
+ end
24
+ end
25
+
26
+ def retry
27
+ @job_id = params[:id]
28
+ job = SolidQueue::Job.find_by(id: @job_id)
29
+
30
+ if job
31
+ # Use the same job retry logic from queues controller
32
+ if perform_job_retry(job)
33
+ flash[:notice] = "Job #{@job_id} has been requeued for retry"
34
+ else
35
+ flash[:error] = "Unable to retry job #{@job_id}"
36
+ end
37
+ else
38
+ flash[:error] = "Job #{@job_id} not found"
39
+ end
40
+
41
+ redirect_to job_path(@job_id)
42
+ end
43
+
44
+ def discard
45
+ @job_id = params[:id]
46
+ job = SolidQueue::Job.find_by(id: @job_id)
47
+
48
+ if job
49
+ # Use the same job discard logic from queues controller
50
+ if perform_job_discard(job)
51
+ flash[:notice] = "Job #{@job_id} has been discarded"
52
+ else
53
+ flash[:error] = "Unable to discard job #{@job_id}"
54
+ end
55
+ else
56
+ flash[:error] = "Job #{@job_id} not found"
57
+ end
58
+
59
+ redirect_to job_path(@job_id)
60
+ end
61
+
62
+ private
63
+
64
+ def perform_job_retry(job)
65
+ ActiveRecord::Base.transaction do
66
+ # Remove the job from failed executions if it failed
67
+ SolidQueue::FailedExecution.where(job_id: job.id).delete_all
68
+
69
+ # Check if the job is in any execution table and handle accordingly
70
+ case
71
+ when SolidQueue::ClaimedExecution.exists?(job_id: job.id)
72
+ # Running job - can't retry while running
73
+ return false
74
+ when SolidQueue::ScheduledExecution.exists?(job_id: job.id)
75
+ # If scheduled, we'll just reschedule it to run immediately
76
+ SolidQueue::ScheduledExecution.where(job_id: job.id).delete_all
77
+ end
78
+
79
+ # Re-create a ReadyExecution for the job to make it pending again
80
+ SolidQueue::ReadyExecution.create!(
81
+ job_id: job.id,
82
+ queue_name: job.queue_name,
83
+ priority: job.priority || 0
84
+ )
85
+
86
+ # Reset the job's finished_at to allow it to run again
87
+ job.update!(finished_at: nil)
88
+
89
+ return true
90
+ rescue => e
91
+ # Failed to retry job
92
+ return false
93
+ end
94
+ end
95
+
96
+ def perform_job_discard(job)
97
+ ActiveRecord::Base.transaction do
98
+ # Remove from all execution tables
99
+ SolidQueue::FailedExecution.where(job_id: job.id).delete_all
100
+ SolidQueue::ReadyExecution.where(job_id: job.id).delete_all
101
+ SolidQueue::ScheduledExecution.where(job_id: job.id).delete_all
102
+ SolidQueue::ClaimedExecution.where(job_id: job.id).delete_all
103
+
104
+ # Mark job as completed (discarded)
105
+ job.update!(finished_at: Time.current)
106
+
107
+ return true
108
+ rescue => e
109
+ # Failed to discard job
110
+ return false
111
+ end
112
+ end
113
+ end
114
+ end
@@ -9,11 +9,127 @@ module ModernQueueDashboard
9
9
  def show
10
10
  @queue_name = params[:id]
11
11
  @queue = QueueSummary.with_stats.detect { |q| q.name == @queue_name }
12
+ @status = params[:status]
12
13
 
13
- return if @queue
14
+ if @queue
15
+ # Set up pagination with Pagy
16
+ page = params[:page] || 1
17
+ per_page = 50
14
18
 
15
- flash[:error] = "Queue not found"
16
- redirect_to queues_path
19
+ if @status.present?
20
+ # Get the accurate count for this status
21
+ @total_count = JobSummary.count_jobs_by_status(@queue_name, @status)
22
+
23
+ # Use our specialized method to get jobs with this status
24
+ @jobs_collection, _ = JobSummary.for_queue_with_status(@queue_name, @status, page.to_i, per_page)
25
+ @pagy = Pagy.new(count: @total_count, page: page.to_i, items: per_page)
26
+ @jobs = @jobs_collection
27
+ else
28
+ # When no status filter, use regular pagination
29
+ @total_count = SolidQueue::Job.where(queue_name: @queue_name).count
30
+ @pagy = Pagy.new(count: @total_count, page: page.to_i, items: per_page)
31
+ @jobs = JobSummary.for_queue(@queue_name, per_page)
32
+ end
33
+ else
34
+ flash[:error] = "Queue not found"
35
+ redirect_to queues_path
36
+ end
37
+ end
38
+
39
+ def retry_job
40
+ @queue_name = params[:id]
41
+ job_id = params[:job_id]
42
+
43
+ # Find the job in the database
44
+ job = SolidQueue::Job.find_by(id: job_id)
45
+
46
+ if job
47
+ # Handle retry logic based on job status
48
+ if perform_job_retry(job)
49
+ flash[:notice] = "Job #{job_id} has been requeued for retry"
50
+ else
51
+ flash[:error] = "Unable to retry job #{job_id}"
52
+ end
53
+ else
54
+ flash[:error] = "Job #{job_id} not found"
55
+ end
56
+
57
+ redirect_to queue_path(@queue_name)
58
+ end
59
+
60
+ def discard_job
61
+ @queue_name = params[:id]
62
+ job_id = params[:job_id]
63
+
64
+ # Find the job in the database
65
+ job = SolidQueue::Job.find_by(id: job_id)
66
+
67
+ if job
68
+ # Handle discard logic
69
+ if perform_job_discard(job)
70
+ flash[:notice] = "Job #{job_id} has been discarded"
71
+ else
72
+ flash[:error] = "Unable to discard job #{job_id}"
73
+ end
74
+ else
75
+ flash[:error] = "Job #{job_id} not found"
76
+ end
77
+
78
+ redirect_to queue_path(@queue_name)
79
+ end
80
+
81
+ private
82
+
83
+ def perform_job_retry(job)
84
+ ActiveRecord::Base.transaction do
85
+ # Remove the job from failed executions if it failed
86
+ SolidQueue::FailedExecution.where(job_id: job.id).delete_all
87
+
88
+ # Check if the job is in any execution table and handle accordingly
89
+ case
90
+ when SolidQueue::ClaimedExecution.exists?(job_id: job.id)
91
+ # Running job - can't retry while running
92
+ return false
93
+ when SolidQueue::ScheduledExecution.exists?(job_id: job.id)
94
+ # If scheduled, we'll just reschedule it to run immediately
95
+ SolidQueue::ScheduledExecution.where(job_id: job.id).delete_all
96
+ end
97
+
98
+ # Re-create a ReadyExecution for the job to make it pending again
99
+ SolidQueue::ReadyExecution.create!(
100
+ job_id: job.id,
101
+ queue_name: job.queue_name,
102
+ priority: job.priority || 0
103
+ )
104
+
105
+ # Reset the job's finished_at to allow it to run again
106
+ job.update!(finished_at: nil)
107
+
108
+ return true
109
+ rescue => e
110
+ # Log the error for debugging
111
+ Rails.logger.error("Error retrying job #{job.id}: #{e.message}")
112
+ return false
113
+ end
114
+ end
115
+
116
+ def perform_job_discard(job)
117
+ ActiveRecord::Base.transaction do
118
+ # Remove from all execution tables
119
+ SolidQueue::FailedExecution.where(job_id: job.id).delete_all
120
+ SolidQueue::ReadyExecution.where(job_id: job.id).delete_all
121
+ SolidQueue::ScheduledExecution.where(job_id: job.id).delete_all
122
+ SolidQueue::ClaimedExecution.where(job_id: job.id).delete_all
123
+
124
+ # Mark job as completed (discarded)
125
+ job.update!(finished_at: Time.current)
126
+
127
+ return true
128
+ rescue => e
129
+ # Log the error for debugging
130
+ Rails.logger.error("Error discarding job #{job.id}: #{e.message}")
131
+ return false
132
+ end
17
133
  end
18
134
  end
19
135
  end
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModernQueueDashboard
4
+ JobStat = Struct.new(:id, :class_name, :queue_name, :arguments, :created_at, :status, :error, :exception_class, :backtrace, keyword_init: true)
5
+
6
+ # Collection class for JobStat objects
7
+ class JobStatCollection
8
+ include Enumerable
9
+
10
+ def initialize(stats)
11
+ @stats = stats
12
+ end
13
+
14
+ def each(&block)
15
+ @stats.each(&block)
16
+ end
17
+
18
+ def limit(num)
19
+ JobStatCollection.new(@stats.take(num))
20
+ end
21
+ end
22
+
23
+ class JobSummary
24
+ class << self
25
+ def for_queue(queue_name, limit = 50)
26
+ return JobStatCollection.new([]) if test_environment?
27
+
28
+ # Get jobs from SolidQueue::Job, ordered by most recently created
29
+ jobs = SolidQueue::Job.where(queue_name: queue_name)
30
+ .order(created_at: :desc)
31
+ .limit(limit)
32
+ .map { |job| job_to_stat(job) }
33
+
34
+ JobStatCollection.new(jobs)
35
+ end
36
+
37
+ def for_queue_with_status(queue_name, status, page = 1, items = 50)
38
+ return JobStatCollection.new([]) if test_environment?
39
+
40
+ # Base query for jobs in this queue
41
+ base_query = SolidQueue::Job.where(queue_name: queue_name)
42
+
43
+ # First, determine how to filter by status
44
+ if status.present?
45
+ # Pre-filter records based on status to get accurate counts
46
+ case status.to_s
47
+ when "completed"
48
+ jobs = base_query.where.not(finished_at: nil)
49
+ filtered_count = jobs.count
50
+ when "failed"
51
+ # Get job IDs that have failed executions
52
+ failed_job_ids = SolidQueue::FailedExecution.pluck(:job_id)
53
+ jobs = base_query.where(id: failed_job_ids)
54
+ filtered_count = jobs.count
55
+ when "running"
56
+ # Get job IDs that have claimed executions
57
+ running_job_ids = SolidQueue::ClaimedExecution.pluck(:job_id)
58
+ jobs = base_query.where(id: running_job_ids)
59
+ filtered_count = jobs.count
60
+ when "scheduled"
61
+ # Get job IDs that have scheduled executions
62
+ scheduled_job_ids = SolidQueue::ScheduledExecution.pluck(:job_id)
63
+ jobs = base_query.where(id: scheduled_job_ids)
64
+ filtered_count = jobs.count
65
+ when "pending"
66
+ # Get job IDs that have ready executions
67
+ pending_job_ids = SolidQueue::ReadyExecution.pluck(:job_id)
68
+ jobs = base_query.where(id: pending_job_ids)
69
+ filtered_count = jobs.count
70
+ else
71
+ # Unknown status - return all jobs but will be filtered later
72
+ jobs = base_query
73
+ filtered_count = jobs.count
74
+ end
75
+ else
76
+ # No status filter - use all jobs
77
+ jobs = base_query
78
+ filtered_count = jobs.count
79
+ end
80
+
81
+ # Apply pagination
82
+ offset = (page - 1) * items
83
+ paginated_jobs = jobs.order(created_at: :desc).offset(offset).limit(items)
84
+
85
+ # Convert to JobStat objects
86
+ job_stats = paginated_jobs.map { |job| job_to_stat(job) }
87
+
88
+ # Double-check status if we couldn't pre-filter accurately
89
+ if status.present? && status.to_s != "completed" && status.to_s != "unknown"
90
+ # For statuses that might need more accurate filtering (though we pre-filtered above)
91
+ job_stats = job_stats.select { |job| job.status == status.to_s }
92
+ end
93
+
94
+ # Return collection and filtered count for pagination
95
+ [ JobStatCollection.new(job_stats), filtered_count ]
96
+ end
97
+
98
+ def all_jobs(limit = 50)
99
+ return JobStatCollection.new([]) if test_environment?
100
+
101
+ # Get all jobs, ordered by most recently created
102
+ jobs = SolidQueue::Job.order(created_at: :desc)
103
+ .limit(limit)
104
+ .map { |job| job_to_stat(job) }
105
+
106
+ JobStatCollection.new(jobs)
107
+ end
108
+
109
+ def count_jobs_by_status(queue_name, status)
110
+ return 0 if test_environment?
111
+
112
+ base_query = SolidQueue::Job.where(queue_name: queue_name)
113
+
114
+ case status.to_s
115
+ when "completed"
116
+ base_query.where.not(finished_at: nil).count
117
+ when "failed"
118
+ failed_job_ids = SolidQueue::FailedExecution.pluck(:job_id)
119
+ base_query.where(id: failed_job_ids).count
120
+ when "running"
121
+ running_job_ids = SolidQueue::ClaimedExecution.pluck(:job_id)
122
+ base_query.where(id: running_job_ids).count
123
+ when "scheduled"
124
+ scheduled_job_ids = SolidQueue::ScheduledExecution.pluck(:job_id)
125
+ base_query.where(id: scheduled_job_ids).count
126
+ when "pending"
127
+ pending_job_ids = SolidQueue::ReadyExecution.pluck(:job_id)
128
+ base_query.where(id: pending_job_ids).count
129
+ else
130
+ base_query.count
131
+ end
132
+ end
133
+
134
+ def job_details(job)
135
+ # This method returns a single JobStat object with all details for a specific job
136
+ return nil unless job
137
+
138
+ # Get detailed information about the job
139
+ job_stat = job_to_stat(job)
140
+
141
+ # Add additional details you might want to include in the job details page
142
+ # For example, you could add execution history, timing information, etc.
143
+
144
+ job_stat
145
+ end
146
+
147
+ private
148
+
149
+ def job_to_stat(job)
150
+ # Determine job status
151
+ status = determine_status(job)
152
+
153
+ # Get error data if job failed
154
+ error = nil
155
+ exception_class = nil
156
+ backtrace = nil
157
+ raw_error = nil
158
+
159
+ if status == "failed"
160
+ failed_execution = SolidQueue::FailedExecution.find_by(job_id: job.id)
161
+ if failed_execution&.error.present?
162
+ # Store the raw error for display
163
+ raw_error = failed_execution.error
164
+ end
165
+ end
166
+
167
+ # Parse arguments - handle cases with no arguments properly
168
+ arguments_display = if job.arguments.nil? || !job.arguments.is_a?(String) || job.arguments.empty? || (job.arguments.is_a?(String) && job.arguments.strip == "")
169
+ "None"
170
+ else
171
+ begin
172
+ # Try to parse as JSON first
173
+ arguments_data = JSON.parse(job.arguments)
174
+ format_arguments(arguments_data)
175
+ rescue JSON::ParserError
176
+ # If we can't parse as JSON, show as is but check if it's meaningful
177
+ raw_value = job.arguments.to_s.strip
178
+ raw_value.present? ? raw_value : "None"
179
+ end
180
+ end
181
+
182
+ # Create job stat with the raw error data
183
+ JobStat.new(
184
+ id: job.id,
185
+ class_name: job.class_name,
186
+ queue_name: job.queue_name,
187
+ arguments: arguments_display,
188
+ created_at: job.created_at,
189
+ status: status,
190
+ error: raw_error,
191
+ exception_class: nil,
192
+ backtrace: nil
193
+ )
194
+ rescue => e
195
+ # Only show parsing error if there were actually arguments to parse
196
+ arg_display = if job.arguments.present? && job.arguments.is_a?(String) && job.arguments.strip != ""
197
+ "Error parsing arguments"
198
+ else
199
+ "None"
200
+ end
201
+
202
+ JobStat.new(
203
+ id: job.id,
204
+ class_name: job.class_name,
205
+ queue_name: job.queue_name,
206
+ arguments: arg_display,
207
+ created_at: job.created_at,
208
+ status: status,
209
+ error: e.message,
210
+ exception_class: nil,
211
+ backtrace: nil
212
+ )
213
+ end
214
+
215
+ def determine_status(job)
216
+ if job.finished_at.present?
217
+ "completed"
218
+ elsif SolidQueue::FailedExecution.exists?(job_id: job.id)
219
+ "failed"
220
+ elsif SolidQueue::ClaimedExecution.exists?(job_id: job.id)
221
+ "running"
222
+ elsif SolidQueue::ScheduledExecution.exists?(job_id: job.id)
223
+ "scheduled"
224
+ elsif SolidQueue::ReadyExecution.exists?(job_id: job.id)
225
+ "pending"
226
+ else
227
+ "unknown"
228
+ end
229
+ end
230
+
231
+ def format_arguments(args)
232
+ return "None" if args.blank?
233
+
234
+ if args.is_a?(Array)
235
+ if args.empty?
236
+ "None"
237
+ else
238
+ # Truncate long arguments to prevent display issues
239
+ args.map do |arg|
240
+ if arg.nil?
241
+ "nil"
242
+ elsif arg.is_a?(String) && arg.length > 100
243
+ arg[0..100] + "..."
244
+ else
245
+ arg.to_s
246
+ end
247
+ end.join(", ")
248
+ end
249
+ elsif args.is_a?(Hash)
250
+ if args.empty?
251
+ "None"
252
+ else
253
+ # Format hash in a more readable way
254
+ args.map do |k, v|
255
+ v_str = v.to_s
256
+ value = v_str.length > 50 ? "#{v_str[0..50]}..." : v_str
257
+ "#{k}: #{value}"
258
+ end.join(", ")
259
+ end
260
+ else
261
+ # For other types, just convert to string
262
+ str = args.to_s
263
+ str.present? ? str : "None"
264
+ end
265
+ end
266
+
267
+ def test_environment?
268
+ ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
269
+ end
270
+ end
271
+ end
272
+ end