diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..e9285fe --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(git add *)", + "Bash(git commit -m ' *)" + ] + } +} diff --git a/Gemfile.lock b/Gemfile.lock index c2a9f94..9649dfe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,9 @@ PATH remote: . specs: solid_queue_dashboard (0.1.0) + pagy (>= 9.0) rails (>= 8.1.3) + solid_queue (>= 1.0) GEM remote: https://rubygems.org/ @@ -92,6 +94,11 @@ GEM drb (2.2.3) erb (6.0.4) erubi (1.13.1) + et-orbi (1.4.0) + tzinfo + fugit (1.12.1) + et-orbi (~> 1.4) + raabro (~> 1.4) globalid (1.3.0) activesupport (>= 6.1) i18n (1.14.8) @@ -132,6 +139,12 @@ GEM nio4r (2.7.5) nokogiri (1.19.3-arm64-darwin) racc (~> 1.4) + nokogiri (1.19.3-x86_64-linux-gnu) + racc (~> 1.4) + pagy (43.5.4) + json + uri + yaml parallel (2.1.0) parser (3.3.11.1) ast (~> 2.4.1) @@ -145,6 +158,7 @@ GEM stringio puma (8.0.1) nio4r (~> 2.0) + raabro (1.4.0) racc (1.8.1) rack (3.2.6) rack-session (2.1.2) @@ -223,7 +237,15 @@ GEM rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) securerandom (0.4.1) + solid_queue (1.4.0) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) sqlite3 (2.9.4-arm64-darwin) + sqlite3 (2.9.4-x86_64-linux-gnu) stringio (3.2.0) thor (1.5.0) timeout (0.6.1) @@ -239,10 +261,12 @@ GEM base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) + yaml (0.4.0) zeitwerk (2.7.5) PLATFORMS arm64-darwin + x86_64-linux DEPENDENCIES puma @@ -251,90 +275,98 @@ DEPENDENCIES sqlite3 CHECKSUMS - action_text-trix (2.1.19) - actioncable (8.1.3) - actionmailbox (8.1.3) - actionmailer (8.1.3) - actionpack (8.1.3) - actiontext (8.1.3) - actionview (8.1.3) - activejob (8.1.3) - activemodel (8.1.3) - activerecord (8.1.3) - activestorage (8.1.3) - activesupport (8.1.3) - ast (2.4.3) - base64 (0.3.0) - bigdecimal (4.1.2) - builder (3.3.0) - concurrent-ruby (1.3.6) - connection_pool (3.0.2) - crass (1.0.6) - date (3.5.1) - drb (2.2.3) - erb (6.0.4) - erubi (1.13.1) - globalid (1.3.0) - i18n (1.14.8) - io-console (0.8.2) - irb (1.18.0) - json (2.19.5) - language_server-protocol (3.17.0.5) - lint_roller (1.1.0) - logger (1.7.0) - loofah (2.25.1) - mail (2.9.0) - marcel (1.1.0) - mini_mime (1.1.5) - minitest (6.0.6) - net-imap (0.6.4) - net-pop (0.1.2) - net-protocol (0.2.2) - net-smtp (0.5.1) - nio4r (2.7.5) - nokogiri (1.19.3-arm64-darwin) - parallel (2.1.0) - parser (3.3.11.1) - pp (0.6.3) - prettyprint (0.2.0) - prism (1.9.0) - psych (5.3.1) - puma (8.0.1) - racc (1.8.1) - rack (3.2.6) - rack-session (2.1.2) - rack-test (2.2.0) - rackup (2.3.1) - rails (8.1.3) - rails-dom-testing (2.3.0) - rails-html-sanitizer (1.7.0) - railties (8.1.3) - rainbow (3.1.1) - rake (13.4.2) - rdoc (7.2.0) - regexp_parser (2.12.0) - reline (0.6.3) - rubocop (1.86.1) - rubocop-ast (1.49.1) - rubocop-performance (1.26.1) - rubocop-rails (2.35.0) - rubocop-rails-omakase (1.1.0) - ruby-progressbar (1.13.0) - securerandom (0.4.1) + action_text-trix (2.1.19) sha256=7012f59421009cf284aa651294896414d653a61a2417c9b8714c8476d2f74009 + actioncable (8.1.3) sha256=e5bc7f75e44e6a22de29c4f43176927c3a9ce4824464b74ed18d8226e75a80f0 + actionmailbox (8.1.3) sha256=df7da474eaa0e70df4ed5a6fef66eb3b3b0f2dbf7f14518deee8d77f1b4aae59 + actionmailer (8.1.3) sha256=831f724891bb70d0aaa4d76581a6321124b6a752cb655c9346aae5479318448d + actionpack (8.1.3) sha256=af998cae4d47c5d581a2cc363b5c77eb718b7c4b45748d81b1887b25621c29a3 + actiontext (8.1.3) sha256=d291019c00e1ea9e6463011fa214f6081a56d7b9a1d224e7d3f6384c1dafc7d2 + actionview (8.1.3) sha256=1347c88c7f3edb38100c5ce0e9fb5e62d7755f3edc1b61cce2eb0b2c6ea2fd5d + activejob (8.1.3) sha256=a149b1766aa8204c3c3da7309e4becd40fcd5529c348cffbf6c9b16b565fe8d3 + activemodel (8.1.3) sha256=90c05cbe4cef3649b8f79f13016191ea94c4525ce4a5c0fb7ef909c4b91c8219 + activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab + activestorage (8.1.3) sha256=0564ce9309143951a67615e1bb4e090ee54b8befed417133cae614479b46384d + activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e + ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f + concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d + date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9 + erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 + et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc + fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68 + globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11 + i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 + io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 + json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59 + language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc + lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 + mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941 + marcel (1.1.0) sha256=fdcfcfa33cc52e93c4308d40e4090a5d4ea279e160a7f6af988260fa970e0bee + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1 + net-imap (0.6.4) sha256=9a5598c67a3022c284d98430ef1d4948e7dbdb62596f61081ea8ca933270a02b + net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 + net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 + net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + nokogiri (1.19.3-arm64-darwin) sha256=71b9bd424b1b7abc18b05052a1a3cfd3627abdca62be280854cc411791357e42 + nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976 + pagy (43.5.4) sha256=2bdf3fa6b1e0cac5bbafe5d077fb24eb971f72f3194f8c6863a0f3867261ce59 + parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356 + parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54 + pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 + puma (8.0.1) sha256=7b94e50c07655718c1fb8ae41a11fc06c7d61293208b3aa608ff71a46d3ad37c + raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882 + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2 + rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 + rails (8.1.3) sha256=6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3 + rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d + rails-html-sanitizer (1.7.0) sha256=28b145cceaf9cc214a9874feaa183c3acba036c9592b19886e0e45efc62b1e89 + railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22 + rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192 + regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb + reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 + rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531 + rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 + rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 + rubocop-rails (2.35.0) sha256=a5d9f0f6c6d9b73d9ddd181c4c0b6d2e00dd17107480828d31c7b369ebfcd49c + rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d + ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + solid_queue (1.4.0) sha256=e6a18d196f0b27cb6e3c77c5b31258b05fb634f8ed64fb1866ed164047216c2a solid_queue_dashboard (0.1.0) - sqlite3 (2.9.4-arm64-darwin) - stringio (3.2.0) - thor (1.5.0) - timeout (0.6.1) - tsort (0.2.0) - tzinfo (2.0.6) - unicode-display_width (3.2.0) - unicode-emoji (4.2.0) - uri (1.1.1) - useragent (0.16.11) - websocket-driver (0.8.0) - websocket-extensions (0.1.5) - zeitwerk (2.7.5) + sqlite3 (2.9.4-arm64-darwin) sha256=1d5aad413a815d236e96d43f05a1acc600b6cd086800770342a3f9c2877499ff + sqlite3 (2.9.4-x86_64-linux-gnu) sha256=537a3eda71b1df1336d0055cbebe55a7317c34870c192c7b6b9d8d0be6871847 + stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 + thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 + timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 + websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962 + websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 + yaml (0.4.0) sha256=240e69d1e6ce3584d6085978719a0faa6218ae426e034d8f9b02fb54d3471942 + zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd BUNDLED WITH 4.0.11 diff --git a/app/assets/stylesheets/solid_queue_dashboard/application.css b/app/assets/stylesheets/solid_queue_dashboard/application.css index 0ebd7fe..1975f05 100644 --- a/app/assets/stylesheets/solid_queue_dashboard/application.css +++ b/app/assets/stylesheets/solid_queue_dashboard/application.css @@ -1,15 +1,293 @@ /* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, - * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS - * files in this directory. Styles in this file should be added after the last require_* statement. - * It is generally better to create a new file per style scope. - * - *= require_tree . *= require_self */ + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --bg: #f8f9fa; + --surface: #ffffff; + --border: #dee2e6; + --text: #212529; + --muted: #6c757d; + --primary: #0d6efd; + --danger: #dc3545; + --warning: #fd7e14; + --success: #198754; + --info: #0dcaf0; + --purple: #6f42c1; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 14px; + background: var(--bg); + color: var(--text); + line-height: 1.5; +} + +/* Layout */ +.sqd-header { + background: var(--surface); + border-bottom: 1px solid var(--border); + padding: 0 1.5rem; + display: flex; + align-items: center; + gap: 2rem; + height: 56px; +} + +.sqd-header__title { + font-size: 16px; + font-weight: 600; + color: var(--text); + text-decoration: none; +} + +.sqd-nav { + display: flex; + gap: 0.25rem; + list-style: none; +} + +.sqd-nav a { + display: block; + padding: 0.35rem 0.75rem; + border-radius: 6px; + color: var(--muted); + text-decoration: none; + font-size: 13px; + font-weight: 500; + transition: background 0.1s, color 0.1s; +} + +.sqd-nav a:hover, +.sqd-nav a.active { + background: var(--bg); + color: var(--text); +} + +.sqd-main { + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1.5rem; +} + +.sqd-page-title { + font-size: 20px; + font-weight: 600; + margin-bottom: 1.5rem; +} + +/* Flash notices */ +.sqd-flash { + padding: 0.75rem 1rem; + border-radius: 6px; + margin-bottom: 1rem; + font-size: 13px; +} +.sqd-flash--notice { background: #d1e7dd; color: #0f5132; border: 1px solid #badbcc; } +.sqd-flash--alert { background: #f8d7da; color: #842029; border: 1px solid #f5c2c7; } + +/* Stat cards */ +.sqd-stats { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.sqd-stat { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1.25rem 1rem; + text-align: center; +} + +.sqd-stat__value { + font-size: 28px; + font-weight: 700; + line-height: 1; + margin-bottom: 0.25rem; +} + +.sqd-stat__label { + font-size: 12px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.sqd-stat--ready .sqd-stat__value { color: var(--success); } +.sqd-stat--scheduled .sqd-stat__value { color: var(--info); } +.sqd-stat--claimed .sqd-stat__value { color: var(--primary); } +.sqd-stat--failed .sqd-stat__value { color: var(--danger); } +.sqd-stat--blocked .sqd-stat__value { color: var(--warning); } +.sqd-stat--queues .sqd-stat__value { color: var(--purple); } +.sqd-stat--processes .sqd-stat__value { color: var(--muted); } + +/* Tables */ +.sqd-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; +} + +.sqd-card__header { + padding: 0.875rem 1rem; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.sqd-card__title { + font-size: 14px; + font-weight: 600; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th { + padding: 0.625rem 1rem; + text-align: left; + font-size: 12px; + font-weight: 600; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +td { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border); + vertical-align: middle; +} + +tr:last-child td { border-bottom: none; } +tbody tr:hover { background: var(--bg); } + +.sqd-empty { + text-align: center; + padding: 3rem 1rem; + color: var(--muted); +} + +/* Badges */ +.sqd-badge { + display: inline-block; + padding: 0.2em 0.55em; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + line-height: 1; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.sqd-badge--ready { background: #d1e7dd; color: #0f5132; } +.sqd-badge--scheduled { background: #cff4fc; color: #055160; } +.sqd-badge--claimed { background: #cfe2ff; color: #084298; } +.sqd-badge--failed { background: #f8d7da; color: #842029; } +.sqd-badge--blocked { background: #fff3cd; color: #664d03; } +.sqd-badge--paused { background: #e2e3e5; color: #41464b; } +.sqd-badge--running { background: #d1e7dd; color: #0f5132; } + +/* Buttons */ +.sqd-btn { + display: inline-flex; + align-items: center; + padding: 0.35rem 0.75rem; + border-radius: 5px; + font-size: 12px; + font-weight: 500; + text-decoration: none; + border: 1px solid transparent; + cursor: pointer; + transition: opacity 0.15s; +} +.sqd-btn:hover { opacity: 0.85; } +.sqd-btn--primary { background: var(--primary); color: #fff; border-color: var(--primary); } +.sqd-btn--danger { background: var(--danger); color: #fff; border-color: var(--danger); } +.sqd-btn--muted { background: var(--surface); color: var(--text); border-color: var(--border); } + +/* Filters */ +.sqd-filters { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 1rem; +} + +.sqd-filters a { + padding: 0.35rem 0.875rem; + border-radius: 20px; + font-size: 12px; + font-weight: 500; + text-decoration: none; + border: 1px solid var(--border); + color: var(--muted); + background: var(--surface); + transition: all 0.1s; +} + +.sqd-filters a:hover, +.sqd-filters a.active { + background: var(--primary); + border-color: var(--primary); + color: #fff; +} + +/* Code / monospace */ +.sqd-mono { + font-family: "SFMono-Regular", Menlo, Monaco, Consolas, monospace; + font-size: 12px; +} + +.sqd-error-msg { + color: var(--danger); + font-size: 12px; +} + +.sqd-truncate { + max-width: 320px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Pagination (pagy) */ +.pagy-nav { + display: flex; + justify-content: center; + gap: 0.25rem; + padding: 1rem; +} + +.pagy-nav a, +.pagy-nav span { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 32px; + padding: 0 0.5rem; + border-radius: 5px; + font-size: 13px; + text-decoration: none; + border: 1px solid var(--border); + color: var(--text); + background: var(--surface); +} + +.pagy-nav a:hover { background: var(--bg); } +.pagy-nav span.current { background: var(--primary); color: #fff; border-color: var(--primary); } +.pagy-nav span.gap, .pagy-nav span.disabled { color: var(--muted); } \ No newline at end of file diff --git a/app/controllers/solid_queue_dashboard/application_controller.rb b/app/controllers/solid_queue_dashboard/application_controller.rb index a95c2ac..cecf97d 100644 --- a/app/controllers/solid_queue_dashboard/application_controller.rb +++ b/app/controllers/solid_queue_dashboard/application_controller.rb @@ -1,4 +1,19 @@ module SolidQueueDashboard class ApplicationController < ActionController::Base + include Pagy::Backend + + before_action :authenticate! + + private + + def authenticate! + return unless (auth = SolidQueueDashboard.authenticate) + + instance_exec(&auth) || request_basic_auth + end + + def request_basic_auth + request_http_basic_authentication("Solid Queue Dashboard") + end end end diff --git a/app/controllers/solid_queue_dashboard/dashboard_controller.rb b/app/controllers/solid_queue_dashboard/dashboard_controller.rb new file mode 100644 index 0000000..88f1fa6 --- /dev/null +++ b/app/controllers/solid_queue_dashboard/dashboard_controller.rb @@ -0,0 +1,15 @@ +module SolidQueueDashboard + class DashboardController < ApplicationController + def index + @stats = { + ready: SolidQueue::ReadyExecution.count, + scheduled: SolidQueue::ScheduledExecution.count, + claimed: SolidQueue::ClaimedExecution.count, + failed: SolidQueue::FailedExecution.count, + blocked: SolidQueue::BlockedExecution.count, + queues: SolidQueue::Job.select(:queue_name).distinct.count, + processes: SolidQueue::Process.count + } + end + end +end diff --git a/app/controllers/solid_queue_dashboard/failed_jobs_controller.rb b/app/controllers/solid_queue_dashboard/failed_jobs_controller.rb new file mode 100644 index 0000000..5f0fdf8 --- /dev/null +++ b/app/controllers/solid_queue_dashboard/failed_jobs_controller.rb @@ -0,0 +1,25 @@ +module SolidQueueDashboard + class FailedJobsController < ApplicationController + def index + scope = SolidQueue::FailedExecution.includes(:job).order(created_at: :desc) + @pagy, @failed_jobs = pagy(scope, limit: 50) + end + + def retry + execution = SolidQueue::FailedExecution.find(params[:id]) + execution.retry + redirect_to failed_jobs_path, notice: "Job queued for retry." + end + + def discard + execution = SolidQueue::FailedExecution.find(params[:id]) + execution.discard + redirect_to failed_jobs_path, notice: "Job discarded." + end + + def discard_all + SolidQueue::FailedExecution.discard_all_in_batches + redirect_to failed_jobs_path, notice: "All failed jobs discarded." + end + end +end diff --git a/app/controllers/solid_queue_dashboard/jobs_controller.rb b/app/controllers/solid_queue_dashboard/jobs_controller.rb new file mode 100644 index 0000000..6cf2240 --- /dev/null +++ b/app/controllers/solid_queue_dashboard/jobs_controller.rb @@ -0,0 +1,23 @@ +module SolidQueueDashboard + class JobsController < ApplicationController + STATUSES = %w[ready scheduled claimed blocked failed].freeze + + def index + @status = params[:status].presence_in(STATUSES) || "ready" + @queue = params[:queue].presence + + scope = case @status + when "ready" then SolidQueue::ReadyExecution.includes(:job) + when "scheduled" then SolidQueue::ScheduledExecution.includes(:job) + when "claimed" then SolidQueue::ClaimedExecution.includes(:job) + when "blocked" then SolidQueue::BlockedExecution.includes(:job) + when "failed" then SolidQueue::FailedExecution.includes(:job) + end + + scope = scope.where(jobs: { queue_name: @queue }) if @queue.present? + scope = scope.order(created_at: :desc) + + @pagy, @jobs = pagy(scope, limit: 50) + end + end +end diff --git a/app/controllers/solid_queue_dashboard/queues_controller.rb b/app/controllers/solid_queue_dashboard/queues_controller.rb new file mode 100644 index 0000000..8165a66 --- /dev/null +++ b/app/controllers/solid_queue_dashboard/queues_controller.rb @@ -0,0 +1,20 @@ +module SolidQueueDashboard + class QueuesController < ApplicationController + def index + all_queues = SolidQueue::Queue.all.sort_by(&:name) + @pagy, @queues = pagy_array(all_queues, limit: 50) + end + + def pause + queue = SolidQueue::Queue.find_by_name(params[:id]) + queue.pause + redirect_to queues_path, notice: "Queue \"#{queue.name}\" paused." + end + + def resume + queue = SolidQueue::Queue.find_by_name(params[:id]) + queue.resume + redirect_to queues_path, notice: "Queue \"#{queue.name}\" resumed." + end + end +end diff --git a/app/helpers/solid_queue_dashboard/application_helper.rb b/app/helpers/solid_queue_dashboard/application_helper.rb index d522058..269bfb8 100644 --- a/app/helpers/solid_queue_dashboard/application_helper.rb +++ b/app/helpers/solid_queue_dashboard/application_helper.rb @@ -1,4 +1,5 @@ module SolidQueueDashboard module ApplicationHelper + include Pagy::Frontend end end diff --git a/app/views/layouts/solid_queue_dashboard/application.html.erb b/app/views/layouts/solid_queue_dashboard/application.html.erb index 9880b30..da2b265 100644 --- a/app/views/layouts/solid_queue_dashboard/application.html.erb +++ b/app/views/layouts/solid_queue_dashboard/application.html.erb @@ -1,17 +1,37 @@ - + - Solid queue dashboard + + + Solid Queue Dashboard <%= csrf_meta_tags %> <%= csp_meta_tag %> - - <%= yield :head %> - - <%= stylesheet_link_tag "solid_queue_dashboard/application", media: "all" %> + <%= stylesheet_link_tag "solid_queue_dashboard/application", media: "all" %> -<%= yield %> +
+ <%= link_to "Solid Queue", root_path, class: "sqd-header__title" %> + +
+ +
+ <% if notice.present? %> +
<%= notice %>
+ <% end %> + <% if alert.present? %> +
<%= alert %>
+ <% end %> + + <%= yield %> +
- + \ No newline at end of file diff --git a/app/views/solid_queue_dashboard/dashboard/index.html.erb b/app/views/solid_queue_dashboard/dashboard/index.html.erb new file mode 100644 index 0000000..ba25592 --- /dev/null +++ b/app/views/solid_queue_dashboard/dashboard/index.html.erb @@ -0,0 +1,60 @@ +

Dashboard

+ +
+
+
<%= @stats[:ready] %>
+
Ready
+
+
+
<%= @stats[:scheduled] %>
+
Scheduled
+
+
+
<%= @stats[:claimed] %>
+
Running
+
+
+
<%= @stats[:blocked] %>
+
Blocked
+
+
+
<%= @stats[:failed] %>
+
Failed
+
+
+
<%= @stats[:queues] %>
+
Queues
+
+
+
<%= @stats[:processes] %>
+
Processes
+
+
+ +
+
+
+ Quick Links +
+
+ <%= link_to "View all ready jobs", jobs_path(status: "ready"), class: "sqd-btn sqd-btn--muted" %> + <%= link_to "View scheduled jobs", jobs_path(status: "scheduled"), class: "sqd-btn sqd-btn--muted" %> + <%= link_to "View failed jobs", failed_jobs_path, class: "sqd-btn sqd-btn--muted" %> + <%= link_to "Manage queues", queues_path, class: "sqd-btn sqd-btn--muted" %> +
+
+ + <% if @stats[:failed] > 0 %> +
+
+ Attention Required +
+
+

+ <%= pluralize(@stats[:failed], "failed job") %> need attention. +

+ <%= link_to "Review failed jobs →", failed_jobs_path, class: "sqd-btn sqd-btn--danger" %> +
+
+ <% end %> +
\ No newline at end of file diff --git a/app/views/solid_queue_dashboard/failed_jobs/index.html.erb b/app/views/solid_queue_dashboard/failed_jobs/index.html.erb new file mode 100644 index 0000000..707cf1c --- /dev/null +++ b/app/views/solid_queue_dashboard/failed_jobs/index.html.erb @@ -0,0 +1,56 @@ +
+

Failed Jobs

+ <% if @failed_jobs.any? %> + <%= button_to "Discard All", discard_all_failed_jobs_path, + method: :delete, + data: { confirm: "Discard all failed jobs? This cannot be undone." }, + class: "sqd-btn sqd-btn--danger" %> + <% end %> +
+ +
+ <% if @failed_jobs.empty? %> +
No failed jobs. All clear!
+ <% else %> + + + + + + + + + + + + <% @failed_jobs.each do |execution| %> + <% job = execution.job %> + + + + + + + + <% end %> + +
Job ClassQueueErrorFailed At
<%= job.class_name %><%= job.queue_name %> + <% if execution.exception_class.present? %> +
+ <%= execution.exception_class %>: <%= execution.message %> +
+ <% else %> + + <% end %> +
<%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %> + <%= button_to "Retry", retry_failed_job_path(execution), + method: :post, class: "sqd-btn sqd-btn--primary", + style: "margin-right: 0.25rem;" %> + <%= button_to "Discard", failed_job_path(execution), + method: :delete, + data: { confirm: "Discard this job?" }, + class: "sqd-btn sqd-btn--danger" %> +
+ <%= pagy_nav(@pagy) if @pagy.pages > 1 %> + <% end %> +
diff --git a/app/views/solid_queue_dashboard/jobs/index.html.erb b/app/views/solid_queue_dashboard/jobs/index.html.erb new file mode 100644 index 0000000..ad4a4cb --- /dev/null +++ b/app/views/solid_queue_dashboard/jobs/index.html.erb @@ -0,0 +1,55 @@ +

Jobs

+ +
+ <%= link_to "Ready", jobs_path(status: "ready", queue: @queue), class: @status == "ready" ? "active" : "" %> + <%= link_to "Scheduled", jobs_path(status: "scheduled", queue: @queue), class: @status == "scheduled" ? "active" : "" %> + <%= link_to "Running", jobs_path(status: "claimed", queue: @queue), class: @status == "claimed" ? "active" : "" %> + <%= link_to "Blocked", jobs_path(status: "blocked", queue: @queue), class: @status == "blocked" ? "active" : "" %> + <%= link_to "Failed", jobs_path(status: "failed", queue: @queue), class: @status == "failed" ? "active" : "" %> +
+ +
+ <% if @jobs.empty? %> +
No <%= @status %> jobs.
+ <% else %> + + + + + + + + + + + + <% @jobs.each do |execution| %> + <% job = execution.job %> + + + + + + + + <% end %> + +
Job ClassQueuePriorityScheduled AtEnqueued At
+ <%= @status %> + <%= job.class_name %> + + <%= link_to job.queue_name, jobs_path(status: @status, queue: job.queue_name), + class: "sqd-mono", style: "color: inherit;" %> + <%= job.priority %> + <%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %> + <%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %>
+ <%= pagy_nav(@pagy) if @pagy.pages > 1 %> + <% end %> +
+ +<% if @queue.present? %> +

+ Filtering by queue: <%= @queue %> — + <%= link_to "Clear filter", jobs_path(status: @status) %> +

+<% end %> \ No newline at end of file diff --git a/app/views/solid_queue_dashboard/queues/index.html.erb b/app/views/solid_queue_dashboard/queues/index.html.erb new file mode 100644 index 0000000..26ee672 --- /dev/null +++ b/app/views/solid_queue_dashboard/queues/index.html.erb @@ -0,0 +1,45 @@ +

Queues

+ +
+ <% if @queues.empty? %> +
No queues found.
+ <% else %> + + + + + + + + + + + + <% @queues.each do |queue| %> + + + + + + + + <% end %> + +
NameSizeLatencyStatus
<%= queue.name %><%= queue.size %><%= queue.human_latency %> + <% if queue.paused? %> + Paused + <% else %> + Running + <% end %> + + <% if queue.paused? %> + <%= button_to "Resume", resume_queue_path(queue.name), + method: :post, class: "sqd-btn sqd-btn--primary" %> + <% else %> + <%= button_to "Pause", pause_queue_path(queue.name), + method: :post, class: "sqd-btn sqd-btn--muted" %> + <% end %> +
+ <%= pagy_nav(@pagy) if @pagy.pages > 1 %> + <% end %> +
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 183a91c..bb6a914 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,2 +1,22 @@ SolidQueueDashboard::Engine.routes.draw do + root to: "dashboard#index" + + resources :queues, only: [ :index ] do + member do + post :pause + post :resume + end + end + + resources :jobs, only: [ :index ] + + resources :failed_jobs, only: [ :index ] do + member do + post :retry + delete :discard + end + collection do + delete :discard_all + end + end end diff --git a/solid_queue_dashboard.gemspec b/solid_queue_dashboard.gemspec index ca91fd2..d2207d9 100644 --- a/solid_queue_dashboard.gemspec +++ b/solid_queue_dashboard.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |spec| # Prevent pushing this gem to RubyGems.org. To allow pushes either set the "allowed_push_host" # to allow pushing to a single host or delete this section to allow pushing to any host. - spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" + spec.metadata["allowed_push_host"] = "TODO: Set to 'https://mygemserver.com'" spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." @@ -24,4 +24,6 @@ Gem::Specification.new do |spec| end spec.add_dependency "rails", ">= 8.1.3" + spec.add_dependency "solid_queue", ">= 1.0" + spec.add_dependency "pagy", ">= 9.0" end