Showing preview only (239K chars total). Download the full file or copy to clipboard to get everything.
Repository: akodkod/solid-queue-dashboard
Branch: main
Commit: 51d25916a553
Files: 125
Total size: 209.4 KB
Directory structure:
gitextract_nv7r3_7n/
├── .github/
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .rubocop.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── Procfile.dev
├── README.md
├── Rakefile
├── app/
│ ├── assets/
│ │ ├── javascripts/
│ │ │ └── solid_queue_dashboard/
│ │ │ ├── alpine.js
│ │ │ └── application.js
│ │ └── stylesheets/
│ │ └── solid_queue_dashboard/
│ │ ├── application.css
│ │ └── tailwind.css
│ ├── controllers/
│ │ └── solid_queue_dashboard/
│ │ ├── appearance_controller.rb
│ │ ├── application_controller.rb
│ │ ├── dashboard_controller.rb
│ │ ├── jobs_controller.rb
│ │ ├── processes_controller.rb
│ │ ├── recurring_tasks_controller.rb
│ │ └── stats_controller.rb
│ ├── helpers/
│ │ └── solid_queue_dashboard/
│ │ ├── appearance_helper.rb
│ │ ├── application_helper.rb
│ │ ├── icons_helper.rb
│ │ ├── jobs_helper.rb
│ │ ├── pagination_helper.rb
│ │ ├── processes_helper.rb
│ │ └── recurring_tasks_helper.rb
│ └── views/
│ ├── layouts/
│ │ └── solid_queue_dashboard/
│ │ └── application.html.erb
│ └── solid_queue_dashboard/
│ ├── application/
│ │ ├── _flash_messages.html.erb
│ │ ├── _footer.html.erb
│ │ ├── _navbar.html.erb
│ │ └── _pagination.html.erb
│ ├── dashboard/
│ │ └── index.html.erb
│ ├── jobs/
│ │ ├── _filters.html.erb
│ │ ├── _table.html.erb
│ │ ├── _table_row.html.erb
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ ├── processes/
│ │ ├── _filters.html.erb
│ │ ├── _table.html.erb
│ │ ├── _table_row.html.erb
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ ├── recurring_tasks/
│ │ ├── _filters.html.erb
│ │ ├── _table.html.erb
│ │ ├── _table_row.html.erb
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ └── stats/
│ └── index.html.erb
├── bin/
│ ├── console
│ ├── dev
│ ├── setup
│ └── setup-test-app
├── bun.lockb
├── config/
│ └── routes.rb
├── lib/
│ ├── solid_queue_dashboard/
│ │ ├── configuration.rb
│ │ ├── decorators/
│ │ │ ├── job_decorator.rb
│ │ │ ├── jobs_decorator.rb
│ │ │ ├── process_decorator.rb
│ │ │ ├── processes_decorator.rb
│ │ │ ├── recurring_task_decorator.rb
│ │ │ └── recurring_tasks_decorator.rb
│ │ ├── engine.rb
│ │ ├── job.rb
│ │ ├── process.rb
│ │ ├── recurring_task.rb
│ │ └── version.rb
│ └── solid_queue_dashboard.rb
├── package.json
├── sig/
│ └── solid_queue_dashboard.rbs
├── solid_queue_dashboard.gemspec
├── tailwind.config.js
├── test/
│ ├── test_helper.rb
│ └── test_solid_queue_dashboard.rb
└── test_app/
├── .ruby-version
├── Gemfile
├── README.md
├── Rakefile
├── app/
│ ├── controllers/
│ │ ├── application_controller.rb
│ │ └── concerns/
│ │ └── .keep
│ ├── jobs/
│ │ ├── accept_arguments_job.rb
│ │ ├── always_fail_job.rb
│ │ ├── application_job.rb
│ │ ├── few_seconds_job.rb
│ │ ├── good_job.rb
│ │ ├── long_running_job.rb
│ │ ├── random_fail_job.rb
│ │ └── retrying_job.rb
│ └── models/
│ ├── application_record.rb
│ └── concerns/
│ └── .keep
├── bin/
│ ├── bundle
│ ├── dev
│ ├── jobs
│ ├── rails
│ ├── rake
│ ├── setup
│ └── thrust
├── config/
│ ├── application.rb
│ ├── boot.rb
│ ├── credentials.yml.enc
│ ├── database.yml
│ ├── environment.rb
│ ├── environments/
│ │ ├── development.rb
│ │ ├── production.rb
│ │ └── test.rb
│ ├── initializers/
│ │ ├── cors.rb
│ │ ├── filter_parameter_logging.rb
│ │ └── inflections.rb
│ ├── locales/
│ │ └── en.yml
│ ├── master.key
│ ├── puma.rb
│ ├── queue.yml
│ ├── recurring.yml
│ └── routes.rb
├── config.ru
├── db/
│ ├── queue_schema.rb
│ ├── schema.rb
│ └── seeds.rb
├── lib/
│ └── tasks/
│ ├── .keep
│ └── jobs.rake
├── public/
│ └── robots.txt
├── script/
│ └── .keep
├── storage/
│ └── .keep
└── vendor/
└── .keep
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/main.yml
================================================
name: Ruby
on:
push:
branches:
- main
pull_request:
jobs:
build:
runs-on: ubuntu-latest
name: Ruby ${{ matrix.ruby }}
strategy:
matrix:
ruby:
- '3.3.4'
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run the default task
run: bundle exec rake
================================================
FILE: .gitignore
================================================
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
/.idea/
/node_modules/
/test_app/storage/*.sqlite3
/test_app/storage/*.sqlite3*
/test_app/log/
/test_app/tmp/
/*.gem
================================================
FILE: .rubocop.yml
================================================
inherit_gem:
rubocop-rails-omakase: rubocop.yml
AllCops:
TargetRubyVersion: 3.0
Style/StringLiterals:
EnforcedStyle: double_quotes
Style/StringLiteralsInInterpolation:
EnforcedStyle: double_quotes
================================================
FILE: CHANGELOG.md
================================================
## [Unreleased]
## [0.2.0] - October 17, 2024
- Show running jobs
- Add charts
- Add auto-refresh
- Add number of current processes and recurring tasks to navbar
- Fix dark mode not switching for the first time
## [0.1.1] - October 7, 2024
- Replace OpenStruct with a Hash to avoid including the `ostruct` gem
## [0.1.0] - October 7, 2024
- Initial release
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official email address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
================================================
FILE: Gemfile
================================================
# frozen_string_literal: true
source "https://rubygems.org"
# Specify your gem's dependencies in solid_queue_dashboard.gemspec
gemspec
gem "vite_ruby", "~> 3.8"
gem "rake", "~> 13.2"
gem "minitest", "~> 5.25"
gem "rubocop", "~> 1.66"
gem "rubocop-rails-omakase", "~> 1.0"
================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)
Copyright (c) 2024 Andrew Kodkod
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: Procfile.dev
================================================
test_app: cd test_app && ./bin/rails server -p 3000
jobs: cd test_app && ./bin/jobs
tailwind: bun watch
================================================
FILE: README.md
================================================
# Solid Queue Dashboard <sup>BETA</sup>
<p align="center">
<a href="https://github.com/akodkod/solid-queue-dashboard#gh-light-mode-only">
<img src="https://github.com/user-attachments/assets/55aa4a3c-da51-471b-8f58-0cf1f9a1f8da" alt="Solid Queue Dashboard Light Mode">
</a>
<a href="https://github.com/akodkod/solid-queue-dashboard#gh-dark-mode-only">
<img src="https://github.com/user-attachments/assets/645558cb-c20f-4d4b-9697-55282710ea6c" alt="Solid Queue Dashboard Dark Mode">
</a>
_👋 I'm Available for Hire → [kodkod.me](https://kodkod.me)_
</p>
## Features
- 🎨 Beautiful UI
- 🧠 Smart status detection
- 📊 Track failure rates
- 💀 Find dead processes
- 📜 View execution history
- 🔍 Filter options
- 🔄 Retry jobs from the UI
- 🥬 Auto-refresh
- 📈 Add charts
- 🐒 No monkey patching
- 💈 TailwindCSS
## Roadmap
- 🚀 Manually trigger jobs
- ⏹️ Cancel long jobs (if possible)
- 📊 More statistics and insights
- 🔎 Search feature
- 🔢 Sorting options
- 🏗️ Add tests
## Installation
To install, run this command in your terminal:
```bash
bundle add solid_queue_dashboard
```
Or add this line to your `Gemfile`:
```bash
gem "solid_queue_dashboard", "~> 0.2.0"
```
Add this line to `routes.rb`:
```ruby
mount SolidQueueDashboard::Engine, at: "/solid-queue"
```
**IMPORTANT: Protect your SolidQueueDashboard with authentication to prevent unauthorized access.**
For example, if using Devise:
```ruby
Rails.application.routes.draw do
authenticate :current_admin do
mount SolidQueueDashboard::Engine, at: "/solid-queue"
end
end
```
## Contributing
After cloning the repo, run:
```
./bin/setup
./bin/setup-test-app
```
To run the test application:
```
gem install foreman
./bin/dev
```
To generate dummy data:
```
cd test_app
rails jobs:generate_dummy_data
```
## License
This gem is open source under the [MIT License](http://opensource.org/licenses/MIT).
---
_Made with love by Ukrainians 💙💛_
_[Help Ukraine](https://u24.gov.ua/)_
================================================
FILE: Rakefile
================================================
# frozen_string_literal: true
require "bundler/gem_tasks"
require "minitest/test_task"
Minitest::TestTask.create
require "rubocop/rake_task"
RuboCop::RakeTask.new
task default: %i[test rubocop]
================================================
FILE: app/assets/javascripts/solid_queue_dashboard/alpine.js
================================================
(()=>{var rt=!1,nt=!1,U=[],it=-1;function qt(e){Cn(e)}function Cn(e){U.includes(e)||U.push(e),Tn()}function Ee(e){let t=U.indexOf(e);t!==-1&&t>it&&U.splice(t,1)}function Tn(){!nt&&!rt&&(rt=!0,queueMicrotask(Rn))}function Rn(){rt=!1,nt=!0;for(let e=0;e<U.length;e++)U[e](),it=e;U.length=0,it=-1,nt=!1}var R,D,L,st,ot=!0;function Ut(e){ot=!1,e(),ot=!0}function Wt(e){R=e.reactive,L=e.release,D=t=>e.effect(t,{scheduler:r=>{ot?qt(r):r()}}),st=e.raw}function at(e){D=e}function Gt(e){let t=()=>{};return[n=>{let i=D(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),L(i))},i},()=>{t()}]}function ve(e,t){let r=!0,n,i=D(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>L(i)}var Jt=[],Yt=[],Xt=[];function Zt(e){Xt.push(e)}function ee(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Yt.push(t))}function Ae(e){Jt.push(e)}function Oe(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function ct(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function Qt(e){if(e._x_cleanups)for(;e._x_cleanups.length;)e._x_cleanups.pop()()}var lt=new MutationObserver(pt),ut=!1;function le(){lt.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ut=!0}function ft(){Mn(),lt.disconnect(),ut=!1}var ce=[];function Mn(){let e=lt.takeRecords();ce.push(()=>e.length>0&&pt(e));let t=ce.length;queueMicrotask(()=>{if(ce.length===t)for(;ce.length>0;)ce.shift()()})}function _(e){if(!ut)return e();ft();let t=e();return le(),t}var dt=!1,Se=[];function er(){dt=!0}function tr(){dt=!1,pt(Se),Se=[]}function pt(e){if(dt){Se=Se.concat(e);return}let t=new Set,r=new Set,n=new Map,i=new Map;for(let o=0;o<e.length;o++)if(!e[o].target._x_ignoreMutationObserver&&(e[o].type==="childList"&&(e[o].addedNodes.forEach(s=>s.nodeType===1&&t.add(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.add(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{ct(s,o)}),n.forEach((o,s)=>{Jt.forEach(a=>a(s,o))});for(let o of r)t.has(o)||Yt.forEach(s=>s(o));t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.has(o)||o.isConnected&&(delete o._x_ignoreSelf,delete o._x_ignore,Xt.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function Ce(e){return F(j(e))}function P(e,t,r){return e._x_dataStack=[t,...j(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function j(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?j(e.host):e.parentNode?j(e.parentNode):[]}function F(e){return new Proxy({objects:e},Nn)}var Nn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?Dn:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function Dn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Te(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Re(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>Pn(n,i),s=>mt(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function Pn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function mt(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),mt(e[t[0]],t.slice(1),r)}}var rr={};function y(e,t){rr[e]=t}function ue(e,t){return Object.entries(rr).forEach(([r,n])=>{let i=null;function o(){if(i)return i;{let[s,a]=_t(t);return i={interceptor:Re,...s},ee(t,a),i}}Object.defineProperty(e,`$${r}`,{get(){return n(t,o())},enumerable:!1})}),e}function nr(e,t,r,...n){try{return r(...n)}catch(i){te(i,e,t)}}function te(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message}
${r?'Expression: "'+r+`"
`:""}`,t),setTimeout(()=>{throw e},0)}var Me=!0;function De(e){let t=Me;Me=!1;let r=e();return Me=t,r}function M(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return ir(...e)}var ir=gt;function or(e){ir=e}function gt(e,t){let r={};ue(r,e);let n=[r,...j(e)],i=typeof t=="function"?In(n,t):Ln(n,t,e);return nr.bind(null,e,t,i)}function In(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(F([n,...e]),i);Ne(r,o)}}var ht={};function kn(e,t){if(ht[e])return ht[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return te(s,t,e),Promise.resolve()}})();return ht[e]=o,o}function Ln(e,t,r){let n=kn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=F([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>te(l,r,t));n.finished?(Ne(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Ne(i,l,a,s,r)}).catch(l=>te(l,r,t)).finally(()=>n.result=void 0)}}}function Ne(e,t,r,n,i){if(Me&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Ne(e,s,r,n)).catch(s=>te(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var bt="x-";function C(e=""){return bt+e}function sr(e){bt=e}var Pe={};function d(e,t){return Pe[e]=t,{before(r){if(!Pe[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=W.indexOf(r);W.splice(n>=0?n:W.indexOf("DEFAULT"),0,e)}}}function ar(e){return Object.keys(Pe).includes(e)}function de(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=wt(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(ur((o,s)=>n[o]=s)).filter(dr).map(jn(n,r)).sort(Fn).map(o=>$n(e,o))}function wt(e){return Array.from(e).map(ur()).filter(t=>!dr(t))}var xt=!1,fe=new Map,cr=Symbol();function lr(e){xt=!0;let t=Symbol();cr=t,fe.set(t,[]);let r=()=>{for(;fe.get(t).length;)fe.get(t).shift()();fe.delete(t)},n=()=>{xt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Gt(e);return t.push(i),[{Alpine:B,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:M.bind(M,e)},()=>t.forEach(a=>a())]}function $n(e,t){let r=()=>{},n=Pe[t.type]||r,[i,o]=_t(e);Oe(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),xt?fe.get(cr).push(n):n())};return s.runCleanups=o,s}var Ie=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),ke=e=>e;function ur(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=fr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var fr=[];function re(e){fr.push(e)}function dr({name:e}){return pr().test(e)}var pr=()=>new RegExp(`^${bt}([^:^.]+)\\b`);function jn(e,t){return({name:r,value:n})=>{let i=r.match(pr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var yt="DEFAULT",W=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",yt,"teleport"];function Fn(e,t){let r=W.indexOf(e.type)===-1?yt:e.type,n=W.indexOf(t.type)===-1?yt:t.type;return W.indexOf(r)-W.indexOf(n)}function G(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function T(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>T(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)T(n,t,!1),n=n.nextElementSibling}function E(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var mr=!1;function _r(){mr&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),mr=!0,document.body||E("Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine's `<script>` tag?"),G(document,"alpine:init"),G(document,"alpine:initializing"),le(),Zt(t=>S(t,T)),ee(t=>vt(t)),Ae((t,r)=>{de(t,r).forEach(n=>n())});let e=t=>!J(t.parentElement,!0);Array.from(document.querySelectorAll(xr().join(","))).filter(e).forEach(t=>{S(t)}),G(document,"alpine:initialized"),setTimeout(()=>{Bn()})}var Et=[],hr=[];function gr(){return Et.map(e=>e())}function xr(){return Et.concat(hr).map(e=>e())}function Le(e){Et.push(e)}function $e(e){hr.push(e)}function J(e,t=!1){return z(e,r=>{if((t?xr():gr()).some(i=>r.matches(i)))return!0})}function z(e,t){if(e){if(t(e))return e;if(e._x_teleportBack&&(e=e._x_teleportBack),!!e.parentElement)return z(e.parentElement,t)}}function yr(e){return gr().some(t=>e.matches(t))}var br=[];function wr(e){br.push(e)}function S(e,t=T,r=()=>{}){lr(()=>{t(e,(n,i)=>{r(n,i),br.forEach(o=>o(n,i)),de(n,n.attributes).forEach(o=>o()),n._x_ignore&&i()})})}function vt(e,t=T){t(e,r=>{ct(r),Qt(r)})}function Bn(){[["ui","dialog",["[x-dialog], [x-popover]"]],["anchor","anchor",["[x-anchor]"]],["sort","sort",["[x-sort]"]]].forEach(([t,r,n])=>{ar(r)||n.some(i=>{if(document.querySelector(i))return E(`found "${i}", but missing ${t} plugin`),!0})})}var St=[],At=!1;function ne(e=()=>{}){return queueMicrotask(()=>{At||setTimeout(()=>{je()})}),new Promise(t=>{St.push(()=>{e(),t()})})}function je(){for(At=!1;St.length;)St.shift()()}function Er(){At=!0}function pe(e,t){return Array.isArray(t)?vr(e,t.join(" ")):typeof t=="object"&&t!==null?zn(e,t):typeof t=="function"?pe(e,t()):vr(e,t)}function vr(e,t){let r=o=>o.split(" ").filter(Boolean),n=o=>o.split(" ").filter(s=>!e.classList.contains(s)).filter(Boolean),i=o=>(e.classList.add(...o),()=>{e.classList.remove(...o)});return t=t===!0?t="":t||"",i(n(t))}function zn(e,t){let r=a=>a.split(" ").filter(Boolean),n=Object.entries(t).flatMap(([a,c])=>c?r(a):!1).filter(Boolean),i=Object.entries(t).flatMap(([a,c])=>c?!1:r(a)).filter(Boolean),o=[],s=[];return i.forEach(a=>{e.classList.contains(a)&&(e.classList.remove(a),s.push(a))}),n.forEach(a=>{e.classList.contains(a)||(e.classList.add(a),o.push(a))}),()=>{s.forEach(a=>e.classList.add(a)),o.forEach(a=>e.classList.remove(a))}}function Y(e,t){return typeof t=="object"&&t!==null?Kn(e,t):Hn(e,t)}function Kn(e,t){let r={};return Object.entries(t).forEach(([n,i])=>{r[n]=e.style[n],n.startsWith("--")||(n=Vn(n)),e.style.setProperty(n,i)}),setTimeout(()=>{e.style.length===0&&e.removeAttribute("style")}),()=>{Y(e,r)}}function Hn(e,t){let r=e.getAttribute("style",t);return e.setAttribute("style",t),()=>{e.setAttribute("style",r||"")}}function Vn(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}function me(e,t=()=>{}){let r=!1;return function(){r?t.apply(this,arguments):(r=!0,e.apply(this,arguments))}}d("transition",(e,{value:t,modifiers:r,expression:n},{evaluate:i})=>{typeof n=="function"&&(n=i(n)),n!==!1&&(!n||typeof n=="boolean"?Un(e,r,t):qn(e,n,t))});function qn(e,t,r){Sr(e,pe,""),{enter:i=>{e._x_transition.enter.during=i},"enter-start":i=>{e._x_transition.enter.start=i},"enter-end":i=>{e._x_transition.enter.end=i},leave:i=>{e._x_transition.leave.during=i},"leave-start":i=>{e._x_transition.leave.start=i},"leave-end":i=>{e._x_transition.leave.end=i}}[r](t)}function Un(e,t,r){Sr(e,Y);let n=!t.includes("in")&&!t.includes("out")&&!r,i=n||t.includes("in")||["enter"].includes(r),o=n||t.includes("out")||["leave"].includes(r);t.includes("in")&&!n&&(t=t.filter((g,b)=>b<t.indexOf("out"))),t.includes("out")&&!n&&(t=t.filter((g,b)=>b>t.indexOf("out")));let s=!t.includes("opacity")&&!t.includes("scale"),a=s||t.includes("opacity"),c=s||t.includes("scale"),l=a?0:1,u=c?_e(t,"scale",95)/100:1,p=_e(t,"delay",0)/1e3,m=_e(t,"origin","center"),w="opacity, transform",$=_e(t,"duration",150)/1e3,we=_e(t,"duration",75)/1e3,f="cubic-bezier(0.4, 0.0, 0.2, 1)";i&&(e._x_transition.enter.during={transformOrigin:m,transitionDelay:`${p}s`,transitionProperty:w,transitionDuration:`${$}s`,transitionTimingFunction:f},e._x_transition.enter.start={opacity:l,transform:`scale(${u})`},e._x_transition.enter.end={opacity:1,transform:"scale(1)"}),o&&(e._x_transition.leave.during={transformOrigin:m,transitionDelay:`${p}s`,transitionProperty:w,transitionDuration:`${we}s`,transitionTimingFunction:f},e._x_transition.leave.start={opacity:1,transform:"scale(1)"},e._x_transition.leave.end={opacity:l,transform:`scale(${u})`})}function Sr(e,t,r={}){e._x_transition||(e._x_transition={enter:{during:r,start:r,end:r},leave:{during:r,start:r,end:r},in(n=()=>{},i=()=>{}){Fe(e,t,{during:this.enter.during,start:this.enter.start,end:this.enter.end},n,i)},out(n=()=>{},i=()=>{}){Fe(e,t,{during:this.leave.during,start:this.leave.start,end:this.leave.end},n,i)}})}window.Element.prototype._x_toggleAndCascadeWithTransitions=function(e,t,r,n){let i=document.visibilityState==="visible"?requestAnimationFrame:setTimeout,o=()=>i(r);if(t){e._x_transition&&(e._x_transition.enter||e._x_transition.leave)?e._x_transition.enter&&(Object.entries(e._x_transition.enter.during).length||Object.entries(e._x_transition.enter.start).length||Object.entries(e._x_transition.enter.end).length)?e._x_transition.in(r):o():e._x_transition?e._x_transition.in(r):o();return}e._x_hidePromise=e._x_transition?new Promise((s,a)=>{e._x_transition.out(()=>{},()=>s(n)),e._x_transitioning&&e._x_transitioning.beforeCancel(()=>a({isFromCancelledTransition:!0}))}):Promise.resolve(n),queueMicrotask(()=>{let s=Ar(e);s?(s._x_hideChildren||(s._x_hideChildren=[]),s._x_hideChildren.push(e)):i(()=>{let a=c=>{let l=Promise.all([c._x_hidePromise,...(c._x_hideChildren||[]).map(a)]).then(([u])=>u?.());return delete c._x_hidePromise,delete c._x_hideChildren,l};a(e).catch(c=>{if(!c.isFromCancelledTransition)throw c})})})};function Ar(e){let t=e.parentNode;if(t)return t._x_hidePromise?t:Ar(t)}function Fe(e,t,{during:r,start:n,end:i}={},o=()=>{},s=()=>{}){if(e._x_transitioning&&e._x_transitioning.cancel(),Object.keys(r).length===0&&Object.keys(n).length===0&&Object.keys(i).length===0){o(),s();return}let a,c,l;Wn(e,{start(){a=t(e,n)},during(){c=t(e,r)},before:o,end(){a(),l=t(e,i)},after:s,cleanup(){c(),l()}})}function Wn(e,t){let r,n,i,o=me(()=>{_(()=>{r=!0,n||t.before(),i||(t.end(),je()),t.after(),e.isConnected&&t.cleanup(),delete e._x_transitioning})});e._x_transitioning={beforeCancels:[],beforeCancel(s){this.beforeCancels.push(s)},cancel:me(function(){for(;this.beforeCancels.length;)this.beforeCancels.shift()();o()}),finish:o},_(()=>{t.start(),t.during()}),Er(),requestAnimationFrame(()=>{if(r)return;let s=Number(getComputedStyle(e).transitionDuration.replace(/,.*/,"").replace("s",""))*1e3,a=Number(getComputedStyle(e).transitionDelay.replace(/,.*/,"").replace("s",""))*1e3;s===0&&(s=Number(getComputedStyle(e).animationDuration.replace("s",""))*1e3),_(()=>{t.before()}),n=!0,requestAnimationFrame(()=>{r||(_(()=>{t.end()}),je(),setTimeout(e._x_transitioning.finish,s+a),i=!0)})})}function _e(e,t,r){if(e.indexOf(t)===-1)return r;let n=e[e.indexOf(t)+1];if(!n||t==="scale"&&isNaN(n))return r;if(t==="duration"||t==="delay"){let i=n.match(/([0-9]+)ms/);if(i)return i[1]}return t==="origin"&&["top","right","left","center","bottom"].includes(e[e.indexOf(t)+2])?[n,e[e.indexOf(t)+2]].join(" "):n}var I=!1;function A(e,t=()=>{}){return(...r)=>I?t(...r):e(...r)}function Or(e){return(...t)=>I&&e(...t)}var Cr=[];function K(e){Cr.push(e)}function Tr(e,t){Cr.forEach(r=>r(e,t)),I=!0,Mr(()=>{S(t,(r,n)=>{n(r,()=>{})})}),I=!1}var Be=!1;function Rr(e,t){t._x_dataStack||(t._x_dataStack=e._x_dataStack),I=!0,Be=!0,Mr(()=>{Gn(t)}),I=!1,Be=!1}function Gn(e){let t=!1;S(e,(n,i)=>{T(n,(o,s)=>{if(t&&yr(o))return s();t=!0,i(o,s)})})}function Mr(e){let t=D;at((r,n)=>{let i=t(r);return L(i),()=>{}}),e(),at(t)}function he(e,t,r,n=[]){switch(e._x_bindings||(e._x_bindings=R({})),e._x_bindings[t]=r,t=n.includes("camel")?ri(t):t,t){case"value":Jn(e,r);break;case"style":Xn(e,r);break;case"class":Yn(e,r);break;case"selected":case"checked":Zn(e,t,r);break;default:Dr(e,t,r);break}}function Jn(e,t){if(e.type==="radio")e.attributes.value===void 0&&(e.value=t),window.fromModel&&(typeof t=="boolean"?e.checked=ge(e.value)===t:e.checked=Nr(e.value,t));else if(e.type==="checkbox")Number.isInteger(t)?e.value=t:!Array.isArray(t)&&typeof t!="boolean"&&![null,void 0].includes(t)?e.value=String(t):Array.isArray(t)?e.checked=t.some(r=>Nr(r,e.value)):e.checked=!!t;else if(e.tagName==="SELECT")ti(e,t);else{if(e.value===t)return;e.value=t===void 0?"":t}}function Yn(e,t){e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_undoAddedClasses=pe(e,t)}function Xn(e,t){e._x_undoAddedStyles&&e._x_undoAddedStyles(),e._x_undoAddedStyles=Y(e,t)}function Zn(e,t,r){Dr(e,t,r),ei(e,t,r)}function Dr(e,t,r){[null,void 0,!1].includes(r)&&ni(t)?e.removeAttribute(t):(Pr(t)&&(r=t),Qn(e,t,r))}function Qn(e,t,r){e.getAttribute(t)!=r&&e.setAttribute(t,r)}function ei(e,t,r){e[t]!==r&&(e[t]=r)}function ti(e,t){let r=[].concat(t).map(n=>n+"");Array.from(e.options).forEach(n=>{n.selected=r.includes(n.value)})}function ri(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function Nr(e,t){return e==t}function ge(e){return[1,"1","true","on","yes",!0].includes(e)?!0:[0,"0","false","off","no",!1].includes(e)?!1:e?Boolean(e):null}function Pr(e){return["disabled","checked","required","readonly","open","selected","autofocus","itemscope","multiple","novalidate","allowfullscreen","allowpaymentrequest","formnovalidate","autoplay","controls","loop","muted","playsinline","default","ismap","reversed","async","defer","nomodule"].includes(e)}function ni(e){return!["aria-pressed","aria-checked","aria-expanded","aria-selected"].includes(e)}function Ir(e,t,r){return e._x_bindings&&e._x_bindings[t]!==void 0?e._x_bindings[t]:Lr(e,t,r)}function kr(e,t,r,n=!0){if(e._x_bindings&&e._x_bindings[t]!==void 0)return e._x_bindings[t];if(e._x_inlineBindings&&e._x_inlineBindings[t]!==void 0){let i=e._x_inlineBindings[t];return i.extract=n,De(()=>M(e,i.expression))}return Lr(e,t,r)}function Lr(e,t,r){let n=e.getAttribute(t);return n===null?typeof r=="function"?r():r:n===""?!0:Pr(t)?!![t,"true"].includes(n):n}function ze(e,t){var r;return function(){var n=this,i=arguments,o=function(){r=null,e.apply(n,i)};clearTimeout(r),r=setTimeout(o,t)}}function Ke(e,t){let r;return function(){let n=this,i=arguments;r||(e.apply(n,i),r=!0,setTimeout(()=>r=!1,t))}}function He({get:e,set:t},{get:r,set:n}){let i=!0,o,s,a=D(()=>{let c=e(),l=r();if(i)n(Ot(c)),i=!1;else{let u=JSON.stringify(c),p=JSON.stringify(l);u!==o?n(Ot(c)):u!==p&&t(Ot(l))}o=JSON.stringify(e()),s=JSON.stringify(r())});return()=>{L(a)}}function Ot(e){return typeof e=="object"?JSON.parse(JSON.stringify(e)):e}function $r(e){(Array.isArray(e)?e:[e]).forEach(r=>r(B))}var X={},jr=!1;function Fr(e,t){if(jr||(X=R(X),jr=!0),t===void 0)return X[e];X[e]=t,typeof t=="object"&&t!==null&&t.hasOwnProperty("init")&&typeof t.init=="function"&&X[e].init(),Te(X[e])}function Br(){return X}var zr={};function Kr(e,t){let r=typeof t!="function"?()=>t:t;return e instanceof Element?Ct(e,r()):(zr[e]=r,()=>{})}function Hr(e){return Object.entries(zr).forEach(([t,r])=>{Object.defineProperty(e,t,{get(){return(...n)=>r(...n)}})}),e}function Ct(e,t,r){let n=[];for(;n.length;)n.pop()();let i=Object.entries(t).map(([s,a])=>({name:s,value:a})),o=wt(i);return i=i.map(s=>o.find(a=>a.name===s.name)?{name:`x-bind:${s.name}`,value:`"${s.value}"`}:s),de(e,i,r).map(s=>{n.push(s.runCleanups),s()}),()=>{for(;n.length;)n.pop()()}}var Vr={};function qr(e,t){Vr[e]=t}function Ur(e,t){return Object.entries(Vr).forEach(([r,n])=>{Object.defineProperty(e,r,{get(){return(...i)=>n.bind(t)(...i)},enumerable:!1})}),e}var ii={get reactive(){return R},get release(){return L},get effect(){return D},get raw(){return st},version:"3.14.1",flushAndStopDeferringMutations:tr,dontAutoEvaluateFunctions:De,disableEffectScheduling:Ut,startObservingMutations:le,stopObservingMutations:ft,setReactivityEngine:Wt,onAttributeRemoved:Oe,onAttributesAdded:Ae,closestDataStack:j,skipDuringClone:A,onlyDuringClone:Or,addRootSelector:Le,addInitSelector:$e,interceptClone:K,addScopeToNode:P,deferMutations:er,mapAttributes:re,evaluateLater:x,interceptInit:wr,setEvaluator:or,mergeProxies:F,extractProp:kr,findClosest:z,onElRemoved:ee,closestRoot:J,destroyTree:vt,interceptor:Re,transition:Fe,setStyles:Y,mutateDom:_,directive:d,entangle:He,throttle:Ke,debounce:ze,evaluate:M,initTree:S,nextTick:ne,prefixed:C,prefix:sr,plugin:$r,magic:y,store:Fr,start:_r,clone:Rr,cloneNode:Tr,bound:Ir,$data:Ce,watch:ve,walk:T,data:qr,bind:Kr},B=ii;function Tt(e,t){let r=Object.create(null),n=e.split(",");for(let i=0;i<n.length;i++)r[n[i]]=!0;return t?i=>!!r[i.toLowerCase()]:i=>!!r[i]}var oi="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly";var Ms=Tt(oi+",async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");var Wr=Object.freeze({}),Ns=Object.freeze([]);var si=Object.prototype.hasOwnProperty,xe=(e,t)=>si.call(e,t),H=Array.isArray,ie=e=>Gr(e)==="[object Map]";var ai=e=>typeof e=="string",Ve=e=>typeof e=="symbol",ye=e=>e!==null&&typeof e=="object";var ci=Object.prototype.toString,Gr=e=>ci.call(e),Rt=e=>Gr(e).slice(8,-1);var qe=e=>ai(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e;var Ue=e=>{let t=Object.create(null);return r=>t[r]||(t[r]=e(r))},li=/-(\w)/g,Ds=Ue(e=>e.replace(li,(t,r)=>r?r.toUpperCase():"")),ui=/\B([A-Z])/g,Ps=Ue(e=>e.replace(ui,"-$1").toLowerCase()),Mt=Ue(e=>e.charAt(0).toUpperCase()+e.slice(1)),Is=Ue(e=>e?`on${Mt(e)}`:""),Nt=(e,t)=>e!==t&&(e===e||t===t);var Dt=new WeakMap,be=[],k,Z=Symbol("iterate"),Pt=Symbol("Map key iterate");function fi(e){return e&&e._isEffect===!0}function en(e,t=Wr){fi(e)&&(e=e.raw);let r=pi(e,t);return t.lazy||r(),r}function tn(e){e.active&&(rn(e),e.options.onStop&&e.options.onStop(),e.active=!1)}var di=0;function pi(e,t){let r=function(){if(!r.active)return e();if(!be.includes(r)){rn(r);try{return _i(),be.push(r),k=r,e()}finally{be.pop(),nn(),k=be[be.length-1]}}};return r.id=di++,r.allowRecurse=!!t.allowRecurse,r._isEffect=!0,r.active=!0,r.raw=e,r.deps=[],r.options=t,r}function rn(e){let{deps:t}=e;if(t.length){for(let r=0;r<t.length;r++)t[r].delete(e);t.length=0}}var oe=!0,kt=[];function mi(){kt.push(oe),oe=!1}function _i(){kt.push(oe),oe=!0}function nn(){let e=kt.pop();oe=e===void 0?!0:e}function N(e,t,r){if(!oe||k===void 0)return;let n=Dt.get(e);n||Dt.set(e,n=new Map);let i=n.get(r);i||n.set(r,i=new Set),i.has(k)||(i.add(k),k.deps.push(i),k.options.onTrack&&k.options.onTrack({effect:k,target:e,type:t,key:r}))}function q(e,t,r,n,i,o){let s=Dt.get(e);if(!s)return;let a=new Set,c=u=>{u&&u.forEach(p=>{(p!==k||p.allowRecurse)&&a.add(p)})};if(t==="clear")s.forEach(c);else if(r==="length"&&H(e))s.forEach((u,p)=>{(p==="length"||p>=n)&&c(u)});else switch(r!==void 0&&c(s.get(r)),t){case"add":H(e)?qe(r)&&c(s.get("length")):(c(s.get(Z)),ie(e)&&c(s.get(Pt)));break;case"delete":H(e)||(c(s.get(Z)),ie(e)&&c(s.get(Pt)));break;case"set":ie(e)&&c(s.get(Z));break}let l=u=>{u.options.onTrigger&&u.options.onTrigger({effect:u,target:e,key:r,type:t,newValue:n,oldValue:i,oldTarget:o}),u.options.scheduler?u.options.scheduler(u):u()};a.forEach(l)}var hi=Tt("__proto__,__v_isRef,__isVue"),on=new Set(Object.getOwnPropertyNames(Symbol).map(e=>Symbol[e]).filter(Ve)),gi=sn();var xi=sn(!0);var Jr=yi();function yi(){let e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...r){let n=h(this);for(let o=0,s=this.length;o<s;o++)N(n,"get",o+"");let i=n[t](...r);return i===-1||i===!1?n[t](...r.map(h)):i}}),["push","pop","shift","unshift","splice"].forEach(t=>{e[t]=function(...r){mi();let n=h(this)[t].apply(this,r);return nn(),n}}),e}function sn(e=!1,t=!1){return function(n,i,o){if(i==="__v_isReactive")return!e;if(i==="__v_isReadonly")return e;if(i==="__v_raw"&&o===(e?t?ki:un:t?Ii:ln).get(n))return n;let s=H(n);if(!e&&s&&xe(Jr,i))return Reflect.get(Jr,i,o);let a=Reflect.get(n,i,o);return(Ve(i)?on.has(i):hi(i))||(e||N(n,"get",i),t)?a:It(a)?!s||!qe(i)?a.value:a:ye(a)?e?fn(a):Qe(a):a}}var bi=wi();function wi(e=!1){return function(r,n,i,o){let s=r[n];if(!e&&(i=h(i),s=h(s),!H(r)&&It(s)&&!It(i)))return s.value=i,!0;let a=H(r)&&qe(n)?Number(n)<r.length:xe(r,n),c=Reflect.set(r,n,i,o);return r===h(o)&&(a?Nt(i,s)&&q(r,"set",n,i,s):q(r,"add",n,i)),c}}function Ei(e,t){let r=xe(e,t),n=e[t],i=Reflect.deleteProperty(e,t);return i&&r&&q(e,"delete",t,void 0,n),i}function vi(e,t){let r=Reflect.has(e,t);return(!Ve(t)||!on.has(t))&&N(e,"has",t),r}function Si(e){return N(e,"iterate",H(e)?"length":Z),Reflect.ownKeys(e)}var Ai={get:gi,set:bi,deleteProperty:Ei,has:vi,ownKeys:Si},Oi={get:xi,set(e,t){return console.warn(`Set operation on key "${String(t)}" failed: target is readonly.`,e),!0},deleteProperty(e,t){return console.warn(`Delete operation on key "${String(t)}" failed: target is readonly.`,e),!0}};var Lt=e=>ye(e)?Qe(e):e,$t=e=>ye(e)?fn(e):e,jt=e=>e,Ze=e=>Reflect.getPrototypeOf(e);function We(e,t,r=!1,n=!1){e=e.__v_raw;let i=h(e),o=h(t);t!==o&&!r&&N(i,"get",t),!r&&N(i,"get",o);let{has:s}=Ze(i),a=n?jt:r?$t:Lt;if(s.call(i,t))return a(e.get(t));if(s.call(i,o))return a(e.get(o));e!==i&&e.get(t)}function Ge(e,t=!1){let r=this.__v_raw,n=h(r),i=h(e);return e!==i&&!t&&N(n,"has",e),!t&&N(n,"has",i),e===i?r.has(e):r.has(e)||r.has(i)}function Je(e,t=!1){return e=e.__v_raw,!t&&N(h(e),"iterate",Z),Reflect.get(e,"size",e)}function Yr(e){e=h(e);let t=h(this);return Ze(t).has.call(t,e)||(t.add(e),q(t,"add",e,e)),this}function Xr(e,t){t=h(t);let r=h(this),{has:n,get:i}=Ze(r),o=n.call(r,e);o?cn(r,n,e):(e=h(e),o=n.call(r,e));let s=i.call(r,e);return r.set(e,t),o?Nt(t,s)&&q(r,"set",e,t,s):q(r,"add",e,t),this}function Zr(e){let t=h(this),{has:r,get:n}=Ze(t),i=r.call(t,e);i?cn(t,r,e):(e=h(e),i=r.call(t,e));let o=n?n.call(t,e):void 0,s=t.delete(e);return i&&q(t,"delete",e,void 0,o),s}function Qr(){let e=h(this),t=e.size!==0,r=ie(e)?new Map(e):new Set(e),n=e.clear();return t&&q(e,"clear",void 0,void 0,r),n}function Ye(e,t){return function(n,i){let o=this,s=o.__v_raw,a=h(s),c=t?jt:e?$t:Lt;return!e&&N(a,"iterate",Z),s.forEach((l,u)=>n.call(i,c(l),c(u),o))}}function Xe(e,t,r){return function(...n){let i=this.__v_raw,o=h(i),s=ie(o),a=e==="entries"||e===Symbol.iterator&&s,c=e==="keys"&&s,l=i[e](...n),u=r?jt:t?$t:Lt;return!t&&N(o,"iterate",c?Pt:Z),{next(){let{value:p,done:m}=l.next();return m?{value:p,done:m}:{value:a?[u(p[0]),u(p[1])]:u(p),done:m}},[Symbol.iterator](){return this}}}}function V(e){return function(...t){{let r=t[0]?`on key "${t[0]}" `:"";console.warn(`${Mt(e)} operation ${r}failed: target is readonly.`,h(this))}return e==="delete"?!1:this}}function Ci(){let e={get(o){return We(this,o)},get size(){return Je(this)},has:Ge,add:Yr,set:Xr,delete:Zr,clear:Qr,forEach:Ye(!1,!1)},t={get(o){return We(this,o,!1,!0)},get size(){return Je(this)},has:Ge,add:Yr,set:Xr,delete:Zr,clear:Qr,forEach:Ye(!1,!0)},r={get(o){return We(this,o,!0)},get size(){return Je(this,!0)},has(o){return Ge.call(this,o,!0)},add:V("add"),set:V("set"),delete:V("delete"),clear:V("clear"),forEach:Ye(!0,!1)},n={get(o){return We(this,o,!0,!0)},get size(){return Je(this,!0)},has(o){return Ge.call(this,o,!0)},add:V("add"),set:V("set"),delete:V("delete"),clear:V("clear"),forEach:Ye(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(o=>{e[o]=Xe(o,!1,!1),r[o]=Xe(o,!0,!1),t[o]=Xe(o,!1,!0),n[o]=Xe(o,!0,!0)}),[e,r,t,n]}var[Ti,Ri,Mi,Ni]=Ci();function an(e,t){let r=t?e?Ni:Mi:e?Ri:Ti;return(n,i,o)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?n:Reflect.get(xe(r,i)&&i in n?r:n,i,o)}var Di={get:an(!1,!1)};var Pi={get:an(!0,!1)};function cn(e,t,r){let n=h(r);if(n!==r&&t.call(e,n)){let i=Rt(e);console.warn(`Reactive ${i} contains both the raw and reactive versions of the same object${i==="Map"?" as keys":""}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`)}}var ln=new WeakMap,Ii=new WeakMap,un=new WeakMap,ki=new WeakMap;function Li(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function $i(e){return e.__v_skip||!Object.isExtensible(e)?0:Li(Rt(e))}function Qe(e){return e&&e.__v_isReadonly?e:dn(e,!1,Ai,Di,ln)}function fn(e){return dn(e,!0,Oi,Pi,un)}function dn(e,t,r,n,i){if(!ye(e))return console.warn(`value cannot be made reactive: ${String(e)}`),e;if(e.__v_raw&&!(t&&e.__v_isReactive))return e;let o=i.get(e);if(o)return o;let s=$i(e);if(s===0)return e;let a=new Proxy(e,s===2?n:r);return i.set(e,a),a}function h(e){return e&&h(e.__v_raw)||e}function It(e){return Boolean(e&&e.__v_isRef===!0)}y("nextTick",()=>ne);y("dispatch",e=>G.bind(G,e));y("watch",(e,{evaluateLater:t,cleanup:r})=>(n,i)=>{let o=t(n),a=ve(()=>{let c;return o(l=>c=l),c},i);r(a)});y("store",Br);y("data",e=>Ce(e));y("root",e=>J(e));y("refs",e=>(e._x_refs_proxy||(e._x_refs_proxy=F(ji(e))),e._x_refs_proxy));function ji(e){let t=[];return z(e,r=>{r._x_refs&&t.push(r._x_refs)}),t}var Ft={};function Bt(e){return Ft[e]||(Ft[e]=0),++Ft[e]}function pn(e,t){return z(e,r=>{if(r._x_ids&&r._x_ids[t])return!0})}function mn(e,t){e._x_ids||(e._x_ids={}),e._x_ids[t]||(e._x_ids[t]=Bt(t))}y("id",(e,{cleanup:t})=>(r,n=null)=>{let i=`${r}${n?`-${n}`:""}`;return Fi(e,i,t,()=>{let o=pn(e,r),s=o?o._x_ids[r]:Bt(r);return n?`${r}-${s}-${n}`:`${r}-${s}`})});K((e,t)=>{e._x_id&&(t._x_id=e._x_id)});function Fi(e,t,r,n){if(e._x_id||(e._x_id={}),e._x_id[t])return e._x_id[t];let i=n();return e._x_id[t]=i,r(()=>{delete e._x_id[t]}),i}y("el",e=>e);_n("Focus","focus","focus");_n("Persist","persist","persist");function _n(e,t,r){y(t,n=>E(`You can't use [$${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}d("modelable",(e,{expression:t},{effect:r,evaluateLater:n,cleanup:i})=>{let o=n(t),s=()=>{let u;return o(p=>u=p),u},a=n(`${t} = __placeholder`),c=u=>a(()=>{},{scope:{__placeholder:u}}),l=s();c(l),queueMicrotask(()=>{if(!e._x_model)return;e._x_removeModelListeners.default();let u=e._x_model.get,p=e._x_model.set,m=He({get(){return u()},set(w){p(w)}},{get(){return s()},set(w){c(w)}});i(m)})});d("teleport",(e,{modifiers:t,expression:r},{cleanup:n})=>{e.tagName.toLowerCase()!=="template"&&E("x-teleport can only be used on a <template> tag",e);let i=hn(r),o=e.content.cloneNode(!0).firstElementChild;e._x_teleport=o,o._x_teleportBack=e,e.setAttribute("data-teleport-template",!0),o.setAttribute("data-teleport-target",!0),e._x_forwardEvents&&e._x_forwardEvents.forEach(a=>{o.addEventListener(a,c=>{c.stopPropagation(),e.dispatchEvent(new c.constructor(c.type,c))})}),P(o,{},e);let s=(a,c,l)=>{l.includes("prepend")?c.parentNode.insertBefore(a,c):l.includes("append")?c.parentNode.insertBefore(a,c.nextSibling):c.appendChild(a)};_(()=>{s(o,i,t),A(()=>{S(o),o._x_ignore=!0})()}),e._x_teleportPutBack=()=>{let a=hn(r);_(()=>{s(e._x_teleport,a,t)})},n(()=>o.remove())});var Bi=document.createElement("div");function hn(e){let t=A(()=>document.querySelector(e),()=>Bi)();return t||E(`Cannot find x-teleport element for selector: "${e}"`),t}var gn=()=>{};gn.inline=(e,{modifiers:t},{cleanup:r})=>{t.includes("self")?e._x_ignoreSelf=!0:e._x_ignore=!0,r(()=>{t.includes("self")?delete e._x_ignoreSelf:delete e._x_ignore})};d("ignore",gn);d("effect",A((e,{expression:t},{effect:r})=>{r(x(e,t))}));function se(e,t,r,n){let i=e,o=c=>n(c),s={},a=(c,l)=>u=>l(c,u);if(r.includes("dot")&&(t=zi(t)),r.includes("camel")&&(t=Ki(t)),r.includes("passive")&&(s.passive=!0),r.includes("capture")&&(s.capture=!0),r.includes("window")&&(i=window),r.includes("document")&&(i=document),r.includes("debounce")){let c=r[r.indexOf("debounce")+1]||"invalid-wait",l=et(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=ze(o,l)}if(r.includes("throttle")){let c=r[r.indexOf("throttle")+1]||"invalid-wait",l=et(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=Ke(o,l)}return r.includes("prevent")&&(o=a(o,(c,l)=>{l.preventDefault(),c(l)})),r.includes("stop")&&(o=a(o,(c,l)=>{l.stopPropagation(),c(l)})),r.includes("once")&&(o=a(o,(c,l)=>{c(l),i.removeEventListener(t,o,s)})),(r.includes("away")||r.includes("outside"))&&(i=document,o=a(o,(c,l)=>{e.contains(l.target)||l.target.isConnected!==!1&&(e.offsetWidth<1&&e.offsetHeight<1||e._x_isShown!==!1&&c(l))})),r.includes("self")&&(o=a(o,(c,l)=>{l.target===e&&c(l)})),(Vi(t)||yn(t))&&(o=a(o,(c,l)=>{qi(l,r)||c(l)})),i.addEventListener(t,o,s),()=>{i.removeEventListener(t,o,s)}}function zi(e){return e.replace(/-/g,".")}function Ki(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function et(e){return!Array.isArray(e)&&!isNaN(e)}function Hi(e){return[" ","_"].includes(e)?e:e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[_\s]/,"-").toLowerCase()}function Vi(e){return["keydown","keyup"].includes(e)}function yn(e){return["contextmenu","click","mouse"].some(t=>e.includes(t))}function qi(e,t){let r=t.filter(o=>!["window","document","prevent","stop","once","capture","self","away","outside","passive"].includes(o));if(r.includes("debounce")){let o=r.indexOf("debounce");r.splice(o,et((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.includes("throttle")){let o=r.indexOf("throttle");r.splice(o,et((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.length===0||r.length===1&&xn(e.key).includes(r[0]))return!1;let i=["ctrl","shift","alt","meta","cmd","super"].filter(o=>r.includes(o));return r=r.filter(o=>!i.includes(o)),!(i.length>0&&i.filter(s=>((s==="cmd"||s==="super")&&(s="meta"),e[`${s}Key`])).length===i.length&&(yn(e.type)||xn(e.key).includes(r[0])))}function xn(e){if(!e)return[];e=Hi(e);let t={ctrl:"control",slash:"/",space:" ",spacebar:" ",cmd:"meta",esc:"escape",up:"arrow-up",down:"arrow-down",left:"arrow-left",right:"arrow-right",period:".",comma:",",equal:"=",minus:"-",underscore:"_"};return t[e]=e,Object.keys(t).map(r=>{if(t[r]===e)return r}).filter(r=>r)}d("model",(e,{modifiers:t,expression:r},{effect:n,cleanup:i})=>{let o=e;t.includes("parent")&&(o=e.parentNode);let s=x(o,r),a;typeof r=="string"?a=x(o,`${r} = __placeholder`):typeof r=="function"&&typeof r()=="string"?a=x(o,`${r()} = __placeholder`):a=()=>{};let c=()=>{let m;return s(w=>m=w),bn(m)?m.get():m},l=m=>{let w;s($=>w=$),bn(w)?w.set(m):a(()=>{},{scope:{__placeholder:m}})};typeof r=="string"&&e.type==="radio"&&_(()=>{e.hasAttribute("name")||e.setAttribute("name",r)});var u=e.tagName.toLowerCase()==="select"||["checkbox","radio"].includes(e.type)||t.includes("lazy")?"change":"input";let p=I?()=>{}:se(e,u,t,m=>{l(zt(e,t,m,c()))});if(t.includes("fill")&&([void 0,null,""].includes(c())||e.type==="checkbox"&&Array.isArray(c())||e.tagName.toLowerCase()==="select"&&e.multiple)&&l(zt(e,t,{target:e},c())),e._x_removeModelListeners||(e._x_removeModelListeners={}),e._x_removeModelListeners.default=p,i(()=>e._x_removeModelListeners.default()),e.form){let m=se(e.form,"reset",[],w=>{ne(()=>e._x_model&&e._x_model.set(zt(e,t,{target:e},c())))});i(()=>m())}e._x_model={get(){return c()},set(m){l(m)}},e._x_forceModelUpdate=m=>{m===void 0&&typeof r=="string"&&r.match(/\./)&&(m=""),window.fromModel=!0,_(()=>he(e,"value",m)),delete window.fromModel},n(()=>{let m=c();t.includes("unintrusive")&&document.activeElement.isSameNode(e)||e._x_forceModelUpdate(m)})});function zt(e,t,r,n){return _(()=>{if(r instanceof CustomEvent&&r.detail!==void 0)return r.detail!==null&&r.detail!==void 0?r.detail:r.target.value;if(e.type==="checkbox")if(Array.isArray(n)){let i=null;return t.includes("number")?i=Kt(r.target.value):t.includes("boolean")?i=ge(r.target.value):i=r.target.value,r.target.checked?n.includes(i)?n:n.concat([i]):n.filter(o=>!Ui(o,i))}else return r.target.checked;else{if(e.tagName.toLowerCase()==="select"&&e.multiple)return t.includes("number")?Array.from(r.target.selectedOptions).map(i=>{let o=i.value||i.text;return Kt(o)}):t.includes("boolean")?Array.from(r.target.selectedOptions).map(i=>{let o=i.value||i.text;return ge(o)}):Array.from(r.target.selectedOptions).map(i=>i.value||i.text);{let i;return e.type==="radio"?r.target.checked?i=r.target.value:i=n:i=r.target.value,t.includes("number")?Kt(i):t.includes("boolean")?ge(i):t.includes("trim")?i.trim():i}}})}function Kt(e){let t=e?parseFloat(e):null;return Wi(t)?t:e}function Ui(e,t){return e==t}function Wi(e){return!Array.isArray(e)&&!isNaN(e)}function bn(e){return e!==null&&typeof e=="object"&&typeof e.get=="function"&&typeof e.set=="function"}d("cloak",e=>queueMicrotask(()=>_(()=>e.removeAttribute(C("cloak")))));$e(()=>`[${C("init")}]`);d("init",A((e,{expression:t},{evaluate:r})=>typeof t=="string"?!!t.trim()&&r(t,{},!1):r(t,{},!1)));d("text",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{_(()=>{e.textContent=o})})})});d("html",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{_(()=>{e.innerHTML=o,e._x_ignoreSelf=!0,S(e),delete e._x_ignoreSelf})})})});re(Ie(":",ke(C("bind:"))));var wn=(e,{value:t,modifiers:r,expression:n,original:i},{effect:o,cleanup:s})=>{if(!t){let c={};Hr(c),x(e,n)(u=>{Ct(e,u,i)},{scope:c});return}if(t==="key")return Gi(e,n);if(e._x_inlineBindings&&e._x_inlineBindings[t]&&e._x_inlineBindings[t].extract)return;let a=x(e,n);o(()=>a(c=>{c===void 0&&typeof n=="string"&&n.match(/\./)&&(c=""),_(()=>he(e,t,c,r))})),s(()=>{e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_undoAddedStyles&&e._x_undoAddedStyles()})};wn.inline=(e,{value:t,modifiers:r,expression:n})=>{t&&(e._x_inlineBindings||(e._x_inlineBindings={}),e._x_inlineBindings[t]={expression:n,extract:!1})};d("bind",wn);function Gi(e,t){e._x_keyExpression=t}Le(()=>`[${C("data")}]`);d("data",(e,{expression:t},{cleanup:r})=>{if(Ji(e))return;t=t===""?"{}":t;let n={};ue(n,e);let i={};Ur(i,n);let o=M(e,t,{scope:i});(o===void 0||o===!0)&&(o={}),ue(o,e);let s=R(o);Te(s);let a=P(e,s);s.init&&M(e,s.init),r(()=>{s.destroy&&M(e,s.destroy),a()})});K((e,t)=>{e._x_dataStack&&(t._x_dataStack=e._x_dataStack,t.setAttribute("data-has-alpine-state",!0))});function Ji(e){return I?Be?!0:e.hasAttribute("data-has-alpine-state"):!1}d("show",(e,{modifiers:t,expression:r},{effect:n})=>{let i=x(e,r);e._x_doHide||(e._x_doHide=()=>{_(()=>{e.style.setProperty("display","none",t.includes("important")?"important":void 0)})}),e._x_doShow||(e._x_doShow=()=>{_(()=>{e.style.length===1&&e.style.display==="none"?e.removeAttribute("style"):e.style.removeProperty("display")})});let o=()=>{e._x_doHide(),e._x_isShown=!1},s=()=>{e._x_doShow(),e._x_isShown=!0},a=()=>setTimeout(s),c=me(p=>p?s():o(),p=>{typeof e._x_toggleAndCascadeWithTransitions=="function"?e._x_toggleAndCascadeWithTransitions(e,p,s,o):p?a():o()}),l,u=!0;n(()=>i(p=>{!u&&p===l||(t.includes("immediate")&&(p?a():o()),c(p),l=p,u=!1)}))});d("for",(e,{expression:t},{effect:r,cleanup:n})=>{let i=Xi(t),o=x(e,i.items),s=x(e,e._x_keyExpression||"index");e._x_prevKeys=[],e._x_lookup={},r(()=>Yi(e,i,o,s)),n(()=>{Object.values(e._x_lookup).forEach(a=>a.remove()),delete e._x_prevKeys,delete e._x_lookup})});function Yi(e,t,r,n){let i=s=>typeof s=="object"&&!Array.isArray(s),o=e;r(s=>{Zi(s)&&s>=0&&(s=Array.from(Array(s).keys(),f=>f+1)),s===void 0&&(s=[]);let a=e._x_lookup,c=e._x_prevKeys,l=[],u=[];if(i(s))s=Object.entries(s).map(([f,g])=>{let b=En(t,g,f,s);n(v=>{u.includes(v)&&E("Duplicate key on x-for",e),u.push(v)},{scope:{index:f,...b}}),l.push(b)});else for(let f=0;f<s.length;f++){let g=En(t,s[f],f,s);n(b=>{u.includes(b)&&E("Duplicate key on x-for",e),u.push(b)},{scope:{index:f,...g}}),l.push(g)}let p=[],m=[],w=[],$=[];for(let f=0;f<c.length;f++){let g=c[f];u.indexOf(g)===-1&&w.push(g)}c=c.filter(f=>!w.includes(f));let we="template";for(let f=0;f<u.length;f++){let g=u[f],b=c.indexOf(g);if(b===-1)c.splice(f,0,g),p.push([we,f]);else if(b!==f){let v=c.splice(f,1)[0],O=c.splice(b-1,1)[0];c.splice(f,0,O),c.splice(b,0,v),m.push([v,O])}else $.push(g);we=g}for(let f=0;f<w.length;f++){let g=w[f];a[g]._x_effects&&a[g]._x_effects.forEach(Ee),a[g].remove(),a[g]=null,delete a[g]}for(let f=0;f<m.length;f++){let[g,b]=m[f],v=a[g],O=a[b],Q=document.createElement("div");_(()=>{O||E('x-for ":key" is undefined or invalid',o,b,a),O.after(Q),v.after(O),O._x_currentIfEl&&O.after(O._x_currentIfEl),Q.before(v),v._x_currentIfEl&&v.after(v._x_currentIfEl),Q.remove()}),O._x_refreshXForScope(l[u.indexOf(b)])}for(let f=0;f<p.length;f++){let[g,b]=p[f],v=g==="template"?o:a[g];v._x_currentIfEl&&(v=v._x_currentIfEl);let O=l[b],Q=u[b],ae=document.importNode(o.content,!0).firstElementChild,Vt=R(O);P(ae,Vt,o),ae._x_refreshXForScope=Sn=>{Object.entries(Sn).forEach(([An,On])=>{Vt[An]=On})},_(()=>{v.after(ae),A(()=>S(ae))()}),typeof Q=="object"&&E("x-for key cannot be an object, it must be a string or an integer",o),a[Q]=ae}for(let f=0;f<$.length;f++)a[$[f]]._x_refreshXForScope(l[u.indexOf($[f])]);o._x_prevKeys=u})}function Xi(e){let t=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,r=/^\s*\(|\)\s*$/g,n=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,i=e.match(n);if(!i)return;let o={};o.items=i[2].trim();let s=i[1].replace(r,"").trim(),a=s.match(t);return a?(o.item=s.replace(t,"").trim(),o.index=a[1].trim(),a[2]&&(o.collection=a[2].trim())):o.item=s,o}function En(e,t,r,n){let i={};return/^\[.*\]$/.test(e.item)&&Array.isArray(t)?e.item.replace("[","").replace("]","").split(",").map(s=>s.trim()).forEach((s,a)=>{i[s]=t[a]}):/^\{.*\}$/.test(e.item)&&!Array.isArray(t)&&typeof t=="object"?e.item.replace("{","").replace("}","").split(",").map(s=>s.trim()).forEach(s=>{i[s]=t[s]}):i[e.item]=t,e.index&&(i[e.index]=r),e.collection&&(i[e.collection]=n),i}function Zi(e){return!Array.isArray(e)&&!isNaN(e)}function vn(){}vn.inline=(e,{expression:t},{cleanup:r})=>{let n=J(e);n._x_refs||(n._x_refs={}),n._x_refs[t]=e,r(()=>delete n._x_refs[t])};d("ref",vn);d("if",(e,{expression:t},{effect:r,cleanup:n})=>{e.tagName.toLowerCase()!=="template"&&E("x-if can only be used on a <template> tag",e);let i=x(e,t),o=()=>{if(e._x_currentIfEl)return e._x_currentIfEl;let a=e.content.cloneNode(!0).firstElementChild;return P(a,{},e),_(()=>{e.after(a),A(()=>S(a))()}),e._x_currentIfEl=a,e._x_undoIf=()=>{T(a,c=>{c._x_effects&&c._x_effects.forEach(Ee)}),a.remove(),delete e._x_currentIfEl},a},s=()=>{e._x_undoIf&&(e._x_undoIf(),delete e._x_undoIf)};r(()=>i(a=>{a?o():s()})),n(()=>e._x_undoIf&&e._x_undoIf())});d("id",(e,{expression:t},{evaluate:r})=>{r(t).forEach(i=>mn(e,i))});K((e,t)=>{e._x_ids&&(t._x_ids=e._x_ids)});re(Ie("@",ke(C("on:"))));d("on",A((e,{value:t,modifiers:r,expression:n},{cleanup:i})=>{let o=n?x(e,n):()=>{};e.tagName.toLowerCase()==="template"&&(e._x_forwardEvents||(e._x_forwardEvents=[]),e._x_forwardEvents.includes(t)||e._x_forwardEvents.push(t));let s=se(e,t,r,a=>{o(()=>{},{scope:{$event:a},params:[a]})});i(()=>s())}));tt("Collapse","collapse","collapse");tt("Intersect","intersect","intersect");tt("Focus","trap","focus");tt("Mask","mask","mask");function tt(e,t,r){d(t,n=>E(`You can't use [x-${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}B.setEvaluator(gt);B.setReactivityEngine({reactive:Qe,effect:en,release:tn,raw:h});var Ht=B;window.Alpine=Ht;queueMicrotask(()=>{Ht.start()});})();
================================================
FILE: app/assets/javascripts/solid_queue_dashboard/application.js
================================================
document.addEventListener('DOMContentLoaded', function() {
// Handle clickable rows
document.body.addEventListener('mouseup', function(event) {
const target = event.target.closest('[data-href]');
if (target) {
event.preventDefault();
const href = target.getAttribute('data-href');
if (event.metaKey || event.ctrlKey || event.button === 1) {
window.open(href, '_blank', 'noopener, noreferrer');
} else if (event.button === 0) {
window.location.href = href;
}
}
});
// Format dates
document.querySelectorAll('[data-date]').forEach(function (element) {
const dateString = element.textContent.trim();
const date = new Date(dateString);
if (isNaN(date.getTime())) return;
const formattedDate = new Intl.DateTimeFormat('en-US', {
year: undefined,
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).format(date);
element.textContent = formattedDate;
element.setAttribute('title', dateString); // Add original date as title
});
// Handle auto-submit elements
document.querySelectorAll('[data-auto-submit]').forEach(function (element) {
element.addEventListener('change', function () {
const form = element.closest('form');
if (form) form.submit();
});
});
// Auto-refresh functionality for dashboard page
const searchParams = new URLSearchParams(window.location.search);
const refreshInterval = parseInt(searchParams.get('auto_refresh_period'));
if (refreshInterval) {
setInterval(refreshHomePage, refreshInterval * 1000);
}
function refreshHomePage() {
// TODO: Implement a smart refresh strategy using Fetch or Turbo
window.location.reload();
}
});
================================================
FILE: app/assets/stylesheets/solid_queue_dashboard/application.css
================================================
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
/*
! tailwindcss v3.4.13 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
letter-spacing: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
:root {
--background: 0 0% 98%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-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: 0.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%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 95%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 11%;
--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%;
}
* {
border-color: hsl(var(--border));
}
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-feature-settings: "rlig" 1, "calt" 1;
}
[data-href] {
cursor: pointer;
}
.link {
text-decoration-line: underline;
}
.link:hover {
opacity: 0.75;
}
/*
Label
*/
.label {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
line-height: 1;
}
.peer:disabled ~ .label {
cursor: not-allowed;
opacity: 0.7;
}
/*
Select
*/
.select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
border-radius: 0px;
padding-top: 0.5rem;
padding-right: 0.75rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
font-size: 1rem;
line-height: 1.5rem;
--tw-shadow: 0 0 #0000;
}
.select:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
border-color: #2563eb;
}
.select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.select:where([size]:not([size="1"])) {
background-image: initial;
background-position: initial;
background-repeat: unset;
background-size: initial;
padding-right: 0.75rem;
-webkit-print-color-adjust: unset;
print-color-adjust: unset;
}
.select {
height: 2.5rem;
width: 12rem;
border-radius: calc(var(--radius) - 2px);
border-width: 1px;
border-color: hsl(var(--input));
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
padding-left: 0.75rem;
padding-right: 0.75rem;
font-size: 0.875rem;
line-height: 1.25rem;
color: hsl(var(--foreground));
--tw-ring-offset-color: hsl(var(--background));
}
.select::file-selector-button {
border-width: 0px;
background-color: transparent;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
}
.select::-moz-placeholder {
color: hsl(var(--muted-foreground));
}
.select::placeholder {
color: hsl(var(--muted-foreground));
}
.select:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
--tw-ring-color: hsl(var(--ring));
--tw-ring-offset-width: 2px;
}
.select:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.select:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
/*
Badge
*/
.badge {
display: inline-flex;
align-items: center;
-moz-column-gap: 0.375rem;
column-gap: 0.375rem;
border-radius: calc(var(--radius) - 2px);
border-width: 1px;
padding-left: 0.375rem;
padding-right: 0.375rem;
padding-top: 0.125rem;
padding-bottom: 0.125rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
}
@media (min-width: 640px) {
.badge {
font-size: 0.75rem;
line-height: 1.25rem;
}
}
@media (forced-colors: active) {
.badge {
outline-style: solid;
}
}
.badge-primary {
border-color: transparent;
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.badge-primary:hover {
background-color: hsl(var(--primary) / 0.8);
}
.badge-secondary {
border-color: transparent;
background-color: hsl(var(--secondary));
color: hsl(var(--secondary-foreground));
}
.badge-secondary:hover {
background-color: hsl(var(--secondary) / 0.8);
}
.badge-destructive {
border-color: transparent;
background-color: hsl(var(--destructive) / 0.15);
color: hsl(var(--destructive));
}
.badge-destructive:is(.dark *) {
background-color: hsl(var(--destructive) / 0.1);
color: hsl(var(--destructive-foreground));
}
.badge-outline {
color: hsl(var(--foreground));
}
.badge-red {
border-color: transparent;
background-color: rgb(239 68 68 / 0.15);
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity));
}
.badge-red:is(.dark *) {
background-color: rgb(239 68 68 / 0.1);
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity));
}
.badge-amber {
border-color: transparent;
background-color: rgb(251 191 36 / 0.2);
--tw-text-opacity: 1;
color: rgb(180 83 9 / var(--tw-text-opacity));
}
.badge-amber:is(.dark *) {
background-color: rgb(251 191 36 / 0.1);
--tw-text-opacity: 1;
color: rgb(251 191 36 / var(--tw-text-opacity));
}
.badge-yellow {
border-color: transparent;
background-color: rgb(250 204 21 / 0.2);
--tw-text-opacity: 1;
color: rgb(161 98 7 / var(--tw-text-opacity));
}
.badge-yellow:is(.dark *) {
background-color: rgb(250 204 21 / 0.1);
--tw-text-opacity: 1;
color: rgb(253 224 71 / var(--tw-text-opacity));
}
.badge-green {
border-color: transparent;
background-color: rgb(34 197 94 / 0.15);
--tw-text-opacity: 1;
color: rgb(21 128 61 / var(--tw-text-opacity));
}
.badge-green:is(.dark *) {
background-color: rgb(34 197 94 / 0.1);
--tw-text-opacity: 1;
color: rgb(74 222 128 / var(--tw-text-opacity));
}
.badge-emerald {
border-color: transparent;
background-color: rgb(16 185 129 / 0.15);
--tw-text-opacity: 1;
color: rgb(4 120 87 / var(--tw-text-opacity));
}
.badge-emerald:is(.dark *) {
background-color: rgb(16 185 129 / 0.1);
--tw-text-opacity: 1;
color: rgb(52 211 153 / var(--tw-text-opacity));
}
.badge-sky {
border-color: transparent;
background-color: rgb(14 165 233 / 0.15);
--tw-text-opacity: 1;
color: rgb(3 105 161 / var(--tw-text-opacity));
}
.badge-sky:is(.dark *) {
background-color: rgb(14 165 233 / 0.1);
--tw-text-opacity: 1;
color: rgb(125 211 252 / var(--tw-text-opacity));
}
.badge-blue {
border-color: transparent;
background-color: rgb(59 130 246 / 0.15);
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
.badge-blue:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(96 165 250 / var(--tw-text-opacity));
}
.badge-indigo {
border-color: transparent;
background-color: rgb(99 102 241 / 0.15);
--tw-text-opacity: 1;
color: rgb(67 56 202 / var(--tw-text-opacity));
}
.badge-indigo:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(129 140 248 / var(--tw-text-opacity));
}
.badge-purple {
border-color: transparent;
background-color: rgb(168 85 247 / 0.15);
--tw-text-opacity: 1;
color: rgb(126 34 206 / var(--tw-text-opacity));
}
.badge-purple:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(192 132 252 / var(--tw-text-opacity));
}
.badge-zinc {
border-color: transparent;
background-color: rgb(82 82 91 / 0.075);
--tw-text-opacity: 1;
color: rgb(63 63 70 / var(--tw-text-opacity));
}
.badge-zinc:is(.dark *) {
background-color: rgb(255 255 255 / 0.05);
--tw-text-opacity: 1;
color: rgb(161 161 170 / var(--tw-text-opacity));
}
/*
Circle
*/
.circle {
display: inline-flex;
width: 0.5rem;
height: 0.5rem;
align-items: center;
justify-content: center;
border-radius: 9999px;
}
.circle-red {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.circle-amber {
--tw-bg-opacity: 1;
background-color: rgb(251 191 36 / var(--tw-bg-opacity));
}
.circle-yellow {
--tw-bg-opacity: 1;
background-color: rgb(250 204 21 / var(--tw-bg-opacity));
}
.circle-green {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
}
.circle-sky {
--tw-bg-opacity: 1;
background-color: rgb(14 165 233 / var(--tw-bg-opacity));
}
.circle-blue {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
.circle-indigo {
--tw-bg-opacity: 1;
background-color: rgb(99 102 241 / var(--tw-bg-opacity));
}
.circle-purple {
--tw-bg-opacity: 1;
background-color: rgb(168 85 247 / var(--tw-bg-opacity));
}
.circle-zinc {
--tw-bg-opacity: 1;
background-color: rgb(161 161 170 / var(--tw-bg-opacity));
}
/*
Alert
*/
.alert {
position: relative;
width: 100%;
border-radius: var(--radius);
border-width: 1px;
padding: 1rem;
}
.alert>svg+div {
--tw-translate-y: -3px;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.alert>svg {
position: absolute;
left: 1rem;
top: 1rem;
color: hsl(var(--foreground));
}
.alert>svg~* {
padding-left: 1.75rem;
}
.alert-default {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
.alert-red {
border-color: rgb(239 68 68 / 0.5);
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity));
}
.alert-red:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(239 68 68 / var(--tw-border-opacity));
--tw-text-opacity: 1;
color: rgb(252 165 165 / var(--tw-text-opacity));
}
.alert-red>svg {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.alert-green {
border-color: rgb(34 197 94 / 0.5);
--tw-text-opacity: 1;
color: rgb(21 128 61 / var(--tw-text-opacity));
}
.alert-green:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(34 197 94 / var(--tw-border-opacity));
--tw-text-opacity: 1;
color: rgb(134 239 172 / var(--tw-text-opacity));
}
.alert-green>svg {
--tw-text-opacity: 1;
color: rgb(34 197 94 / var(--tw-text-opacity));
}
.alert-yellow {
border-color: rgb(234 179 8 / 0.5);
--tw-text-opacity: 1;
color: rgb(161 98 7 / var(--tw-text-opacity));
}
.alert-yellow:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(234 179 8 / var(--tw-border-opacity));
--tw-text-opacity: 1;
color: rgb(253 224 71 / var(--tw-text-opacity));
}
.alert-yellow>svg {
--tw-text-opacity: 1;
color: rgb(234 179 8 / var(--tw-text-opacity));
}
.alert-blue {
border-color: rgb(59 130 246 / 0.5);
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
.alert-blue:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity));
--tw-text-opacity: 1;
color: rgb(147 197 253 / var(--tw-text-opacity));
}
.alert-blue>svg {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
.alert-title {
margin-bottom: 0.25rem;
font-weight: 500;
line-height: 1;
letter-spacing: -0.025em;
}
.alert-description {
font-size: 0.875rem;
line-height: 1.25rem;
}
.alert-description p {
line-height: 1.625;
}
/*
Button
*/
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: calc(var(--radius) - 2px);
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
--tw-ring-offset-color: hsl(var(--background));
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.btn:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
--tw-ring-color: hsl(var(--ring));
--tw-ring-offset-width: 2px;
}
.btn:disabled {
pointer-events: none;
opacity: 0.5;
}
.btn-outline {
border-width: 1px;
border-color: hsl(var(--input));
background-color: hsl(var(--background));
}
.btn-outline:hover {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.btn-secondary {
background-color: hsl(var(--secondary));
color: hsl(var(--secondary-foreground));
}
.btn-secondary:hover {
background-color: hsl(var(--secondary) / 0.8);
}
.btn-xs {
height: 1.75rem;
gap: 0.25rem;
border-radius: calc(var(--radius) - 2px);
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.btn-sm {
height: 2.25rem;
gap: 0.375rem;
border-radius: calc(var(--radius) - 2px);
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.btn-md {
height: 2.5rem;
gap: 0.375rem;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.btn-icon {
height: 2.5rem;
width: 2.5rem;
}
/*
Info Line
*/
.info-line {
display: flex;
align-items: center;
gap: 1rem;
}
.info-line-label {
white-space: nowrap;
--tw-text-opacity: 1;
color: rgb(113 113 122 / var(--tw-text-opacity));
}
.info-line-separator {
height: 1px;
flex: 1 1 0%;
--tw-translate-y: 1px;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
background-color: rgb(9 9 11 / 0.1);
}
.info-line-separator:is(.dark *) {
border-color: rgb(255 255 255 / 0.1);
}
.info-line-value {
text-align: right;
font-weight: 500;
--tw-text-opacity: 1;
color: rgb(9 9 11 / var(--tw-text-opacity));
}
.info-line-value:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
/*
Card
*/
.card {
border-radius: var(--radius);
border-width: 1px;
background-color: hsl(var(--card));
color: hsl(var(--card-foreground));
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.card-header {
display: flex;
flex-direction: column;
}
.card-header > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.375rem * var(--tw-space-y-reverse));
}
.card-header {
padding: 1.5rem;
}
.card-title {
font-size: 1.5rem;
line-height: 2rem;
font-weight: 600;
line-height: 1;
letter-spacing: -0.025em;
}
.card-description {
font-size: 0.875rem;
line-height: 1.25rem;
color: hsl(var(--muted-foreground));
}
.card-content {
padding: 1.5rem;
padding-top: 0px;
}
.card-footer {
display: flex;
align-items: center;
padding: 1.5rem;
padding-top: 0px;
}
/*
Navbar
*/
.navbar {
display: flex;
align-items: center;
gap: 1rem;
background-color: hsl(var(--background));
padding-top: 1rem;
padding-bottom: 1rem;
}
.navbar-section {
display: flex;
gap: 0.375rem;
}
.navbar-item {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: calc(var(--radius) - 2px);
padding-left: 0.75rem;
padding-right: 0.75rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.navbar-item:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
--tw-ring-color: hsl(var(--ring));
--tw-ring-offset-width: 2px;
}
.navbar-item-default {
color: hsl(var(--foreground));
}
.navbar-item-default:hover {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.navbar-item-current {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
/*
Table
*/
.table-wrapper {
position: relative;
width: 100%;
overflow: auto;
}
.table {
width: 100%;
caption-side: bottom;
font-size: 0.875rem;
line-height: 1.25rem;
}
.table-header tr {
border-bottom-width: 1px;
}
.table-body tr:last-child {
border-width: 0px;
}
.table-row {
border-bottom-width: 1px;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.table-row:hover {
background-color: hsl(var(--muted) / 0.5);
}
.table-row[data-state="selected"] {
background-color: hsl(var(--muted));
}
.table-head {
height: 3rem;
white-space: nowrap;
padding-left: 1rem;
padding-right: 1rem;
text-align: left;
vertical-align: middle;
font-weight: 500;
color: hsl(var(--muted-foreground));
}
.table-head:has([role=checkbox]) {
padding-right: 0px;
}
.table-cell {
padding: 1rem;
vertical-align: middle;
}
.table-cell:has([role=checkbox]) {
padding-right: 0px;
}
/*
Pagination
*/
.pagination {
display: flex;
justify-content: center;
}
.pagination-content {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.25rem;
}
.pagination-item {
display: inline-block;
}
.pagination-link {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: calc(var(--radius) - 2px);
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
--tw-ring-offset-color: hsl(var(--background));
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.pagination-link:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
--tw-ring-color: hsl(var(--ring));
--tw-ring-offset-width: 2px;
}
.pagination-link:disabled {
pointer-events: none;
opacity: 0.5;
}
.pagination-link-active {
border-width: 1px;
border-color: hsl(var(--input));
background-color: hsl(var(--background));
}
.pagination-link-active:hover {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.pagination-link-inactive:hover {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.pagination-link-default {
height: 2.5rem;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.pagination-previous,
.pagination-next {
height: 2.5rem;
gap: 0.25rem;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.pagination-previous {
padding-left: 0.625rem;
}
.pagination-next {
padding-right: 0.625rem;
}
.pagination-ellipsis {
display: flex;
height: 2.25rem;
width: 2.25rem;
align-items: center;
justify-content: center;
}
.static {
position: static;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.right-6 {
right: 1.5rem;
}
.top-6 {
top: 1.5rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.\!mt-4 {
margin-top: 1rem !important;
}
.-ml-0\.5 {
margin-left: -0.125rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mb-8 {
margin-bottom: 2rem;
}
.ml-0\.5 {
margin-left: 0.125rem;
}
.ml-1 {
margin-left: 0.25rem;
}
.ml-1\.5 {
margin-left: 0.375rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.ml-4 {
margin-left: 1rem;
}
.ml-8 {
margin-left: 2rem;
}
.ml-auto {
margin-left: auto;
}
.mr-0\.5 {
margin-right: 0.125rem;
}
.mr-1 {
margin-right: 0.25rem;
}
.mr-1\.5 {
margin-right: 0.375rem;
}
.mt-0\.5 {
margin-top: 0.125rem;
}
.mt-1 {
margin-top: 0.25rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.mt-4 {
margin-top: 1rem;
}
.mt-6 {
margin-top: 1.5rem;
}
.mt-8 {
margin-top: 2rem;
}
.inline-block {
display: inline-block;
}
.flex {
display: flex;
}
.inline-flex {
display: inline-flex;
}
.table {
display: table;
}
.table-cell {
display: table-cell;
}
.table-row {
display: table-row;
}
.grid {
display: grid;
}
.\!size-4 {
width: 1rem !important;
height: 1rem !important;
}
.size-3 {
width: 0.75rem;
height: 0.75rem;
}
.size-3\.5 {
width: 0.875rem;
height: 0.875rem;
}
.size-4 {
width: 1rem;
height: 1rem;
}
.h-5 {
height: 1.25rem;
}
.w-24 {
width: 6rem;
}
.w-40 {
width: 10rem;
}
.w-5 {
width: 1.25rem;
}
.max-w-\[1920px\] {
max-width: 1920px;
}
.-translate-y-px {
--tw-translate-y: -1px;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.translate-y-px {
--tw-translate-y: 1px;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.list-decimal {
list-style-type: decimal;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-6 {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.\!flex-row {
flex-direction: row !important;
}
.flex-row {
flex-direction: row;
}
.flex-wrap {
flex-wrap: wrap;
}
.items-start {
align-items: flex-start;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.gap-0\.5 {
gap: 0.125rem;
}
.gap-1 {
gap: 0.25rem;
}
.gap-1\.5 {
gap: 0.375rem;
}
.gap-2 {
gap: 0.5rem;
}
.gap-4 {
gap: 1rem;
}
.space-y-1 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
}
.space-y-1\.5 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.375rem * var(--tw-space-y-reverse));
}
.space-y-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
}
.space-y-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
}
.overflow-hidden {
overflow: hidden;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.whitespace-nowrap {
white-space: nowrap;
}
.text-balance {
text-wrap: balance;
}
.border-b {
border-bottom-width: 1px;
}
.border-t {
border-top-width: 1px;
}
.border-t-4 {
border-top-width: 4px;
}
.border-t-amber-500 {
--tw-border-opacity: 1;
border-top-color: rgb(245 158 11 / var(--tw-border-opacity));
}
.border-t-blue-500 {
--tw-border-opacity: 1;
border-top-color: rgb(59 130 246 / var(--tw-border-opacity));
}
.border-t-purple-500 {
--tw-border-opacity: 1;
border-top-color: rgb(168 85 247 / var(--tw-border-opacity));
}
.border-t-red-500 {
--tw-border-opacity: 1;
border-top-color: rgb(239 68 68 / var(--tw-border-opacity));
}
.border-t-sky-500 {
--tw-border-opacity: 1;
border-top-color: rgb(14 165 233 / var(--tw-border-opacity));
}
.border-t-zinc-500 {
--tw-border-opacity: 1;
border-top-color: rgb(113 113 122 / var(--tw-border-opacity));
}
.bg-primary\/7\.5 {
background-color: hsl(var(--primary) / 0.075);
}
.\!p-0 {
padding: 0px !important;
}
.p-0 {
padding: 0px;
}
.p-6 {
padding: 1.5rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.py-12 {
padding-top: 3rem;
padding-bottom: 3rem;
}
.pb-\[15vh\] {
padding-bottom: 15vh;
}
.pl-1 {
padding-left: 0.25rem;
}
.pl-6 {
padding-left: 1.5rem;
}
.pt-5 {
padding-top: 1.25rem;
}
.pt-6 {
padding-top: 1.5rem;
}
.text-center {
text-align: center;
}
.font-mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.font-bold {
font-weight: 700;
}
.font-medium {
font-weight: 500;
}
.font-normal {
font-weight: 400;
}
.capitalize {
text-transform: capitalize;
}
.tracking-tight {
letter-spacing: -0.025em;
}
.tracking-tighter {
letter-spacing: -0.05em;
}
.text-amber-500 {
--tw-text-opacity: 1;
color: rgb(245 158 11 / var(--tw-text-opacity));
}
.text-amber-600 {
--tw-text-opacity: 1;
color: rgb(217 119 6 / var(--tw-text-opacity));
}
.text-black {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
}
.text-foreground {
color: hsl(var(--foreground));
}
.text-muted-foreground {
color: hsl(var(--muted-foreground));
}
.text-muted-foreground\/30 {
color: hsl(var(--muted-foreground) / 0.3);
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.text-red-600 {
--tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity));
}
.text-zinc-900 {
--tw-text-opacity: 1;
color: rgb(24 24 27 / var(--tw-text-opacity));
}
.opacity-50 {
opacity: 0.5;
}
@keyframes enter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0));
}
}
@keyframes exit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0));
}
}
.running {
animation-play-state: running;
}
.marker\:text-sm *::marker {
font-size: 0.875rem;
line-height: 1.25rem;
}
.marker\:text-muted-foreground *::marker {
color: hsl(var(--muted-foreground));
}
.marker\:text-sm::marker {
font-size: 0.875rem;
line-height: 1.25rem;
}
.marker\:text-muted-foreground::marker {
color: hsl(var(--muted-foreground));
}
.hover\:bg-primary\/10:hover {
background-color: hsl(var(--primary) / 0.1);
}
.hover\:text-foreground:hover {
color: hsl(var(--foreground));
}
.hover\:underline:hover {
text-decoration-line: underline;
}
.dark\:border-t-amber-600:is(.dark *) {
--tw-border-opacity: 1;
border-top-color: rgb(217 119 6 / var(--tw-border-opacity));
}
.dark\:border-t-blue-600:is(.dark *) {
--tw-border-opacity: 1;
border-top-color: rgb(37 99 235 / var(--tw-border-opacity));
}
.dark\:border-t-purple-600:is(.dark *) {
--tw-border-opacity: 1;
border-top-color: rgb(147 51 234 / var(--tw-border-opacity));
}
.dark\:border-t-sky-600:is(.dark *) {
--tw-border-opacity: 1;
border-top-color: rgb(2 132 199 / var(--tw-border-opacity));
}
.dark\:border-t-zinc-600:is(.dark *) {
--tw-border-opacity: 1;
border-top-color: rgb(82 82 91 / var(--tw-border-opacity));
}
.dark\:text-red-500:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.dark\:text-white:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:text-zinc-100:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(244 244 245 / var(--tw-text-opacity));
}
@media (min-width: 640px) {
.sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sm\:gap-16 {
gap: 4rem;
}
.sm\:px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.sm\:pb-\[25vh\] {
padding-bottom: 25vh;
}
}
@media (min-width: 768px) {
.md\:gap-20 {
gap: 5rem;
}
}
@media (min-width: 1024px) {
.lg\:px-8 {
padding-left: 2rem;
padding-right: 2rem;
}
}
================================================
FILE: app/assets/stylesheets/solid_queue_dashboard/tailwind.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 98%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-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: 0.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%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 95%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 11%;
--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%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
[data-href] {
@apply cursor-pointer;
}
.link {
@apply underline hover:opacity-75;
}
}
@layer components {
/*
Label
*/
.label {
@apply text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70;
}
/*
Select
*/
.select {
@apply form-select w-48 border border-input bg-white dark:bg-black text-foreground rounded-md h-10 px-3 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
}
/*
Badge
*/
.badge {
@apply border inline-flex items-center gap-x-1.5 rounded-md px-1.5 py-0.5 text-sm/5 font-medium sm:text-xs/5 forced-colors:outline;
}
.badge-primary {
@apply border-transparent bg-primary text-primary-foreground hover:bg-primary/80;
}
.badge-secondary {
@apply border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80;
}
.badge-destructive {
@apply border-transparent bg-destructive/15 text-destructive dark:bg-destructive/10 dark:text-destructive-foreground;
}
.badge-outline {
@apply text-foreground;
}
.badge-red {
@apply border-transparent bg-red-500/15 text-red-700 dark:bg-red-500/10 dark:text-red-400;
}
.badge-orange {
@apply border-transparent bg-orange-500/15 text-orange-700 dark:bg-orange-500/10 dark:text-orange-400;
}
.badge-amber {
@apply border-transparent bg-amber-400/20 text-amber-700 dark:bg-amber-400/10 dark:text-amber-400;
}
.badge-yellow {
@apply border-transparent bg-yellow-400/20 text-yellow-700 dark:bg-yellow-400/10 dark:text-yellow-300;
}
.badge-lime {
@apply border-transparent bg-lime-400/20 text-lime-700 dark:bg-lime-400/10 dark:text-lime-300;
}
.badge-green {
@apply border-transparent bg-green-500/15 text-green-700 dark:bg-green-500/10 dark:text-green-400;
}
.badge-emerald {
@apply border-transparent bg-emerald-500/15 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400;
}
.badge-teal {
@apply border-transparent bg-teal-500/15 text-teal-700 dark:bg-teal-500/10 dark:text-teal-300;
}
.badge-cyan {
@apply border-transparent bg-cyan-400/20 text-cyan-700 dark:bg-cyan-400/10 dark:text-cyan-300;
}
.badge-sky {
@apply border-transparent bg-sky-500/15 text-sky-700 dark:bg-sky-500/10 dark:text-sky-300;
}
.badge-blue {
@apply border-transparent bg-blue-500/15 text-blue-700 dark:text-blue-400;
}
.badge-indigo {
@apply border-transparent bg-indigo-500/15 text-indigo-700 dark:text-indigo-400;
}
.badge-violet {
@apply border-transparent bg-violet-500/15 text-violet-700 dark:text-violet-400;
}
.badge-purple {
@apply border-transparent bg-purple-500/15 text-purple-700 dark:text-purple-400;
}
.badge-fuchsia {
@apply border-transparent bg-fuchsia-400/15 text-fuchsia-700 dark:bg-fuchsia-400/10 dark:text-fuchsia-400;
}
.badge-pink {
@apply border-transparent bg-pink-400/15 text-pink-700 dark:bg-pink-400/10 dark:text-pink-400;
}
.badge-rose {
@apply border-transparent bg-rose-400/15 text-rose-700 dark:bg-rose-400/10 dark:text-rose-400;
}
.badge-zinc {
@apply border-transparent bg-zinc-600/7.5 text-zinc-700 dark:bg-white/5 dark:text-zinc-400;
}
/*
Circle
*/
.circle {
@apply size-2 inline-flex items-center justify-center rounded-full;
}
.circle-primary {
@apply bg-primary;
}
.circle-secondary {
@apply bg-secondary;
}
.circle-destructive {
@apply bg-destructive;
}
.circle-outline {
@apply border border-current bg-background;
}
.circle-red {
@apply bg-red-500;
}
.circle-orange {
@apply bg-orange-500;
}
.circle-amber {
@apply bg-amber-400;
}
.circle-yellow {
@apply bg-yellow-400;
}
.circle-lime {
@apply bg-lime-400;
}
.circle-green {
@apply bg-green-500;
}
.circle-emerald {
@apply bg-emerald-500;
}
.circle-teal {
@apply bg-teal-500;
}
.circle-cyan {
@apply bg-cyan-400;
}
.circle-sky {
@apply bg-sky-500;
}
.circle-blue {
@apply bg-blue-500;
}
.circle-indigo {
@apply bg-indigo-500;
}
.circle-violet {
@apply bg-violet-500;
}
.circle-purple {
@apply bg-purple-500;
}
.circle-fuchsia {
@apply bg-fuchsia-400;
}
.circle-pink {
@apply bg-pink-400;
}
.circle-rose {
@apply bg-rose-400;
}
.circle-zinc {
@apply bg-zinc-400;
}
/*
Alert
*/
.alert {
@apply relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground;
}
.alert-default {
@apply bg-background text-foreground;
}
.alert-destructive {
@apply border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive;
}
.alert-red {
@apply border-red-500/50 text-red-700 dark:border-red-500 dark:text-red-300 [&>svg]:text-red-500;
}
.alert-green {
@apply border-green-500/50 text-green-700 dark:border-green-500 dark:text-green-300 [&>svg]:text-green-500;
}
.alert-yellow {
@apply border-yellow-500/50 text-yellow-700 dark:border-yellow-500 dark:text-yellow-300 [&>svg]:text-yellow-500;
}
.alert-blue {
@apply border-blue-500/50 text-blue-700 dark:border-blue-500 dark:text-blue-300 [&>svg]:text-blue-500;
}
.alert-purple {
@apply border-purple-500/50 text-purple-700 dark:border-purple-500 dark:text-purple-300 [&>svg]:text-purple-500;
}
.alert-pink {
@apply border-pink-500/50 text-pink-700 dark:border-pink-500 dark:text-pink-300 [&>svg]:text-pink-500;
}
.alert-title {
@apply mb-1 font-medium leading-none tracking-tight;
}
.alert-description {
@apply text-sm [&_p]:leading-relaxed;
}
/*
Button
*/
.btn {
@apply inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
}
.btn-default {
@apply bg-primary text-primary-foreground hover:bg-primary/90;
}
.btn-destructive {
@apply bg-destructive text-destructive-foreground hover:bg-destructive/90;
}
.btn-outline {
@apply border border-input bg-background hover:bg-accent hover:text-accent-foreground;
}
.btn-secondary {
@apply bg-secondary text-secondary-foreground hover:bg-secondary/80;
}
.btn-ghost {
@apply hover:bg-accent hover:text-accent-foreground;
}
.btn-link {
@apply text-primary underline-offset-4 hover:underline;
}
.btn-xs {
@apply h-7 rounded-md px-2 gap-1;
}
.btn-sm {
@apply h-9 rounded-md px-3 gap-1.5;
}
.btn-md {
@apply h-10 px-4 py-2 gap-1.5;
}
.btn-lg {
@apply h-11 rounded-md px-8 gap-2;
}
.btn-icon {
@apply h-10 w-10;
}
/*
Info Line
*/
.info-line {
@apply flex items-center gap-4;
}
.info-line-label {
@apply text-zinc-500 whitespace-nowrap;
}
.info-line-separator {
@apply h-px flex-1 bg-zinc-950/10 dark:border-white/10 translate-y-px;
}
.info-line-value {
@apply font-medium text-right text-zinc-950 dark:text-white;
}
/*
Card
*/
.card {
@apply rounded-lg border bg-card text-card-foreground shadow-sm;
}
.card-header {
@apply flex flex-col space-y-1.5 p-6;
}
.card-title {
@apply text-2xl font-semibold leading-none tracking-tight;
}
.card-description {
@apply text-sm text-muted-foreground;
}
.card-content {
@apply p-6 pt-0;
}
.card-footer {
@apply flex items-center p-6 pt-0;
}
/*
Navbar
*/
.navbar {
@apply flex items-center gap-4 bg-background py-4;
}
.navbar-section {
@apply flex gap-1.5;
}
.navbar-item {
@apply inline-flex items-center rounded-md justify-center px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
}
.navbar-item-default {
@apply text-foreground hover:bg-accent hover:text-accent-foreground;
}
.navbar-item-current {
@apply bg-primary text-primary-foreground;
}
/*
Table
*/
.table-wrapper {
@apply relative w-full overflow-auto;
}
.table {
@apply w-full caption-bottom text-sm;
}
.table-header {
@apply [&_tr]:border-b;
}
.table-body {
@apply [&_tr:last-child]:border-0;
}
.table-footer {
@apply border-t bg-muted/50 font-medium [&>tr]:last:border-b-0;
}
.table-row {
@apply border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted;
}
.table-head {
@apply h-12 px-4 text-left align-middle font-medium text-muted-foreground whitespace-nowrap [&:has([role=checkbox])]:pr-0;
}
.table-cell {
@apply p-4 align-middle [&:has([role=checkbox])]:pr-0;
}
.table-caption {
@apply mt-4 text-sm text-muted-foreground;
}
/*
Pagination
*/
.pagination {
@apply flex justify-center;
}
.pagination-content {
@apply flex flex-row items-center gap-1;
}
.pagination-item {
@apply inline-block;
}
.pagination-link {
@apply inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
}
.pagination-link-active {
@apply border border-input bg-background hover:bg-accent hover:text-accent-foreground;
}
.pagination-link-inactive {
@apply hover:bg-accent hover:text-accent-foreground;
}
.pagination-link-icon {
@apply h-9 w-9;
}
.pagination-link-default {
@apply h-10 px-4 py-2;
}
.pagination-previous,
.pagination-next {
@apply h-10 px-4 py-2 gap-1;
}
.pagination-previous {
@apply pl-2.5;
}
.pagination-next {
@apply pr-2.5;
}
.pagination-ellipsis {
@apply flex h-9 w-9 items-center justify-center;
}
}
================================================
FILE: app/controllers/solid_queue_dashboard/appearance_controller.rb
================================================
module SolidQueueDashboard
class AppearanceController < ApplicationController
def toggle
cookies[:dark_mode] = cookies[:dark_mode] == "true" ? "false" : "true"
redirect_to request.referer
end
end
end
================================================
FILE: app/controllers/solid_queue_dashboard/application_controller.rb
================================================
module SolidQueueDashboard
class ApplicationController < ActionController::Base
include SolidQueueDashboard::PaginationHelper
layout "solid_queue_dashboard/application"
end
end
================================================
FILE: app/controllers/solid_queue_dashboard/dashboard_controller.rb
================================================
module SolidQueueDashboard
class DashboardController < ApplicationController
def index
@jobs = SolidQueueDashboard.decorate(SolidQueue::Job.all)
load_charts
end
private
def load_charts
case params[:chart_period] || "30m"
when "15m"
n = 1
last = 15
when "30m"
n = 1
last = 30
when "1h"
n = 2
last = 30
when "3h"
n = 6
last = 30
when "6h"
n = 12
last = 30
when "12h"
n = 20
last = 36
when "1d"
n = 30
last = 48
when "3d"
n = 90 # 1.5 hours
last = 48
when "1w"
n = 180 # 3 hours
last = 56
else
n = 1
last = 30
end
@charts = [
{
name: "Enqueued",
data: SolidQueue::Job.group_by_minute(:created_at, last: last, n: n).count,
color: "#A1A1AB"
},
{
name: "Finished",
data: @jobs.finished.group_by_minute(:finished_at, last: last, n: n).count,
color: "#23C55E"
},
{
name: "Retried",
data: @jobs.retried.group_by_minute(:finished_at, last: last, n: n).count,
color: "#FBBF26"
},
{
name: "Failed",
data: SolidQueue::FailedExecution.group_by_minute(:created_at, last: last, n: n).count,
color: "#F04444"
}
]
end
end
end
================================================
FILE: app/controllers/solid_queue_dashboard/jobs_controller.rb
================================================
module SolidQueueDashboard
class JobsController < ApplicationController
before_action :set_jobs, only: [ :index ]
before_action :set_job, only: [ :show, :retry ]
def index
@job_queue_names = SolidQueueDashboard.job_queue_names
@job_class_names = SolidQueueDashboard.job_class_names
end
def show
@job_history = SolidQueueDashboard.decorate(
SolidQueue::Job
.where.not(id: @job.id)
.where(class_name: @job.class_name)
.order(id: :desc)
.limit(10)
)
end
def retry
@job.retry
redirect_to @job, notice: "Job has been scheduled for retry"
end
private
def set_jobs
jobs = SolidQueue::Job.order(id: :desc)
jobs = SolidQueueDashboard.decorate(jobs).with_status(params[:status]) if params[:status].present?
jobs = jobs.where(class_name: params[:class_name]) if params[:class_name].present?
jobs = jobs.where(queue_name: params[:queue_name]) if params[:queue_name].present?
@pagination = paginate(jobs, page: params[:page].to_i, per_page: params[:per_page].to_i)
@jobs = SolidQueueDashboard.decorate(@pagination[:records])
end
def set_job
@job = SolidQueueDashboard.decorate(SolidQueue::Job.find(params[:id]))
end
end
end
================================================
FILE: app/controllers/solid_queue_dashboard/processes_controller.rb
================================================
module SolidQueueDashboard
class ProcessesController < ApplicationController
before_action :set_processes, only: [ :index ]
before_action :set_process, only: [ :show ]
def index
@process_kinds = SolidQueue::Process.distinct.pluck(:kind)
@process_hostnames = SolidQueue::Process.distinct.pluck(:hostname)
end
def show
end
private
def set_processes
@processes = SolidQueue::Process.all
@processes = @processes.where(kind: params[:kind]) if params[:kind].present?
@processes = @processes.where(hostname: params[:hostname]) if params[:hostname].present?
@processes = @processes.order(id: :desc)
@processes = SolidQueueDashboard.decorate(@processes)
end
def set_process
@process = SolidQueueDashboard.decorate(SolidQueue::Process.find(params[:id]))
end
end
end
================================================
FILE: app/controllers/solid_queue_dashboard/recurring_tasks_controller.rb
================================================
module SolidQueueDashboard
class RecurringTasksController < ApplicationController
before_action :set_recurring_tasks, only: [ :index ]
before_action :set_recurring_task, only: [ :show, :enqueue ]
def index
end
def show
end
def enqueue
@recurring_task.enqueue(at: Time.current)
redirect_to recurring_tasks_path, notice: "Recurring task enqueued"
end
private
def set_recurring_tasks
@recurring_tasks = SolidQueueDashboard.decorate(SolidQueue::RecurringTask.all)
@recurring_tasks = @recurring_tasks.with_type(params[:type]) if params[:type].present?
@recurring_tasks = @recurring_tasks.order(id: :desc)
@recurring_tasks = SolidQueueDashboard.decorate(@recurring_tasks)
end
def set_recurring_task
@recurring_task = SolidQueueDashboard.decorate(SolidQueue::RecurringTask.find(params[:id]))
end
end
end
================================================
FILE: app/controllers/solid_queue_dashboard/stats_controller.rb
================================================
module SolidQueueDashboard
class StatsController < ApplicationController
def index
@jobs = SolidQueueDashboard.decorate(SolidQueue::Job.all)
@job_class_names = SolidQueueDashboard.job_class_names
end
end
end
================================================
FILE: app/helpers/solid_queue_dashboard/appearance_helper.rb
================================================
module SolidQueueDashboard
module AppearanceHelper
def dark_mode?
cookies[:dark_mode] == "true"
end
end
end
================================================
FILE: app/helpers/solid_queue_dashboard/application_helper.rb
================================================
module SolidQueueDashboard
module ApplicationHelper
def empty_value
tag.span("—", class: "text-muted-foreground/30")
end
end
end
================================================
FILE: app/helpers/solid_queue_dashboard/icons_helper.rb
================================================
module SolidQueueDashboard
module IconsHelper
def icon_refresh_cw(options = {})
svg_options = {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}.merge(options)
tag.svg(**svg_options) do
concat tag.path(d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8")
concat tag.path(d: "M21 3v5h-5")
concat tag.path(d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16")
concat tag.path(d: "M8 16H3v5")
end
end
def icon_triangle_alert(options = {})
svg_options = {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}.merge(options)
tag.svg(**svg_options) do
concat tag.path(d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3")
concat tag.path(d: "M12 9v4")
concat tag.path(d: "M12 17h.01")
end
end
def icon_server(options = {})
svg_options = {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}.merge(options)
tag.svg(**svg_options) do
concat tag.rect(width: "20", height: "8", x: "2", y: "2", rx: "2", ry: "2")
concat tag.rect(width: "20", height: "8", x: "2", y: "14", rx: "2", ry: "2")
concat tag.line(x1: "6", x2: "6.01", y1: "6", y2: "6")
concat tag.line(x1: "6", x2: "6.01", y1: "18", y2: "18")
end
end
def icon_layout_dashboard(options = {})
svg_options = {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}.merge(options)
tag.svg(**svg_options) do
concat tag.rect(width: "7", height: "9", x: "3", y: "3", rx: "1")
concat tag.rect(width: "7", height: "5", x: "14", y: "3", rx: "1")
concat tag.rect(width: "7", height: "9", x: "14", y: "12", rx: "1")
concat tag.rect(width: "7", height: "5", x: "3", y: "16", rx: "1")
end
end
def icon_logs(options = {})
svg_options = {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}.merge(options)
tag.svg(**svg_options) do
concat tag.path(d: "M13 12h8")
concat tag.path(d: "M13 18h8")
concat tag.path(d: "M13 6h8")
concat tag.path(d: "M3 12h1")
concat tag.path(d: "M3 18h1")
concat tag.path(d: "M3 6h1")
concat tag.path(d: "M8 12h1")
concat tag.path(d: "M8 18h1")
concat tag.path(d: "M8 6h1")
end
end
def icon_clock(options = {})
svg_options = {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}.merge(options)
tag.svg(**svg_options) do
concat tag.circle(cx: "12", cy: "12", r: "10")
concat tag.polyline(points: "12 6 12 12 16 14")
end
end
def icon_github(options = {})
svg_options = {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}.merge(options)
tag.svg(**svg_options) do
concat tag.path(d: "M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4")
concat tag.path(d: "M9 18c-4.51 2-5-2-7-2")
end
end
def icon_x(options = {})
svg_options = {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}.merge(options)
tag.svg(**svg_options) do
concat tag.path(d: "M18 6 6 18")
concat tag.path(d: "m6 6 12 12")
end
end
def icon_moon(options = {})
svg_options = {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}.merge(options)
tag.svg(**svg_options) do
concat tag.path(d: "M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z")
end
end
def icon_sun(options = {})
svg_options = {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}.merge(options)
tag.svg(**svg_options) do
concat tag.circle(cx: "12", cy: "12", r: "4")
concat tag.path(d: "M12 2v2")
concat tag.path(d: "M12 20v2")
concat tag.path(d: "m4.93 4.93 1.41 1.41")
concat tag.path(d: "m17.66 17.66 1.41 1.41")
concat tag.path(d: "M2 12h2")
concat tag.path(d: "M20 12h2")
concat tag.path(d: "m6.34 17.66-1.41 1.41")
concat tag.path(d: "m19.07 4.93-1.41 1.41")
end
end
def icon_play(options = {})
svg_options = {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}.merge(options)
tag.svg(**svg_options) do
concat tag.polygon(points: "6 3 20 12 6 21 6 3")
end
end
def icon_chart_scatter(options = {})
svg_options = {
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}.merge(options)
tag.svg(**svg_options) do
concat tag.circle(cx: "7.5", cy: "7.5", r: ".5", fill: "currentColor")
concat tag.circle(cx: "18.5", cy: "5.5", r: ".5", fill: "currentColor")
concat tag.circle(cx: "11.5", cy: "11.5", r: ".5", fill: "currentColor")
concat tag.circle(cx: "7.5", cy: "16.5", r: ".5", fill: "currentColor")
concat tag.circle(cx: "17.5", cy: "14.5", r: ".5", fill: "currentColor")
concat tag.path(d: "M3 3v16a2 2 0 0 0 2 2h16")
end
end
end
end
================================================
FILE: app/helpers/solid_queue_dashboard/jobs_helper.rb
================================================
module SolidQueueDashboard
module JobsHelper
def job_status_circle(status, options = {})
options[:class] = [ "circle", job_status_circle_class(status), options[:class] ].compact_blank.join(" ")
tag.span("", **options)
end
def job_status_circle_class(status)
{
"green": "circle-green",
"amber": "circle-amber",
"red": "circle-red",
"blue": "circle-blue",
"sky": "circle-sky",
"zinc": "circle-zinc",
"indigo": "circle-indigo",
"purple": "circle-purple"
}[Job::STATUS_COLORS[status]&.to_sym || :zinc]
end
def job_status_badge(status, options = {})
options[:class] = [ "badge", job_status_badge_class(status), options[:class] ].compact_blank.join(" ")
tag.span(status.to_s.titleize, **options)
end
def job_status_badge_class(status)
{
"green": "badge-green",
"amber": "badge-amber",
"red": "badge-red",
"blue": "badge-blue",
"sky": "badge-sky",
"zinc": "badge-zinc",
"indigo": "badge-indigo",
"purple": "badge-purple"
}[Job::STATUS_COLORS[status]&.to_sym || :zinc]
end
def format_failure_rate(failure_rate, options = {})
badge_variant = case failure_rate
when 0..1
"badge-emerald"
when 1..5
"badge-amber"
else
"badge-red"
end
options[:class] = [ "badge", badge_variant, options[:class] ].compact_blank.join(" ")
tag.span(number_to_percentage(failure_rate, precision: 2, strip_insignificant_zeros: true), **options)
end
def any_jobs_filters?
params[:class_name].present? || params[:status].present? || params[:queue_name].present?
end
end
end
================================================
FILE: app/helpers/solid_queue_dashboard/pagination_helper.rb
================================================
module SolidQueueDashboard
module PaginationHelper
def paginate(scope, page:, per_page:)
page = [ page.to_i, 1 ].max
per_page = per_page.zero? ? 25 : [ per_page.to_i, 1 ].max
offset = (page - 1) * per_page
records = scope.offset(offset).limit(per_page)
total_count = scope.count
total_pages = (total_count.to_f / per_page).ceil
{
records: records,
current_page: page,
per_page: per_page,
total_pages: total_pages,
total_count: total_count
}
end
def page_range(current_page, total_pages, window: 2)
if total_pages <= 7
(1..total_pages).to_a
else
[
1,
(current_page - window..current_page + window).to_a,
total_pages
].flatten.uniq.sort.reject { |p| p < 1 || p > total_pages }.tap do |range|
range.each_cons(2) do |a, b|
range.insert(range.index(b), :gap) if b - a > 1
end
end
end
end
end
end
================================================
FILE: app/helpers/solid_queue_dashboard/processes_helper.rb
================================================
module SolidQueueDashboard
module ProcessesHelper
def process_kind_circle(kind, options = {})
options[:class] = [ "circle", process_kind_circle_class(kind), options[:class] ].compact_blank.join(" ")
tag.span("", **options)
end
def process_kind_circle_class(kind)
{
"blue": "circle-blue",
"green": "circle-green",
"yellow": "circle-yellow",
"purple": "circle-purple",
"sky": "circle-sky"
}[Process::KIND_COLORS[kind]&.to_sym || :zinc]
end
def process_kind_badge(kind, options = {})
options[:class] = [ "badge", process_kind_badge_class(kind), options[:class] ].compact_blank.join(" ")
tag.span(kind.to_s.titleize, **options)
end
def process_kind_badge_class(kind)
{
"blue": "badge-blue",
"green": "badge-green",
"yellow": "badge-yellow",
"purple": "badge-purple",
"sky": "badge-sky"
}[Process::KIND_COLORS[kind]&.to_sym || :zinc]
end
def any_processes_filters?
params[:kind].present? || params[:hostname].present?
end
end
end
================================================
FILE: app/helpers/solid_queue_dashboard/recurring_tasks_helper.rb
================================================
module SolidQueueDashboard
module RecurringTasksHelper
def recurring_task_circle(type, options = {})
options[:class] = [ "circle", recurring_task_circle_class(type), options[:class] ].compact_blank.join(" ")
tag.span("", **options)
end
def recurring_task_circle_class(type)
{
"amber": "circle-amber",
"sky": "circle-sky",
"zinc": "circle-zinc"
}[RecurringTask::TYPE_COLORS[type]&.to_sym || :zinc]
end
def recurring_task_type_badge(type, options = {})
options[:class] = [ "badge", recurring_task_type_badge_class(type), options[:class] ].compact_blank.join(" ")
tag.span(type.to_s.titleize, **options)
end
def recurring_task_type_badge_class(type)
{
"amber": "badge-amber",
"sky": "badge-sky",
"zinc": "badge-zinc"
}[RecurringTask::TYPE_COLORS[type]&.to_sym || :zinc]
end
def any_recurring_tasks_filters?
params[:class_name].present? || params[:queue_name].present?
end
end
end
================================================
FILE: app/views/layouts/solid_queue_dashboard/application.html.erb
================================================
<!DOCTYPE html>
<html class="<%= dark_mode? ? "dark" : "" %>" lang="en">
<head>
<title>Solid Queue Dashboard</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "solid_queue_dashboard/application", media: "all" %>
<%= javascript_include_tag "solid_queue_dashboard/application" %>
<%= javascript_include_tag "chartkick" %>
<%= javascript_include_tag "Chart.bundle" %>
<%= javascript_include_tag "solid_queue_dashboard/alpine", defer: true %>
</head>
<body class="pb-[15vh] sm:pb-[25vh]">
<div class="max-w-[1920px] mx-auto px-4 sm:px-6 lg:px-8">
<%= render "navbar" %>
<%= render "flash_messages" %>
<%= yield %>
<%= render "footer" %>
</div>
</body>
</html>
================================================
FILE: app/views/solid_queue_dashboard/application/_flash_messages.html.erb
================================================
<% if flash.any? %>
<div class="space-y-4 mb-8">
<% flash.each do |type, message| %>
<% alert_class = case type
when "notice" then "alert alert-blue"
when "success" then "alert alert-green"
when "error" then "alert alert-red"
when "alert" then "alert alert-yellow"
else "alert alert-default"
end %>
<div class="<%= alert_class %>" role="alert">
<% if type == "notice" %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
<% elsif type == "success" %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<% elsif type == "error" %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
<% elsif type == "alert" %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
<% end %>
<h3 class="alert-title"><%= type.capitalize %></h3>
<div class="alert-description">
<%= message %>
</div>
</div>
<% end %>
</div>
<% end %>
================================================
FILE: app/views/solid_queue_dashboard/application/_footer.html.erb
================================================
<footer class="mt-6">
<p class="text-xs text-center text-muted-foreground">
<a
href="https://github.com/akodkod/solid_queue_dashboard"
target="_blank"
class="hover:underline hover:text-foreground"
>
<%= icon_github class: "inline-block size-3 mr-0.5 -translate-y-px" %>GitHub
</a>
</p>
</footer>
================================================
FILE: app/views/solid_queue_dashboard/application/_navbar.html.erb
================================================
<nav class="navbar mb-6">
<%= link_to root_path, class: "inline-flex items-center gap-0.5 text-xl font-bold tracking-tight translate-y-px pl-1" do %>
<span class="circle circle-blue"></span>
<span class="circle circle-green"></span>
<span class="circle circle-yellow"></span>
<span class="circle circle-red"></span>
<span class="ml-1.5 -translate-y-px">Solid Queue</span>
<% end %>
<div class="navbar-section">
<%= link_to root_path, class: "navbar-item #{current_page?(controller: 'dashboard', action: 'index') ? 'navbar-item-current' : 'navbar-item-default'}" do %>
<%= icon_layout_dashboard class: "size-4 mr-1.5" %>
Dashboard
<% end %>
<%= link_to jobs_path, class: "navbar-item #{current_page?(jobs_path) ? 'navbar-item-current' : 'navbar-item-default'}" do %>
<%= icon_logs class: "size-4 mr-1.5" %>
Jobs
<% end %>
<%= link_to processes_path, class: "navbar-item #{current_page?(processes_path) ? 'navbar-item-current' : 'navbar-item-default'}" do %>
<%= icon_server class: "size-4 mr-1.5" %>
Processes
<span class="badge <%= current_page?(processes_path) ? 'badge-secondary' : 'badge-primary' %> ml-1 text-xs">
<%= SolidQueue::Process.count %>
</span>
<% end %>
<%= link_to recurring_tasks_path, class: "navbar-item #{current_page?(recurring_tasks_path) ? 'navbar-item-current' : 'navbar-item-default'}" do %>
<%= icon_clock class: "size-4 mr-1.5" %>
Recurring Tasks
<span class="badge <%= current_page?(recurring_tasks_path) ? 'badge-secondary' : 'badge-primary' %> ml-1 text-xs">
<%= SolidQueue::RecurringTask.count %>
</span>
<% end %>
<%= link_to stats_path, class: "navbar-item #{current_page?(stats_path) ? 'navbar-item-current' : 'navbar-item-default'}" do %>
<%= icon_chart_scatter class: "size-4 mr-1.5" %>
Stats
<% end %>
</div>
<div class="ml-auto flex gap-4">
<% if current_page?(controller: 'dashboard', action: 'index') %>
<%= form_with url: root_path, method: :get, class: "flex gap-1.5 items-center" do |form| %>
<%= form.hidden_field :chart_period %>
<%= icon_refresh_cw class: "size-4 text-muted-foreground" %>
<%= form.select :auto_refresh_period,
[["Auto-Refresh Off", "off"], ["15 seconds", "15"], ["30 seconds", "30"], ["1 minute", "60"], ["3 minutes", "180"], ["5 minutes", "300"], ["10 minutes", "600"], ["15 minutes", "900"], ["30 minutes", "1800"], ["1 hour", "3600"]],
{ selected: params[:auto_refresh_period].presence || "off" },
class: "select w-40",
data: { auto_submit: true }
%>
<% end %>
<% end %>
<%= form_with url: toggle_appearance_path, method: :post do |f| %>
<button
type="submit"
class="btn btn-icon btn-secondary"
>
<% if dark_mode?%>
<%= icon_sun class: "size-4" %>
<% else %>
<%= icon_moon class: "size-4" %>
<% end %>
</button>
<% end %>
</div>
</nav>
================================================
FILE: app/views/solid_queue_dashboard/application/_pagination.html.erb
================================================
<% if total_pages > 1 %>
<nav class="pagination" role="navigation" aria-label="pagination">
<div class="pagination-content">
<%= link_to raw('« Previous'), url_for(page: current_page - 1, per_page: per_page), class: "pagination-link pagination-previous #{current_page > 1 ? 'pagination-link-active' : 'pagination-link-inactive'}", rel: "prev" unless current_page == 1 %>
<% page_range(current_page, total_pages).each do |page| %>
<% if page.is_a?(Integer) %>
<span class="pagination-item">
<%= link_to page, url_for(page: page, per_page: per_page), class: "pagination-link pagination-link-default #{page == current_page ? 'pagination-link-active' : 'pagination-link-inactive'}" %>
</span>
<% else %>
<span class="pagination-ellipsis">…</span>
<% end %>
<% end %>
<%= link_to raw('Next »'), url_for(page: current_page + 1, per_page: per_page), class: "pagination-link pagination-next #{current_page < total_pages ? 'pagination-link-active' : 'pagination-link-inactive'}", rel: "next" unless current_page == total_pages %>
</div>
</nav>
<% end %>
================================================
FILE: app/views/solid_queue_dashboard/dashboard/index.html.erb
================================================
<div class="card card-content p-6 relative">
<%= form_with url: root_path, method: :get, class: "absolute top-6 right-6" do |form| %>
<%= form.hidden_field :auto_refresh_period %>
<%= form.select :chart_period,
[["15 minutes", "15m"], ["30 minutes", "30m"], ["1 hour", "1h"], ["3 hours", "3h"], ["6 hours", "6h"], ["12 hours", "12h"], ["1 day", "1d"], ["3 days", "3d"], ["1 week", "1w"]],
{ selected: params[:chart_period].presence || "30m" },
class: "select",
data: { auto_submit: true }
%>
<% end %>
<%= line_chart @charts, points: false, thousands: "," %>
</div>
<div class="grid grid-cols-6 gap-4 mt-4">
<% SolidQueueDashboard::Job::STATUSES.each do |status| %>
<div class="card" data-href="<%= jobs_path(status:) %>">
<div class="card-content pt-5">
<h4 class="font-medium">
<%= status.to_s.titleize %>
<span class="ml-0.5 -translate-y-px circle <%= job_status_circle_class(status) %>"></span>
</h4>
<p class="text-4xl font-bold mt-1 text-black dark:text-white">
<%= number_with_delimiter(@jobs.with_status(status).count) %>
</p>
</div>
</div>
<% end %>
</div>
================================================
FILE: app/views/solid_queue_dashboard/jobs/_filters.html.erb
================================================
<%= form_with url: jobs_path, method: :get, class: "space-y-3" do |form| %>
<div>
<label class="label">Status</label>
<div class="flex flex-wrap gap-1 mt-1">
<% SolidQueueDashboard::Job::STATUSES.each do |status| %>
<%= button_tag type: :submit,
name: :status,
value: status,
class: "px-2 badge #{params[:status] == status.to_s ? 'badge-primary' : 'badge-outline'}" do
%>
<span class="circle <%= job_status_circle_class(status) %>"></span>
<%= status.to_s.titleize %>
<span class="opacity-50 -ml-0.5">
<%= SolidQueueDashboard.decorate(SolidQueue::Job.all).with_status(status).count %>
</span>
<% end %>
<% end %>
<% if params[:status].present? %>
<%= button_tag type: :submit, name: :status, value: nil, class: "badge badge-destructive gap-1" do %>
<%= icon_x class: "size-3.5 text-red-500" %>
Clear
<% end %>
<% end %>
</div>
</div>
<div>
<label class="label">Job Class</label>
<div class="flex flex-wrap gap-1 mt-1">
<% @job_class_names.each do |class_name| %>
<%= button_tag type: :submit,
name: :class_name,
value: class_name,
class: "px-2 badge #{params[:class_name] == class_name ? 'badge-primary' : 'badge-outline'}" do
%>
<%= class_name.to_s.titleize %>
<span class="opacity-50 -ml-0.5">
<%= SolidQueue::Job.where(class_name:).count %>
</span>
<% end %>
<% end %>
<% if params[:class_name].present? %>
<%= button_tag type: :submit, name: :class_name, value: nil, class: "badge badge-destructive gap-1" do %>
<%= icon_x class: "size-3.5 text-red-500" %>
Clear
<% end %>
<% end %>
</div>
</div>
<% if @job_queue_names.size > 1 %>
<div>
<label class="label">Queue</label>
<div class="flex flex-wrap gap-1 mt-1">
<% @job_queue_names.each do |queue_name| %>
<%= button_tag type: :submit,
name: :queue_name,
value: queue_name,
class: "px-2 badge #{params[:queue_name] == queue_name ? 'badge-primary' : 'badge-outline'}" do
%>
<%= queue_name.to_s.titleize %>
<span class="opacity-50 -ml-0.5">
<%= SolidQueue::Job.where(queue_name:).count %>
</span>
<% end %>
<% end %>
<% if params[:queue_name].present? %>
<%= button_tag type: :submit, name: :queue_name, value: nil, class: "badge badge-destructive gap-1" do %>
<%= icon_x class: "size-3.5 text-red-500" %>
Clear
<% end %>
<% end %>
</div>
</div>
<% end %>
<% if any_jobs_filters? %>
<hr>
<%= link_to jobs_path, class: "btn btn-outline btn-xs" do %>
<%= icon_x class: "size-4 text-red-500" %>
Clear All Filters
<% end %>
<% end %>
<% end %>
================================================
FILE: app/views/solid_queue_dashboard/jobs/_table.html.erb
================================================
<% highlight_ids = local_assigns[:highlight_ids] || [] %>
<div class="table-wrapper">
<table class="table">
<thead class="table-header">
<tr class="table-row">
<th class="table-head pl-6">ID</th>
<th class="table-head">Status</th>
<th class="table-head">Class</th>
<th class="table-head">Queue</th>
<th class="table-head">Scheduled At</th>
<th class="table-head"></th>
</tr>
</thead>
<tbody class="table-body">
<%= render partial: "solid_queue_dashboard/jobs/table_row", collection: jobs, as: :job, locals: { highlight_ids: highlight_ids } %>
</tbody>
</table>
</div>
================================================
FILE: app/views/solid_queue_dashboard/jobs/_table_row.html.erb
================================================
<% highlight = local_assigns[:highlight] || local_assigns[:highlight_ids]&.include?(job.id) %>
<tr class="table-row <%= highlight ? "bg-primary/7.5 hover:bg-primary/10" : "" %>" data-href="<%= job_path(job) %>">
<td class="table-cell pl-6 font-medium text-zinc-900 dark:text-zinc-100 whitespace-nowrap">
<span class="mr-1 <%= job_status_circle_class(job.status) %>"></span>
<%= job.id %>
<% if job.arguments["executions"] > 0 %>
<span class="text-xs text-muted-foreground">
(x<%= job.arguments["executions"] + 1 %>)
</span>
<% end %>
</td>
<td class="table-cell">
<%= job_status_badge(job.status) %>
<% if job.running? %>
<br />
<span class="text-xs text-muted-foreground">
<%= time_ago_in_words(job.claimed_execution.created_at, include_seconds: true) %>
</span>
<% end %>
</td>
<td class="table-cell">
<% if job.class_name == SolidQueueDashboard::Job::COMMAND_CLASS_NAME %>
<span class="font-medium">
<%= truncate(job.arguments["arguments"][0], length: 50) %>
</span>
<br>
<span class="text-xs text-muted-foreground">
Recurring Command
</span>
<% else %>
<p class="font-medium"><%= job.class_name %></p>
<% if job.arguments["arguments"].present? %>
<p class="inline-flex flex-wrap gap-0.5 items-start mt-0.5">
<% job.arguments["arguments"].each do |argument| %>
<span class="badge badge-zinc text-xs"><%= truncate(JSON.pretty_generate(argument), length: 60) %></span>
<% end %>
</p>
<% end %>
<% end %>
</td>
<td class="table-cell">
<%= job.queue_name.titleize %>
</td>
<td class="table-cell">
<%= tag.span job.scheduled_at, data: { date: true }, class: "font-medium" %>
<br />
<span class="text-xs text-muted-foreground">
<%= time_ago_in_words(job.scheduled_at, include_seconds: true) %> ago
</span>
</td>
<td class="table-cell">
<% if job.running? %>
<p class="text-sm text-muted-foreground">
Running for <strong class="font-medium text-foreground"><%= time_ago_in_words(job.claimed_execution.created_at, include_seconds: true) %></strong>
<% if job.claimed_execution.process %>
by <strong class="font-medium text-foreground">
<%= process_kind_badge(job.claimed_execution.process.kind) %>
<%= link_to "##{job.claimed_execution.process.id}", process_path(job.claimed_execution.process), class: "link" %>
</strong>
<% end %>
</p>
<% elsif job.success? || job.retried? %>
<p class="text-sm text-muted-foreground">
<%= job.retried? ? "Failed" : "Finished" %> at <strong class="font-medium text-foreground" data-date="<%= job.finished_at.to_fs(:database) %>"><%= job.finished_at %></strong><br />
<span class="text-xs"><%= time_ago_in_words(job.scheduled_at, include_seconds: true) %> ago</span>
</p>
<% elsif job.failed? %>
<div class="flex items-center justify-between">
<p class="text-sm text-balance font-medium">
<%= icon_triangle_alert class: "inline-block size-4 text-red-600 dark:text-red-500 -translate-y-px mr-0.5" %>
<%= job.error_message %>
</p>
<%= form_with url: retry_job_path(job), method: :post do %>
<button
type="submit"
class="btn btn-icon btn-outline"
title="Retry"
>
<%= icon_refresh_cw class: "size-4 text-amber-500" %>
</button>
<% end %>
</div>
<% elsif job.pending? %>
<p class="text-sm text-muted-foreground">
Pending for <strong class="font-medium text-foreground"><%= time_ago_in_words(job.scheduled_at, include_seconds: true) %></strong>
</p>
<% end %>
</td>
</tr>
================================================
FILE: app/views/solid_queue_dashboard/jobs/index.html.erb
================================================
<div class="card">
<div class="card-header border-b">
<h2 class="card-title">Jobs</h2>
<div class="!mt-4">
<%= render "filters" %>
</div>
</div>
<div class="card-content !p-0">
<% if @jobs.any? %>
<%= render "solid_queue_dashboard/jobs/table", jobs: @jobs %>
<% else %>
<div class="px-6 py-12">
<p class="text-muted-foreground text-center">
No jobs found
</p>
<% if any_jobs_filters? %>
<div class="flex justify-center mt-2">
<%= link_to "Clear Filters", jobs_path, class: "btn btn-outline" %>
</div>
<% end %>
</div>
<% end %>
</div>
<% if @pagination[:total_pages] > 1 %>
<div class="card-footer pt-6 border-t flex justify-between">
<%= render partial: 'solid_queue_dashboard/application/pagination', locals: {
current_page: @pagination[:current_page],
total_pages: @pagination[:total_pages],
per_page: @pagination[:per_page]
} %>
<div class="flex items-center gap-2">
<span class="text-muted-foreground text-sm whitespace-nowrap">
Show
</span>
<%= form_with url: jobs_path, method: :get do |f| %>
<%= f.hidden_field :page, value: 1 %>
<%= f.select :per_page,
[10, 25, 50, 100],
{ selected: @pagination[:per_page] },
class: "select w-24",
data: { auto_submit: true }
%>
<% end %>
<span class="text-muted-foreground text-sm whitespace-nowrap">
per page
</span>
</div>
</div>
<% end %>
</div>
================================================
FILE: app/views/solid_queue_dashboard/jobs/show.html.erb
================================================
<div class="flex items-center justify-between gap-4">
<h1 class="text-4xl flex items-center gap-2">
<%= job_status_circle(@job.status, class: "size-4") %>
<span class="tracking-tighter font-bold">
Job #<%= @job.id %>
</span>
<% if @job.arguments["executions"] > 0 %>
<span class="ml-2 badge badge-zinc">
<%= (@job.arguments["executions"] + 1).ordinalize %> attempt
</span>
<% end %>
</h1>
<div class="flex gap-2">
<% if @job.failed? %>
<%= form_with url: retry_job_path(@job) do %>
<button class="btn btn-outline btn-md">
<%= icon_refresh_cw class: "size-4 text-amber-600" %>
<span>Retry Job</span>
</button>
<% end %>
<% end %>
</div>
</div>
<div class="card mt-6">
<div class="card-content pt-6 grid grid-cols-1 sm:grid-cols-2 sm:gap-16 md:gap-20">
<div class="space-y-4">
<div class="info-line">
<span class="info-line-label">Status</span>
<span class="info-line-separator"></span>
<span class="info-line-value"><%= job_status_badge(@job.status) %></span>
</div>
<div class="info-line">
<span class="info-line-label">Queue</span>
<span class="info-line-separator"></span>
<span class="info-line-value">
<%= link_to @job.queue_name.titleize, jobs_path(queue_name: @job.queue_name), class: "link" %>
</span>
</div>
<div class="info-line">
<span class="info-line-label">Class</span>
<span class="info-line-separator"></span>
<span class="info-line-value">
<%= link_to @job.class_name, jobs_path(class_name: @job.class_name), class: "link" %>
</span>
</div>
<div class="info-line">
<span class="info-line-label">Active Job ID</span>
<span class="info-line-separator"></span>
<span class="info-line-value"><%= @job.active_job_id %></span>
</div>
</div>
<div class="space-y-4">
<div class="info-line">
<span class="info-line-label">Created At</span>
<span class="info-line-separator"></span>
<span class="info-line-value" data-date><%= @job.created_at %></span>
</div>
<div class="info-line">
<span class="info-line-label">Scheduled At</span>
<span class="info-line-separator"></span>
<span class="info-line-value" data-date><%= @job.scheduled_at %></span>
</div>
<% if @job.success? || @job.retried? %>
<div class="info-line">
<span class="info-line-label">Finished At</span>
<span class="info-line-separator"></span>
<span class="text-sm text-muted-foreground font-normal">
<%= time_ago_in_words(@job.finished_at, include_seconds: true) %> ago
</span>
<span class="info-line-value" data-date><%= @job.finished_at.to_fs(:database) %></span>
</div>
<div class="info-line">
<span class="info-line-label">Time Taken</span>
<span class="info-line-separator"></span>
<span class="info-line-value"><%= distance_of_time_in_words(@job.scheduled_at, @job.finished_at, include_seconds: true) %></span>
</div>
<% end %>
<% if @job.failed? %>
<div class="info-line">
<span class="info-line-label">Failed At</span>
<span class="info-line-separator"></span>
<span class="info-line-value" data-date><%= @job.failed_execution.created_at.to_fs(:database) %></span>
</div>
<div class="info-line">
<span class="info-line-label">Time Taken</span>
<span class="info-line-separator"></span>
<span class="info-line-value"><%= distance_of_time_in_words(@job.scheduled_at, @job.failed_execution.created_at, include_seconds: true) %></span>
</div>
<% end %>
</div>
</div>
</div>
<% if @job.arguments["arguments"].present? %>
<div x-data="{ showAll: false }" class="card mt-8 overflow-hidden">
<div class="card-header border-b border-t-4 border-t-blue-500 dark:border-t-blue-600 flex flex-row justify-between gap-4">
<div class="space-y-1.5">
<% if @job.class_name === SolidQueueDashboard::Job::COMMAND_CLASS_NAME %>
<h3 class="card-title">Command</h3>
<p class="card-description">The command that was executed</p>
<% else %>
<h3 class="card-title">Arguments</h3>
<p class="card-description">The arguments that were passed to this job</p>
<% end %>
</div>
<button x-on:click="showAll = !showAll" class="btn btn-sm btn-outline">
<span x-show="!showAll">Show All Metadata</span>
<span x-show="showAll">Hide All Metadata</span>
</button>
</div>
<div class="card-content pt-6">
<% if @job.class_name === SolidQueueDashboard::Job::COMMAND_CLASS_NAME %>
<pre x-show="!showAll"><%= @job.arguments["arguments"][0] %></pre>
<% else %>
<div x-show="!showAll">
<% @job.arguments["arguments"].each do |argument| %>
<span class="badge badge-zinc text-sm"><%= JSON.pretty_generate(argument) %></span>
<% end %>
</div>
<% end %>
<pre x-show="showAll"><%= JSON.pretty_generate(@job.arguments) %></pre>
</div>
</div>
<% end %>
<% if @job.failed? %>
<div class="card mt-8 overflow-hidden">
<div class="card-content pt-5 border-t-4 border-t-red-500">
<h4 class="text-lg font-bold">
<%= @job.error_message %>
</h4>
<% backtrace = Rails.backtrace_cleaner.clean(@job.failed_execution.error["backtrace"]) %>
<% if backtrace.any? %>
<ul class="list-decimal marker:text-sm marker:text-muted-foreground mt-1 ml-8 space-y-1">
<% backtrace.each do |line| %>
<li class="font-mono"><%= line %></li>
<% end %>
</ul>
<% end %>
</div>
</div>
<% end %>
<% if @job.execution_history.size > 1 %>
<div class="card mt-8 overflow-hidden">
<div class="card-header border-b border-t-4 border-t-zinc-500 dark:border-t-zinc-600">
<h3 class="card-title">Execution History</h3>
<p class="card-description">The history of executions of this exact job</p>
</div>
<div class="card-content !p-0">
<%= render "table", jobs: SolidQueueDashboard.decorate(@job.execution_history.order(id: :desc)), highlight_ids: [@job.id] %>
</div>
</div>
<% end %>
<% if @job_history.size > 1 %>
<div class="card mt-8 overflow-hidden">
<div class="card-header border-b border-t-4 border-t-zinc-500 dark:border-t-zinc-600 flex !flex-row justify-between items-center">
<div class="space-y-1.5">
<h3 class="card-title">Similar Jobs</h3>
<p class="card-description">The history of executions of the same job class</p>
</div>
<%= link_to "View All", jobs_path(class_name: @job.class_name), class: "btn btn-outline btn-sm" %>
</div>
<div class="card-content !p-0">
<%= render "table", jobs: @job_history %>
</div>
<div class="card-footer border-t pt-6">
<p class="text-sm text-muted-foreground">
Showing the last 10 executions of this job
</p>
</div>
</div>
<% end %>
================================================
FILE: app/views/solid_queue_dashboard/processes/_filters.html.erb
================================================
<%= form_with url: processes_path, method: :get, class: "space-y-3" do |form| %>
<% if @process_kinds.any? %>
<div>
<label class="label">Kind</label>
<div class="flex flex-wrap gap-1 mt-1">
<% @process_kinds.each do |kind| %>
<%= button_tag type: :submit,
name: :kind,
value: kind,
class: "px-2 badge #{params[:kind] == kind.to_s ? 'badge-primary' : 'badge-outline'}" do
%>
<%= process_kind_circle(kind) %>
<%= kind.to_s.titleize %>
<span class="opacity-50 -ml-0.5">
<%= SolidQueue::Process.where(kind:).count %>
</span>
<% end %>
<% end %>
<% if params[:kind].present? %>
<%= button_tag type: :submit, name: :kind, value: nil, class: "badge badge-destructive gap-1" do %>
<%= icon_x class: "size-3.5 text-red-500" %>
Clear
<% end %>
<% end %>
</div>
</div>
<% end %>
<% if @process_hostnames.any? %>
<div>
<label class="label">Hostname</label>
<div class="flex flex-wrap gap-1 mt-1">
<% @process_hostnames.each do |hostname| %>
<%= button_tag type: :submit,
name: :hostname,
value: hostname,
class: "px-2 badge #{params[:hostname] == hostname ? 'badge-primary' : 'badge-outline'}" do
%>
<%= hostname %>
<span class="opacity-50 -ml-0.5">
<%= SolidQueue::Process.where(hostname:).count %>
</span>
<% end %>
<% end %>
<% if params[:hostname].present? %>
<%= button_tag type: :submit, name: :hostname, value: nil, class: "badge badge-destructive gap-1" do %>
<%= icon_x class: "size-3.5 text-red-500" %>
Clear
<% end %>
<% end %>
</div>
</div>
<% end %>
<% if any_processes_filters? %>
<hr>
<%= link_to processes_path, class: "btn btn-outline btn-xs" do %>
<%= icon_x class: "size-4 text-red-500" %>
Clear All Filters
<% end %>
<% end %>
<% end %>
================================================
FILE: app/views/solid_queue_dashboard/processes/_table.html.erb
================================================
<div class="table-wrapper">
<table class="table">
<thead class="table-header">
<tr class="table-row">
<th class="table-head pl-6">ID</th>
<th class="table-head">Kind</th>
<th class="table-head">Hostname</th>
<th class="table-head">PID</th>
<th class="table-head">Launched At</th>
<th class="table-head">Last Heartbeat</th>
<th class="table-head"></th>
</tr>
</thead>
<tbody class="table-body">
<%= render partial: 'solid_queue_dashboard/processes/table_row', collection: processes, as: :process %>
</tbody>
</table>
</div>
================================================
FILE: app/views/solid_queue_dashboard/processes/_table_row.html.erb
================================================
<tr class="table-row" data-href="<%= process_path(process) %>">
<td class="table-cell pl-6 font-medium text-zinc-900 dark:text-zinc-100">
<%= process.id %>
</td>
<td class="table-cell">
<%= process_kind_badge(process.kind) %>
<% if process.claimed_executions.any? %>
<span class="ml-0.5 text-xs text-muted-foreground">
Running <%= pluralize(process.claimed_executions.count, "job") %>
</span>
<% end %>
</td>
<td class="table-cell"><%= process.hostname %></td>
<td class="table-cell"><%= process.pid %></td>
<td class="table-cell">
<span data-date="true"><%= process.created_at.to_fs(:database) %></span><br />
<span class="text-xs text-muted-foreground">
<%= time_ago_in_words(process.created_at, include_seconds: true) %> ago
</span>
</td>
<td class="table-cell" title="<%= process.last_heartbeat_at %>">
<span data-date="true"><%= process.last_heartbeat_at.to_fs(:database) %></span><br />
<span class="text-xs text-muted-foreground">
<%= time_ago_in_words(process.last_heartbeat_at, include_seconds: true) %> ago
</span>
</td>
<td class="table-cell">
<% if process.dead? %>
<span class="badge badge-red">Dead?</span>
<% end %>
</td>
</tr>
================================================
FILE: app/views/solid_queue_dashboard/processes/index.html.erb
================================================
<div class="card">
<div class="card-header border-b">
<h2 class="card-title">Processes</h2>
<div class="!mt-4">
<%= render "filters" %>
</div>
</div>
<div class="card-content !p-0">
<% if @processes.any? %>
<%= render "solid_queue_dashboard/processes/table", processes: @processes %>
<% else %>
<div class="px-6 py-12">
<p class="text-muted-foreground text-center">
No processes found
</p>
<% if any_processes_filters? %>
<div class="flex justify-center mt-2">
<%= link_to "Clear Filters", processes_path, class: "btn btn-outline btn-md" %>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
================================================
FILE: app/views/solid_queue_dashboard/processes/show.html.erb
================================================
<h1 class="text-4xl flex items-center gap-2">
<%= process_kind_circle(@process.kind, class: "!size-4") %>
<span class="tracking-tighter font-bold">
Process #<%= @process.id %>
</span>
<% if @process.dead? %>
<span class="badge badge-red">Dead?</span>
<% end %>
</h1>
<div class="card mt-6">
<div class="card-content pt-6 grid grid-cols-1 sm:grid-cols-2 sm:gap-16">
<div class="space-y-4">
<div class="info-line">
<span class="info-line-label">Kind</span>
<span class="info-line-separator"></span>
<span class="info-line-value"><%= process_kind_badge(@process.kind) %></span>
</div>
<div class="info-line">
<span class="info-line-label">Hostname</span>
<span class="info-line-separator"></span>
<span class="info-line-value"><%= @process.hostname %></span>
</div>
<div class="info-line">
<span class="info-line-label">PID</span>
<span class="info-line-separator"></span>
<span class="info-line-value"><%= @process.pid %></span>
</div>
<div class="info-line">
<span class="info-line-label">Name</span>
<span class="info-line-separator"></span>
<span class="info-line-value"><%= @process.name %></span>
</div>
</div>
<div class="space-y-4">
<div class="info-line">
<span class="info-line-label">Last Heartbeat</span>
<span class="info-line-separator"></span>
<span class="text-sm text-muted-foreground font-normal">
<%= time_ago_in_words(@process.last_heartbeat_at, include_seconds: true) %> ago
</span>
<span class="info-line-value" data-date><%= @process.last_heartbeat_at.to_fs(:database) %></span>
</div>
<div class="info-line">
<span class="info-line-label">Created At</span>
<span class="info-line-separator"></span>
<span class="text-sm text-muted-foreground font-normal">
<%= time_ago_in_words(@process.created_at, include_seconds: true) %> ago
</span>
<span class="info-line-value" data-date><%= @process.created_at.to_fs(:database) %></span>
</div>
<div class="info-line">
<span class="info-line-label">Time Running</span>
<span class="info-line-separator"></span>
<span class="info-line-value"><%= distance_of_time_in_words(@process.created_at, Time.current, include_seconds: true) %></span>
</div>
</div>
</div>
</div>
<% if @process.claimed_executions.any? %>
<div class="card mt-8 overflow-hidden">
<div class="card-header border-b border-t-4 border-t-sky-500 dark:border-t-sky-600">
<h2 class="card-title">Running Jobs</h2>
<p class="card-description">Jobs that this process is currently running</p>
</div>
<div class="card-content p-0">
<%= render partial: 'solid_queue_dashboard/jobs/table', locals: { jobs: @process.claimed_executions.map { |execution| SolidQueueDashboard::Decorators::JobDecorator.new(execution.job) } } %>
</div>
</div>
<% end %>
<% if @process.metadata.present? %>
<div class="card mt-8 overflow-hidden">
<div class="card-header border-b border-t-4 border-t-amber-500 dark:border-t-amber-600">
<h2 class="card-title">Metadata</h2>
<p class="card-description">Additional information about this process</p>
</div>
<div class="card-content pt-6">
<pre><%= JSON.pretty_generate(@process.metadata) %></pre>
</div>
</div>
<% end %>
================================================
FILE: app/views/solid_queue_dashboard/recurring_tasks/_filters.html.erb
================================================
<%= form_with url: recurring_tasks_path, method: :get, class: "space-y-3" do |form| %>
<div>
<div class="flex flex-wrap gap-1 mt-1">
<% SolidQueueDashboard::RecurringTask::TYPES.each do |type| %>
<%= button_tag type: :submit,
name: :type,
value: type,
class: "px-2 badge #{params[:type] == type.to_s ? 'badge-primary' : 'badge-outline'}" do
%>
<%= recurring_task_circle(type) %>
<%= type.to_s.titleize %>
<span class="opacity-50 -ml-0.5">
<%= SolidQueueDashboard.decorate(SolidQueue::RecurringTask.all).with_type(type).count %>
</span>
<% end %>
<% end %>
<% if params[:type].present? %>
<%= button_tag type: :submit, name: :type, value: nil, class: "badge badge-destructive gap-1" do %>
<%= icon_x class: "size-3.5 text-red-500" %>
Clear
<% end %>
<% end %>
</div>
</div>
<% end %>
================================================
FILE: app/views/solid_queue_dashboard/recurring_tasks/_table.html.erb
================================================
<div class="table-wrapper">
<table class="table">
<thead class="table-header">
<tr class="table-row">
<th class="table-head pl-6">ID</th>
<th class="table-head">Type</th>
<th class="table-head"></th>
<th class="table-head">Schedule</th>
<th class="table-head">Queue Name</th>
<th class="table-head">Priority</th>
<th class="table-head">Key</th>
<th class="table-head"></th>
</tr>
</thead>
<tbody class="table-body">
<%= render partial: 'solid_queue_dashboard/recurring_tasks/table_row', collection: recurring_tasks, as: :recurring_task %>
</tbody>
</table>
</div>
================================================
FILE: app/views/solid_queue_dashboard/recurring_tasks/_table_row.html.erb
================================================
<tr class="table-row" data-href="<%= recurring_task_path(recurring_task) %>">
<td class="table-cell pl-6 font-medium text-zinc-900 dark:text-zinc-100">
<%= recurring_task.id %>
</td>
<td class="table-cell">
<%= recurring_task_type_badge(recurring_task.type) %>
</td>
<td class="table-cell">
<span class="font-medium">
<%= recurring_task.class_name || recurring_task.command %>
</span>
</td>
<td class="table-cell"><%= recurring_task.schedule %></td>
<td class="table-cell"><%= recurring_task.queue_name&.titleize || empty_value %></td>
<td class="table-cell"><%= recurring_task.priority || empty_value %></td>
<td class="table-cell"><%= recurring_task.key %></td>
<td class="table-cell">
<%= form_with url: enqueue_recurring_task_path(recurring_task), method: :post do %>
<button
type="submit"
class="btn btn-md btn-outline"
>
<%= icon_play class: "size-4 text-amber-500 mr-1" %>
Enqueue Now
</button>
<% end %>
</td>
</tr>
================================================
FILE: app/views/solid_queue_dashboard/recurring_tasks/index.html.erb
================================================
<div class="card">
<div class="card-header border-b">
<h2 class="card-title">Recurring Tasks</h2>
<div class="!mt-4">
<%= render "solid_queue_dashboard/recurring_tasks/filters" %>
</div>
</div>
<div class="card-content !p-0">
<% if @recurring_tasks.any? %>
<%= render "solid_queue_dashboard/recurring_tasks/table", recurring_tasks: @recurring_tasks %>
<% else %>
<div class="px-6 py-12">
<p class="text-muted-foreground text-center">
No recurring tasks found
</p>
</div>
<% end %>
</div>
</div>
================================================
FILE: app/views/solid_queue_dashboard/recurring_tasks/show.html.erb
================================================
<h1 class="text-4xl flex items-center gap-2">
<%= recurring_task_circle(@recurring_task.type, class: "size-4") %>
<span class="tracking-tighter font-bold">
Recurring Task #<%= @recurring_task.id %>
</span>
</h1>
<div class="card mt-6">
<div class="card-content pt-6 grid grid-cols-1 sm:grid-cols-2 sm:gap-16">
<div class="space-y-4">
<div class="info-line">
<span class="info-line-label">Type</span>
<div class="info-line-separator"></div>
<span class="info-line-value"><%= recurring_task_type_badge(@recurring_task.type) %></span>
</div>
<div class="info-line">
<span class="info-line-label">Key</span>
<div class="info-line-separator"></div>
<span class="info-line-value"><%= @recurring_task.key %></span>
</div>
<div class="info-line">
<span class="info-line-label">Schedule</span>
<div class="info-line-separator"></div>
<span class="info-line-value"><%= @recurring_task.schedule %></span>
</div>
<% if @recurring_task.queue_name? %>
<div class="info-line">
<span class="info-line-label">Queue Name</span>
<div class="info-line-separator"></div>
<span class="info-line-value"><%= @recurring_task.queue_name %></span>
</div>
<% end %>
</div>
<div class="space-y-4">
<% if @recurring_task.priority? %>
<div class="info-line">
<span class="info-line-label">Priority</span>
<div class="info-line-separator"></div>
<span class="info-line-value"><%= @recurring_task.priority %></span>
</div>
<% end %>
<div class="info-line">
<span class="info-line-label">Static</span>
<div class="info-line-separator"></div>
<span class="info-line-value"><%= @recurring_task.static ? "Yes" : "No" %></span>
</div>
<div class="info-line">
<span class="info-line-label">Created At</span>
<div class="info-line-separator"></div>
<span class="info-line-value" data-date><%= @recurring_task.created_at.to_fs(:database) %></span>
</div>
<div class="info-line">
<span class="info-line-label">Updated At</span>
<div class="info-line-separator"></div>
<span class="info-line-value" data-date><%= @recurring_task.updated_at.to_fs(:database) %></span>
</div>
</div>
</div>
</div>
<% if @recurring_task.command.present? %>
<div class="card mt-8 overflow-hidden">
<div class="card-header border-b border-t-4 border-t-amber-500 dark:border-t-amber-600">
<h3 class="card-title">Command</h3>
<p class="card-description">The command to be executed</p>
</div>
<div class="card-content pt-6">
<pre><%= @recurring_task.command %></pre>
</div>
</div>
<% end %>
<% if @recurring_task.class_name.present? %>
<div class="card mt-8 overflow-hidden">
<div class="card-header border-b border-t-4 border-t-blue-500 dark:border-t-blue-600">
<h3 class="card-title">Job Class</h3>
<p class="card-description">The job class to be executed</p>
</div>
<div class="card-content pt-6">
<pre><%= @recurring_task.class_name %></pre>
</div>
</div>
<% end %>
<% if @recurring_task.arguments.present? %>
<div class="card mt-8 overflow-hidden">
<div class="card-header border-b border-t-4 border-t-purple-500 dark:border-t-purple-600">
<h3 class="card-title">Arguments</h3>
<p class="card-description">The arguments to be passed to the job</p>
</div>
<div class="card-content pt-6">
<pre><%= JSON.pretty_generate(@recurring_task.arguments) %></pre>
</div>
</div>
<% end %>
<% if @recurring_task.description.present? %>
<div class="card mt-8 overflow-hidden">
<div class="card-header border-b border-t-4 border-t-amber-500 dark:border-t-amber-600">
<h3 class="card-title">Description</h3>
<p class="card-description">Additional information about this recurring task</p>
</div>
<div class="card-content pt-6">
<pre><%= @recurring_task.description %></pre>
</div>
</div>
<% end %>
<div class="card mt-8 overflow-hidden">
<div class="card-header border-b border-t-4 border-t-zinc-500 dark:border-t-zinc-600">
<h3 class="card-title">Next Runs</h3>
<p class="card-description">Based on the current schedule</p>
</div>
<div class="card-content pt-5">
<ul class="list-decimal marker:text-sm marker:text-muted-foreground ml-4 space-y-1">
<% @recurring_task.next_runs.each do |run_time| %>
<li>
<span data-date><%= run_time.strftime("%Y-%m-%d %H:%M:%S %Z") %></span>
<span class="text-muted-foreground">
in <%= distance_of_time_in_words(Time.current, run_time) %>
</span>
</li>
<% end %>
</ul>
</div>
</div>
================================================
FILE: app/views/solid_queue_dashboard/stats/index.html.erb
================================================
<div class="card mt-4">
<div class="card-header border-b">
<h3 class="card-title">Failure Rate</h3>
</div>
<div class="card-content !p-0">
<div class="table-wrapper">
<table class="table">
<thead class="table-header">
<tr class="table-row">
<th class="table-head">Job</th>
<th class="table-head">Failure Rate</th>
</tr>
</thead>
<tbody class="table-body">
<% @job_class_names.each do |class_name| %>
<tr class="table-row" data-href="<%= jobs_path(class_name:, status: :failed) %>">
<td class="table-cell font-medium"><%= class_name.titleize %></td>
<td class="table-cell font-medium"><%= format_failure_rate(SolidQueueDashboard.decorate(SolidQueue::Job.where(class_name:)).failure_rate) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
================================================
FILE: bin/console
================================================
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/setup"
require "solid_queue_dashboard"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
require "irb"
IRB.start(__FILE__)
================================================
FILE: bin/dev
================================================
#!/usr/bin/env bash
exec foreman start -f Procfile.dev
================================================
FILE: bin/setup
================================================
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
bundle install
================================================
FILE: bin/setup-test-app
================================================
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
cd test_app
bundle install
bin/rails db:setup
================================================
FILE: config/routes.rb
================================================
SolidQueueDashboard::Engine.routes.draw do
resources :jobs, only: [ :index, :show ] do
member do
post :retry
end
end
resources :processes, only: [ :index, :show ]
resources :recurring_tasks, path: "recurring-tasks", only: [ :index, :show ] do
member do
post :enqueue
end
end
get "stats", to: "stats#index", as: :stats
post "appearance/toggle", to: "appearance#toggle", as: :toggle_appearance
root "dashboard#index"
end
================================================
FILE: lib/solid_queue_dashboard/configuration.rb
================================================
module SolidQueueDashboard
class Configuration
attr_accessor :title
def initialize
@title = "Solid Queue Dashboard"
end
end
def self.configuration
@configuration ||= Configuration.new
end
def self.configure
yield(configuration)
end
end
================================================
FILE: lib/solid_queue_dashboard/decorators/job_decorator.rb
================================================
module SolidQueueDashboard
module Decorators
class JobDecorator < SimpleDelegator
def color
Job.status_color(status)
end
def status
return @status if defined?(@status)
@status = if running?
Job::RUNNING
elsif retried?
Job::RETRIED
elsif failed?
Job::FAILED
elsif success?
Job::SUCCESS
elsif scheduled?
Job::SCHEDULED
else
Job::PENDING
end
end
def running?
return @running if defined?(@running)
@running = claimed_execution.present?
end
def success?
return @success if defined?(@success)
@success = finished_at.present? && !failed? && !retried?
end
def retried?
return @retried if defined?(@retried)
@retried = finished_at.present? && !failed_execution.present? &&
(arguments["executions"].to_i > 0 || execution_history.where(scheduled_at: finished_at..).any?)
end
def failed?
return @failed if defined?(@failed)
@failed = failed_execution.present?
end
def scheduled?
return @scheduled if defined?(@scheduled)
@scheduled = scheduled_at.present? && scheduled_at > Time.current
end
def pending?
return @pending if defined?(@pending)
@pending = !finished_at.present? && !running?
end
def execution_history
SolidQueue::Job.where(active_job_id: active_job_id)
end
def error_message
return @error_message if defined?(@error_message)
@error_message = failed_execution ?
"#{failed_execution.error["exception_class"]}: #{failed_execution.error["message"]}" :
nil
end
end
end
end
================================================
FILE: lib/solid_queue_dashboard/decorators/jobs_decorator.rb
================================================
module SolidQueueDashboard
module Decorators
class JobsDecorator < SimpleDelegator
def with_status(status)
case status.to_sym
when Job::RUNNING
running
when Job::SUCCESS
success
when Job::FAILED
failed
when Job::SCHEDULED
scheduled
when Job::PENDING
pending
when Job::RETRIED
retried
else
raise "Invalid status: #{status}"
end
end
def running
where.associated(:claimed_execution)
end
def success
where.not(finished_at: nil)
.where.not(id: failed)
.where.not(id: retried)
end
def scheduled
where(finished_at: nil, scheduled_at: Time.current..)
end
def pending
where(finished_at: nil, scheduled_at: ..Time.current)
.where.not(id: failed)
.where.not(id: running)
end
def retried
where(finished_at: ..Time.current)
.where.not(id: failed)
.where(
active_job_id: SolidQueue::Job
.select(:active_job_id)
.group(:active_job_id)
.having("COUNT(*) > 1")
)
.where.not(
id: SolidQueue::Job
.select("MAX(id)")
.group(:active_job_id)
)
end
def failure_rate
success_count = success.count
retried_count = retried.count
failed_count = failed.count
total = success_count + retried_count + failed_count
return 0 if total.zero?
(failed_count + retried_count).to_f / total * 100
end
def each
super do |job|
yield JobDecorator.new(job)
end
end
def to_a
super.map { |job| JobDecorator.new(job) }
end
end
end
end
================================================
FILE: lib/solid_queue_dashboard/decorators/process_decorator.rb
================================================
module SolidQueueDashboard
module Decorators
class ProcessDecorator < SimpleDelegator
def color
Process.kind_color(kind)
end
def dead?
last_heartbeat_at < Process::HEARTBEAT_DEAD_THRESHOLD.ago
end
end
end
end
================================================
FILE: lib/solid_queue_dashboard/decorators/processes_decorator.rb
================================================
module SolidQueueDashboard
module Decorators
class ProcessesDecorator < SimpleDelegator
def each
super do |job|
yield ProcessDecorator.new(job)
end
end
def to_a
super.map { |job| ProcessDecorator.new(job) }
end
end
end
end
================================================
FILE: lib/solid_queue_dashboard/decorators/recurring_task_decorator.rb
================================================
module SolidQueueDashboard
module Decorators
class RecurringTaskDecorator < SimpleDelegator
def type
if command.present?
RecurringTask::COMMAND
elsif class_name.present?
RecurringTask::JOB
else
"Unknown"
end
end
def next_runs(count: 5)
cron = Fugit.parse(schedule)
return [] unless cron
cron.next.take(count)
end
end
end
end
================================================
FILE: lib/solid_queue_dashboard/decorators/recurring_tasks_decorator.rb
================================================
module SolidQueueDashboard
module Decorators
class RecurringTasksDecorator < SimpleDelegator
def with_type(type)
case type.to_sym
when RecurringTask::JOB
where.not(class_name: nil)
when RecurringTask::COMMAND
where.not(command: nil)
else
raise "Unknown type: #{type}"
end
end
def each
super do |task|
yield RecurringTaskDecorator.new(task)
end
end
def to_a
super.map { |task| RecurringTaskDecorator.new(task) }
end
end
end
end
================================================
FILE: lib/solid_queue_dashboard/engine.rb
================================================
module SolidQueueDashboard
class Engine < ::Rails::Engine
isolate_namespace SolidQueueDashboard
initializer "solid_queue_dashboard.assets.precompile" do |app|
app.config.assets.precompile += %w[
solid_queue_dashboard/alpine.js
solid_queue_dashboard/application.js
solid_queue_dashboard/application.css
]
end
end
end
================================================
FILE: lib/solid_queue_dashboard/job.rb
================================================
module SolidQueueDashboard
module Job
# Constants
RUNNING = :running
SUCCESS = :success
RETRIED = :retried
FAILED = :failed
PENDING = :pending
SCHEDULED = :scheduled
STATUSES = [ RUNNING, SUCCESS, RETRIED, FAILED, SCHEDULED, PENDING ]
STATUS_COLORS = {
SUCCESS => "green",
RETRIED => "amber",
FAILED => "red",
SCHEDULED => "purple",
PENDING => "zinc",
RUNNING => "sky"
}
COMMAND_CLASS_NAME = "SolidQueue::RecurringJob"
def self.status_color(status)
STATUS_COLORS[status] || "zinc"
end
end
end
================================================
FILE: lib/solid_queue_dashboard/process.rb
================================================
module SolidQueueDashboard
module Process
# Constants
SUPERVISOR = "Supervisor"
DISPATCHER = "Dispatcher"
WORKER = "Worker"
SCHEDULER = "Scheduler"
KINDS = [ SUPERVISOR, DISPATCHER, WORKER, SCHEDULER ]
KIND_COLORS = {
SUPERVISOR => "yellow",
DISPATCHER => "green",
WORKER => "sky",
SCHEDULER => "purple"
}
HEARTBEAT_DEAD_THRESHOLD = 3.minutes
def self.kind_color(kind)
KIND_COLORS[kind] || "zinc"
end
end
end
================================================
FILE: lib/solid_queue_dashboard/recurring_task.rb
================================================
module SolidQueueDashboard
module RecurringTask
# Constants
COMMAND = :command
JOB = :job
TYPES = [ COMMAND, JOB ]
TYPE_COLORS = {
COMMAND => "amber",
JOB => "sky"
}
end
end
================================================
FILE: lib/solid_queue_dashboard/version.rb
================================================
# frozen_string_literal: true
module SolidQueueDashboard
VERSION = "0.2.0"
end
================================================
FILE: lib/solid_queue_dashboard.rb
================================================
# frozen_string_literal: true
require "rails"
require "groupdate"
require "chartkick"
require_relative "solid_queue_dashboard/version"
require_relative "solid_queue_dashboard/configuration"
require_relative "solid_queue_dashboard/engine"
require_relative "solid_queue_dashboard/job"
require_relative "solid_queue_dashboard/process"
require_relative "solid_queue_dashboard/recurring_task"
require_relative "solid_queue_dashboard/decorators/job_decorator"
require_relative "solid_queue_dashboard/decorators/jobs_decorator"
require_relative "solid_queue_dashboard/decorators/process_decorator"
require_relative "solid_queue_dashboard/decorators/processes_decorator"
require_relative "solid_queue_dashboard/decorators/recurring_task_decorator"
require_relative "solid_queue_dashboard/decorators/recurring_tasks_decorator"
module SolidQueueDashboard
class Error < StandardError; end
def self.job_queue_names
SolidQueue::Job.distinct.pluck(:queue_name)
end
def self.job_class_names
SolidQueue::Job.distinct.pluck(:class_name)
end
def self.decorate(object)
case object
when SolidQueue::Job
Decorators::JobDecorator.new(object)
when SolidQueue::Job.const_get(:ActiveRecord_Relation)
Decorators::JobsDecorator.new(object)
when SolidQueue::Process
Decorators::ProcessDecorator.new(object)
when SolidQueue::Process.const_get(:ActiveRecord_Relation)
Decorators::ProcessesDecorator.new(object)
when SolidQueue::RecurringTask
Decorators::RecurringTaskDecorator.new(object)
when SolidQueue::RecurringTask.const_get(:ActiveRecord_Relation)
Decorators::RecurringTasksDecorator.new(object)
else
raise Error, "Cannot decorate #{object.class}"
end
end
end
================================================
FILE: package.json
================================================
{
"private": true,
"scripts": {
"build": "tailwindcss -i ./app/assets/stylesheets/solid_queue_dashboard/tailwind.css -o ./app/assets/stylesheets/solid_queue_dashboard/application.css",
"watch": "tailwindcss -i ./app/assets/stylesheets/solid_queue_dashboard/tailwind.css -o ./app/assets/stylesheets/solid_queue_dashboard/application.css --watch"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"tailwindcss": "^3.4.13",
"tailwindcss-animate": "^1.0.7"
}
}
================================================
FILE: sig/solid_queue_dashboard.rbs
================================================
module SolidQueueDashboard
VERSION: String
# See the writing guide of rbs: https://github.com/ruby/rbs#guides
end
================================================
FILE: solid_queue_dashboard.gemspec
================================================
# frozen_string_literal: true
require_relative "lib/solid_queue_dashboard/version"
Gem::Specification.new do |spec|
spec.name = "solid_queue_dashboard"
spec.version = SolidQueueDashboard::VERSION
spec.authors = [ "Andrew Kodkod" ]
spec.email = [ "andrew@kodkod.me" ]
spec.summary = "Solid Queue Dashboard"
spec.description = "Dashboard for Solid Queue"
spec.homepage = "https://github.com/akodkod/solid_queue_dashboard"
spec.license = "MIT"
spec.required_ruby_version = ">= 3.0.0"
spec.metadata["allowed_push_host"] = "https://rubygems.org"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/akodkod/solid_queue_dashboard"
spec.metadata["changelog_uri"] = "https://github.com/akodkod/solid_queue_dashboard/CHANGELOG.md"
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
gemspec = File.basename(__FILE__)
spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
ls.readlines("\x0", chomp: true).reject do |filename|
(filename == gemspec) ||
filename.start_with?(*%w[bin/ test/ spec/ features/ test_app/ .git .github appveyor Gemfile]) ||
filename.end_with?(".gem") # Exclude gem files
end
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = [ "lib" ]
spec.add_dependency "solid_queue", ">= 1.0.0"
spec.add_dependency "groupdate", ">= 6.5"
spec.add_dependency "chartkick", ">= 5.0"
end
================================================
FILE: tailwind.config.js
================================================
const { fontFamily } = require("tailwindcss/defaultTheme")
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
"./app/views/**/*.html.erb",
"./app/helpers/**/*.rb",
"./app/javascript/**/*.js",
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: `var(--radius)`,
md: `calc(var(--radius) - 2px)`,
sm: "calc(var(--radius) - 4px)",
},
opacity: {
7.5: "0.075",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [
require("tailwindcss-animate"),
require("@tailwindcss/forms")({ strategy: "class" }),
],
}
================================================
FILE: test/test_helper.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
require "solid_queue_dashboard"
require "minitest/autorun"
================================================
FILE: test/test_solid_queue_dashboard.rb
================================================
# frozen_string_literal: true
require "test_helper"
class TestSolidQueueDashboard < Minitest::Test
def test_that_it_has_a_version_number
assert ::SolidQueueDashboard::VERSION
end
end
================================================
FILE: test_app/.ruby-version
================================================
3.3.4
================================================
FILE: test_app/Gemfile
================================================
source "https://rubygems.org"
gem "rails", "~> 8.0.0.beta1"
gem "sqlite3", ">= 2.1"
gem "puma", ">= 5.0"
gem "propshaft"
gem "solid_queue"
gem 'solid_queue_dashboard', path: '..'
gem "thruster", require: false
gem "ostruct"
gem "tzinfo-data", platforms: %i[ windows jruby ]
group :development, :test do
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
end
================================================
FILE: test_app/README.md
================================================
# Test App
================================================
FILE: test_app/Rakefile
================================================
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative "config/application"
Rails.application.load_tasks
================================================
FILE: test_app/app/controllers/application_controller.rb
================================================
class ApplicationController < ActionController::API
end
================================================
FILE: test_app/app/controllers/concerns/.keep
================================================
================================================
FILE: test_app/app/jobs/accept_arguments_job.rb
================================================
class AcceptArgumentsJob < ApplicationJob
queue_as :default
def perform(*args)
# Do something later
end
end
================================================
FILE: test_app/app/jobs/always_fail_job.rb
================================================
class AlwaysFailJob < ApplicationJob
queue_as :default
def perform(*args)
raise "Sorry, I always fail :)"
end
end
================================================
FILE: test_app/app/jobs/application_job.rb
================================================
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked
# Most jobs are safe to ignore if the underlying records are no longer available
# discard_on ActiveJob::DeserializationError
end
================================================
FILE: test_app/app/jobs/few_seconds_job.rb
================================================
class FewSecondsJob < ApplicationJob
queue_as :another_queue
def perform(*args)
sleep rand(1..3).seconds
end
end
================================================
FILE: test_app/app/jobs/good_job.rb
================================================
class GoodJob < ApplicationJob
queue_as :default
def perform(*args)
# I'm a good job. I never fail :)
end
end
================================================
FILE: test_app/app/jobs/long_running_job.rb
================================================
class LongRunningJob < ApplicationJob
queue_as :default
def perform(*args)
sleep rand(10..60).minutes
end
end
================================================
FILE: test_app/app/jobs/random_fail_job.rb
================================================
class RandomFailJob < ApplicationJob
queue_as :default
def perform(*args)
return if rand(1..100) <= 50
raise "Sorry, I failed randomly :("
end
end
================================================
FILE: test_app/app/jobs/retrying_job.rb
================================================
class RetryingJob < ApplicationJob
queue_as :default
retry_on StandardError, wait:
gitextract_nv7r3_7n/
├── .github/
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .rubocop.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── Procfile.dev
├── README.md
├── Rakefile
├── app/
│ ├── assets/
│ │ ├── javascripts/
│ │ │ └── solid_queue_dashboard/
│ │ │ ├── alpine.js
│ │ │ └── application.js
│ │ └── stylesheets/
│ │ └── solid_queue_dashboard/
│ │ ├── application.css
│ │ └── tailwind.css
│ ├── controllers/
│ │ └── solid_queue_dashboard/
│ │ ├── appearance_controller.rb
│ │ ├── application_controller.rb
│ │ ├── dashboard_controller.rb
│ │ ├── jobs_controller.rb
│ │ ├── processes_controller.rb
│ │ ├── recurring_tasks_controller.rb
│ │ └── stats_controller.rb
│ ├── helpers/
│ │ └── solid_queue_dashboard/
│ │ ├── appearance_helper.rb
│ │ ├── application_helper.rb
│ │ ├── icons_helper.rb
│ │ ├── jobs_helper.rb
│ │ ├── pagination_helper.rb
│ │ ├── processes_helper.rb
│ │ └── recurring_tasks_helper.rb
│ └── views/
│ ├── layouts/
│ │ └── solid_queue_dashboard/
│ │ └── application.html.erb
│ └── solid_queue_dashboard/
│ ├── application/
│ │ ├── _flash_messages.html.erb
│ │ ├── _footer.html.erb
│ │ ├── _navbar.html.erb
│ │ └── _pagination.html.erb
│ ├── dashboard/
│ │ └── index.html.erb
│ ├── jobs/
│ │ ├── _filters.html.erb
│ │ ├── _table.html.erb
│ │ ├── _table_row.html.erb
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ ├── processes/
│ │ ├── _filters.html.erb
│ │ ├── _table.html.erb
│ │ ├── _table_row.html.erb
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ ├── recurring_tasks/
│ │ ├── _filters.html.erb
│ │ ├── _table.html.erb
│ │ ├── _table_row.html.erb
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ └── stats/
│ └── index.html.erb
├── bin/
│ ├── console
│ ├── dev
│ ├── setup
│ └── setup-test-app
├── bun.lockb
├── config/
│ └── routes.rb
├── lib/
│ ├── solid_queue_dashboard/
│ │ ├── configuration.rb
│ │ ├── decorators/
│ │ │ ├── job_decorator.rb
│ │ │ ├── jobs_decorator.rb
│ │ │ ├── process_decorator.rb
│ │ │ ├── processes_decorator.rb
│ │ │ ├── recurring_task_decorator.rb
│ │ │ └── recurring_tasks_decorator.rb
│ │ ├── engine.rb
│ │ ├── job.rb
│ │ ├── process.rb
│ │ ├── recurring_task.rb
│ │ └── version.rb
│ └── solid_queue_dashboard.rb
├── package.json
├── sig/
│ └── solid_queue_dashboard.rbs
├── solid_queue_dashboard.gemspec
├── tailwind.config.js
├── test/
│ ├── test_helper.rb
│ └── test_solid_queue_dashboard.rb
└── test_app/
├── .ruby-version
├── Gemfile
├── README.md
├── Rakefile
├── app/
│ ├── controllers/
│ │ ├── application_controller.rb
│ │ └── concerns/
│ │ └── .keep
│ ├── jobs/
│ │ ├── accept_arguments_job.rb
│ │ ├── always_fail_job.rb
│ │ ├── application_job.rb
│ │ ├── few_seconds_job.rb
│ │ ├── good_job.rb
│ │ ├── long_running_job.rb
│ │ ├── random_fail_job.rb
│ │ └── retrying_job.rb
│ └── models/
│ ├── application_record.rb
│ └── concerns/
│ └── .keep
├── bin/
│ ├── bundle
│ ├── dev
│ ├── jobs
│ ├── rails
│ ├── rake
│ ├── setup
│ └── thrust
├── config/
│ ├── application.rb
│ ├── boot.rb
│ ├── credentials.yml.enc
│ ├── database.yml
│ ├── environment.rb
│ ├── environments/
│ │ ├── development.rb
│ │ ├── production.rb
│ │ └── test.rb
│ ├── initializers/
│ │ ├── cors.rb
│ │ ├── filter_parameter_logging.rb
│ │ └── inflections.rb
│ ├── locales/
│ │ └── en.yml
│ ├── master.key
│ ├── puma.rb
│ ├── queue.yml
│ ├── recurring.yml
│ └── routes.rb
├── config.ru
├── db/
│ ├── queue_schema.rb
│ ├── schema.rb
│ └── seeds.rb
├── lib/
│ └── tasks/
│ ├── .keep
│ └── jobs.rake
├── public/
│ └── robots.txt
├── script/
│ └── .keep
├── storage/
│ └── .keep
└── vendor/
└── .keep
SYMBOL INDEX (377 symbols across 41 files)
FILE: app/assets/javascripts/solid_queue_dashboard/alpine.js
function qt (line 1) | function qt(e){Cn(e)}
function Cn (line 1) | function Cn(e){U.includes(e)||U.push(e),Tn()}
function Ee (line 1) | function Ee(e){let t=U.indexOf(e);t!==-1&&t>it&&U.splice(t,1)}
function Tn (line 1) | function Tn(){!nt&&!rt&&(rt=!0,queueMicrotask(Rn))}
function Rn (line 1) | function Rn(){rt=!1,nt=!0;for(let e=0;e<U.length;e++)U[e](),it=e;U.lengt...
function Ut (line 1) | function Ut(e){ot=!1,e(),ot=!0}
function Wt (line 1) | function Wt(e){R=e.reactive,L=e.release,D=t=>e.effect(t,{scheduler:r=>{o...
function at (line 1) | function at(e){D=e}
function Gt (line 1) | function Gt(e){let t=()=>{};return[n=>{let i=D(n);return e._x_effects||(...
function ve (line 1) | function ve(e,t){let r=!0,n,i=D(()=>{let o=e();JSON.stringify(o),r?n=o:q...
function Zt (line 1) | function Zt(e){Xt.push(e)}
function ee (line 1) | function ee(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[])...
function Ae (line 1) | function Ae(e){Jt.push(e)}
function Oe (line 1) | function Oe(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e...
function ct (line 1) | function ct(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCl...
function Qt (line 1) | function Qt(e){if(e._x_cleanups)for(;e._x_cleanups.length;)e._x_cleanups...
function le (line 1) | function le(){lt.observe(document,{subtree:!0,childList:!0,attributes:!0...
function ft (line 1) | function ft(){Mn(),lt.disconnect(),ut=!1}
function Mn (line 1) | function Mn(){let e=lt.takeRecords();ce.push(()=>e.length>0&&pt(e));let ...
function _ (line 1) | function _(e){if(!ut)return e();ft();let t=e();return le(),t}
function er (line 1) | function er(){dt=!0}
function tr (line 1) | function tr(){dt=!1,pt(Se),Se=[]}
function pt (line 1) | function pt(e){if(dt){Se=Se.concat(e);return}let t=new Set,r=new Set,n=n...
function Ce (line 1) | function Ce(e){return F(j(e))}
function P (line 1) | function P(e,t,r){return e._x_dataStack=[t,...j(r||e)],()=>{e._x_dataSta...
function j (line 1) | function j(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="f...
function F (line 1) | function F(e){return new Proxy({objects:e},Nn)}
method ownKeys (line 1) | ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(...
method has (line 1) | has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prot...
method get (line 1) | get({objects:e},t,r){return t=="toJSON"?Dn:Reflect.get(e.find(n=>Reflect...
method set (line 1) | set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.c...
function Dn (line 1) | function Dn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.g...
function Te (line 1) | function Te(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,...
function Re (line 1) | function Re(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,ini...
function Pn (line 1) | function Pn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}
function mt (line 1) | function mt(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)...
function y (line 1) | function y(e,t){rr[e]=t}
function ue (line 1) | function ue(e,t){return Object.entries(rr).forEach(([r,n])=>{let i=null;...
function nr (line 1) | function nr(e,t,r,...n){try{return r(...n)}catch(i){te(i,e,t)}}
function te (line 1) | function te(e,t,r=void 0){e=Object.assign(e??{message:"No error message ...
function De (line 5) | function De(e){let t=Me;Me=!1;let r=e();return Me=t,r}
function M (line 5) | function M(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}
function x (line 5) | function x(...e){return ir(...e)}
function or (line 5) | function or(e){ir=e}
function gt (line 5) | function gt(e,t){let r={};ue(r,e);let n=[r,...j(e)],i=typeof t=="functio...
function In (line 5) | function In(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t....
function kn (line 5) | function kn(e,t){if(ht[e])return ht[e];let r=Object.getPrototypeOf(async...
function Ln (line 5) | function Ln(e,t,r){let n=kn(t,r);return(i=()=>{},{scope:o={},params:s=[]...
function Ne (line 5) | function Ne(e,t,r,n,i){if(Me&&typeof t=="function"){let o=t.apply(r,n);o...
function C (line 5) | function C(e=""){return bt+e}
function sr (line 5) | function sr(e){bt=e}
function d (line 5) | function d(e,t){return Pe[e]=t,{before(r){if(!Pe[r]){console.warn(String...
function ar (line 5) | function ar(e){return Object.keys(Pe).includes(e)}
function de (line 5) | function de(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Obje...
function wt (line 5) | function wt(e){return Array.from(e).map(ur()).filter(t=>!dr(t))}
function lr (line 5) | function lr(e){xt=!0;let t=Symbol();cr=t,fe.set(t,[]);let r=()=>{for(;fe...
function _t (line 5) | function _t(e){let t=[],r=a=>t.push(a),[n,i]=Gt(e);return t.push(i),[{Al...
function $n (line 5) | function $n(e,t){let r=()=>{},n=Pe[t.type]||r,[i,o]=_t(e);Oe(e,t.origina...
function ur (line 5) | function ur(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=fr....
function re (line 5) | function re(e){fr.push(e)}
function dr (line 5) | function dr({name:e}){return pr().test(e)}
function jn (line 5) | function jn(e,t){return({name:r,value:n})=>{let i=r.match(pr()),o=r.matc...
function Fn (line 5) | function Fn(e,t){let r=W.indexOf(e.type)===-1?yt:e.type,n=W.indexOf(t.ty...
function G (line 5) | function G(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles...
function T (line 5) | function T(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoo...
function E (line 5) | function E(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}
function _r (line 5) | function _r(){mr&&E("Alpine has already been initialized on this page. C...
function gr (line 5) | function gr(){return Et.map(e=>e())}
function xr (line 5) | function xr(){return Et.concat(hr).map(e=>e())}
function Le (line 5) | function Le(e){Et.push(e)}
function $e (line 5) | function $e(e){hr.push(e)}
function J (line 5) | function J(e,t=!1){return z(e,r=>{if((t?xr():gr()).some(i=>r.matches(i))...
function z (line 5) | function z(e,t){if(e){if(t(e))return e;if(e._x_teleportBack&&(e=e._x_tel...
function yr (line 5) | function yr(e){return gr().some(t=>e.matches(t))}
function wr (line 5) | function wr(e){br.push(e)}
function S (line 5) | function S(e,t=T,r=()=>{}){lr(()=>{t(e,(n,i)=>{r(n,i),br.forEach(o=>o(n,...
function vt (line 5) | function vt(e,t=T){t(e,r=>{ct(r),Qt(r)})}
function Bn (line 5) | function Bn(){[["ui","dialog",["[x-dialog], [x-popover]"]],["anchor","an...
function ne (line 5) | function ne(e=()=>{}){return queueMicrotask(()=>{At||setTimeout(()=>{je(...
function je (line 5) | function je(){for(At=!1;St.length;)St.shift()()}
function Er (line 5) | function Er(){At=!0}
function pe (line 5) | function pe(e,t){return Array.isArray(t)?vr(e,t.join(" ")):typeof t=="ob...
function vr (line 5) | function vr(e,t){let r=o=>o.split(" ").filter(Boolean),n=o=>o.split(" ")...
function zn (line 5) | function zn(e,t){let r=a=>a.split(" ").filter(Boolean),n=Object.entries(...
function Y (line 5) | function Y(e,t){return typeof t=="object"&&t!==null?Kn(e,t):Hn(e,t)}
function Kn (line 5) | function Kn(e,t){let r={};return Object.entries(t).forEach(([n,i])=>{r[n...
function Hn (line 5) | function Hn(e,t){let r=e.getAttribute("style",t);return e.setAttribute("...
function Vn (line 5) | function Vn(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}
function me (line 5) | function me(e,t=()=>{}){let r=!1;return function(){r?t.apply(this,argume...
function qn (line 5) | function qn(e,t,r){Sr(e,pe,""),{enter:i=>{e._x_transition.enter.during=i...
function Un (line 5) | function Un(e,t,r){Sr(e,Y);let n=!t.includes("in")&&!t.includes("out")&&...
function Sr (line 5) | function Sr(e,t,r={}){e._x_transition||(e._x_transition={enter:{during:r...
function Ar (line 5) | function Ar(e){let t=e.parentNode;if(t)return t._x_hidePromise?t:Ar(t)}
function Fe (line 5) | function Fe(e,t,{during:r,start:n,end:i}={},o=()=>{},s=()=>{}){if(e._x_t...
function Wn (line 5) | function Wn(e,t){let r,n,i,o=me(()=>{_(()=>{r=!0,n||t.before(),i||(t.end...
function _e (line 5) | function _e(e,t,r){if(e.indexOf(t)===-1)return r;let n=e[e.indexOf(t)+1]...
function A (line 5) | function A(e,t=()=>{}){return(...r)=>I?t(...r):e(...r)}
function Or (line 5) | function Or(e){return(...t)=>I&&e(...t)}
function K (line 5) | function K(e){Cr.push(e)}
function Tr (line 5) | function Tr(e,t){Cr.forEach(r=>r(e,t)),I=!0,Mr(()=>{S(t,(r,n)=>{n(r,()=>...
function Rr (line 5) | function Rr(e,t){t._x_dataStack||(t._x_dataStack=e._x_dataStack),I=!0,Be...
function Gn (line 5) | function Gn(e){let t=!1;S(e,(n,i)=>{T(n,(o,s)=>{if(t&&yr(o))return s();t...
function Mr (line 5) | function Mr(e){let t=D;at((r,n)=>{let i=t(r);return L(i),()=>{}}),e(),at...
function he (line 5) | function he(e,t,r,n=[]){switch(e._x_bindings||(e._x_bindings=R({})),e._x...
function Jn (line 5) | function Jn(e,t){if(e.type==="radio")e.attributes.value===void 0&&(e.val...
function Yn (line 5) | function Yn(e,t){e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_und...
function Xn (line 5) | function Xn(e,t){e._x_undoAddedStyles&&e._x_undoAddedStyles(),e._x_undoA...
function Zn (line 5) | function Zn(e,t,r){Dr(e,t,r),ei(e,t,r)}
function Dr (line 5) | function Dr(e,t,r){[null,void 0,!1].includes(r)&&ni(t)?e.removeAttribute...
function Qn (line 5) | function Qn(e,t,r){e.getAttribute(t)!=r&&e.setAttribute(t,r)}
function ei (line 5) | function ei(e,t,r){e[t]!==r&&(e[t]=r)}
function ti (line 5) | function ti(e,t){let r=[].concat(t).map(n=>n+"");Array.from(e.options).f...
function ri (line 5) | function ri(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperC...
function Nr (line 5) | function Nr(e,t){return e==t}
function ge (line 5) | function ge(e){return[1,"1","true","on","yes",!0].includes(e)?!0:[0,"0",...
function Pr (line 5) | function Pr(e){return["disabled","checked","required","readonly","open",...
function ni (line 5) | function ni(e){return!["aria-pressed","aria-checked","aria-expanded","ar...
function Ir (line 5) | function Ir(e,t,r){return e._x_bindings&&e._x_bindings[t]!==void 0?e._x_...
function kr (line 5) | function kr(e,t,r,n=!0){if(e._x_bindings&&e._x_bindings[t]!==void 0)retu...
function Lr (line 5) | function Lr(e,t,r){let n=e.getAttribute(t);return n===null?typeof r=="fu...
function ze (line 5) | function ze(e,t){var r;return function(){var n=this,i=arguments,o=functi...
function Ke (line 5) | function Ke(e,t){let r;return function(){let n=this,i=arguments;r||(e.ap...
function He (line 5) | function He({get:e,set:t},{get:r,set:n}){let i=!0,o,s,a=D(()=>{let c=e()...
function Ot (line 5) | function Ot(e){return typeof e=="object"?JSON.parse(JSON.stringify(e)):e}
function $r (line 5) | function $r(e){(Array.isArray(e)?e:[e]).forEach(r=>r(B))}
function Fr (line 5) | function Fr(e,t){if(jr||(X=R(X),jr=!0),t===void 0)return X[e];X[e]=t,typ...
function Br (line 5) | function Br(){return X}
function Kr (line 5) | function Kr(e,t){let r=typeof t!="function"?()=>t:t;return e instanceof ...
function Hr (line 5) | function Hr(e){return Object.entries(zr).forEach(([t,r])=>{Object.define...
function Ct (line 5) | function Ct(e,t,r){let n=[];for(;n.length;)n.pop()();let i=Object.entrie...
function qr (line 5) | function qr(e,t){Vr[e]=t}
function Ur (line 5) | function Ur(e,t){return Object.entries(Vr).forEach(([r,n])=>{Object.defi...
method reactive (line 5) | get reactive(){return R}
method release (line 5) | get release(){return L}
method effect (line 5) | get effect(){return D}
method raw (line 5) | get raw(){return st}
function Tt (line 5) | function Tt(e,t){let r=Object.create(null),n=e.split(",");for(let i=0;i<...
function fi (line 5) | function fi(e){return e&&e._isEffect===!0}
function en (line 5) | function en(e,t=Wr){fi(e)&&(e=e.raw);let r=pi(e,t);return t.lazy||r(),r}
function tn (line 5) | function tn(e){e.active&&(rn(e),e.options.onStop&&e.options.onStop(),e.a...
function pi (line 5) | function pi(e,t){let r=function(){if(!r.active)return e();if(!be.include...
function rn (line 5) | function rn(e){let{deps:t}=e;if(t.length){for(let r=0;r<t.length;r++)t[r...
function mi (line 5) | function mi(){kt.push(oe),oe=!1}
function _i (line 5) | function _i(){kt.push(oe),oe=!0}
function nn (line 5) | function nn(){let e=kt.pop();oe=e===void 0?!0:e}
function N (line 5) | function N(e,t,r){if(!oe||k===void 0)return;let n=Dt.get(e);n||Dt.set(e,...
function q (line 5) | function q(e,t,r,n,i,o){let s=Dt.get(e);if(!s)return;let a=new Set,c=u=>...
function yi (line 5) | function yi(){let e={};return["includes","indexOf","lastIndexOf"].forEac...
function sn (line 5) | function sn(e=!1,t=!1){return function(n,i,o){if(i==="__v_isReactive")re...
function wi (line 5) | function wi(e=!1){return function(r,n,i,o){let s=r[n];if(!e&&(i=h(i),s=h...
function Ei (line 5) | function Ei(e,t){let r=xe(e,t),n=e[t],i=Reflect.deleteProperty(e,t);retu...
function vi (line 5) | function vi(e,t){let r=Reflect.has(e,t);return(!Ve(t)||!on.has(t))&&N(e,...
function Si (line 5) | function Si(e){return N(e,"iterate",H(e)?"length":Z),Reflect.ownKeys(e)}
method set (line 5) | set(e,t){return console.warn(`Set operation on key "${String(t)}" failed...
method deleteProperty (line 5) | deleteProperty(e,t){return console.warn(`Delete operation on key "${Stri...
function We (line 5) | function We(e,t,r=!1,n=!1){e=e.__v_raw;let i=h(e),o=h(t);t!==o&&!r&&N(i,...
function Ge (line 5) | function Ge(e,t=!1){let r=this.__v_raw,n=h(r),i=h(e);return e!==i&&!t&&N...
function Je (line 5) | function Je(e,t=!1){return e=e.__v_raw,!t&&N(h(e),"iterate",Z),Reflect.g...
function Yr (line 5) | function Yr(e){e=h(e);let t=h(this);return Ze(t).has.call(t,e)||(t.add(e...
function Xr (line 5) | function Xr(e,t){t=h(t);let r=h(this),{has:n,get:i}=Ze(r),o=n.call(r,e);...
function Zr (line 5) | function Zr(e){let t=h(this),{has:r,get:n}=Ze(t),i=r.call(t,e);i?cn(t,r,...
function Qr (line 5) | function Qr(){let e=h(this),t=e.size!==0,r=ie(e)?new Map(e):new Set(e),n...
function Ye (line 5) | function Ye(e,t){return function(n,i){let o=this,s=o.__v_raw,a=h(s),c=t?...
function Xe (line 5) | function Xe(e,t,r){return function(...n){let i=this.__v_raw,o=h(i),s=ie(...
function V (line 5) | function V(e){return function(...t){{let r=t[0]?`on key "${t[0]}" `:"";c...
function Ci (line 5) | function Ci(){let e={get(o){return We(this,o)},get size(){return Je(this...
function an (line 5) | function an(e,t){let r=t?e?Ni:Mi:e?Ri:Ti;return(n,i,o)=>i==="__v_isReact...
function cn (line 5) | function cn(e,t,r){let n=h(r);if(n!==r&&t.call(e,n)){let i=Rt(e);console...
function Li (line 5) | function Li(e){switch(e){case"Object":case"Array":return 1;case"Map":cas...
function $i (line 5) | function $i(e){return e.__v_skip||!Object.isExtensible(e)?0:Li(Rt(e))}
function Qe (line 5) | function Qe(e){return e&&e.__v_isReadonly?e:dn(e,!1,Ai,Di,ln)}
function fn (line 5) | function fn(e){return dn(e,!0,Oi,Pi,un)}
function dn (line 5) | function dn(e,t,r,n,i){if(!ye(e))return console.warn(`value cannot be ma...
function h (line 5) | function h(e){return e&&h(e.__v_raw)||e}
function It (line 5) | function It(e){return Boolean(e&&e.__v_isRef===!0)}
function ji (line 5) | function ji(e){let t=[];return z(e,r=>{r._x_refs&&t.push(r._x_refs)}),t}
function Bt (line 5) | function Bt(e){return Ft[e]||(Ft[e]=0),++Ft[e]}
function pn (line 5) | function pn(e,t){return z(e,r=>{if(r._x_ids&&r._x_ids[t])return!0})}
function mn (line 5) | function mn(e,t){e._x_ids||(e._x_ids={}),e._x_ids[t]||(e._x_ids[t]=Bt(t))}
function Fi (line 5) | function Fi(e,t,r,n){if(e._x_id||(e._x_id={}),e._x_id[t])return e._x_id[...
function _n (line 5) | function _n(e,t,r){y(t,n=>E(`You can't use [$${t}] without first install...
method get (line 5) | get(){return u()}
method set (line 5) | set(w){p(w)}
method get (line 5) | get(){return s()}
method set (line 5) | set(w){c(w)}
function hn (line 5) | function hn(e){let t=A(()=>document.querySelector(e),()=>Bi)();return t|...
function se (line 5) | function se(e,t,r,n){let i=e,o=c=>n(c),s={},a=(c,l)=>u=>l(c,u);if(r.incl...
function zi (line 5) | function zi(e){return e.replace(/-/g,".")}
function Ki (line 5) | function Ki(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperC...
function et (line 5) | function et(e){return!Array.isArray(e)&&!isNaN(e)}
function Hi (line 5) | function Hi(e){return[" ","_"].includes(e)?e:e.replace(/([a-z])([A-Z])/g...
function Vi (line 5) | function Vi(e){return["keydown","keyup"].includes(e)}
function yn (line 5) | function yn(e){return["contextmenu","click","mouse"].some(t=>e.includes(...
function qi (line 5) | function qi(e,t){let r=t.filter(o=>!["window","document","prevent","stop...
function xn (line 5) | function xn(e){if(!e)return[];e=Hi(e);let t={ctrl:"control",slash:"/",sp...
method get (line 5) | get(){return c()}
method set (line 5) | set(m){l(m)}
function zt (line 5) | function zt(e,t,r,n){return _(()=>{if(r instanceof CustomEvent&&r.detail...
function Kt (line 5) | function Kt(e){let t=e?parseFloat(e):null;return Wi(t)?t:e}
function Ui (line 5) | function Ui(e,t){return e==t}
function Wi (line 5) | function Wi(e){return!Array.isArray(e)&&!isNaN(e)}
function bn (line 5) | function bn(e){return e!==null&&typeof e=="object"&&typeof e.get=="funct...
function Gi (line 5) | function Gi(e,t){e._x_keyExpression=t}
function Ji (line 5) | function Ji(e){return I?Be?!0:e.hasAttribute("data-has-alpine-state"):!1}
function Yi (line 5) | function Yi(e,t,r,n){let i=s=>typeof s=="object"&&!Array.isArray(s),o=e;...
function Xi (line 5) | function Xi(e){let t=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,r=/^\s*\(|\)\s*$/g...
function En (line 5) | function En(e,t,r,n){let i={};return/^\[.*\]$/.test(e.item)&&Array.isArr...
function Zi (line 5) | function Zi(e){return!Array.isArray(e)&&!isNaN(e)}
function vn (line 5) | function vn(){}
function tt (line 5) | function tt(e,t,r){d(t,n=>E(`You can't use [x-${t}] without first instal...
FILE: app/assets/javascripts/solid_queue_dashboard/application.js
function refreshHomePage (line 56) | function refreshHomePage() {
FILE: app/controllers/solid_queue_dashboard/appearance_controller.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
class AppearanceController (line 2) | class AppearanceController < ApplicationController
method toggle (line 3) | def toggle
FILE: app/controllers/solid_queue_dashboard/application_controller.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
class ApplicationController (line 2) | class ApplicationController < ActionController::Base
FILE: app/controllers/solid_queue_dashboard/dashboard_controller.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
class DashboardController (line 2) | class DashboardController < ApplicationController
method index (line 3) | def index
method load_charts (line 10) | def load_charts
FILE: app/controllers/solid_queue_dashboard/jobs_controller.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
class JobsController (line 2) | class JobsController < ApplicationController
method index (line 6) | def index
method show (line 11) | def show
method retry (line 21) | def retry
method set_jobs (line 28) | def set_jobs
method set_job (line 38) | def set_job
FILE: app/controllers/solid_queue_dashboard/processes_controller.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
class ProcessesController (line 2) | class ProcessesController < ApplicationController
method index (line 6) | def index
method show (line 11) | def show
method set_processes (line 16) | def set_processes
method set_process (line 24) | def set_process
FILE: app/controllers/solid_queue_dashboard/recurring_tasks_controller.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
class RecurringTasksController (line 2) | class RecurringTasksController < ApplicationController
method index (line 6) | def index
method show (line 9) | def show
method enqueue (line 12) | def enqueue
method set_recurring_tasks (line 19) | def set_recurring_tasks
method set_recurring_task (line 26) | def set_recurring_task
FILE: app/controllers/solid_queue_dashboard/stats_controller.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
class StatsController (line 2) | class StatsController < ApplicationController
method index (line 3) | def index
FILE: app/helpers/solid_queue_dashboard/appearance_helper.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type AppearanceHelper (line 2) | module AppearanceHelper
function dark_mode? (line 3) | def dark_mode?
FILE: app/helpers/solid_queue_dashboard/application_helper.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type ApplicationHelper (line 2) | module ApplicationHelper
function empty_value (line 3) | def empty_value
FILE: app/helpers/solid_queue_dashboard/icons_helper.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type IconsHelper (line 2) | module IconsHelper
function icon_refresh_cw (line 3) | def icon_refresh_cw(options = {})
function icon_triangle_alert (line 24) | def icon_triangle_alert(options = {})
function icon_server (line 44) | def icon_server(options = {})
function icon_layout_dashboard (line 65) | def icon_layout_dashboard(options = {})
function icon_logs (line 86) | def icon_logs(options = {})
function icon_clock (line 112) | def icon_clock(options = {})
function icon_github (line 131) | def icon_github(options = {})
function icon_x (line 150) | def icon_x(options = {})
function icon_moon (line 169) | def icon_moon(options = {})
function icon_sun (line 187) | def icon_sun(options = {})
function icon_play (line 213) | def icon_play(options = {})
function icon_chart_scatter (line 231) | def icon_chart_scatter(options = {})
FILE: app/helpers/solid_queue_dashboard/jobs_helper.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type JobsHelper (line 2) | module JobsHelper
function job_status_circle (line 3) | def job_status_circle(status, options = {})
function job_status_circle_class (line 8) | def job_status_circle_class(status)
function job_status_badge (line 21) | def job_status_badge(status, options = {})
function job_status_badge_class (line 26) | def job_status_badge_class(status)
function format_failure_rate (line 39) | def format_failure_rate(failure_rate, options = {})
function any_jobs_filters? (line 53) | def any_jobs_filters?
FILE: app/helpers/solid_queue_dashboard/pagination_helper.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type PaginationHelper (line 2) | module PaginationHelper
function paginate (line 3) | def paginate(scope, page:, per_page:)
function page_range (line 22) | def page_range(current_page, total_pages, window: 2)
FILE: app/helpers/solid_queue_dashboard/processes_helper.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type ProcessesHelper (line 2) | module ProcessesHelper
function process_kind_circle (line 3) | def process_kind_circle(kind, options = {})
function process_kind_circle_class (line 8) | def process_kind_circle_class(kind)
function process_kind_badge (line 18) | def process_kind_badge(kind, options = {})
function process_kind_badge_class (line 23) | def process_kind_badge_class(kind)
function any_processes_filters? (line 33) | def any_processes_filters?
FILE: app/helpers/solid_queue_dashboard/recurring_tasks_helper.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type RecurringTasksHelper (line 2) | module RecurringTasksHelper
function recurring_task_circle (line 3) | def recurring_task_circle(type, options = {})
function recurring_task_circle_class (line 8) | def recurring_task_circle_class(type)
function recurring_task_type_badge (line 16) | def recurring_task_type_badge(type, options = {})
function recurring_task_type_badge_class (line 21) | def recurring_task_type_badge_class(type)
function any_recurring_tasks_filters? (line 29) | def any_recurring_tasks_filters?
FILE: lib/solid_queue_dashboard.rb
type SolidQueueDashboard (line 19) | module SolidQueueDashboard
class Error (line 20) | class Error < StandardError; end
function job_queue_names (line 22) | def self.job_queue_names
function job_class_names (line 26) | def self.job_class_names
function decorate (line 30) | def self.decorate(object)
FILE: lib/solid_queue_dashboard/configuration.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
class Configuration (line 2) | class Configuration
method initialize (line 5) | def initialize
function configuration (line 10) | def self.configuration
function configure (line 14) | def self.configure
FILE: lib/solid_queue_dashboard/decorators/job_decorator.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type Decorators (line 2) | module Decorators
class JobDecorator (line 3) | class JobDecorator < SimpleDelegator
method color (line 4) | def color
method status (line 8) | def status
method running? (line 26) | def running?
method success? (line 31) | def success?
method retried? (line 36) | def retried?
method failed? (line 42) | def failed?
method scheduled? (line 47) | def scheduled?
method pending? (line 52) | def pending?
method execution_history (line 57) | def execution_history
method error_message (line 61) | def error_message
FILE: lib/solid_queue_dashboard/decorators/jobs_decorator.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type Decorators (line 2) | module Decorators
class JobsDecorator (line 3) | class JobsDecorator < SimpleDelegator
method with_status (line 4) | def with_status(status)
method running (line 23) | def running
method success (line 27) | def success
method scheduled (line 33) | def scheduled
method pending (line 37) | def pending
method retried (line 43) | def retried
method failure_rate (line 59) | def failure_rate
method each (line 70) | def each
method to_a (line 76) | def to_a
FILE: lib/solid_queue_dashboard/decorators/process_decorator.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type Decorators (line 2) | module Decorators
class ProcessDecorator (line 3) | class ProcessDecorator < SimpleDelegator
method color (line 4) | def color
method dead? (line 8) | def dead?
FILE: lib/solid_queue_dashboard/decorators/processes_decorator.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type Decorators (line 2) | module Decorators
class ProcessesDecorator (line 3) | class ProcessesDecorator < SimpleDelegator
method each (line 4) | def each
method to_a (line 10) | def to_a
FILE: lib/solid_queue_dashboard/decorators/recurring_task_decorator.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type Decorators (line 2) | module Decorators
class RecurringTaskDecorator (line 3) | class RecurringTaskDecorator < SimpleDelegator
method type (line 4) | def type
method next_runs (line 14) | def next_runs(count: 5)
FILE: lib/solid_queue_dashboard/decorators/recurring_tasks_decorator.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type Decorators (line 2) | module Decorators
class RecurringTasksDecorator (line 3) | class RecurringTasksDecorator < SimpleDelegator
method with_type (line 4) | def with_type(type)
method each (line 15) | def each
method to_a (line 21) | def to_a
FILE: lib/solid_queue_dashboard/engine.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
class Engine (line 2) | class Engine < ::Rails::Engine
FILE: lib/solid_queue_dashboard/job.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type Job (line 2) | module Job
function status_color (line 24) | def self.status_color(status)
FILE: lib/solid_queue_dashboard/process.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type Process (line 2) | module Process
function kind_color (line 20) | def self.kind_color(kind)
FILE: lib/solid_queue_dashboard/recurring_task.rb
type SolidQueueDashboard (line 1) | module SolidQueueDashboard
type RecurringTask (line 2) | module RecurringTask
FILE: lib/solid_queue_dashboard/version.rb
type SolidQueueDashboard (line 3) | module SolidQueueDashboard
FILE: test/test_solid_queue_dashboard.rb
class TestSolidQueueDashboard (line 5) | class TestSolidQueueDashboard < Minitest::Test
method test_that_it_has_a_version_number (line 6) | def test_that_it_has_a_version_number
FILE: test_app/app/controllers/application_controller.rb
class ApplicationController (line 1) | class ApplicationController < ActionController::API
FILE: test_app/app/jobs/accept_arguments_job.rb
class AcceptArgumentsJob (line 1) | class AcceptArgumentsJob < ApplicationJob
method perform (line 4) | def perform(*args)
FILE: test_app/app/jobs/always_fail_job.rb
class AlwaysFailJob (line 1) | class AlwaysFailJob < ApplicationJob
method perform (line 4) | def perform(*args)
FILE: test_app/app/jobs/application_job.rb
class ApplicationJob (line 1) | class ApplicationJob < ActiveJob::Base
FILE: test_app/app/jobs/few_seconds_job.rb
class FewSecondsJob (line 1) | class FewSecondsJob < ApplicationJob
method perform (line 4) | def perform(*args)
FILE: test_app/app/jobs/good_job.rb
class GoodJob (line 1) | class GoodJob < ApplicationJob
method perform (line 4) | def perform(*args)
FILE: test_app/app/jobs/long_running_job.rb
class LongRunningJob (line 1) | class LongRunningJob < ApplicationJob
method perform (line 4) | def perform(*args)
FILE: test_app/app/jobs/random_fail_job.rb
class RandomFailJob (line 1) | class RandomFailJob < ApplicationJob
method perform (line 4) | def perform(*args)
FILE: test_app/app/jobs/retrying_job.rb
class RetryingJob (line 1) | class RetryingJob < ApplicationJob
method perform (line 5) | def perform(*args)
FILE: test_app/app/models/application_record.rb
class ApplicationRecord (line 1) | class ApplicationRecord < ActiveRecord::Base
FILE: test_app/config/application.rb
type TestApp (line 21) | module TestApp
class Application (line 22) | class Application < Rails::Application
Condensed preview — 125 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (233K chars).
[
{
"path": ".github/workflows/main.yml",
"chars": 448,
"preview": "name: Ruby\n\non:\n push:\n branches:\n - main\n\n pull_request:\n\njobs:\n build:\n runs-on: ubuntu-latest\n name:"
},
{
"path": ".gitignore",
"chars": 191,
"preview": "/.bundle/\n/.yardoc\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\n/.idea/\n/node_modules/\n/test_app/storage/*.sqli"
},
{
"path": ".rubocop.yml",
"chars": 208,
"preview": "inherit_gem:\n rubocop-rails-omakase: rubocop.yml\n\nAllCops:\n TargetRubyVersion: 3.0\n\nStyle/StringLiterals:\n EnforcedSt"
},
{
"path": "CHANGELOG.md",
"chars": 363,
"preview": "## [Unreleased]\n\n## [0.2.0] - October 17, 2024\n\n- Show running jobs\n- Add charts\n- Add auto-refresh\n- Add number of curr"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 5487,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
},
{
"path": "Gemfile",
"chars": 275,
"preview": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\n# Specify your gem's dependencies in solid_queue_dashboard"
},
{
"path": "LICENSE.txt",
"chars": 1080,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2024 Andrew Kodkod\n\nPermission is hereby granted, free of charge, to any person obt"
},
{
"path": "Procfile.dev",
"chars": 114,
"preview": "test_app: cd test_app && ./bin/rails server -p 3000\njobs: cd test_app && ./bin/jobs\ntailwind: bun watch\n"
},
{
"path": "README.md",
"chars": 1974,
"preview": "# Solid Queue Dashboard <sup>BETA</sup>\n\n<p align=\"center\">\n <a href=\"https://github.com/akodkod/solid-queue-dashboard#"
},
{
"path": "Rakefile",
"chars": 199,
"preview": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"minitest/test_task\"\n\nMinitest::TestTask.create\n\nrequ"
},
{
"path": "app/assets/javascripts/solid_queue_dashboard/alpine.js",
"chars": 44659,
"preview": "(()=>{var rt=!1,nt=!1,U=[],it=-1;function qt(e){Cn(e)}function Cn(e){U.includes(e)||U.push(e),Tn()}function Ee(e){let t="
},
{
"path": "app/assets/javascripts/solid_queue_dashboard/application.js",
"chars": 1799,
"preview": "document.addEventListener('DOMContentLoaded', function() {\n // Handle clickable rows\n document.body.addEventListener('"
},
{
"path": "app/assets/stylesheets/solid_queue_dashboard/application.css",
"chars": 40889,
"preview": "*, ::before, ::after {\n --tw-border-spacing-x: 0;\n --tw-border-spacing-y: 0;\n --tw-translate-x: 0;\n --tw-translate-y"
},
{
"path": "app/assets/stylesheets/solid_queue_dashboard/tailwind.css",
"chars": 12076,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n :root {\n --background: 0 0% 98%;\n --fo"
},
{
"path": "app/controllers/solid_queue_dashboard/appearance_controller.rb",
"chars": 224,
"preview": "module SolidQueueDashboard\n class AppearanceController < ApplicationController\n def toggle\n cookies[:dark_mode]"
},
{
"path": "app/controllers/solid_queue_dashboard/application_controller.rb",
"chars": 190,
"preview": "module SolidQueueDashboard\n class ApplicationController < ActionController::Base\n include SolidQueueDashboard::Pagin"
},
{
"path": "app/controllers/solid_queue_dashboard/dashboard_controller.rb",
"chars": 1475,
"preview": "module SolidQueueDashboard\n class DashboardController < ApplicationController\n def index\n @jobs = SolidQueueDas"
},
{
"path": "app/controllers/solid_queue_dashboard/jobs_controller.rb",
"chars": 1300,
"preview": "module SolidQueueDashboard\n class JobsController < ApplicationController\n before_action :set_jobs, only: [ :index ]\n"
},
{
"path": "app/controllers/solid_queue_dashboard/processes_controller.rb",
"chars": 857,
"preview": "module SolidQueueDashboard\n class ProcessesController < ApplicationController\n before_action :set_processes, only: ["
},
{
"path": "app/controllers/solid_queue_dashboard/recurring_tasks_controller.rb",
"chars": 901,
"preview": "module SolidQueueDashboard\n class RecurringTasksController < ApplicationController\n before_action :set_recurring_tas"
},
{
"path": "app/controllers/solid_queue_dashboard/stats_controller.rb",
"chars": 232,
"preview": "module SolidQueueDashboard\n class StatsController < ApplicationController\n def index\n @jobs = SolidQueueDashboa"
},
{
"path": "app/helpers/solid_queue_dashboard/appearance_helper.rb",
"chars": 126,
"preview": "module SolidQueueDashboard\n module AppearanceHelper\n def dark_mode?\n cookies[:dark_mode] == \"true\"\n end\n en"
},
{
"path": "app/helpers/solid_queue_dashboard/application_helper.rb",
"chars": 147,
"preview": "module SolidQueueDashboard\n module ApplicationHelper\n def empty_value\n tag.span(\"—\", class: \"text-muted-foregro"
},
{
"path": "app/helpers/solid_queue_dashboard/icons_helper.rb",
"chars": 7778,
"preview": "module SolidQueueDashboard\n module IconsHelper\n def icon_refresh_cw(options = {})\n svg_options = {\n xmln"
},
{
"path": "app/helpers/solid_queue_dashboard/jobs_helper.rb",
"chars": 1744,
"preview": "module SolidQueueDashboard\n module JobsHelper\n def job_status_circle(status, options = {})\n options[:class] = ["
},
{
"path": "app/helpers/solid_queue_dashboard/pagination_helper.rb",
"chars": 1015,
"preview": "module SolidQueueDashboard\n module PaginationHelper\n def paginate(scope, page:, per_page:)\n page = [ page.to_i,"
},
{
"path": "app/helpers/solid_queue_dashboard/processes_helper.rb",
"chars": 1107,
"preview": "module SolidQueueDashboard\n module ProcessesHelper\n def process_kind_circle(kind, options = {})\n options[:class"
},
{
"path": "app/helpers/solid_queue_dashboard/recurring_tasks_helper.rb",
"chars": 1027,
"preview": "module SolidQueueDashboard\n module RecurringTasksHelper\n def recurring_task_circle(type, options = {})\n options"
},
{
"path": "app/views/layouts/solid_queue_dashboard/application.html.erb",
"chars": 718,
"preview": "<!DOCTYPE html>\n<html class=\"<%= dark_mode? ? \"dark\" : \"\" %>\" lang=\"en\">\n<head>\n <title>Solid Queue Dashboard</title>\n\n"
},
{
"path": "app/views/solid_queue_dashboard/application/_flash_messages.html.erb",
"chars": 2193,
"preview": "<% if flash.any? %>\n <div class=\"space-y-4 mb-8\">\n <% flash.each do |type, message| %>\n <% alert_class = case t"
},
{
"path": "app/views/solid_queue_dashboard/application/_footer.html.erb",
"chars": 336,
"preview": "<footer class=\"mt-6\">\n <p class=\"text-xs text-center text-muted-foreground\">\n <a\n href=\"https://github.com/akod"
},
{
"path": "app/views/solid_queue_dashboard/application/_navbar.html.erb",
"chars": 3063,
"preview": "<nav class=\"navbar mb-6\">\n <%= link_to root_path, class: \"inline-flex items-center gap-0.5 text-xl font-bold tracking-t"
},
{
"path": "app/views/solid_queue_dashboard/application/_pagination.html.erb",
"chars": 1168,
"preview": "<% if total_pages > 1 %>\n <nav class=\"pagination\" role=\"navigation\" aria-label=\"pagination\">\n <div class=\"pagination"
},
{
"path": "app/views/solid_queue_dashboard/dashboard/index.html.erb",
"chars": 1196,
"preview": "<div class=\"card card-content p-6 relative\">\n <%= form_with url: root_path, method: :get, class: \"absolute top-6 right-"
},
{
"path": "app/views/solid_queue_dashboard/jobs/_filters.html.erb",
"chars": 3003,
"preview": "<%= form_with url: jobs_path, method: :get, class: \"space-y-3\" do |form| %>\n <div>\n <label class=\"label\">Status</lab"
},
{
"path": "app/views/solid_queue_dashboard/jobs/_table.html.erb",
"chars": 655,
"preview": "<% highlight_ids = local_assigns[:highlight_ids] || [] %>\n\n<div class=\"table-wrapper\">\n <table class=\"table\">\n <thea"
},
{
"path": "app/views/solid_queue_dashboard/jobs/_table_row.html.erb",
"chars": 3841,
"preview": "<% highlight = local_assigns[:highlight] || local_assigns[:highlight_ids]&.include?(job.id) %>\n\n<tr class=\"table-row <%="
},
{
"path": "app/views/solid_queue_dashboard/jobs/index.html.erb",
"chars": 1636,
"preview": "<div class=\"card\">\n <div class=\"card-header border-b\">\n <h2 class=\"card-title\">Jobs</h2>\n\n <div class=\"!mt-4\">\n "
},
{
"path": "app/views/solid_queue_dashboard/jobs/show.html.erb",
"chars": 7239,
"preview": "<div class=\"flex items-center justify-between gap-4\">\n <h1 class=\"text-4xl flex items-center gap-2\">\n <%= job_status"
},
{
"path": "app/views/solid_queue_dashboard/processes/_filters.html.erb",
"chars": 2133,
"preview": "<%= form_with url: processes_path, method: :get, class: \"space-y-3\" do |form| %>\n <% if @process_kinds.any? %>\n <div"
},
{
"path": "app/views/solid_queue_dashboard/processes/_table.html.erb",
"chars": 616,
"preview": "<div class=\"table-wrapper\">\n <table class=\"table\">\n <thead class=\"table-header\">\n <tr class=\"table-row\">\n "
},
{
"path": "app/views/solid_queue_dashboard/processes/_table_row.html.erb",
"chars": 1259,
"preview": "<tr class=\"table-row\" data-href=\"<%= process_path(process) %>\">\n <td class=\"table-cell pl-6 font-medium text-zinc-900 d"
},
{
"path": "app/views/solid_queue_dashboard/processes/index.html.erb",
"chars": 722,
"preview": "<div class=\"card\">\n <div class=\"card-header border-b\">\n <h2 class=\"card-title\">Processes</h2>\n\n <div class=\"!mt-4"
},
{
"path": "app/views/solid_queue_dashboard/processes/show.html.erb",
"chars": 3491,
"preview": "<h1 class=\"text-4xl flex items-center gap-2\">\n <%= process_kind_circle(@process.kind, class: \"!size-4\") %>\n\n <span cla"
},
{
"path": "app/views/solid_queue_dashboard/recurring_tasks/_filters.html.erb",
"chars": 961,
"preview": "<%= form_with url: recurring_tasks_path, method: :get, class: \"space-y-3\" do |form| %>\n <div>\n <div class=\"flex flex"
},
{
"path": "app/views/solid_queue_dashboard/recurring_tasks/_table.html.erb",
"chars": 666,
"preview": "<div class=\"table-wrapper\">\n <table class=\"table\">\n <thead class=\"table-header\">\n <tr class=\"table-row\">\n "
},
{
"path": "app/views/solid_queue_dashboard/recurring_tasks/_table_row.html.erb",
"chars": 1031,
"preview": "<tr class=\"table-row\" data-href=\"<%= recurring_task_path(recurring_task) %>\">\n <td class=\"table-cell pl-6 font-medium t"
},
{
"path": "app/views/solid_queue_dashboard/recurring_tasks/index.html.erb",
"chars": 578,
"preview": "<div class=\"card\">\n <div class=\"card-header border-b\">\n <h2 class=\"card-title\">Recurring Tasks</h2>\n\n <div class="
},
{
"path": "app/views/solid_queue_dashboard/recurring_tasks/show.html.erb",
"chars": 4861,
"preview": "<h1 class=\"text-4xl flex items-center gap-2\">\n <%= recurring_task_circle(@recurring_task.type, class: \"size-4\") %>\n\n <"
},
{
"path": "app/views/solid_queue_dashboard/stats/index.html.erb",
"chars": 929,
"preview": "<div class=\"card mt-4\">\n <div class=\"card-header border-b\">\n <h3 class=\"card-title\">Failure Rate</h3>\n </div>\n <di"
},
{
"path": "bin/console",
"chars": 295,
"preview": "#!/usr/bin/env ruby\n# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nrequire \"solid_queue_dashboard\"\n\n# You can ad"
},
{
"path": "bin/dev",
"chars": 55,
"preview": "#!/usr/bin/env bash\n\nexec foreman start -f Procfile.dev"
},
{
"path": "bin/setup",
"chars": 74,
"preview": "#!/usr/bin/env bash\nset -euo pipefail\nIFS=$'\\n\\t'\nset -vx\n\nbundle install\n"
},
{
"path": "bin/setup-test-app",
"chars": 105,
"preview": "#!/usr/bin/env bash\nset -euo pipefail\nIFS=$'\\n\\t'\nset -vx\n\ncd test_app\nbundle install\nbin/rails db:setup\n"
},
{
"path": "config/routes.rb",
"chars": 467,
"preview": "SolidQueueDashboard::Engine.routes.draw do\n resources :jobs, only: [ :index, :show ] do\n member do\n post :retry"
},
{
"path": "lib/solid_queue_dashboard/configuration.rb",
"chars": 277,
"preview": "module SolidQueueDashboard\n class Configuration\n attr_accessor :title\n\n def initialize\n @title = \"Solid Queu"
},
{
"path": "lib/solid_queue_dashboard/decorators/job_decorator.rb",
"chars": 1794,
"preview": "module SolidQueueDashboard\n module Decorators\n class JobDecorator < SimpleDelegator\n def color\n Job.stat"
},
{
"path": "lib/solid_queue_dashboard/decorators/jobs_decorator.rb",
"chars": 1867,
"preview": "module SolidQueueDashboard\n module Decorators\n class JobsDecorator < SimpleDelegator\n def with_status(status)\n "
},
{
"path": "lib/solid_queue_dashboard/decorators/process_decorator.rb",
"chars": 262,
"preview": "module SolidQueueDashboard\n module Decorators\n class ProcessDecorator < SimpleDelegator\n def color\n Proc"
},
{
"path": "lib/solid_queue_dashboard/decorators/processes_decorator.rb",
"chars": 294,
"preview": "module SolidQueueDashboard\n module Decorators\n class ProcessesDecorator < SimpleDelegator\n def each\n sup"
},
{
"path": "lib/solid_queue_dashboard/decorators/recurring_task_decorator.rb",
"chars": 449,
"preview": "module SolidQueueDashboard\n module Decorators\n class RecurringTaskDecorator < SimpleDelegator\n def type\n "
},
{
"path": "lib/solid_queue_dashboard/decorators/recurring_tasks_decorator.rb",
"chars": 581,
"preview": "module SolidQueueDashboard\n module Decorators\n class RecurringTasksDecorator < SimpleDelegator\n def with_type(t"
},
{
"path": "lib/solid_queue_dashboard/engine.rb",
"chars": 369,
"preview": "module SolidQueueDashboard\n class Engine < ::Rails::Engine\n isolate_namespace SolidQueueDashboard\n\n initializer \""
},
{
"path": "lib/solid_queue_dashboard/job.rb",
"chars": 595,
"preview": "module SolidQueueDashboard\n module Job\n # Constants\n RUNNING = :running\n SUCCESS = :success\n RETRIED = :ret"
},
{
"path": "lib/solid_queue_dashboard/process.rb",
"chars": 491,
"preview": "module SolidQueueDashboard\n module Process\n # Constants\n SUPERVISOR = \"Supervisor\"\n DISPATCHER = \"Dispatcher\"\n"
},
{
"path": "lib/solid_queue_dashboard/recurring_task.rb",
"chars": 216,
"preview": "module SolidQueueDashboard\n module RecurringTask\n # Constants\n COMMAND = :command\n JOB = :job\n\n TYPES = [ C"
},
{
"path": "lib/solid_queue_dashboard/version.rb",
"chars": 82,
"preview": "# frozen_string_literal: true\n\nmodule SolidQueueDashboard\n VERSION = \"0.2.0\"\nend\n"
},
{
"path": "lib/solid_queue_dashboard.rb",
"chars": 1741,
"preview": "# frozen_string_literal: true\n\nrequire \"rails\"\nrequire \"groupdate\"\nrequire \"chartkick\"\nrequire_relative \"solid_queue_das"
},
{
"path": "package.json",
"chars": 534,
"preview": "{\n \"private\": true,\n \"scripts\": {\n \"build\": \"tailwindcss -i ./app/assets/stylesheets/solid_queue_dashboard/tailwind"
},
{
"path": "sig/solid_queue_dashboard.rbs",
"chars": 118,
"preview": "module SolidQueueDashboard\n VERSION: String\n # See the writing guide of rbs: https://github.com/ruby/rbs#guides\nend\n"
},
{
"path": "solid_queue_dashboard.gemspec",
"chars": 1616,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"lib/solid_queue_dashboard/version\"\n\nGem::Specification.new do |spec|\n "
},
{
"path": "tailwind.config.js",
"chars": 2282,
"preview": "const { fontFamily } = require(\"tailwindcss/defaultTheme\")\n\n/** @type {import('tailwindcss').Config} */\nmodule.exports ="
},
{
"path": "test/test_helper.rb",
"chars": 146,
"preview": "# frozen_string_literal: true\n\n$LOAD_PATH.unshift File.expand_path(\"../lib\", __dir__)\nrequire \"solid_queue_dashboard\"\n\nr"
},
{
"path": "test/test_solid_queue_dashboard.rb",
"chars": 193,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\nclass TestSolidQueueDashboard < Minitest::Test\n def test_that_it_"
},
{
"path": "test_app/.ruby-version",
"chars": 6,
"preview": "3.3.4\n"
},
{
"path": "test_app/Gemfile",
"chars": 379,
"preview": "source \"https://rubygems.org\"\n\ngem \"rails\", \"~> 8.0.0.beta1\"\ngem \"sqlite3\", \">= 2.1\"\ngem \"puma\", \">= 5.0\"\ngem \"propshaft"
},
{
"path": "test_app/README.md",
"chars": 11,
"preview": "# Test App\n"
},
{
"path": "test_app/Rakefile",
"chars": 227,
"preview": "# Add your own tasks in files placed in lib/tasks ending in .rake,\n# for example lib/tasks/capistrano.rake, and they wil"
},
{
"path": "test_app/app/controllers/application_controller.rb",
"chars": 56,
"preview": "class ApplicationController < ActionController::API\nend\n"
},
{
"path": "test_app/app/controllers/concerns/.keep",
"chars": 0,
"preview": ""
},
{
"path": "test_app/app/jobs/accept_arguments_job.rb",
"chars": 119,
"preview": "class AcceptArgumentsJob < ApplicationJob\n queue_as :default\n\n def perform(*args)\n # Do something later\n end\nend\n"
},
{
"path": "test_app/app/jobs/always_fail_job.rb",
"chars": 125,
"preview": "class AlwaysFailJob < ApplicationJob\n queue_as :default\n\n def perform(*args)\n raise \"Sorry, I always fail :)\"\n end"
},
{
"path": "test_app/app/jobs/application_job.rb",
"chars": 269,
"preview": "class ApplicationJob < ActiveJob::Base\n # Automatically retry jobs that encountered a deadlock\n # retry_on ActiveRecor"
},
{
"path": "test_app/app/jobs/few_seconds_job.rb",
"chars": 124,
"preview": "class FewSecondsJob < ApplicationJob\n queue_as :another_queue\n\n def perform(*args)\n sleep rand(1..3).seconds\n end\n"
},
{
"path": "test_app/app/jobs/good_job.rb",
"chars": 121,
"preview": "class GoodJob < ApplicationJob\n queue_as :default\n\n def perform(*args)\n # I'm a good job. I never fail :)\n end\nend"
},
{
"path": "test_app/app/jobs/long_running_job.rb",
"chars": 121,
"preview": "class LongRunningJob < ApplicationJob\n queue_as :default\n\n def perform(*args)\n sleep rand(10..60).minutes\n end\nend"
},
{
"path": "test_app/app/jobs/random_fail_job.rb",
"chars": 163,
"preview": "class RandomFailJob < ApplicationJob\n queue_as :default\n\n def perform(*args)\n return if rand(1..100) <= 50\n\n rai"
},
{
"path": "test_app/app/jobs/retrying_job.rb",
"chars": 198,
"preview": "class RetryingJob < ApplicationJob\n queue_as :default\n retry_on StandardError, wait: 5.seconds, attempts: 3\n\n def per"
},
{
"path": "test_app/app/models/application_record.rb",
"chars": 74,
"preview": "class ApplicationRecord < ActiveRecord::Base\n primary_abstract_class\nend\n"
},
{
"path": "test_app/app/models/concerns/.keep",
"chars": 0,
"preview": ""
},
{
"path": "test_app/bin/bundle",
"chars": 2801,
"preview": "#!/usr/bin/env ruby\n# frozen_string_literal: true\n\n#\n# This file was generated by Bundler.\n#\n# The application 'bundle' "
},
{
"path": "test_app/bin/dev",
"chars": 56,
"preview": "#!/usr/bin/env ruby\nexec \"./bin/rails\", \"server\", *ARGV\n"
},
{
"path": "test_app/bin/jobs",
"chars": 117,
"preview": "#!/usr/bin/env ruby\n\nrequire_relative \"../config/environment\"\nrequire \"solid_queue/cli\"\n\nSolidQueue::Cli.start(ARGV)\n"
},
{
"path": "test_app/bin/rails",
"chars": 141,
"preview": "#!/usr/bin/env ruby\nAPP_PATH = File.expand_path(\"../config/application\", __dir__)\nrequire_relative \"../config/boot\"\nrequ"
},
{
"path": "test_app/bin/rake",
"chars": 90,
"preview": "#!/usr/bin/env ruby\nrequire_relative \"../config/boot\"\nrequire \"rake\"\nRake.application.run\n"
},
{
"path": "test_app/bin/setup",
"chars": 1032,
"preview": "#!/usr/bin/env ruby\nrequire \"fileutils\"\n\nAPP_ROOT = File.expand_path(\"..\", __dir__)\nAPP_NAME = \"test-app\"\n\ndef system!(*"
},
{
"path": "test_app/bin/thrust",
"chars": 104,
"preview": "#!/usr/bin/env ruby\nrequire \"rubygems\"\nrequire \"bundler/setup\"\n\nload Gem.bin_path(\"thruster\", \"thrust\")\n"
},
{
"path": "test_app/config/application.rb",
"chars": 1551,
"preview": "require_relative \"boot\"\n\nrequire \"rails\"\n# Pick the frameworks you want:\nrequire \"active_model/railtie\"\nrequire \"active_"
},
{
"path": "test_app/config/boot.rb",
"chars": 128,
"preview": "ENV[\"BUNDLE_GEMFILE\"] ||= File.expand_path(\"../Gemfile\", __dir__)\n\nrequire \"bundler/setup\" # Set up gems listed in the G"
},
{
"path": "test_app/config/credentials.yml.enc",
"chars": 548,
"preview": "ZBhnsj0GRj3aqXy4hhkzqAmLW/3gBUqWrFHjTiik0RVGI+PJDVawlfIA6ALEX4t8ZQh9UPbsTvgKWBolxwCpE6hcFGOEMIo2Jyi1Mcu3kcRX8XKakSldkIAk"
},
{
"path": "test_app/config/database.yml",
"chars": 478,
"preview": "default: &default\n adapter: sqlite3\n pool: <%= ENV.fetch(\"RAILS_MAX_THREADS\") { 5 } %>\n timeout: 5000\n\ndevelopment:\n "
},
{
"path": "test_app/config/environment.rb",
"chars": 128,
"preview": "# Load the Rails application.\nrequire_relative \"application\"\n\n# Initialize the Rails application.\nRails.application.init"
},
{
"path": "test_app/config/environments/development.rb",
"chars": 1740,
"preview": "require \"active_support/core_ext/integer/time\"\n\nRails.application.configure do\n # Settings specified here will take pre"
},
{
"path": "test_app/config/environments/production.rb",
"chars": 2447,
"preview": "require \"active_support/core_ext/integer/time\"\n\nRails.application.configure do\n # Settings specified here will take pre"
},
{
"path": "test_app/config/environments/test.rb",
"chars": 1854,
"preview": "# The test environment is used exclusively to run your application's\n# test suite. You never need to work with it otherw"
},
{
"path": "test_app/config/initializers/cors.rb",
"chars": 504,
"preview": "# Be sure to restart your server when you modify this file.\n\n# Avoid CORS issues when API is called from the frontend ap"
},
{
"path": "test_app/config/initializers/filter_parameter_logging.rb",
"chars": 468,
"preview": "# Be sure to restart your server when you modify this file.\n\n# Configure parameters to be partially matched (e.g. passw "
},
{
"path": "test_app/config/initializers/inflections.rb",
"chars": 649,
"preview": "# Be sure to restart your server when you modify this file.\n\n# Add new inflection rules using the following format. Infl"
},
{
"path": "test_app/config/locales/en.yml",
"chars": 908,
"preview": "# Files in the config/locales directory are used for internationalization and\n# are automatically loaded by Rails. If yo"
},
{
"path": "test_app/config/master.key",
"chars": 32,
"preview": "c90d2d971451d5d35efbd837a2751096"
},
{
"path": "test_app/config/puma.rb",
"chars": 1962,
"preview": "# This configuration file will be evaluated by Puma. The top-level methods that\n# are invoked here are part of Puma's co"
},
{
"path": "test_app/config/queue.yml",
"chars": 290,
"preview": "default: &default\n dispatchers:\n - polling_interval: 1\n batch_size: 500\n workers:\n - queues: \"*\"\n thre"
},
{
"path": "test_app/config/recurring.yml",
"chars": 270,
"preview": " development:\n periodic_cleanup:\n class: RandomFailJob\n queue: background\n args: [ 1000, { batch_size: 500"
},
{
"path": "test_app/config/routes.rb",
"chars": 155,
"preview": "Rails.application.routes.draw do\n mount SolidQueueDashboard::Engine, at: \"/solid-queue\"\n\n get \"up\", to: \"rails/health#"
},
{
"path": "test_app/config.ru",
"chars": 160,
"preview": "# This file is used by Rack-based servers to start the application.\n\nrequire_relative \"config/environment\"\n\nrun Rails.ap"
},
{
"path": "test_app/db/queue_schema.rb",
"chars": 6278,
"preview": "ActiveRecord::Schema[7.1].define(version: 1) do\n create_table \"solid_queue_blocked_executions\", force: :cascade do |t|\n"
},
{
"path": "test_app/db/schema.rb",
"chars": 762,
"preview": "# This file is auto-generated from the current state of the database. Instead\n# of editing this file, please use the mig"
},
{
"path": "test_app/db/seeds.rb",
"chars": 494,
"preview": "# This file should ensure the existence of records required to run the application in every environment (production,\n# d"
},
{
"path": "test_app/lib/tasks/.keep",
"chars": 0,
"preview": ""
},
{
"path": "test_app/lib/tasks/jobs.rake",
"chars": 1071,
"preview": "require \"active_support/testing/time_helpers\"\n\nnamespace :jobs do\n desc \"Generate dummy data for jobs\"\n task :generate"
},
{
"path": "test_app/public/robots.txt",
"chars": 99,
"preview": "# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file\n"
},
{
"path": "test_app/script/.keep",
"chars": 0,
"preview": ""
},
{
"path": "test_app/storage/.keep",
"chars": 0,
"preview": ""
},
{
"path": "test_app/vendor/.keep",
"chars": 0,
"preview": ""
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the akodkod/solid-queue-dashboard GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 125 files (209.4 KB), approximately 68.4k tokens, and a symbol index with 377 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.