sktop 0.1.6 → 0.2.0

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: 72e27a287d42246bf03393f3f5d8fd83fcf2dcb52d3c32a242f6631a556d5a90
4
- data.tar.gz: a26fa56421cf3fa038f2d7fa7ec86e204189be4bd21419d1ddf896631f59a8cc
3
+ metadata.gz: d1e0891fc2a69763f5784c7cc96fc69103d14362aa67a08b82b6c13aa578985b
4
+ data.tar.gz: fea7b7284c37d666516d42c2bcc97e0f6b3db4cbb53628e2e3b35c6521c44845
5
5
  SHA512:
6
- metadata.gz: d7aa57eac65085688adadc80a4d587c6c3e0c100c09518675bf4cc65fdcfac93e37e404a7476eda3a4b4ec2d2b7a595f3e6231a2b1c414475c4907fd52ff4275
7
- data.tar.gz: ef09ee20ae517a01e85c427bc291d8adff140e0239342336f22365df774efdf83c0df76b34839ed99e15d63a66c36de4eaeb4f7634a24d098c27fab6894055ec
6
+ metadata.gz: 59b4378e2dae18d69bcbd9c3a6492ff0bcad4d830ad9d0c893fa0b9f70f6845d87b6652dcc850d7a02efaea10c00343203d8b495c8eded6318a9b6c648e1fb5e
7
+ data.tar.gz: eb54c39df9aa1ec70bcb719b11a7ab0aac88970586345793e890b67dda9f753f207e8f7e5ea0923153118b66c59952f60ff46309e28da281e2bc4f87cbd48c4a
data/lib/sktop/cli.rb CHANGED
@@ -7,7 +7,18 @@ require "io/console"
7
7
  require "io/wait"
8
8
 
9
9
  module Sktop
10
+ # Command-line interface for the Sktop Sidekiq monitor.
11
+ # Handles argument parsing, Redis configuration, and the main event loop.
12
+ #
13
+ # @example Basic usage
14
+ # Sktop::CLI.new.run
15
+ #
16
+ # @example With custom arguments
17
+ # Sktop::CLI.new(["-r", "redis://myhost:6379/1", "-q"]).run
10
18
  class CLI
19
+ # Create a new CLI instance.
20
+ #
21
+ # @param args [Array<String>] command-line arguments (defaults to ARGV)
11
22
  def initialize(args = ARGV)
12
23
  @args = args
