job_harbor 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +16 -1
  4. data/app/assets/stylesheets/job_harbor/application.css +1 -1
  5. data/app/assets/stylesheets/job_harbor/application.tailwind.css +0 -31
  6. data/app/components/job_harbor/badge_component.rb +12 -28
  7. data/app/components/job_harbor/info_line_component.rb +20 -0
  8. data/app/components/job_harbor/job_row_component.rb +4 -8
  9. data/app/components/job_harbor/job_search_form_component.html.erb +4 -0
  10. data/app/components/job_harbor/job_search_form_component.rb +9 -0
  11. data/app/components/job_harbor/jobs_table_component.html.erb +26 -0
  12. data/app/components/job_harbor/jobs_table_component.rb +10 -0
  13. data/app/components/job_harbor/nav_link_component.rb +1 -2
  14. data/app/components/job_harbor/pagination_component.rb +10 -10
  15. data/app/components/job_harbor/queue_card_component.rb +4 -6
  16. data/app/components/job_harbor/queue_pause_form_component.html.erb +5 -0
  17. data/app/components/job_harbor/queue_pause_form_component.rb +6 -0
  18. data/app/components/job_harbor/stat_card_component.rb +10 -13
  19. data/app/components/job_harbor/status_tabs_component.rb +43 -0
  20. data/app/components/job_harbor/worker_card_component.rb +3 -13
  21. data/app/controllers/job_harbor/jobs_controller.rb +18 -53
  22. data/app/controllers/job_harbor/queues/pauses_controller.rb +32 -0
  23. data/app/controllers/job_harbor/queues_controller.rb +1 -6
  24. data/app/models/job_harbor/chart_data.rb +10 -4
  25. data/app/models/job_harbor/dashboard_stats.rb +5 -31
  26. data/app/models/job_harbor/job_presenter.rb +5 -68
  27. data/app/models/job_harbor/job_query.rb +105 -0
  28. data/app/models/job_harbor/job_status.rb +90 -0
  29. data/app/models/job_harbor/queue_stats.rb +9 -38
  30. data/app/views/job_harbor/dashboard/index.html.erb +14 -39
  31. data/app/views/job_harbor/jobs/index.html.erb +5 -56
  32. data/app/views/job_harbor/jobs/search.html.erb +4 -27
  33. data/app/views/job_harbor/jobs/show.html.erb +21 -55
  34. data/app/views/job_harbor/queues/index.html.erb +2 -0
  35. data/app/views/job_harbor/queues/show.html.erb +5 -54
  36. data/app/views/job_harbor/recurring_tasks/show.html.erb +9 -31
  37. data/app/views/job_harbor/workers/index.html.erb +2 -2
  38. data/app/views/layouts/job_harbor/application.html.erb +27 -25
  39. data/config/routes.rb +9 -7
  40. data/lib/job_harbor/configuration.rb +6 -2
  41. data/lib/job_harbor/engine.rb +9 -1
  42. data/lib/job_harbor/time_formats.rb +12 -0
  43. data/lib/job_harbor/version.rb +1 -1
  44. data/lib/job_harbor.rb +5 -0
  45. metadata +15 -3
  46. data/app/helpers/job_harbor/application_helper.rb +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61fd4075abe3acb8265885f2ff9e5d007bede4df800c2c466b98e11cbc1810b2
4
- data.tar.gz: c7b3836df141e4711ccb3f4828eb926da10f6f5d9901a996338e7977cb011641
3
+ metadata.gz: f3332e364e2b6fdeaa34782a5e991dd65d22e19769cc242b2bae74ec56e97c77
4
+ data.tar.gz: 21b0649426482640b7dfad3d0bca05858a062faa4115daedb9413e48ba2687cf
5
5
  SHA512:
