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 .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Style/Documentation:

Metrics/ClassLength:
Max: 500
Exclude:
- 'app/services/solid_queue_monitor/stylesheet_generator.rb'

Metrics/ModuleLength:
Max: 200
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## [0.4.0] - 2026-01-09

### Added

- Auto-refresh feature for real-time dashboard monitoring
- Configurable auto-refresh interval via `config.auto_refresh_interval` (default: 30 seconds)
- Toggle to enable/disable auto-refresh globally via `config.auto_refresh_enabled`
- Compact auto-refresh controls integrated into header with:
- iOS-style toggle switch to enable/disable auto-refresh
- Live countdown timer showing seconds until next refresh
- Pulsing green indicator when auto-refresh is active
- Icon-based refresh button for immediate page reload
- Informative tooltip on hover explaining the feature
- User preference persistence via localStorage (survives page reloads)
- Responsive design for auto-refresh controls on mobile devices

## [0.3.2] - 2025-06-12

### Added
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
solid_queue_monitor (0.3.2)
solid_queue_monitor (0.4.0)
rails (>= 7.0)
solid_queue (>= 0.1.0)

Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
- **Advanced Job Filtering**: Filter jobs by class name, queue, status, and job arguments
- **Quick Actions**: Retry or discard failed jobs, execute or reject scheduled jobs directly from any view
- **Performance Optimized**: Designed for high-volume applications with smart pagination
- **Auto-refresh**: Real-time monitoring with configurable auto-refresh interval and toggle
- **Optional Authentication**: Secure your dashboard with HTTP Basic Authentication
- **Responsive Design**: Works on desktop and mobile devices
- **Zero Dependencies**: No additional JavaScript libraries or frameworks required
Expand All @@ -44,7 +45,7 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
Add this line to your application's Gemfile:

```ruby
gem 'solid_queue_monitor', '~> 0.3.2'
gem 'solid_queue_monitor', '~> 0.4.0'
```

Then execute:
Expand Down Expand Up @@ -83,6 +84,13 @@ SolidQueueMonitor.setup do |config|

# Number of jobs to display per page
config.jobs_per_page = 25

# Auto-refresh settings
# Enable or disable auto-refresh globally (users can still toggle it in the UI)
config.auto_refresh_enabled = true

