side_bro 0.2.2

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +37 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +50 -0
  5. data/Rakefile +10 -0
  6. data/lib/side_bro/version.rb +5 -0
  7. data/lib/side_bro/web/action.rb +115 -0
  8. data/lib/side_bro/web/application.rb +331 -0
  9. data/lib/side_bro/web/helpers.rb +176 -0
  10. data/lib/side_bro/web/router.rb +50 -0
  11. data/lib/side_bro/web.rb +96 -0
  12. data/lib/side_bro.rb +8 -0
  13. data/sig/side_bro.rbs +4 -0
  14. data/web/assets/images/.keep +0 -0
  15. data/web/assets/javascripts/application.js +62 -0
  16. data/web/assets/javascripts/base-charts.js +1 -0
  17. data/web/assets/javascripts/dashboard-charts.js +1 -0
  18. data/web/assets/javascripts/dashboard.js +262 -0
  19. data/web/assets/javascripts/metrics.js +1 -0
  20. data/web/assets/stylesheets/style.css +636 -0
  21. data/web/locales/en.yml +88 -0
  22. data/web/views/_footer.html.erb +3 -0
  23. data/web/views/_job_info.html.erb +23 -0
  24. data/web/views/_metrics_period_select.html.erb +5 -0
  25. data/web/views/_nav.html.erb +76 -0
  26. data/web/views/_paging.html.erb +11 -0
  27. data/web/views/_poll_link.html.erb +4 -0
  28. data/web/views/_summary.html.erb +44 -0
  29. data/web/views/busy.html.erb +104 -0
  30. data/web/views/dashboard.html.erb +124 -0
  31. data/web/views/dead.html.erb +31 -0
  32. data/web/views/layout.html.erb +61 -0
  33. data/web/views/metrics.html.erb +56 -0
  34. data/web/views/metrics_for_job.html.erb +67 -0
  35. data/web/views/morgue.html.erb +82 -0
  36. data/web/views/queue.html.erb +190 -0
  37. data/web/views/queues.html.erb +59 -0
  38. data/web/views/retries.html.erb +99 -0
  39. data/web/views/retry.html.erb +32 -0
  40. data/web/views/scheduled.html.erb +79 -0
  41. data/web/views/scheduled_job_info.html.erb +31 -0
  42. metadata +126 -0