6
- metadata.gz: fca96f13699a78acf5e848527c86df9f98e53ca6f1ce7ab7bd4b7d0ba923e737e8badadc86d990627bed83ffe8c81d07b1c324e0654f4f0704a5790bc223bf26
7
- data.tar.gz: f3b64f11f8a06145c40830703b2d48f7ced097e01a173b3c63a9c645bb880701bf4e13174c2c3ecc9a570d7bbec9e4a2bd422888e9751c645138429720bfb412
6
+ metadata.gz: 679c4bc2d82c6230251cb8bebecba1301f855fd5ec45c38b12edf0c06968c9fb8225cd346facf47c3fdcfe8c0ae6370571419d2ea124ca8f6f3f0f88b8921dfb
7
+ data.tar.gz: 4dabc6987def5ebc3aa0da908820e5a53562a089ec26ca3061e560bf8d2987883de3a6a3e3fcd6ab9d3a863183cdb70f8c1daf1490f8f1af376dacce71da1270
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2026 Evan Sparkman
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -107,6 +107,9 @@ JobHarbor.configure do |config|
107
107
  # Theme: :dark or :light (default: :dark)
108
108
  config.theme = :dark
109
109
 
110
+ # Deprecated: config.primary_color has no effect and will be removed in 1.0.
111
+ # Assigning it logs a deprecation warning.
112
+
110
113
  # Jobs per page (default: 25)
111
114
  config.jobs_per_page = 25
112
115
 
@@ -275,6 +278,18 @@ alert. If your app has no root route, or the request is not HTML, the response i
275
278
  - Solid Queue >= 1.0, < 2
276
279
  - ViewComponent >= 4.1
277
280
 
281
+ ## Development
282
+
283
+ To work on the gem itself, clone the repo and run `bundle install`. The dummy Rails app the tests run against lives in `test/dummy`.
284
+
285
+ ```bash
286
+ bin/ci # rubocop, bundler-audit, brakeman, unit tests, system tests, seed replant
287
+ bin/rails test # everything except the system tests
288
+ bin/rails test:system # browser tests only
289
+ ```
290
+
291
+ The system tests drive headless Chrome through selenium-webdriver, so they need Google Chrome installed locally.
292
+
278
293
  ## License
279
294
 
280
- MIT License. See [LICENSE](LICENSE) for details.
295
+ MIT License. See [MIT-LICENSE](MIT-LICENSE) for details.
@@ -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-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}
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-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-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-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}.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}
@@ -142,10 +142,6 @@
142
142
  line-height: 1;
143
143
  letter-spacing: -0.025em;
144
144
  }
145
- .card-description {
146
- font-size: 0.875rem;
147
- color: hsl(var(--muted-foreground));
148
- }
149
145
  .card-content { padding: 1.5rem; padding-top: 0; }
150
146
  .card-footer { display: flex; align-items: center; padding: 1.5rem; padding-top: 0; }
151
147
 
@@ -163,8 +159,6 @@
163
159
  }
164
160
  .badge-primary { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); }
165
161
  .badge-secondary { background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); }
166
- .badge-destructive { background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); }
167
- .badge-outline { border: 1px solid hsl(var(--border)); color: hsl(var(--foreground)); background-color: transparent; }
168
162
 
169
163
  .badge-red { background-color: rgb(239 68 68 / 0.1); color: rgb(239 68 68); }
170
164
  .dark .badge-red { background-color: rgb(239 68 68 / 0.15); color: rgb(252 165 165); }
@@ -235,13 +229,6 @@
235
229
  height: 2.5rem;
236
230
  }
237
231
  .btn-outline:hover { background-color: hsl(var(--accent)); }
238
- .btn-ghost {
239
- background-color: transparent;
240
- color: hsl(var(--foreground));
241
- padding: 0.5rem 1rem;
242
- height: 2.5rem;
243
- }
244
- .btn-ghost:hover { background-color: hsl(var(--accent)); }
245
232
  .btn-icon {
246
233
  padding: 0;
247
234
  width: 2.5rem;
@@ -349,12 +336,6 @@
349
336
  color: rgb(217 119 6);
350
337
  }
