sktop 0.2.0 → 0.3.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: d1e0891fc2a69763f5784c7cc96fc69103d14362aa67a08b82b6c13aa578985b
4
- data.tar.gz: fea7b7284c37d666516d42c2bcc97e0f6b3db4cbb53628e2e3b35c6521c44845
3
+ metadata.gz: ca4fae6f1476e43b4393759a8e1b6108d6d5a7629b9601d951b7f385ede63e49
4
+ data.tar.gz: 56350127188d3cd045a03f087cbbd67b25f4f402f8ab2e9f374d63661ab1fede
5
5
  SHA512:
6
- metadata.gz: 59b4378e2dae18d69bcbd9c3a6492ff0bcad4d830ad9d0c893fa0b9f70f6845d87b6652dcc850d7a02efaea10c00343203d8b495c8eded6318a9b6c648e1fb5e
7
- data.tar.gz: eb54c39df9aa1ec70bcb719b11a7ab0aac88970586345793e890b67dda9f753f207e8f7e5ea0923153118b66c59952f60ff46309e28da281e2bc4f87cbd48c4a
6
+ metadata.gz: d5456687d96c071a4919e9069ecf1c7778e47c74d20082113c1e43d3a507559175990bca6c6fd4c9c22a36769c33560f2ae2fca9ad861810bc020974de4a206e
7
+ data.tar.gz: 2c61aec794743e8239dafcfef96ee7ba92099bfbea3d5dd03fa71ddc5c18b3017025f732b785b8aefdcd46b3bd7e5abc1f04cdcf1010585f63cd97c4119b243a
data/lib/sktop/cli.rb CHANGED
@@ -120,6 +120,14 @@ module Sktop
120
120
  @options[:initial_view] = :dead
121
121
  end
122
122
 
123
+ opts.on("-b", "--batches", "Batches view (Pro/Enterprise)") do
124
+ @options[:initial_view] = :batches
125
+ end
126
+
127
+ opts.on("-c", "--cron", "Periodic/Cron jobs view (Enterprise)") do
128
+ @options[:initial_view] = :periodic
129
+ end
130
+
123
131
  opts.separator ""
124
132
 
125
133
  opts.on("-1", "--once", "Display once and exit (no auto-refresh)") do
@@ -223,7 +231,11 @@ module Sktop
223
231
  workers: collector.workers,
224
232
  retry_jobs: collector.retry_jobs(limit: 500),
225
233
  scheduled_jobs: collector.scheduled_jobs(limit: 500),
226
- dead_jobs: collector.dead_jobs(limit: 500)
234
+ dead_jobs: collector.dead_jobs(limit: 500),
235
+ # Enterprise/Pro features
236
+ edition: collector.edition,
237
+ batches: collector.batches(limit: 500),
238
+ periodic_jobs: collector.periodic_jobs
227
239
  }
228
240
 
229
241
  # If viewing queue jobs, refresh that data too
@@ -331,6 +343,10 @@ module Sktop
331
343
  @display.current_view = :scheduled
332
344
  when 'd', 'D'
333
345
  @display.current_view = :dead
346
+ when 'b', 'B'
347
+ @display.current_view = :batches
348
+ when 'c', 'C'
349
+ @display.current_view = :periodic
334
350
  when 'm', 'M'
335
351
  @display.current_view = :main
336
352
  when "\r", "\n" # Enter key
data/lib/sktop/display.rb CHANGED
@@ -71,7 +71,7 @@ module Sktop
71
71
  # Check if the current view supports item selection.
72
72
  # @return [Boolean] true if the view supports selection
73
73
  def selectable_view?
74
- [:queues, :queue_jobs, :processes, :retries, :dead].include?(@current_view)
74
+ [:queues, :queue_jobs, :processes, :retries, :dead, :batches].include?(@current_view)
75
75
  end
76
76
 
77
77
  # Move up by one page in the current view.
@@ -135,8 +135,9 @@ module Sktop
135
135
  end
136
136
 