@@ -0,0 +1,88 @@
1
+ en:
2
+ dashboard: "Dashboard"
3
+ busy: "Busy"
4
+ queues: "Queues"
5
+ retries: "Retries"
6
+ scheduled: "Scheduled"
7
+ dead: "Dead"
8
+ morgue: "Morgue"
9
+ metrics: "Metrics"
10
+ processed: "Processed"
11
+ failed: "Failed"
12
+ enqueued: "Enqueued"
13
+ workers: "Workers"
14
+ processes: "Processes"
15
+ jobs: "Jobs"
16
+ queue: "Queue"
17
+ size: "Size"
18
+ latency: "Latency"
19
+ actions: "Actions"
20
+ delete: "Delete"
21
+ clear: "Clear"
22
+ pause: "Pause"
23
+ unpause: "Unpause"
24
+ retry: "Retry"
25
+ retry_now: "Retry Now"
26
+ kill: "Kill"
27
+ retry_all: "Retry All"
28
+ delete_all: "Delete All"
29
+ kill_all: "Kill All"
30
+ add_to_queue: "Add to Queue"
31
+ add_all_to_queue: "Add All to Queue"
32
+ quiet: "Quiet"
33
+ stop: "Stop"
34
+ quiet_all: "Quiet All"
35
+ stop_all: "Stop All"
36
+ next_retry: "Next"
37
+ retry_count: "#"
38
+ error: "Error"
39
+ class: "Class"
40
+ args: "Args"
41
+ jid: "JID"
42
+ queue_name: "Queue"
43
+ enqueued_at: "Enqueued At"
44
+ backtrace: "Backtrace"
45
+ show_backtrace: "Show backtrace"
46
+ hostname: "Hostname"
47
+ pid: "PID"
48
+ version: "Version"
49
+ started_at: "Started At"
50
+ rss: "RSS"
51
+ concurrency: "Concurrency"
52
+ tags: "Tags"
53
+ run_at: "Run At"
54
+ run_time: "Run Time"
55
+ thread: "Thread"
56
+ scheduled_at: "Scheduled At"
57
+ last_retry: "Last Retry"
58
+ next_page: "Next"
59
+ prev_page: "Prev"
60
+ page_of: "Page %{page} of %{total}"
61
+ no_jobs: "No jobs"
62
+ no_processes: "No processes"
63
+ no_retries: "No retries"
64
+ no_scheduled: "No scheduled jobs"
65
+ no_dead: "No dead jobs"
66
+ redis_info: "Redis Info"
67
+ redis_version: "Version"
68
+ redis_uptime: "Uptime"
69
+ redis_clients: "Connected Clients"
70
+ redis_memory: "Memory Used"
71
+ redis_peak_memory: "Peak Memory"
72
+ realtime_chart: "Realtime"
73
+ historical_chart: "Historical"
74
+ days_7: "7 days"
75
+ days_30: "30 days"
76
+ days_90: "90 days"
77
+ days_180: "180 days"
78
+ poll_interval: "Poll interval (s)"
79
+ metrics_period: "Period"
80
+ success: "Success"
81
+ total_exec_time: "Total Exec Time"
82
+ avg_exec_time: "Avg Exec Time"
83
+ job_class: "Job Class"
84
+ show_all: "Show All"
85
+ position: "Position"
86
+ select_all: "Select All"
87
+ bulk_actions: "Bulk Actions"
88
+ locale: "Language"
@@ -0,0 +1,3 @@
1
+ <div style="padding: 12px 0; color: var(--text-faint); font-size: 11px; font-family: var(--mono);">
2
+ SideBro <%= SideBro::VERSION %> · Sidekiq <%= Sidekiq::VERSION %>
3
+ </div>
@@ -0,0 +1,23 @@
1
+ <table class="job-info">
2
+ <tr><th><%= action.t(:jid) %></th><td><%= action.h(@job["jid"]) %></td></tr>
3
+ <tr><th><%= action.t(:class) %></th><td><%= action.h(action.job_display_class(@job)) %></td></tr>
4
+ <tr><th><%= action.t(:queue) %></th><td><%= action.h(@job["queue"]) %></td></tr>
5
+ <tr><th><%= action.t(:args) %></th><td><code><%= action.h(action.display_args(@job)) %></code></td></tr>
6
+ <%- if @job["enqueued_at"] %>
7
+ <tr><th><%= action.t(:enqueued_at) %></th><td><%= Time.at(@job["enqueued_at"]).utc %></td></tr>
8
+ <%- end %>
9
+ <%- if @job["error_class"] %>
10
+ <tr><th><%= action.t(:error) %></th><td><%= action.h("#{@job["error_class"]}: #{@job["error_message"]}") %></td></tr>
11
+ <%- end %>
12
+ <%- if @job["error_backtrace"]&.any? %>
13
+ <tr>
14
+ <th><%= action.t(:backtrace) %></th>
15
+ <td>
16
+ <details>
17
+ <summary><%= action.t(:show_backtrace) %></summary>
18
+ <pre><%= action.h(@job["error_backtrace"].first(20).join("\n")) %></pre>
19
+ </details>
20
+ </td>
21
+ </tr>
22
+ <%- end %>
23
+ </table>
@@ -0,0 +1,5 @@
1
+ <div class="period-select">
2
+ <%- %w[1h 8h 24h 72h].each do |p| %>
3
+ <a href="?period=<%= p %>" class="<%= @period == p ? 'active' : '' %>"><%= p %></a>
4
+ <%- end %>
5
+ </div>
@@ -0,0 +1,76 @@
1
+ <%
2
+ begin
3
+ _stats = Sidekiq::Stats.new
4
+ _busy = _stats.workers_size
5
+ _queues_count = Sidekiq::Queue.all.size
6
+ _retries = _stats.retry_size
7
+ _scheduled = _stats.scheduled_size
8
+ _dead = _stats.dead_size
9
+ rescue
10
+ _busy = _queues_count = _retries = _scheduled = _dead = 0
11
+ end
12
+ %>
13
+ <aside class="sidebar">
14
+ <div class="brand">
15
+ <div class="brand-mark"></div>
16
+ <div>
17
+ <div class="brand-name">Side<span>Bro</span></div>
18
+ </div>
19
+ <div class="brand-pill">live</div>
20
+ </div>
21
+
22
+ <div class="nav-label">Overview</div>
23
+ <nav class="nav">
24
+ <a href="<%= action.root_path %>" class="<%= action.current_path?('') ? 'active' : '' %>">
25
+ <span class="dot"></span> Dashboard <span class="count">—</span>
26
+ </a>
27
+ </nav>
28
+
29
+ <div class="nav-label">Workers</div>
30
+ <nav class="nav">
31
+ <a href="<%= action.root_path %>busy" class="<%= action.current_path?('busy') ? 'active' : '' %>">
32
+ <span class="dot"></span> Busy <span class="count"><%= _busy %></span>
33
+ </a>
34
+ <a href="<%= action.root_path %>queues" class="<%= action.request.path.include?('/queues') ? 'active' : '' %>">
35
+ <span class="dot"></span> Queues <span class="count"><%= _queues_count %></span>
36
+ </a>
37
+ </nav>
38
+
39
+ <div class="nav-label">Lifecycle</div>
40
+ <nav class="nav">
41
+ <a href="<%= action.root_path %>retries" class="<%= action.request.path.include?('/retries') ? 'active' : '' %>">
42
+ <span class="dot"></span> Retries <span class="count"><%= _retries %></span>
43
+ </a>
44
+ <a href="<%= action.root_path %>scheduled" class="<%= action.request.path.include?('/scheduled') ? 'active' : '' %>">
45
+ <span class="dot"></span> Scheduled <span class="count"><%= _scheduled %></span>
46
+ </a>
47
+ <a href="<%= action.root_path %>morgue" class="<%= action.request.path.include?('/morgue') ? 'active' : '' %>">
48
+ <span class="dot"></span> Dead <span class="count"><%= _dead %></span>
49
+ </a>
50
+ </nav>
51
+
52
+ <div class="nav-label">Monitoring</div>
53
+ <nav class="nav">
54
+ <a href="<%= action.root_path %>metrics" class="<%= action.current_path?('metrics') ? 'active' : '' %>">
55
+ <span class="dot"></span> Metrics <span class="count">—</span>
56
+ </a>
57
+ </nav>
58
+
59
+ <%- SideBro::Web.extensions.each do |ext| %>
60
+ <%- next unless ext[:tab] %>
61
+ <div class="nav-label"><%= action.h(ext[:tab]) %></div>
62
+ <nav class="nav">
63
+ <a href="<%= action.root_path %><%= action.h(ext[:name]) %>">
64
+ <span class="dot"></span> <%= action.h(ext[:tab]) %>
65
+ </a>
66
+ </nav>
67
+ <%- end %>
68
+
69
+ <div class="sidebar-footer">
70
+ <div class="avatar">SB</div>
71
+ <div class="who">
72
+ sidebro · v<%= SideBro::VERSION %>
73
+ <small>sidekiq <%= Sidekiq::VERSION %></small>
74
+ </div>
75
+ </div>
76
+ </aside>
@@ -0,0 +1,11 @@
1
+ <%- if @total && @total > 0 %>
2
+ <div class="paging">
3
+ <%- if @page > 0 %>
4
+ <a href="?<%= action.query_string("page" => @page, "per_page" => @per_page) %>">&laquo; <%= action.t(:prev_page) %></a>
5
+ <%- end %>
6
+ <span><%= action.t(:page_of, page: @page + 1, total: [(@total.to_f / @per_page).ceil, 1].max) %></span>
7
+ <%- if (@page + 1) * @per_page < @total %>
8
+ <a href="?<%= action.query_string("page" => @page + 2, "per_page" => @per_page) %>"><%= action.t(:next_page) %> &raquo;</a>
9
+ <%- end %>
10
+ </div>
11
+ <%- end %>
@@ -0,0 +1,4 @@
1
+ <div class="poll-link">
2
+ <input type="range" id="poll-interval" min="2" max="20" value="5">
3
+ <label for="poll-interval"><%= action.t(:poll_interval) %></label>
4
+ </div>
@@ -0,0 +1,44 @@
1
+ <%
2
+ begin
3
+ _s = Sidekiq::Stats.new
4
+ rescue
5
+ _s = nil
6
+ end
7
+ %>
8
+ <div class="stat-strip">
9
+ <div class="stat" data-tone="processed">
10
+ <div class="stat-label">Processed</div>
11
+ <div class="stat-value"><%= action.number_with_delimiter(_s&.processed || 0) %></div>
12
+ <div class="stat-delta">total</div>
13
+ </div>
14
+ <div class="stat" data-tone="failed">
15
+ <div class="stat-label"><span class="dot-mini" style="background:var(--red);box-shadow:0 0 6px var(--red)"></span> Failed</div>
16
+ <div class="stat-value"><%= action.number_with_delimiter(_s&.failed || 0) %></div>
17
+ <div class="stat-delta"><%= _s && _s.processed > 0 ? "%.2f%% rate" % (_s.failed.to_f / _s.processed * 100) : "—" %></div>
18
+ </div>
19
+ <a class="stat" data-tone="busy" href="<%= action.root_path %>busy">
20
+ <div class="stat-label"><span class="dot-mini" style="background:var(--cyan);box-shadow:0 0 6px var(--cyan)"></span> Busy</div>
21
+ <div class="stat-value"><%= _s&.workers_size || 0 %></div>
22
+ <div class="stat-delta">workers</div>
23
+ </a>
24
+ <a class="stat" data-tone="enqueued" href="<%= action.root_path %>queues">
25
+ <div class="stat-label"><span class="dot-mini" style="background:var(--violet-300)"></span> Enqueued</div>
26
+ <div class="stat-value"><%= action.number_with_delimiter(_s&.enqueued || 0) %></div>
27
+ <div class="stat-delta">pending</div>
28
+ </a>
29
+ <a class="stat" data-tone="retries" href="<%= action.root_path %>retries">
30
+ <div class="stat-label"><span class="dot-mini" style="background:var(--amber)"></span> Retries</div>
31
+ <div class="stat-value"><%= _s&.retry_size || 0 %></div>
32
+ <div class="stat-delta">scheduled</div>
33
+ </a>
34
+ <a class="stat" data-tone="scheduled" href="<%= action.root_path %>scheduled">
35
+ <div class="stat-label">Scheduled</div>
36
+ <div class="stat-value"><%= _s&.scheduled_size || 0 %></div>
37
+ <div class="stat-delta">upcoming</div>
38
+ </a>
39
+ <div class="stat" data-tone="dead">
40
+ <div class="stat-label">Dead</div>
41
+ <div class="stat-value"><%= _s&.dead_size || 0 %></div>
42
+ <div class="stat-delta">morgue</div>
43
+ </div>
44
+ </div>
@@ -0,0 +1,104 @@
1
+ <% @page_title = "Busy" %>
2
+
3
+ <div class="page-head">
4
+ <div class="page-title-block">
5
+ <div class="eyebrow"><span>Workers</span></div>
6
+ <h1><%= action.t(:busy) %></h1>
7
+ </div>
8
+ </div>
9
+
10
+ <div class="panel">
11
+ <div class="panel-head">
12
+ <div class="ph-title"><%= action.t(:processes) %></div>
13
+ <div class="ph-meta mono"><%= @processes.size %> processes</div>
14
+ <div class="ph-spacer"></div>
15
+ <%- unless @processes.empty? %>
16
+ <form method="post" action="<%= action.root_path %>busy" style="display:inline; display:flex; gap:8px;">
17
+ <button type="submit" name="quiet" value="1" class="btn ghost"><%= action.t(:quiet_all) %></button>
18
+ <button type="submit" name="stop" value="1" class="btn danger"><%= action.t(:stop_all) %></button>
19
+ </form>
20
+ <%- end %>
21
+ </div>
22
+ <%- if @processes.empty? %>
23
+ <div class="empty"><%= action.t(:no_processes) %></div>
24
+ <%- else %>
25
+ <div class="table-scroll">
26
+ <table>
27
+ <thead>
28
+ <tr>
29
+ <th><%= action.t(:hostname) %></th>
30
+ <th><%= action.t(:queues) %></th>
31
+ <th><%= action.t(:started_at) %></th>
32
+ <th><%= action.t(:rss) %></th>
33
+ <th><%= action.t(:concurrency) %></th>
34
+ <th><%= action.t(:busy) %></th>
35
+ <th class="col-actions"><%= action.t(:actions) %></th>
36
+ </tr>
37
+ </thead>
38
+ <tbody>
39
+ <%- @processes.each do |process| %>
40
+ <tr>
41
+ <td><span class="mono" style="font-size:12px"><%= action.h(process["hostname"]) %>:<%= action.h(process["pid"].to_s) %></span></td>
42
+ <td><%= action.h(Array(process["queues"]).join(", ")) %></td>
43
+ <td><span class="mono" style="font-size:12px"><%= process["started_at"] ? Time.at(process["started_at"]).utc.strftime("%Y-%m-%d %H:%M") : "—" %></span></td>
44
+ <td><%= action.format_memory(process["rss"]) %></td>
45
+ <td><%= process["concurrency"] %></td>
46
+ <td><span style="color:var(--cyan)"><%= process["busy"] %></span> / <%= process["concurrency"] %></td>
47
+ <td class="col-actions">
48
+ <form method="post" action="<%= action.root_path %>busy" style="display:inline-flex; gap:4px;">
49
+ <input type="hidden" name="identity" value="<%= action.h(process.identity) %>">
50
+ <button type="submit" name="quiet_process" value="1" class="iconlink" title="<%= action.t(:quiet) %>">
51
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="1" y1="1" x2="23" y2="23"/><path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"/><path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23M12 20l-3.3.3M12 20l3.3.3M12 20v4"/></svg>
52
+ </button>
53
+ <button type="submit" name="stop_process" value="1" class="iconlink danger" title="<%= action.t(:stop) %>">
54
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
55
+ </button>
56
+ </form>
57
+ </td>
58
+ </tr>
59
+ <%- end %>
60
+ </tbody>
61
+ </table>
62
+ </div>
63
+ <%- end %>
64
+ </div>
65
+
66
+ <div class="panel">
67
+ <div class="panel-head">
68
+ <div class="ph-title"><%= action.t(:jobs) %></div>
69
+ <div class="ph-meta mono"><%= @workers.size %> running</div>
70
+ </div>
71
+ <%- if @workers.empty? %>
72
+ <div class="empty"><%= action.t(:no_jobs) %></div>
73
+ <%- else %>
74
+ <div class="table-scroll">
75
+ <table>
76
+ <thead>
77
+ <tr>
78
+ <th><%= action.t(:hostname) %></th>
79
+ <th><%= action.t(:thread) %></th>
80
+ <th><%= action.t(:jid) %></th>
81
+ <th><%= action.t(:queue) %></th>
82
+ <th><%= action.t(:class) %></th>
83
+ <th><%= action.t(:args) %></th>
84
+ <th><%= action.t(:run_at) %></th>
85
+ </tr>
86
+ </thead>
87
+ <tbody>
88
+ <%- @workers.each do |process, thread, work| %>
89
+ <%- job = work.is_a?(Hash) ? work : (work.respond_to?(:job) ? work.job : work) %>
90
+ <tr>
91
+ <td><span class="mono" style="font-size:12px"><%= action.h(process.to_s) %></span></td>
92
+ <td><span class="mono" style="font-size:12px"><%= action.h(thread.to_s) %></span></td>
93
+ <td><span class="mono" style="font-size:12px; color:var(--violet-200)"><%= action.h(job["jid"].to_s) %></span></td>
94
+ <td><%= action.h(job["queue"].to_s) %></td>
95
+ <td><span class="job-name"><%= action.h(action.job_display_class(job)) %></span></td>
96
+ <td><code class="args" style="font-size:11px"><%= action.h(action.format_args_short(job)) %></code></td>
97
+ <td><span class="mono" style="font-size:12px"><%= job["run_at"] ? action.relative_time(Time.at(job["run_at"].to_f)) : "—" %></span></td>
98
+ </tr>
99
+ <%- end %>
100
+ </tbody>
101
+ </table>
102
+ </div>
103
+ <%- end %>
104
+ </div>
@@ -0,0 +1,124 @@
1
+ <% @page_title = "Dashboard" %>
2
+ <%= action.render_partial(:summary) %>
3
+ <%
4
+ redis_info = begin
5
+ Sidekiq.redis { |c| c.info }
6
+ rescue
7
+ {}
8
+ end
9
+ %>
10
+
11
+ <div class="page-head">
12
+ <div class="page-title-block">
13
+ <div class="eyebrow">
14
+ <span>Real-time</span>
15
+ <span class="pill" id="pollPill">5s POLL</span>
16
+ </div>
17
+ <h1>
18
+ <span class="queue-name">Dashboard</span>
19
+ <span class="badge-num" id="liveBadge" style="background:rgba(164,255,92,.12);border-color:rgba(164,255,92,.3);color:var(--lime)"><span class="mono">●</span> live</span>
20
+ </h1>
21
+ </div>
22
+ <div class="page-head-meta">
23
+ <div class="meta-item" style="min-width:240px">
24
+ <span class="meta-k">Polling interval</span>
25
+ <div style="display:flex; align-items:center; gap:10px; width:100%;">
26
+ <input type="range" id="pollSlider" min="1" max="30" value="5" class="poll-slider"/>
27
+ <span class="meta-v" style="min-width:46px;text-align:right;color:var(--violet-200)" id="pollValue">5 sec</span>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ </div>
32
+
33
+ <!-- Realtime chart panel -->
34
+ <div class="panel">
35
+ <div class="panel-head">
36
+ <div class="ph-title">Throughput · last <span id="chartWindow">60s</span></div>
37
+ <div class="ph-meta"><span id="chartTime"></span></div>
38
+ <div class="ph-spacer"></div>
39
+ <div style="display:flex; align-items:center; gap:14px; font-size:12px; color:var(--text-dim);">
40
+ <span style="display:inline-flex; align-items:center; gap:6px"><span class="legend-dot" style="background:var(--cyan); box-shadow:0 0 6px var(--cyan)"></span>Processed</span>
41
+ <span style="display:inline-flex; align-items:center; gap:6px"><span class="legend-dot" style="background:var(--red); box-shadow:0 0 6px var(--red)"></span>Failed</span>
42
+ </div>
43
+ </div>
44
+ <div class="chart-wrap">
45
+ <svg id="liveChart" viewBox="0 0 1200 280" preserveAspectRatio="none"></svg>
46
+ <div class="chart-tooltip" id="chartTip">
47
+ <div class="tip-time" id="tipTime"></div>
48
+ <div class="tip-row"><span class="legend-dot" style="background:var(--cyan)"></span>Processed: <b id="tipProc">0</b></div>
49
+ <div class="tip-row"><span class="legend-dot" style="background:var(--red)"></span>Failed: <b id="tipFail">0</b></div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <!-- History panel -->
55
+ <div class="panel">
56
+ <div class="panel-head">
57
+ <div class="ph-title">History</div>
58
+ <div class="ph-meta">Total processed over selected period</div>
59
+ <div class="ph-spacer"></div>
60
+ <div class="filter-group" id="historyTabs" style="gap:4px">
61
+ <button class="chip" data-range="week">1 week</button>
62
+ <button class="chip active" data-range="month">1 month</button>
63
+ <button class="chip" data-range="3m">3 months</button>
64
+ <button class="chip" data-range="6m">6 months</button>
65
+ </div>
66
+ </div>
67
+ <div class="chart-wrap" style="padding: 24px 28px 28px;">
68
+ <svg id="historyChart" viewBox="0 0 1200 320" preserveAspectRatio="none"></svg>
69
+ </div>
70
+ </div>
71
+
72
+ <!-- Redis panel -->
73
+ <div class="panel">
74
+ <div class="panel-head">
75
+ <div class="ph-title">Redis</div>
76
+ <div class="ph-meta"><%= action.h(redis_info["redis_version"] ? "v#{redis_info["redis_version"]} · " : "") %>connected</div>
77
+ <div class="ph-spacer"></div>
78
+ <span class="ctx-pill" style="color:var(--lime); border-color:rgba(164,255,92,.3); background:rgba(164,255,92,.06)">
79
+ <span class="ctx-dot" style="background:var(--lime); box-shadow:0 0 6px var(--lime)"></span>healthy
80
+ </span>
81
+ </div>
82
+ <div class="redis-grid">
83
+ <div class="redis-card" data-tone="violet">
84
+ <div class="redis-num"><%= action.h(redis_info["redis_version"] || "—") %></div>
85
+ <div class="redis-label">Version</div>
86
+ <div class="redis-foot">stable channel</div>
87
+ </div>
88
+ <div class="redis-card" data-tone="cyan">
89
+ <div class="redis-num"><%= action.h(redis_info["uptime_in_days"] || "—") %></div>
90
+ <div class="redis-label">Uptime <small>(days)</small></div>
91
+ <div class="redis-foot">&nbsp;</div>
92
+ </div>
93
+ <div class="redis-card" data-tone="magenta">
94
+ <div class="redis-num"><%= action.h(redis_info["connected_clients"] || "—") %></div>
95
+ <div class="redis-label">Connections</div>
96
+ <div class="redis-foot">&nbsp;</div>
97
+ </div>
98
+ <%
99
+ _used_bytes = redis_info["used_memory"].to_i
100
+ _peak_bytes = redis_info["used_memory_peak"].to_i
101
+ _mem_pct = (_peak_bytes > 0 ? [(_used_bytes * 100.0 / _peak_bytes).round, 100].min : 0)
102
+ %>
103
+ <div class="redis-card" data-tone="amber">
104
+ <div class="redis-num"><%= action.h(redis_info["used_memory_human"] || "—") %></div>
105
+ <div class="redis-label">Memory Usage</div>
106
+ <div class="redis-bar"><i style="width:<%= _mem_pct %>%; background:var(--amber)"></i></div>
107
+ </div>
108
+ <div class="redis-card" data-tone="red">
109
+ <div class="redis-num"><%= action.h(redis_info["used_memory_peak_human"] || "—") %></div>
110
+ <div class="redis-label">Peak Memory</div>
111
+ <div class="redis-foot">&nbsp;</div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+
116
+ <script nonce="<%= nonce %>">
117
+ window.HISTORY_DATA = {
118
+ processed: <%= @history.processed.values.reverse.to_json %>,
119
+ failed: <%= @history.failed.values.reverse.to_json %>,
120
+ labels: <%= @history.processed.keys.reverse.to_json %>
121
+ };
122
+ window.SIDE_BRO_ROOT = '<%= action.root_path %>';
123
+ </script>
124
+ <script src="<%= action.root_path %>assets/javascripts/dashboard.js" nonce="<%= nonce %>"></script>
@@ -0,0 +1,31 @@
1
+ <% @page_title = "Dead: #{action.job_display_class(@job)}" %>
2
+ <% @breadcrumb = [{label: "Dead", href: "#{action.root_path}morgue"}, {label: action.job_display_class(@job)}] %>
3
+
4
+ <div class="page-head">
5
+ <div class="page-title-block">
6
+ <div class="eyebrow"><span>Lifecycle</span></div>
7
+ <h1><span class="queue-name"><%= action.h(action.job_display_class(@job)) %></span></h1>
8
+ </div>
9
+ <div class="page-head-meta">
10
+ <form method="post" action="<%= action.root_path %>morgue" style="display:inline-flex; gap:8px;">
11
+ <input type="hidden" name="key[]" value="<%= action.h(@job.jid) %>">
12
+ <button type="submit" name="action" value="retry" class="btn ghost">
13
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
14
+ <%= action.t(:retry_now) %>
15
+ </button>
16
+ <button type="submit" name="action" value="delete" class="btn danger">
17
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>
18
+ <%= action.t(:delete) %>
19
+ </button>
20
+ </form>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="panel">
25
+ <div class="panel-head">
26
+ <div class="ph-title">Job Details</div>
27
+ </div>
28
+ <div style="padding: 20px 16px;">
29
+ <%= action.render_partial(:job_info) %>
30
+ </div>
31
+ </div>
@@ -0,0 +1,61 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= action.locale %>" dir="<%= action.rtl? ? 'rtl' : 'ltr' %>">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>SideBro<%= @page_title ? " — #{@page_title}" : "" %></title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="<%= action.root_path %>assets/stylesheets/style.css" nonce="<%= nonce %>">
11
+ </head>
12
+ <body>
13
+ <div class="app">
14
+
15
+ <%= action.render_partial(:nav) %>
16
+
17
+ <main class="main">
18
+ <div class="topbar">
19
+ <div class="breadcrumb">
20
+ <%- if @breadcrumb && @breadcrumb.length > 1 %>
21
+ <%- @breadcrumb[0..-2].each do |item| %>
22
+ <span><%= action.h(item[:label]) %></span>
23
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 6 15 12 9 18"/></svg>
24
+ <%- end %>
25
+ <b><%= action.h(@breadcrumb.last[:label]) %></b>
26
+ <%- elsif @breadcrumb %>
27
+ <b><%= action.h(@breadcrumb.last[:label]) %></b>
28
+ <%- else %>
29
+ <b><%= action.h(@page_title || "Dashboard") %></b>
30
+ <%- end %>
31
+ </div>
32
+ <div class="topbar-actions">
33
+ <button class="live-toggle" id="liveToggle">
34
+ <span class="pulse"></span>
35
+ <span>Live · <span class="mono" id="liveInterval">5s</span></span>
36
+ </button>
37
+ <button class="icon-btn" title="Refresh" id="refreshBtn">
38
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
39
+ </button>
40
+ </div>
41
+ </div>
42
+
43
+ <%- if action.flash.any? %>
44
+ <div class="flash-bar">
45
+ <%- %w[notice error].each do |_level| %>
46
+ <%- if (msg = action.flash[_level]) %>
47
+ <div class="flash flash-<%= _level %>"><%= action.h(msg) %></div>
48
+ <%- end %>
49
+ <%- end %>
50
+ </div>
51
+ <%- end %>
52
+
53
+ <%= content %>
54
+
55
+ <%= action.render_partial(:footer) %>
56
+ </main>
57
+
58
+ </div>
59
+ <script src="<%= action.root_path %>assets/javascripts/application.js" nonce="<%= nonce %>"></script>
60
+ </body>
61
+ </html>
@@ -0,0 +1,56 @@
1
+ <% @page_title = "Metrics" %>
2
+
3
+ <div class="page-head">
4
+ <div class="page-title-block">
5
+ <div class="eyebrow"><span>Monitoring</span></div>
6
+ <h1><%= action.t(:metrics) %></h1>
7
+ </div>
8
+ <div class="page-head-meta">
9
+ <%= action.render_partial(:metrics_period_select) %>
10
+ </div>
11
+ </div>
12
+
13
+ <%- if !action.metrics_enabled? %>
14
+ <div class="panel">
15
+ <div class="empty">Metrics require Sidekiq 7.0 or later.</div>
16
+ </div>
17
+ <%- elsif @metrics.nil? || @metrics.job_results.empty? %>
18
+ <div class="panel">
19
+ <div class="empty"><%= action.t(:no_jobs) %></div>
20
+ </div>
21
+ <%- else %>
22
+ <div class="panel">
23
+ <div class="panel-head">
24
+ <div class="ph-title">Job Metrics</div>
25
+ <div class="ph-meta mono">Period: <%= @period %></div>
26
+ </div>
27
+ <table>
28
+ <thead>
29
+ <tr>
30
+ <th><%= action.t(:job_class) %></th>
31
+ <th><%= action.t(:success) %></th>
32
+ <th><%= action.t(:failed) %></th>
33
+ <th><%= action.t(:total_exec_time) %></th>
34
+ <th><%= action.t(:avg_exec_time) %></th>
35
+ </tr>
36
+ </thead>
37
+ <tbody>
38
+ <%- @metrics.job_results.first(20).each do |klass, result| %>
39
+ <%- processed = result.totals["p"].to_i %>
40
+ <%- failures = result.totals["f"].to_i %>
41
+ <%- success = processed - failures %>
42
+ <%- total_s = result.totals["s"].to_f %>
43
+ <%- completed = [success, 0].max %>
44
+ <%- avg_s = completed > 0 ? total_s / completed : 0.0 %>
45
+ <tr>
46
+ <td><a href="<%= action.root_path %>metrics/<%= action.h(klass) %>"><span class="job-name"><%= action.h(klass) %></span></a></td>
47
+ <td><span style="color:var(--lime)"><%= success %></span></td>
48
+ <td><span style="color:var(--red)"><%= failures %></span></td>
49
+ <td><span class="mono" style="font-size:12px"><%= "%.2fs" % total_s %></span></td>
50
+ <td><span class="mono" style="font-size:12px"><%= "%.3fs" % avg_s %></span></td>
51
+ </tr>
52
+ <%- end %>
53
+ </tbody>
54
+ </table>
55
+ </div>
56
+ <%- end %>