solid_queue_tui 0.1.2 → 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/exe/sqtui +19 -2
- data/lib/solid_queue_tui/application.rb +37 -4
- data/lib/solid_queue_tui/cli.rb +2 -2
- data/lib/solid_queue_tui/components/job_table.rb +2 -3
- data/lib/solid_queue_tui/data/hourly_stats_query.rb +85 -0
- data/lib/solid_queue_tui/data/jobs_query.rb +31 -0
- data/lib/solid_queue_tui/data/processes_query.rb +23 -0
- data/lib/solid_queue_tui/data/stats.rb +15 -3
- data/lib/solid_queue_tui/formatting_helpers.rb +63 -0
- data/lib/solid_queue_tui/version.rb +1 -1
- data/lib/solid_queue_tui/views/blocked_view.rb +9 -77
- data/lib/solid_queue_tui/views/concerns/confirmable.rb +53 -0
- data/lib/solid_queue_tui/views/concerns/filterable.rb +4 -0
- data/lib/solid_queue_tui/views/concerns/paginatable.rb +79 -0
- data/lib/solid_queue_tui/views/dashboard_view.rb +228 -22
- data/lib/solid_queue_tui/views/failed_view.rb +49 -152
- data/lib/solid_queue_tui/views/finished_view.rb +12 -80
- data/lib/solid_queue_tui/views/in_progress_view.rb +9 -72
- data/lib/solid_queue_tui/views/job_detail_view.rb +179 -31
- data/lib/solid_queue_tui/views/processes_view.rb +2 -24
- data/lib/solid_queue_tui/views/queues_view.rb +226 -87
- data/lib/solid_queue_tui/views/recurring_tasks_view.rb +22 -69
- data/lib/solid_queue_tui/views/scheduled_view.rb +39 -142
- data/lib/solid_queue_tui.rb +4 -0
- metadata +9 -3
|
@@ -4,47 +4,21 @@ module SolidQueueTui
|
|
|
4
4
|
module Views
|
|
5
5
|
class FinishedView
|
|
6
6
|
include Filterable
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
LOAD_THRESHOLD = 10
|
|
7
|
+
include Paginatable
|
|
8
|
+
include FormattingHelpers
|
|
10
9
|
|
|
11
10
|
def initialize(tui)
|
|
12
11
|
@tui = tui
|
|
13
|
-
|
|
14
|
-
@table_state.select(0)
|
|
15
|
-
@selected_row = 0
|
|
16
|
-
@jobs = []
|
|
17
|
-
@total_count = nil
|
|
18
|
-
@all_loaded = false
|
|
12
|
+
init_pagination
|
|
19
13
|
init_filter
|
|
20
14
|
end
|
|
21
15
|
|
|
22
16
|
def update(jobs:)
|
|
23
|
-
|
|
24
|
-
@all_loaded = jobs.size < SolidQueueTui.page_size
|
|
25
|
-
@selected_row = @selected_row.clamp(0, [@jobs.size - 1, 0].max)
|
|
26
|
-
@table_state.select(@selected_row)
|
|
17
|
+
update_items(jobs)
|
|
27
18
|
end
|
|
28
19
|
|
|
29
20
|
def append(jobs:)
|
|
30
|
-
|
|
31
|
-
@all_loaded = jobs.size < SolidQueueTui.page_size
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def total_count=(count)
|
|
35
|
-
@total_count = count
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def current_offset
|
|
39
|
-
@jobs.size
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def reset_pagination!
|
|
43
|
-
@jobs = []
|
|
44
|
-
@total_count = nil
|
|
45
|
-
@all_loaded = false
|
|
46
|
-
@selected_row = 0
|
|
47
|
-
@table_state.select(0)
|
|
21
|
+
append_items(jobs)
|
|
48
22
|
end
|
|
49
23
|
|
|
50
24
|
def render(frame, area)
|
|
@@ -72,11 +46,6 @@ module SolidQueueTui
|
|
|
72
46
|
end
|
|
73
47
|
end
|
|
74
48
|
|
|
75
|
-
def selected_item
|
|
76
|
-
return nil if @jobs.empty? || @selected_row >= @jobs.size
|
|
77
|
-
@jobs[@selected_row]
|
|
78
|
-
end
|
|
79
|
-
|
|
80
49
|
def bindings
|
|
81
50
|
if filter_mode?
|
|
82
51
|
filter_bindings
|
|
@@ -85,9 +54,9 @@ module SolidQueueTui
|
|
|
85
54
|
{ key: "j/k", action: "Navigate" },
|
|
86
55
|
{ key: "Enter", action: "Detail" },
|
|
87
56
|
{ key: "/", action: "Filter" },
|
|
88
|
-
|
|
57
|
+
clear_filter_binding,
|
|
89
58
|
{ key: "G/g", action: "Bottom/Top" }
|
|
90
|
-
]
|
|
59
|
+
].compact
|
|
91
60
|
end
|
|
92
61
|
end
|
|
93
62
|
|
|
@@ -101,10 +70,6 @@ module SolidQueueTui
|
|
|
101
70
|
|
|
102
71
|
private
|
|
103
72
|
|
|
104
|
-
def needs_more?
|
|
105
|
-
!@all_loaded && @selected_row >= @jobs.size - LOAD_THRESHOLD
|
|
106
|
-
end
|
|
107
|
-
|
|
108
73
|
def handle_normal_input(event)
|
|
109
74
|
case event
|
|
110
75
|
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
@@ -120,32 +85,13 @@ module SolidQueueTui
|
|
|
120
85
|
in { type: :key, code: "/" }
|
|
121
86
|
enter_filter_mode
|
|
122
87
|
nil
|
|
123
|
-
in { type: :key, code: "
|
|
88
|
+
in { type: :key, code: "c" }
|
|
124
89
|
clear_filter
|
|
125
90
|
else
|
|
126
91
|
nil
|
|
127
92
|
end
|
|
128
93
|
end
|
|
129
94
|
|
|
130
|
-
def move_selection(delta)
|
|
131
|
-
return if @jobs.empty?
|
|
132
|
-
@selected_row = (@selected_row + delta).clamp(0, @jobs.size - 1)
|
|
133
|
-
@table_state.select(@selected_row)
|
|
134
|
-
:load_more if needs_more?
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def jump_to_top
|
|
138
|
-
@selected_row = 0
|
|
139
|
-
@table_state.select(0)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def jump_to_bottom
|
|
143
|
-
return if @jobs.empty?
|
|
144
|
-
@selected_row = @jobs.size - 1
|
|
145
|
-
@table_state.select(@selected_row)
|
|
146
|
-
return :load_more if needs_more?
|
|
147
|
-
end
|
|
148
|
-
|
|
149
95
|
def render_table(frame, area)
|
|
150
96
|
columns = [
|
|
151
97
|
{ key: :id, label: "ID", width: 8 },
|
|
@@ -156,14 +102,14 @@ module SolidQueueTui
|
|
|
156
102
|
{ key: :duration, label: "DURATION", width: 12 }
|
|
157
103
|
]
|
|
158
104
|
|
|
159
|
-
rows =
|
|
105
|
+
rows = items.map do |job|
|
|
160
106
|
{
|
|
161
107
|
id: job.id,
|
|
162
108
|
queue_name: job.queue_name,
|
|
163
109
|
class_name: job.class_name,
|
|
164
110
|
priority: job.priority,
|
|
165
111
|
finished_at: format_time(job.finished_at),
|
|
166
|
-
duration:
|
|
112
|
+
duration: job_duration(job.created_at, job.finished_at)
|
|
167
113
|
}
|
|
168
114
|
end
|
|
169
115
|
|
|
@@ -180,23 +126,9 @@ module SolidQueueTui
|
|
|
180
126
|
table.render(frame, area, @table_state)
|
|
181
127
|
end
|
|
182
128
|
|
|
183
|
-
def
|
|
184
|
-
return "n/a" unless time
|
|
185
|
-
time.strftime("%Y-%m-%d %H:%M:%S")
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def format_duration(created, finished)
|
|
129
|
+
def job_duration(created, finished)
|
|
189
130
|
return "n/a" unless created && finished
|
|
190
|
-
|
|
191
|
-
if seconds < 1
|
|
192
|
-
"<1s"
|
|
193
|
-
elsif seconds < 60
|
|
194
|
-
"#{seconds}s"
|
|
195
|
-
elsif seconds < 3600
|
|
196
|
-
"#{seconds / 60}m #{seconds % 60}s"
|
|
197
|
-
else
|
|
198
|
-
"#{seconds / 3600}h #{(seconds % 3600) / 60}m"
|
|
199
|
-
end
|
|
131
|
+
format_duration((finished - created).to_i)
|
|
200
132
|
end
|
|
201
133
|
end
|
|
202
134
|
end
|
|
@@ -4,47 +4,21 @@ module SolidQueueTui
|
|
|
4
4
|
module Views
|
|
5
5
|
class InProgressView
|
|
6
6
|
include Filterable
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
LOAD_THRESHOLD = 10
|
|
7
|
+
include Paginatable
|
|
8
|
+
include FormattingHelpers
|
|
10
9
|
|
|
11
10
|
def initialize(tui)
|
|
12
11
|
@tui = tui
|
|
13
|
-
|
|
14
|
-
@table_state.select(0)
|
|
15
|
-
@selected_row = 0
|
|
16
|
-
@jobs = []
|
|
17
|
-
@total_count = nil
|
|
18
|
-
@all_loaded = false
|
|
12
|
+
init_pagination
|
|
19
13
|
init_filter
|
|
20
14
|
end
|
|
21
15
|
|
|
22
16
|
def update(jobs:)
|
|
23
|
-
|
|
24
|
-
@all_loaded = jobs.size < SolidQueueTui.page_size
|
|
25
|
-
@selected_row = @selected_row.clamp(0, [@jobs.size - 1, 0].max)
|
|
26
|
-
@table_state.select(@selected_row)
|
|
17
|
+
update_items(jobs)
|
|
27
18
|
end
|
|
28
19
|
|
|
29
20
|
def append(jobs:)
|
|
30
|
-
|
|
31
|
-
@all_loaded = jobs.size < SolidQueueTui.page_size
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def total_count=(count)
|
|
35
|
-
@total_count = count
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def current_offset
|
|
39
|
-
@jobs.size
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def reset_pagination!
|
|
43
|
-
@jobs = []
|
|
44
|
-
@total_count = nil
|
|
45
|
-
@all_loaded = false
|
|
46
|
-
@selected_row = 0
|
|
47
|
-
@table_state.select(0)
|
|
21
|
+
append_items(jobs)
|
|
48
22
|
end
|
|
49
23
|
|
|
50
24
|
def render(frame, area)
|
|
@@ -72,11 +46,6 @@ module SolidQueueTui
|
|
|
72
46
|
end
|
|
73
47
|
end
|
|
74
48
|
|
|
75
|
-
def selected_item
|
|
76
|
-
return nil if @jobs.empty? || @selected_row >= @jobs.size
|
|
77
|
-
@jobs[@selected_row]
|
|
78
|
-
end
|
|
79
|
-
|
|
80
49
|
def bindings
|
|
81
50
|
if filter_mode?
|
|
82
51
|
filter_bindings
|
|
@@ -85,8 +54,9 @@ module SolidQueueTui
|
|
|
85
54
|
{ key: "j/k", action: "Navigate" },
|
|
86
55
|
{ key: "Enter", action: "Detail" },
|
|
87
56
|
{ key: "/", action: "Filter" },
|
|
57
|
+
clear_filter_binding,
|
|
88
58
|
{ key: "G/g", action: "Bottom/Top" }
|
|
89
|
-
]
|
|
59
|
+
].compact
|
|
90
60
|
end
|
|
91
61
|
end
|
|
92
62
|
|
|
@@ -100,10 +70,6 @@ module SolidQueueTui
|
|
|
100
70
|
|
|
101
71
|
private
|
|
102
72
|
|
|
103
|
-
def needs_more?
|
|
104
|
-
!@all_loaded && @selected_row >= @jobs.size - LOAD_THRESHOLD
|
|
105
|
-
end
|
|
106
|
-
|
|
107
73
|
def handle_normal_input(event)
|
|
108
74
|
case event
|
|
109
75
|
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
@@ -119,32 +85,13 @@ module SolidQueueTui
|
|
|
119
85
|
in { type: :key, code: "/" }
|
|
120
86
|
enter_filter_mode
|
|
121
87
|
nil
|
|
122
|
-
in { type: :key, code: "
|
|
88
|
+
in { type: :key, code: "c" }
|
|
123
89
|
clear_filter
|
|
124
90
|
else
|
|
125
91
|
nil
|
|
126
92
|
end
|
|
127
93
|
end
|
|
128
94
|
|
|
129
|
-
def move_selection(delta)
|
|
130
|
-
return if @jobs.empty?
|
|
131
|
-
@selected_row = (@selected_row + delta).clamp(0, @jobs.size - 1)
|
|
132
|
-
@table_state.select(@selected_row)
|
|
133
|
-
:load_more if needs_more?
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def jump_to_top
|
|
137
|
-
@selected_row = 0
|
|
138
|
-
@table_state.select(0)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def jump_to_bottom
|
|
142
|
-
return if @jobs.empty?
|
|
143
|
-
@selected_row = @jobs.size - 1
|
|
144
|
-
@table_state.select(@selected_row)
|
|
145
|
-
return :load_more if needs_more?
|
|
146
|
-
end
|
|
147
|
-
|
|
148
95
|
def render_table(frame, area)
|
|
149
96
|
columns = [
|
|
150
97
|
{ key: :id, label: "ID", width: 8 },
|
|
@@ -155,7 +102,7 @@ module SolidQueueTui
|
|
|
155
102
|
{ key: :started_at, label: "STARTED", width: 12 }
|
|
156
103
|
]
|
|
157
104
|
|
|
158
|
-
rows =
|
|
105
|
+
rows = items.map do |job|
|
|
159
106
|
{
|
|
160
107
|
id: job.id,
|
|
161
108
|
queue_name: job.queue_name,
|
|
@@ -179,16 +126,6 @@ module SolidQueueTui
|
|
|
179
126
|
table.render(frame, area, @table_state)
|
|
180
127
|
end
|
|
181
128
|
|
|
182
|
-
def time_ago(time)
|
|
183
|
-
return "n/a" unless time
|
|
184
|
-
seconds = (Time.now.utc - time).to_i
|
|
185
|
-
case seconds
|
|
186
|
-
when 0..59 then "#{seconds}s ago"
|
|
187
|
-
when 60..3599 then "#{seconds / 60}m ago"
|
|
188
|
-
when 3600..86399 then "#{seconds / 3600}h ago"
|
|
189
|
-
else "#{seconds / 86400}d ago"
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
129
|
end
|
|
193
130
|
end
|
|
194
131
|
end
|
|
@@ -3,16 +3,24 @@
|
|
|
3
3
|
module SolidQueueTui
|
|
4
4
|
module Views
|
|
5
5
|
class JobDetailView
|
|
6
|
+
include Confirmable
|
|
7
|
+
include FormattingHelpers
|
|
8
|
+
|
|
6
9
|
def initialize(tui)
|
|
7
10
|
@tui = tui
|
|
8
11
|
@job = nil
|
|
9
12
|
@failed_job = nil
|
|
13
|
+
@process = nil
|
|
14
|
+
@running_jobs = []
|
|
10
15
|
@scroll_offset = 0
|
|
16
|
+
init_confirm
|
|
11
17
|
end
|
|
12
18
|
|
|
13
|
-
def show(job: nil, failed_job: nil)
|
|
19
|
+
def show(job: nil, failed_job: nil, process: nil, running_jobs: [])
|
|
14
20
|
@job = job
|
|
15
21
|
@failed_job = failed_job
|
|
22
|
+
@process = process
|
|
23
|
+
@running_jobs = running_jobs
|
|
16
24
|
@scroll_offset = 0
|
|
17
25
|
@active = true
|
|
18
26
|
end
|
|
@@ -21,6 +29,9 @@ module SolidQueueTui
|
|
|
21
29
|
@active = false
|
|
22
30
|
@job = nil
|
|
23
31
|
@failed_job = nil
|
|
32
|
+
@process = nil
|
|
33
|
+
@running_jobs = []
|
|
34
|
+
@confirm_action = nil
|
|
24
35
|
end
|
|
25
36
|
|
|
26
37
|
def active? = @active
|
|
@@ -35,12 +46,60 @@ module SolidQueueTui
|
|
|
35
46
|
|
|
36
47
|
if @failed_job
|
|
37
48
|
render_failed_detail(frame, inner)
|
|
49
|
+
elsif @process
|
|
50
|
+
render_process_detail(frame, inner)
|
|
38
51
|
elsif @job
|
|
39
52
|
render_job_detail(frame, inner)
|
|
40
53
|
end
|
|
54
|
+
|
|
55
|
+
render_confirm_popup(frame, area) if confirm_mode?
|
|
41
56
|
end
|
|
42
57
|
|
|
43
58
|
def handle_input(event)
|
|
59
|
+
if confirm_mode?
|
|
60
|
+
handle_confirm_input(event)
|
|
61
|
+
else
|
|
62
|
+
handle_normal_input(event)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def bindings
|
|
67
|
+
if confirm_mode?
|
|
68
|
+
confirm_bindings
|
|
69
|
+
else
|
|
70
|
+
bindings = [
|
|
71
|
+
{ key: "Esc", action: "Close" },
|
|
72
|
+
{ key: "j/k", action: "Scroll" }
|
|
73
|
+
]
|
|
74
|
+
if @failed_job
|
|
75
|
+
bindings += [
|
|
76
|
+
{ key: "R", action: "Retry" },
|
|
77
|
+
{ key: "D", action: "Discard" }
|
|
78
|
+
]
|
|
79
|
+
end
|
|
80
|
+
bindings
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def capturing_input?
|
|
85
|
+
confirm_mode?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def breadcrumb
|
|
89
|
+
if @failed_job
|
|
90
|
+
"failed:#{@failed_job.job_id}"
|
|
91
|
+
elsif @process
|
|
92
|
+
"process:#{@process.id}"
|
|
93
|
+
elsif @job
|
|
94
|
+
"jobs:#{@job.id}"
|
|
95
|
+
else
|
|
96
|
+
"detail"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def handle_normal_input(event)
|
|
44
103
|
case event
|
|
45
104
|
in { type: :key, code: "esc" } | { type: :key, code: "q" }
|
|
46
105
|
hide
|
|
@@ -52,48 +111,38 @@ module SolidQueueTui
|
|
|
52
111
|
@scroll_offset += 1
|
|
53
112
|
nil
|
|
54
113
|
in { type: :key, code: "R" }
|
|
55
|
-
if @failed_job
|
|
56
|
-
|
|
57
|
-
hide
|
|
58
|
-
:refresh
|
|
59
|
-
end
|
|
114
|
+
@confirm_action = :retry if @failed_job
|
|
115
|
+
nil
|
|
60
116
|
in { type: :key, code: "D" }
|
|
61
|
-
if @failed_job
|
|
62
|
-
|
|
63
|
-
hide
|
|
64
|
-
:refresh
|
|
65
|
-
end
|
|
117
|
+
@confirm_action = :discard if @failed_job
|
|
118
|
+
nil
|
|
66
119
|
else
|
|
67
120
|
nil
|
|
68
121
|
end
|
|
69
122
|
end
|
|
70
123
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
bindings += [
|
|
78
|
-
{ key: "R", action: "Retry" },
|
|
79
|
-
{ key: "D", action: "Discard" }
|
|
80
|
-
]
|
|
124
|
+
def confirm_message
|
|
125
|
+
case @confirm_action
|
|
126
|
+
when :retry
|
|
127
|
+
"Retry job ##{@failed_job&.job_id} (#{@failed_job&.class_name})? [y/n]"
|
|
128
|
+
when :discard
|
|
129
|
+
"Discard job ##{@failed_job&.job_id} (#{@failed_job&.class_name})? This cannot be undone. [y/n]"
|
|
81
130
|
end
|
|
82
|
-
bindings
|
|
83
131
|
end
|
|
84
132
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
133
|
+
def execute_confirm_action(action)
|
|
134
|
+
case action
|
|
135
|
+
when :retry
|
|
136
|
+
Actions::RetryJob.call(@failed_job.id)
|
|
137
|
+
hide
|
|
138
|
+
:refresh
|
|
139
|
+
when :discard
|
|
140
|
+
Actions::DiscardJob.call(@failed_job.id)
|
|
141
|
+
hide
|
|
142
|
+
:refresh
|
|
92
143
|
end
|
|
93
144
|
end
|
|
94
145
|
|
|
95
|
-
private
|
|
96
|
-
|
|
97
146
|
def render_failed_detail(frame, area)
|
|
98
147
|
lines = []
|
|
99
148
|
|
|
@@ -202,6 +251,104 @@ module SolidQueueTui
|
|
|
202
251
|
)
|
|
203
252
|
end
|
|
204
253
|
|
|
254
|
+
def render_process_detail(frame, area)
|
|
255
|
+
lines = []
|
|
256
|
+
|
|
257
|
+
# Section 1: Process Info
|
|
258
|
+
lines << section_header("Process Info")
|
|
259
|
+
lines << detail_line("ID", @process.id.to_s)
|
|
260
|
+
lines << detail_line("Kind", @process.kind)
|
|
261
|
+
lines << detail_line("PID", @process.pid.to_s)
|
|
262
|
+
lines << detail_line("Hostname", @process.hostname || "n/a")
|
|
263
|
+
lines << detail_line("Name", @process.name || "n/a")
|
|
264
|
+
lines << empty_line
|
|
265
|
+
|
|
266
|
+
# Section 2: Status
|
|
267
|
+
lines << section_header("Status")
|
|
268
|
+
alive = @process.alive?
|
|
269
|
+
lines << @tui.text_line(spans: [
|
|
270
|
+
@tui.text_span(content: " #{"Status".ljust(16)}", style: @tui.style(fg: :dark_gray)),
|
|
271
|
+
@tui.text_span(content: alive ? "alive" : "dead",
|
|
272
|
+
style: @tui.style(fg: alive ? :green : :red, modifiers: [:bold]))
|
|
273
|
+
])
|
|
274
|
+
lines << detail_line("Last Heartbeat", time_ago(@process.last_heartbeat_at))
|
|
275
|
+
lines << detail_line("Uptime", format_duration(@process.uptime))
|
|
276
|
+
lines << detail_line("Created At", format_time(@process.created_at))
|
|
277
|
+
lines << empty_line
|
|
278
|
+
|
|
279
|
+
# Section 3: Configuration
|
|
280
|
+
lines << section_header("Configuration")
|
|
281
|
+
queues_str = Array(@process.queues).join(", ")
|
|
282
|
+
lines << detail_line("Queues", queues_str.empty? ? "n/a" : queues_str)
|
|
283
|
+
lines << detail_line("Threads", (@process.thread_count || "n/a").to_s)
|
|
284
|
+
if @process.metadata.is_a?(Hash) && @process.metadata["polling_interval"]
|
|
285
|
+
lines << detail_line("Poll Interval", "#{@process.metadata["polling_interval"]}s")
|
|
286
|
+
end
|
|
287
|
+
lines << detail_line("Supervisor ID", (@process.supervisor_id || "n/a").to_s)
|
|
288
|
+
lines << empty_line
|
|
289
|
+
|
|
290
|
+
# Section 4: Running Jobs (Workers only)
|
|
291
|
+
if @process.kind == "Worker"
|
|
292
|
+
lines << section_header("Running Jobs (#{@running_jobs.size})")
|
|
293
|
+
if @running_jobs.empty?
|
|
294
|
+
lines << @tui.text_line(spans: [
|
|
295
|
+
@tui.text_span(content: " Idle — no running jobs", style: @tui.style(fg: :dark_gray))
|
|
296
|
+
])
|
|
297
|
+
else
|
|
298
|
+
lines << @tui.text_line(spans: [
|
|
299
|
+
@tui.text_span(
|
|
300
|
+
content: " #{"ID".ljust(8)}#{"CLASS".ljust(30)}#{"QUEUE".ljust(16)}STARTED",
|
|
301
|
+
style: @tui.style(fg: :cyan, modifiers: [:bold])
|
|
302
|
+
)
|
|
303
|
+
])
|
|
304
|
+
@running_jobs.each do |rj|
|
|
305
|
+
lines << @tui.text_line(spans: [
|
|
306
|
+
@tui.text_span(content: " #{rj.job_id.to_s.ljust(8)}", style: @tui.style(fg: :white)),
|
|
307
|
+
@tui.text_span(content: rj.class_name.to_s.ljust(30), style: @tui.style(fg: :yellow)),
|
|
308
|
+
@tui.text_span(content: rj.queue_name.to_s.ljust(16), style: @tui.style(fg: :white)),
|
|
309
|
+
@tui.text_span(content: time_ago(rj.started_at), style: @tui.style(fg: :dark_gray))
|
|
310
|
+
])
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
lines << empty_line
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Section 5: Raw Metadata
|
|
317
|
+
if @process.metadata.is_a?(Hash) && !@process.metadata.empty?
|
|
318
|
+
lines << section_header("Raw Metadata")
|
|
319
|
+
meta_str = JSON.pretty_generate(@process.metadata) rescue @process.metadata.to_s
|
|
320
|
+
meta_str.split("\n").each do |meta_line|
|
|
321
|
+
lines << @tui.text_line(spans: [
|
|
322
|
+
@tui.text_span(content: " #{meta_line}", style: @tui.style(fg: :white))
|
|
323
|
+
])
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
visible_lines = lines.drop(@scroll_offset)
|
|
328
|
+
|
|
329
|
+
border_color = @process.alive? ? :green : :red
|
|
330
|
+
title_text = " #{@process.kind} ##{@process.id} — PID: #{@process.pid} "
|
|
331
|
+
|
|
332
|
+
frame.render_widget(
|
|
333
|
+
@tui.paragraph(
|
|
334
|
+
text: visible_lines,
|
|
335
|
+
block: @tui.block(
|
|
336
|
+
title: title_text,
|
|
337
|
+
title_style: @tui.style(fg: border_color, modifiers: [:bold]),
|
|
338
|
+
titles: [
|
|
339
|
+
{ content: " Esc:Close j/k:Scroll ",
|
|
340
|
+
position: :bottom, alignment: :right }
|
|
341
|
+
],
|
|
342
|
+
borders: [:all],
|
|
343
|
+
border_type: :rounded,
|
|
344
|
+
border_style: @tui.style(fg: border_color),
|
|
345
|
+
style: @tui.style(fg: :white)
|
|
346
|
+
)
|
|
347
|
+
),
|
|
348
|
+
area
|
|
349
|
+
)
|
|
350
|
+
end
|
|
351
|
+
|
|
205
352
|
def section_header(title)
|
|
206
353
|
@tui.text_line(spans: [
|
|
207
354
|
@tui.text_span(content: " ── #{title} ", style: @tui.style(fg: :cyan, modifiers: [:bold]))
|
|
@@ -221,6 +368,7 @@ module SolidQueueTui
|
|
|
221
368
|
])
|
|
222
369
|
end
|
|
223
370
|
|
|
371
|
+
# Override: includes UTC suffix for detail view precision
|
|
224
372
|
def format_time(time)
|
|
225
373
|
return "n/a" unless time
|
|
226
374
|
time.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module SolidQueueTui
|
|
4
4
|
module Views
|
|
5
5
|
class ProcessesView
|
|
6
|
+
include FormattingHelpers
|
|
7
|
+
|
|
6
8
|
KIND_COLORS = {
|
|
7
9
|
"Worker" => :green,
|
|
8
10
|
"Dispatcher" => :yellow,
|
|
@@ -113,30 +115,6 @@ module SolidQueueTui
|
|
|
113
115
|
@table_state.select(@selected_row)
|
|
114
116
|
end
|
|
115
117
|
|
|
116
|
-
def time_ago(time)
|
|
117
|
-
return "n/a" unless time
|
|
118
|
-
seconds = (Time.now.utc - time).to_i
|
|
119
|
-
case seconds
|
|
120
|
-
when 0..59 then "#{seconds}s ago"
|
|
121
|
-
when 60..3599 then "#{seconds / 60}m ago"
|
|
122
|
-
when 3600..86399 then "#{seconds / 3600}h ago"
|
|
123
|
-
else "#{seconds / 86400}d ago"
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def format_duration(seconds)
|
|
128
|
-
return "n/a" unless seconds
|
|
129
|
-
seconds = seconds.to_i
|
|
130
|
-
if seconds < 60
|
|
131
|
-
"#{seconds}s"
|
|
132
|
-
elsif seconds < 3600
|
|
133
|
-
"#{seconds / 60}m #{seconds % 60}s"
|
|
134
|
-
elsif seconds < 86400
|
|
135
|
-
"#{seconds / 3600}h #{(seconds % 3600) / 60}m"
|
|
136
|
-
else
|
|
137
|
-
"#{seconds / 86400}d #{(seconds % 86400) / 3600}h"
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
118
|
end
|
|
141
119
|
end
|
|
142
120
|
end
|