137
137
  # Ordered list of views for cycling with arrow keys.
138
+ # Enterprise/Pro views (batches, periodic) are at the end.
138
139
  # @return [Array<Symbol>] the view order
139
- VIEW_ORDER = [:main, :queues, :processes, :workers, :retries, :scheduled, :dead].freeze
140
+ VIEW_ORDER = [:main, :queues, :processes, :workers, :retries, :scheduled, :dead, :batches, :periodic].freeze
140
141
 
141
142
  # Switch to the next view in the cycle.
142
143
  # @return [Symbol] the new current view
@@ -274,6 +275,31 @@ module Sktop
274
275
  def queue_jobs_cache
275
276
  @data[:queue_jobs] || []
276
277
  end
278
+
279
+ # @return [Array<Hash>] batch information (Pro/Enterprise)
280
+ def batches
281
+ @data[:batches] || []
282
+ end
283
+
284
+ # @return [Array<Hash>] periodic job information (Enterprise)
285
+ def periodic_jobs
286
+ @data[:periodic_jobs] || []
287
+ end
288
+
289
+ # @return [String] Sidekiq edition ("Enterprise", "Pro", or "OSS")
290
+ def edition
291
+ @data[:edition] || "OSS"
292
+ end
293
+
294
+ # @return [Boolean] true if Pro features available
295
+ def pro?
296
+ %w[Pro Enterprise].include?(edition)
297
+ end
298
+
299
+ # @return [Boolean] true if Enterprise features available
300
+ def enterprise?
301
+ edition == "Enterprise"
302
+ end
277
303
  end
278
304
 
279
305
  private
@@ -294,6 +320,10 @@ module Sktop
294
320
  build_scheduled_detail(collector)
295
321
  when :dead
296
322
  build_dead_detail(collector)
323
+ when :batches
324
+ build_batches_detail(collector)
325
+ when :periodic
326
+ build_periodic_detail(collector)
297
327
  else
298
328
  build_main_view(collector)
299
329
  end
@@ -434,6 +464,30 @@ module Sktop
434
464
  lines
435
465
  end
436
466
 
467
+ def build_batches_detail(collector)
468
+ lines = []
469
+ lines << header_bar
470
+ lines << ""
471
+ stats_meters(collector.overview, collector.processes).each_line(chomp: true) { |l| lines << l }
472
+ lines << ""
473
+ max_rows = terminal_height - 12
474
+ batches_selectable(collector.batches, max_rows, collector.pro?).each_line(chomp: true) { |l| lines << l }
475
+ lines << :footer
476
+ lines
477
+ end
478
+
479
+ def build_periodic_detail(collector)
480
+ lines = []
481
+ lines << header_bar
482
+ lines << ""
483
+ stats_meters(collector.overview, collector.processes).each_line(chomp: true) { |l| lines << l }
484
+ lines << ""
485
+ max_rows = terminal_height - 12
486
+ periodic_scrollable(collector.periodic_jobs, max_rows, collector.enterprise?).each_line(chomp: true) { |l| lines << l }
487
+ lines << :footer
488
+ lines
489
+ end
490
+
437
491
  def render_with_overwrite(content_parts)
438
492
  width = terminal_width
439
493
  height = terminal_height
@@ -1343,6 +1397,139 @@ module Sktop
1343
1397
  lines.join("\n")
1344
1398
  end
1345
1399
 
