cosmonats 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/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 +35 -2
- data/lib/cosmo/config.rb +8 -6
- data/lib/cosmo/defaults.yml +31 -30
- data/lib/cosmo/job/processor.rb +58 -19
- data/lib/cosmo/job.rb +1 -1
- data/lib/cosmo/logger.rb +4 -0
- data/lib/cosmo/processor.rb +7 -1
- data/lib/cosmo/stream/data.rb +1 -0
- data/lib/cosmo/stream/processor.rb +18 -3
- data/lib/cosmo/stream.rb +2 -2
- 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
- data/sig/cosmo/processor.rbs +1 -1
- data/sig/cosmo/stream/data.rbs +1 -1
- data/sig/cosmo/stream/processor.rbs +2 -0
- data/sig/cosmo/stream.rbs +1 -0
- metadata +59 -3
- /data/sig/cosmo/{message.rbs → stream/message.rbs} +0 -0
|
@@ -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>
|
|
@@ -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/_enqueued', stream_name: @stream_name) %>"
|
|
12
|
+
hx-trigger="load"
|
|
13
|
+
hx-swap="innerHTML">
|
|
14
|
+
<div class="alert alert-info">Loading enqueued 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/_scheduled') %>"
|
|
12
|
+
hx-trigger="load, every 5s"
|
|
13
|
+
hx-swap="innerHTML">
|
|
14
|
+
<div class="alert alert-info">Loading scheduled jobs…</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</section>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Cosmo<%= " — #{h(content_for :title)}" if content_for?(:title) %></title>
|
|
7
|
+
<script src="<%= url_for('/assets/htmx.min.js.gz') %>" defer></script>
|
|
8
|
+
<link rel="stylesheet" href="<%= url_for('/assets/app.css') %>">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header>
|
|
12
|
+
<div class="container">
|
|
13
|
+
<nav class="nav">
|
|
14
|
+
<a href="<%= url_for('/jobs') %>" class="navbar-brand">🚀 Cosmo</a>
|
|
15
|
+
<ul class="nav-list">
|
|
16
|
+
<li>
|
|
17
|
+
<a href="<%= url_for('/jobs') %>" class="<%= "active" if current_page?('/jobs') %>">Jobs</a>
|
|
18
|
+
</li>
|
|
19
|
+
<li>
|
|
20
|
+
<a href="<%= url_for('/streams') %>" class="<%= "active" if current_page?('/streams') %>">Streams</a>
|
|
21
|
+
</li>
|
|
22
|
+
<li>
|
|
23
|
+
<a href="<%= url_for('/actions') %>" class="<%= "active" if current_page?('/actions') %>">Actions</a>
|
|
24
|
+
</li>
|
|
25
|
+
</ul>
|
|
26
|
+
</nav>
|
|
27
|
+
</div>
|
|
28
|
+
</header>
|
|
29
|
+
<main class="container">
|
|
30
|
+
<%= content_for :view %>
|
|
31
|
+
</main>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<div class="cards-container">
|
|
2
|
+
<article class="stat-card">
|
|
3
|
+
<h3><%= format_numbers(@state.messages) %></h3>
|
|
4
|
+
<p>Messages</p>
|
|
5
|
+
</article>
|
|
6
|
+
<article class="stat-card">
|
|
7
|
+
<h3><%= format_bytes(@state.bytes) %></h3>
|
|
8
|
+
<p>Size</p>
|
|
9
|
+
</article>
|
|
10
|
+
<article class="stat-card">
|
|
11
|
+
<h3><%= @state.consumer_count %></h3>
|
|
12
|
+
<p>Consumers</p>
|
|
13
|
+
</article>
|
|
14
|
+
<article class="stat-card">
|
|
15
|
+
<h3><%= format_numbers(@state.first_seq) + " → " + format_numbers(@state.last_seq) %></h3>
|
|
16
|
+
<p>Sequence Range</p>
|
|
17
|
+
</article>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="nav">
|
|
21
|
+
<a href="<%= url_for('/streams') %>"
|
|
22
|
+
hx-get="<%= url_for('/streams') %>"
|
|
23
|
+
hx-target="#content"
|
|
24
|
+
hx-push-url="true"
|
|
25
|
+
class="btn">Back to Streams</a>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<h2>Configuration</h2>
|
|
29
|
+
<div class="table-container">
|
|
30
|
+
<table>
|
|
31
|
+
<tbody>
|
|
32
|
+
<tr>
|
|
33
|
+
<th>Name</th>
|
|
34
|
+
<td>
|
|
35
|
+
<code><%= h(@name) %></code>
|
|
36
|
+
</td>
|
|
37
|
+
</tr>
|
|
38
|
+
<tr>
|
|
39
|
+
<th>Retention</th>
|
|
40
|
+
<td>
|
|
41
|
+
<code><%= h(@config.retention.to_s) %></code>
|
|
42
|
+
</td>
|
|
43
|
+
</tr>
|
|
44
|
+
<tr>
|
|
45
|
+
<th>Storage</th>
|
|
46
|
+
<td>
|
|
47
|
+
<code><%= h(@config.storage.to_s) %></code>
|
|
48
|
+
</td>
|
|
49
|
+
</tr>
|
|
50
|
+
<tr>
|
|
51
|
+
<th>Discard Policy</th>
|
|
52
|
+
<td>
|
|
53
|
+
<code><%= h(@config.discard.to_s) %></code>
|
|
54
|
+
</td>
|
|
55
|
+
</tr>
|
|
56
|
+
<tr>
|
|
57
|
+
<th>Max Consumers</th>
|
|
58
|
+
<td>
|
|
59
|
+
<code><%= @config.max_consumers || "Unlimited" %></code>
|
|
60
|
+
</td>
|
|
61
|
+
</tr>
|
|
62
|
+
<tr>
|
|
63
|
+
<th>Max Messages</th>
|
|
64
|
+
<td>
|
|
65
|
+
<code><%= @config.max_msgs || "Unlimited" %></code>
|
|
66
|
+
</td>
|
|
67
|
+
</tr>
|
|
68
|
+
<tr>
|
|
69
|
+
<th>Max Bytes</th>
|
|
70
|
+
<td>
|
|
71
|
+
<code><%= @config.max_bytes&.positive? ? format_bytes(@config.max_bytes) : "Unlimited" %></code>
|
|
72
|
+
</td>
|
|
73
|
+
</tr>
|
|
74
|
+
<tr>
|
|
75
|
+
<th>Max Age</th>
|
|
76
|
+
<td>
|
|
77
|
+
<code><%= @config.max_age&.positive? ? "#{@config.max_age}ns" : "Unlimited" %></code>
|
|
78
|
+
</td>
|
|
79
|
+
</tr>
|
|
80
|
+
</tbody>
|
|
81
|
+
</table>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<h2>Subjects</h2>
|
|
85
|
+
<div class="subjects" style="margin-top: var(--space-2x);">
|
|
86
|
+
<% Array(@config.subjects).each do |sub| -%>
|
|
87
|
+
<div class="subject-tag" style="padding: var(--space) var(--space-2x); font-size: 1rem;"><%= h(sub) %></div>
|
|
88
|
+
<% end -%>
|
|
89
|
+
</div>
|