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 +4 -4
- data/lib/sktop/cli.rb +17 -1
- data/lib/sktop/display.rb +194 -3
- data/lib/sktop/stats_collector.rb +104 -0
- 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: ca4fae6f1476e43b4393759a8e1b6108d6d5a7629b9601d951b7f385ede63e49
|
|
4
|
+
data.tar.gz: 56350127188d3cd045a03f087cbbd67b25f4f402f8ab2e9f374d63661ab1fede
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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.
|
|
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-
|
|
11
|
+
date: 2026-01-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: sidekiq
|