solid_queue_tui 0.1.2 → 0.1.3

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.
@@ -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
- Actions::RetryJob.call(@failed_job.id)
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
- Actions::DiscardJob.call(@failed_job.id)
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 bindings
72
- bindings = [
73
- { key: "Esc", action: "Close" },
74
- { key: "j/k", action: "Scroll" }
75
- ]
76
- if @failed_job
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 breadcrumb
86
- if @failed_job
87
- "failed:#{@failed_job.job_id}"
88
- elsif @job
89
- "jobs:#{@job.id}"
90
- else
91
- "detail"
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