# Auto-refresh interval in seconds (default: 30)
config.auto_refresh_interval = 30
end
```

Expand Down
104 changes: 103 additions & 1 deletion app/services/solid_queue_monitor/html_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def generate_body
</div>
#{generate_footer}
</div>
#{generate_auto_refresh_script}
HTML
end

Expand Down Expand Up @@ -88,7 +89,10 @@ def render_message
def generate_header
<<-HTML
<header>
<h1>Solid Queue Monitor</h1>
<div class="header-top">
<h1>Solid Queue Monitor</h1>
#{generate_auto_refresh_controls}
</div>
<nav class="navigation">
<a href="#{root_path}" class="nav-link">Overview</a>
<a href="#{ready_jobs_path}" class="nav-link">Ready Jobs</a>
Expand All @@ -110,6 +114,104 @@ def generate_footer
HTML
end

def generate_auto_refresh_controls
return '' unless SolidQueueMonitor.auto_refresh_enabled

interval = SolidQueueMonitor.auto_refresh_interval
<<-HTML
<div class="auto-refresh-container" title="Auto-refresh every #{interval}s" data-tooltip="Auto-refresh: Dashboard updates automatically every #{interval} seconds. Toggle to enable/disable.">
<span class="auto-refresh-indicator" id="auto-refresh-indicator"></span>
<span class="auto-refresh-countdown" id="auto-refresh-countdown">#{interval}s</span>
<label class="auto-refresh-switch" title="Toggle auto-refresh">
<input type="checkbox" id="auto-refresh-toggle" checked>
<span class="switch-slider"></span>
</label>
<button class="refresh-now-btn" id="refresh-now-btn" title="Refresh now">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16"/>
</svg>
</button>
</div>
HTML
end

def generate_auto_refresh_script
return '' unless SolidQueueMonitor.auto_refresh_enabled

"<script>#{auto_refresh_javascript}</script>"
end

def auto_refresh_javascript
interval = SolidQueueMonitor.auto_refresh_interval
<<-JS
(function() {
var REFRESH_INTERVAL = #{interval};
var countdown = REFRESH_INTERVAL;
var timerId = null;
var isEnabled = localStorage.getItem('sqm_auto_refresh') !== 'false';
#{auto_refresh_dom_elements}
#{auto_refresh_functions}
#{auto_refresh_event_listeners}
#{auto_refresh_init}
})();
JS
end

def auto_refresh_dom_elements
<<-JS
var toggle = document.getElementById('auto-refresh-toggle');
var indicator = document.getElementById('auto-refresh-indicator');
var countdownEl = document.getElementById('auto-refresh-countdown');
var refreshBtn = document.getElementById('refresh-now-btn');
JS
end

def auto_refresh_functions
<<-JS
function updateUI() {
if (toggle) toggle.checked = isEnabled;
if (indicator) indicator.classList.toggle('active', isEnabled);
if (countdownEl) {
countdownEl.textContent = countdown + 's';
countdownEl.style.opacity = isEnabled ? '1' : '0.4';
}
}
function tick() {
countdown--;
if (countdown <= 0) { refresh(); } else { updateUI(); }
}
function startTimer() {
stopTimer();
countdown = REFRESH_INTERVAL;
updateUI();
timerId = setInterval(tick, 1000);
}
function stopTimer() {
if (timerId) { clearInterval(timerId); timerId = null; }
}
function refresh() { window.location.reload(); }
function setEnabled(enabled) {
isEnabled = enabled;
localStorage.setItem('sqm_auto_refresh', enabled ? 'true' : 'false');
if (enabled) { startTimer(); } else { stopTimer(); countdown = REFRESH_INTERVAL; updateUI(); }
}
JS
end

def auto_refresh_event_listeners
<<-JS
if (toggle) { toggle.addEventListener('change', function() { setEnabled(this.checked); }); }
if (refreshBtn) { refreshBtn.addEventListener('click', function() { refresh(); }); }
JS
end

def auto_refresh_init
<<-JS
updateUI();
if (isEnabled) { startTimer(); }
JS
end

def default_url_options
{ only_path: true }
end
Expand Down
177 changes: 177 additions & 0 deletions app/services/solid_queue_monitor/stylesheet_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,183 @@ def generate
.solid_queue_monitor .execute-button:hover {
background: #2563eb;
}

/* Header top row with title and auto-refresh */
.solid_queue_monitor .header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}

/* Auto-refresh styles - compact design */
.solid_queue_monitor .auto-refresh-container {
position: relative;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.625rem;
background: white;
border-radius: 2rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
font-size: 0.75rem;
color: #6b7280;
cursor: default;
}

/* Tooltip styles */
.solid_queue_monitor .auto-refresh-container::after {
content: attr(data-tooltip);
position: absolute;
top: calc(100% + 8px);
right: 0;
background: #1f2937;
color: white;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.75rem;
line-height: 1.4;
white-space: nowrap;
max-width: 280px;
white-space: normal;
text-align: left;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
z-index: 1000;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
pointer-events: none;
}

/* Tooltip arrow */
.solid_queue_monitor .auto-refresh-container::before {
content: "";
position: absolute;
top: calc(100% + 2px);
right: 16px;
border: 6px solid transparent;
border-bottom-color: #1f2937;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
z-index: 1001;
pointer-events: none;
}

.solid_queue_monitor .auto-refresh-container:hover::after,
.solid_queue_monitor .auto-refresh-container:hover::before {
opacity: 1;
visibility: visible;
}

.solid_queue_monitor .auto-refresh-indicator {
width: 6px;
height: 6px;
border-radius: 50%;
background: #d1d5db;
flex-shrink: 0;
}

.solid_queue_monitor .auto-refresh-indicator.active {
background: var(--success-color);
animation: pulse 2s infinite;
}

@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}

.solid_queue_monitor .auto-refresh-countdown {
font-variant-numeric: tabular-nums;
font-weight: 500;
min-width: 1.75rem;
color: var(--text-color);
transition: opacity 0.2s;
}

/* Toggle switch */
.solid_queue_monitor .auto-refresh-switch {
position: relative;
display: inline-block;
width: 32px;
height: 18px;
flex-shrink: 0;
}

.solid_queue_monitor .auto-refresh-switch input {
opacity: 0;
width: 0;
height: 0;
}

.solid_queue_monitor .switch-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #d1d5db;
transition: 0.2s;
border-radius: 18px;
}

.solid_queue_monitor .switch-slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 2px;
bottom: 2px;
background-color: white;
transition: 0.2s;
border-radius: 50%;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}

.solid_queue_monitor .auto-refresh-switch input:checked + .switch-slider {
background-color: var(--success-color);
}

.solid_queue_monitor .auto-refresh-switch input:checked + .switch-slider:before {
transform: translateX(14px);
}

.solid_queue_monitor .refresh-now-btn {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
padding: 0.25rem;
border-radius: 0.25rem;
cursor: pointer;
color: #9ca3af;
transition: all 0.2s;
}

.solid_queue_monitor .refresh-now-btn:hover {
color: var(--primary-color);
background: rgba(59, 130, 246, 0.1);
}

@media (max-width: 768px) {
.solid_queue_monitor .header-top {
flex-direction: column;
gap: 0.75rem;
}

.solid_queue_monitor .auto-refresh-container {
align-self: center;
}

/* Hide tooltip on mobile - use native title instead */
.solid_queue_monitor .auto-refresh-container::after,
.solid_queue_monitor .auto-refresh-container::before {
display: none;
}
}
CSS
end
end
Expand Down
2 changes: 2 additions & 0 deletions config/initializers/solid_queue_monitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@
config.username = 'admin' # Change this in your application
config.password = 'password' # Change this in your application
config.jobs_per_page = 25
config.auto_refresh_enabled = true # Enable/disable auto-refresh globally
config.auto_refresh_interval = 30 # Auto-refresh interval in seconds
end
7 changes: 7 additions & 0 deletions lib/generators/solid_queue_monitor/templates/initializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@

# Number of jobs to display per page
# config.jobs_per_page = 25

# Auto-refresh settings
# Enable or disable auto-refresh globally (users can still toggle it in the UI)
# config.auto_refresh_enabled = true

# Auto-refresh interval in seconds (default: 30)
# config.auto_refresh_interval = 30
end
Loading