1400
+ # Selectable batches view (Pro/Enterprise)
1401
+ def batches_selectable(batches, max_rows, pro_available)
1402
+ width = terminal_width
1403
+ lines = []
1404
+
1405
+ unless pro_available
1406
+ lines << section_bar("Batches - Requires Sidekiq Pro or Enterprise")
1407
+ lines << @pastel.dim(" Sidekiq Pro or Enterprise is required for batch support.")
1408
+ lines << @pastel.dim(" Visit https://sidekiq.org for more information.")
1409
+ return lines.join("\n")
1410
+ end
1411
+
1412
+ scroll_offset = @scroll_offsets[@current_view]
1413
+ data_rows = max_rows - 3
1414
+ max_scroll = [batches.length - data_rows, 0].max
1415
+ scroll_offset = [[scroll_offset, 0].max, max_scroll].min
1416
+ @scroll_offsets[@current_view] = scroll_offset
1417
+
1418
+ @selected_index[@current_view] = [[@selected_index[@current_view], 0].max, [batches.length - 1, 0].max].min
1419
+
1420
+ selected = @selected_index[@current_view]
1421
+ if selected < scroll_offset
1422
+ scroll_offset = selected
1423
+ @scroll_offsets[@current_view] = scroll_offset
1424
+ elsif selected >= scroll_offset + data_rows
1425
+ scroll_offset = selected - data_rows + 1
1426
+ @scroll_offsets[@current_view] = scroll_offset
1427
+ end
1428
+
1429
+ scroll_indicator = batches.length > data_rows ? " [#{scroll_offset + 1}-#{[scroll_offset + data_rows, batches.length].min}/#{batches.length}]" : ""
1430
+ lines << section_bar("Batches#{scroll_indicator} - ↑↓ select, m=main")
1431
+
1432
+ if batches.empty?
1433
+ lines << @pastel.dim(" No active batches")
1434
+ return lines.join("\n")
1435
+ end
1436
+
1437
+ desc_width = [30, (width - 70) / 2].max
1438
+ bid_width = 24
1439
+
1440
+ header = sprintf(" %-#{bid_width}s %-#{desc_width}s %8s %8s %8s %10s", "BID", "DESCRIPTION", "TOTAL", "PENDING", "FAILED", "STATUS")
1441
+ lines << format_table_header(header)
1442
+
1443
+ batches.drop(scroll_offset).first(data_rows).each_with_index do |batch, idx|
1444
+ actual_idx = scroll_offset + idx
1445
+ bid = truncate(batch[:bid], bid_width)
1446
+ desc = truncate(batch[:description] || "(no description)", desc_width)
1447
+ total = batch[:total].to_s
1448
+ pending = batch[:pending].to_s
1449
+ failures = batch[:failures].to_s
1450
+ status = batch[:complete] ? "COMPLETE" : "RUNNING"
1451
+
1452
+ pending_colored = batch[:pending] > 0 ? @pastel.yellow(sprintf("%8s", pending)) : sprintf("%8s", pending)
1453
+ failures_colored = batch[:failures] > 0 ? @pastel.red(sprintf("%8s", failures)) : sprintf("%8s", failures)
1454
+ status_colored = batch[:complete] ? @pastel.green(sprintf("%10s", status)) : @pastel.cyan(sprintf("%10s", status))
1455
+
1456
+ row = sprintf(" %-#{bid_width}s %-#{desc_width}s %8s %s %s %s", bid, desc, total, pending_colored, failures_colored, status_colored)
1457
+
1458
+ if actual_idx == selected
1459
+ lines << @pastel.black.on_white(row + " " * [width - visible_string_length(row), 0].max)
1460
+ else
1461
+ lines << row
1462
+ end
1463
+ end
1464
+
1465
+ remaining = batches.length - scroll_offset - data_rows
1466
+ if remaining > 0
1467
+ lines << @pastel.dim(" ↓ #{remaining} more")
1468
+ end
1469
+
1470
+ if @status_message && @status_time && (Time.now - @status_time) < 3
1471
+ lines << @pastel.green(" #{@status_message}")
1472
+ end
1473
+
1474
+ lines.join("\n")
1475
+ end
1476
+
1477
+ # Scrollable periodic jobs view (Enterprise)
1478
+ def periodic_scrollable(jobs, max_rows, enterprise_available)
1479
+ width = terminal_width
1480
+ lines = []
1481
+
1482
+ unless enterprise_available
1483
+ lines << section_bar("Periodic Jobs - Requires Sidekiq Enterprise")
1484
+ lines << @pastel.dim(" Sidekiq Enterprise is required for periodic job support.")
1485
+ lines << @pastel.dim(" Visit https://sidekiq.org for more information.")
1486
+ return lines.join("\n")
1487
+ end
1488
+
1489
+ scroll_offset = @scroll_offsets[@current_view]
1490
+ data_rows = max_rows - 2
1491
+ max_scroll = [jobs.length - data_rows, 0].max
1492
+ scroll_offset = [[scroll_offset, 0].max, max_scroll].min
1493
+ @scroll_offsets[@current_view] = scroll_offset
1494
+
1495
+ scroll_indicator = jobs.length > data_rows ? " [#{scroll_offset + 1}-#{[scroll_offset + data_rows, jobs.length].min}/#{jobs.length}]" : ""
1496
+ lines << section_bar("Periodic Jobs#{scroll_indicator} - ↑↓ to scroll, m=main")
1497
+
1498
+ if jobs.empty?
1499
+ lines << @pastel.dim(" No periodic jobs configured")
1500
+ return lines.join("\n")
1501
+ end
1502
+
1503
+ klass_width = [40, (width - 50) / 2].max
1504
+ schedule_width = 20
1505
+ queue_width = 15
1506
+
1507
+ header = sprintf(" %-#{klass_width}s %-#{schedule_width}s %-#{queue_width}s %s", "JOB CLASS", "SCHEDULE", "QUEUE", "LAST RUN")
1508
+ lines << format_table_header(header)
1509
+
1510
+ jobs.drop(scroll_offset).first(data_rows).each do |job|
1511
+ klass = truncate(job[:klass], klass_width)
1512
+ schedule = truncate(job[:schedule], schedule_width)
1513
+ queue = truncate(job[:options]["queue"] || "default", queue_width)
1514
+ last_run = if job[:history].any?
1515
+ last = job[:history].first
1516
+ last.is_a?(Hash) ? (last["enqueued_at"] || "N/A") : last.to_s
1517
+ else
1518
+ "Never"
1519
+ end
1520
+ last_run = truncate(last_run.to_s, 20)
1521
+
1522
+ lines << sprintf(" %-#{klass_width}s %-#{schedule_width}s %-#{queue_width}s %s", klass, @pastel.cyan(schedule), queue, last_run)
1523
+ end
1524
+
1525
+ remaining = jobs.length - scroll_offset - data_rows
1526
+ if remaining > 0
1527
+ lines << @pastel.dim(" ↓ #{remaining} more")
1528
+ end
1529
+
1530
+ lines.join("\n")
1531
+ end
1532
+
1346
1533
  def function_bar
