Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
/spec/dummy/log/*.log
/spec/dummy/storage/
/spec/dummy/tmp/

coverage/
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ AllCops:
- "bin/release"
- "README.md"
- "spec/dummy/config/database.yml"
- "spec/dummy/db/seeds.rb"

Layout/SpaceInsideArrayLiteralBrackets:
Enabled: true
Expand Down
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

[Unreleased]: https://github.com/eclectic-coding/solid_stack_web/compare/HEAD
## [0.1.0] - 2026-05-24

### Added

- Overview dashboard with live stats across Solid Queue, Solid Cache, and Solid Cable
- Solid Queue job monitoring: filterable list by status (ready, scheduled, claimed, blocked) with paginated results
- Solid Queue failed jobs: dedicated view with per-job retry and discard actions
- Queue management: pause and resume individual queues
- Worker process list showing all registered Solid Queue processes
- Solid Cache monitoring: entry count and total byte size
- Solid Cable monitoring: message count and active channel count
- Turbo Stream job discard: removes the row inline, or replaces the table with an empty state when the last job is discarded
- Authentication hook — configure via `SolidQueueWeb.authenticate { |controller| ... }` in an initializer; falls back to HTTP Basic if the block returns falsy
- Configurable page size via `SolidQueueWeb.page_size` (default: 25)
- Inline CSS delivery — no asset pipeline dependency, safe to mount in any host app
- Two-tier contextual navigation per section (Queue / Cache / Cable)
- No runtime JavaScript dependency — all interactions use standard form POSTs or Turbo Stream

[Unreleased]: https://github.com/eclectic-coding/solid_stack_web/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/eclectic-coding/solid_stack_web/releases/tag/v0.1.0
70 changes: 59 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,78 @@
[![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.3-ruby)](https://www.ruby-lang.org)
[![codecov](https://codecov.io/gh/eclectic-coding/solid_stack_web/branch/main/graph/badge.svg)](https://codecov.io/gh/eclectic-coding/solid_stack_web)

Short description and motivation.
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.

## Usage
How to use my plugin.
## Features

- **Overview dashboard** with live counts across all three Solid Stack components
- **Solid Queue** — browse jobs by status (ready, scheduled, claimed, blocked), manage failed jobs (retry / discard), pause/resume queues, and inspect worker processes
- **Solid Cache** — entry count and total byte size at a glance
- **Solid Cable** — active message count and distinct channel count
- **Turbo Stream** job discard — removes the row inline without a full page reload
- **Authentication hook** — plug in your own auth logic (Devise, Basic Auth, custom) via a one-line initializer
- **Zero asset pipeline coupling** — CSS is injected inline; safe to mount in any host app

## Installation
Add this line to your application's Gemfile:

Add the gem to your application's `Gemfile`:

```ruby
gem "solid_stack_web"
```

And then execute:
Run:

```bash
$ bundle
bundle install
```

Or install it yourself as:
```bash
$ gem install solid_stack_web
Mount the engine in `config/routes.rb`:

```ruby
mount SolidStackWeb::Engine, at: "/solid_stack"
```

The dashboard will be available at `/solid_stack` (or whatever path you choose).

## Configuration

Create an initializer at `config/initializers/solid_stack_web.rb`:

```ruby
SolidStackWeb.configure do |config|
# Number of items per paginated page (default: 25)
config.page_size = 50

# Authentication — block runs in controller context.
# Return a truthy value to allow access; falsy falls back to HTTP Basic.
config.authenticate do
current_user&.admin?
end
end
```

### Authentication

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.

## Requirements

- Ruby >= 3.3
- Rails >= 8.1.3
- [solid_queue](https://github.com/rails/solid_queue) >= 1.0
- [solid_cache](https://github.com/rails/solid_cache) >= 1.0
- [solid_cable](https://github.com/rails/solid_cable) >= 1.0

## Contributing
Contribution directions go here.

1. Fork the repository
2. Create a feature branch (`git checkout -b feat/my-feature`)
3. Run the test suite: `bundle exec rake`
4. Open a pull request

Bug reports and feature requests are welcome on [GitHub Issues](https://github.com/eclectic-coding/solid_stack_web/issues).

## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
32 changes: 32 additions & 0 deletions app/assets/stylesheets/solid_stack_web/_01_base.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
*, *::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;
--radius: 6px;
--shadow: 0 1px 3px rgba(0,0,0,.08);
}

body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text);
background: var(--bg);
}

a { color: var(--primary); text-decoration: none; }
a:hover { text-decoration: underline; }

.sqw-monospace { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 13px; }
.sqw-muted { color: var(--muted); }
.sqw-truncate { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
83 changes: 83 additions & 0 deletions app/assets/stylesheets/solid_stack_web/_02_layout.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
.sqw-header {
background: var(--surface);
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow);
position: sticky;
top: 0;
z-index: 100;
}

.sqw-header__inner {
display: flex;
align-items: center;
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
height: 52px;
}

.sqw-header__logo {
font-weight: 700;
font-size: 16px;
color: var(--text);
white-space: nowrap;
}
.sqw-header__logo:hover { text-decoration: none; color: var(--primary); }

.sqw-nav { display: flex; gap: 0.25rem; }

.sqw-nav__link {
padding: 0.35rem 0.75rem;
border-radius: var(--radius);
color: var(--muted);
font-size: 13px;
font-weight: 500;
transition: background 0.1s, color 0.1s;
}
.sqw-nav__link:hover { background: var(--bg); color: var(--text); text-decoration: none; }
.sqw-nav__link--active { background: var(--bg); color: var(--text); }

.sqw-subnav {
background: var(--bg);
border-bottom: 1px solid var(--border);
}

.sqw-subnav__inner {
display: flex;
align-items: center;
gap: 0.125rem;
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
height: 36px;
}

.sqw-subnav__link {
padding: 0.2rem 0.625rem;
border-radius: var(--radius);
color: var(--muted);
font-size: 12px;
font-weight: 500;
transition: background 0.1s, color 0.1s;
}
.sqw-subnav__link:hover { background: var(--surface); color: var(--text); text-decoration: none; }
.sqw-subnav__link--active { background: var(--surface); color: var(--text); }

.sqw-main {
max-width: 1200px;
margin: 0 auto;
padding: 1.5rem;
}

.sqw-page-header { margin-bottom: 1.25rem; }
.sqw-page-title { font-size: 20px; font-weight: 600; }

.sqw-flash {
padding: 0.75rem 1rem;
border-radius: var(--radius);
margin-bottom: 1rem;
font-size: 13px;
}
.sqw-flash--notice { background: #d1e7dd; color: #0a3622; border: 1px solid #a3cfbb; }
.sqw-flash--alert { background: #f8d7da; color: #58151c; border: 1px solid #f1aeb5; }
31 changes: 31 additions & 0 deletions app/assets/stylesheets/solid_stack_web/_03_stats.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.sqw-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}

.sqw-stat {
display: flex;
flex-direction: column;
gap: 0.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem 1.25rem;
box-shadow: var(--shadow);
color: var(--text);
transition: box-shadow 0.15s;
}
a.sqw-stat:hover { box-shadow: 0 3px 8px rgba(0,0,0,.12); text-decoration: none; }

.sqw-stat__label { font-size: 12px; font-weight: 500; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.sqw-stat__value { font-size: 28px; font-weight: 700; line-height: 1; }

.sqw-stat--ready .sqw-stat__value { color: var(--success); }
.sqw-stat--scheduled .sqw-stat__value { color: var(--info); }
.sqw-stat--claimed .sqw-stat__value { color: var(--primary); }
.sqw-stat--blocked .sqw-stat__value { color: var(--warning); }
.sqw-stat--failed .sqw-stat__value { color: var(--danger); }
.sqw-stat--cache .sqw-stat__value { color: var(--purple); }
.sqw-stat--cable .sqw-stat__value { color: var(--info); }
39 changes: 39 additions & 0 deletions app/assets/stylesheets/solid_stack_web/_04_table.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.sqw-table {
width: 100%;
border-collapse: collapse;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}

.sqw-table th,
.sqw-table td {
padding: 0.6rem 0.875rem;
text-align: left;
border-bottom: 1px solid var(--border);
}

.sqw-table th {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .05em;
color: var(--muted);
background: var(--bg);
}

.sqw-table tbody tr:last-child td { border-bottom: none; }
.sqw-table tbody tr:hover { background: #f9fafb; }

.sqw-actions { text-align: right; white-space: nowrap; }

.sqw-empty {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 3rem 1.5rem;
text-align: center;
color: var(--muted);
}
20 changes: 20 additions & 0 deletions app/assets/stylesheets/solid_stack_web/_05_badges.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.sqw-badge {
display: inline-block;
padding: 0.2em 0.55em;
font-size: 11px;
font-weight: 600;
border-radius: 4px;
text-transform: uppercase;
letter-spacing: .04em;
}

.sqw-badge--ready { background: #d1e7dd; color: #0a3622; }
.sqw-badge--scheduled { background: #cff4fc; color: #055160; }
.sqw-badge--claimed { background: #cfe2ff; color: #084298; }
.sqw-badge--blocked { background: #fff3cd; color: #664d03; }
.sqw-badge--failed { background: #f8d7da; color: #58151c; }
.sqw-badge--paused { background: #e2e3e5; color: #41464b; }
.sqw-badge--queue { background: #e9ecef; color: #495057; font-weight: 500; text-transform: none; letter-spacing: 0; }
.sqw-badge--worker { background: #cfe2ff; color: #084298; }
.sqw-badge--supervisor { background: #d1e7dd; color: #0a3622; }
.sqw-badge--dispatcher { background: #fff3cd; color: #664d03; }
40 changes: 40 additions & 0 deletions app/assets/stylesheets/solid_stack_web/_06_buttons.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.sqw-btn {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.4rem 0.875rem;
font-size: 13px;
font-weight: 500;
border: 1px solid transparent;
border-radius: var(--radius);
cursor: pointer;
background: var(--primary);
color: #fff;
transition: opacity 0.1s;
}
.sqw-btn:hover { opacity: 0.88; text-decoration: none; }

.sqw-btn--sm { padding: 0.25rem 0.6rem; font-size: 12px; }
.sqw-btn--danger { background: var(--danger); }
.sqw-btn--muted { background: var(--surface); color: var(--text); border-color: var(--border); }
.sqw-btn--muted:hover { background: var(--bg); }

.sqw-tabs {
display: flex;
gap: 0.25rem;
margin-bottom: 1rem;
border-bottom: 1px solid var(--border);
padding-bottom: 0;
}

.sqw-tab {
padding: 0.5rem 1rem;
font-size: 13px;
font-weight: 500;
color: var(--muted);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.1s, border-color 0.1s;
}
.sqw-tab:hover { color: var(--text); text-decoration: none; }
.sqw-tab--active { color: var(--primary); border-bottom-color: var(--primary); }
Loading
Loading