cosmonats 0.8.0 → 0.9.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 +21 -11
- data/lib/cosmo/api/counter.rb +1 -1
- data/lib/cosmo/api/cron/entry.rb +10 -11
- data/lib/cosmo/api/cron.rb +3 -3
- data/lib/cosmo/api/job.rb +1 -1
- data/lib/cosmo/api/kv.rb +3 -3
- data/lib/cosmo/api/stats/busy.rb +5 -0
- data/lib/cosmo/api/stats/metrics.rb +3 -3
- data/lib/cosmo/api/stats.rb +15 -0
- data/lib/cosmo/api/stream.rb +7 -2
- data/lib/cosmo/client.rb +10 -13
- data/lib/cosmo/job/data.rb +1 -1
- data/lib/cosmo/job/processor.rb +5 -6
- data/lib/cosmo/utils.rb +0 -1
- data/lib/cosmo/version.rb +1 -1
- data/lib/cosmo/web/assets/app.css +271 -629
- data/lib/cosmo/web/assets/app.js +158 -0
- data/lib/cosmo/web/assets/fonts/OFL.txt +92 -0
- data/lib/cosmo/web/assets/fonts/geist-mono.woff2 +0 -0
- data/lib/cosmo/web/assets/fonts/geist.woff2 +0 -0
- data/lib/cosmo/web/assets/logo.svg +10 -0
- data/lib/cosmo/web/cache.rb +75 -0
- data/lib/cosmo/web/chart.rb +62 -38
- data/lib/cosmo/web/circuit.rb +55 -0
- data/lib/cosmo/web/context.rb +0 -4
- data/lib/cosmo/web/controllers/application.rb +16 -0
- data/lib/cosmo/web/controllers/batches.rb +1 -0
- data/lib/cosmo/web/controllers/crons.rb +9 -2
- data/lib/cosmo/web/controllers/jobs.rb +50 -59
- data/lib/cosmo/web/controllers/metrics.rb +1 -0
- data/lib/cosmo/web/controllers/overview.rb +70 -0
- data/lib/cosmo/web/controllers/processes.rb +33 -0
- data/lib/cosmo/web/controllers/streams.rb +26 -28
- data/lib/cosmo/web/helpers/application.rb +57 -0
- data/lib/cosmo/web/renderer.rb +19 -6
- data/lib/cosmo/web/throughput.rb +104 -0
- data/lib/cosmo/web/views/batches/_table.erb +42 -54
- data/lib/cosmo/web/views/batches/index.erb +3 -10
- data/lib/cosmo/web/views/crons/_table.erb +32 -53
- data/lib/cosmo/web/views/crons/index.erb +2 -9
- data/lib/cosmo/web/views/jobs/_args.erb +5 -0
- data/lib/cosmo/web/views/jobs/_busy.erb +25 -121
- data/lib/cosmo/web/views/jobs/_dead.erb +42 -82
- data/lib/cosmo/web/views/jobs/_enqueued.erb +33 -113
- data/lib/cosmo/web/views/jobs/_panel.erb +29 -0
- data/lib/cosmo/web/views/jobs/_queue_row.erb +18 -0
- data/lib/cosmo/web/views/jobs/_queues.erb +20 -0
- data/lib/cosmo/web/views/jobs/_scheduled.erb +20 -54
- data/lib/cosmo/web/views/jobs/index.erb +4 -17
- data/lib/cosmo/web/views/layout.erb +73 -22
- data/lib/cosmo/web/views/metrics/_chart.erb +37 -32
- data/lib/cosmo/web/views/metrics/_panel.erb +3 -3
- data/lib/cosmo/web/views/metrics/_table.erb +24 -40
- data/lib/cosmo/web/views/metrics/index.erb +8 -16
- data/lib/cosmo/web/views/overview/_badges.erb +3 -0
- data/lib/cosmo/web/views/overview/_kpis.erb +19 -0
- data/lib/cosmo/web/views/overview/_running.erb +18 -0
- data/lib/cosmo/web/views/overview/_throughput.erb +50 -0
- data/lib/cosmo/web/views/overview/_workers.erb +25 -0
- data/lib/cosmo/web/views/overview/index.erb +28 -0
- data/lib/cosmo/web/views/processes/_table.erb +67 -0
- data/lib/cosmo/web/views/processes/index.erb +1 -0
- data/lib/cosmo/web/views/shared/_pager.erb +22 -0
- data/lib/cosmo/web/views/shared/unavailable.erb +7 -0
- data/lib/cosmo/web/views/streams/_info.erb +28 -86
- data/lib/cosmo/web/views/streams/_list.erb +7 -0
- data/lib/cosmo/web/views/streams/_pause_banner.erb +8 -16
- data/lib/cosmo/web/views/streams/_stream.erb +52 -0
- data/lib/cosmo/web/views/streams/index.erb +3 -11
- data/lib/cosmo/web/views/streams/info.erb +3 -11
- data/lib/cosmo/web.rb +66 -35
- data/sig/cosmo/api/cron/entry.rbs +2 -1
- data/sig/cosmo/api/stats/busy.rbs +2 -0
- data/sig/cosmo/api/stats/metrics.rbs +1 -1
- data/sig/cosmo/api/stats.rbs +4 -0
- data/sig/cosmo/api/stream.rbs +2 -0
- data/sig/cosmo/client.rbs +1 -0
- metadata +31 -18
- data/lib/cosmo/utils/overrides.rb +0 -206
- data/lib/cosmo/utils/warnings.rb +0 -17
- data/lib/cosmo/web/controllers/actions.rb +0 -16
- data/lib/cosmo/web/views/actions/index.erb +0 -7
- data/lib/cosmo/web/views/jobs/_processes.erb +0 -145
- data/lib/cosmo/web/views/jobs/_stats.erb +0 -77
- data/lib/cosmo/web/views/jobs/_tabs.erb +0 -14
- data/lib/cosmo/web/views/jobs/busy.erb +0 -18
- data/lib/cosmo/web/views/jobs/dead.erb +0 -18
- data/lib/cosmo/web/views/jobs/enqueued.erb +0 -18
- data/lib/cosmo/web/views/jobs/scheduled.erb +0 -18
- data/lib/cosmo/web/views/streams/_stream_row.erb +0 -42
- data/lib/cosmo/web/views/streams/_table.erb +0 -25
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<% metrics_path = Cosmo::Config.metrics.enabled ? '/metrics' : nil -%>
|
|
2
|
+
<% kpis = [
|
|
3
|
+
['Processed', @processed, 'all time', 'var(--c1)', metrics_path],
|
|
4
|
+
['Failed', @failed, 'all time', 'var(--err)', metrics_path],
|
|
5
|
+
['Busy', @busy, 'running now', 'var(--c3)', '/jobs/busy'],
|
|
6
|
+
['Enqueued', @enqueued, 'waiting in queues', 'var(--c2)', '/jobs/enqueued'],
|
|
7
|
+
['Retries', @retries, 'redeliveries', 'var(--warn)', '/jobs']
|
|
8
|
+
] -%>
|
|
9
|
+
<% kpis << ['Scheduled', @scheduled, 'delayed & cron', 'var(--c5)', '/jobs/scheduled'] if Cosmo::Config.scheduled.enabled -%>
|
|
10
|
+
<% kpis << ['Dead', @dead, 'gave up', 'var(--c4)', '/jobs/dead'] if Cosmo::Config.dead.enabled -%>
|
|
11
|
+
<div class="kpis">
|
|
12
|
+
<% kpis.each do |label, value, sub, color, link| -%>
|
|
13
|
+
<<%= link ? %(a href="#{url_for(link)}") : 'div' %> class="kpi" style="--hue:<%= color %>">
|
|
14
|
+
<div class="kpi-label"><i></i><%= label %></div>
|
|
15
|
+
<div class="kpi-val"><b title="<%= format_numbers(value) %>"><%= compact_number(value) %></b></div>
|
|
16
|
+
<div class="kpi-sub"><%= sub %></div>
|
|
17
|
+
</<%= link ? 'a' : 'div' %>>
|
|
18
|
+
<% end -%>
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<div class="card-head bar ruled">
|
|
2
|
+
<div class="card-title">Running now</div>
|
|
3
|
+
<a class="small-mono" href="<%= url_for('/jobs/busy') %>"><%= @total %> busy →</a>
|
|
4
|
+
</div>
|
|
5
|
+
<% if @jobs.empty? -%>
|
|
6
|
+
<div class="empty"><b>All quiet</b>No jobs are running right now.</div>
|
|
7
|
+
<% else -%>
|
|
8
|
+
<div class="feed">
|
|
9
|
+
<% @jobs.each do |job| -%>
|
|
10
|
+
<div class="feed-row" title="<%= h(job.dig(:data, :jid)) %> · <%= h(job[:worker]) %>">
|
|
11
|
+
<span class="mono faint"><%= Time.at(job[:started_at].to_i).strftime('%H:%M:%S') %></span>
|
|
12
|
+
<span class="dot" style="background:<%= hue(job[:stream]) %>"></span>
|
|
13
|
+
<span class="ellip"><%= h(job.dig(:data, :class)) %></span>
|
|
14
|
+
<span class="mono mute"><%= h(job[:stream]) %> · <%= elapsed(job[:started_at]) %></span>
|
|
15
|
+
</div>
|
|
16
|
+
<% end -%>
|
|
17
|
+
</div>
|
|
18
|
+
<% end -%>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<section class="card card-pad grow2" id="throughput"
|
|
2
|
+
hx-get="<%= url_for('/_throughput', days: @days) %>" hx-trigger="cosmo:poll" data-poll="30" hx-swap="outerHTML">
|
|
3
|
+
<div class="card-head">
|
|
4
|
+
<div class="card-title">Throughput</div>
|
|
5
|
+
<div class="legend"><i></i>succeeded / day</div>
|
|
6
|
+
<div class="legend"><i class="fail"></i>failed</div>
|
|
7
|
+
<div class="seg">
|
|
8
|
+
<% @ranges.each do |range| -%>
|
|
9
|
+
<button type="button" class="<%= 'on' if range == @days %>" hx-get="<%= url_for('/_throughput', days: range) %>"
|
|
10
|
+
hx-target="#throughput" hx-swap="outerHTML" hx-indicator="#global-spinner"><%= range %>d</button>
|
|
11
|
+
<% end -%>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<% if @chart.empty? -%>
|
|
15
|
+
<div class="empty"><b>No runs yet</b>Nothing recorded in the last <%= @days %> days.</div>
|
|
16
|
+
<% else -%>
|
|
17
|
+
<div class="chart">
|
|
18
|
+
<div class="yaxis"><% @chart.ticks.each do |tick| -%><span><%= compact_number(tick) %></span><% end -%></div>
|
|
19
|
+
<div class="plot-col">
|
|
20
|
+
<div class="plot">
|
|
21
|
+
<svg viewBox="0 0 <%= Cosmo::Web::Throughput::WIDTH %> <%= Cosmo::Web::Throughput::HEIGHT %>" preserveAspectRatio="none" aria-hidden="true">
|
|
22
|
+
<defs>
|
|
23
|
+
<linearGradient id="thrFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="var(--c2)" stop-opacity="0.22"/><stop offset="1" stop-color="var(--c1)" stop-opacity="0"/></linearGradient>
|
|
24
|
+
<linearGradient id="thrLine" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="var(--c3)"/><stop offset="0.55" stop-color="var(--c1)"/><stop offset="1" stop-color="var(--c2)"/></linearGradient>
|
|
25
|
+
<linearGradient id="thrFail" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="var(--err)" stop-opacity="0.95"/><stop offset="1" stop-color="var(--err)" stop-opacity="0.45"/></linearGradient>
|
|
26
|
+
</defs>
|
|
27
|
+
<path d="<%= @chart.grid %>" stroke="var(--line)" stroke-width="1" vector-effect="non-scaling-stroke" fill="none"/>
|
|
28
|
+
<path d="<%= @chart.area %>" fill="url(#thrFill)"/>
|
|
29
|
+
<path d="<%= @chart.line %>" stroke="url(#thrLine)" stroke-width="1.75" vector-effect="non-scaling-stroke" fill="none" stroke-linejoin="round"/>
|
|
30
|
+
<path d="<%= @chart.failures %>" fill="url(#thrFail)"/>
|
|
31
|
+
</svg>
|
|
32
|
+
<% @chart.points.each do |point| -%>
|
|
33
|
+
<div class="hit<%= ' flip' if point[:center] > 62 %>" style="left:<%= point[:left] %>%;width:<%= point[:width] %>%">
|
|
34
|
+
<div class="hv-line"></div>
|
|
35
|
+
<div class="hv-dot" style="top:<%= point[:top] %>%"></div>
|
|
36
|
+
<div class="tip">
|
|
37
|
+
<span class="mono mute" style="font-size:10.5px"><%= point[:label] %></span>
|
|
38
|
+
<div><span class="mute">succeeded</span><span class="num"><%= format_numbers(point[:count]) %></span></div>
|
|
39
|
+
<div><span class="mute">failed</span><span class="num" style="color:var(--err)"><%= format_numbers(point[:failed]) %></span></div>
|
|
40
|
+
<div><span class="mute">avg exec</span><span class="num"><%= duration(point[:exec_ms]) %></span></div>
|
|
41
|
+
<div><span class="mute">avg wait</span><span class="num"><%= duration(point[:wait_ms]) %></span></div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<% end -%>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="xaxis"><% @chart.labels.each do |label| -%><span><%= label %></span><% end -%></div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<% end -%>
|
|
50
|
+
</section>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<div class="card-head">
|
|
2
|
+
<div class="card-title">Workers</div>
|
|
3
|
+
<div class="mono mute" style="font-size:11px"><%= @busy %> / <%= @threads %> busy</div>
|
|
4
|
+
</div>
|
|
5
|
+
<% if @processes.empty? -%>
|
|
6
|
+
<div class="empty"><b>No worker processes</b>Start one with <code>cosmo -C config/cosmo.yml</code>.</div>
|
|
7
|
+
<% else -%>
|
|
8
|
+
<div class="cells">
|
|
9
|
+
<% @cells.each do |stream| -%>
|
|
10
|
+
<i<% if stream %> style="background:<%= hue(stream) %>" title="<%= h(stream) %>"<% end %>></i>
|
|
11
|
+
<% end -%>
|
|
12
|
+
</div>
|
|
13
|
+
<% if @hidden.positive? -%><div class="small-mono">+<%= @hidden %> more threads</div><% end -%>
|
|
14
|
+
<div class="procs">
|
|
15
|
+
<% @processes.each do |process| -%>
|
|
16
|
+
<div class="proc">
|
|
17
|
+
<div class="ellip" title="<%= h(process[:cmdline]) %>"><%= h(process[:hostname]) %> · pid <%= h(process[:pid]) %></div>
|
|
18
|
+
<span class="mute"><%= h(process[:busy]) %>/<%= h(process[:concurrency]) %></span>
|
|
19
|
+
<span class="faint"><%= process[:rss] ? format_bytes(process[:rss].to_i * 1024) : '—' %></span>
|
|
20
|
+
<span style="color:<%= { 'running' => 'var(--ok)', 'quiet' => 'var(--warn)', 'stopping' => 'var(--err)' }.fetch(process[:state].to_s, 'var(--mute)') %>"><%= h(process[:state]) %></span>
|
|
21
|
+
</div>
|
|
22
|
+
<% end -%>
|
|
23
|
+
</div>
|
|
24
|
+
<a class="small-mono" href="<%= url_for('/processes') %>">All <%= @total %> process(es) →</a>
|
|
25
|
+
<% end -%>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<div hx-get="<%= url_for('/_kpis') %>" hx-trigger="load, cosmo:poll" data-poll="<%= poll_interval %>" hx-swap="innerHTML">
|
|
2
|
+
<div class="kpis"><div class="kpi"><div class="loading">Loading statistics…</div></div></div>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="row-wrap">
|
|
6
|
+
<% if @metrics -%>
|
|
7
|
+
<section class="card card-pad grow2" id="throughput" hx-get="<%= url_for('/_throughput') %>" hx-trigger="load" hx-swap="outerHTML">
|
|
8
|
+
<div class="card-head"><div class="card-title">Throughput</div></div>
|
|
9
|
+
<div class="loading">Loading metrics…</div>
|
|
10
|
+
</section>
|
|
11
|
+
<% end -%>
|
|
12
|
+
<section class="card card-pad <%= @metrics ? 'grow1' : 'grow2' %>"
|
|
13
|
+
hx-get="<%= url_for('/_workers') %>" hx-trigger="load, cosmo:poll" data-poll="<%= poll_interval %>" hx-swap="innerHTML">
|
|
14
|
+
<div class="card-head"><div class="card-title">Workers</div></div>
|
|
15
|
+
<div class="loading">Loading workers…</div>
|
|
16
|
+
</section>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="row-wrap">
|
|
20
|
+
<section class="card grow2" hx-get="<%= url_for('/jobs/_queues', compact: 1) %>" hx-trigger="load, cosmo:poll" data-poll="<%= poll_interval %>" hx-swap="innerHTML">
|
|
21
|
+
<div class="card-head bar"><div class="card-title">Queues</div></div>
|
|
22
|
+
<div class="loading">Loading queues…</div>
|
|
23
|
+
</section>
|
|
24
|
+
<section class="card grow1" hx-get="<%= url_for('/_running') %>" hx-trigger="load, cosmo:poll" data-poll="<%= poll_interval %>" hx-swap="innerHTML">
|
|
25
|
+
<div class="card-head bar ruled"><div class="card-title">Running now</div></div>
|
|
26
|
+
<div class="loading">Loading jobs…</div>
|
|
27
|
+
</section>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<% states = { 'running' => 'var(--ok)', 'quiet' => 'var(--warn)', 'stopping' => 'var(--err)' } -%>
|
|
2
|
+
<% kinds = { 'jobs' => 'Jobs', 'streams' => 'Streams', 'jobs+streams' => 'Jobs & Streams', 'none' => 'No subscriptions' } -%>
|
|
3
|
+
<section class="card" id="processes"
|
|
4
|
+
<% if @polling -%>
|
|
5
|
+
hx-get="<%= url_for('/processes', @query.merge(page: @page)) %>" hx-trigger="cosmo:poll" data-poll="<%= poll_interval %>" hx-swap="outerHTML"
|
|
6
|
+
<% end -%>>
|
|
7
|
+
<div class="card-head bar ruled">
|
|
8
|
+
<div class="card-title"><%= @total %> process(es) alive</div>
|
|
9
|
+
<% if @polling -%>
|
|
10
|
+
<button type="button" class="btn quiet" hx-get="<%= url_for('/processes', @query.merge(page: @page, poll: 0)) %>"
|
|
11
|
+
hx-target="#processes" hx-swap="outerHTML" hx-indicator="#global-spinner">⏸ Stop polling</button>
|
|
12
|
+
<% else -%>
|
|
13
|
+
<button type="button" class="btn" hx-get="<%= url_for('/processes', @query.except(:poll).merge(page: @page)) %>"
|
|
14
|
+
hx-target="#processes" hx-swap="outerHTML" hx-indicator="#global-spinner">▶ Live poll</button>
|
|
15
|
+
<% end -%>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<% if @groups.empty? -%>
|
|
19
|
+
<div class="empty"><b>No worker processes</b>Start one with <code>cosmo -C config/cosmo.yml</code>.</div>
|
|
20
|
+
<% else -%>
|
|
21
|
+
<div class="scroll"><div style="min-width:1040px">
|
|
22
|
+
<div class="thead t-procs">
|
|
23
|
+
<span>Process</span><span>Command</span><span>Subscriptions</span><span class="r">Busy</span><span>State</span>
|
|
24
|
+
<span class="r">RTT</span><span class="r">Reconn.</span><span class="r">Memory</span><span class="r">Uptime</span><span class="r">Last beat</span>
|
|
25
|
+
</div>
|
|
26
|
+
<% @groups.each do |kind, group| -%>
|
|
27
|
+
<div class="trow group t-procs"><span class="full"><%= h(kinds.fetch(kind, kind)) %> (<%= group.size %>)</span></div>
|
|
28
|
+
<% group.each do |process| -%>
|
|
29
|
+
<% nats = Hash(process[:nats]) -%>
|
|
30
|
+
<% cmdline = process[:cmdline].to_s -%>
|
|
31
|
+
<div class="trow hover t-procs">
|
|
32
|
+
<div class="stack">
|
|
33
|
+
<span class="strong ellip"><%= h(process[:hostname]) %>:<%= h(process[:pid]) %></span>
|
|
34
|
+
<span class="small-mono ellip"><%= h(nats[:ip] || 'unknown IP') %><% if nats[:name] -%> · <%= h(nats[:name]) %><% end -%></span>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="stack">
|
|
37
|
+
<% if cmdline.length > 32 -%>
|
|
38
|
+
<details>
|
|
39
|
+
<summary class="mono ellip" style="font-size:11.5px" title="<%= h(cmdline) %>"><%= h(cmdline) %></summary>
|
|
40
|
+
<pre class="code-block"><%= h(cmdline) %></pre>
|
|
41
|
+
</details>
|
|
42
|
+
<% else -%>
|
|
43
|
+
<span class="mono" style="font-size:11.5px"><%= h(cmdline) %></span>
|
|
44
|
+
<% end -%>
|
|
45
|
+
<span class="small-mono">cosmo <%= h(process[:version]) %> · <%= h(process[:ruby]) %><% if process[:http_port] -%> · http :<%= h(process[:http_port]) %><% end -%></span>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="stack">
|
|
48
|
+
<% Hash(process[:subscriptions]).each do |type, names| -%>
|
|
49
|
+
<% next if Array(names).empty? -%>
|
|
50
|
+
<span class="mono ellip" style="font-size:11.5px" title="<%= h(Array(names).join(', ')) %>"><% if kind == 'jobs+streams' -%><span class="faint"><%= h(type) %>:</span> <% end -%><%= h(Array(names).join(', ')) %></span>
|
|
51
|
+
<% end -%>
|
|
52
|
+
</div>
|
|
53
|
+
<span class="mono r"><%= h(process[:busy]) %>/<%= h(process[:concurrency]) %></span>
|
|
54
|
+
<span class="pill" style="--tone:<%= states.fetch(process[:state].to_s, 'var(--acc)') %>"><%= h(process[:state]) %></span>
|
|
55
|
+
<span class="mono mute r"><%= nats[:rtt] ? "#{h(nats[:rtt])}ms" : '—' %></span>
|
|
56
|
+
<span class="mono mute r"><%= h(nats[:reconnects].to_i) %></span>
|
|
57
|
+
<span class="mono mute r"><%= process[:rss] ? format_bytes(process[:rss].to_i * 1024) : '—' %></span>
|
|
58
|
+
<span class="mono mute r"><%= elapsed(process[:started_at]) %></span>
|
|
59
|
+
<span class="mono mute r"><%= elapsed(process[:beat_at]) %> ago</span>
|
|
60
|
+
</div>
|
|
61
|
+
<% end -%>
|
|
62
|
+
<% end -%>
|
|
63
|
+
</div></div>
|
|
64
|
+
<% end -%>
|
|
65
|
+
|
|
66
|
+
<%= render('shared/_pager', { path: '/processes', query: @query, page: @page, total_pages: @total_pages, target: '#processes' }) %>
|
|
67
|
+
</section>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= @table %>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<% if @total_pages.to_i > 1 -%>
|
|
2
|
+
<% link = ->(page) { url_for(@path, Hash(@query).merge(page: page)) } -%>
|
|
3
|
+
<nav class="pager">
|
|
4
|
+
<% if @page > 1 -%>
|
|
5
|
+
<a class="btn quiet" hx-get="<%= link.(@page - 1) %>" hx-target="<%= @target %>" hx-swap="outerHTML" hx-indicator="#global-spinner">← Prev</a>
|
|
6
|
+
<% else -%>
|
|
7
|
+
<span class="btn quiet disabled">← Prev</span>
|
|
8
|
+
<% end -%>
|
|
9
|
+
<% pages(@page, @total_pages).each do |p| -%>
|
|
10
|
+
<% if p == :gap -%>
|
|
11
|
+
<span class="gap">…</span>
|
|
12
|
+
<% else -%>
|
|
13
|
+
<a class="btn<%= ' cur' if p == @page %>" hx-get="<%= link.(p) %>" hx-target="<%= @target %>" hx-swap="outerHTML" hx-indicator="#global-spinner"><%= p %></a>
|
|
14
|
+
<% end -%>
|
|
15
|
+
<% end -%>
|
|
16
|
+
<% if @page < @total_pages -%>
|
|
17
|
+
<a class="btn quiet" hx-get="<%= link.(@page + 1) %>" hx-target="<%= @target %>" hx-swap="outerHTML" hx-indicator="#global-spinner">Next →</a>
|
|
18
|
+
<% else -%>
|
|
19
|
+
<span class="btn quiet disabled">Next →</span>
|
|
20
|
+
<% end -%>
|
|
21
|
+
</nav>
|
|
22
|
+
<% end -%>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<section class="card" data-reload="<%= @retry %>">
|
|
2
|
+
<div class="empty">
|
|
3
|
+
<b>NATS is unreachable</b>
|
|
4
|
+
<span>Nothing was fetched, so this page shows no data. It tries again every <%= @retry %> seconds.</span>
|
|
5
|
+
<a class="btn" href="<%= h(@request.fullpath) %>">Retry now</a>
|
|
6
|
+
</div>
|
|
7
|
+
</section>
|
|
@@ -1,92 +1,34 @@
|
|
|
1
|
-
<div
|
|
2
|
-
<
|
|
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>
|
|
1
|
+
<div>
|
|
2
|
+
<a class="small-mono" href="<%= url_for('/streams') %>">← Back to streams</a>
|
|
18
3
|
</div>
|
|
19
4
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<div class="
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
hx-target="#content"
|
|
26
|
-
hx-push-url="true"
|
|
27
|
-
class="btn">Back to Streams</a>
|
|
5
|
+
<div class="kpis">
|
|
6
|
+
<div class="kpi" style="--hue:var(--c1)"><div class="kpi-label"><i></i>Messages</div><div class="kpi-val"><b><%= format_numbers(@state.messages) %></b></div></div>
|
|
7
|
+
<div class="kpi" style="--hue:var(--c2)"><div class="kpi-label"><i></i>Size</div><div class="kpi-val"><b><%= format_bytes(@state.bytes) %></b></div></div>
|
|
8
|
+
<div class="kpi" style="--hue:var(--c3)"><div class="kpi-label"><i></i>Consumers</div><div class="kpi-val"><b><%= @state.consumer_count %></b></div></div>
|
|
9
|
+
<div class="kpi" style="--hue:var(--c4)"><div class="kpi-label"><i></i>Sequence range</div><div class="kpi-val"><b style="font-size:22px"><%= format_numbers(@state.first_seq) %> → <%= format_numbers(@state.last_seq) %></b></div></div>
|
|
28
10
|
</div>
|
|
29
11
|
|
|
30
|
-
|
|
31
|
-
<div class="table-container">
|
|
32
|
-
<table>
|
|
33
|
-
<tbody>
|
|
34
|
-
<tr>
|
|
35
|
-
<th>Name</th>
|
|
36
|
-
<td>
|
|
37
|
-
<code><%= h(@name) %></code>
|
|
38
|
-
</td>
|
|
39
|
-
</tr>
|
|
40
|
-
<tr>
|
|
41
|
-
<th>Retention</th>
|
|
42
|
-
<td>
|
|
43
|
-
<code><%= h(@config.retention.to_s) %></code>
|
|
44
|
-
</td>
|
|
45
|
-
</tr>
|
|
46
|
-
<tr>
|
|
47
|
-
<th>Storage</th>
|
|
48
|
-
<td>
|
|
49
|
-
<code><%= h(@config.storage.to_s) %></code>
|
|
50
|
-
</td>
|
|
51
|
-
</tr>
|
|
52
|
-
<tr>
|
|
53
|
-
<th>Discard Policy</th>
|
|
54
|
-
<td>
|
|
55
|
-
<code><%= h(@config.discard.to_s) %></code>
|
|
56
|
-
</td>
|
|
57
|
-
</tr>
|
|
58
|
-
<tr>
|
|
59
|
-
<th>Max Consumers</th>
|
|
60
|
-
<td>
|
|
61
|
-
<code><%= @config.max_consumers || "Unlimited" %></code>
|
|
62
|
-
</td>
|
|
63
|
-
</tr>
|
|
64
|
-
<tr>
|
|
65
|
-
<th>Max Messages</th>
|
|
66
|
-
<td>
|
|
67
|
-
<code><%= @config.max_msgs || "Unlimited" %></code>
|
|
68
|
-
</td>
|
|
69
|
-
</tr>
|
|
70
|
-
<tr>
|
|
71
|
-
<th>Max Bytes</th>
|
|
72
|
-
<td>
|
|
73
|
-
<code><%= @config.max_bytes&.positive? ? format_bytes(@config.max_bytes) : "Unlimited" %></code>
|
|
74
|
-
</td>
|
|
75
|
-
</tr>
|
|
76
|
-
<tr>
|
|
77
|
-
<th>Max Age</th>
|
|
78
|
-
<td>
|
|
79
|
-
<code><%= @config.max_age&.positive? ? "#{@config.max_age}ns" : "Unlimited" %></code>
|
|
80
|
-
</td>
|
|
81
|
-
</tr>
|
|
82
|
-
</tbody>
|
|
83
|
-
</table>
|
|
84
|
-
</div>
|
|
12
|
+
<%= render('streams/_pause_banner', { name: @name, paused: @paused }) %>
|
|
85
13
|
|
|
86
|
-
<
|
|
87
|
-
<div class="
|
|
88
|
-
<%
|
|
89
|
-
|
|
14
|
+
<section class="card">
|
|
15
|
+
<div class="card-head bar ruled"><div class="card-title">Configuration</div></div>
|
|
16
|
+
<% rows = {
|
|
17
|
+
'Name' => @name,
|
|
18
|
+
'Retention' => @config.retention,
|
|
19
|
+
'Storage' => @config.storage,
|
|
20
|
+
'Replicas' => @config.num_replicas || 1,
|
|
21
|
+
'Discard policy' => @config.discard,
|
|
22
|
+
'Max consumers' => @config.max_consumers.to_i.positive? ? @config.max_consumers : 'Unlimited',
|
|
23
|
+
'Max messages' => @config.max_msgs.to_i.positive? ? format_numbers(@config.max_msgs) : 'Unlimited',
|
|
24
|
+
'Max bytes' => @config.max_bytes.to_i.positive? ? format_bytes(@config.max_bytes) : 'Unlimited',
|
|
25
|
+
'Max age' => @config.max_age.to_i.positive? ? "#{format_span(@config.max_age.to_i / 1_000_000_000)} (#{@config.max_age}ns)" : 'Unlimited'
|
|
26
|
+
} -%>
|
|
27
|
+
<% rows.each do |label, value| -%>
|
|
28
|
+
<div class="trow t-kv"><span class="eyebrow"><%= label %></span><code><%= h(value) %></code></div>
|
|
90
29
|
<% end -%>
|
|
91
|
-
|
|
92
|
-
|
|
30
|
+
<div class="trow t-kv">
|
|
31
|
+
<span class="eyebrow">Subjects</span>
|
|
32
|
+
<div class="subjects"><% Array(@config.subjects).each do |subject| -%><span class="subject"><%= h(subject) %></span><% end -%></div>
|
|
33
|
+
</div>
|
|
34
|
+
</section>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<% if @streams.empty? -%>
|
|
2
|
+
<section class="card"><div class="empty"><b>No streams</b>Run <code>cosmo --setup</code> to create them.</div></section>
|
|
3
|
+
<% else -%>
|
|
4
|
+
<% @streams.each do |stream| -%>
|
|
5
|
+
<%= render('streams/_stream', { stream: }) %>
|
|
6
|
+
<% end -%>
|
|
7
|
+
<% end -%>
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
<div
|
|
3
|
-
<span
|
|
4
|
-
<
|
|
5
|
-
hx-target="#pause-banner-<%= h(@name) %>"
|
|
6
|
-
hx-swap="outerHTML"
|
|
7
|
-
class="btn btn-success">▶ Resume Stream</a>
|
|
1
|
+
<section class="card banner" style="<%= 'border-color:color-mix(in oklch,var(--warn) 45%,var(--line))' if @paused %>">
|
|
2
|
+
<div class="stack">
|
|
3
|
+
<span class="strong"><%= @paused ? '⏸ Stream is paused' : '▶ Stream is active' %></span>
|
|
4
|
+
<span class="mute"><%= @paused ? 'Consumers are not fetching new messages.' : 'Consumers are fetching new messages.' %></span>
|
|
8
5
|
</div>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
hx-target="#pause-banner-<%= h(@name) %>"
|
|
14
|
-
hx-swap="outerHTML"
|
|
15
|
-
class="btn btn-warning">⏸ Pause Stream</a>
|
|
16
|
-
</div>
|
|
17
|
-
<% end -%>
|
|
6
|
+
<button type="button" class="btn<%= ' quiet' unless @paused %>"
|
|
7
|
+
hx-patch="<%= url_for(@paused ? '/streams/unpause' : '/streams/pause', { name: u(@name), view: 'banner' }) %>"
|
|
8
|
+
hx-target="closest section" hx-swap="outerHTML" hx-indicator="#global-spinner"><%= @paused ? '▶ Resume stream' : '⏸ Pause stream' %></button>
|
|
9
|
+
</section>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<% s = @stream -%>
|
|
2
|
+
<% state = s[:state] -%>
|
|
3
|
+
<% config = s[:config] -%>
|
|
4
|
+
<% max_age = config.max_age.to_i -%>
|
|
5
|
+
<section class="card">
|
|
6
|
+
<div class="stream-head">
|
|
7
|
+
<div class="stack" style="gap:6px">
|
|
8
|
+
<a class="stream-name" style="--hue:<%= s[:paused] ? 'var(--warn)' : hue(s[:name]) %>"
|
|
9
|
+
href="<%= url_for('/streams/info', name: u(s[:name])) %>"><i></i><%= h(s[:name]) %></a>
|
|
10
|
+
<div class="mono mute ellip" style="font-size:11.5px" title="<%= h(Array(config.subjects).join(', ')) %>"><%= h(Array(config.subjects).join(' · ')) %></div>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="stat"><span class="eyebrow">Messages</span><b><%= compact_number(state.messages) %></b></div>
|
|
13
|
+
<div class="stat"><span class="eyebrow">Size</span><b><%= format_bytes(state.bytes) %></b></div>
|
|
14
|
+
<div class="stat"><span class="eyebrow">Retention</span><b><%= h(config.retention) %><%= " · #{format_span(max_age / 1_000_000_000)}" if max_age.positive? %></b></div>
|
|
15
|
+
<div class="stat"><span class="eyebrow">Replicas</span><b>R<%= h(config.num_replicas || 1) %></b></div>
|
|
16
|
+
<div class="stat"><span class="eyebrow">Sequence</span><b class="ellip"><%= compact_number(state.first_seq) %> → <%= compact_number(state.last_seq) %></b></div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="stream-tools">
|
|
19
|
+
<% if s[:paused] -%><span class="paused-tag">paused · consumers are not fetching</span><% end -%>
|
|
20
|
+
<span class="spacer"></span>
|
|
21
|
+
<a class="btn quiet" href="<%= url_for('/streams/info', name: u(s[:name])) %>">Details</a>
|
|
22
|
+
<button type="button" class="btn<%= ' quiet' unless s[:paused] %>"
|
|
23
|
+
hx-patch="<%= url_for(s[:paused] ? '/streams/unpause' : '/streams/pause', name: u(s[:name])) %>"
|
|
24
|
+
hx-target="closest section" hx-swap="outerHTML" hx-indicator="#global-spinner"><%= s[:paused] ? '▶ Resume' : '⏸ Pause' %></button>
|
|
25
|
+
</div>
|
|
26
|
+
<% if s[:consumers].empty? -%>
|
|
27
|
+
<div class="thead top tint"><span>No consumers</span></div>
|
|
28
|
+
<% else -%>
|
|
29
|
+
<div class="scroll"><div style="min-width:760px">
|
|
30
|
+
<div class="thead top tint t-cons"><span>Consumer</span><span class="r">Pending</span><span class="r">Ack pending</span><span class="r">Ack wait</span><span class="r">Redeliv.</span><span>Lag</span></div>
|
|
31
|
+
<% s[:consumers].each do |consumer| -%>
|
|
32
|
+
<% cfg = Hash(consumer['config']) -%>
|
|
33
|
+
<% pending = consumer['num_pending'].to_i -%>
|
|
34
|
+
<% redelivered = consumer['num_redelivered'].to_i -%>
|
|
35
|
+
<% ratio = pending.fdiv([state.messages.to_i, 1].max).clamp(0, 1) -%>
|
|
36
|
+
<% tone = ratio > 0.7 ? 'var(--err)' : ratio > 0.3 ? 'var(--warn)' : 'var(--ok)' -%>
|
|
37
|
+
<% filters = Array(cfg['filter_subjects']).concat(Array(cfg['filter_subject'])).reject(&:empty?) -%>
|
|
38
|
+
<div class="trow t-cons">
|
|
39
|
+
<div class="stack">
|
|
40
|
+
<span class="strong ellip"><%= h(consumer['name']) %></span>
|
|
41
|
+
<span class="small-mono ellip" title="<%= h(filters.join(', ')) %>">deliver: <%= h(cfg['deliver_policy']) %><%= " · #{h(filters.join(', '))}" if filters.any? %></span>
|
|
42
|
+
</div>
|
|
43
|
+
<span class="r"><%= format_numbers(pending) %></span>
|
|
44
|
+
<span class="r mute"><%= format_numbers(consumer['num_ack_pending'].to_i) %></span>
|
|
45
|
+
<span class="r mute"><%= format_span(cfg['ack_wait'].to_i / 1_000_000_000) %></span>
|
|
46
|
+
<span class="r" style="color:<%= redelivered > 100 ? 'var(--warn)' : 'var(--mute)' %>"><%= format_numbers(redelivered) %></span>
|
|
47
|
+
<div class="lag"><div class="lag-track"><i style="width:<%= [ratio * 100, 2].max.round(1) %>%;background:<%= tone %>"></i></div><span style="color:<%= tone %>"><%= compact_number(pending) %></span></div>
|
|
48
|
+
</div>
|
|
49
|
+
<% end -%>
|
|
50
|
+
</div></div>
|
|
51
|
+
<% end -%>
|
|
52
|
+
</section>
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
<
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
<div id="content">
|
|
5
|
-
<div hx-get="<%= url_for('/streams/_table') %>"
|
|
6
|
-
hx-trigger="load, every <%= poll_interval %>s"
|
|
7
|
-
hx-swap="innerHTML">
|
|
8
|
-
<div class="alert alert-info">Loading streams…</div>
|
|
9
|
-
</div>
|
|
10
|
-
</div>
|
|
11
|
-
</section>
|
|
1
|
+
<div class="view" hx-get="<%= url_for('/streams') %>" hx-trigger="load, cosmo:poll" data-poll="<%= poll_interval %>" hx-swap="innerHTML">
|
|
2
|
+
<section class="card"><div class="loading">Loading streams…</div></section>
|
|
3
|
+
</div>
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
<
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
<div id="content">
|
|
5
|
-
<div hx-get="<%= url_for("/streams/_info", { name: u(@name) }) %>"
|
|
6
|
-
hx-trigger="load"
|
|
7
|
-
hx-swap="innerHTML">
|
|
8
|
-
<div class="alert alert-info">Loading stream info…</div>
|
|
9
|
-
</div>
|
|
10
|
-
</div>
|
|
11
|
-
</section>
|
|
1
|
+
<div class="view" hx-get="<%= url_for('/streams/info', { name: u(@name) }) %>" hx-trigger="load" hx-swap="innerHTML">
|
|
2
|
+
<section class="card"><div class="loading">Loading stream info…</div></section>
|
|
3
|
+
</div>
|