solid_stack_web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +82 -0
  4. data/Rakefile +41 -0
  5. data/app/assets/stylesheets/solid_stack_web/_01_base.css +32 -0
  6. data/app/assets/stylesheets/solid_stack_web/_02_layout.css +83 -0
  7. data/app/assets/stylesheets/solid_stack_web/_03_stats.css +31 -0
  8. data/app/assets/stylesheets/solid_stack_web/_04_table.css +39 -0
  9. data/app/assets/stylesheets/solid_stack_web/_05_badges.css +20 -0
  10. data/app/assets/stylesheets/solid_stack_web/_06_buttons.css +40 -0
  11. data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +77 -0
  12. data/app/assets/stylesheets/solid_stack_web/application.css +1 -0
  13. data/app/controllers/solid_stack_web/application_controller.rb +43 -0
  14. data/app/controllers/solid_stack_web/cable_controller.rb +8 -0
  15. data/app/controllers/solid_stack_web/cache_controller.rb +8 -0
  16. data/app/controllers/solid_stack_web/dashboard_controller.rb +22 -0
  17. data/app/controllers/solid_stack_web/failed_jobs_controller.rb +25 -0
  18. data/app/controllers/solid_stack_web/jobs_controller.rb +42 -0
  19. data/app/controllers/solid_stack_web/processes_controller.rb +7 -0
  20. data/app/controllers/solid_stack_web/queues_controller.rb +26 -0
  21. data/app/helpers/solid_stack_web/application_helper.rb +9 -0
  22. data/app/views/layouts/solid_stack_web/application.html.erb +48 -0
  23. data/app/views/solid_stack_web/cable/index.html.erb +27 -0
  24. data/app/views/solid_stack_web/cache/index.html.erb +14 -0
  25. data/app/views/solid_stack_web/dashboard/index.html.erb +72 -0
  26. data/app/views/solid_stack_web/failed_jobs/index.html.erb +41 -0
  27. data/app/views/solid_stack_web/jobs/_empty.html.erb +3 -0
  28. data/app/views/solid_stack_web/jobs/destroy.turbo_stream.erb +7 -0
  29. data/app/views/solid_stack_web/jobs/index.html.erb +50 -0
  30. data/app/views/solid_stack_web/processes/index.html.erb +32 -0
  31. data/app/views/solid_stack_web/queues/index.html.erb +44 -0
  32. data/config/routes.rb +21 -0
  33. data/lib/solid_stack_web/engine.rb +19 -0
  34. data/lib/solid_stack_web/version.rb +3 -0
  35. data/lib/solid_stack_web.rb +25 -0
  36. data/lib/tasks/solid_stack_web_tasks.rake +4 -0
  37. metadata +151 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9ff5e759d441ff203317f09e35d4e9fb6876f81b544d4b075093c3d2cf397293
4
+ data.tar.gz: 9be02459700b7da623c939241ffc49e0147a4a460fb68f85f0ff3d63b10cea7f
5
+ SHA512:
6
+ metadata.gz: 8dcdb3c9445a05628849c51d6081b5a3f399249b690da8a254583e134b0d761b04a9d9228818614d1b0239d4226648cec75efd8da6c7b8a990f0832df57730a4
7
+ data.tar.gz: ea5a60256af380188021c58aeeab6ab7e58c8eea3bd5aaf9ff03c3306d763cebb667fef58482e09fc1ee677894b110f03ed6cadb989bc254b6cb73d997909a52
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Chuck Smith
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # SolidStackWeb
2
+
3
+ [![CI](https://github.com/eclectic-coding/solid_stack_web/actions/workflows/ci.yml/badge.svg)](https://github.com/eclectic-coding/solid_stack_web/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/solid_stack_web.svg)](https://badge.fury.io/rb/solid_stack_web)
5
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.3-ruby)](https://www.ruby-lang.org)
6
+ [![codecov](https://codecov.io/gh/eclectic-coding/solid_stack_web/branch/main/graph/badge.svg)](https://codecov.io/gh/eclectic-coding/solid_stack_web)
7
+
8
+ A mountable Rails engine that provides a unified web dashboard for the full [Solid Stack](https://github.com/rails/solid_queue) — **Solid Queue**, **Solid Cache**, and **Solid Cable** — in a single interface with no asset pipeline dependency and no JavaScript runtime requirement.
9
+
10
+ ## Features
11
+
12
+ - **Overview dashboard** with live counts across all three Solid Stack components
13
+ - **Solid Queue** — browse jobs by status (ready, scheduled, claimed, blocked), manage failed jobs (retry / discard), pause/resume queues, and inspect worker processes
14
+ - **Solid Cache** — entry count and total byte size at a glance
15
+ - **Solid Cable** — active message count and distinct channel count
16
+ - **Turbo Stream** job discard — removes the row inline without a full page reload
17
+ - **Authentication hook** — plug in your own auth logic (Devise, Basic Auth, custom) via a one-line initializer
18
+ - **Zero asset pipeline coupling** — CSS is injected inline; safe to mount in any host app
19
+
20
+ ## Installation
21
+
22
+ Add the gem to your application's `Gemfile`:
23
+
24
+ ```ruby
25
+ gem "solid_stack_web"
26
+ ```
27
+
28
+ Run:
29
+
30
+ ```bash
31
+ bundle install
32
+ ```
33
+
34
+ Mount the engine in `config/routes.rb`:
35
+
36
+ ```ruby
37
+ mount SolidStackWeb::Engine, at: "/solid_stack"
38
+ ```
39
+
40
+ The dashboard will be available at `/solid_stack` (or whatever path you choose).
41
+
42
+ ## Configuration
43
+
44
+ Create an initializer at `config/initializers/solid_stack_web.rb`:
45
+
46
+ ```ruby
47
+ SolidStackWeb.configure do |config|
48
+ # Number of items per paginated page (default: 25)
49
+ config.page_size = 50
50
+
51
+ # Authentication — block runs in controller context.
52
+ # Return a truthy value to allow access; falsy falls back to HTTP Basic.
53
+ config.authenticate do
54
+ current_user&.admin?
55
+ end
56
+ end
57
+ ```
58
+
59
+ ### Authentication
60
+
61
+ The `authenticate` block is evaluated in the context of each request's controller instance, so any helper method available to controllers (e.g. `current_user` from Devise) works directly. If the block returns `false` or `nil`, the engine falls back to HTTP Basic authentication. If no `authenticate` block is configured, the dashboard is open.
62
+
63
+ ## Requirements
64
+
65
+ - Ruby >= 3.3
66
+ - Rails >= 8.1.3
67
+ - [solid_queue](https://github.com/rails/solid_queue) >= 1.0
68
+ - [solid_cache](https://github.com/rails/solid_cache) >= 1.0
69
+ - [solid_cable](https://github.com/rails/solid_cable) >= 1.0
70
+
71
+ ## Contributing
72
+
73
+ 1. Fork the repository
74
+ 2. Create a feature branch (`git checkout -b feat/my-feature`)
75
+ 3. Run the test suite: `bundle exec rake`
76
+ 4. Open a pull request
77
+
78
+ Bug reports and feature requests are welcome on [GitHub Issues](https://github.com/eclectic-coding/solid_stack_web/issues).
79
+
80
+ ## License
81
+
82
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+ require "rspec/core/rake_task"
6
+ require "bundler/audit/task"
7
+
8
+ RuboCop::RakeTask.new
9
+ RSpec::Core::RakeTask.new(:spec)
10
+ Bundler::Audit::Task.new
11
+
12
+ desc "Run bundler-audit, rubocop, and rspec (full CI suite)"
13
+ task ci: ["bundle:audit:update", "bundle:audit:check", :rubocop, :spec]
14
+
15
+ task default: :ci
16
+
17
+ namespace :dev do
18
+ def dummy_env
19
+ ENV["RAILS_ENV"] = "development"
20
+ require File.expand_path("spec/dummy/config/environment", __dir__)
21
+ end
22
+
23
+ desc "Create and migrate the dummy app development database"
24
+ task :setup do
25
+ dummy_env
26
+ ActiveRecord::Tasks::DatabaseTasks.root = File.expand_path("spec/dummy", __dir__)
27
+ db_config = ActiveRecord::Base.configurations.find_db_config("development")
28
+ ActiveRecord::Tasks::DatabaseTasks.create(db_config)
29
+ load File.expand_path("spec/dummy/db/schema.rb", __dir__)
30
+ puts "Development database ready."
31
+ end
32
+
33
+ desc "Seed the dummy app development database with fake data"
34
+ task :seed do
35
+ dummy_env
36
+ load File.expand_path("spec/dummy/db/seeds.rb", __dir__)
37
+ end
38
+
39
+ desc "Reset and reseed the dummy app development database"
40
+ task reset: [:setup, :seed]
41
+ end
@@ -0,0 +1,32 @@
1
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2
+
3
+ :root {
4
+ --bg: #f8f9fa;
5
+ --surface: #ffffff;
6
+ --border: #dee2e6;
7
+ --text: #212529;
8
+ --muted: #6c757d;
9
+ --primary: #0d6efd;
10
+ --danger: #dc3545;
11
+ --warning: #fd7e14;
12
+ --success: #198754;
13
+ --info: #0dcaf0;
14
+ --purple: #6f42c1;
15
+ --radius: 6px;
16
+ --shadow: 0 1px 3px rgba(0,0,0,.08);
17
+ }
18
+
19
+ body {
20
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
21
+ font-size: 14px;
22
+ line-height: 1.5;
23
+ color: var(--text);
24
+ background: var(--bg);
25
+ }
26
+
27
+ a { color: var(--primary); text-decoration: none; }
28
+ a:hover { text-decoration: underline; }
29
+
30
+ .sqw-monospace { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 13px; }
31
+ .sqw-muted { color: var(--muted); }
32
+ .sqw-truncate { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -0,0 +1,83 @@
1
+ .sqw-header {
2
+ background: var(--surface);
3
+ border-bottom: 1px solid var(--border);
4
+ box-shadow: var(--shadow);
5
+ position: sticky;
6
+ top: 0;
7
+ z-index: 100;
8
+ }
9
+
10
+ .sqw-header__inner {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: 2rem;
14
+ max-width: 1200px;
15
+ margin: 0 auto;
16
+ padding: 0 1.5rem;
17
+ height: 52px;
18
+ }
19
+
20
+ .sqw-header__logo {
21
+ font-weight: 700;
22
+ font-size: 16px;
23
+ color: var(--text);
24
+ white-space: nowrap;
25
+ }
26
+ .sqw-header__logo:hover { text-decoration: none; color: var(--primary); }
27
+
28
+ .sqw-nav { display: flex; gap: 0.25rem; }
29
+
30
+ .sqw-nav__link {
31
+ padding: 0.35rem 0.75rem;
32
+ border-radius: var(--radius);
33
+ color: var(--muted);
34
+ font-size: 13px;
35
+ font-weight: 500;
36
+ transition: background 0.1s, color 0.1s;
37
+ }
38
+ .sqw-nav__link:hover { background: var(--bg); color: var(--text); text-decoration: none; }
39
+ .sqw-nav__link--active { background: var(--bg); color: var(--text); }
40
+
41
+ .sqw-subnav {
42
+ background: var(--bg);
43
+ border-bottom: 1px solid var(--border);
44
+ }
45
+
46
+ .sqw-subnav__inner {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 0.125rem;
50
+ max-width: 1200px;
51
+ margin: 0 auto;
52
+ padding: 0 1.5rem;
53
+ height: 36px;
54
+ }
55
+
56
+ .sqw-subnav__link {
57
+ padding: 0.2rem 0.625rem;
58
+ border-radius: var(--radius);
59
+ color: var(--muted);
60
+ font-size: 12px;
61
+ font-weight: 500;
62
+ transition: background 0.1s, color 0.1s;
63
+ }
64
+ .sqw-subnav__link:hover { background: var(--surface); color: var(--text); text-decoration: none; }
65
+ .sqw-subnav__link--active { background: var(--surface); color: var(--text); }
66
+
67
+ .sqw-main {
68
+ max-width: 1200px;
69
+ margin: 0 auto;
70
+ padding: 1.5rem;
71
+ }
72
+
73
+ .sqw-page-header { margin-bottom: 1.25rem; }
74
+ .sqw-page-title { font-size: 20px; font-weight: 600; }
75
+
76
+ .sqw-flash {
77
+ padding: 0.75rem 1rem;
78
+ border-radius: var(--radius);
79
+ margin-bottom: 1rem;
80
+ font-size: 13px;
81
+ }
82
+ .sqw-flash--notice { background: #d1e7dd; color: #0a3622; border: 1px solid #a3cfbb; }
83
+ .sqw-flash--alert { background: #f8d7da; color: #58151c; border: 1px solid #f1aeb5; }
@@ -0,0 +1,31 @@
1
+ .sqw-stats-grid {
2
+ display: grid;
3
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
4
+ gap: 1rem;
5
+ margin-bottom: 1.5rem;
6
+ }
7
+
8
+ .sqw-stat {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 0.5rem;
12
+ background: var(--surface);
13
+ border: 1px solid var(--border);
14
+ border-radius: var(--radius);
15
+ padding: 1rem 1.25rem;
16
+ box-shadow: var(--shadow);
17
+ color: var(--text);
18
+ transition: box-shadow 0.15s;
19
+ }
20
+ a.sqw-stat:hover { box-shadow: 0 3px 8px rgba(0,0,0,.12); text-decoration: none; }
21
+
22
+ .sqw-stat__label { font-size: 12px; font-weight: 500; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
23
+ .sqw-stat__value { font-size: 28px; font-weight: 700; line-height: 1; }
24
+
25
+ .sqw-stat--ready .sqw-stat__value { color: var(--success); }
26
+ .sqw-stat--scheduled .sqw-stat__value { color: var(--info); }
27
+ .sqw-stat--claimed .sqw-stat__value { color: var(--primary); }
28
+ .sqw-stat--blocked .sqw-stat__value { color: var(--warning); }
29
+ .sqw-stat--failed .sqw-stat__value { color: var(--danger); }
30
+ .sqw-stat--cache .sqw-stat__value { color: var(--purple); }
31
+ .sqw-stat--cable .sqw-stat__value { color: var(--info); }
@@ -0,0 +1,39 @@
1
+ .sqw-table {
2
+ width: 100%;
3
+ border-collapse: collapse;
4
+ background: var(--surface);
5
+ border: 1px solid var(--border);
6
+ border-radius: var(--radius);
7
+ box-shadow: var(--shadow);
8
+ overflow: hidden;
9
+ }
10
+
11
+ .sqw-table th,
12
+ .sqw-table td {
13
+ padding: 0.6rem 0.875rem;
14
+ text-align: left;
15
+ border-bottom: 1px solid var(--border);
16
+ }
17
+
18
+ .sqw-table th {
19
+ font-size: 11px;
20
+ font-weight: 600;
21
+ text-transform: uppercase;
22
+ letter-spacing: .05em;
23
+ color: var(--muted);
24
+ background: var(--bg);
25
+ }
26
+
27
+ .sqw-table tbody tr:last-child td { border-bottom: none; }
28
+ .sqw-table tbody tr:hover { background: #f9fafb; }
29
+
30
+ .sqw-actions { text-align: right; white-space: nowrap; }
31
+
32
+ .sqw-empty {
33
+ background: var(--surface);
34
+ border: 1px solid var(--border);
35
+ border-radius: var(--radius);
36
+ padding: 3rem 1.5rem;
37
+ text-align: center;
38
+ color: var(--muted);
39
+ }
@@ -0,0 +1,20 @@
1
+ .sqw-badge {
2
+ display: inline-block;
3
+ padding: 0.2em 0.55em;
4
+ font-size: 11px;
5
+ font-weight: 600;
6
+ border-radius: 4px;
7
+ text-transform: uppercase;
8
+ letter-spacing: .04em;
9
+ }
10
+
11
+ .sqw-badge--ready { background: #d1e7dd; color: #0a3622; }
12
+ .sqw-badge--scheduled { background: #cff4fc; color: #055160; }
13
+ .sqw-badge--claimed { background: #cfe2ff; color: #084298; }
14
+ .sqw-badge--blocked { background: #fff3cd; color: #664d03; }
15
+ .sqw-badge--failed { background: #f8d7da; color: #58151c; }
16
+ .sqw-badge--paused { background: #e2e3e5; color: #41464b; }
17
+ .sqw-badge--queue { background: #e9ecef; color: #495057; font-weight: 500; text-transform: none; letter-spacing: 0; }
18
+ .sqw-badge--worker { background: #cfe2ff; color: #084298; }
19
+ .sqw-badge--supervisor { background: #d1e7dd; color: #0a3622; }
20
+ .sqw-badge--dispatcher { background: #fff3cd; color: #664d03; }
@@ -0,0 +1,40 @@
1
+ .sqw-btn {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: 0.3rem;
5
+ padding: 0.4rem 0.875rem;
6
+ font-size: 13px;
7
+ font-weight: 500;
8
+ border: 1px solid transparent;
9
+ border-radius: var(--radius);
10
+ cursor: pointer;
11
+ background: var(--primary);
12
+ color: #fff;
13
+ transition: opacity 0.1s;
14
+ }
15
+ .sqw-btn:hover { opacity: 0.88; text-decoration: none; }
16
+
17
+ .sqw-btn--sm { padding: 0.25rem 0.6rem; font-size: 12px; }
18
+ .sqw-btn--danger { background: var(--danger); }
19
+ .sqw-btn--muted { background: var(--surface); color: var(--text); border-color: var(--border); }
20
+ .sqw-btn--muted:hover { background: var(--bg); }
21
+
22
+ .sqw-tabs {
23
+ display: flex;
24
+ gap: 0.25rem;
25
+ margin-bottom: 1rem;
26
+ border-bottom: 1px solid var(--border);
27
+ padding-bottom: 0;
28
+ }
29
+
30
+ .sqw-tab {
31
+ padding: 0.5rem 1rem;
32
+ font-size: 13px;
33
+ font-weight: 500;
34
+ color: var(--muted);
35
+ border-bottom: 2px solid transparent;
36
+ margin-bottom: -1px;
37
+ transition: color 0.1s, border-color 0.1s;
38
+ }
39
+ .sqw-tab:hover { color: var(--text); text-decoration: none; }
40
+ .sqw-tab--active { color: var(--primary); border-bottom-color: var(--primary); }
@@ -0,0 +1,77 @@
1
+ .sqw-gem-grid {
2
+ display: grid;
3
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
4
+ gap: 1.5rem;
5
+ margin-bottom: 1.5rem;
6
+ }
7
+
8
+ .sqw-gem-card {
9
+ background: var(--surface);
10
+ border: 1px solid var(--border);
11
+ border-top: 3px solid var(--border);
12
+ border-radius: var(--radius);
13
+ box-shadow: var(--shadow);
14
+ overflow: hidden;
15
+ }
16
+
17
+ .sqw-gem-card--queue { border-top-color: var(--primary); }
18
+ .sqw-gem-card--cache { border-top-color: var(--purple); }
19
+ .sqw-gem-card--cable { border-top-color: var(--info); }
20
+
21
+ .sqw-gem-card__header {
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: space-between;
25
+ padding: 0.875rem 1.25rem;
26
+ border-bottom: 1px solid var(--border);
27
+ }
28
+
29
+ .sqw-gem-card__title {
30
+ font-size: 13px;
31
+ font-weight: 700;
32
+ text-transform: uppercase;
33
+ letter-spacing: .06em;
34
+ color: var(--text);
35
+ }
36
+
37
+ .sqw-gem-card__link {
38
+ font-size: 12px;
39
+ font-weight: 500;
40
+ color: var(--muted);
41
+ }
42
+ .sqw-gem-card__link:hover { color: var(--primary); text-decoration: none; }
43
+
44
+ .sqw-gem-card__body {
45
+ display: grid;
46
+ grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
47
+ gap: 1.25rem;
48
+ padding: 1.25rem;
49
+ }
50
+
51
+ .sqw-inline-stat {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: 0.25rem;
55
+ color: var(--text);
56
+ transition: opacity 0.15s;
57
+ }
58
+ a.sqw-inline-stat:hover { opacity: 0.7; text-decoration: none; }
59
+
60
+ .sqw-inline-stat__label {
61
+ font-size: 11px;
62
+ font-weight: 500;
63
+ color: var(--muted);
64
+ text-transform: uppercase;
65
+ letter-spacing: .04em;
66
+ }
67
+
68
+ .sqw-inline-stat__value { font-size: 28px; font-weight: 700; line-height: 1; }
69
+
70
+ .sqw-inline-stat--ready .sqw-inline-stat__value { color: var(--success); }
71
+ .sqw-inline-stat--scheduled .sqw-inline-stat__value { color: var(--info); }
72
+ .sqw-inline-stat--claimed .sqw-inline-stat__value { color: var(--primary); }
73
+ .sqw-inline-stat--blocked .sqw-inline-stat__value { color: var(--warning); }
74
+ .sqw-inline-stat--failed .sqw-inline-stat__value { color: var(--danger); }
75
+ .sqw-inline-stat--cache .sqw-inline-stat__value { color: var(--purple); }
76
+ .sqw-inline-stat--cable .sqw-inline-stat__value { color: var(--info); }
77
+ .sqw-inline-stat--neutral .sqw-inline-stat__value { color: var(--muted); }
@@ -0,0 +1 @@
1
+ /* Styles are loaded inline via the inline_styles helper which globs _*.css files. */
@@ -0,0 +1,43 @@
1
+ module SolidStackWeb
2
+ class ApplicationController < ActionController::Base
3
+ include Pagy::Method
4
+
5
+ before_action :authenticate!
6
+ around_action :with_database_connection
7
+
8
+ helper_method :current_section
9
+
10
+ private
11
+
12
+ def current_section
13
+ case controller_name
14
+ when "jobs", "failed_jobs", "queues", "processes" then :queue
15
+ when "cache" then :cache
16
+ when "cable" then :cable
17
+ else :overview
18
+ end
19
+ end
20
+
21
+ def with_database_connection
22
+ config = SolidStackWeb.connects_to
23
+ return yield unless config
24
+
25
+ if config.key?(:reading) && config.key?(:writing)
26
+ role = request.get? ? config[:reading] : config[:writing]
27
+ ActiveRecord::Base.connected_to(role: role) { yield }
28
+ else
29
+ ActiveRecord::Base.connected_to(**config) { yield }
30
+ end
31
+ end
32
+
33
+ def authenticate!
34
+ return unless (auth = SolidStackWeb.authenticate)
35
+
36
+ instance_exec(&auth) || request_basic_auth
37
+ end
38
+
39
+ def request_basic_auth
40
+ request_http_basic_authentication("Solid Stack Dashboard")
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,8 @@
1
+ module SolidStackWeb
2
+ class CableController < ApplicationController
3
+ def index
4
+ @total_messages = ::SolidCable::Message.count
5
+ @channels = ::SolidCable::Message.distinct.pluck(:channel).sort
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module SolidStackWeb
2
+ class CacheController < ApplicationController
3
+ def index
4
+ @total_entries = ::SolidCache::Entry.count
5
+ @total_byte_size = ::SolidCache::Entry.sum(:byte_size)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,22 @@
1
+ module SolidStackWeb
2
+ class DashboardController < ApplicationController
3
+ def index
4
+ @queue_stats = {
5
+ ready: ::SolidQueue::ReadyExecution.count,
6
+ scheduled: ::SolidQueue::ScheduledExecution.count,
7
+ claimed: ::SolidQueue::ClaimedExecution.count,
8
+ blocked: ::SolidQueue::BlockedExecution.count,
9
+ failed: ::SolidQueue::FailedExecution.count,
10
+ processes: ::SolidQueue::Process.count
11
+ }
12
+ @cache_stats = {
13
+ entries: ::SolidCache::Entry.count,
14
+ byte_size: ::SolidCache::Entry.sum(:byte_size)
15
+ }
16
+ @cable_stats = {
17
+ messages: ::SolidCable::Message.count,
18
+ channels: ::SolidCable::Message.distinct.count(:channel)
19
+ }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ module SolidStackWeb
2
+ class FailedJobsController < ApplicationController
3
+ def index
4
+ scope = ::SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
5
+ @pagy, @executions = pagy(scope)
6
+ end
7
+
8
+ def destroy
9
+ execution = ::SolidQueue::FailedExecution.find(params[:id])
10
+ execution.job.destroy!
11
+ @executions_remain = ::SolidQueue::FailedExecution.exists?
12
+
13
+ respond_to do |format|
14
+ format.html { redirect_to failed_jobs_path }
15
+ format.turbo_stream
16
+ end
17
+ end
18
+
19
+ def retry
20
+ execution = ::SolidQueue::FailedExecution.find(params[:id])
21
+ execution.retry
22
+ redirect_to failed_jobs_path
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ module SolidStackWeb
2
+ class JobsController < ApplicationController
3
+ EXECUTION_MODELS = {
4
+ "ready" => ::SolidQueue::ReadyExecution,
5
+ "scheduled" => ::SolidQueue::ScheduledExecution,
6
+ "claimed" => ::SolidQueue::ClaimedExecution,
7
+ "blocked" => ::SolidQueue::BlockedExecution
8
+ }.freeze
9
+
10
+ DISCARDABLE = %w[ready scheduled blocked].freeze
11
+
12
+ before_action :set_status
13
+ before_action :require_discardable, only: :destroy
14
+
15
+ def index
16
+ scope = EXECUTION_MODELS[@status].includes(:job).order(created_at: :desc)
17
+ @pagy, @executions = pagy(scope)
18
+ end
19
+
20
+ def destroy
21
+ model = EXECUTION_MODELS[@status]
22
+ execution = model.find(params[:id])
23
+ execution.job.destroy!
24
+ @executions_remain = model.exists?
25
+
26
+ respond_to do |format|
27
+ format.html { redirect_to jobs_path(status: @status) }
28
+ format.turbo_stream
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def set_status
35
+ @status = params[:status].presence_in(EXECUTION_MODELS.keys) || "ready"
36
+ end
37
+
38
+ def require_discardable
39
+ head :unprocessable_entity unless DISCARDABLE.include?(@status)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,7 @@
1
+ module SolidStackWeb
2
+ class ProcessesController < ApplicationController
3
+ def index
4
+ @processes = ::SolidQueue::Process.order(:kind, :name)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ module SolidStackWeb
2
+ class QueuesController < ApplicationController
3
+ def index
4
+ queue_names = ::SolidQueue::ReadyExecution.distinct.pluck(:queue_name)
5
+ paused = ::SolidQueue::Pause.pluck(:queue_name).to_set
6
+
7
+ @queues = queue_names.sort.map do |name|
8
+ {
9
+ name: name,
10
+ size: ::SolidQueue::ReadyExecution.where(queue_name: name).count,
11
+ paused: paused.include?(name)
12
+ }
13
+ end
14
+ end
15
+
16
+ def pause
17
+ ::SolidQueue::Pause.find_or_create_by!(queue_name: params[:id])
18
+ redirect_to queues_path
19
+ end
20
+
21
+ def resume
22
+ ::SolidQueue::Pause.find_by(queue_name: params[:id])&.destroy
23
+ redirect_to queues_path
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ module SolidStackWeb
2
+ module ApplicationHelper
3
+ def inline_styles
4
+ dir = SolidStackWeb::Engine.root.join("app/assets/stylesheets/solid_stack_web")
5
+ css = dir.glob("_*.css").sort.map(&:read).join("\n")
6
+ content_tag(:style, css.html_safe)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,48 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Solid Stack Dashboard</title>
7
+ <%= csrf_meta_tags %>
8
+ <%= csp_meta_tag %>
9
+ <%= inline_styles %>
10
+ </head>
11
+ <body>
12
+ <header class="sqw-header">
13
+ <div class="sqw-header__inner">
14
+ <%= link_to "Solid Stack", root_path, class: "sqw-header__logo" %>
15
+ <nav class="sqw-nav">
16
+ <%= link_to "Queue", jobs_path,
17
+ class: "sqw-nav__link#{" sqw-nav__link--active" if current_section == :queue}" %>
18
+ <%= link_to "Cache", cache_path,
19
+ class: "sqw-nav__link#{" sqw-nav__link--active" if current_section == :cache}" %>
20
+ <%= link_to "Cable", cable_path,
21
+ class: "sqw-nav__link#{" sqw-nav__link--active" if current_section == :cable}" %>
22
+ </nav>
23
+ </div>
24
+ </header>
25
+
26
+ <% if current_section == :queue %>
27
+ <nav class="sqw-subnav">
28
+ <div class="sqw-subnav__inner">
29
+ <%= link_to "Jobs", jobs_path,
30
+ class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "jobs"}" %>
31
+ <%= link_to "Failed", failed_jobs_path,
32
+ class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "failed_jobs"}" %>
33
+ <%= link_to "Queues", queues_path,
34
+ class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "queues"}" %>
35
+ <%= link_to "Processes", processes_path,
36
+ class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "processes"}" %>
37
+ </div>
38
+ </nav>
39
+ <% end %>
40
+
41
+ <main class="sqw-main">
42
+ <% flash.each do |type, message| %>
43
+ <div class="sqw-flash sqw-flash--<%= type %>"><%= message %></div>
44
+ <% end %>
45
+ <%= yield %>
46
+ </main>
47
+ </body>
48
+ </html>
@@ -0,0 +1,27 @@
1
+ <div class="sqw-page-header">
2
+ <h1 class="sqw-page-title">Solid Cable</h1>
3
+ </div>
4
+
5
+ <div class="sqw-stats-grid">
6
+ <div class="sqw-stat sqw-stat--cable">
7
+ <span class="sqw-stat__label">Total Messages</span>
8
+ <span class="sqw-stat__value"><%= @total_messages %></span>
9
+ </div>
10
+ <div class="sqw-stat sqw-stat--cable">
11
+ <span class="sqw-stat__label">Channels</span>
12
+ <span class="sqw-stat__value"><%= @channels.size %></span>
13
+ </div>
14
+ </div>
15
+
16
+ <% if @channels.any? %>
17
+ <table class="sqw-table">
18
+ <thead>
19
+ <tr><th>Channel</th></tr>
20
+ </thead>
21
+ <tbody>
22
+ <% @channels.each do |channel| %>
23
+ <tr><td class="sqw-monospace"><%= channel %></td></tr>
24
+ <% end %>
25
+ </tbody>
26
+ </table>
27
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <div class="sqw-page-header">
2
+ <h1 class="sqw-page-title">Solid Cache</h1>
3
+ </div>
4
+
5
+ <div class="sqw-stats-grid">
6
+ <div class="sqw-stat sqw-stat--cache">
7
+ <span class="sqw-stat__label">Total Entries</span>
8
+ <span class="sqw-stat__value"><%= @total_entries %></span>
9
+ </div>
10
+ <div class="sqw-stat sqw-stat--cache">
11
+ <span class="sqw-stat__label">Total Size</span>
12
+ <span class="sqw-stat__value"><%= number_to_human_size(@total_byte_size) %></span>
13
+ </div>
14
+ </div>
@@ -0,0 +1,72 @@
1
+ <div class="sqw-page-header">
2
+ <h1 class="sqw-page-title">Overview</h1>
3
+ </div>
4
+
5
+ <div class="sqw-gem-grid">
6
+ <div class="sqw-gem-card sqw-gem-card--queue">
7
+ <div class="sqw-gem-card__header">
8
+ <span class="sqw-gem-card__title">Solid Queue</span>
9
+ <%= link_to "View Jobs →", jobs_path, class: "sqw-gem-card__link" %>
10
+ </div>
11
+ <div class="sqw-gem-card__body">
12
+ <a href="<%= jobs_path(status: "ready") %>" class="sqw-inline-stat sqw-inline-stat--ready">
13
+ <span class="sqw-inline-stat__label">Ready</span>
14
+ <span class="sqw-inline-stat__value"><%= @queue_stats[:ready] %></span>
15
+ </a>
16
+ <a href="<%= jobs_path(status: "scheduled") %>" class="sqw-inline-stat sqw-inline-stat--scheduled">
17
+ <span class="sqw-inline-stat__label">Scheduled</span>
18
+ <span class="sqw-inline-stat__value"><%= @queue_stats[:scheduled] %></span>
19
+ </a>
20
+ <a href="<%= jobs_path(status: "claimed") %>" class="sqw-inline-stat sqw-inline-stat--claimed">
21
+ <span class="sqw-inline-stat__label">Running</span>
22
+ <span class="sqw-inline-stat__value"><%= @queue_stats[:claimed] %></span>
23
+ </a>
24
+ <a href="<%= jobs_path(status: "blocked") %>" class="sqw-inline-stat sqw-inline-stat--blocked">
25
+ <span class="sqw-inline-stat__label">Blocked</span>
26
+ <span class="sqw-inline-stat__value"><%= @queue_stats[:blocked] %></span>
27
+ </a>
28
+ <a href="<%= failed_jobs_path %>" class="sqw-inline-stat sqw-inline-stat--failed">
29
+ <span class="sqw-inline-stat__label">Failed</span>
30
+ <span class="sqw-inline-stat__value"><%= @queue_stats[:failed] %></span>
31
+ </a>
32
+ <a href="<%= processes_path %>" class="sqw-inline-stat sqw-inline-stat--neutral">
33
+ <span class="sqw-inline-stat__label">Processes</span>
34
+ <span class="sqw-inline-stat__value"><%= @queue_stats[:processes] %></span>
35
+ </a>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="sqw-gem-card sqw-gem-card--cache">
40
+ <div class="sqw-gem-card__header">
41
+ <span class="sqw-gem-card__title">Solid Cache</span>
42
+ <%= link_to "View Cache →", cache_path, class: "sqw-gem-card__link" %>
43
+ </div>
44
+ <div class="sqw-gem-card__body">
45
+ <div class="sqw-inline-stat sqw-inline-stat--cache">
46
+ <span class="sqw-inline-stat__label">Entries</span>
47
+ <span class="sqw-inline-stat__value"><%= @cache_stats[:entries] %></span>
48
+ </div>
49
+ <div class="sqw-inline-stat sqw-inline-stat--cache">
50
+ <span class="sqw-inline-stat__label">Size</span>
51
+ <span class="sqw-inline-stat__value"><%= number_to_human_size(@cache_stats[:byte_size]) %></span>
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="sqw-gem-card sqw-gem-card--cable">
57
+ <div class="sqw-gem-card__header">
58
+ <span class="sqw-gem-card__title">Solid Cable</span>
59
+ <%= link_to "View Cable →", cable_path, class: "sqw-gem-card__link" %>
60
+ </div>
61
+ <div class="sqw-gem-card__body">
62
+ <div class="sqw-inline-stat sqw-inline-stat--cable">
63
+ <span class="sqw-inline-stat__label">Messages</span>
64
+ <span class="sqw-inline-stat__value"><%= @cable_stats[:messages] %></span>
65
+ </div>
66
+ <div class="sqw-inline-stat sqw-inline-stat--cable">
67
+ <span class="sqw-inline-stat__label">Channels</span>
68
+ <span class="sqw-inline-stat__value"><%= @cable_stats[:channels] %></span>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
@@ -0,0 +1,41 @@
1
+ <div class="sqw-page-header">
2
+ <h1 class="sqw-page-title">Failed Jobs</h1>
3
+ </div>
4
+
5
+ <div id="sqw-jobs-table">
6
+ <% if @executions.any? %>
7
+ <table class="sqw-table">
8
+ <thead>
9
+ <tr>
10
+ <th>Job Class</th>
11
+ <th>Queue</th>
12
+ <th>Error</th>
13
+ <th>Failed At</th>
14
+ <th></th>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+ <% @executions.each do |execution| %>
19
+ <tr id="execution_<%= execution.id %>">
20
+ <td class="sqw-monospace"><%= execution.job.class_name %></td>
21
+ <td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
22
+ <td class="sqw-muted sqw-truncate"><%= execution.error&.lines&.first&.strip %></td>
23
+ <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
24
+ <td class="sqw-actions">
25
+ <%= button_to "Retry", retry_failed_job_path(execution),
26
+ method: :post, class: "sqw-btn sqw-btn--sm" %>
27
+ <%= button_to "Discard", failed_job_path(execution),
28
+ method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
29
+ data: { turbo_confirm: "Discard this job?" } %>
30
+ </td>
31
+ </tr>
32
+ <% end %>
33
+ </tbody>
34
+ </table>
35
+ <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
36
+ <% else %>
37
+ <div class="sqw-empty">
38
+ <p>No failed jobs.</p>
39
+ </div>
40
+ <% end %>
41
+ </div>
@@ -0,0 +1,3 @@
1
+ <div id="sqw-empty" class="sqw-empty">
2
+ <p>No <strong><%= @status %></strong> jobs.</p>
3
+ </div>
@@ -0,0 +1,7 @@
1
+ <% if @executions_remain %>
2
+ <%= turbo_stream.remove "execution_#{params[:id]}" %>
3
+ <% else %>
4
+ <%= turbo_stream.replace "sqw-jobs-table" do %>
5
+ <%= render "empty" %>
6
+ <% end %>
7
+ <% end %>
@@ -0,0 +1,50 @@
1
+ <div class="sqw-page-header">
2
+ <h1 class="sqw-page-title">Jobs</h1>
3
+ </div>
4
+
5
+ <div class="sqw-tabs">
6
+ <% [["ready", "Ready"], ["scheduled", "Scheduled"], ["claimed", "Running"], ["blocked", "Blocked"]].each do |status, label| %>
7
+ <%= link_to label, jobs_path(status: status),
8
+ class: "sqw-tab #{"sqw-tab--active" if @status == status}" %>
9
+ <% end %>
10
+ </div>
11
+
12
+ <div id="sqw-jobs-table">
13
+ <% if @executions.any? %>
14
+ <table class="sqw-table">
15
+ <thead>
16
+ <tr>
17
+ <th>Job Class</th>
18
+ <th>Queue</th>
19
+ <th>Priority</th>
20
+ <th>Enqueued At</th>
21
+ <% if @status == "scheduled" %><th>Scheduled At</th><% end %>
22
+ <th></th>
23
+ </tr>
24
+ </thead>
25
+ <tbody>
26
+ <% @executions.each do |execution| %>
27
+ <tr id="execution_<%= execution.id %>">
28
+ <td class="sqw-monospace"><%= execution.job.class_name %></td>
29
+ <td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
30
+ <td><%= execution.job.priority %></td>
31
+ <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
32
+ <% if @status == "scheduled" %>
33
+ <td class="sqw-muted"><%= execution.scheduled_at&.strftime("%b %d %H:%M") %></td>
34
+ <% end %>
35
+ <td class="sqw-actions">
36
+ <% if %w[ready scheduled blocked].include?(@status) %>
37
+ <%= button_to "Discard", job_path(execution, status: @status),
38
+ method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
39
+ data: { turbo_confirm: "Discard this job?" } %>
40
+ <% end %>
41
+ </td>
42
+ </tr>
43
+ <% end %>
44
+ </tbody>
45
+ </table>
46
+ <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
47
+ <% else %>
48
+ <%= render "empty" %>
49
+ <% end %>
50
+ </div>
@@ -0,0 +1,32 @@
1
+ <div class="sqw-page-header">
2
+ <h1 class="sqw-page-title">Processes</h1>
3
+ </div>
4
+
5
+ <% if @processes.any? %>
6
+ <table class="sqw-table">
7
+ <thead>
8
+ <tr>
9
+ <th>Kind</th>
10
+ <th>Name</th>
11
+ <th>PID</th>
12
+ <th>Host</th>
13
+ <th>Last Heartbeat</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ <% @processes.each do |process| %>
18
+ <tr>
19
+ <td><span class="sqw-badge sqw-badge--<%= process.kind.downcase %>"><%= process.kind %></span></td>
20
+ <td class="sqw-monospace"><%= process.name %></td>
21
+ <td class="sqw-monospace"><%= process.pid %></td>
22
+ <td class="sqw-muted"><%= process.hostname %></td>
23
+ <td class="sqw-muted"><%= process.last_heartbeat_at&.strftime("%b %d %H:%M:%S") %></td>
24
+ </tr>
25
+ <% end %>
26
+ </tbody>
27
+ </table>
28
+ <% else %>
29
+ <div class="sqw-empty">
30
+ <p>No active processes.</p>
31
+ </div>
32
+ <% end %>
@@ -0,0 +1,44 @@
1
+ <div class="sqw-page-header">
2
+ <h1 class="sqw-page-title">Queues</h1>
3
+ </div>
4
+
5
+ <% if @queues.any? %>
6
+ <table class="sqw-table">
7
+ <thead>
8
+ <tr>
9
+ <th>Name</th>
10
+ <th>Size</th>
11
+ <th>Status</th>
12
+ <th></th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <% @queues.each do |queue| %>
17
+ <tr>
18
+ <td class="sqw-monospace"><%= queue[:name] %></td>
19
+ <td><%= queue[:size] %></td>
20
+ <td>
21
+ <% if queue[:paused] %>
22
+ <span class="sqw-badge sqw-badge--paused">Paused</span>
23
+ <% else %>
24
+ <span class="sqw-badge sqw-badge--ready">Running</span>
25
+ <% end %>
26
+ </td>
27
+ <td class="sqw-actions">
28
+ <% if queue[:paused] %>
29
+ <%= button_to "Resume", resume_queue_path(queue[:name]),
30
+ method: :delete, class: "sqw-btn sqw-btn--sm" %>
31
+ <% else %>
32
+ <%= button_to "Pause", pause_queue_path(queue[:name]),
33
+ method: :post, class: "sqw-btn sqw-btn--sm" %>
34
+ <% end %>
35
+ </td>
36
+ </tr>
37
+ <% end %>
38
+ </tbody>
39
+ </table>
40
+ <% else %>
41
+ <div class="sqw-empty">
42
+ <p>No queues with ready jobs.</p>
43
+ </div>
44
+ <% end %>
data/config/routes.rb ADDED
@@ -0,0 +1,21 @@
1
+ SolidStackWeb::Engine.routes.draw do
2
+ root to: "dashboard#index"
3
+
4
+ resources :jobs, only: [:index, :destroy]
5
+
6
+ resources :failed_jobs, only: [:index, :destroy] do
7
+ member { post :retry }
8
+ end
9
+
10
+ resources :queues, only: [:index] do
11
+ member do
12
+ post :pause
13
+ delete :resume
14
+ end
15
+ end
16
+
17
+ resources :processes, only: [:index]
18
+
19
+ get "cache", to: "cache#index", as: :cache
20
+ get "cable", to: "cable#index", as: :cable
21
+ end
@@ -0,0 +1,19 @@
1
+ require "pagy"
2
+ require "pagy/toolbox/paginators/method"
3
+ require "solid_queue"
4
+ require "solid_cache"
5
+ require "solid_cable"
6
+
7
+ module SolidStackWeb
8
+ class Engine < ::Rails::Engine
9
+ isolate_namespace SolidStackWeb
10
+
11
+ config.i18n.load_path += Gem.find_files("pagy/locales/en.yml")
12
+
13
+ initializer "solid_stack_web.pagy" do |app|
14
+ app.config.after_initialize do
15
+ Pagy::OPTIONS[:limit] = SolidStackWeb.page_size
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module SolidStackWeb
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,25 @@
1
+ require "solid_stack_web/version"
2
+ require "solid_stack_web/engine"
3
+
4
+ module SolidStackWeb
5
+ class << self
6
+ attr_writer :page_size, :connects_to
7
+
8
+ def page_size
9
+ @page_size || 25
10
+ end
11
+
12
+ def connects_to
13
+ @connects_to
14
+ end
15
+
16
+ def configure
17
+ yield self
18
+ end
19
+
20
+ def authenticate(&block)
21
+ @authenticate = block if block_given?
22
+ @authenticate
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :solid_stack_web do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: solid_stack_web
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chuck Smith
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 8.1.3
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 8.1.3
26
+ - !ruby/object:Gem::Dependency
27
+ name: pagy
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '43.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '43.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: solid_queue
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: solid_cache
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '1.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: solid_cable
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '1.0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '1.0'
82
+ description: Mount SolidStackWeb in any Rails app using the Solid Stack to get a single
83
+ dashboard covering Solid Queue job monitoring, Solid Cache statistics, and Solid
84
+ Cable connection observability — all without leaving your app.
85
+ email:
86
+ - eclectic-coding@users.noreply.github.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - MIT-LICENSE
92
+ - README.md
93
+ - Rakefile
94
+ - app/assets/stylesheets/solid_stack_web/_01_base.css
95
+ - app/assets/stylesheets/solid_stack_web/_02_layout.css
96
+ - app/assets/stylesheets/solid_stack_web/_03_stats.css
97
+ - app/assets/stylesheets/solid_stack_web/_04_table.css
98
+ - app/assets/stylesheets/solid_stack_web/_05_badges.css
99
+ - app/assets/stylesheets/solid_stack_web/_06_buttons.css
100
+ - app/assets/stylesheets/solid_stack_web/_07_dashboard.css
101
+ - app/assets/stylesheets/solid_stack_web/application.css
102
+ - app/controllers/solid_stack_web/application_controller.rb
103
+ - app/controllers/solid_stack_web/cable_controller.rb
104
+ - app/controllers/solid_stack_web/cache_controller.rb
105
+ - app/controllers/solid_stack_web/dashboard_controller.rb
106
+ - app/controllers/solid_stack_web/failed_jobs_controller.rb
107
+ - app/controllers/solid_stack_web/jobs_controller.rb
108
+ - app/controllers/solid_stack_web/processes_controller.rb
109
+ - app/controllers/solid_stack_web/queues_controller.rb
110
+ - app/helpers/solid_stack_web/application_helper.rb
111
+ - app/views/layouts/solid_stack_web/application.html.erb
112
+ - app/views/solid_stack_web/cable/index.html.erb
113
+ - app/views/solid_stack_web/cache/index.html.erb
114
+ - app/views/solid_stack_web/dashboard/index.html.erb
115
+ - app/views/solid_stack_web/failed_jobs/index.html.erb
116
+ - app/views/solid_stack_web/jobs/_empty.html.erb
117
+ - app/views/solid_stack_web/jobs/destroy.turbo_stream.erb
118
+ - app/views/solid_stack_web/jobs/index.html.erb
119
+ - app/views/solid_stack_web/processes/index.html.erb
120
+ - app/views/solid_stack_web/queues/index.html.erb
121
+ - config/routes.rb
122
+ - lib/solid_stack_web.rb
123
+ - lib/solid_stack_web/engine.rb
124
+ - lib/solid_stack_web/version.rb
125
+ - lib/tasks/solid_stack_web_tasks.rake
126
+ homepage: https://github.com/eclectic-coding/solid_stack_web
127
+ licenses:
128
+ - MIT
129
+ metadata:
130
+ homepage_uri: https://github.com/eclectic-coding/solid_stack_web
131
+ source_code_uri: https://github.com/eclectic-coding/solid_stack_web
132
+ changelog_uri: https://github.com/eclectic-coding/solid_stack_web/blob/main/CHANGELOG.md
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '3.3'
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ requirements: []
147
+ rubygems_version: 4.0.10
148
+ specification_version: 4
149
+ summary: A unified Rails engine dashboard for Solid Queue, Solid Cache, and Solid
150
+ Cable.
151
+ test_files: []