351
338
  .dark .alert-warning { color: rgb(252 211 77); background-color: rgb(251 191 36 / 0.1); }
352
- .alert-info {
353
- border-color: rgb(14 165 233 / 0.3);
354
- background-color: rgb(14 165 233 / 0.05);
355
- color: rgb(2 132 199);
356
- }
357
- .dark .alert-info { color: rgb(125 211 252); background-color: rgb(14 165 233 / 0.1); }
358
339
 
359
340
  /* Pagination */
360
341
  .pagination-nav { display: flex; align-items: center; justify-content: center; gap: 0.25rem; }
@@ -437,18 +418,6 @@
437
418
  .dark .sqd-rate-high { background-color: rgb(239 68 68 / 0.15); color: rgb(252 165 165); }
438
419
 
439
420
  /* Legacy compat: sqd- prefixed classes used by existing tests */
440
- .sqd-nav-link { }
441
- .sqd-nav-badge { }
442
- .sqd-theme-toggle { }
443
- .sqd-theme-icon-sun, .sqd-theme-icon-moon { }
444
- .sqd-refresh-selector { }
445
- .sqd-refresh-select { }
446
- .sqd-per-page-selector { }
447
- .sqd-per-page-select { }
448
- .sqd-filters { }
449
- .sqd-filter-select { }
450
- .sqd-failure-rates { }
451
- .sqd-failure-table { }
452
421
  .sqd-retry-badge {
453
422
  padding: 0.125rem 0.375rem;
454
423
  font-size: 0.6875rem;
@@ -2,30 +2,11 @@
2
2
 
3
3
  module JobHarbor
4
4
  class BadgeComponent < ApplicationComponent
5
- VALID_STATUSES = %w[pending scheduled in_progress failed finished blocked active paused stale].freeze
6
-
7
- STATUS_COLORS = {
8
- "pending" => "badge-sky",
9
- "scheduled" => "badge-yellow",
10
- "in_progress" => "badge-amber",
11
- "failed" => "badge-red",
12
- "finished" => "badge-green",
13
- "blocked" => "badge-zinc",
14
- "active" => "badge-green",
15
- "paused" => "badge-amber",
16
- "stale" => "badge-zinc"
17
- }.freeze
18
-
19
- STATUS_CIRCLES = {
20
- "pending" => "circle-sky",
21
- "scheduled" => "circle-yellow",
22
- "in_progress" => "circle-amber",
23
- "failed" => "circle-red",
24
- "finished" => "circle-green",
25
- "blocked" => "circle-zinc",
26
- "active" => "circle-green",
27
- "paused" => "circle-amber",
28
- "stale" => "circle-zinc"
5
+ # Statuses that belong to workers and queues rather than jobs.
6
+ OTHER_COLORS = {
7
+ "active" => "green",
8
+ "paused" => "amber",
9
+ "stale" => "zinc"
29
10
  }.freeze
30
11
 
31
12
  def initialize(status:)
@@ -44,13 +25,16 @@ module JobHarbor
44
25
  private
45
26
 
46
27
  def css_classes
47
- status_key = VALID_STATUSES.include?(@status) ? @status : "pending"
48
- "badge #{STATUS_COLORS[status_key]}"
28
+ "badge badge-#{color}"
49
29
  end
50
30
 
51
31
  def circle_class
52
- status_key = VALID_STATUSES.include?(@status) ? @status : "pending"
53
- STATUS_CIRCLES[status_key]
32
+ "circle-#{color}"
33
+ end
34
+
35
+ # Anything unrecognised renders as the first job status.
36
+ def color
37
+ JobStatus.find(@status)&.color || OTHER_COLORS.fetch(@status) { JobStatus.all.first.color }
54
38
  end
55
39
 
56
40
  def display_text
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobHarbor
4
+ class InfoLineComponent < ApplicationComponent
5
+ def initialize(label:, value: nil)
6
+ @label = label
7
+ @value = value
8
+ end
9
+
10
+ def call
11
+ content_tag(:div, class: "info-line") do
12
+ safe_join([
13
+ content_tag(:span, @label, class: "info-line-label"),
14
+ content_tag(:span, "", class: "info-line-separator"),
15
+ content_tag(:span, content? ? content : @value, class: "info-line-value")
16
+ ])
17
+ end
18
+ end
19
+ end
20
+ end
@@ -38,7 +38,7 @@ module JobHarbor
38
38
  end
39
39
 
40
40
  def retry_badge_tag
41
- return nil unless @job.respond_to?(:retry_badge) && @job.retry_badge.present?
41
+ return nil unless @job.retry_badge.present?
42
42
 
43
43
  content_tag(:span, @job.retry_badge, class: "sqd-retry-badge ml-1")
44
44
  end
@@ -59,7 +59,7 @@ module JobHarbor
59
59
  end
60
60
 
61
61
  def running_duration_tag
62
- return nil unless @job.respond_to?(:running_duration) && @job.running_duration.present?
62
+ return nil unless @job.running_duration.present?
63
63
 
64
64
  content_tag(:span, " (#{@job.running_duration})", class: "text-xs text-sky-500")
65
65
  end
@@ -67,7 +67,7 @@ module JobHarbor
67
67
  def scheduled_cell
68
68
  content_tag(:td) do
69
69
  if @job.scheduled_at
70
- time_tag(@job.scheduled_at, @job.scheduled_at.strftime("%b %d, %H:%M:%S"))
70
+ time_tag(@job.scheduled_at, @job.scheduled_at.to_fs(:job_harbor_short))
71
71
  else
72
72
  relative_time_tag
73
73
  end
@@ -75,11 +75,7 @@ module JobHarbor
75
75
  end
76
76
 
77
77
  def relative_time_tag
78
- if @job.respond_to?(:relative_created_at)
79
- content_tag(:span, @job.relative_created_at, class: "text-xs text-muted-foreground")
80
- else
81
- content_tag(:span, "\u2014", class: "text-muted-foreground")
82
- end
78
+ content_tag(:span, @job.relative_created_at, class: "text-xs text-muted-foreground")
83
79
  end
84
80
 
85
81
  def actions_cell
@@ -0,0 +1,4 @@
1
+ <%= form_with url: search_path, method: :get, class: "flex flex-wrap items-center gap-2" do |f| %>
2
+ <%= f.text_field :q, placeholder: "Search jobs...", class: "select w-full sm:w-64", value: @query %>
3
+ <%= f.submit "Search", class: "btn btn-secondary btn-sm" %>
4
+ <% end %>
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobHarbor
4
+ class JobSearchFormComponent < ApplicationComponent
5
+ def initialize(query: nil)
6
+ @query = query
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ <div class="card">
2
+ <% if @title %>
3
+ <div class="card-header">
4
+ <h2 class="card-title"><%= @title %></h2>
5
+ </div>
6
+ <% end %>
7
+ <div class="table-wrapper">
8
+ <table class="sqd-table">
9
+ <thead>
10
+ <tr>
11
+ <th>ID</th>
12
+ <th>Job Class</th>
13
+ <th>Queue</th>
14
+ <th>Status</th>
15
+ <th>Scheduled At</th>
16
+ <th>Actions</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <% @jobs.each do |job| %>
21
+ <%= render JobHarbor::JobRowComponent.new(job: job) %>
22
+ <% end %>
23
+ </tbody>
24
+ </table>
25
+ </div>
26
+ </div>
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobHarbor
4
+ class JobsTableComponent < ApplicationComponent
5
+ def initialize(jobs:, title: nil)
6
+ @jobs = jobs
7
+ @title = title
8
+ end
9
+ end
10
+ end
@@ -2,10 +2,9 @@
2
2
 
3
3
  module JobHarbor
4
4
  class NavLinkComponent < ApplicationComponent
5
- def initialize(path:, label:, icon:, active: false, badge: nil)
5
+ def initialize(path:, label:, active: false, badge: nil)
6
6
  @path = path
7
7
  @label = label
8
- @icon = icon.to_sym
9
8
  @active = active
10
9
  @badge = badge
11
10
  end
@@ -2,12 +2,12 @@
2
2
 
3
3
  module JobHarbor
4
4
  class PaginationComponent < ApplicationComponent
5
- def initialize(pagy:)
6
- @pagy = pagy
5
+ def initialize(page_info:)
6
+ @page_info = page_info
7
7
  end
8
8
 
9
9
  def render?
10
- @pagy.pages > 1
10
+ @page_info.pages > 1
11
11
  end
12
12
 
13
13
  def call
@@ -23,16 +23,16 @@ module JobHarbor
23
23
  private
24
24
 
25
25
  def prev_link
26
- if @pagy.prev
27
- link_to "\u2190 Prev", url_for(page: @pagy.prev), class: "pagination-link"
26
+ if @page_info.prev
27
+ link_to "\u2190 Prev", url_for(page: @page_info.prev), class: "pagination-link"
28
28
  else
29
29
  content_tag(:span, "\u2190 Prev", class: "pagination-link pagination-disabled")
30
30
  end
31
31
  end
32
32
 
33
33
  def next_link
34
- if @pagy.next
35
- link_to "Next \u2192", url_for(page: @pagy.next), class: "pagination-link"
34
+ if @page_info.next
35
+ link_to "Next \u2192", url_for(page: @page_info.next), class: "pagination-link"
36
36
  else
37
37
  content_tag(:span, "Next \u2192", class: "pagination-link pagination-disabled")
38
38
  end
@@ -42,7 +42,7 @@ module JobHarbor
42
42
  series.map do |item|
43
43
  case item
44
44
  when Integer
45
- if item == @pagy.page
45
+ if item == @page_info.page
46
46
  content_tag(:span, item, class: "pagination-link pagination-active")
47
47
  else
48
48
  link_to item, url_for(page: item), class: "pagination-link"
@@ -54,8 +54,8 @@ module JobHarbor
54
54
  end
55
55
 
56
56
  def series
57
- pages = @pagy.pages
58
- current = @pagy.page
57
+ pages = @page_info.pages
58
+ current = @page_info.page
59
59
 
60
60
  if pages <= 7
61
61
  (1..pages).to_a
@@ -2,6 +2,9 @@
2
2
 
3
3
  module JobHarbor
4
4
  class QueueCardComponent < ApplicationComponent
5
+ # Failed is left off the card; the queue page shows it.
6
+ STATUSES = %w[pending scheduled in_progress blocked].map { |key| JobStatus.find(key) }.freeze
7
+
5
8
  def initialize(queue:)
6
9
  @queue = queue
7
10
  end
@@ -31,12 +34,7 @@ module JobHarbor
31
34
 
32
35
  def stats
33
36
  content_tag(:div, class: "grid grid-cols-4 gap-4 mb-4") do
34
- safe_join([
35
- stat("Pending", @queue.pending_count),
36
- stat("Scheduled", @queue.scheduled_count),
37
- stat("In Progress", @queue.in_progress_count),
38
- stat("Blocked", @queue.blocked_count)
39
- ])
37
+ safe_join(STATUSES.map { |status| stat(status.label, @queue.count(status)) })
40
38
  end
41
39
  end
42
40
 
@@ -0,0 +1,5 @@
1
+ <%= form_with url: queues_pauses_path, method: :post, class: "flex flex-wrap items-center gap-2 mb-4" do |f| %>
2
+ <%= f.text_field :name, placeholder: "Queue name", maxlength: JobHarbor::QueueStats::MAX_NAME_LENGTH, required: true, autocomplete: "off",
3
+ "aria-label": "Queue name to pause", class: "select w-full sm:w-64" %>
4
+ <%= f.button "Pause", type: "submit", class: "btn btn-secondary btn-sm" %>
5
+ <% end %>
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobHarbor
4
+ class QueuePauseFormComponent < ApplicationComponent
5
+ end
6
+ end
@@ -2,16 +2,13 @@
2
2
 
3
3
  module JobHarbor
4
4
  class StatCardComponent < ApplicationComponent
5
- CIRCLE_COLORS = {
6
- pending: "circle-sky",
7
- scheduled: "circle-yellow",
8
- in_progress: "circle-amber",
9
- failed: "circle-red",
10
- finished: "circle-green",
11
- blocked: "circle-zinc",
12
- workers: "circle-green",
13
- queues: "circle-sky",
14
- throughput: "circle-green"
5
+ # Cards that are not about a job status.
6
+ OTHER_COLORS = {
7
+ workers: "green",
8
+ queues: "sky",
9
+ throughput: "green",
10
+ live: "green",
11
+ stale: "zinc"
15
12
  }.freeze
16
13
 
17
14
  def initialize(label:, value:, type: nil, link: nil)
@@ -48,10 +45,10 @@ module JobHarbor
48
45
  def circle_indicator
49
46
  return nil unless @type
50
47
 
51
- circle_class = CIRCLE_COLORS[@type]
52
- return nil unless circle_class
48
+ color = JobStatus.find(@type)&.color || OTHER_COLORS[@type]
49
+ return nil unless color
53
50
 
54
- content_tag(:span, "", class: "circle #{circle_class}")
51
+ content_tag(:span, "", class: "circle circle-#{color}")
55
52
  end
56
53
 
57
54
  def value_display
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobHarbor
4
+ class StatusTabsComponent < ApplicationComponent
5
+ # status is the key of the open tab, or nil for "All"; counts is keyed "all" and by status key.
6
+ def initialize(status:, counts:)
7
+ @status = status
8
+ @counts = counts
9
+ end
10
+
11
+ def call
12
+ content_tag(:div, class: "flex gap-1 mb-4 flex-wrap") do
13
+ safe_join([ all_tab, *JobStatus.all.map { |job_status| status_tab(job_status) } ])
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def all_tab
20
+ link_to jobs_path, class: tab_classes(selected: !@status) do
21
+ safe_join([ "All ", count_tag("all", "ml-1 opacity-75") ])
22
+ end
23
+ end
24
+
25
+ def status_tab(job_status)
26
+ link_to public_send(job_status.path_helper), class: tab_classes(selected: @status == job_status.key) do
27
+ safe_join([
28
+ content_tag(:span, "", class: "circle #{job_status.circle_class}"),
29
+ " #{job_status.label} ",
30
+ count_tag(job_status.key, "opacity-75")
31
+ ])
32
+ end
33
+ end
34
+
35
+ def count_tag(key, css_class)
36
+ content_tag(:span, @counts[key], class: css_class)
37
+ end
38
+
39
+ def tab_classes(selected:)
40
+ "badge #{selected ? 'badge-primary' : 'badge-secondary'}"
41
+ end
42
+ end
43
+ end
@@ -50,19 +50,9 @@ module JobHarbor
50
50
  def details
51
51
  content_tag(:div, class: "space-y-1") do
52
52
  safe_join([
53
- info_line("Hostname", @worker.hostname),
54
- info_line("PID", @worker.pid),
55
- info_line("Last Heartbeat", heartbeat_time)
56
- ])
57
- end
58
- end
59
-
60
- def info_line(label, value)
61
- content_tag(:div, class: "info-line") do
62
- safe_join([
63
- content_tag(:span, label, class: "info-line-label"),
64
- content_tag(:span, "", class: "info-line-separator"),
65
- content_tag(:span, value, class: "info-line-value")
53
+ render(InfoLineComponent.new(label: "Hostname", value: @worker.hostname)),
54
+ render(InfoLineComponent.new(label: "PID", value: @worker.pid)),
55
+ render(InfoLineComponent.new(label: "Last Heartbeat", value: heartbeat_time))
66
56
  ])
67
57
  end
68
58
  end