solid_queue_tui 0.1.3 → 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 +4 -4
- data/exe/qtop +35 -0
- data/lib/solid_queue_tui/application.rb +1 -0
- data/lib/solid_queue_tui/cli.rb +2 -2
- data/lib/solid_queue_tui/data/hourly_stats_query.rb +85 -0
- data/lib/solid_queue_tui/data/stats.rb +15 -3
- data/lib/solid_queue_tui/version.rb +1 -1
- data/lib/solid_queue_tui/views/blocked_view.rb +3 -2
- data/lib/solid_queue_tui/views/concerns/filterable.rb +4 -0
- data/lib/solid_queue_tui/views/dashboard_view.rb +226 -18
- data/lib/solid_queue_tui/views/failed_view.rb +4 -3
- data/lib/solid_queue_tui/views/finished_view.rb +3 -3
- data/lib/solid_queue_tui/views/in_progress_view.rb +3 -2
- data/lib/solid_queue_tui/views/queues_view.rb +4 -1
- data/lib/solid_queue_tui/views/scheduled_view.rb +3 -2
- data/lib/solid_queue_tui.rb +1 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: edcec2bda0cc128879fda2b066fcbd560c7879c4a2554d53d3ff01c50c4d5040
|
|
4
|
+
data.tar.gz: 2457d9eb670fddb7a851f64dbe37c1f2b69fdcd9608d69e2feb5b0af4b406358
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a6f0174997d605c9e0d83c22c8a53973b017cd2e4cdd9cc260ace6f249d1a029c0c475a78124d5fd9537b9e5d1ca361c4b64adbd4a47c5842d0d05ede3ae153d
|
|
7
|
+
data.tar.gz: af07fa966c63b5f63b4f2e8d034e0d44b023294d443403b9130a631a5ecd3c52913dc0d71ef42ae85de8d81d37fc9e3c5e8def0f983cff6e372526bdf8e710bd
|
data/exe/qtop
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Boot the host Rails application's environment.
|
|
5
|
+
# This ensures ActiveRecord, Solid Queue models, and the database
|
|
6
|
+
# connection pool are fully loaded before the TUI starts.
|
|
7
|
+
env_file = File.join(Dir.pwd, "config", "environment.rb")
|
|
8
|
+
|
|
9
|
+
unless File.exist?(env_file)
|
|
10
|
+
$stderr.puts "Error: config/environment.rb not found in #{Dir.pwd}"
|
|
11
|
+
$stderr.puts ""
|
|
12
|
+
$stderr.puts "qtop must be run from your Rails application's root directory."
|
|
13
|
+
$stderr.puts "Make sure solid_queue_tui is in your Gemfile and Solid Queue is configured."
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
require env_file
|
|
18
|
+
|
|
19
|
+
# Logs must never hit STDOUT/STDERR — that would corrupt the TUI.
|
|
20
|
+
# If a log/ directory exists (traditional Rails), write there.
|
|
21
|
+
# Otherwise (Rails 8 defaults), silence logging.
|
|
22
|
+
#TODO: figure out logging for rails 8, docker
|
|
23
|
+
|
|
24
|
+
log_dir = File.join(Dir.pwd, "log")
|
|
25
|
+
if Dir.exist?(log_dir)
|
|
26
|
+
log_file = File.join(log_dir, "#{Rails.env}.log")
|
|
27
|
+
tui_logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(log_file))
|
|
28
|
+
tui_logger.push_tags("SQTUI")
|
|
29
|
+
else
|
|
30
|
+
tui_logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(File::NULL))
|
|
31
|
+
end
|
|
32
|
+
ActiveRecord::Base.logger = tui_logger
|
|
33
|
+
Rails.logger = tui_logger
|
|
34
|
+
|
|
35
|
+
SolidQueueTui::CLI.run(ARGV)
|
|
@@ -492,6 +492,7 @@ module SolidQueueTui
|
|
|
492
492
|
help_section("Actions"),
|
|
493
493
|
help_line("r", "Refresh data"),
|
|
494
494
|
help_line("/", "Filter by class name"),
|
|
495
|
+
help_line("c", "Clear active filter"),
|
|
495
496
|
help_line("R", "Retry failed job (in Failed view)"),
|
|
496
497
|
help_line("D", "Discard failed job (in Failed view)"),
|
|
497
498
|
help_line("A", "Retry all failed jobs"),
|
data/lib/solid_queue_tui/cli.rb
CHANGED
|
@@ -15,7 +15,7 @@ module SolidQueueTui
|
|
|
15
15
|
options = {}
|
|
16
16
|
|
|
17
17
|
OptionParser.new do |opts|
|
|
18
|
-
opts.banner = "Usage:
|
|
18
|
+
opts.banner = "Usage: qtop [options]"
|
|
19
19
|
opts.separator ""
|
|
20
20
|
opts.separator "Options:"
|
|
21
21
|
|
|
@@ -36,7 +36,7 @@ module SolidQueueTui
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
opts.on("-v", "--version", "Show version") do
|
|
39
|
-
puts "
|
|
39
|
+
puts "qtop v#{SolidQueueTui::VERSION}"
|
|
40
40
|
exit
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Data
|
|
5
|
+
class HourlyStatsQuery
|
|
6
|
+
Result = Struct.new(:data, :total, :peak, :avg, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
def self.enqueued_per_hour
|
|
9
|
+
raw = SolidQueue::Job
|
|
10
|
+
.where(created_at: 24.hours.ago..)
|
|
11
|
+
.group(hour_sql(:created_at))
|
|
12
|
+
.count
|
|
13
|
+
build_result(raw)
|
|
14
|
+
rescue => e
|
|
15
|
+
empty_result
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.processed_per_hour
|
|
19
|
+
raw = SolidQueue::Job
|
|
20
|
+
.where.not(finished_at: nil)
|
|
21
|
+
.where(finished_at: 24.hours.ago..)
|
|
22
|
+
.group(hour_sql(:finished_at))
|
|
23
|
+
.count
|
|
24
|
+
build_result(raw)
|
|
25
|
+
rescue => e
|
|
26
|
+
empty_result
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.failed_per_hour
|
|
30
|
+
raw = SolidQueue::FailedExecution
|
|
31
|
+
.where(created_at: 24.hours.ago..)
|
|
32
|
+
.group(hour_sql(:created_at))
|
|
33
|
+
.count
|
|
34
|
+
build_result(raw)
|
|
35
|
+
rescue => e
|
|
36
|
+
empty_result
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.empty_result
|
|
40
|
+
now = Time.now.utc
|
|
41
|
+
data = (0..23).map { |i| (now - (23 - i) * 3600).strftime("%H").to_i }
|
|
42
|
+
Result.new(data: data.map { 0 }, total: 0, peak: 0, avg: 0)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def hour_sql(column)
|
|
49
|
+
if sqlite?
|
|
50
|
+
Arel.sql("strftime('%Y-%m-%d %H:00:00', #{column})")
|
|
51
|
+
else
|
|
52
|
+
Arel.sql("DATE_TRUNC('hour', #{column})")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_result(raw_hash)
|
|
57
|
+
now = Time.now.utc
|
|
58
|
+
|
|
59
|
+
lookup = {}
|
|
60
|
+
raw_hash.each do |key, count|
|
|
61
|
+
time = key.is_a?(String) ? Time.parse("#{key} UTC") : key
|
|
62
|
+
lookup[time.strftime("%Y-%m-%d %H")] = count
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# 24-slot array, oldest to newest — values only (for sparkline)
|
|
66
|
+
data = (0..23).map do |i|
|
|
67
|
+
hour_time = now - (23 - i) * 3600
|
|
68
|
+
key = hour_time.strftime("%Y-%m-%d %H")
|
|
69
|
+
lookup[key] || 0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
total = data.sum
|
|
73
|
+
peak = data.max || 0
|
|
74
|
+
avg = total / 24
|
|
75
|
+
|
|
76
|
+
Result.new(data: data, total: total, peak: peak, avg: avg)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def sqlite?
|
|
80
|
+
SolidQueue::Job.connection.adapter_name.downcase.include?("sqlite")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -5,7 +5,9 @@ module SolidQueueTui
|
|
|
5
5
|
class Stats
|
|
6
6
|
attr_reader :ready, :claimed, :failed, :scheduled, :blocked,
|
|
7
7
|
:total_jobs, :completed_jobs, :process_count,
|
|
8
|
-
:processes_by_kind
|
|
8
|
+
:processes_by_kind,
|
|
9
|
+
:enqueued_per_hour, :processed_per_hour, :failed_per_hour,
|
|
10
|
+
:queue_depths
|
|
9
11
|
|
|
10
12
|
def initialize(data)
|
|
11
13
|
@ready = data[:ready]
|
|
@@ -17,6 +19,10 @@ module SolidQueueTui
|
|
|
17
19
|
@completed_jobs = data[:completed_jobs]
|
|
18
20
|
@process_count = data[:process_count]
|
|
19
21
|
@processes_by_kind = data[:processes_by_kind]
|
|
22
|
+
@enqueued_per_hour = data[:enqueued_per_hour]
|
|
23
|
+
@processed_per_hour = data[:processed_per_hour]
|
|
24
|
+
@failed_per_hour = data[:failed_per_hour]
|
|
25
|
+
@queue_depths = data[:queue_depths]
|
|
20
26
|
end
|
|
21
27
|
|
|
22
28
|
def self.fetch
|
|
@@ -29,7 +35,11 @@ module SolidQueueTui
|
|
|
29
35
|
total_jobs: SolidQueue::Job.count,
|
|
30
36
|
completed_jobs: SolidQueue::Job.finished.count,
|
|
31
37
|
process_count: SolidQueue::Process.count,
|
|
32
|
-
processes_by_kind: SolidQueue::Process.group(:kind).count
|
|
38
|
+
processes_by_kind: SolidQueue::Process.group(:kind).count,
|
|
39
|
+
enqueued_per_hour: HourlyStatsQuery.enqueued_per_hour,
|
|
40
|
+
processed_per_hour: HourlyStatsQuery.processed_per_hour,
|
|
41
|
+
failed_per_hour: HourlyStatsQuery.failed_per_hour,
|
|
42
|
+
queue_depths: SolidQueue::ReadyExecution.group(:queue_name).count
|
|
33
43
|
)
|
|
34
44
|
rescue => e
|
|
35
45
|
empty(error: e.message)
|
|
@@ -39,7 +49,9 @@ module SolidQueueTui
|
|
|
39
49
|
new(
|
|
40
50
|
ready: 0, claimed: 0, failed: 0, scheduled: 0, blocked: 0,
|
|
41
51
|
total_jobs: 0, completed_jobs: 0, process_count: 0,
|
|
42
|
-
processes_by_kind: {}
|
|
52
|
+
processes_by_kind: {},
|
|
53
|
+
enqueued_per_hour: nil, processed_per_hour: nil, failed_per_hour: nil,
|
|
54
|
+
queue_depths: {}
|
|
43
55
|
)
|
|
44
56
|
end
|
|
45
57
|
end
|
|
@@ -54,8 +54,9 @@ module SolidQueueTui
|
|
|
54
54
|
{ key: "j/k", action: "Navigate" },
|
|
55
55
|
{ key: "Enter", action: "Detail" },
|
|
56
56
|
{ key: "/", action: "Filter" },
|
|
57
|
+
clear_filter_binding,
|
|
57
58
|
{ key: "G/g", action: "Bottom/Top" }
|
|
58
|
-
]
|
|
59
|
+
].compact
|
|
59
60
|
end
|
|
60
61
|
end
|
|
61
62
|
|
|
@@ -84,7 +85,7 @@ module SolidQueueTui
|
|
|
84
85
|
in { type: :key, code: "/" }
|
|
85
86
|
enter_filter_mode
|
|
86
87
|
nil
|
|
87
|
-
in { type: :key, code: "
|
|
88
|
+
in { type: :key, code: "c" }
|
|
88
89
|
clear_filter
|
|
89
90
|
else
|
|
90
91
|
nil
|
|
@@ -116,6 +116,10 @@ module SolidQueueTui
|
|
|
116
116
|
"#{base_title} (#{parts.join(', ')})"
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
def clear_filter_binding
|
|
120
|
+
@filters.empty? ? nil : { key: "c", action: "Clear Filter" }
|
|
121
|
+
end
|
|
122
|
+
|
|
119
123
|
def filter_bindings
|
|
120
124
|
[
|
|
121
125
|
{ key: "Tab", action: "Next Field" },
|
|
@@ -7,7 +7,6 @@ module SolidQueueTui
|
|
|
7
7
|
|
|
8
8
|
def initialize(tui)
|
|
9
9
|
@tui = tui
|
|
10
|
-
@selected_row = 0
|
|
11
10
|
end
|
|
12
11
|
|
|
13
12
|
def update(stats:)
|
|
@@ -25,7 +24,7 @@ module SolidQueueTui
|
|
|
25
24
|
)
|
|
26
25
|
|
|
27
26
|
render_overview_panels(frame, top)
|
|
28
|
-
|
|
27
|
+
render_metrics(frame, bottom)
|
|
29
28
|
end
|
|
30
29
|
|
|
31
30
|
def handle_input(event)
|
|
@@ -47,6 +46,8 @@ module SolidQueueTui
|
|
|
47
46
|
|
|
48
47
|
private
|
|
49
48
|
|
|
49
|
+
# --- Top panels (sticky) ---
|
|
50
|
+
|
|
50
51
|
def render_overview_panels(frame, area)
|
|
51
52
|
return unless @stats
|
|
52
53
|
|
|
@@ -129,28 +130,228 @@ module SolidQueueTui
|
|
|
129
130
|
)
|
|
130
131
|
end
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
# --- Bottom section: chart + queue summary ---
|
|
134
|
+
|
|
135
|
+
def render_metrics(frame, area)
|
|
133
136
|
return unless @stats
|
|
134
137
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
@tui.
|
|
140
|
-
@tui.
|
|
138
|
+
chart_area, bottom_area = @tui.layout_split(
|
|
139
|
+
area,
|
|
140
|
+
direction: :vertical,
|
|
141
|
+
constraints: [
|
|
142
|
+
@tui.constraint_percentage(70),
|
|
143
|
+
@tui.constraint_percentage(30)
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
queue_area, summary_area = @tui.layout_split(
|
|
148
|
+
bottom_area,
|
|
149
|
+
direction: :horizontal,
|
|
150
|
+
constraints: [
|
|
151
|
+
@tui.constraint_percentage(50),
|
|
152
|
+
@tui.constraint_percentage(50)
|
|
153
|
+
]
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
render_throughput_chart(frame, chart_area)
|
|
157
|
+
render_queue_depth(frame, queue_area)
|
|
158
|
+
render_summary(frame, summary_area)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def render_throughput_chart(frame, area)
|
|
162
|
+
enqueued = @stats.enqueued_per_hour
|
|
163
|
+
processed = @stats.processed_per_hour
|
|
164
|
+
failed = @stats.failed_per_hour
|
|
165
|
+
|
|
166
|
+
# Convert 24-element arrays to [x, y] coordinate pairs
|
|
167
|
+
enqueued_data = to_chart_data(enqueued)
|
|
168
|
+
processed_data = to_chart_data(processed)
|
|
169
|
+
failed_data = to_chart_data(failed)
|
|
170
|
+
|
|
171
|
+
# Y-axis bounds
|
|
172
|
+
all_values = [enqueued&.data, processed&.data, failed&.data].compact.flatten
|
|
173
|
+
y_max = (all_values.max || 10).to_f
|
|
174
|
+
y_max = 10.0 if y_max == 0
|
|
175
|
+
|
|
176
|
+
# X-axis labels at key positions
|
|
177
|
+
now = Time.now.utc
|
|
178
|
+
x_labels = [0, 6, 12, 18, 23].map do |i|
|
|
179
|
+
(now - (23 - i) * 3600).strftime("%H:%M")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Y-axis labels
|
|
183
|
+
y_labels = ["0", format_number((y_max / 2).round), format_number(y_max.round)]
|
|
184
|
+
|
|
185
|
+
datasets = []
|
|
186
|
+
if enqueued_data.any?
|
|
187
|
+
datasets << RatatuiRuby::Widgets::Dataset.new(
|
|
188
|
+
name: "",
|
|
189
|
+
data: enqueued_data,
|
|
190
|
+
style: @tui.style(fg: :cyan),
|
|
191
|
+
marker: :braille,
|
|
192
|
+
graph_type: :line
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if processed_data.any?
|
|
197
|
+
datasets << RatatuiRuby::Widgets::Dataset.new(
|
|
198
|
+
name: "",
|
|
199
|
+
data: processed_data,
|
|
200
|
+
style: @tui.style(fg: :green),
|
|
201
|
+
marker: :braille,
|
|
202
|
+
graph_type: :line
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if failed_data.any?
|
|
207
|
+
datasets << RatatuiRuby::Widgets::Dataset.new(
|
|
208
|
+
name: "",
|
|
209
|
+
data: failed_data,
|
|
210
|
+
style: @tui.style(fg: :red),
|
|
211
|
+
marker: :braille,
|
|
212
|
+
graph_type: :line
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
x_axis = RatatuiRuby::Widgets::Axis.new(
|
|
217
|
+
bounds: [0.0, 23.0],
|
|
218
|
+
labels: x_labels,
|
|
219
|
+
style: @tui.style(fg: :dark_gray)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
y_axis = RatatuiRuby::Widgets::Axis.new(
|
|
223
|
+
bounds: [0.0, y_max],
|
|
224
|
+
labels: y_labels,
|
|
225
|
+
style: @tui.style(fg: :dark_gray)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
chart = @tui.chart(
|
|
229
|
+
datasets: datasets,
|
|
230
|
+
x_axis: x_axis,
|
|
231
|
+
y_axis: y_axis,
|
|
232
|
+
block: @tui.block(
|
|
233
|
+
title: " Throughput (24h) ",
|
|
234
|
+
title_style: @tui.style(fg: :cyan, modifiers: [:bold]),
|
|
235
|
+
titles: [
|
|
236
|
+
{ content: " ● Enqueued ", position: :top, alignment: :right, style: @tui.style(fg: :cyan) },
|
|
237
|
+
{ content: "● Processed ", position: :top, alignment: :right, style: @tui.style(fg: :green) },
|
|
238
|
+
{ content: "● Failed ", position: :top, alignment: :right, style: @tui.style(fg: :red) }
|
|
239
|
+
],
|
|
240
|
+
borders: [:all],
|
|
241
|
+
border_type: :rounded,
|
|
242
|
+
border_style: @tui.style(fg: :dark_gray)
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
frame.render_widget(chart, area)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def render_queue_depth(frame, area)
|
|
250
|
+
lines = []
|
|
251
|
+
|
|
252
|
+
queue_depths = @stats.queue_depths
|
|
253
|
+
if queue_depths.any?
|
|
254
|
+
total_depth = queue_depths.values.sum
|
|
255
|
+
sorted = queue_depths.sort_by { |_, v| -v }
|
|
256
|
+
top_queues = sorted.first(5)
|
|
257
|
+
remaining = sorted.drop(5)
|
|
258
|
+
max_depth = top_queues.first&.last || 1
|
|
259
|
+
|
|
260
|
+
top_queues.each_with_index do |(name, count), idx|
|
|
261
|
+
pct = total_depth > 0 ? (count.to_f / total_depth * 100).round(1) : 0
|
|
262
|
+
bar_width = 20
|
|
263
|
+
filled = max_depth > 0 ? (count.to_f / max_depth * bar_width).round : 0
|
|
264
|
+
empty_bar = bar_width - filled
|
|
265
|
+
|
|
266
|
+
lines << @tui.text_line(spans: [
|
|
267
|
+
@tui.text_span(content: " #{name.ljust(14)}", style: @tui.style(fg: :white)),
|
|
268
|
+
@tui.text_span(content: "#{"█" * filled}", style: @tui.style(fg: :cyan)),
|
|
269
|
+
@tui.text_span(content: "#{"░" * empty_bar}", style: @tui.style(fg: :dark_gray)),
|
|
270
|
+
@tui.text_span(content: " #{format_number(count).rjust(6)} (#{pct}%)", style: @tui.style(fg: :dark_gray))
|
|
271
|
+
])
|
|
272
|
+
|
|
273
|
+
if idx < top_queues.size - 1
|
|
274
|
+
lines << @tui.text_line(spans: [
|
|
275
|
+
@tui.text_span(content: "", style: @tui.style(fg: :dark_gray))
|
|
276
|
+
])
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
if remaining.any?
|
|
281
|
+
others_count = remaining.sum { |_, v| v }
|
|
282
|
+
pct = total_depth > 0 ? (others_count.to_f / total_depth * 100).round(1) : 0
|
|
283
|
+
lines << @tui.text_line(spans: [
|
|
284
|
+
@tui.text_span(content: " +#{remaining.size} more", style: @tui.style(fg: :dark_gray)),
|
|
285
|
+
@tui.text_span(content: " #{format_number(others_count).rjust(26)} (#{pct}%)", style: @tui.style(fg: :dark_gray))
|
|
286
|
+
])
|
|
287
|
+
end
|
|
288
|
+
else
|
|
289
|
+
lines << @tui.text_line(spans: [
|
|
290
|
+
@tui.text_span(content: " No queued jobs", style: @tui.style(fg: :dark_gray))
|
|
141
291
|
])
|
|
142
|
-
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
frame.render_widget(
|
|
295
|
+
@tui.paragraph(
|
|
296
|
+
text: lines,
|
|
297
|
+
block: @tui.block(
|
|
298
|
+
title: " Queue Depth ",
|
|
299
|
+
title_style: @tui.style(fg: :cyan, modifiers: [:bold]),
|
|
300
|
+
borders: [:all],
|
|
301
|
+
border_type: :rounded,
|
|
302
|
+
border_style: @tui.style(fg: :dark_gray)
|
|
303
|
+
)
|
|
304
|
+
),
|
|
305
|
+
area
|
|
306
|
+
)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def render_summary(frame, area)
|
|
310
|
+
lines = []
|
|
311
|
+
|
|
312
|
+
# Throughput totals (24h)
|
|
313
|
+
enq_total = @stats.enqueued_per_hour&.total || 0
|
|
314
|
+
proc_total = @stats.processed_per_hour&.total || 0
|
|
315
|
+
fail_total = @stats.failed_per_hour&.total || 0
|
|
316
|
+
|
|
317
|
+
[
|
|
318
|
+
["Enqueued", enq_total, :cyan],
|
|
319
|
+
["Processed", proc_total, :green],
|
|
320
|
+
["Failed", fail_total, :red]
|
|
321
|
+
].each do |label, value, color|
|
|
322
|
+
lines << @tui.text_line(spans: [
|
|
323
|
+
@tui.text_span(content: " #{label.ljust(12)}", style: @tui.style(fg: color)),
|
|
324
|
+
@tui.text_span(content: format_number(value).rjust(10), style: @tui.style(fg: :white, modifiers: [:bold]))
|
|
325
|
+
])
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Separator
|
|
329
|
+
lines << @tui.text_line(spans: [
|
|
330
|
+
@tui.text_span(content: "", style: @tui.style(fg: :dark_gray))
|
|
331
|
+
])
|
|
332
|
+
|
|
333
|
+
# Overall totals + completion bar
|
|
334
|
+
[
|
|
335
|
+
["Total", @stats.total_jobs, :white],
|
|
336
|
+
["Completed", @stats.completed_jobs, :green]
|
|
337
|
+
].each do |label, value, color|
|
|
338
|
+
lines << @tui.text_line(spans: [
|
|
339
|
+
@tui.text_span(content: " #{label.ljust(12)}", style: @tui.style(fg: :dark_gray)),
|
|
340
|
+
@tui.text_span(content: format_number(value).rjust(10), style: @tui.style(fg: color, modifiers: [:bold]))
|
|
341
|
+
])
|
|
342
|
+
end
|
|
143
343
|
|
|
144
344
|
if @stats.total_jobs > 0
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
filled = (
|
|
148
|
-
|
|
345
|
+
ratio = @stats.completed_jobs.to_f / @stats.total_jobs
|
|
346
|
+
bar_w = 30
|
|
347
|
+
filled = (ratio * bar_w).round
|
|
348
|
+
empty_bar = bar_w - filled
|
|
149
349
|
|
|
150
350
|
lines << @tui.text_line(spans: [
|
|
151
|
-
@tui.text_span(content: "
|
|
152
|
-
@tui.text_span(content: "#{
|
|
153
|
-
@tui.text_span(content: "
|
|
351
|
+
@tui.text_span(content: " ", style: @tui.style(fg: :dark_gray)),
|
|
352
|
+
@tui.text_span(content: "#{"█" * filled}", style: @tui.style(fg: :green)),
|
|
353
|
+
@tui.text_span(content: "#{"░" * empty_bar}", style: @tui.style(fg: :dark_gray)),
|
|
354
|
+
@tui.text_span(content: " #{(ratio * 100).round(1)}%", style: @tui.style(fg: :white))
|
|
154
355
|
])
|
|
155
356
|
end
|
|
156
357
|
|
|
@@ -158,7 +359,7 @@ module SolidQueueTui
|
|
|
158
359
|
@tui.paragraph(
|
|
159
360
|
text: lines,
|
|
160
361
|
block: @tui.block(
|
|
161
|
-
title: "
|
|
362
|
+
title: " Summary ",
|
|
162
363
|
title_style: @tui.style(fg: :cyan, modifiers: [:bold]),
|
|
163
364
|
borders: [:all],
|
|
164
365
|
border_type: :rounded,
|
|
@@ -169,6 +370,13 @@ module SolidQueueTui
|
|
|
169
370
|
)
|
|
170
371
|
end
|
|
171
372
|
|
|
373
|
+
# --- Helpers ---
|
|
374
|
+
|
|
375
|
+
def to_chart_data(result)
|
|
376
|
+
return [] unless result
|
|
377
|
+
result.data.each_with_index.map { |v, i| [i.to_f, v.to_f] }
|
|
378
|
+
end
|
|
379
|
+
|
|
172
380
|
def status_line(label, value, color)
|
|
173
381
|
bar_char = value.to_i > 0 ? "●" : "○"
|
|
174
382
|
@tui.text_line(spans: [
|
|
@@ -65,8 +65,9 @@ module SolidQueueTui
|
|
|
65
65
|
{ key: "R", action: "Retry" },
|
|
66
66
|
{ key: "D", action: "Discard" },
|
|
67
67
|
{ key: "A", action: "Retry All" },
|
|
68
|
-
{ key: "/", action: "Filter" }
|
|
69
|
-
|
|
68
|
+
{ key: "/", action: "Filter" },
|
|
69
|
+
clear_filter_binding
|
|
70
|
+
].compact
|
|
70
71
|
end
|
|
71
72
|
end
|
|
72
73
|
|
|
@@ -104,7 +105,7 @@ module SolidQueueTui
|
|
|
104
105
|
in { type: :key, code: "/" }
|
|
105
106
|
enter_filter_mode
|
|
106
107
|
nil
|
|
107
|
-
in { type: :key, code: "
|
|
108
|
+
in { type: :key, code: "c" }
|
|
108
109
|
clear_filter
|
|
109
110
|
else
|
|
110
111
|
nil
|
|
@@ -54,9 +54,9 @@ module SolidQueueTui
|
|
|
54
54
|
{ key: "j/k", action: "Navigate" },
|
|
55
55
|
{ key: "Enter", action: "Detail" },
|
|
56
56
|
{ key: "/", action: "Filter" },
|
|
57
|
-
|
|
57
|
+
clear_filter_binding,
|
|
58
58
|
{ key: "G/g", action: "Bottom/Top" }
|
|
59
|
-
]
|
|
59
|
+
].compact
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
@@ -85,7 +85,7 @@ module SolidQueueTui
|
|
|
85
85
|
in { type: :key, code: "/" }
|
|
86
86
|
enter_filter_mode
|
|
87
87
|
nil
|
|
88
|
-
in { type: :key, code: "
|
|
88
|
+
in { type: :key, code: "c" }
|
|
89
89
|
clear_filter
|
|
90
90
|
else
|
|
91
91
|
nil
|
|
@@ -54,8 +54,9 @@ module SolidQueueTui
|
|
|
54
54
|
{ key: "j/k", action: "Navigate" },
|
|
55
55
|
{ key: "Enter", action: "Detail" },
|
|
56
56
|
{ key: "/", action: "Filter" },
|
|
57
|
+
clear_filter_binding,
|
|
57
58
|
{ key: "G/g", action: "Bottom/Top" }
|
|
58
|
-
]
|
|
59
|
+
].compact
|
|
59
60
|
end
|
|
60
61
|
end
|
|
61
62
|
|
|
@@ -84,7 +85,7 @@ module SolidQueueTui
|
|
|
84
85
|
in { type: :key, code: "/" }
|
|
85
86
|
enter_filter_mode
|
|
86
87
|
nil
|
|
87
|
-
in { type: :key, code: "
|
|
88
|
+
in { type: :key, code: "c" }
|
|
88
89
|
clear_filter
|
|
89
90
|
else
|
|
90
91
|
nil
|
|
@@ -101,9 +101,10 @@ module SolidQueueTui
|
|
|
101
101
|
{ key: "j/k", action: "Navigate" },
|
|
102
102
|
{ key: "Enter", action: "Detail" },
|
|
103
103
|
{ key: "/", action: "Filter" },
|
|
104
|
+
clear_filter_binding,
|
|
104
105
|
{ key: "Esc", action: "Back" },
|
|
105
106
|
{ key: "G/g", action: "Bottom/Top" }
|
|
106
|
-
]
|
|
107
|
+
].compact
|
|
107
108
|
end
|
|
108
109
|
end
|
|
109
110
|
end
|
|
@@ -233,6 +234,8 @@ module SolidQueueTui
|
|
|
233
234
|
in { type: :key, code: "/" }
|
|
234
235
|
enter_filter_mode
|
|
235
236
|
nil
|
|
237
|
+
in { type: :key, code: "c" }
|
|
238
|
+
clear_filter
|
|
236
239
|
in { type: :key, code: "esc" }
|
|
237
240
|
exit_detail_mode
|
|
238
241
|
else
|
|
@@ -67,10 +67,11 @@ module SolidQueueTui
|
|
|
67
67
|
{ key: "j/k", action: "Navigate" },
|
|
68
68
|
{ key: "Enter", action: "Detail" },
|
|
69
69
|
{ key: "/", action: "Filter" },
|
|
70
|
+
clear_filter_binding,
|
|
70
71
|
{ key: "N", action: "Run Now" },
|
|
71
72
|
{ key: "D", action: "Discard" },
|
|
72
73
|
{ key: "G/g", action: "Bottom/Top" }
|
|
73
|
-
]
|
|
74
|
+
].compact
|
|
74
75
|
end
|
|
75
76
|
end
|
|
76
77
|
|
|
@@ -101,7 +102,7 @@ module SolidQueueTui
|
|
|
101
102
|
in { type: :key, code: "/" }
|
|
102
103
|
enter_filter_mode
|
|
103
104
|
nil
|
|
104
|
-
in { type: :key, code: "
|
|
105
|
+
in { type: :key, code: "c" }
|
|
105
106
|
clear_filter
|
|
106
107
|
else
|
|
107
108
|
nil
|
data/lib/solid_queue_tui.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "solid_queue_tui/formatting_helpers"
|
|
|
5
5
|
|
|
6
6
|
# Data layer
|
|
7
7
|
require_relative "solid_queue_tui/data/stats"
|
|
8
|
+
require_relative "solid_queue_tui/data/hourly_stats_query"
|
|
8
9
|
require_relative "solid_queue_tui/data/jobs_query"
|
|
9
10
|
require_relative "solid_queue_tui/data/queues_query"
|
|
10
11
|
require_relative "solid_queue_tui/data/processes_query"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_queue_tui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shiva Reddy
|
|
@@ -84,11 +84,13 @@ description: Real-time terminal dashboard to monitor and manage Solid Queue jobs
|
|
|
84
84
|
Built with ratatui_ruby for native Rust rendering performance.
|
|
85
85
|
email:
|
|
86
86
|
executables:
|
|
87
|
+
- qtop
|
|
87
88
|
- sqtui
|
|
88
89
|
extensions: []
|
|
89
90
|
extra_rdoc_files: []
|
|
90
91
|
files:
|
|
91
92
|
- LICENSE.txt
|
|
93
|
+
- exe/qtop
|
|
92
94
|
- exe/sqtui
|
|
93
95
|
- lib/solid_queue_tui.rb
|
|
94
96
|
- lib/solid_queue_tui/actions/discard_job.rb
|
|
@@ -103,6 +105,7 @@ files:
|
|
|
103
105
|
- lib/solid_queue_tui/components/help_bar.rb
|
|
104
106
|
- lib/solid_queue_tui/components/job_table.rb
|
|
105
107
|
- lib/solid_queue_tui/data/failed_query.rb
|
|
108
|
+
- lib/solid_queue_tui/data/hourly_stats_query.rb
|
|
106
109
|
- lib/solid_queue_tui/data/jobs_query.rb
|
|
107
110
|
- lib/solid_queue_tui/data/processes_query.rb
|
|
108
111
|
- lib/solid_queue_tui/data/queues_query.rb
|