sktop 0.1.4 → 0.1.6
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.
- checksums.yaml +4 -4
- data/lib/sktop/cli.rb +139 -17
- data/lib/sktop/display.rb +240 -29
- data/lib/sktop/job_actions.rb +9 -0
- data/lib/sktop/stats_collector.rb +48 -2
- data/lib/sktop/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72e27a287d42246bf03393f3f5d8fd83fcf2dcb52d3c32a242f6631a556d5a90
|
|
4
|
+
data.tar.gz: a26fa56421cf3fa038f2d7fa7ec86e204189be4bd21419d1ddf896631f59a8cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7aa57eac65085688adadc80a4d587c6c3e0c100c09518675bf4cc65fdcfac93e37e404a7476eda3a4b4ec2d2b7a595f3e6231a2b1c414475c4907fd52ff4275
|
|
7
|
+
data.tar.gz: ef09ee20ae517a01e85c427bc291d8adff140e0239342336f22365df774efdf83c0df76b34839ed99e15d63a66c36de4eaeb4f7634a24d098c27fab6894055ec
|
data/lib/sktop/cli.rb
CHANGED
|
@@ -164,6 +164,10 @@ module Sktop
|
|
|
164
164
|
# Set up signal handler for Ctrl+C (works even when blocked)
|
|
165
165
|
Signal.trap("INT") { @running = false }
|
|
166
166
|
|
|
167
|
+
# Render loading screen immediately
|
|
168
|
+
@display.connection_status = :connecting
|
|
169
|
+
@display.render_loading
|
|
170
|
+
|
|
167
171
|
# Start background thread for data fetching
|
|
168
172
|
fetch_thread = Thread.new do
|
|
169
173
|
while @running
|
|
@@ -179,6 +183,7 @@ module Sktop
|
|
|
179
183
|
next unless can_fetch
|
|
180
184
|
|
|
181
185
|
begin
|
|
186
|
+
@display.connection_status = :updating
|
|
182
187
|
collector.refresh!
|
|
183
188
|
# Cache a snapshot of the data
|
|
184
189
|
snapshot = {
|
|
@@ -190,12 +195,24 @@ module Sktop
|
|
|
190
195
|
scheduled_jobs: collector.scheduled_jobs(limit: 500),
|
|
191
196
|
dead_jobs: collector.dead_jobs(limit: 500)
|
|
192
197
|
}
|
|
198
|
+
|
|
199
|
+
# If viewing queue jobs, refresh that data too
|
|
200
|
+
if @display.current_view == :queue_jobs && @display.selected_queue
|
|
201
|
+
snapshot[:queue_jobs] = collector.queue_jobs(@display.selected_queue, limit: 500)
|
|
202
|
+
end
|
|
203
|
+
|
|
193
204
|
@data_mutex.synchronize do
|
|
205
|
+
# Preserve queue_jobs if not refreshed above but still in that view
|
|
206
|
+
if @cached_data && @cached_data[:queue_jobs] && !snapshot[:queue_jobs]
|
|
207
|
+
snapshot[:queue_jobs] = @cached_data[:queue_jobs]
|
|
208
|
+
end
|
|
194
209
|
@cached_data = snapshot
|
|
195
210
|
@data_version += 1
|
|
196
211
|
end
|
|
212
|
+
@display.connection_status = :connected
|
|
197
213
|
rescue => e
|
|
198
|
-
|
|
214
|
+
@display.connection_status = :error
|
|
215
|
+
# Will retry next interval
|
|
199
216
|
ensure
|
|
200
217
|
@fetch_mutex.synchronize { @fetch_in_progress = false }
|
|
201
218
|
end
|
|
@@ -211,13 +228,7 @@ module Sktop
|
|
|
211
228
|
begin
|
|
212
229
|
# Set up raw mode for keyboard input
|
|
213
230
|
STDIN.raw do |stdin|
|
|
214
|
-
#
|
|
215
|
-
10.times do
|
|
216
|
-
break if @data_mutex.synchronize { @cached_data }
|
|
217
|
-
sleep 0.1
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# Initial render
|
|
231
|
+
# Initial render (will show loading or data if already fetched)
|
|
221
232
|
render_cached_data
|
|
222
233
|
|
|
223
234
|
while @running
|
|
@@ -258,9 +269,11 @@ module Sktop
|
|
|
258
269
|
|
|
259
270
|
def render_cached_data
|
|
260
271
|
data = @data_mutex.synchronize { @cached_data }
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
272
|
+
if data
|
|
273
|
+
@display.render_refresh_from_cache(data)
|
|
274
|
+
else
|
|
275
|
+
@display.render_loading
|
|
276
|
+
end
|
|
264
277
|
end
|
|
265
278
|
|
|
266
279
|
def handle_keypress(key, stdin)
|
|
@@ -279,6 +292,8 @@ module Sktop
|
|
|
279
292
|
@display.current_view = :dead
|
|
280
293
|
when 'm', 'M'
|
|
281
294
|
@display.current_view = :main
|
|
295
|
+
when "\r", "\n" # Enter key
|
|
296
|
+
handle_enter_action
|
|
282
297
|
when "\x12" # Ctrl+R - retry job
|
|
283
298
|
handle_retry_action
|
|
284
299
|
when "\x18" # Ctrl+X - delete job
|
|
@@ -296,8 +311,10 @@ module Sktop
|
|
|
296
311
|
@display.select_up
|
|
297
312
|
when "[B" # Down arrow
|
|
298
313
|
@display.select_down
|
|
299
|
-
when "[C" # Right arrow
|
|
300
|
-
|
|
314
|
+
when "[C" # Right arrow - next view
|
|
315
|
+
@display.next_view
|
|
316
|
+
when "[D" # Left arrow - previous view
|
|
317
|
+
@display.previous_view
|
|
301
318
|
when "[5~" # Page Up
|
|
302
319
|
@display.page_up
|
|
303
320
|
when "[6~" # Page Down
|
|
@@ -307,12 +324,12 @@ module Sktop
|
|
|
307
324
|
when "x", "X" # Alt+X - Delete All
|
|
308
325
|
handle_delete_all_action
|
|
309
326
|
else
|
|
310
|
-
# Just Escape key - go to main
|
|
311
|
-
|
|
327
|
+
# Just Escape key - go back or to main
|
|
328
|
+
handle_escape_action
|
|
312
329
|
end
|
|
313
330
|
else
|
|
314
|
-
# Just Escape key - go to main
|
|
315
|
-
|
|
331
|
+
# Just Escape key - go back or to main
|
|
332
|
+
handle_escape_action
|
|
316
333
|
end
|
|
317
334
|
when "\u0003" # Ctrl+C
|
|
318
335
|
raise Interrupt
|
|
@@ -359,6 +376,12 @@ module Sktop
|
|
|
359
376
|
end
|
|
360
377
|
|
|
361
378
|
def handle_delete_action
|
|
379
|
+
# Handle queue_jobs view separately
|
|
380
|
+
if @display.current_view == :queue_jobs
|
|
381
|
+
handle_delete_queue_job_action
|
|
382
|
+
return
|
|
383
|
+
end
|
|
384
|
+
|
|
362
385
|
return unless [:retries, :dead].include?(@display.current_view)
|
|
363
386
|
|
|
364
387
|
data = @data_mutex.synchronize { @cached_data }
|
|
@@ -497,5 +520,104 @@ module Sktop
|
|
|
497
520
|
end
|
|
498
521
|
end
|
|
499
522
|
|
|
523
|
+
def handle_enter_action
|
|
524
|
+
return unless @display.current_view == :queues
|
|
525
|
+
|
|
526
|
+
data = @data_mutex.synchronize { @cached_data }
|
|
527
|
+
unless data
|
|
528
|
+
@display.set_status("No data available")
|
|
529
|
+
return
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
queues = data[:queues]
|
|
533
|
+
selected_idx = @display.selected_index
|
|
534
|
+
|
|
535
|
+
if queues.empty?
|
|
536
|
+
@display.set_status("No queues")
|
|
537
|
+
return
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
if selected_idx >= queues.length
|
|
541
|
+
@display.set_status("Invalid selection")
|
|
542
|
+
return
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
queue = queues[selected_idx]
|
|
546
|
+
queue_name = queue[:name]
|
|
547
|
+
|
|
548
|
+
# Fetch jobs from the queue
|
|
549
|
+
begin
|
|
550
|
+
collector = StatsCollector.new
|
|
551
|
+
jobs = collector.queue_jobs(queue_name, limit: 500)
|
|
552
|
+
|
|
553
|
+
# Update cached data with queue jobs
|
|
554
|
+
@data_mutex.synchronize do
|
|
555
|
+
@cached_data[:queue_jobs] = jobs
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
@display.selected_queue = queue_name
|
|
559
|
+
@display.current_view = :queue_jobs
|
|
560
|
+
@display.set_status("Loaded #{jobs.length} jobs from #{queue_name}")
|
|
561
|
+
rescue => e
|
|
562
|
+
@display.set_status("Error loading queue: #{e.message}")
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def handle_escape_action
|
|
567
|
+
if @display.current_view == :queue_jobs
|
|
568
|
+
# Go back to queues view
|
|
569
|
+
@display.current_view = :queues
|
|
570
|
+
@display.selected_queue = nil
|
|
571
|
+
else
|
|
572
|
+
# Go to main view
|
|
573
|
+
@display.current_view = :main
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def handle_delete_queue_job_action
|
|
578
|
+
return unless @display.current_view == :queue_jobs
|
|
579
|
+
|
|
580
|
+
data = @data_mutex.synchronize { @cached_data }
|
|
581
|
+
unless data
|
|
582
|
+
@display.set_status("No data available")
|
|
583
|
+
return
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
jobs = data[:queue_jobs] || []
|
|
587
|
+
selected_idx = @display.selected_index
|
|
588
|
+
queue_name = @display.selected_queue
|
|
589
|
+
|
|
590
|
+
if jobs.empty?
|
|
591
|
+
@display.set_status("No jobs to delete")
|
|
592
|
+
return
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
if selected_idx >= jobs.length
|
|
596
|
+
@display.set_status("Invalid selection")
|
|
597
|
+
return
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
job = jobs[selected_idx]
|
|
601
|
+
unless job[:jid]
|
|
602
|
+
@display.set_status("Job has no JID")
|
|
603
|
+
return
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
begin
|
|
607
|
+
Sktop::JobActions.delete_queue_job(queue_name, job[:jid])
|
|
608
|
+
@display.set_status("Deleted #{job[:class]}")
|
|
609
|
+
|
|
610
|
+
# Refresh the queue jobs
|
|
611
|
+
collector = StatsCollector.new
|
|
612
|
+
new_jobs = collector.queue_jobs(queue_name, limit: 500)
|
|
613
|
+
@data_mutex.synchronize do
|
|
614
|
+
@cached_data[:queue_jobs] = new_jobs
|
|
615
|
+
end
|
|
616
|
+
@rendered_version = -1
|
|
617
|
+
rescue => e
|
|
618
|
+
@display.set_status("Error: #{e.message}")
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
|
|
500
622
|
end
|
|
501
623
|
end
|
data/lib/sktop/display.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Sktop
|
|
4
4
|
class Display
|
|
5
|
-
attr_accessor :current_view
|
|
5
|
+
attr_accessor :current_view, :connection_status, :last_update
|
|
6
6
|
|
|
7
7
|
def initialize
|
|
8
8
|
@pastel = Pastel.new
|
|
@@ -13,8 +13,13 @@ module Sktop
|
|
|
13
13
|
@selected_index = Hash.new(0) # Track selected row per view
|
|
14
14
|
@status_message = nil
|
|
15
15
|
@status_time = nil
|
|
16
|
+
@connection_status = :connecting # :connecting, :connected, :updating, :error
|
|
17
|
+
@last_update = nil
|
|
18
|
+
@selected_queue = nil # Track which queue is being viewed in queue_jobs view
|
|
16
19
|
end
|
|
17
20
|
|
|
21
|
+
attr_accessor :selected_queue
|
|
22
|
+
|
|
18
23
|
def scroll_up
|
|
19
24
|
@scroll_offsets[@current_view] = [@scroll_offsets[@current_view] - 1, 0].max
|
|
20
25
|
end
|
|
@@ -40,7 +45,7 @@ module Sktop
|
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
def selectable_view?
|
|
43
|
-
[:processes, :retries, :dead].include?(@current_view)
|
|
48
|
+
[:queues, :queue_jobs, :processes, :retries, :dead].include?(@current_view)
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
def page_up(page_size = nil)
|
|
@@ -84,6 +89,18 @@ module Sktop
|
|
|
84
89
|
# Don't reset scroll when switching views - preserve position
|
|
85
90
|
end
|
|
86
91
|
|
|
92
|
+
VIEW_ORDER = [:main, :queues, :processes, :workers, :retries, :scheduled, :dead].freeze
|
|
93
|
+
|
|
94
|
+
def next_view
|
|
95
|
+
current_idx = VIEW_ORDER.index(@current_view) || 0
|
|
96
|
+
@current_view = VIEW_ORDER[(current_idx + 1) % VIEW_ORDER.length]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def previous_view
|
|
100
|
+
current_idx = VIEW_ORDER.index(@current_view) || 0
|
|
101
|
+
@current_view = VIEW_ORDER[(current_idx - 1) % VIEW_ORDER.length]
|
|
102
|
+
end
|
|
103
|
+
|
|
87
104
|
def reset_cursor
|
|
88
105
|
print @cursor.move_to(0, 0)
|
|
89
106
|
print @cursor.hide
|
|
@@ -114,11 +131,24 @@ module Sktop
|
|
|
114
131
|
end
|
|
115
132
|
|
|
116
133
|
def render_refresh_from_cache(data)
|
|
134
|
+
@connection_status = :connected
|
|
135
|
+
@last_update = Time.now
|
|
117
136
|
cached = CachedData.new(data)
|
|
118
137
|
content = build_output(cached)
|
|
119
138
|
render_with_overwrite(content)
|
|
120
139
|
end
|
|
121
140
|
|
|
141
|
+
def render_loading
|
|
142
|
+
lines = []
|
|
143
|
+
lines << header_bar
|
|
144
|
+
lines << ""
|
|
145
|
+
lines << @pastel.cyan(" Connecting to Redis...")
|
|
146
|
+
lines << ""
|
|
147
|
+
lines << @pastel.dim(" Waiting for data...")
|
|
148
|
+
lines << :footer
|
|
149
|
+
render_with_overwrite(lines)
|
|
150
|
+
end
|
|
151
|
+
|
|
122
152
|
# Simple wrapper to make cached hash act like collector
|
|
123
153
|
class CachedData
|
|
124
154
|
def initialize(data)
|
|
@@ -152,6 +182,10 @@ module Sktop
|
|
|
152
182
|
def dead_jobs(limit: 50)
|
|
153
183
|
@data[:dead_jobs]&.first(limit) || []
|
|
154
184
|
end
|
|
185
|
+
|
|
186
|
+
def queue_jobs_cache
|
|
187
|
+
@data[:queue_jobs] || []
|
|
188
|
+
end
|
|
155
189
|
end
|
|
156
190
|
|
|
157
191
|
private
|
|
@@ -160,6 +194,8 @@ module Sktop
|
|
|
160
194
|
case @current_view
|
|
161
195
|
when :queues
|
|
162
196
|
build_queues_detail(collector)
|
|
197
|
+
when :queue_jobs
|
|
198
|
+
build_queue_jobs_detail(collector)
|
|
163
199
|
when :processes
|
|
164
200
|
build_processes_detail(collector)
|
|
165
201
|
when :workers
|
|
@@ -230,7 +266,18 @@ module Sktop
|
|
|
230
266
|
lines << ""
|
|
231
267
|
# Calculate available rows: height - header(1) - blank(1) - section(1) - table_header(1) - footer(1) = height - 5
|
|
232
268
|
max_rows = terminal_height - 5
|
|
233
|
-
|
|
269
|
+
queues_selectable(collector.queues, max_rows).each_line(chomp: true) { |l| lines << l }
|
|
270
|
+
lines << :footer
|
|
271
|
+
lines
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def build_queue_jobs_detail(collector)
|
|
275
|
+
lines = []
|
|
276
|
+
lines << header_bar
|
|
277
|
+
lines << ""
|
|
278
|
+
max_rows = terminal_height - 5
|
|
279
|
+
jobs = collector.respond_to?(:queue_jobs_cache) ? collector.queue_jobs_cache : []
|
|
280
|
+
queue_jobs_selectable(jobs, max_rows).each_line(chomp: true) { |l| lines << l }
|
|
234
281
|
lines << :footer
|
|
235
282
|
lines
|
|
236
283
|
end
|
|
@@ -370,15 +417,47 @@ module Sktop
|
|
|
370
417
|
timestamp = Time.now.strftime("%H:%M:%S")
|
|
371
418
|
title = "sktop"
|
|
372
419
|
|
|
420
|
+
# Connection status indicator
|
|
421
|
+
status_text = case @connection_status
|
|
422
|
+
when :connecting
|
|
423
|
+
@pastel.yellow.on_blue(" ● Connecting ")
|
|
424
|
+
when :updating
|
|
425
|
+
@pastel.cyan.on_blue(" ↻ Updating ")
|
|
426
|
+
when :error
|
|
427
|
+
@pastel.red.on_blue(" ✗ Error ")
|
|
428
|
+
else # :connected
|
|
429
|
+
if @last_update
|
|
430
|
+
age = Time.now - @last_update
|
|
431
|
+
if age < 5
|
|
432
|
+
@pastel.green.on_blue(" ● Connected ")
|
|
433
|
+
else
|
|
434
|
+
@pastel.green.on_blue(" ● #{format_update_age(age)} ago ")
|
|
435
|
+
end
|
|
436
|
+
else
|
|
437
|
+
@pastel.green.on_blue(" ● Connected ")
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
373
441
|
left = @pastel.white.on_blue.bold(" #{title} ")
|
|
374
442
|
right = @pastel.white.on_blue.bold(" #{timestamp} ")
|
|
375
443
|
|
|
376
444
|
left_len = visible_string_length(left)
|
|
445
|
+
status_len = visible_string_length(status_text)
|
|
377
446
|
right_len = visible_string_length(right)
|
|
378
|
-
middle_width = width - left_len - right_len
|
|
379
|
-
middle = @pastel.on_blue(" " * middle_width)
|
|
447
|
+
middle_width = width - left_len - status_len - right_len
|
|
448
|
+
middle = @pastel.on_blue(" " * [middle_width, 0].max)
|
|
380
449
|
|
|
381
|
-
left + middle + right
|
|
450
|
+
left + status_text + middle + right
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def format_update_age(seconds)
|
|
454
|
+
if seconds < 60
|
|
455
|
+
"#{seconds.round}s"
|
|
456
|
+
elsif seconds < 3600
|
|
457
|
+
"#{(seconds / 60).round}m"
|
|
458
|
+
else
|
|
459
|
+
"#{(seconds / 3600).round}h"
|
|
460
|
+
end
|
|
382
461
|
end
|
|
383
462
|
|
|
384
463
|
def section_bar(title)
|
|
@@ -554,6 +633,133 @@ module Sktop
|
|
|
554
633
|
lines.join("\n")
|
|
555
634
|
end
|
|
556
635
|
|
|
636
|
+
# Selectable queues view - press Enter to view queue contents
|
|
637
|
+
def queues_selectable(queues, max_rows)
|
|
638
|
+
width = terminal_width
|
|
639
|
+
lines = []
|
|
640
|
+
|
|
641
|
+
scroll_offset = @scroll_offsets[@current_view]
|
|
642
|
+
data_rows = max_rows - 3 # Account for section bar, header, and status line
|
|
643
|
+
max_scroll = [queues.length - data_rows, 0].max
|
|
644
|
+
scroll_offset = [[scroll_offset, 0].max, max_scroll].min
|
|
645
|
+
@scroll_offsets[@current_view] = scroll_offset
|
|
646
|
+
|
|
647
|
+
# Clamp selected index
|
|
648
|
+
@selected_index[@current_view] = [[@selected_index[@current_view], 0].max, [queues.length - 1, 0].max].min
|
|
649
|
+
|
|
650
|
+
# Auto-scroll to keep selection visible
|
|
651
|
+
selected = @selected_index[@current_view]
|
|
652
|
+
if selected < scroll_offset
|
|
653
|
+
scroll_offset = selected
|
|
654
|
+
@scroll_offsets[@current_view] = scroll_offset
|
|
655
|
+
elsif selected >= scroll_offset + data_rows
|
|
656
|
+
scroll_offset = selected - data_rows + 1
|
|
657
|
+
@scroll_offsets[@current_view] = scroll_offset
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
scroll_indicator = queues.length > data_rows ? " [#{scroll_offset + 1}-#{[scroll_offset + data_rows, queues.length].min}/#{queues.length}]" : ""
|
|
661
|
+
lines << section_bar("Queues#{scroll_indicator} - ↑↓ select, Enter=view jobs, m=main")
|
|
662
|
+
|
|
663
|
+
if queues.empty?
|
|
664
|
+
lines << @pastel.dim(" No queues")
|
|
665
|
+
return lines.join("\n")
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
name_width = [40, width - 40].max
|
|
669
|
+
header = sprintf(" %-#{name_width}s %10s %10s %10s", "NAME", "SIZE", "LATENCY", "STATUS")
|
|
670
|
+
lines << format_table_header(header)
|
|
671
|
+
|
|
672
|
+
queues.drop(scroll_offset).first(data_rows).each_with_index do |queue, idx|
|
|
673
|
+
actual_idx = scroll_offset + idx
|
|
674
|
+
row = format_queue_row(queue, name_width)
|
|
675
|
+
|
|
676
|
+
if actual_idx == selected
|
|
677
|
+
lines << @pastel.black.on_white(row + " " * [width - visible_string_length(row), 0].max)
|
|
678
|
+
else
|
|
679
|
+
lines << row
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
remaining = queues.length - scroll_offset - data_rows
|
|
684
|
+
if remaining > 0
|
|
685
|
+
lines << @pastel.dim(" ↓ #{remaining} more")
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# Status message
|
|
689
|
+
if @status_message && @status_time && (Time.now - @status_time) < 3
|
|
690
|
+
lines << @pastel.green(" #{@status_message}")
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
lines.join("\n")
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Selectable queue jobs view - press Ctrl+X to delete job
|
|
697
|
+
def queue_jobs_selectable(jobs, max_rows)
|
|
698
|
+
width = terminal_width
|
|
699
|
+
lines = []
|
|
700
|
+
|
|
701
|
+
scroll_offset = @scroll_offsets[@current_view]
|
|
702
|
+
data_rows = max_rows - 3 # Account for section bar, header, and status line
|
|
703
|
+
max_scroll = [jobs.length - data_rows, 0].max
|
|
704
|
+
scroll_offset = [[scroll_offset, 0].max, max_scroll].min
|
|
705
|
+
@scroll_offsets[@current_view] = scroll_offset
|
|
706
|
+
|
|
707
|
+
# Clamp selected index
|
|
708
|
+
@selected_index[@current_view] = [[@selected_index[@current_view], 0].max, [jobs.length - 1, 0].max].min
|
|
709
|
+
|
|
710
|
+
# Auto-scroll to keep selection visible
|
|
711
|
+
selected = @selected_index[@current_view]
|
|
712
|
+
if selected < scroll_offset
|
|
713
|
+
scroll_offset = selected
|
|
714
|
+
@scroll_offsets[@current_view] = scroll_offset
|
|
715
|
+
elsif selected >= scroll_offset + data_rows
|
|
716
|
+
scroll_offset = selected - data_rows + 1
|
|
717
|
+
@scroll_offsets[@current_view] = scroll_offset
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
queue_name = @selected_queue || "unknown"
|
|
721
|
+
scroll_indicator = jobs.length > data_rows ? " [#{scroll_offset + 1}-#{[scroll_offset + data_rows, jobs.length].min}/#{jobs.length}]" : ""
|
|
722
|
+
lines << section_bar("Queue: #{queue_name}#{scroll_indicator} - ↑↓ select, ^X=delete, Esc=back")
|
|
723
|
+
|
|
724
|
+
if jobs.empty?
|
|
725
|
+
lines << @pastel.dim(" No jobs in queue")
|
|
726
|
+
return lines.join("\n")
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
job_width = [35, (width - 50) / 2].max
|
|
730
|
+
args_width = [35, (width - 50) / 2].max
|
|
731
|
+
|
|
732
|
+
header = sprintf(" %-#{job_width}s %-20s %-#{args_width}s", "JOB", "ENQUEUED", "ARGS")
|
|
733
|
+
lines << format_table_header(header)
|
|
734
|
+
|
|
735
|
+
jobs.drop(scroll_offset).first(data_rows).each_with_index do |job, idx|
|
|
736
|
+
actual_idx = scroll_offset + idx
|
|
737
|
+
klass = truncate(job[:class].to_s, job_width)
|
|
738
|
+
enqueued = job[:enqueued_at]&.strftime("%Y-%m-%d %H:%M:%S") || "N/A"
|
|
739
|
+
args = truncate(job[:args].inspect, args_width)
|
|
740
|
+
|
|
741
|
+
row = sprintf(" %-#{job_width}s %-20s %-#{args_width}s", klass, enqueued, args)
|
|
742
|
+
|
|
743
|
+
if actual_idx == selected
|
|
744
|
+
lines << @pastel.black.on_white(row + " " * [width - visible_string_length(row), 0].max)
|
|
745
|
+
else
|
|
746
|
+
lines << row
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
remaining = jobs.length - scroll_offset - data_rows
|
|
751
|
+
if remaining > 0
|
|
752
|
+
lines << @pastel.dim(" ↓ #{remaining} more")
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# Status message
|
|
756
|
+
if @status_message && @status_time && (Time.now - @status_time) < 3
|
|
757
|
+
lines << @pastel.green(" #{@status_message}")
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
lines.join("\n")
|
|
761
|
+
end
|
|
762
|
+
|
|
557
763
|
def format_queue_row(queue, name_width)
|
|
558
764
|
name = truncate(queue[:name], name_width)
|
|
559
765
|
size = format_number(queue[:size])
|
|
@@ -1036,31 +1242,36 @@ module Sktop
|
|
|
1036
1242
|
end
|
|
1037
1243
|
|
|
1038
1244
|
def function_bar
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
end
|
|
1245
|
+
# Map keys to views for highlighting
|
|
1246
|
+
view_keys = {
|
|
1247
|
+
"m" => :main,
|
|
1248
|
+
"q" => :queues,
|
|
1249
|
+
"p" => :processes,
|
|
1250
|
+
"w" => :workers,
|
|
1251
|
+
"r" => :retries,
|
|
1252
|
+
"s" => :scheduled,
|
|
1253
|
+
"d" => :dead
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
items = [
|
|
1257
|
+
["m", "Main"],
|
|
1258
|
+
["q", "Queues"],
|
|
1259
|
+
["p", "Procs"],
|
|
1260
|
+
["w", "Workers"],
|
|
1261
|
+
["r", "Retries"],
|
|
1262
|
+
["s", "Sched"],
|
|
1263
|
+
["d", "Dead"],
|
|
1264
|
+
["^C", "Quit"]
|
|
1265
|
+
]
|
|
1061
1266
|
|
|
1062
1267
|
bar = items.map do |key, label|
|
|
1063
|
-
|
|
1268
|
+
view = view_keys[key]
|
|
1269
|
+
if view && view == @current_view
|
|
1270
|
+
# Highlight current view with inverted colors
|
|
1271
|
+
@pastel.black.on_white.bold(key) + @pastel.black.on_green.bold(label)
|
|
1272
|
+
else
|
|
1273
|
+
@pastel.black.on_cyan.bold(key) + @pastel.white.on_blue(label)
|
|
1274
|
+
end
|
|
1064
1275
|
end.join(" ")
|
|
1065
1276
|
|
|
1066
1277
|
width = terminal_width
|
data/lib/sktop/job_actions.rb
CHANGED
|
@@ -41,6 +41,15 @@ module Sktop
|
|
|
41
41
|
count
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
def delete_queue_job(queue_name, jid)
|
|
45
|
+
queue = Sidekiq::Queue.new(queue_name)
|
|
46
|
+
job = queue.find { |j| j.jid == jid }
|
|
47
|
+
raise "Job not found in queue #{queue_name} (JID: #{jid})" unless job
|
|
48
|
+
|
|
49
|
+
job.delete
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
44
53
|
def quiet_process(identity)
|
|
45
54
|
process = find_process(identity)
|
|
46
55
|
raise "Process not found (identity: #{identity})" unless process
|
|
@@ -63,15 +63,61 @@ module Sktop
|
|
|
63
63
|
|
|
64
64
|
def workers
|
|
65
65
|
Sidekiq::Workers.new.map do |process_id, thread_id, work|
|
|
66
|
+
extract_worker_info(process_id, thread_id, work)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def extract_worker_info(process_id, thread_id, work)
|
|
71
|
+
# Sidekiq 7+ returns Work objects, older versions return hashes
|
|
72
|
+
if work.is_a?(Hash)
|
|
73
|
+
# Older Sidekiq - work is a hash with string keys
|
|
74
|
+
payload = work["payload"]
|
|
66
75
|
{
|
|
67
76
|
process_id: process_id,
|
|
68
77
|
thread_id: thread_id,
|
|
69
78
|
queue: work["queue"],
|
|
70
|
-
class:
|
|
71
|
-
args:
|
|
79
|
+
class: payload["class"],
|
|
80
|
+
args: payload["args"] || [],
|
|
72
81
|
run_at: Time.at(work["run_at"]),
|
|
73
82
|
elapsed: Time.now - Time.at(work["run_at"])
|
|
74
83
|
}
|
|
84
|
+
elsif work.respond_to?(:job)
|
|
85
|
+
# Sidekiq 7+ Work object with job accessor
|
|
86
|
+
job = work.job
|
|
87
|
+
{
|
|
88
|
+
process_id: process_id,
|
|
89
|
+
thread_id: thread_id,
|
|
90
|
+
queue: work.queue,
|
|
91
|
+
class: job["class"],
|
|
92
|
+
args: job["args"] || [],
|
|
93
|
+
run_at: Time.at(work.run_at),
|
|
94
|
+
elapsed: Time.now - Time.at(work.run_at)
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
# Fallback for other versions - try payload
|
|
98
|
+
payload = work.payload
|
|
99
|
+
{
|
|
100
|
+
process_id: process_id,
|
|
101
|
+
thread_id: thread_id,
|
|
102
|
+
queue: work.queue,
|
|
103
|
+
class: payload["class"],
|
|
104
|
+
args: payload["args"] || [],
|
|
105
|
+
run_at: Time.at(work.run_at),
|
|
106
|
+
elapsed: Time.now - Time.at(work.run_at)
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def queue_jobs(queue_name, limit: 100)
|
|
112
|
+
queue = Sidekiq::Queue.new(queue_name)
|
|
113
|
+
queue.first(limit).map do |job|
|
|
114
|
+
{
|
|
115
|
+
jid: job.jid,
|
|
116
|
+
class: job.klass,
|
|
117
|
+
args: job.args,
|
|
118
|
+
enqueued_at: job.enqueued_at,
|
|
119
|
+
created_at: job.created_at
|
|
120
|
+
}
|
|
75
121
|
end
|
|
76
122
|
end
|
|
77
123
|
|
data/lib/sktop/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sktop
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- James
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01-
|
|
11
|
+
date: 2026-01-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: sidekiq
|