13
24
  @options = {
@@ -20,6 +31,11 @@ module Sktop
20
31
  @running = true
21
32
  end
22
33
 
34
+ # Run the CLI application.
35
+ # Parses options, configures Redis, and starts the TUI.
36
+ #
37
+ # @return [void]
38
+ # @raise [SystemExit] on connection errors or interrupts
23
39
  def run
24
40
  parse_options!
25
41
  configure_redis
@@ -39,6 +55,10 @@ module Sktop
39
55
  exit 1
40
56
  end
41
57
 
58
+ # Gracefully shutdown the application.
59
+ # Restores terminal state and shows the cursor.
60
+ #
61
+ # @return [void]
42
62
  def shutdown
43
63
  @running = false
44
64
  print "\e[?25h" # Show cursor
@@ -48,6 +68,9 @@ module Sktop
48
68
 
49
69
  private
50
70
 
71
+ # Parse command-line options and populate @options hash.
72
+ # @return [void]
73
+ # @api private
51
74
  def parse_options!
52
75
  parser = OptionParser.new do |opts|
53
76
  opts.banner = "Usage: sktop [options]"
@@ -117,6 +140,9 @@ module Sktop
117
140
  parser.parse!(@args)
118
141
  end
119
142
 
143
+ # Configure Sidekiq's Redis connection from CLI options.
144
+ # @return [void]
145
+ # @api private
120
146
  def configure_redis
121
147
  Sidekiq.configure_client do |config|
122
148
  if @options[:namespace]
@@ -130,6 +156,10 @@ module Sktop
130
156
  end
131
157
  end
132
158
 
159
+ # Start the main TUI loop with background data fetching.
160
+ # Handles both one-shot mode and interactive auto-refresh mode.
161
+ # @return [void]
162
+ # @api private
133
163
  def start_watcher
134
164
  collector = StatsCollector.new
135
165
  @display = Display.new
@@ -267,6 +297,10 @@ module Sktop
267
297
  end
268
298
  end
269
299
 
300
+ # Render the display from the cached data snapshot.
301
+ # Shows loading screen if no data is available yet.
302
+ # @return [void]
303
+ # @api private
270
304
  def render_cached_data
271
305
  data = @data_mutex.synchronize { @cached_data }
272
306
  if data
@@ -276,6 +310,13 @@ module Sktop
276
310
  end
277
311
  end
278
312
 
313
+ # Handle a keyboard input event.
314
+ # Routes to appropriate view or action handler.
315
+ #
316
+ # @param key [String] the key character pressed
317
+ # @param stdin [IO] the stdin IO object for reading escape sequences
318
+ # @return [void]
319
+ # @api private
279
320
  def handle_keypress(key, stdin)
280
321
  case key
281
322
  when 'q', 'Q'
@@ -336,6 +377,10 @@ module Sktop
336
377
  end
337
378
  end
338
379
 
380
+ # Handle Ctrl+R to retry the selected job.
381
+ # Only works in retries and dead views.
382
+ # @return [void]
383
+ # @api private
339
384
  def handle_retry_action
340
385
  return unless [:retries, :dead].include?(@display.current_view)
341
386
 
@@ -375,6 +420,10 @@ module Sktop
375
420
  end
376
421
  end
377
422
 
423
+ # Handle Ctrl+X to delete the selected job.
424
+ # Works in retries, dead, and queue_jobs views.
425
+ # @return [void]
426
+ # @api private
378
427
  def handle_delete_action
379
428
  # Handle queue_jobs view separately
380
429
  if @display.current_view == :queue_jobs
@@ -420,6 +469,10 @@ module Sktop
420
469
  end
421
470
  end
422
471
 
472
+ # Handle Alt+R to retry all jobs in the current view.
473
+ # Only works in retries and dead views.
474
+ # @return [void]
475
+ # @api private
423
476
  def handle_retry_all_action
424
477
  return unless [:retries, :dead].include?(@display.current_view)
425
478
 
@@ -433,6 +486,10 @@ module Sktop
433
486
  end
434
487
  end
435
488
 
489
+ # Handle Alt+X to delete all jobs in the current view.
490
+ # Only works in retries and dead views.
491
+ # @return [void]
492
+ # @api private
436
493
  def handle_delete_all_action
437
494
  return unless [:retries, :dead].include?(@display.current_view)
438
495
 
@@ -446,6 +503,10 @@ module Sktop
446
503
  end
447
504
  end
448
505
 
506
+ # Handle Ctrl+Q to quiet the selected Sidekiq process.
507
+ # Only works in processes view.
508
+ # @return [void]
509
+ # @api private
449
510
  def handle_quiet_process_action
450
511
  return unless @display.current_view == :processes
451
512
 
@@ -483,6 +544,10 @@ module Sktop
483
544
  end
484
545
  end
485
546
 
547
+ # Handle Ctrl+K to stop/kill the selected Sidekiq process.
548
+ # Only works in processes view.
549
+ # @return [void]
550
+ # @api private
486
551
  def handle_stop_process_action
487
552
  return unless @display.current_view == :processes
488
553
 
@@ -520,6 +585,10 @@ module Sktop
520
585
  end
521
586
  end
522
587
 
588
+ # Handle Enter key to view jobs in the selected queue.
589
+ # Only works in queues view.
590
+ # @return [void]
591
+ # @api private
523
592
  def handle_enter_action
524
593
  return unless @display.current_view == :queues
525
594
 
@@ -563,6 +632,10 @@ module Sktop
563
632
  end
564
633
  end
565
634
 
635
+ # Handle Escape key to go back or return to main view.
636
+ # From queue_jobs returns to queues, otherwise returns to main.
637
+ # @return [void]
638
+ # @api private
566
639
  def handle_escape_action
567
640
  if @display.current_view == :queue_jobs
568
641
  # Go back to queues view
@@ -574,6 +647,10 @@ module Sktop
574
647
  end
575
648
  end
576
649
 
650
+ # Handle Ctrl+X to delete a job from the current queue.
651
+ # Only works in queue_jobs view.
652
+ # @return [void]
653
+ # @api private
577
654
  def handle_delete_queue_job_action
578
655
  return unless @display.current_view == :queue_jobs
579
656
 
data/lib/sktop/display.rb CHANGED
@@ -1,9 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sktop
4
+ # Handles all terminal display rendering for the Sktop TUI.
5
+ # Manages views, scrolling, selection, and ANSI-colored output.
6
+ #
7
+ # @example Basic usage
8
+ # display = Sktop::Display.new
9
+ # display.current_view = :queues
10
+ # display.render(collector)
11
+ #
12
+ # @example With cached data for auto-refresh
13
+ # display = Sktop::Display.new
14
+ # display.render_refresh_from_cache(data_hash)
4
15
  class Display
16
+ # @return [Symbol] the current view (:main, :queues, :processes, :workers, :retries, :scheduled, :dead, :queue_jobs)
17
+ # @return [Symbol] the connection status (:connecting, :connected, :updating, :error)
18
+ # @return [Time, nil] the timestamp of the last successful data update
5
19
  attr_accessor :current_view, :connection_status, :last_update
6
20
 
21
+ # Create a new Display instance with default settings.
7
22
  def initialize
8
23
  @pastel = Pastel.new
9
24
  @cursor = TTY::Cursor
@@ -18,16 +33,23 @@ module Sktop
18
33
  @selected_queue = nil # Track which queue is being viewed in queue_jobs view
19
34
  end
20
35
 
36
+ # @return [String, nil] the name of the currently selected queue (for queue_jobs view)
21
37
  attr_accessor :selected_queue
22
38
 
39
+ # Scroll the current view up by one line.
40
+ # @return [Integer] the new scroll offset
23
41
  def scroll_up
24
42
  @scroll_offsets[@current_view] = [@scroll_offsets[@current_view] - 1, 0].max
25
43
  end
26
44
 
45
+ # Scroll the current view down by one line.
46
+ # @return [Integer] the new scroll offset
27
47
  def scroll_down
28
48
  @scroll_offsets[@current_view] += 1
29
49
  end
30
50
 
51
+ # Move selection up in selectable views, or scroll up in non-selectable views.
52
+ # @return [Integer] the new selected index or scroll offset
31
53
  def select_up
32
54
  if selectable_view?
33
55
  @selected_index[@current_view] = [@selected_index[@current_view] - 1, 0].max
@@ -36,6 +58,8 @@ module Sktop
36
58
  end
37
59
  end
38
60
 
61
+ # Move selection down in selectable views, or scroll down in non-selectable views.
62
+ # @return [Integer] the new selected index or scroll offset
39
63
  def select_down
40
64
  if selectable_view?
41
65
  @selected_index[@current_view] += 1
@@ -44,10 +68,15 @@ module Sktop
44
68
  end
45
69
  end
46
70
 
71
+ # Check if the current view supports item selection.
72
+ # @return [Boolean] true if the view supports selection
47
73
  def selectable_view?
48
74
  [:queues, :queue_jobs, :processes, :retries, :dead].include?(@current_view)
49
75
  end
50
76
 
77
+ # Move up by one page in the current view.
78
+ # @param page_size [Integer, nil] custom page size (defaults to terminal height - 8)
79
+ # @return [Integer] the new selected index or scroll offset
51
80
  def page_up(page_size = nil)
52
81
  page_size ||= default_page_size
53
82
  if selectable_view?
@@ -57,6 +86,9 @@ module Sktop
57
86
  end
58
87
  end
59
88
 
89
+ # Move down by one page in the current view.
90
+ # @param page_size [Integer, nil] custom page size (defaults to terminal height - 8)
91
+ # @return [Integer] the new selected index or scroll offset
60
92
  def page_down(page_size = nil)
61
93
  page_size ||= default_page_size
62
94
  if selectable_view?
@@ -66,51 +98,77 @@ module Sktop
66
98
  end
67
99
  end
68
100
 
101
+ # Calculate the default page size based on terminal height.
102
+ # @return [Integer] the page size (minimum 5)
69
103
  def default_page_size
70
104
  # Use terminal height minus header/footer overhead as page size
71
105
  [terminal_height - 8, 5].max
72
106
  end
73
107
 
108
+ # Get the currently selected index for the current view.
109
+ # @return [Integer] the selected index (0-based)
74
110
  def selected_index
75
111
  @selected_index[@current_view]
76
112
  end
77
113
 
114
+ # Set a temporary status message to display.
115
+ # The message will be shown for approximately 3 seconds.
116
+ # @param message [String] the status message to display
117
+ # @return [void]
78
118
  def set_status(message)
79
119
  @status_message = message
80
120
  @status_time = Time.now
81
121
  end
82
122
 
123
+ # Reset the scroll offset for the current view to zero.
124
+ # @return [Integer] 0
83
125
  def reset_scroll
84
126
  @scroll_offsets[@current_view] = 0
85
127
  end
86
128
 
129
+ # Set the current view, preserving scroll position.
130
+ # @param view [Symbol] the view to switch to
131
+ # @return [Symbol] the new current view
87
132
  def current_view=(view)
88
133
  @current_view = view
89
134
  # Don't reset scroll when switching views - preserve position
90
135
  end
91
136
 
137
+ # Ordered list of views for cycling with arrow keys.
138
+ # @return [Array<Symbol>] the view order
92
139
  VIEW_ORDER = [:main, :queues, :processes, :workers, :retries, :scheduled, :dead].freeze
93
140
 
141
+ # Switch to the next view in the cycle.
142
+ # @return [Symbol] the new current view
94
143
  def next_view
95
144
  current_idx = VIEW_ORDER.index(@current_view) || 0
96
145
  @current_view = VIEW_ORDER[(current_idx + 1) % VIEW_ORDER.length]
97
146
  end
98
147
 
148
+ # Switch to the previous view in the cycle.
149
+ # @return [Symbol] the new current view
99
150
  def previous_view
100
151
  current_idx = VIEW_ORDER.index(@current_view) || 0
101
152
  @current_view = VIEW_ORDER[(current_idx - 1) % VIEW_ORDER.length]
102
153
  end
103
154
 
155
+ # Reset cursor to top-left and hide it.
156
+ # @return [void]
104
157
  def reset_cursor
105
158
  print @cursor.move_to(0, 0)
106
159
  print @cursor.hide
107
160
  $stdout.flush
108
161
  end
109
162
 
163
+ # Show the cursor (call on exit).
164
+ # @return [void]
110
165
  def show_cursor
111
166
  print @cursor.show
112
167
  end
113
168
 
169
+ # Update the cached terminal size from TTY::Screen.
170
+ # Call this before entering raw mode or when terminal resizes.
171
+ # @return [Array<Integer>, nil] [height, width] or nil if unchanged
114
172
  def update_terminal_size
115
173
  # Force refresh of terminal size using TTY::Screen
116
174
  height = TTY::Screen.height
@@ -120,16 +178,26 @@ module Sktop
120
178
  end
121
179
  end
122
180
 
181
+ # Render the current view as a string (for one-shot mode).
182
+ # @param collector [StatsCollector, CachedData] the data source
183
+ # @return [String] the rendered output
123
184
  def render(collector)
124
185
  content_parts = build_output(collector)
125
186
  content_parts.reject { |p| p == :footer }.map(&:to_s).join("\n")
126
187
  end
127
188
 
189
+ # Render the current view directly to the terminal with screen clearing.
190
+ # @param collector [StatsCollector] the data source
191
+ # @return [void]
128
192
  def render_refresh(collector)
129
193
  content = build_output(collector)
130
194
  render_with_overwrite(content)
131
195
  end
132
196
 
197
+ # Render the current view from cached data hash.
198
+ # Updates connection status and last update time.
199
+ # @param data [Hash] cached data hash with :overview, :queues, :processes, etc.
200
+ # @return [void]
133
201
  def render_refresh_from_cache(data)
134
202
  @connection_status = :connected
135
203
  @last_update = Time.now
@@ -138,6 +206,8 @@ module Sktop
138
206
  render_with_overwrite(content)
139
207
  end
140
208
 
209
+ # Render a loading screen while waiting for initial data.
210
+ # @return [void]
141
211
  def render_loading
142
212
  lines = []
143
213
  lines << header_bar
@@ -149,40 +219,58 @@ module Sktop
149
219
  render_with_overwrite(lines)
150
220
  end
151
221
 
152
- # Simple wrapper to make cached hash act like collector
222
+ # Wrapper class to make a cached data hash act like a StatsCollector.
223
+ # Provides the same interface methods that Display expects.
224
+ #
225
+ # @example
226
+ # cached = CachedData.new({ overview: {...}, queues: [...] })
227
+ # cached.overview # => {...}
153
228
  class CachedData
229
+ # Create a new CachedData wrapper.
230
+ # @param data [Hash] the raw data hash
154
231
  def initialize(data)
155
232
  @data = data
156
233
  end
157
234
 
235
+ # @return [Hash] overview statistics
158
236
  def overview
159
237
  @data[:overview]
160
238
  end
161
239
 
240
+ # @return [Array<Hash>] queue information
162
241
  def queues
163
242
  @data[:queues]
164
243
  end
165
244
 
245
+ # @return [Array<Hash>] process information
166
246
  def processes
167
247
  @data[:processes]
168
248
  end
169
249
 
250
+ # @return [Array<Hash>] worker information
170
251
  def workers
171
252
  @data[:workers]
172
253
  end
173
254
 
255
+ # @param limit [Integer] maximum jobs to return
256
+ # @return [Array<Hash>] retry job information
174
257
  def retry_jobs(limit: 50)
175
258
  @data[:retry_jobs]&.first(limit) || []
176
259
  end
177
260
 
261
+ # @param limit [Integer] maximum jobs to return
262
+ # @return [Array<Hash>] scheduled job information
178
263
  def scheduled_jobs(limit: 50)
179
264
  @data[:scheduled_jobs]&.first(limit) || []
180
265
  end
181
266
 
267
+ # @param limit [Integer] maximum jobs to return
268
+ # @return [Array<Hash>] dead job information
182
269
  def dead_jobs(limit: 50)
183
270
  @data[:dead_jobs]&.first(limit) || []
184
271
  end
185
272
 
273
+ # @return [Array<Hash>] cached queue jobs data
186
274
  def queue_jobs_cache
187
275
  @data[:queue_jobs] || []
188
276
  end
@@ -264,8 +352,10 @@ module Sktop
264
352
  lines = []
265
353
  lines << header_bar
266
354
  lines << ""
267
- # Calculate available rows: height - header(1) - blank(1) - section(1) - table_header(1) - footer(1) = height - 5
268
- max_rows = terminal_height - 5
355
+ stats_meters(collector.overview, collector.processes).each_line(chomp: true) { |l| lines << l }
356
+ lines << ""
357
+ # Calculate available rows: height - header(1) - blank(1) - stats(6) - blank(1) - section(1) - table_header(1) - footer(1) = height - 12
358
+ max_rows = terminal_height - 12
269
359
  queues_selectable(collector.queues, max_rows).each_line(chomp: true) { |l| lines << l }
270
360
  lines << :footer
271
361
  lines
@@ -275,7 +365,9 @@ module Sktop
275
365
  lines = []
276
366
  lines << header_bar
277
367
  lines << ""
278
- max_rows = terminal_height - 5
368
+ stats_meters(collector.overview, collector.processes).each_line(chomp: true) { |l| lines << l }
369
+ lines << ""
370
+ max_rows = terminal_height - 12
279
371
  jobs = collector.respond_to?(:queue_jobs_cache) ? collector.queue_jobs_cache : []
280
372
  queue_jobs_selectable(jobs, max_rows).each_line(chomp: true) { |l| lines << l }
281
373
  lines << :footer
@@ -286,7 +378,9 @@ module Sktop
286
378
  lines = []
287
379
  lines << header_bar
288
380
  lines << ""
289
- max_rows = terminal_height - 5
381
+ stats_meters(collector.overview, collector.processes).each_line(chomp: true) { |l| lines << l }
382
+ lines << ""
383
+ max_rows = terminal_height - 12
290
384
  processes_selectable(collector.processes, max_rows).each_line(chomp: true) { |l| lines << l }
291
385
  lines << :footer
292
386
  lines
@@ -296,7 +390,9 @@ module Sktop
296
390
  lines = []
297
391
  lines << header_bar
298
392
  lines << ""
299
- max_rows = terminal_height - 5
393
+ stats_meters(collector.overview, collector.processes).each_line(chomp: true) { |l| lines << l }
394
+ lines << ""
395
+ max_rows = terminal_height - 12
300
396
  workers_section(collector.workers, max_rows: max_rows, scrollable: true).each_line(chomp: true) { |l| lines << l }
301
397
  lines << :footer
302
398
  lines
@@ -306,7 +402,9 @@ module Sktop
306
402
  lines = []
307
403
  lines << header_bar
308
404
  lines << ""
309
- max_rows = terminal_height - 5
405
+ stats_meters(collector.overview, collector.processes).each_line(chomp: true) { |l| lines << l }
406
+ lines << ""
407
+ max_rows = terminal_height - 12
310
408
  retries_scrollable(collector.retry_jobs(limit: 500), max_rows).each_line(chomp: true) { |l| lines << l }
311
409
  lines << :footer
312
410
  lines
@@ -316,7 +414,9 @@ module Sktop
316
414
  lines = []
317
415
  lines << header_bar
318
416
  lines << ""
319
- max_rows = terminal_height - 5
417
+ stats_meters(collector.overview, collector.processes).each_line(chomp: true) { |l| lines << l }
418
+ lines << ""
419
+ max_rows = terminal_height - 12
320
420
  scheduled_scrollable(collector.scheduled_jobs(limit: 500), max_rows).each_line(chomp: true) { |l| lines << l }
321
421
  lines << :footer
322
422
  lines
@@ -326,7 +426,9 @@ module Sktop
326
426
  lines = []
327
427
  lines << header_bar
328
428
  lines << ""
329
- max_rows = terminal_height - 5
429
+ stats_meters(collector.overview, collector.processes).each_line(chomp: true) { |l| lines << l }
430
+ lines << ""
431
+ max_rows = terminal_height - 12
330
432
  dead_scrollable(collector.dead_jobs(limit: 500), max_rows).each_line(chomp: true) { |l| lines << l }
331
433
  lines << :footer
332
434
  lines
@@ -1,8 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sktop
4
+ # Provides methods for performing actions on Sidekiq jobs and processes.
5
+ # All methods are class methods and can be called directly on the module.
6
+ #
7
+ # @example Retry a failed job
8
+ # Sktop::JobActions.retry_job("abc123", :retry)
9
+ #
10
+ # @example Delete a dead job
11
+ # Sktop::JobActions.delete_job("abc123", :dead)
12
+ #
13
+ # @example Quiet a Sidekiq process
14
+ # Sktop::JobActions.quiet_process("myhost:12345:abc")
4
15
  module JobActions
5
16
  class << self
17
+ # Retry a specific job from the retry or dead queue.
18
+ #
19
+ # @param jid [String] the job ID to retry
20
+ # @param source [Symbol] the source queue (:retry or :dead)
21
+ # @return [Boolean] true if the job was successfully retried
22
+ # @raise [RuntimeError] if the job is not found
23
+ #
24
+ # @example Retry a job from the retry queue
25
+ # Sktop::JobActions.retry_job("abc123def456", :retry)
6
26
  def retry_job(jid, source)
7
27
  job = find_job(jid, source)
8
28
  raise "Job not found (JID: #{jid})" unless job
@@ -11,6 +31,12 @@ module Sktop
11
31
  true
12
32
  end
13
33
 
34
+ # Delete a specific job from the retry or dead queue.
35
+ #
36
+ # @param jid [String] the job ID to delete
37
+ # @param source [Symbol] the source queue (:retry or :dead)
38
+ # @return [Boolean] true if the job was successfully deleted
39
+ # @raise [RuntimeError] if the job is not found
14
40
  def delete_job(jid, source)
15
41
  job = find_job(jid, source)
16
42
  raise "Job not found (JID: #{jid})" unless job
@@ -19,6 +45,12 @@ module Sktop
19
45
  true
20
46
  end
21
47
 
48
+ # Kill a specific job, moving it to the dead queue.
49
+ #
50
+ # @param jid [String] the job ID to kill
51
+ # @param source [Symbol] the source queue (:retry or :scheduled)
52
+ # @return [Boolean] true if the job was successfully killed
53
+ # @raise [RuntimeError] if the job is not found
22
54
  def kill_job(jid, source)
23
55
  job = find_job(jid, source)
24
56
  raise "Job not found (JID: #{jid})" unless job
@@ -27,6 +59,10 @@ module Sktop
27
59
  true
28
60
  end
29
61
 
62
+ # Retry all jobs in a sorted set (retry or dead queue).
63
+ #
64
+ # @param source [Symbol] the source queue (:retry or :dead)
65
+ # @return [Integer] the number of jobs retried
30
66
  def retry_all(source)
31
67
  set = get_set(source)
32
68
  count = set.size
@@ -34,6 +70,10 @@ module Sktop
34
70
  count
35
71
  end
36
72
 
73
+ # Delete all jobs from a sorted set (retry or dead queue).
74
+ #
75
+ # @param source [Symbol] the source queue (:retry or :dead)
76
+ # @return [Integer] the number of jobs deleted
37
77
  def delete_all(source)
38
78
  set = get_set(source)
39
79
  count = set.size
@@ -41,6 +81,12 @@ module Sktop
41
81
  count
42
82
  end
43
83
 
84
+ # Delete a specific job from a named queue.
85
+ #
86
+ # @param queue_name [String] the name of the queue
87
+ # @param jid [String] the job ID to delete
88
+ # @return [Boolean] true if the job was successfully deleted
89
+ # @raise [RuntimeError] if the job is not found in the queue
44
90
  def delete_queue_job(queue_name, jid)
45
91
  queue = Sidekiq::Queue.new(queue_name)
46
92
  job = queue.find { |j| j.jid == jid }
@@ -50,6 +96,12 @@ module Sktop
50
96
  true
51
97
  end
52
98
 
99
+ # Send the QUIET signal to a Sidekiq process.
100
+ # A quieted process stops fetching new jobs but finishes current work.
101
+ #
102
+ # @param identity [String] the process identity (e.g., "hostname:pid:tag")
103
+ # @return [Boolean] true if the signal was sent successfully
104
+ # @raise [RuntimeError] if the process is not found
53
105
  def quiet_process(identity)
54
106
  process = find_process(identity)
55
107
  raise "Process not found (identity: #{identity})" unless process
@@ -58,6 +110,12 @@ module Sktop
58
110
  true
59
111
  end
60
112
 
113
+ # Send the STOP signal to a Sidekiq process.
114
+ # This initiates a graceful shutdown of the process.
115
+ #
116
+ # @param identity [String] the process identity (e.g., "hostname:pid:tag")
117
+ # @return [Boolean] true if the signal was sent successfully
118
+ # @raise [RuntimeError] if the process is not found
61
119
  def stop_process(identity)
62
120
  process = find_process(identity)
63
121
  raise "Process not found (identity: #{identity})" unless process
@@ -68,6 +126,12 @@ module Sktop
68
126
 
69
127
  private
70
128
 
129
+ # Get the appropriate Sidekiq sorted set for a given source.
130
+ #
131
+ # @param source [Symbol] the source type (:retry, :dead, or :scheduled)
132
+ # @return [Sidekiq::RetrySet, Sidekiq::DeadSet, Sidekiq::ScheduledSet]
133
+ # @raise [RuntimeError] if the source is unknown
134
+ # @api private
71
135
  def get_set(source)
72
136
  case source
73
137
  when :retry
@@ -81,6 +145,12 @@ module Sktop
81
145
  end
82
146
  end
83
147
 
148
+ # Find a job by JID in a sorted set.
149
+ #
150
+ # @param jid [String] the job ID to find
151
+ # @param source [Symbol] the source type (:retry, :dead, or :scheduled)
152
+ # @return [Sidekiq::SortedEntry, nil] the job entry or nil if not found
153
+ # @api private
84
154
  def find_job(jid, source)
85
155
  set = get_set(source)
86
156
 
@@ -98,6 +168,11 @@ module Sktop
98
168
  nil
99
169
  end
100
170
 
171
+ # Find a Sidekiq process by its identity string.
172
+ #
173
+ # @param identity [String] the process identity
174
+ # @return [Sidekiq::Process, nil] the process or nil if not found
175
+ # @api private
101
176
  def find_process(identity)
102
177
  Sidekiq::ProcessSet.new.find { |p| p["identity"] == identity }
103
178
  end
@@ -1,16 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sktop
4
+ # Collects statistics and data from Sidekiq for display in the TUI.
5
+ # Wraps the Sidekiq API to provide a consistent interface for querying
6
+ # queues, processes, workers, and job data.
7
+ #
8
+ # @example Basic usage
9
+ # collector = Sktop::StatsCollector.new
10
+ # puts collector.overview[:processed]
11
+ # puts collector.queues.length
12
+ #
13
+ # @example Refreshing data
14
+ # collector = Sktop::StatsCollector.new
15
+ # loop do
16
+ # collector.refresh!
17
+ # display_stats(collector.overview)
18
+ # sleep 2
19
+ # end
4
20
  class StatsCollector
21
+ # Create a new StatsCollector instance.
22
+ # Initializes with fresh statistics from Sidekiq.
5
23
  def initialize
6
24
  @stats = Sidekiq::Stats.new
7
25
  end
8
26
 
27
+ # Refresh the cached statistics from Sidekiq.
28
+ # Call this method periodically to get updated data.
29
+ #
30
+ # @return [self] the collector instance for method chaining
9
31
  def refresh!
10
32
  @stats = Sidekiq::Stats.new
11
33
  self
12
34
  end
13
35
 
36
+ # Get an overview of Sidekiq statistics.
37
+ #
38
+ # @return [Hash] overview statistics
39
+ # @option return [Integer] :processed total jobs processed
40
+ # @option return [Integer] :failed total jobs failed
41
+ # @option return [Integer] :scheduled_size jobs in scheduled queue
42
+ # @option return [Integer] :retry_size jobs in retry queue
43
+ # @option return [Integer] :dead_size jobs in dead queue
44
+ # @option return [Integer] :enqueued total jobs across all queues
45
+ # @option return [Float] :default_queue_latency latency of default queue in seconds
14
46
  def overview
15
47
  {
16
48
  processed: @stats.processed,
@@ -23,6 +55,13 @@ module Sktop
23
55
  }
24
56
  end
25
57
 
58
+ # Get information about all Sidekiq queues.
59
+ #
60
+ # @return [Array<Hash>] array of queue information hashes
61
+ # @option return [String] :name queue name
62
+ # @option return [Integer] :size number of jobs in queue
63
+ # @option return [Float] :latency queue latency in seconds
64
+ # @option return [Boolean] :paused whether the queue is paused
26
65
  def queues
27
66
  Sidekiq::Queue.all.map do |queue|
28
67
  {
@@ -34,6 +73,21 @@ module Sktop
34
73
  end
35
74
  end
36
75
 
76
+ # Get information about all running Sidekiq processes.
77
+ #
78
+ # @return [Array<Hash>] array of process information hashes
79
+ # @option return [String] :identity unique process identifier
80
+ # @option return [String] :hostname the host running the process
81
+ # @option return [Integer] :pid process ID
82
+ # @option return [String, nil] :tag optional process tag
83
+ # @option return [Time] :started_at when the process started
84
+ # @option return [Integer] :concurrency number of worker threads
85
+ # @option return [Integer] :busy number of threads currently processing
86
+ # @option return [Array<String>] :queues queues this process listens to
87
+ # @option return [Array<String>] :labels process labels
88
+ # @option return [Boolean] :quiet whether the process is quieted
89
+ # @option return [Boolean] :stopping whether the process is stopping
90
+ # @option return [Integer, nil] :rss memory usage in KB
37
91
  def processes
38
92
  Sidekiq::ProcessSet.new.map do |process|
39
93
  # Use direct hash access for status flags - the method accessors
@@ -61,12 +115,30 @@ module Sktop
61
115
  end
62
116
  end
63
117
 
118
+ # Get information about all currently running workers.
119
+ #
120
+ # @return [Array<Hash>] array of worker information hashes
121
+ # @option return [String] :process_id the process identity
122
+ # @option return [String] :thread_id the thread identifier
123
+ # @option return [String] :queue the queue being processed
124
+ # @option return [String] :class the job class name
125
+ # @option return [Array] :args the job arguments
126
+ # @option return [Time] :run_at when the job started
127
+ # @option return [Float] :elapsed seconds since job started
64
128
  def workers
65
129
  Sidekiq::Workers.new.map do |process_id, thread_id, work|
66
130
  extract_worker_info(process_id, thread_id, work)
67
131
  end
68
132
  end
69
133
 
134
+ # Extract worker information from Sidekiq's Workers API.
135
+ # Handles both Sidekiq 6.x (hash-based) and 7.x (Work object) formats.
136
+ #
137
+ # @param process_id [String] the process identity
138
+ # @param thread_id [String] the thread identifier
139
+ # @param work [Hash, Sidekiq::Work] the work data (format depends on Sidekiq version)
140
+ # @return [Hash] normalized worker information
141
+ # @api private
70
142
  def extract_worker_info(process_id, thread_id, work)
71
143
  # Sidekiq 7+ returns Work objects, older versions return hashes
72
144
  if work.is_a?(Hash)
@@ -108,6 +180,16 @@ module Sktop
108
180
  end
109
181
  end
110
182
 
183
+ # Get jobs from a specific queue.
184
+ #
185
+ # @param queue_name [String] the name of the queue
186
+ # @param limit [Integer] maximum number of jobs to return (default: 100)
187
+ # @return [Array<Hash>] array of job information hashes
188
+ # @option return [String] :jid the job ID
189
+ # @option return [String] :class the job class name
190
+ # @option return [Array] :args the job arguments
191
+ # @option return [Time, nil] :enqueued_at when the job was enqueued
192
+ # @option return [Time, nil] :created_at when the job was created
111
193
  def queue_jobs(queue_name, limit: 100)
112
194
  queue = Sidekiq::Queue.new(queue_name)
113
195
  queue.first(limit).map do |job|
@@ -121,6 +203,15 @@ module Sktop
121
203
  end
122
204
  end
123
205
 
206
+ # Get jobs from the scheduled queue.
207
+ #
208
+ # @param limit [Integer] maximum number of jobs to return (default: 10)
209
+ # @return [Array<Hash>] array of scheduled job information hashes
210
+ # @option return [String] :class the job class name
211
+ # @option return [String] :queue the target queue
212
+ # @option return [Array] :args the job arguments
213
+ # @option return [Time] :scheduled_at when the job is scheduled to run
214
+ # @option return [Time, nil] :created_at when the job was created
124
215
  def scheduled_jobs(limit: 10)
125
216
  Sidekiq::ScheduledSet.new.first(limit).map do |job|
126
217
  {
@@ -133,6 +224,18 @@ module Sktop
133
224
  end
134
225
  end
135
226
 
227
+ # Get jobs from the retry queue.
228
+ #
229
+ # @param limit [Integer] maximum number of jobs to return (default: 10)
230
+ # @return [Array<Hash>] array of retry job information hashes
231
+ # @option return [String] :jid the job ID
232
+ # @option return [String] :class the job class name
233
+ # @option return [String] :queue the original queue
234
+ # @option return [Array] :args the job arguments
235
+ # @option return [Time, nil] :failed_at when the job failed
236
+ # @option return [Integer] :retry_count number of retry attempts
237
+ # @option return [String] :error_class the exception class name
238
+ # @option return [String] :error_message the exception message
136
239
  def retry_jobs(limit: 10)
137
240
  Sidekiq::RetrySet.new.first(limit).map do |job|
138
241
  {
@@ -148,6 +251,17 @@ module Sktop
148
251
  end
149
252
  end
150
253
 
254
+ # Get jobs from the dead (morgue) queue.
255
+ #
256
+ # @param limit [Integer] maximum number of jobs to return (default: 10)
257
+ # @return [Array<Hash>] array of dead job information hashes
258
+ # @option return [String] :jid the job ID
259
+ # @option return [String] :class the job class name
260
+ # @option return [String] :queue the original queue
261
+ # @option return [Array] :args the job arguments
262
+ # @option return [Time, nil] :failed_at when the job failed
263
+ # @option return [String] :error_class the exception class name
264
+ # @option return [String] :error_message the exception message
151
265
  def dead_jobs(limit: 10)
152
266
  Sidekiq::DeadSet.new.first(limit).map do |job|
153
267
  {
@@ -162,6 +276,12 @@ module Sktop
162
276
  end
163
277
  end
164
278
 
279
+ # Get historical statistics for processed and failed jobs.
280
+ #
281
+ # @param days [Integer] number of days of history to retrieve (default: 7)
282
+ # @return [Hash] history data
283
+ # @option return [Hash] :processed daily processed counts keyed by date
284
+ # @option return [Hash] :failed daily failed counts keyed by date
165
285
  def history(days: 7)
166
286
  stats_history = Sidekiq::Stats::History.new(days)
167
287
  {
data/lib/sktop/version.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sktop
4
- VERSION = "0.1.6"
4
+ # Current version of the Sktop gem.
5
+ # Follows semantic versioning (major.minor.patch).
6
+ # @return [String] the version string
7
+ VERSION = "0.2.0"
5
8
  end
data/lib/sktop.rb CHANGED
@@ -13,10 +13,31 @@ require_relative "sktop/job_actions"
13
13
  require_relative "sktop/display"
14
14
  require_relative "sktop/cli"
15
15
 
16
+ # Sktop is a terminal-based Sidekiq monitoring tool similar to htop.
17
+ # It provides real-time visibility into Sidekiq processes, queues,
18
+ # workers, and job status with an interactive TUI.
19
+ #
20
+ # @example Running from command line
21
+ # sktop -r redis://localhost:6379/0
22
+ #
23
+ # @example Using in Ruby code
24
+ # Sktop.configure_redis(url: "redis://localhost:6379/0")
25
+ # Sktop::CLI.new.run
26
+ #
27
+ # @see https://github.com/jamez01/sktop
16
28
  module Sktop
29
+ # Base error class for all Sktop errors
17
30
  class Error < StandardError; end
18
31
 
19
32
  class << self
33
+ # Configure the Redis connection for Sidekiq client mode.
34
+ #
35
+ # @param url [String, nil] Redis connection URL (e.g., "redis://localhost:6379/0")
36
+ # @param namespace [String, nil] Redis namespace for Sidekiq keys (currently unused)
37
+ # @return [void]
38
+ #
39
+ # @example Configure with a custom Redis URL
40
+ # Sktop.configure_redis(url: "redis://myredis:6379/1")
20
41
  def configure_redis(url: nil, namespace: nil)
21
42
  options = {}
22
43
  options[:url] = url if url
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.6
4
+ version: 0.2.0
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-18 00:00:00.000000000 Z
11
+ date: 2026-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq