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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e8dee8bdcd3f097ef51306b502c32a52bcdeafbc9530f3bb772384a7313fe59
4
- data.tar.gz: c8d92835a338e49c2018e3bb355cd7fda64632ba26b5425c15d1544e06938707
3
+ metadata.gz: 72e27a287d42246bf03393f3f5d8fd83fcf2dcb52d3c32a242f6631a556d5a90
4
+ data.tar.gz: a26fa56421cf3fa038f2d7fa7ec86e204189be4bd21419d1ddf896631f59a8cc
5
5
  SHA512:
6
- metadata.gz: 1cb75f32423642511c8f026fe5ba681771b97db3fcd6aa07dbad769f0b9b95ee095551c02e5691115b4b1e6a468ca955e2f2fe9f5e52da52eee08caeb614ebf4
7
- data.tar.gz: 420e5fa5c70237c4c39c92701bd43077a9672cdfe8b869c66f5e567860f03f9cc2185b9dad2d9a34174075b3ab85bbb803ec41fe62ca24d15e47cd958d18e3de
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
- # Ignore fetch errors, will retry next interval
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
- # Wait for initial data (with timeout)
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
- return unless data
262
-
263
- @display.render_refresh_from_cache(data)
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 (unused for now)
300
- when "[D" # Left arrow (unused for now)
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
- @display.current_view = :main
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
- @display.current_view = :main
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
- queues_scrollable(collector.queues, max_rows).each_line(chomp: true) { |l| lines << l }
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
- items = if @current_view == :main
1040
- [
1041
- ["q", "Queues"],
1042
- ["p", "Procs"],
1043
- ["w", "Workers"],
1044
- ["r", "Retries"],
1045
- ["s", "Sched"],
1046
- ["d", "Dead"],
1047
- ["^C", "Quit"]
1048
- ]
1049
- else
1050
- [
1051
- ["m", "Main"],
1052
- ["q", "Queues"],
1053
- ["p", "Procs"],
1054
- ["w", "Workers"],
1055
- ["r", "Retries"],
1056
- ["s", "Sched"],
1057
- ["d", "Dead"],
1058
- ["^C", "Quit"]
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
- @pastel.black.on_cyan.bold(key) + @pastel.white.on_blue(label)
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
@@ -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: work["payload"]["class"],
71
- args: work["payload"]["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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sktop
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.6"
5
5
  end
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
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-17 00:00:00.000000000 Z
11
+ date: 2026-01-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq