Skip to content

Commit 682db41

Browse files
authored
feat: add auto-refresh feature for real-time dashboard monitoring (#12)
* feat: add auto-refresh feature for real-time dashboard monitoring - Add configurable auto-refresh with toggle (default: 30 seconds) - Integrate compact controls into header with iOS-style toggle switch - Add live countdown timer and pulsing indicator when active - Add icon-based refresh button for immediate page reload - Add informative tooltip on hover explaining the feature - Persist user preference in localStorage across sessions - Support responsive design for mobile devices - Add configuration options: auto_refresh_enabled, auto_refresh_interval * fix: resolve rubocop offenses for auto-refresh feature - Split generate_auto_refresh_script into smaller methods - Exclude stylesheet_generator.rb from ClassLength check (CSS template)
1 parent 31fce34 commit 682db41

10 files changed

Lines changed: 322 additions & 5 deletions

File tree

.rubocop.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Style/Documentation:
2222

2323
Metrics/ClassLength:
2424
Max: 500
25+
Exclude:
26+
- 'app/services/solid_queue_monitor/stylesheet_generator.rb'
2527

2628
Metrics/ModuleLength:
2729
Max: 200

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# Changelog
22

3+
## [0.4.0] - 2026-01-09
4+
5+
### Added
6+
7+
- Auto-refresh feature for real-time dashboard monitoring
8+
- Configurable auto-refresh interval via `config.auto_refresh_interval` (default: 30 seconds)
9+
- Toggle to enable/disable auto-refresh globally via `config.auto_refresh_enabled`
10+
- Compact auto-refresh controls integrated into header with:
11+
- iOS-style toggle switch to enable/disable auto-refresh
12+
- Live countdown timer showing seconds until next refresh
13+
- Pulsing green indicator when auto-refresh is active
14+
- Icon-based refresh button for immediate page reload
15+
- Informative tooltip on hover explaining the feature
16+
- User preference persistence via localStorage (survives page reloads)
17+
- Responsive design for auto-refresh controls on mobile devices
18+
319
## [0.3.2] - 2025-06-12
420

521
### Added

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
solid_queue_monitor (0.3.2)
4+
solid_queue_monitor (0.4.0)
55
rails (>= 7.0)
66
solid_queue (>= 0.1.0)
77

README.md

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

4647
```ruby
47-
gem 'solid_queue_monitor', '~> 0.3.2'
48+
gem 'solid_queue_monitor', '~> 0.4.0'
4849
```
4950

5051
Then execute:
@@ -83,6 +84,13 @@ SolidQueueMonitor.setup do |config|
8384

8485
# Number of jobs to display per page
8586
config.jobs_per_page = 25
87+
88+
# Auto-refresh settings
89+
# Enable or disable auto-refresh globally (users can still toggle it in the UI)
90+
config.auto_refresh_enabled = true
91+
92+
# Auto-refresh interval in seconds (default: 30)
93+
config.auto_refresh_interval = 30
8694
end
8795
```
8896

app/services/solid_queue_monitor/html_generator.rb

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def generate_body
5050
</div>
5151
#{generate_footer}
5252
</div>
53+
#{generate_auto_refresh_script}
5354
HTML
5455
end
5556

@@ -88,7 +89,10 @@ def render_message
8889
def generate_header
8990
<<-HTML
9091
<header>
91-
<h1>Solid Queue Monitor</h1>
92+
<div class="header-top">
93+
<h1>Solid Queue Monitor</h1>
94+
#{generate_auto_refresh_controls}
95+
</div>
9296
<nav class="navigation">
9397
<a href="#{root_path}" class="nav-link">Overview</a>
9498
<a href="#{ready_jobs_path}" class="nav-link">Ready Jobs</a>
@@ -110,6 +114,104 @@ def generate_footer
110114
HTML
111115
end
112116

117+
def generate_auto_refresh_controls
118+
return '' unless SolidQueueMonitor.auto_refresh_enabled
119+
120+
interval = SolidQueueMonitor.auto_refresh_interval
121+
<<-HTML
122+
<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.">
123+
<span class="auto-refresh-indicator" id="auto-refresh-indicator"></span>
124+
<span class="auto-refresh-countdown" id="auto-refresh-countdown">#{interval}s</span>
125+
<label class="auto-refresh-switch" title="Toggle auto-refresh">
126+
<input type="checkbox" id="auto-refresh-toggle" checked>
127+
<span class="switch-slider"></span>
128+
</label>
129+
<button class="refresh-now-btn" id="refresh-now-btn" title="Refresh now">
130+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
131+
<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"/>
132+
</svg>
133+
</button>
134+
</div>
135+
HTML
136+
end
137+
138+
def generate_auto_refresh_script
139+
return '' unless SolidQueueMonitor.auto_refresh_enabled
140+
141+
"<script>#{auto_refresh_javascript}</script>"
142+
end
143+
144+
def auto_refresh_javascript
145+
interval = SolidQueueMonitor.auto_refresh_interval
146+
<<-JS
147+
(function() {
148+
var REFRESH_INTERVAL = #{interval};
149+
var countdown = REFRESH_INTERVAL;
150+
var timerId = null;
151+
var isEnabled = localStorage.getItem('sqm_auto_refresh') !== 'false';
152+
#{auto_refresh_dom_elements}
153+
#{auto_refresh_functions}
154+
#{auto_refresh_event_listeners}
155+
#{auto_refresh_init}
156+
})();
157+
JS
158+
end
159+
160+
def auto_refresh_dom_elements
161+
<<-JS
162+
var toggle = document.getElementById('auto-refresh-toggle');
163+
var indicator = document.getElementById('auto-refresh-indicator');
164+
var countdownEl = document.getElementById('auto-refresh-countdown');
165+
var refreshBtn = document.getElementById('refresh-now-btn');
166+
JS
167+
end
168+
169+
def auto_refresh_functions
170+
<<-JS
171+
function updateUI() {
172+
if (toggle) toggle.checked = isEnabled;
173+
if (indicator) indicator.classList.toggle('active', isEnabled);
174+
if (countdownEl) {
175+
countdownEl.textContent = countdown + 's';
176+
countdownEl.style.opacity = isEnabled ? '1' : '0.4';
177+
}
178+
}
179+
function tick() {
180+
countdown--;
181+
if (countdown <= 0) { refresh(); } else { updateUI(); }
182+
}
183+
function startTimer() {
184+
stopTimer();
185+
countdown = REFRESH_INTERVAL;
186+
updateUI();
187+
timerId = setInterval(tick, 1000);
188+
}
189+
function stopTimer() {
190+
if (timerId) { clearInterval(timerId); timerId = null; }
191+
}
192+
function refresh() { window.location.reload(); }
193+
function setEnabled(enabled) {
194+
isEnabled = enabled;
195+
localStorage.setItem('sqm_auto_refresh', enabled ? 'true' : 'false');
196+
if (enabled) { startTimer(); } else { stopTimer(); countdown = REFRESH_INTERVAL; updateUI(); }
197+
}
198+
JS
199+
end
200+
201+
def auto_refresh_event_listeners
202+
<<-JS
203+
if (toggle) { toggle.addEventListener('change', function() { setEnabled(this.checked); }); }
204+
if (refreshBtn) { refreshBtn.addEventListener('click', function() { refresh(); }); }
205+
JS
206+
end
207+
208+
def auto_refresh_init
209+
<<-JS
210+
updateUI();
211+
if (isEnabled) { startTimer(); }
212+
JS
213+
end
214+
113215
def default_url_options
114216
{ only_path: true }
115217
end

app/services/solid_queue_monitor/stylesheet_generator.rb

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,183 @@ def generate
585585
.solid_queue_monitor .execute-button:hover {
586586
background: #2563eb;
587587
}
588+
589+
/* Header top row with title and auto-refresh */
590+
.solid_queue_monitor .header-top {
591+
display: flex;
592+
justify-content: space-between;
593+
align-items: center;
594+
margin-bottom: 0.5rem;
595+
}
596+
597+
/* Auto-refresh styles - compact design */
598+
.solid_queue_monitor .auto-refresh-container {
599+
position: relative;
600+
display: flex;
601+
align-items: center;
602+
gap: 0.5rem;
603+
padding: 0.375rem 0.625rem;
604+
background: white;
605+
border-radius: 2rem;
606+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
607+
font-size: 0.75rem;
608+
color: #6b7280;
609+
cursor: default;
610+
}
611+
612+
/* Tooltip styles */
613+
.solid_queue_monitor .auto-refresh-container::after {
614+
content: attr(data-tooltip);
615+
position: absolute;
616+
top: calc(100% + 8px);
617+
right: 0;
618+
background: #1f2937;
619+
color: white;
620+
padding: 0.5rem 0.75rem;
621+
border-radius: 0.375rem;
622+
font-size: 0.75rem;
623+
line-height: 1.4;
624+
white-space: nowrap;
625+
max-width: 280px;
626+
white-space: normal;
627+
text-align: left;
628+
opacity: 0;
629+
visibility: hidden;
630+
transition: opacity 0.2s, visibility 0.2s;
631+
z-index: 1000;
632+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
633+
pointer-events: none;
634+
}
635+
636+
/* Tooltip arrow */
637+
.solid_queue_monitor .auto-refresh-container::before {
638+
content: "";
639+
position: absolute;
640+
top: calc(100% + 2px);
641+
right: 16px;
642+
border: 6px solid transparent;
643+
border-bottom-color: #1f2937;
644+
opacity: 0;
645+
visibility: hidden;
646+
transition: opacity 0.2s, visibility 0.2s;
647+
z-index: 1001;
648+
pointer-events: none;
649+
}
650+
651+
.solid_queue_monitor .auto-refresh-container:hover::after,
652+
.solid_queue_monitor .auto-refresh-container:hover::before {
653+
opacity: 1;
654+
visibility: visible;
655+
}
656+
657+
.solid_queue_monitor .auto-refresh-indicator {
658+
width: 6px;
659+
height: 6px;
660+
border-radius: 50%;
661+
background: #d1d5db;
662+
flex-shrink: 0;
663+
}
664+
665+
.solid_queue_monitor .auto-refresh-indicator.active {
666+
background: var(--success-color);
667+
animation: pulse 2s infinite;
668+
}
669+
670+
@keyframes pulse {
671+
0%, 100% { opacity: 1; }
672+
50% { opacity: 0.5; }
673+
}
674+
675+
.solid_queue_monitor .auto-refresh-countdown {
676+
font-variant-numeric: tabular-nums;
677+
font-weight: 500;
678+
min-width: 1.75rem;
679+
color: var(--text-color);
680+
transition: opacity 0.2s;
681+
}
682+
683+
/* Toggle switch */
684+
.solid_queue_monitor .auto-refresh-switch {
685+
position: relative;
686+
display: inline-block;
687+
width: 32px;
688+
height: 18px;
689+
flex-shrink: 0;
690+
}
691+
692+
.solid_queue_monitor .auto-refresh-switch input {
693+
opacity: 0;
694+
width: 0;
695+
height: 0;
696+
}
697+
698+
.solid_queue_monitor .switch-slider {
699+
position: absolute;
700+
cursor: pointer;
701+
top: 0;
702+
left: 0;
703+
right: 0;
704+
bottom: 0;
705+
background-color: #d1d5db;
706+
transition: 0.2s;
707+
border-radius: 18px;
708+
}
709+
710+
.solid_queue_monitor .switch-slider:before {
711+
position: absolute;
712+
content: "";
713+
height: 14px;
714+
width: 14px;
715+
left: 2px;
716+
bottom: 2px;
717+
background-color: white;
718+
transition: 0.2s;
719+
border-radius: 50%;
720+
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
721+
}
722+
723+
.solid_queue_monitor .auto-refresh-switch input:checked + .switch-slider {
724+
background-color: var(--success-color);
725+
}
726+
727+
.solid_queue_monitor .auto-refresh-switch input:checked + .switch-slider:before {
728+
transform: translateX(14px);
729+
}
730+
731+
.solid_queue_monitor .refresh-now-btn {
732+
display: flex;
733+
align-items: center;
734+
justify-content: center;
735+
background: transparent;
736+
border: none;
737+
padding: 0.25rem;
738+
border-radius: 0.25rem;
739+
cursor: pointer;
740+
color: #9ca3af;
741+
transition: all 0.2s;
742+
}
743+
744+
.solid_queue_monitor .refresh-now-btn:hover {
745+
color: var(--primary-color);
746+
background: rgba(59, 130, 246, 0.1);
747+
}
748+
749+
@media (max-width: 768px) {
750+
.solid_queue_monitor .header-top {
751+
flex-direction: column;
752+
gap: 0.75rem;
753+
}
754+
755+
.solid_queue_monitor .auto-refresh-container {
756+
align-self: center;
757+
}
758+
759+
/* Hide tooltip on mobile - use native title instead */
760+
.solid_queue_monitor .auto-refresh-container::after,
761+
.solid_queue_monitor .auto-refresh-container::before {
762+
display: none;
763+
}
764+
}
588765
CSS
589766
end
590767
end

config/initializers/solid_queue_monitor.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@
44
config.username = 'admin' # Change this in your application
55
config.password = 'password' # Change this in your application
66
config.jobs_per_page = 25
7+
config.auto_refresh_enabled = true # Enable/disable auto-refresh globally
8+
config.auto_refresh_interval = 30 # Auto-refresh interval in seconds
79
end

lib/generators/solid_queue_monitor/templates/initializer.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,11 @@
1313

1414
# Number of jobs to display per page
1515
# config.jobs_per_page = 25
16+
17+
# Auto-refresh settings
18+
# Enable or disable auto-refresh globally (users can still toggle it in the UI)
19+
# config.auto_refresh_enabled = true
20+
21+
# Auto-refresh interval in seconds (default: 30)
22+
# config.auto_refresh_interval = 30
1623
end

0 commit comments

Comments
 (0)