job_harbor 0.6.1 → 0.7.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.
- checksums.yaml +4 -4
- data/README.md +136 -16
- data/app/assets/stylesheets/job_harbor/application.css +1 -1
- data/app/assets/stylesheets/job_harbor/application.tailwind.css +8 -0
- data/app/components/job_harbor/badge_component.rb +5 -3
- data/app/components/job_harbor/chart_component.rb +2 -4
- data/app/components/job_harbor/failure_rates_component.rb +19 -7
- data/app/components/job_harbor/job_filters_component.rb +7 -2
- data/app/components/job_harbor/job_row_component.rb +9 -4
- data/app/components/job_harbor/per_page_selector_component.rb +1 -3
- data/app/components/job_harbor/queue_card_component.rb +3 -2
- data/app/components/job_harbor/refresh_selector_component.rb +9 -4
- data/app/components/job_harbor/theme_toggle_component.rb +1 -2
- data/app/components/job_harbor/worker_card_component.rb +19 -12
- data/app/controllers/job_harbor/application_controller.rb +7 -3
- data/app/controllers/job_harbor/dashboard_controller.rb +1 -1
- data/app/controllers/job_harbor/jobs_controller.rb +73 -41
- data/app/controllers/job_harbor/queues_controller.rb +4 -5
- data/app/controllers/job_harbor/recurring_tasks_controller.rb +36 -8
- data/app/controllers/job_harbor/workers_controller.rb +2 -1
- data/app/models/job_harbor/chart_data.rb +22 -63
- data/app/models/job_harbor/dashboard_stats.rb +3 -20
- data/app/models/job_harbor/failure_stats.rb +8 -44
- data/app/models/job_harbor/job_presenter.rb +83 -133
- data/app/models/job_harbor/page_info.rb +9 -0
- data/app/models/job_harbor/processes.rb +25 -0
- data/app/models/job_harbor/queue_stats.rb +15 -12
- data/app/models/job_harbor/recurring_task_arguments.rb +29 -0
- data/app/models/job_harbor/sensitive_value_filter.rb +68 -0
- data/app/views/job_harbor/jobs/index.html.erb +12 -8
- data/app/views/job_harbor/jobs/search.html.erb +3 -3
- data/app/views/job_harbor/jobs/show.html.erb +15 -5
- data/app/views/job_harbor/queues/show.html.erb +9 -12
- data/app/views/job_harbor/recurring_tasks/show.html.erb +6 -6
- data/app/views/job_harbor/workers/index.html.erb +4 -2
- data/app/views/layouts/job_harbor/application.html.erb +58 -27
- data/lib/job_harbor/configuration.rb +30 -3
- data/lib/job_harbor/engine.rb +4 -11
- data/lib/job_harbor/version.rb +1 -1
- metadata +19 -12
- data/lib/tasks/solidqueue_dashboard_tasks.rake +0 -4
- data/lib/tasks/tailwind.rake +0 -129
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61fd4075abe3acb8265885f2ff9e5d007bede4df800c2c466b98e11cbc1810b2
|
|
4
|
+
data.tar.gz: c7b3836df141e4711ccb3f4828eb926da10f6f5d9901a996338e7977cb011641
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fca96f13699a78acf5e848527c86df9f98e53ca6f1ce7ab7bd4b7d0ba923e737e8badadc86d990627bed83ffe8c81d07b1c324e0654f4f0704a5790bc223bf26
|
|
7
|
+
data.tar.gz: f3b64f11f8a06145c40830703b2d48f7ced097e01a173b3c63a9c645bb880701bf4e13174c2c3ecc9a570d7bbec9e4a2bd422888e9751c645138429720bfb412
|
data/README.md
CHANGED
|
@@ -100,7 +100,8 @@ Create an initializer at `config/initializers/job_harbor.rb`:
|
|
|
100
100
|
|
|
101
101
|
```ruby
|
|
102
102
|
JobHarbor.configure do |config|
|
|
103
|
-
# Authorization - must return true to allow access
|
|
103
|
+
# Authorization - must return true to allow access.
|
|
104
|
+
# Default: open in development and test, denied in every other environment until you set this.
|
|
104
105
|
config.authorize_with = -> { current_user&.admin? }
|
|
105
106
|
|
|
106
107
|
# Theme: :dark or :light (default: :dark)
|
|
@@ -109,12 +110,13 @@ JobHarbor.configure do |config|
|
|
|
109
110
|
# Jobs per page (default: 25)
|
|
110
111
|
config.jobs_per_page = 25
|
|
111
112
|
|
|
112
|
-
# Auto-refresh polling interval in seconds (default:
|
|
113
|
-
|
|
113
|
+
# Auto-refresh polling interval in seconds (default: 30)
|
|
114
|
+
# The selector offers Off, 15, 30, 60 and 300; any other value is added as its own option
|
|
115
|
+
config.poll_interval = 30
|
|
114
116
|
|
|
115
117
|
# Toggle features on/off
|
|
116
118
|
config.enable_recurring_tasks = true # default: true
|
|
117
|
-
config.enable_real_time_updates = true # default: true
|
|
119
|
+
config.enable_real_time_updates = true # default: true (false removes auto-refresh entirely)
|
|
118
120
|
config.enable_failure_stats = true # default: true
|
|
119
121
|
config.enable_charts = true # default: true
|
|
120
122
|
|
|
@@ -127,33 +129,151 @@ JobHarbor.configure do |config|
|
|
|
127
129
|
|
|
128
130
|
# Also accepts a proc for dynamic paths:
|
|
129
131
|
# config.return_to_app_path = -> { main_app.root_path }
|
|
132
|
+
|
|
133
|
+
# Sensitive values (default: nil, use the app's config.filter_parameters)
|
|
134
|
+
# An array replaces that list; false shows raw values and brings argument search back.
|
|
135
|
+
config.filter_parameters = nil
|
|
130
136
|
end
|
|
131
137
|
```
|
|
132
138
|
|
|
139
|
+
### Sensitive values
|
|
140
|
+
|
|
141
|
+
Job arguments, the full payload and recurring task arguments are filtered with your app's
|
|
142
|
+
`config.filter_parameters`, read each time a page renders. Values under keys like `password` or
|
|
143
|
+
`token` show as `[FILTERED]`. Active Job's own `_aj_*` markers are left as they are.
|
|
144
|
+
|
|
145
|
+
While filtering is on, search matches job class, job ID and Active Job ID, but not argument
|
|
146
|
+
text: matching hidden values would let a search reveal them. Set `config.filter_parameters = false`
|
|
147
|
+
to show raw values and search arguments again.
|
|
148
|
+
|
|
149
|
+
Key filters don't redact free text or arguments passed positionally (not in a hash), because
|
|
150
|
+
those have no key. Error messages and backtraces are also shown as written. A Proc in
|
|
151
|
+
`filter_parameters` can scrub all of these by pattern; the dashboard runs it over positional
|
|
152
|
+
strings, error messages and backtraces too:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
Rails.application.config.filter_parameters << ->(_key, value) {
|
|
156
|
+
value.gsub!(/\b(tok|sk)_live_\w+/, "[FILTERED]") if value.is_a?(String)
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
If a filter raises, the whole value shows as `[FILTERED]`.
|
|
161
|
+
|
|
162
|
+
`config.filter_parameters` accepts only `nil` (use the app's list), `false` (raw values) or an
|
|
163
|
+
Array of Strings, Symbols, Regexps and Procs (replaces the list). Anything else, including `true` or `[true]`, raises `ArgumentError` at boot so a
|
|
164
|
+
misconfiguration can't quietly show everything. An empty Array `[]` means "mask nothing": values
|
|
165
|
+
show raw and argument search comes back, the same as `false`.
|
|
166
|
+
|
|
167
|
+
Search queries you type appear in your request logs as the `q` parameter. Add `:q` to your app's
|
|
168
|
+
`filter_parameters` if searches may contain sensitive values.
|
|
169
|
+
|
|
133
170
|
## Authorization
|
|
134
171
|
|
|
135
|
-
|
|
172
|
+
**Production installs must configure `authorize_with`.** Until you do, the dashboard
|
|
173
|
+
denies every request in every environment except development and test (production,
|
|
174
|
+
staging, and so on) and logs a warning at boot. In development and test it stays open
|
|
175
|
+
so you can try it out without setup.
|
|
176
|
+
|
|
177
|
+
`authorize_with` is a lambda evaluated in the context of the dashboard controller
|
|
178
|
+
(`instance_exec`), so it can call your host app's helpers such as `current_user`. Return
|
|
179
|
+
a truthy value to allow the request. A rule you set explicitly is used as-is in every
|
|
180
|
+
environment, including `-> { true }` if you really want the dashboard open.
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
# config/initializers/job_harbor.rb
|
|
184
|
+
JobHarbor.configure do |config|
|
|
185
|
+
# Require an admin
|
|
186
|
+
config.authorize_with = -> { current_user&.admin? }
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### HTTP basic auth
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
config.authorize_with = -> {
|
|
194
|
+
authenticate_or_request_with_http_basic("Job Harbor") do |user, password|
|
|
195
|
+
ActiveSupport::SecurityUtils.secure_compare(user, Rails.application.credentials.job_harbor_user!) &
|
|
196
|
+
ActiveSupport::SecurityUtils.secure_compare(password, Rails.application.credentials.job_harbor_password!)
|
|
197
|
+
end
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The bang readers raise `KeyError` when a credential is missing, so an unset credential
|
|
202
|
+
can never be matched by empty basic-auth credentials.
|
|
203
|
+
|
|
204
|
+
### Devise
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
config.authorize_with = -> {
|
|
208
|
+
authenticate_user!
|
|
209
|
+
current_user.admin?
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Rails 8 authentication generator
|
|
214
|
+
|
|
215
|
+
The dashboard inherits your `ApplicationController`, so the generated `Authentication`
|
|
216
|
+
concern already requires a session before this rule runs:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
config.authorize_with = -> { Current.user&.admin? }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The dashboard is an isolated engine, so the generated concern's bare
|
|
223
|
+
`redirect_to new_session_path` resolves against the engine's routes and raises
|
|
224
|
+
`ActionController::UrlGenerationError` for visitors without a session. Point it at the
|
|
225
|
+
host's routes instead, in your `Authentication` concern:
|
|
136
226
|
|
|
137
227
|
```ruby
|
|
138
|
-
|
|
139
|
-
|
|
228
|
+
def request_authentication
|
|
229
|
+
session[:return_to_after_authenticating] = request.url
|
|
230
|
+
redirect_to main_app.new_session_path
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Or leave the concern alone and override it for the dashboard only:
|
|
140
235
|
|
|
141
|
-
|
|
142
|
-
config.
|
|
236
|
+
```ruby
|
|
237
|
+
# config/initializers/job_harbor.rb
|
|
238
|
+
Rails.application.config.to_prepare do
|
|
239
|
+
JobHarbor::ApplicationController.class_eval do
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
def request_authentication
|
|
243
|
+
session[:return_to_after_authenticating] = request.url
|
|
244
|
+
redirect_to main_app.new_session_path
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
```
|
|
143
249
|
|
|
144
|
-
|
|
145
|
-
|
|
250
|
+
`main_app.new_session_path` from inside the engine is covered by the gem's tests; the
|
|
251
|
+
generated concern and the `to_prepare` override themselves are not exercised against a
|
|
252
|
+
real Rails 8 generated app.
|
|
146
253
|
|
|
147
|
-
|
|
254
|
+
### Pundit or similar
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
148
257
|
config.authorize_with = -> { authorize(:job_harbor, :manage?) }
|
|
149
258
|
```
|
|
150
259
|
|
|
260
|
+
### When access is denied
|
|
261
|
+
|
|
262
|
+
If the rule redirects or halts on its own (Devise's `authenticate_user!` throws to
|
|
263
|
+
Warden, whose failure app redirects), that response is used as-is. Pundit's `authorize` raises `Pundit::NotAuthorizedError` instead
|
|
264
|
+
of returning false, so handle it with a `rescue_from` in your `ApplicationController`.
|
|
265
|
+
Without one it surfaces as a 500.
|
|
266
|
+
|
|
267
|
+
When the rule returns false, browser requests are redirected to your app's root path with an "Admin access required."
|
|
268
|
+
alert. If your app has no root route, or the request is not HTML, the response is
|
|
269
|
+
`403 Forbidden`.
|
|
270
|
+
|
|
151
271
|
## Requirements
|
|
152
272
|
|
|
153
|
-
- Ruby >= 3.
|
|
154
|
-
- Rails >=
|
|
155
|
-
- Solid Queue >= 0
|
|
156
|
-
- ViewComponent >=
|
|
273
|
+
- Ruby >= 3.3
|
|
274
|
+
- Rails >= 8.1
|
|
275
|
+
- Solid Queue >= 1.0, < 2
|
|
276
|
+
- ViewComponent >= 4.1
|
|
157
277
|
|
|
158
278
|
## License
|
|
159
279
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! tailwindcss v4.1.3 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-sky-500:oklch(68.5% .169 237.323);--spacing:.25rem;--breakpoint-2xl:96rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root{--background:0 0% 98%;--foreground:240 10% 3.9%;--card:0 0% 100%;--card-foreground:240 10% 3.9%;--primary:240 5.9% 10%;--primary-foreground:0 0% 98%;--secondary:240 4.8% 95.9%;--secondary-foreground:240 5.9% 10%;--muted:240 4.8% 95.9%;--muted-foreground:240 3.8% 46.1%;--accent:240 4.8% 93.8%;--accent-foreground:240 5.9% 10%;--destructive:0 84.2% 60.2%;--destructive-foreground:0 0% 98%;--border:240 5.9% 90%;--input:240 5.9% 90%;--ring:240 5.9% 10%;--radius:.65rem;--chart-1:12 76% 61%;--chart-2:173 58% 39%;--chart-3:197 37% 24%;--chart-4:43 74% 66%;--chart-5:27 87% 67%}.dark{--background:240 10% 5.4%;--foreground:0 0% 92%;--card:240 10% 3.9%;--card-foreground:0 0% 92%;--primary:0 0% 95%;--primary-foreground:240 5.9% 10%;--secondary:240 3.7% 15.9%;--secondary-foreground:0 0% 92%;--muted:240 3.7% 15.9%;--muted-foreground:240 5% 64.9%;--accent:240 3.7% 15.9%;--accent-foreground:0 0% 92%;--destructive:0 62.8% 30.6%;--destructive-foreground:0 0% 92%;--border:240 3.7% 15.9%;--input:240 3.7% 15.9%;--ring:240 4.9% 83.9%;--chart-1:220 70% 50%;--chart-2:160 60% 45%;--chart-3:30 80% 55%;--chart-4:280 65% 60%;--chart-5:340 75% 55%}*,:before,:after{box-sizing:border-box}body{background-color:hsl(var(--background));color:hsl(var(--foreground));-webkit-font-smoothing:antialiased;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji}}@layer components{.navbar{border-bottom:1px solid hsl(var(--border));background-color:hsl(var(--card));align-items:center;gap:1rem;height:3.5rem;padding:0 1.5rem;display:flex}.navbar-section{align-items:center;gap:.375rem;display:flex}.navbar-item{color:hsl(var(--muted-foreground));border-radius:calc(var(--radius) - 2px);white-space:nowrap;align-items:center;gap:.375rem;padding:.375rem .75rem;font-size:.875rem;font-weight:500;text-decoration:none;transition:color .15s,background-color .15s;display:inline-flex}.navbar-item:hover{color:hsl(var(--foreground));background-color:hsl(var(--accent))}.navbar-item-current{color:hsl(var(--primary-foreground));background-color:hsl(var(--primary))}.navbar-item-current:hover{color:hsl(var(--primary-foreground));background-color:hsl(var(--primary));opacity:.9}.navbar-badge{border-radius:calc(var(--radius) - 4px);background-color:hsl(var(--muted));color:hsl(var(--muted-foreground));padding:.125rem .375rem;font-size:.75rem;font-weight:600}.navbar-item-current .navbar-badge{background-color:hsl(var(--primary-foreground)/.2);color:hsl(var(--primary-foreground))}.card{border-radius:var(--radius);border:1px solid hsl(var(--border));background-color:hsl(var(--card));color:hsl(var(--card-foreground));box-shadow:0 1px 2px #0000000d}.card-header{flex-direction:column;gap:.375rem;padding:1.5rem;display:flex}.card-title{letter-spacing:-.025em;margin:0;font-size:1rem;font-weight:600;line-height:1}.card-description{color:hsl(var(--muted-foreground));font-size:.875rem}.card-content{padding:1.5rem;padding-top:0}.card-footer{align-items:center;padding:0 1.5rem 1.5rem;display:flex}.badge{border-radius:calc(var(--radius) - 2px);white-space:nowrap;align-items:center;gap:.375rem;padding:.125rem .625rem;font-size:.75rem;font-weight:500;transition:color .15s,background-color .15s;display:inline-flex}.badge-primary{background-color:hsl(var(--primary));color:hsl(var(--primary-foreground))}.badge-secondary{background-color:hsl(var(--secondary));color:hsl(var(--secondary-foreground))}.badge-destructive{background-color:hsl(var(--destructive));color:hsl(var(--destructive-foreground))}.badge-outline{border:1px solid hsl(var(--border));color:hsl(var(--foreground));background-color:#0000}.badge-red{color:#ef4444;background-color:#ef44441a}.dark .badge-red{color:#fca5a5;background-color:#ef444426}.badge-amber{color:#d97706;background-color:#fbbf241a}.dark .badge-amber{color:#fcd34d;background-color:#fbbf2426}.badge-yellow{color:#a16207;background-color:#facc151a}.dark .badge-yellow{color:#fde047;background-color:#facc1526}.badge-green{color:#16a34a;background-color:#22c55e1a}.dark .badge-green{color:#86efac;background-color:#22c55e26}.badge-sky{color:#0284c7;background-color:#0ea5e91a}.dark .badge-sky{color:#7dd3fc;background-color:#0ea5e926}.badge-zinc{color:#52525b;background-color:#52525b1a}.dark .badge-zinc{color:#a1a1aa;background-color:#52525b33}.circle{border-radius:9999px;flex-shrink:0;width:.5rem;height:.5rem;display:inline-block}.circle-red{background-color:#ef4444}.circle-amber{background-color:#fbbf24}.circle-green{background-color:#22c55e}.circle-yellow{background-color:#facc15}.circle-sky{background-color:#0ea5e9}.circle-zinc{background-color:#52525b}.btn{white-space:nowrap;border-radius:calc(var(--radius) - 2px);cursor:pointer;border:none;justify-content:center;align-items:center;gap:.5rem;font-size:.875rem;font-weight:500;line-height:1;text-decoration:none;transition:color .15s,background-color .15s,opacity .15s;display:inline-flex}.btn:disabled{opacity:.5;cursor:not-allowed}.btn-default{background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));height:2.5rem;padding:.5rem 1rem}.btn-default:hover{opacity:.9}.btn-secondary{background-color:hsl(var(--secondary));color:hsl(var(--secondary-foreground));border:1px solid hsl(var(--border));height:2.5rem;padding:.5rem 1rem}.btn-secondary:hover{background-color:hsl(var(--accent))}.btn-destructive{background-color:hsl(var(--destructive));color:hsl(var(--destructive-foreground));height:2.5rem;padding:.5rem 1rem}.btn-destructive:hover{opacity:.9}.btn-outline{border:1px solid hsl(var(--border));color:hsl(var(--foreground));background-color:#0000;height:2.5rem;padding:.5rem 1rem}.btn-outline:hover{background-color:hsl(var(--accent))}.btn-ghost{color:hsl(var(--foreground));background-color:#0000;height:2.5rem;padding:.5rem 1rem}.btn-ghost:hover{background-color:hsl(var(--accent))}.btn-icon{width:2.5rem;height:2.5rem;color:hsl(var(--foreground));border:1px solid hsl(var(--border));background-color:#0000;padding:0}.btn-icon:hover{background-color:hsl(var(--accent))}.btn-sm{height:1.75rem;padding:.25rem .5rem;font-size:.75rem}.btn-xs{height:1.5rem;padding:.125rem .5rem;font-size:.75rem}.select{border-radius:calc(var(--radius) - 2px);border:1px solid hsl(var(--input));background-color:hsl(var(--card));width:12rem;height:2.25rem;color:hsl(var(--foreground));cursor:pointer;align-items:center;padding:.375rem .75rem;font-size:.875rem;display:inline-flex}.select:focus{outline:2px solid hsl(var(--ring));outline-offset:2px}.select option{background-color:hsl(var(--card));color:hsl(var(--foreground))}.table-wrapper{overflow-x:auto}table.sqd-table{border-collapse:collapse;caption-side:bottom;width:100%;font-size:.875rem}table.sqd-table thead tr{border-bottom:1px solid hsl(var(--border))}table.sqd-table th{text-align:left;vertical-align:middle;text-transform:uppercase;letter-spacing:.05em;height:2.5rem;color:hsl(var(--muted-foreground));white-space:nowrap;padding:0 1rem;font-size:.75rem;font-weight:500}table.sqd-table tbody tr{border-bottom:1px solid hsl(var(--border));transition:background-color .15s}table.sqd-table tbody tr:hover{background-color:hsl(var(--muted)/.5)}table.sqd-table td{vertical-align:middle;padding:.75rem 1rem}.info-line{align-items:center;gap:1rem;padding:.5rem 0;display:flex}.info-line-label{color:hsl(var(--muted-foreground));white-space:nowrap;flex-shrink:0;font-size:.875rem}.info-line-separator{background-color:hsl(var(--border));flex:1;height:1px}.info-line-value{color:hsl(var(--foreground));text-align:right;white-space:nowrap;font-size:.875rem;font-weight:500}.alert{border-radius:var(--radius);border:1px solid;align-items:center;gap:.75rem;padding:1rem;font-size:.875rem;display:flex;position:relative}.alert-success{color:#16a34a;background-color:#22c55e0d;border-color:#22c55e4d}.dark .alert-success{color:#86efac;background-color:#22c55e1a}.alert-error{color:#dc2626;background-color:#ef44440d;border-color:#ef44444d}.dark .alert-error{color:#fca5a5;background-color:#ef44441a}.alert-warning{color:#d97706;background-color:#fbbf240d;border-color:#fbbf244d}.dark .alert-warning{color:#fcd34d;background-color:#fbbf241a}.alert-info{color:#0284c7;background-color:#0ea5e90d;border-color:#0ea5e94d}.dark .alert-info{color:#7dd3fc;background-color:#0ea5e91a}.pagination-nav{justify-content:center;align-items:center;gap:.25rem;display:flex}.pagination-link{min-width:2rem;height:2rem;color:hsl(var(--muted-foreground));border-radius:calc(var(--radius) - 2px);justify-content:center;align-items:center;padding:0 .5rem;font-size:.875rem;font-weight:500;text-decoration:none;transition:background-color .15s;display:inline-flex}.pagination-link:hover{background-color:hsl(var(--accent));color:hsl(var(--foreground))}.pagination-active{background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));border:1px solid hsl(var(--primary))}.pagination-disabled{opacity:.5;cursor:not-allowed}.link{color:hsl(var(--foreground));text-underline-offset:4px;text-decoration:underline}.link:hover{opacity:.75}.sqd-chart{width:100%;height:200px;position:relative}.sqd-chart canvas{width:100%!important;height:100%!important}.code-block{background-color:hsl(var(--muted));border-radius:calc(var(--radius) - 2px);white-space:pre-wrap;word-break:break-word;padding:.75rem;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.8125rem;overflow-x:auto}.stat-card{border-radius:var(--radius);border:1px solid hsl(var(--border));background-color:hsl(var(--card));flex-direction:column;gap:.25rem;padding:1.5rem;text-decoration:none;transition:border-color .15s;display:flex}a.stat-card:hover{border-color:hsl(var(--foreground)/.2)}.stat-card-label{color:hsl(var(--muted-foreground));align-items:center;gap:.5rem;font-size:.875rem;font-weight:500;display:flex}.stat-card-value{color:hsl(var(--foreground));font-size:2.25rem;font-weight:700;line-height:1}.sqd-rate-low{color:#16a34a;background-color:#22c55e1a}.dark .sqd-rate-low{color:#86efac;background-color:#22c55e26}.sqd-rate-medium{color:#d97706;background-color:#fbbf241a}.dark .sqd-rate-medium{color:#fcd34d;background-color:#fbbf2426}.sqd-rate-high{color:#dc2626;background-color:#ef44441a}.dark .sqd-rate-high{color:#fca5a5;background-color:#ef444426}.sqd-retry-badge{border-radius:calc(var(--radius) - 4px);color:#d97706;background-color:#fbbf2426;padding:.125rem .375rem;font-size:.6875rem;font-weight:600}.dark .sqd-retry-badge{color:#fcd34d}html:not(.dark) .sqd-theme-icon-sun{display:none}html:not(.dark) .sqd-theme-icon-moon,html.dark .sqd-theme-icon-sun{display:block}html.dark .sqd-theme-icon-moon{display:none}}@layer utilities{.relative{position:relative}.static{position:static}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-4{margin-left:calc(var(--spacing)*4)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-6{height:calc(var(--spacing)*6)}.h-12{height:calc(var(--spacing)*12)}.min-h-screen{min-height:100vh}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-4{width:calc(var(--spacing)*4)}.w-6{width:calc(var(--spacing)*6)}.w-12{width:calc(var(--spacing)*12)}.w-full{width:100%}.max-w-screen-2xl{max-width:var(--breakpoint-2xl)}.flex-shrink-0{flex-shrink:0}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}.border{border-style:var(--tw-border-style);border-width:1px}.px-6{padding-inline:calc(var(--spacing)*6)}.py-6{padding-block:calc(var(--spacing)*6)}.py-12{padding-block:calc(var(--spacing)*12)}.pl-1{padding-left:calc(var(--spacing)*1)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-sky-500{color:var(--color-sky-500)}.uppercase{text-transform:uppercase}.no-underline{text-decoration-line:none}.opacity-75{opacity:.75}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}@media (min-width:40rem){.sm\:inline{display:inline}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-500:oklch(63.7% .237 25.331);--color-sky-500:oklch(68.5% .169 237.323);--spacing:.25rem;--breakpoint-2xl:96rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root{--background:0 0% 98%;--foreground:240 10% 3.9%;--card:0 0% 100%;--card-foreground:240 10% 3.9%;--primary:240 5.9% 10%;--primary-foreground:0 0% 98%;--secondary:240 4.8% 95.9%;--secondary-foreground:240 5.9% 10%;--muted:240 4.8% 95.9%;--muted-foreground:240 3.8% 46.1%;--accent:240 4.8% 93.8%;--accent-foreground:240 5.9% 10%;--destructive:0 84.2% 60.2%;--destructive-foreground:0 0% 98%;--border:240 5.9% 90%;--input:240 5.9% 90%;--ring:240 5.9% 10%;--radius:.65rem;--chart-1:12 76% 61%;--chart-2:173 58% 39%;--chart-3:197 37% 24%;--chart-4:43 74% 66%;--chart-5:27 87% 67%}.dark{--background:240 10% 5.4%;--foreground:0 0% 92%;--card:240 10% 3.9%;--card-foreground:0 0% 92%;--primary:0 0% 95%;--primary-foreground:240 5.9% 10%;--secondary:240 3.7% 15.9%;--secondary-foreground:0 0% 92%;--muted:240 3.7% 15.9%;--muted-foreground:240 5% 64.9%;--accent:240 3.7% 15.9%;--accent-foreground:0 0% 92%;--destructive:0 62.8% 30.6%;--destructive-foreground:0 0% 92%;--border:240 3.7% 15.9%;--input:240 3.7% 15.9%;--ring:240 4.9% 83.9%;--chart-1:220 70% 50%;--chart-2:160 60% 45%;--chart-3:30 80% 55%;--chart-4:280 65% 60%;--chart-5:340 75% 55%}*,:before,:after{box-sizing:border-box}body{background-color:hsl(var(--background));color:hsl(var(--foreground));-webkit-font-smoothing:antialiased;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji}}@layer components{.navbar{border-bottom:1px solid hsl(var(--border));background-color:hsl(var(--card));align-items:center;gap:1rem;height:3.5rem;padding:0 1.5rem;display:flex}.navbar-section{align-items:center;gap:.375rem;display:flex}@media not all and (min-width:40rem){.navbar{flex-wrap:wrap;gap:.5rem;height:auto;min-height:3.5rem;padding:.5rem 1rem}.navbar-section{flex-wrap:wrap;flex-basis:100%;order:3}}.navbar-item{color:hsl(var(--muted-foreground));border-radius:calc(var(--radius) - 2px);white-space:nowrap;align-items:center;gap:.375rem;padding:.375rem .75rem;font-size:.875rem;font-weight:500;text-decoration:none;transition:color .15s,background-color .15s;display:inline-flex}.navbar-item:hover{color:hsl(var(--foreground));background-color:hsl(var(--accent))}.navbar-item-current{color:hsl(var(--primary-foreground));background-color:hsl(var(--primary))}.navbar-item-current:hover{color:hsl(var(--primary-foreground));background-color:hsl(var(--primary));opacity:.9}.navbar-badge{border-radius:calc(var(--radius) - 4px);background-color:hsl(var(--muted));color:hsl(var(--muted-foreground));padding:.125rem .375rem;font-size:.75rem;font-weight:600}.navbar-item-current .navbar-badge{background-color:hsl(var(--primary-foreground)/.2);color:hsl(var(--primary-foreground))}.card{border-radius:var(--radius);border:1px solid hsl(var(--border));background-color:hsl(var(--card));color:hsl(var(--card-foreground));box-shadow:0 1px 2px #0000000d}.card-header{flex-direction:column;gap:.375rem;padding:1.5rem;display:flex}.card-title{letter-spacing:-.025em;margin:0;font-size:1rem;font-weight:600;line-height:1}.card-description{color:hsl(var(--muted-foreground));font-size:.875rem}.card-content{padding:1.5rem;padding-top:0}.card-footer{align-items:center;padding:0 1.5rem 1.5rem;display:flex}.badge{border-radius:calc(var(--radius) - 2px);white-space:nowrap;align-items:center;gap:.375rem;padding:.125rem .625rem;font-size:.75rem;font-weight:500;transition:color .15s,background-color .15s;display:inline-flex}.badge-primary{background-color:hsl(var(--primary));color:hsl(var(--primary-foreground))}.badge-secondary{background-color:hsl(var(--secondary));color:hsl(var(--secondary-foreground))}.badge-destructive{background-color:hsl(var(--destructive));color:hsl(var(--destructive-foreground))}.badge-outline{border:1px solid hsl(var(--border));color:hsl(var(--foreground));background-color:#0000}.badge-red{color:#ef4444;background-color:#ef44441a}.dark .badge-red{color:#fca5a5;background-color:#ef444426}.badge-amber{color:#d97706;background-color:#fbbf241a}.dark .badge-amber{color:#fcd34d;background-color:#fbbf2426}.badge-yellow{color:#a16207;background-color:#facc151a}.dark .badge-yellow{color:#fde047;background-color:#facc1526}.badge-green{color:#16a34a;background-color:#22c55e1a}.dark .badge-green{color:#86efac;background-color:#22c55e26}.badge-sky{color:#0284c7;background-color:#0ea5e91a}.dark .badge-sky{color:#7dd3fc;background-color:#0ea5e926}.badge-zinc{color:#52525b;background-color:#52525b1a}.dark .badge-zinc{color:#a1a1aa;background-color:#52525b33}.circle{border-radius:9999px;flex-shrink:0;width:.5rem;height:.5rem;display:inline-block}.circle-red{background-color:#ef4444}.circle-amber{background-color:#fbbf24}.circle-green{background-color:#22c55e}.circle-yellow{background-color:#facc15}.circle-sky{background-color:#0ea5e9}.circle-zinc{background-color:#52525b}.btn{white-space:nowrap;border-radius:calc(var(--radius) - 2px);cursor:pointer;border:none;justify-content:center;align-items:center;gap:.5rem;font-size:.875rem;font-weight:500;line-height:1;text-decoration:none;transition:color .15s,background-color .15s,opacity .15s;display:inline-flex}.btn:disabled{opacity:.5;cursor:not-allowed}.btn-default{background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));height:2.5rem;padding:.5rem 1rem}.btn-default:hover{opacity:.9}.btn-secondary{background-color:hsl(var(--secondary));color:hsl(var(--secondary-foreground));border:1px solid hsl(var(--border));height:2.5rem;padding:.5rem 1rem}.btn-secondary:hover{background-color:hsl(var(--accent))}.btn-destructive{background-color:hsl(var(--destructive));color:hsl(var(--destructive-foreground));height:2.5rem;padding:.5rem 1rem}.btn-destructive:hover{opacity:.9}.btn-outline{border:1px solid hsl(var(--border));color:hsl(var(--foreground));background-color:#0000;height:2.5rem;padding:.5rem 1rem}.btn-outline:hover{background-color:hsl(var(--accent))}.btn-ghost{color:hsl(var(--foreground));background-color:#0000;height:2.5rem;padding:.5rem 1rem}.btn-ghost:hover{background-color:hsl(var(--accent))}.btn-icon{width:2.5rem;height:2.5rem;color:hsl(var(--foreground));border:1px solid hsl(var(--border));background-color:#0000;padding:0}.btn-icon:hover{background-color:hsl(var(--accent))}.btn-sm{height:1.75rem;padding:.25rem .5rem;font-size:.75rem}.btn-xs{height:1.5rem;padding:.125rem .5rem;font-size:.75rem}.select{border-radius:calc(var(--radius) - 2px);border:1px solid hsl(var(--input));background-color:hsl(var(--card));width:12rem;height:2.25rem;color:hsl(var(--foreground));cursor:pointer;align-items:center;padding:.375rem .75rem;font-size:.875rem;display:inline-flex}.select:focus{outline:2px solid hsl(var(--ring));outline-offset:2px}.select option{background-color:hsl(var(--card));color:hsl(var(--foreground))}.table-wrapper{overflow-x:auto}table.sqd-table{border-collapse:collapse;caption-side:bottom;width:100%;font-size:.875rem}table.sqd-table thead tr{border-bottom:1px solid hsl(var(--border))}table.sqd-table th{text-align:left;vertical-align:middle;text-transform:uppercase;letter-spacing:.05em;height:2.5rem;color:hsl(var(--muted-foreground));white-space:nowrap;padding:0 1rem;font-size:.75rem;font-weight:500}table.sqd-table tbody tr{border-bottom:1px solid hsl(var(--border));transition:background-color .15s}table.sqd-table tbody tr:hover{background-color:hsl(var(--muted)/.5)}table.sqd-table td{vertical-align:middle;padding:.75rem 1rem}.info-line{align-items:center;gap:1rem;padding:.5rem 0;display:flex}.info-line-label{color:hsl(var(--muted-foreground));white-space:nowrap;flex-shrink:0;font-size:.875rem}.info-line-separator{background-color:hsl(var(--border));flex:1;height:1px}.info-line-value{color:hsl(var(--foreground));text-align:right;white-space:nowrap;font-size:.875rem;font-weight:500}@media not all and (min-width:40rem){.info-line-value{white-space:normal;overflow-wrap:anywhere;min-width:0}}.alert{border-radius:var(--radius);border:1px solid;align-items:center;gap:.75rem;padding:1rem;font-size:.875rem;display:flex;position:relative}.alert-success{color:#16a34a;background-color:#22c55e0d;border-color:#22c55e4d}.dark .alert-success{color:#86efac;background-color:#22c55e1a}.alert-error{color:#dc2626;background-color:#ef44440d;border-color:#ef44444d}.dark .alert-error{color:#fca5a5;background-color:#ef44441a}.alert-warning{color:#d97706;background-color:#fbbf240d;border-color:#fbbf244d}.dark .alert-warning{color:#fcd34d;background-color:#fbbf241a}.alert-info{color:#0284c7;background-color:#0ea5e90d;border-color:#0ea5e94d}.dark .alert-info{color:#7dd3fc;background-color:#0ea5e91a}.pagination-nav{justify-content:center;align-items:center;gap:.25rem;display:flex}.pagination-link{min-width:2rem;height:2rem;color:hsl(var(--muted-foreground));border-radius:calc(var(--radius) - 2px);justify-content:center;align-items:center;padding:0 .5rem;font-size:.875rem;font-weight:500;text-decoration:none;transition:background-color .15s;display:inline-flex}.pagination-link:hover{background-color:hsl(var(--accent));color:hsl(var(--foreground))}.pagination-active{background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));border:1px solid hsl(var(--primary))}.pagination-disabled{opacity:.5;cursor:not-allowed}.link{color:hsl(var(--foreground));text-underline-offset:4px;text-decoration:underline}.link:hover{opacity:.75}.sqd-chart{width:100%;height:200px;position:relative}.sqd-chart canvas{width:100%!important;height:100%!important}.code-block{background-color:hsl(var(--muted));border-radius:calc(var(--radius) - 2px);white-space:pre-wrap;word-break:break-word;padding:.75rem;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.8125rem;overflow-x:auto}.stat-card{border-radius:var(--radius);border:1px solid hsl(var(--border));background-color:hsl(var(--card));flex-direction:column;gap:.25rem;padding:1.5rem;text-decoration:none;transition:border-color .15s;display:flex}a.stat-card:hover{border-color:hsl(var(--foreground)/.2)}.stat-card-label{color:hsl(var(--muted-foreground));align-items:center;gap:.5rem;font-size:.875rem;font-weight:500;display:flex}.stat-card-value{color:hsl(var(--foreground));font-size:2.25rem;font-weight:700;line-height:1}.sqd-rate-low{color:#16a34a;background-color:#22c55e1a}.dark .sqd-rate-low{color:#86efac;background-color:#22c55e26}.sqd-rate-medium{color:#d97706;background-color:#fbbf241a}.dark .sqd-rate-medium{color:#fcd34d;background-color:#fbbf2426}.sqd-rate-high{color:#dc2626;background-color:#ef44441a}.dark .sqd-rate-high{color:#fca5a5;background-color:#ef444426}.sqd-retry-badge{border-radius:calc(var(--radius) - 4px);color:#d97706;background-color:#fbbf2426;padding:.125rem .375rem;font-size:.6875rem;font-weight:600}.dark .sqd-retry-badge{color:#fcd34d}html:not(.dark) .sqd-theme-icon-sun{display:none}html:not(.dark) .sqd-theme-icon-moon,html.dark .sqd-theme-icon-sun{display:block}html.dark .sqd-theme-icon-moon{display:none}}@layer utilities{.collapse{visibility:collapse}.relative{position:relative}.static{position:static}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-6{height:calc(var(--spacing)*6)}.h-12{height:calc(var(--spacing)*12)}.max-h-100{max-height:calc(var(--spacing)*100)}.min-h-screen{min-height:100vh}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-4{width:calc(var(--spacing)*4)}.w-6{width:calc(var(--spacing)*6)}.w-12{width:calc(var(--spacing)*12)}.w-20{width:calc(var(--spacing)*20)}.w-full{width:100%}.max-w-screen-2xl{max-width:var(--breakpoint-2xl)}.flex-shrink-0{flex-shrink:0}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-t-4{border-top-style:var(--tw-border-style);border-top-width:4px}.border-t-red-500{border-top-color:var(--color-red-500)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-12{padding-block:calc(var(--spacing)*12)}.pl-1{padding-left:calc(var(--spacing)*1)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-foreground{color:hsl(var(--foreground))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-primary{color:hsl(var(--primary))}.text-red-500{color:var(--color-red-500)}.text-sky-500{color:var(--color-sky-500)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.no-underline{text-decoration-line:none}.opacity-75{opacity:.75}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}@media (min-width:40rem){.sm\:ml-4{margin-left:calc(var(--spacing)*4)}.sm\:inline{display:inline}.sm\:w-64{width:calc(var(--spacing)*64)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
|
@@ -77,6 +77,11 @@
|
|
|
77
77
|
background-color: hsl(var(--card));
|
|
78
78
|
}
|
|
79
79
|
.navbar-section { display: flex; align-items: center; gap: 0.375rem; }
|
|
80
|
+
/* Small screens: the links drop to their own full-width row and wrap if they must */
|
|
81
|
+
@media (width < 40rem) {
|
|
82
|
+
.navbar { flex-wrap: wrap; height: auto; min-height: 3.5rem; padding: 0.5rem 1rem; gap: 0.5rem; }
|
|
83
|
+
.navbar-section { order: 3; flex-basis: 100%; flex-wrap: wrap; }
|
|
84
|
+
}
|
|
80
85
|
.navbar-item {
|
|
81
86
|
display: inline-flex;
|
|
82
87
|
align-items: center;
|
|
@@ -311,6 +316,9 @@
|
|
|
311
316
|
text-align: right;
|
|
312
317
|
white-space: nowrap;
|
|
313
318
|
}
|
|
319
|
+
@media (width < 40rem) {
|
|
320
|
+
.info-line-value { min-width: 0; white-space: normal; overflow-wrap: anywhere; }
|
|
321
|
+
}
|
|
314
322
|
|
|
315
323
|
/* Alerts / Flash */
|
|
316
324
|
.alert {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class BadgeComponent < ApplicationComponent
|
|
5
|
-
VALID_STATUSES = %w[pending scheduled in_progress failed finished blocked active paused].freeze
|
|
5
|
+
VALID_STATUSES = %w[pending scheduled in_progress failed finished blocked active paused stale].freeze
|
|
6
6
|
|
|
7
7
|
STATUS_COLORS = {
|
|
8
8
|
"pending" => "badge-sky",
|
|
@@ -12,7 +12,8 @@ module JobHarbor
|
|
|
12
12
|
"finished" => "badge-green",
|
|
13
13
|
"blocked" => "badge-zinc",
|
|
14
14
|
"active" => "badge-green",
|
|
15
|
-
"paused" => "badge-amber"
|
|
15
|
+
"paused" => "badge-amber",
|
|
16
|
+
"stale" => "badge-zinc"
|
|
16
17
|
}.freeze
|
|
17
18
|
|
|
18
19
|
STATUS_CIRCLES = {
|
|
@@ -23,7 +24,8 @@ module JobHarbor
|
|
|
23
24
|
"finished" => "circle-green",
|
|
24
25
|
"blocked" => "circle-zinc",
|
|
25
26
|
"active" => "circle-green",
|
|
26
|
-
"paused" => "circle-amber"
|
|
27
|
+
"paused" => "circle-amber",
|
|
28
|
+
"stale" => "circle-zinc"
|
|
27
29
|
}.freeze
|
|
28
30
|
|
|
29
31
|
def initialize(status:)
|
|
@@ -20,7 +20,7 @@ module JobHarbor
|
|
|
20
20
|
|
|
21
21
|
def header
|
|
22
22
|
content_tag(:div, class: "card-header") do
|
|
23
|
-
content_tag(:div, class: "flex items-center justify-between") do
|
|
23
|
+
content_tag(:div, class: "flex flex-wrap items-center justify-between gap-2") do
|
|
24
24
|
safe_join([
|
|
25
25
|
content_tag(:h3, "Job Activity", class: "card-title"),
|
|
26
26
|
range_selector
|
|
@@ -39,9 +39,7 @@ module JobHarbor
|
|
|
39
39
|
active = range[:value] == @current_range
|
|
40
40
|
css_class = active ? "btn btn-default btn-xs" : "btn btn-secondary btn-xs"
|
|
41
41
|
|
|
42
|
-
link_to range[:label],
|
|
43
|
-
"#{root_path}?chart_range=#{range[:value]}",
|
|
44
|
-
class: css_class
|
|
42
|
+
link_to range[:label], root_path(chart_range: range[:value]), class: css_class
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
def body
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class FailureRatesComponent < ApplicationComponent
|
|
5
|
+
LOW_THRESHOLD = 5
|
|
6
|
+
MEDIUM_THRESHOLD = 20
|
|
7
|
+
|
|
5
8
|
def initialize(stats:)
|
|
6
9
|
@stats = stats
|
|
7
10
|
end
|
|
@@ -38,11 +41,13 @@ module JobHarbor
|
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
def stats_table
|
|
41
|
-
content_tag(:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
content_tag(:div, class: "table-wrapper") do
|
|
45
|
+
content_tag(:table, class: "sqd-table sqd-failure-table") do
|
|
46
|
+
safe_join([
|
|
47
|
+
table_header,
|
|
48
|
+
table_body
|
|
49
|
+
])
|
|
50
|
+
end
|
|
46
51
|
end
|
|
47
52
|
end
|
|
48
53
|
|
|
@@ -77,8 +82,15 @@ module JobHarbor
|
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
def rate_badge(rate)
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
content_tag(:span, "#{rate}%", class: "badge #{rate_badge_class(rate)}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def rate_badge_class(rate)
|
|
89
|
+
case rate
|
|
90
|
+
when 0...LOW_THRESHOLD then "sqd-rate-low"
|
|
91
|
+
when LOW_THRESHOLD...MEDIUM_THRESHOLD then "sqd-rate-medium"
|
|
92
|
+
else "sqd-rate-high"
|
|
93
|
+
end
|
|
82
94
|
end
|
|
83
95
|
end
|
|
84
96
|
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class JobFiltersComponent < ApplicationComponent
|
|
5
5
|
def initialize(class_names:, queue_names:, current_class:, current_queue:, current_path:, params: {})
|
|
6
|
-
@class_names = class_names
|
|
7
|
-
@queue_names = queue_names
|
|
6
|
+
@class_names = with_selected(class_names, current_class)
|
|
7
|
+
@queue_names = with_selected(queue_names, current_queue)
|
|
8
8
|
@current_class = current_class
|
|
9
9
|
@current_queue = current_queue
|
|
10
10
|
@current_path = current_path
|
|
@@ -22,6 +22,11 @@ module JobHarbor
|
|
|
22
22
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
|
+
# The active filter stays selectable even after its last job is gone.
|
|
26
|
+
def with_selected(names, current)
|
|
27
|
+
current.present? ? (names | [ current ]).sort : names
|
|
28
|
+
end
|
|
29
|
+
|
|
25
30
|
def class_filter
|
|
26
31
|
content_tag(:div, class: "flex items-center gap-1.5") do
|
|
27
32
|
safe_join([
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class JobRowComponent < ApplicationComponent
|
|
5
|
-
def initialize(job:)
|
|
5
|
+
def initialize(job:, return_to: nil)
|
|
6
6
|
@job = job
|
|
7
|
+
@return_to = return_to
|
|
7
8
|
end
|
|
8
9
|
|
|
9
10
|
def call
|
|
@@ -92,17 +93,21 @@ module JobHarbor
|
|
|
92
93
|
end
|
|
93
94
|
end
|
|
94
95
|
|
|
96
|
+
def return_to
|
|
97
|
+
@return_to || request.fullpath
|
|
98
|
+
end
|
|
99
|
+
|
|
95
100
|
def retry_button
|
|
96
101
|
return unless @job.can_retry?
|
|
97
102
|
|
|
98
|
-
button_to "Retry", retry_job_path(@job), method: :post, class: "btn btn-secondary btn-xs"
|
|
103
|
+
button_to "Retry", retry_job_path(@job), method: :post, params: { return_to: return_to }, class: "btn btn-secondary btn-xs"
|
|
99
104
|
end
|
|
100
105
|
|
|
101
106
|
def discard_button
|
|
102
107
|
return unless @job.can_discard?
|
|
103
108
|
|
|
104
|
-
button_to "Discard", discard_job_path(@job), method: :delete, class: "btn btn-destructive btn-xs",
|
|
105
|
-
data: { confirm: "Are you sure you want to discard this job?" }
|
|
109
|
+
button_to "Discard", discard_job_path(@job), method: :delete, params: { return_to: return_to }, class: "btn btn-destructive btn-xs",
|
|
110
|
+
form: { data: { confirm: "Are you sure you want to discard this job?" } }
|
|
106
111
|
end
|
|
107
112
|
end
|
|
108
113
|
end
|
|
@@ -15,9 +15,7 @@ module JobHarbor
|
|
|
15
15
|
safe_join([
|
|
16
16
|
content_tag(:span, "Show", class: "text-sm text-muted-foreground"),
|
|
17
17
|
content_tag(:select,
|
|
18
|
-
class: "sqd-per-page-select select"
|
|
19
|
-
style: "width: 5rem;",
|
|
20
|
-
data: { action: "change->per-page#change" }
|
|
18
|
+
class: "sqd-per-page-select select w-20"
|
|
21
19
|
) do
|
|
22
20
|
safe_join(PAGE_SIZES.map { |size| option_tag(size) })
|
|
23
21
|
end
|
|
@@ -30,11 +30,12 @@ module JobHarbor
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
32
|
def stats
|
|
33
|
-
content_tag(:div, class: "grid grid-cols-
|
|
33
|
+
content_tag(:div, class: "grid grid-cols-4 gap-4 mb-4") do
|
|
34
34
|
safe_join([
|
|
35
35
|
stat("Pending", @queue.pending_count),
|
|
36
36
|
stat("Scheduled", @queue.scheduled_count),
|
|
37
|
-
stat("In Progress", @queue.in_progress_count)
|
|
37
|
+
stat("In Progress", @queue.in_progress_count),
|
|
38
|
+
stat("Blocked", @queue.blocked_count)
|
|
38
39
|
])
|
|
39
40
|
end
|
|
40
41
|
end
|
|
@@ -41,15 +41,20 @@ module JobHarbor
|
|
|
41
41
|
|
|
42
42
|
def interval_select
|
|
43
43
|
content_tag(:select,
|
|
44
|
-
class: "sqd-refresh-select select",
|
|
45
|
-
style: "width: 5rem;",
|
|
46
|
-
data: { action: "change->refresh-selector#change" },
|
|
44
|
+
class: "sqd-refresh-select select w-20",
|
|
47
45
|
"aria-label": "Auto-refresh interval"
|
|
48
46
|
) do
|
|
49
|
-
safe_join(
|
|
47
|
+
safe_join(intervals.map { |interval| option_tag(interval) })
|
|
50
48
|
end
|
|
51
49
|
end
|
|
52
50
|
|
|
51
|
+
# A configured poll_interval outside the standard choices gets its own option, so the select never shows a blank.
|
|
52
|
+
def intervals
|
|
53
|
+
return INTERVALS if INTERVALS.any? { |interval| interval[:value] == @default_interval }
|
|
54
|
+
|
|
55
|
+
(INTERVALS + [ { label: "#{@default_interval}s", value: @default_interval } ]).sort_by { |interval| interval[:value] }
|
|
56
|
+
end
|
|
57
|
+
|
|
53
58
|
def option_tag(interval)
|
|
54
59
|
selected = interval[:value] == @default_interval
|
|
55
60
|
content_tag(:option, interval[:label], value: interval[:value], selected: selected)
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class WorkerCardComponent < ApplicationComponent
|
|
5
|
-
STALE_THRESHOLD = 5.minutes
|
|
6
|
-
|
|
7
5
|
def initialize(worker:)
|
|
8
6
|
@worker = worker
|
|
9
7
|
end
|
|
@@ -14,8 +12,13 @@ module JobHarbor
|
|
|
14
12
|
content_tag(:div, class: "card-header") do
|
|
15
13
|
content_tag(:div, class: "flex items-center justify-between") do
|
|
16
14
|
safe_join([
|
|
17
|
-
content_tag(:
|
|
18
|
-
|
|
15
|
+
content_tag(:div, class: "flex items-center gap-2") do
|
|
16
|
+
safe_join([
|
|
17
|
+
content_tag(:span, worker_name, class: "card-title"),
|
|
18
|
+
content_tag(:span, kind_label, class: "badge badge-secondary", data: { kind: @worker.kind })
|
|
19
|
+
])
|
|
20
|
+
end,
|
|
21
|
+
content_tag(:span, render(BadgeComponent.new(status: heartbeat_status)), data: { heartbeat: true })
|
|
19
22
|
])
|
|
20
23
|
end
|
|
21
24
|
end,
|
|
@@ -35,14 +38,13 @@ module JobHarbor
|
|
|
35
38
|
@worker.name.presence || "Worker ##{@worker.id}"
|
|
36
39
|
end
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
# Solid Queue 1.7 reports supervisors as "Supervisor(fork)" or "Supervisor(async)".
|
|
42
|
+
def kind_label
|
|
43
|
+
@worker.kind.sub(/\(.*\)\z/, "")
|
|
40
44
|
end
|
|
41
45
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@worker.last_heartbeat_at < STALE_THRESHOLD.ago
|
|
46
|
+
def heartbeat_status
|
|
47
|
+
Processes.alive?(@worker) ? :active : :stale
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def details
|
|
@@ -71,13 +73,18 @@ module JobHarbor
|
|
|
71
73
|
time_ago_in_words(@worker.last_heartbeat_at) + " ago"
|
|
72
74
|
end
|
|
73
75
|
|
|
76
|
+
# Workers record the queues they poll as a comma-joined string in metadata.
|
|
77
|
+
def queues
|
|
78
|
+
@queues ||= @worker.metadata["queues"].to_s.split(",").map(&:strip).reject(&:empty?)
|
|
79
|
+
end
|
|
80
|
+
|
|
74
81
|
def queues_list
|
|
75
|
-
return "".html_safe
|
|
82
|
+
return "".html_safe if queues.empty?
|
|
76
83
|
|
|
77
84
|
content_tag(:div, class: "mt-3 flex items-center gap-1 flex-wrap") do
|
|
78
85
|
safe_join([
|
|
79
86
|
content_tag(:span, "Queues:", class: "text-xs text-muted-foreground"),
|
|
80
|
-
|
|
87
|
+
queues.map { |q| content_tag(:code, q, class: "badge badge-secondary text-xs font-mono") }
|
|
81
88
|
].flatten)
|
|
82
89
|
end
|
|
83
90
|
end
|
|
@@ -13,9 +13,13 @@ module JobHarbor
|
|
|
13
13
|
private
|
|
14
14
|
|
|
15
15
|
def authorize_access
|
|
16
|
-
return if JobHarbor.configuration.authorize(self)
|
|
16
|
+
return if JobHarbor.configuration.authorize(self) || performed?
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
if request.format.html? && main_app.respond_to?(:root_path)
|
|
19
|
+
redirect_to main_app.root_path, alert: "Admin access required."
|
|
20
|
+
else
|
|
21
|
+
head :forbidden
|
|
22
|
+
end
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
def sq_config
|
|
@@ -28,7 +32,7 @@ module JobHarbor
|
|
|
28
32
|
|
|
29
33
|
def nav_counts
|
|
30
34
|
@nav_counts ||= {
|
|
31
|
-
workers:
|
|
35
|
+
workers: Processes.live_workers.count,
|
|
32
36
|
recurring_tasks: recurring_task_count
|
|
33
37
|
}
|
|
34
38
|
end
|