1347
1534
  # Map keys to views for highlighting
1348
1535
  view_keys = {
@@ -1352,7 +1539,9 @@ module Sktop
1352
1539
  "w" => :workers,
1353
1540
  "r" => :retries,
1354
1541
  "s" => :scheduled,
1355
- "d" => :dead
1542
+ "d" => :dead,
1543
+ "b" => :batches,
1544
+ "c" => :periodic
1356
1545
  }
1357
1546
 
1358
1547
  items = [
@@ -1363,6 +1552,8 @@ module Sktop
1363
1552
  ["r", "Retries"],
1364
1553
  ["s", "Sched"],
1365
1554
  ["d", "Dead"],
1555
+ ["b", "Batch"],
1556
+ ["c", "Cron"],
1366
1557
  ["^C", "Quit"]
1367
1558
  ]
1368
1559
 
@@ -289,5 +289,109 @@ module Sktop
289
289
  failed: stats_history.failed
290
290
  }
291
291
  end
292
+
293
+ # Check if Sidekiq Enterprise is loaded.
294
+ #
295
+ # @return [Boolean] true if Enterprise features are available
296
+ def enterprise?
297
+ defined?(Sidekiq::Enterprise)
298
+ end
299
+
300
+ # Check if Sidekiq Pro is loaded.
301
+ #
302
+ # @return [Boolean] true if Pro features are available
303
+ def pro?
304
+ defined?(Sidekiq::Pro) || enterprise?
305
+ end
306
+
307
+ # Get the Sidekiq edition name.
308
+ #
309
+ # @return [String] "Enterprise", "Pro", or "OSS"
310
+ def edition
311
+ if enterprise?
312
+ "Enterprise"
313
+ elsif pro?
314
+ "Pro"
315
+ else
316
+ "OSS"
317
+ end
318
+ end
319
+
320
+ # Get information about active batches (requires Sidekiq Pro or Enterprise).
321
+ #
322
+ # @param limit [Integer] maximum number of batches to return (default: 50)
323
+ # @return [Array<Hash>] array of batch information hashes
324
+ # @option return [String] :bid the batch ID
325
+ # @option return [String, nil] :description batch description
326
+ # @option return [Integer] :total total jobs in batch
327
+ # @option return [Integer] :pending jobs not yet completed
328
+ # @option return [Integer] :failures failed jobs
329
+ # @option return [Time, nil] :created_at when the batch was created
330
+ # @option return [Boolean] :complete whether all jobs have run
331
+ # @return [Array] empty array if Pro/Enterprise not available
332
+ def batches(limit: 50)
333
+ return [] unless pro? && defined?(Sidekiq::BatchSet)
334
+
335
+ Sidekiq::BatchSet.new.first(limit).map do |status|
336
+ {
337
+ bid: status.bid,
338
+ description: status.description,
339
+ total: status.total,
340
+ pending: status.pending,
341
+ failures: status.failures,
342
+ created_at: status.created_at,
343
+ complete: status.complete?
344
+ }
345
+ end
346
+ rescue StandardError
347
+ []
348
+ end
349
+
350
+ # Get information about periodic jobs (requires Sidekiq Enterprise).
351
+ #
352
+ # @return [Array<Hash>] array of periodic job information hashes
353
+ # @option return [String] :lid the loop identifier
354
+ # @option return [String] :schedule the cron expression
355
+ # @option return [String] :klass the worker class name
356
+ # @option return [Hash] :options job options (queue, retry, etc.)
357
+ # @option return [Array] :history recent execution history
358
+ # @return [Array] empty array if Enterprise not available
359
+ def periodic_jobs
360
+ return [] unless enterprise? && defined?(Sidekiq::Periodic::LoopSet)
361
+
362
+ Sidekiq::Periodic::LoopSet.new.map do |lop|
363
+ {
364
+ lid: lop.lid,
365
+ schedule: lop.schedule,
366
+ klass: lop.klass,
367
+ options: lop.options || {},
368
+ history: lop.history || []
369
+ }
370
+ end
371
+ rescue StandardError
372
+ []
373
+ end
374
+
375
+ # Get the total count of active batches (requires Sidekiq Pro or Enterprise).
376
+ #
377
+ # @return [Integer] number of active batches, or 0 if not available
378
+ def batch_count
379
+ return 0 unless pro? && defined?(Sidekiq::BatchSet)
380
+
381
+ Sidekiq::BatchSet.new.size
382
+ rescue StandardError
383
+ 0
384
+ end
385
+
386
+ # Get the count of periodic jobs (requires Sidekiq Enterprise).
387
+ #
388
+ # @return [Integer] number of periodic jobs, or 0 if not available
389
+ def periodic_count
390
+ return 0 unless enterprise? && defined?(Sidekiq::Periodic::LoopSet)
391
+
392
+ Sidekiq::Periodic::LoopSet.new.size
393
+ rescue StandardError
394
+ 0
395
+ end
292
396
  end
293
397
  end
data/lib/sktop/version.rb CHANGED
@@ -4,5 +4,5 @@ module Sktop
4
4
  # Current version of the Sktop gem.
5
5
  # Follows semantic versioning (major.minor.patch).
6
6
  # @return [String] the version string
7
- VERSION = "0.2.0"
7
+ VERSION = "0.3.0"
8
8
  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.2.0
4
+ version: 0.3.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-30 00:00:00.000000000 Z
11
+ date: 2026-01-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq