cosmonats 0.1.4 → 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/README.md +8 -7
- data/lib/cosmo/api/busy.rb +66 -0
- data/lib/cosmo/api/counter.rb +70 -0
- data/lib/cosmo/api/job.rb +46 -0
- data/lib/cosmo/api/kv.rb +63 -0
- data/lib/cosmo/api/stats.rb +44 -0
- data/lib/cosmo/api/stream.rb +110 -0
- data/lib/cosmo/api.rb +11 -0
- data/lib/cosmo/cli.rb +6 -4
- data/lib/cosmo/client.rb +33 -2
- data/lib/cosmo/config.rb +8 -6
- data/lib/cosmo/defaults.yml +31 -30
- data/lib/cosmo/job/processor.rb +52 -18
- data/lib/cosmo/job.rb +1 -1
- data/lib/cosmo/logger.rb +4 -0
- data/lib/cosmo/processor.rb +1 -1
- data/lib/cosmo/utils/overrides.rb +15 -0
- data/lib/cosmo/utils/warnings.rb +17 -0
- data/lib/cosmo/utils.rb +14 -0
- data/lib/cosmo/version.rb +1 -1
- data/lib/cosmo/web/assets/app.css +431 -0
- data/lib/cosmo/web/assets/htmx.2.0.8.min.js.gz +0 -0
- data/lib/cosmo/web/context.rb +28 -0
- data/lib/cosmo/web/controllers/actions.rb +16 -0
- data/lib/cosmo/web/controllers/application.rb +43 -0
- data/lib/cosmo/web/controllers/jobs.rb +97 -0
- data/lib/cosmo/web/controllers/streams.rb +44 -0
- data/lib/cosmo/web/helpers/application.rb +76 -0
- data/lib/cosmo/web/renderer.rb +58 -0
- data/lib/cosmo/web/views/actions/index.erb +7 -0
- data/lib/cosmo/web/views/jobs/_busy.erb +50 -0
- data/lib/cosmo/web/views/jobs/_dead.erb +65 -0
- data/lib/cosmo/web/views/jobs/_enqueued.erb +60 -0
- data/lib/cosmo/web/views/jobs/_scheduled.erb +49 -0
- data/lib/cosmo/web/views/jobs/_stats.erb +69 -0
- data/lib/cosmo/web/views/jobs/busy.erb +16 -0
- data/lib/cosmo/web/views/jobs/dead.erb +17 -0
- data/lib/cosmo/web/views/jobs/enqueued.erb +16 -0
- data/lib/cosmo/web/views/jobs/index.erb +12 -0
- data/lib/cosmo/web/views/jobs/scheduled.erb +17 -0
- data/lib/cosmo/web/views/layout.erb +33 -0
- data/lib/cosmo/web/views/streams/_info.erb +89 -0
- data/lib/cosmo/web/views/streams/_table.erb +42 -0
- data/lib/cosmo/web/views/streams/index.erb +11 -0
- data/lib/cosmo/web/views/streams/info.erb +11 -0
- data/lib/cosmo/web.rb +66 -0
- data/lib/cosmo.rb +2 -7
- data/sig/cosmo/api/busy.rbs +35 -0
- data/sig/cosmo/api/counter.rbs +34 -0
- data/sig/cosmo/api/job.rbs +31 -0
- data/sig/cosmo/api/kv.rbs +30 -0
- data/sig/cosmo/api/stats.rbs +21 -0
- data/sig/cosmo/api/stream.rbs +44 -0
- data/sig/cosmo/client.rbs +13 -3
- metadata +58 -2
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cosmo/web/controllers/application"
|
|
4
|
+
|
|
5
|
+
module Cosmo
|
|
6
|
+
class Web
|
|
7
|
+
module Controllers
|
|
8
|
+
class Actions < Application
|
|
9
|
+
def index
|
|
10
|
+
content_for :title, "Actions"
|
|
11
|
+
ok render("actions/index", layout: true)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cosmo
|
|
4
|
+
class Web
|
|
5
|
+
module Controllers
|
|
6
|
+
class Application
|
|
7
|
+
include Renderer
|
|
8
|
+
|
|
9
|
+
def initialize(request)
|
|
10
|
+
@request = request
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def content_for(name, content)
|
|
14
|
+
@content_for ||= {}
|
|
15
|
+
@content_for[name] = content
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render(template, locals = nil, layout: false)
|
|
19
|
+
defaults = { request: @request }
|
|
20
|
+
locals = Hash(locals).merge(defaults)
|
|
21
|
+
view = erb(template, locals)
|
|
22
|
+
return view unless layout
|
|
23
|
+
|
|
24
|
+
@content_for ||= {}
|
|
25
|
+
@content_for[:view] = view
|
|
26
|
+
erb("layout", defaults, @content_for)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def params
|
|
30
|
+
@request.params
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def path
|
|
34
|
+
@request.path
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def hx_request?
|
|
38
|
+
@request.get_header("HTTP_HX_REQUEST") == "true"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cosmo/web/controllers/application"
|
|
4
|
+
|
|
5
|
+
module Cosmo
|
|
6
|
+
class Web
|
|
7
|
+
module Controllers
|
|
8
|
+
class Jobs < Application
|
|
9
|
+
def index
|
|
10
|
+
content_for :title, "Jobs"
|
|
11
|
+
ok render("jobs/index", layout: true)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def busy
|
|
15
|
+
return _busy if hx_request?
|
|
16
|
+
|
|
17
|
+
content_for :title, "Busy Jobs"
|
|
18
|
+
ok render("jobs/busy", layout: true)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def enqueued
|
|
22
|
+
return _enqueued if hx_request?
|
|
23
|
+
|
|
24
|
+
content_for :title, "Enqueued Jobs"
|
|
25
|
+
stream_name, _stream_names = streams
|
|
26
|
+
ok render("jobs/enqueued", { stream_name: }, layout: true)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def scheduled
|
|
30
|
+
return _scheduled if hx_request?
|
|
31
|
+
|
|
32
|
+
content_for :title, "Scheduled Jobs"
|
|
33
|
+
ok render("jobs/scheduled", layout: true)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def dead
|
|
37
|
+
return _dead if hx_request?
|
|
38
|
+
|
|
39
|
+
content_for :title, "Dead Jobs"
|
|
40
|
+
ok render("jobs/dead", layout: true)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def retry
|
|
44
|
+
seq = path.split("/").last.to_i
|
|
45
|
+
stream = API::Stream.new("dead")
|
|
46
|
+
stream.retry(seq)
|
|
47
|
+
ok
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def delete
|
|
51
|
+
seq = path.split("/").last.to_i
|
|
52
|
+
stream = API::Stream.new("dead")
|
|
53
|
+
stream.delete(seq)
|
|
54
|
+
ok
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def _scheduled
|
|
58
|
+
stream = API::Stream.new("scheduled")
|
|
59
|
+
jobs = stream.messages(page: params["page"], limit: params["limit"])
|
|
60
|
+
ok render("jobs/_scheduled", { jobs: jobs, total: stream.total })
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def _dead
|
|
64
|
+
stream = API::Stream.new("dead")
|
|
65
|
+
jobs = stream.messages(page: params["page"], limit: params["limit"])
|
|
66
|
+
ok render("jobs/_dead", { jobs: jobs, total: stream.total })
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def _busy
|
|
70
|
+
limit = (limit || 25).to_i
|
|
71
|
+
jobs = API::Busy.instance.list(limit:)
|
|
72
|
+
ok render("jobs/_busy", { jobs: jobs, total: API::Busy.instance.size })
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def _enqueued
|
|
76
|
+
stream_name, stream_names = streams
|
|
77
|
+
stream = API::Stream.new(stream_name)
|
|
78
|
+
jobs = stream.messages(page: params["page"], limit: params["limit"])
|
|
79
|
+
|
|
80
|
+
ok render("jobs/_enqueued", { jobs:, total: stream.total, stream_name:, stream_names: })
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def _stats
|
|
84
|
+
ok render("jobs/_stats", API::Stats.summary)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def streams
|
|
90
|
+
stream_names = API::Stream.jobs.map(&:name)
|
|
91
|
+
stream_name = params.fetch("stream_name", stream_names.first)
|
|
92
|
+
[stream_name, stream_names]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cosmo/web/controllers/application"
|
|
4
|
+
|
|
5
|
+
module Cosmo
|
|
6
|
+
class Web
|
|
7
|
+
module Controllers
|
|
8
|
+
class Streams < Application
|
|
9
|
+
def index
|
|
10
|
+
return _table if hx_request?
|
|
11
|
+
|
|
12
|
+
content_for :title, "Streams"
|
|
13
|
+
ok render("streams/index", layout: true)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def info
|
|
17
|
+
name = Rack::Utils.unescape(@request.params["name"])
|
|
18
|
+
return _info if hx_request?
|
|
19
|
+
|
|
20
|
+
content_for :title, "Streams"
|
|
21
|
+
ok render("streams/info", { name: name }, layout: true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def _table
|
|
25
|
+
streams = API::Stream.all.map do |stream|
|
|
26
|
+
state, config = stream.info.values
|
|
27
|
+
{ name: stream.name, messages: state.messages, bytes: state.bytes,
|
|
28
|
+
first_seq: state.first_seq, last_seq: state.last_seq,
|
|
29
|
+
consumer_count: state.consumer_count,
|
|
30
|
+
subjects: config.subjects }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
ok render("streams/_table", { streams: streams })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def _info
|
|
37
|
+
name = Rack::Utils.unescape(@request.params["name"])
|
|
38
|
+
state = API::Stream.new(name).info.merge(name:)
|
|
39
|
+
ok render("streams/_info", state)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cosmo/web/renderer"
|
|
4
|
+
|
|
5
|
+
module Cosmo
|
|
6
|
+
class Web
|
|
7
|
+
module Helpers
|
|
8
|
+
module Application
|
|
9
|
+
include Renderer
|
|
10
|
+
|
|
11
|
+
def format_bytes(bytes)
|
|
12
|
+
b = bytes.to_i
|
|
13
|
+
return "0 B" if b.zero?
|
|
14
|
+
|
|
15
|
+
sizes = %w[B KB MB GB TB]
|
|
16
|
+
i = [(Math.log(b) / Math.log(1024)).floor, sizes.size - 1].min
|
|
17
|
+
"#{(b / (1024.0**i)).round(2)} #{sizes[i]}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def format_numbers(num)
|
|
21
|
+
num.to_s.reverse.gsub(/(\d{3})(?=\d)/, "\\1,").reverse
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def format_timestamp(value)
|
|
25
|
+
return "N/A" unless value
|
|
26
|
+
|
|
27
|
+
Time.at(value.to_f).strftime("%Y-%m-%d %H:%M:%S")
|
|
28
|
+
rescue StandardError
|
|
29
|
+
value.to_s
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def elapsed(value)
|
|
33
|
+
elapsed = Time.now.to_i - value.to_i
|
|
34
|
+
|
|
35
|
+
if elapsed < 60
|
|
36
|
+
"#{elapsed}s"
|
|
37
|
+
elsif elapsed < 3600
|
|
38
|
+
"#{elapsed / 60}m #{elapsed % 60}s"
|
|
39
|
+
else
|
|
40
|
+
"#{elapsed / 3600}h #{(elapsed % 3600) / 60}m"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def time_until(value)
|
|
45
|
+
return "N/A" unless value
|
|
46
|
+
|
|
47
|
+
diff = value.to_f - Time.now.to_f
|
|
48
|
+
return "Ready" if diff <= 0
|
|
49
|
+
return "#{diff.to_i}s" if diff < 60
|
|
50
|
+
return "#{(diff / 60).to_i}m" if diff < 3_600
|
|
51
|
+
return "#{(diff / 3_600).to_i}h" if diff < 86_400
|
|
52
|
+
|
|
53
|
+
"#{(diff / 86_400).to_i}d"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def h(value)
|
|
57
|
+
Rack::Utils.escape_html(value.to_s)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def u(value)
|
|
61
|
+
Rack::Utils.escape(value.to_s)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def current_page?(path)
|
|
65
|
+
request_path = @request.path_info
|
|
66
|
+
request_path = "/" if request_path.empty?
|
|
67
|
+
request_path == url_for(path)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def referrer?(path)
|
|
71
|
+
URI(@request.referrer).path == path
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cosmo
|
|
4
|
+
class Web
|
|
5
|
+
module Renderer
|
|
6
|
+
ASSETS_ROOT = File.expand_path("assets", __dir__).freeze
|
|
7
|
+
VIEWS_ROOT = File.expand_path("views", __dir__).freeze
|
|
8
|
+
|
|
9
|
+
def redirect_to(url, status = 302)
|
|
10
|
+
[status, { "location" => url_for(url) }, []]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def serve(filename, content_type, headers = nil)
|
|
14
|
+
path = File.join(ASSETS_ROOT, filename)
|
|
15
|
+
return not_found unless File.exist?(path)
|
|
16
|
+
|
|
17
|
+
respond_with 200, body: File.read(path), headers: { "content-type" => content_type }.merge(headers || {})
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ok(body = "")
|
|
21
|
+
headers = { "content-type" => "text/html; charset=utf-8" }
|
|
22
|
+
respond_with 200, body:, headers:
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def no_content
|
|
26
|
+
respond_with 204, body: ""
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def not_found
|
|
30
|
+
body = "<div class='alert alert-danger'>404 — Not Found</div>"
|
|
31
|
+
headers = { "content-type" => "text/html; charset=utf-8" }
|
|
32
|
+
respond_with 404, body:, headers:
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Prepend the mount prefix to an internal route.
|
|
36
|
+
# url_for("/jobs") # => "/admin/cosmo/jobs" (mounted)
|
|
37
|
+
# url_for("/jobs") # => "/jobs" (standalone)
|
|
38
|
+
def url_for(path, params = nil)
|
|
39
|
+
url = "#{@request.script_name}#{path}"
|
|
40
|
+
url = "#{url}?#{params.to_a.map { _1.join("=") }.join("&")}" if params
|
|
41
|
+
url
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def respond_with(status, headers: nil, body: "")
|
|
47
|
+
[status, Hash(headers), [body]]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def erb(path, locals, content_for = nil)
|
|
51
|
+
path = File.join(VIEWS_ROOT, "#{path}.erb")
|
|
52
|
+
erb = ERB.new(File.read(path), trim_mode: "-")
|
|
53
|
+
context = Context.new(locals, content_for)
|
|
54
|
+
erb.result(context.binding)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<div class="pending">
|
|
2
|
+
<span></span>
|
|
3
|
+
<span class="text-muted"><%= @total %> job(s) running</span>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<% if @jobs.empty? -%>
|
|
7
|
+
<div class="alert alert-success">No jobs are currently running</div>
|
|
8
|
+
<% else -%>
|
|
9
|
+
<div class="table-container">
|
|
10
|
+
<table>
|
|
11
|
+
<thead>
|
|
12
|
+
<tr>
|
|
13
|
+
<th>Job</th>
|
|
14
|
+
<th>JID[Args]</th>
|
|
15
|
+
<th>Stream</th>
|
|
16
|
+
<th>Worker</th>
|
|
17
|
+
<th>Running for</th>
|
|
18
|
+
</tr>
|
|
19
|
+
</thead>
|
|
20
|
+
<tbody>
|
|
21
|
+
<% @jobs.each do |job| -%>
|
|
22
|
+
<tr>
|
|
23
|
+
<td>
|
|
24
|
+
<div class="job-class">
|
|
25
|
+
<%= h(job.dig(:data, :class).to_s) %>
|
|
26
|
+
</div>
|
|
27
|
+
</td>
|
|
28
|
+
<td>
|
|
29
|
+
<div class="job-id">
|
|
30
|
+
<details>
|
|
31
|
+
<summary>
|
|
32
|
+
<code><%= h(job.dig(:data, :jid)) %></code>
|
|
33
|
+
</summary>
|
|
34
|
+
<code><%= h(JSON.pretty_generate(job.dig(:data, :args))) %></code>
|
|
35
|
+
</details>
|
|
36
|
+
</div>
|
|
37
|
+
</td>
|
|
38
|
+
<td>
|
|
39
|
+
<code><%= h(job[:stream].to_s) %></code>
|
|
40
|
+
</td>
|
|
41
|
+
<td><code style="font-size: var(--font-size-small);"><%= h(job[:worker].to_s) %></code></td>
|
|
42
|
+
<td>
|
|
43
|
+
<%= elapsed(job[:started_at]) %>
|
|
44
|
+
</td>
|
|
45
|
+
</tr>
|
|
46
|
+
<% end -%>
|
|
47
|
+
</tbody>
|
|
48
|
+
</table>
|
|
49
|
+
</div>
|
|
50
|
+
<% end -%>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<% if @jobs.empty? -%>
|
|
2
|
+
<div class="alert alert-success">No dead jobs found</div>
|
|
3
|
+
<% else -%>
|
|
4
|
+
<div class="table-container">
|
|
5
|
+
<table>
|
|
6
|
+
<thead>
|
|
7
|
+
<tr>
|
|
8
|
+
<th>Job</th>
|
|
9
|
+
<th>JID[Args]</th>
|
|
10
|
+
<th>Stream</th>
|
|
11
|
+
<th>Error</th>
|
|
12
|
+
<th>Failed</th>
|
|
13
|
+
<th>Actions</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody>
|
|
17
|
+
<% @jobs.each do |job| -%>
|
|
18
|
+
<tr class="dead-row" id="dead-row-<%= job.seq %>">
|
|
19
|
+
<td>
|
|
20
|
+
<div class="job-class">
|
|
21
|
+
<%= h(job.data[:class].to_s) %>
|
|
22
|
+
</div>
|
|
23
|
+
</td>
|
|
24
|
+
<td>
|
|
25
|
+
<div class="job-id">
|
|
26
|
+
<details>
|
|
27
|
+
<summary>
|
|
28
|
+
<code><%= h(job.data[:jid].to_s) %></code>
|
|
29
|
+
</summary>
|
|
30
|
+
<code><%= h((JSON.pretty_generate(job.data[:args]) rescue job.data[:args].to_s)) %></code>
|
|
31
|
+
</details>
|
|
32
|
+
</div>
|
|
33
|
+
</td>
|
|
34
|
+
<td>
|
|
35
|
+
<code><%= h(job.stream) %></code>
|
|
36
|
+
</td>
|
|
37
|
+
<td style="max-width: 400px;">
|
|
38
|
+
<% if job.data[:error] -%>
|
|
39
|
+
<div class="error-message">
|
|
40
|
+
<strong><%= h(job.data[:error].to_s.split(":").first || "Error") %></strong>
|
|
41
|
+
<p><%= h(job.data[:error]) %></p>
|
|
42
|
+
</div>
|
|
43
|
+
<% end -%>
|
|
44
|
+
</td>
|
|
45
|
+
<td style="white-space: nowrap;">
|
|
46
|
+
<time><%= h(job.timestamp) %></time>
|
|
47
|
+
</td>
|
|
48
|
+
<td>
|
|
49
|
+
<a hx-patch="<%= url_for("/jobs/retry/#{job.seq}") %>"
|
|
50
|
+
hx-target="#dead-row-<%= job.seq %>"
|
|
51
|
+
hx-swap="innerHTML"
|
|
52
|
+
class="btn btn-primary"
|
|
53
|
+
style="width: 100%; text-align: center; margin-bottom: 3px">↻ Retry</a>
|
|
54
|
+
<a hx-delete="<%= url_for("/jobs/delete/#{job.seq}") %>"
|
|
55
|
+
hx-target="#dead-row-<%= job.seq %>"
|
|
56
|
+
hx-swap="innerHTML"
|
|
57
|
+
class="btn btn-danger"
|
|
58
|
+
style="width: 100%; text-align: center; margin-bottom: 3px">✕ Delete</a>
|
|
59
|
+
</td>
|
|
60
|
+
</tr>
|
|
61
|
+
<% end -%>
|
|
62
|
+
</tbody>
|
|
63
|
+
</table>
|
|
64
|
+
</div>
|
|
65
|
+
<% end -%>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<div class="nav">
|
|
2
|
+
<% @stream_names.each do |stream_name| -%>
|
|
3
|
+
<button hx-get="<%= url_for('/jobs/enqueued', { stream_name: }) %>"
|
|
4
|
+
hx-target="#content"
|
|
5
|
+
hx-swap="innerHTML"
|
|
6
|
+
hx-push-url="true"
|
|
7
|
+
class="btn <%= "btn-primary" if @stream_name.to_s == stream_name.to_s %>">
|
|
8
|
+
<%= h(stream_name) %>
|
|
9
|
+
</button>
|
|
10
|
+
<% end -%>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="pending">
|
|
14
|
+
<span></span>
|
|
15
|
+
<span class="text-muted"><%= @total %> message(s) pending</span>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<% if @jobs.empty? -%>
|
|
19
|
+
<div class="alert alert-success">Stream <code><%= h(@stream_name) %></code> is empty</div>
|
|
20
|
+
<% else -%>
|
|
21
|
+
<div class="table-container">
|
|
22
|
+
<table>
|
|
23
|
+
<thead>
|
|
24
|
+
<tr>
|
|
25
|
+
<th>Job</th>
|
|
26
|
+
<th>JID[Args]</th>
|
|
27
|
+
<th>Stream</th>
|
|
28
|
+
<th>Seq</th>
|
|
29
|
+
</tr>
|
|
30
|
+
</thead>
|
|
31
|
+
<tbody>
|
|
32
|
+
<% @jobs.each do |job| -%>
|
|
33
|
+
<tr>
|
|
34
|
+
<td>
|
|
35
|
+
<div class="job-class">
|
|
36
|
+
<%= h(job.data[:class].to_s) %>
|
|
37
|
+
</div>
|
|
38
|
+
</td>
|
|
39
|
+
<td>
|
|
40
|
+
<div class="job-id">
|
|
41
|
+
<details>
|
|
42
|
+
<summary>
|
|
43
|
+
<code><%= h(job.data[:jid].to_s) %></code>
|
|
44
|
+
</summary>
|
|
45
|
+
<code><%= h((JSON.pretty_generate(job.data[:args]) rescue job.data[:args].to_s)) %></code>
|
|
46
|
+
</details>
|
|
47
|
+
</div>
|
|
48
|
+
</td>
|
|
49
|
+
<td>
|
|
50
|
+
<code><%= h(job.stream) %></code>
|
|
51
|
+
</td>
|
|
52
|
+
<td>
|
|
53
|
+
<code><%= job.seq %></code>
|
|
54
|
+
</td>
|
|
55
|
+
</tr>
|
|
56
|
+
<% end -%>
|
|
57
|
+
</tbody>
|
|
58
|
+
</table>
|
|
59
|
+
</div>
|
|
60
|
+
<% end -%>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<div class="pending">
|
|
2
|
+
<span></span>
|
|
3
|
+
<span class="text-muted"><%= @total %> job(s) scheduled</span>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<% if @jobs.empty? -%>
|
|
7
|
+
<div class="alert alert-success">No scheduled jobs found</div>
|
|
8
|
+
<% else -%>
|
|
9
|
+
<div class="table-container">
|
|
10
|
+
<table>
|
|
11
|
+
<thead>
|
|
12
|
+
<tr>
|
|
13
|
+
<th>Job</th>
|
|
14
|
+
<th>JID[Args]</th>
|
|
15
|
+
<th>Stream</th>
|
|
16
|
+
<th>When</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% @jobs.each do |job| -%>
|
|
21
|
+
<tr>
|
|
22
|
+
<td>
|
|
23
|
+
<div class="job-class">
|
|
24
|
+
<%= h(job.data[:class].to_s) %>
|
|
25
|
+
</div>
|
|
26
|
+
</td>
|
|
27
|
+
<td>
|
|
28
|
+
<div class="job-id">
|
|
29
|
+
<details>
|
|
30
|
+
<summary>
|
|
31
|
+
<code><%= h(job.data[:jid].to_s) %></code>
|
|
32
|
+
</summary>
|
|
33
|
+
<code><%= h((JSON.pretty_generate(job.data[:args]) rescue job.data[:args].to_s)) %></code>
|
|
34
|
+
</details>
|
|
35
|
+
</div>
|
|
36
|
+
</td>
|
|
37
|
+
<td>
|
|
38
|
+
<code><%= h(job.x_stream) %></code>
|
|
39
|
+
</td>
|
|
40
|
+
<td style="white-space: nowrap;">
|
|
41
|
+
<div class="time-badge"><%= h(time_until(job.execute_at)) %></div>
|
|
42
|
+
<time><%= h(format_timestamp(job.execute_at)) %></time>
|
|
43
|
+
</td>
|
|
44
|
+
</tr>
|
|
45
|
+
<% end -%>
|
|
46
|
+
</tbody>
|
|
47
|
+
</table>
|
|
48
|
+
</div>
|
|
49
|
+
<% end -%>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<div class="cards-container">
|
|
2
|
+
<article class="stat-card">
|
|
3
|
+
<div class="row">
|
|
4
|
+
<p>💪 Processed</p>
|
|
5
|
+
<h4><%= format_numbers(@processed) %></h4>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="row">
|
|
8
|
+
<p>😥 Failed</p>
|
|
9
|
+
<h4><%= format_numbers(@failed) %></h4>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="row">
|
|
12
|
+
<p>🔄 Retries</p>
|
|
13
|
+
<h4><%= format_numbers(@retries) %></h4>
|
|
14
|
+
</div>
|
|
15
|
+
</article>
|
|
16
|
+
<article class="stat-card">
|
|
17
|
+
<h3><%= format_numbers(@busy) %></h3>
|
|
18
|
+
<p>
|
|
19
|
+
<a href="<%= url_for('/jobs/busy') %>"
|
|
20
|
+
hx-get="<%= url_for('/jobs/busy') %>"
|
|
21
|
+
hx-target="#content"
|
|
22
|
+
hx-push-url="true">⏳ Busy</a>
|
|
23
|
+
</p>
|
|
24
|
+
</article>
|
|
25
|
+
<article class="stat-card">
|
|
26
|
+
<h3><%= format_numbers(@enqueued) %></h3>
|
|
27
|
+
<p>
|
|
28
|
+
<a href="<%= url_for('/jobs/enqueued') %>"
|
|
29
|
+
hx-get="<%= url_for('/jobs/enqueued') %>"
|
|
30
|
+
hx-target="#content"
|
|
31
|
+
hx-push-url="true"
|
|
32
|
+
class="<%= 'active' if referrer?('/jobs/enqueued') %>">📬 Enqueued</a>
|
|
33
|
+
</p>
|
|
34
|
+
</article>
|
|
35
|
+
<article class="stat-card">
|
|
36
|
+
<h3><%= format_numbers(@scheduled) %></h3>
|
|
37
|
+
<p>
|
|
38
|
+
<a href="<%= url_for('/jobs/scheduled') %>"
|
|
39
|
+
hx-get="<%= url_for('/jobs/scheduled') %>"
|
|
40
|
+
hx-target="#content"
|
|
41
|
+
hx-push-url="true"
|
|
42
|
+
class="<%= 'active' if referrer?('/jobs/scheduled') %>">⏰ Scheduled</a>
|
|
43
|
+
</p>
|
|
44
|
+
</article>
|
|
45
|
+
<article class="stat-card">
|
|
46
|
+
<h3><%= format_numbers(@dead) %></h3>
|
|
47
|
+
<p>
|
|
48
|
+
<a href="<%= url_for('/jobs/dead') %>"
|
|
49
|
+
hx-get="<%= url_for('/jobs/dead') %>"
|
|
50
|
+
hx-target="#content"
|
|
51
|
+
hx-push-url="true"
|
|
52
|
+
class="<%= 'active' if referrer?('/jobs/dead') %>">💀 Dead</a>
|
|
53
|
+
</p>
|
|
54
|
+
</article>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<script>
|
|
58
|
+
(function () {
|
|
59
|
+
const links = document.querySelectorAll('.cards-container a');
|
|
60
|
+
|
|
61
|
+
links.forEach(link => {
|
|
62
|
+
link.addEventListener('click', () => {
|
|
63
|
+
links.forEach(item => item.classList.remove('active'));
|
|
64
|
+
|
|
65
|
+
link.classList.add('active');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
})();
|
|
69
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<section>
|
|
2
|
+
<header></header>
|
|
3
|
+
|
|
4
|
+
<div hx-get="<%= url_for('/jobs/_stats') %>"
|
|
5
|
+
hx-trigger="load, every 5s"
|
|
6
|
+
hx-swap="innerHTML">
|
|
7
|
+
<div class="alert alert-info">Loading statistics…</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div id="content"
|
|
11
|
+
hx-get="<%= url_for('/jobs/_busy') %>"
|
|
12
|
+
hx-trigger="load, every 5s"
|
|
13
|
+
hx-swap="innerHTML">
|
|
14
|
+
<div class="alert alert-info">Loading busy jobs…</div>
|
|
15
|
+
</div>
|
|
16
|
+
</section>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<section>
|
|
2
|
+
<header></header>
|
|
3
|
+
|
|
4
|
+
<div hx-get="<%= url_for('/jobs/_stats') %>"
|
|
5
|
+
hx-trigger="load, every 5s"
|
|
6
|
+
hx-swap="innerHTML">
|
|
7
|
+
<div class="alert alert-info">Loading statistics…</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div id="content">
|
|
11
|
+
<div hx-get="<%= url_for('/jobs/_dead') %>"
|
|
12
|
+
hx-trigger="load, every 5s"
|
|
13
|
+
hx-swap="innerHTML">
|
|
14
|
+
<div class="alert alert-info">Loading dead jobs…</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</section>
|