solid_stack_web 0.3.0 → 0.5.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +117 -26
  3. data/app/assets/stylesheets/solid_stack_web/_03_stats.css +27 -0
  4. data/app/assets/stylesheets/solid_stack_web/_04_table.css +4 -0
  5. data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +85 -12
  6. data/app/assets/stylesheets/solid_stack_web/_09_detail.css +42 -0
  7. data/app/controllers/solid_stack_web/application_controller.rb +1 -1
  8. data/app/controllers/solid_stack_web/cache/flushes_controller.rb +8 -0
  9. data/app/controllers/solid_stack_web/cache_controller.rb +2 -0
  10. data/app/controllers/solid_stack_web/cache_entries_controller.rb +32 -0
  11. data/app/controllers/solid_stack_web/dashboard_controller.rb +4 -16
  12. data/app/controllers/solid_stack_web/metrics_controller.rb +15 -0
  13. data/app/controllers/solid_stack_web/queues_controller.rb +4 -0
  14. data/app/controllers/solid_stack_web/stats_controller.rb +39 -0
  15. data/app/helpers/solid_stack_web/application_helper.rb +90 -2
  16. data/app/javascript/solid_stack_web/application.js +7 -1
  17. data/app/javascript/solid_stack_web/refresh_controller.js +52 -0
  18. data/app/javascript/solid_stack_web/search_controller.js +16 -0
  19. data/app/javascript/solid_stack_web/sparkline_tooltip_controller.js +23 -0
  20. data/app/models/solid_stack_web/alert_webhook.rb +67 -0
  21. data/app/models/solid_stack_web/cable_stats.rb +10 -0
  22. data/app/models/solid_stack_web/cache_size_stats.rb +33 -0
  23. data/app/models/solid_stack_web/cache_stats.rb +11 -0
  24. data/app/models/solid_stack_web/cache_timeline.rb +40 -0
  25. data/app/models/solid_stack_web/queue_depth_sparkline.rb +30 -0
  26. data/app/models/solid_stack_web/queue_stats.rb +34 -0
  27. data/app/models/solid_stack_web/throughput_sparkline.rb +23 -0
  28. data/app/views/layouts/solid_stack_web/application.html.erb +13 -0
  29. data/app/views/solid_stack_web/cache/index.html.erb +84 -2
  30. data/app/views/solid_stack_web/cache_entries/index.html.erb +66 -0
  31. data/app/views/solid_stack_web/cache_entries/show.html.erb +48 -0
  32. data/app/views/solid_stack_web/dashboard/index.html.erb +45 -4
  33. data/app/views/solid_stack_web/history/index.html.erb +6 -3
  34. data/app/views/solid_stack_web/jobs/index.html.erb +11 -7
  35. data/app/views/solid_stack_web/processes/index.html.erb +3 -0
  36. data/app/views/solid_stack_web/queues/index.html.erb +6 -1
  37. data/app/views/solid_stack_web/stats/index.html.erb +48 -0
  38. data/config/importmap.rb +5 -0
  39. data/config/routes.rb +4 -0
  40. data/lib/solid_stack_web/version.rb +1 -1
  41. data/lib/solid_stack_web.rb +41 -1
  42. metadata +19 -1
@@ -1,3 +1,5 @@
1
+ <%= turbo_frame_tag "sqw-dashboard", target: "_top",
2
+ data: { controller: "refresh", refresh_interval_value: SolidStackWeb.dashboard_refresh_interval } do %>
1
3
  <div class="sqw-page-header">
2
4
  <h1 class="sqw-page-title">Overview</h1>
3
5
  </div>
@@ -29,10 +31,42 @@
29
31
  <span class="sqw-inline-stat__label">Failed</span>
30
32
  <span class="sqw-inline-stat__value"><%= @queue_stats[:failed] %></span>
31
33
  </a>
34
+ <a href="<%= history_path(period: "1h") %>" class="sqw-inline-stat sqw-inline-stat--neutral">
35
+ <span class="sqw-inline-stat__label">Done (1h)</span>
36
+ <span class="sqw-inline-stat__value"><%= @queue_stats[:done_1h] %></span>
37
+ </a>
38
+ <a href="<%= history_path(period: "24h") %>" class="sqw-inline-stat sqw-inline-stat--neutral">
39
+ <span class="sqw-inline-stat__label">Done (24h)</span>
40
+ <span class="sqw-inline-stat__value"><%= @queue_stats[:done_24h] %></span>
41
+ </a>
42
+ <% if @queue_stats.key?(:slow_jobs) %>
43
+ <a href="<%= stats_path %>" class="sqw-inline-stat sqw-inline-stat--<%= @queue_stats[:slow_jobs] > 0 ? "failed" : "neutral" %>">
44
+ <span class="sqw-inline-stat__label">Slow (24h)</span>
45
+ <span class="sqw-inline-stat__value"><%= @queue_stats[:slow_jobs] %></span>
46
+ </a>
47
+ <% end %>
32
48
  <a href="<%= processes_path %>" class="sqw-inline-stat sqw-inline-stat--neutral">
33
- <span class="sqw-inline-stat__label">Processes</span>
34
- <span class="sqw-inline-stat__value"><%= @queue_stats[:processes] %></span>
49
+ <span class="sqw-inline-stat__label">Healthy</span>
50
+ <span class="sqw-inline-stat__value"><%= @queue_stats[:processes_healthy] %></span>
35
51
  </a>
52
+ <% if @queue_stats[:processes_stale] > 0 %>
53
+ <a href="<%= processes_path %>" class="sqw-inline-stat sqw-inline-stat--failed">
54
+ <span class="sqw-inline-stat__label">Stale</span>
55
+ <span class="sqw-inline-stat__value"><%= @queue_stats[:processes_stale] %></span>
56
+ </a>
57
+ <% end %>
58
+ </div>
59
+ <div class="sqw-sparkline-wrap" data-controller="sparkline-tooltip">
60
+ <span class="sqw-sparkline-label">Throughput — last 12 hours</span>
61
+ <div class="sqw-sparkline-positioner">
62
+ <%= throughput_sparkline_svg(@throughput) %>
63
+ <div class="sqw-sparkline-tip" data-sparkline-tooltip-target="tip" hidden></div>
64
+ </div>
65
+ <div class="sqw-sparkline-axis">
66
+ <span>12h ago</span>
67
+ <span>6h ago</span>
68
+ <span>now</span>
69
+ </div>
36
70
  </div>
37
71
  </div>
38
72
 
@@ -42,14 +76,20 @@
42
76
  <%= link_to "View Cache →", cache_path, class: "sqw-gem-card__link" %>
43
77
  </div>
44
78
  <div class="sqw-gem-card__body">
45
- <div class="sqw-inline-stat sqw-inline-stat--cache">
79
+ <%= link_to cache_entries_path, class: "sqw-inline-stat sqw-inline-stat--cache" do %>
46
80
  <span class="sqw-inline-stat__label">Entries</span>
47
81
  <span class="sqw-inline-stat__value"><%= @cache_stats[:entries] %></span>
48
- </div>
82
+ <% end %>
49
83
  <div class="sqw-inline-stat sqw-inline-stat--cache">
50
84
  <span class="sqw-inline-stat__label">Size</span>
51
85
  <span class="sqw-inline-stat__value"><%= number_to_human_size(@cache_stats[:byte_size]) %></span>
52
86
  </div>
87
+ <% if @cache_stats[:oldest_entry] %>
88
+ <div class="sqw-inline-stat sqw-inline-stat--cache">
89
+ <span class="sqw-inline-stat__label">Oldest</span>
90
+ <span class="sqw-inline-stat__value sqw-inline-stat__value--sm" title="<%= @cache_stats[:oldest_entry].strftime("%b %d, %Y %H:%M") %>"><%= time_ago_in_words(@cache_stats[:oldest_entry]) %></span>
91
+ </div>
92
+ <% end %>
53
93
  </div>
54
94
  </div>
55
95
 
@@ -70,3 +110,4 @@
70
110
  </div>
71
111
  </div>
72
112
  </div>
113
+ <% end %>
@@ -1,3 +1,5 @@
1
+ <%= turbo_frame_tag "sqw-history-table",
2
+ data: { controller: "refresh", refresh_interval_value: SolidStackWeb.default_refresh_interval } do %>
1
3
  <div class="sqw-page-header sqw-page-header--split">
2
4
  <h1 class="sqw-page-title">Job History</h1>
3
5
  <div class="sqw-header-actions">
@@ -8,14 +10,14 @@
8
10
  </div>
9
11
  </div>
10
12
 
11
- <form class="sqw-filters" action="<%= history_path %>" method="get">
13
+ <form class="sqw-filters" action="<%= history_path %>" method="get" data-controller="search">
12
14
  <% if @queue.present? %>
13
15
  <input type="hidden" name="queue" value="<%= @queue %>">
14
16
  <% end %>
15
17
  <input type="hidden" name="period" value="<%= @period %>">
16
18
  <input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
17
- placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class">
18
- <button type="submit" class="sqw-btn sqw-btn--muted sqw-btn--sm">Search</button>
19
+ placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
20
+ data-action="input->search#filter">
19
21
  <% if @search.present? %>
20
22
  <%= link_to "Clear", history_path(queue: @queue, period: @period), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
21
23
  <% end %>
@@ -70,4 +72,5 @@
70
72
  <div class="sqw-empty">
71
73
  <p>No finished jobs found.</p>
72
74
  </div>
75
+ <% end %>
73
76
  <% end %>
@@ -29,14 +29,18 @@
29
29
  <% end %>
30
30
  </div>
31
31
 
32
- <turbo-frame id="sqw-jobs-filter" data-turbo-action="advance">
33
- <form class="sqw-filters" action="<%= jobs_path %>" method="get">
32
+ <%= turbo_frame_tag "sqw-jobs-filter",
33
+ data: { turbo_action: "advance", controller: "refresh",
34
+ refresh_interval_value: SolidStackWeb.default_refresh_interval } do %>
35
+ <form class="sqw-filters" action="<%= jobs_path %>" method="get" data-controller="search">
34
36
  <%= hidden_field_tag :status, @status %>
35
37
  <%= hidden_field_tag :period, @period %>
36
38
  <input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
37
- placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class">
39
+ placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
40
+ data-action="input->search#filter">
38
41
  <% if @queue_options.size > 1 %>
39
- <select name="queue" class="sqw-select" aria-label="Filter by queue" onchange="this.form.requestSubmit()">
42
+ <select name="queue" class="sqw-select" aria-label="Filter by queue"
43
+ data-action="change->search#select">
40
44
  <option value="">All queues</option>
41
45
  <% @queue_options.each do |q| %>
42
46
  <option value="<%= q %>" <%= "selected" if @queue == q %>><%= q %></option>
@@ -44,14 +48,14 @@
44
48
  </select>
45
49
  <% end %>
46
50
  <% if @priority_options.size > 1 %>
47
- <select name="priority" class="sqw-select" aria-label="Filter by priority" onchange="this.form.requestSubmit()">
51
+ <select name="priority" class="sqw-select" aria-label="Filter by priority"
52
+ data-action="change->search#select">
48
53
  <option value="">All priorities</option>
49
54
  <% @priority_options.each do |p| %>
50
55
  <option value="<%= p %>" <%= "selected" if @priority.to_s == p.to_s %>>Priority <%= p %></option>
51
56
  <% end %>
52
57
  </select>
53
58
  <% end %>
54
- <button type="submit" class="sqw-btn sqw-btn--muted sqw-btn--sm">Search</button>
55
59
  <% if @search.present? || @queue.present? || @priority.present? %>
56
60
  <%= link_to "Clear", jobs_path(status: @status, period: @period), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
57
61
  <% end %>
@@ -151,4 +155,4 @@
151
155
  <%= render "empty" %>
152
156
  <% end %>
153
157
  </div>
154
- </turbo-frame>
158
+ <% end %>
@@ -1,3 +1,5 @@
1
+ <%= turbo_frame_tag "sqw-processes", target: "_top",
2
+ data: { controller: "refresh", refresh_interval_value: SolidStackWeb.default_refresh_interval } do %>
1
3
  <div class="sqw-page-header">
2
4
  <h1 class="sqw-page-title">Processes</h1>
3
5
  </div>
@@ -30,3 +32,4 @@
30
32
  <p>No active processes.</p>
31
33
  </div>
32
34
  <% end %>
35
+ <% end %>
@@ -8,6 +8,7 @@
8
8
  <tr>
9
9
  <th>Name</th>
10
10
  <th>Size</th>
11
+ <th>Depth (12h)</th>
11
12
  <th>Status</th>
12
13
  <th></th>
13
14
  </tr>
@@ -17,6 +18,10 @@
17
18
  <tr>
18
19
  <td class="sqw-monospace"><%= link_to queue[:name], queue_path(queue[:name]) %></td>
19
20
  <td><%= link_to queue[:size], queue_path(queue[:name]) %></td>
21
+ <td class="sqw-queue-sparkline" data-controller="sparkline-tooltip">
22
+ <%= queue_depth_sparkline_svg(@sparklines[queue[:name]]) %>
23
+ <div class="sqw-sparkline-tip" data-sparkline-tooltip-target="tip" hidden></div>
24
+ </td>
20
25
  <td>
21
26
  <% if queue[:paused] %>
22
27
  <span class="sqw-badge sqw-badge--paused">Paused</span>
@@ -41,4 +46,4 @@
41
46
  <div class="sqw-empty">
42
47
  <p>No queues with ready jobs.</p>
43
48
  </div>
44
- <% end %>
49
+ <% end %>
@@ -0,0 +1,48 @@
1
+ <div class="sqw-page-header">
2
+ <h1 class="sqw-page-title">Performance Stats</h1>
3
+ </div>
4
+
5
+ <% if @stats.any? %>
6
+ <table class="sqw-table">
7
+ <thead>
8
+ <tr>
9
+ <% [
10
+ ["class_name", "Job Class"],
11
+ ["count", "Executions"],
12
+ ["avg", "Avg"],
13
+ ["p50", "p50"],
14
+ ["p95", "p95"],
15
+ ["min", "Min"],
16
+ ["max", "Max"]
17
+ ].each do |col, label| %>
18
+ <th>
19
+ <% next_dir = (@sort == col && @direction == "desc") ? "asc" : "desc" %>
20
+ <%= link_to stats_path(sort: col, direction: next_dir) do %>
21
+ <%= label %>
22
+ <% if @sort == col %>
23
+ <span class="sqw-sort-indicator"><%= @direction == "desc" ? "↓" : "↑" %></span>
24
+ <% end %>
25
+ <% end %>
26
+ </th>
27
+ <% end %>
28
+ </tr>
29
+ </thead>
30
+ <tbody>
31
+ <% @stats.each do |row| %>
32
+ <tr>
33
+ <td class="sqw-monospace"><%= row[:class_name] %></td>
34
+ <td><%= row[:count] %></td>
35
+ <td class="sqw-muted"><%= format_duration(row[:avg]) %></td>
36
+ <td class="sqw-muted"><%= format_duration(row[:p50]) %></td>
37
+ <td><strong><%= format_duration(row[:p95]) %></strong></td>
38
+ <td class="sqw-muted"><%= format_duration(row[:min]) %></td>
39
+ <td class="sqw-muted"><%= format_duration(row[:max]) %></td>
40
+ </tr>
41
+ <% end %>
42
+ </tbody>
43
+ </table>
44
+ <% else %>
45
+ <div class="sqw-empty">
46
+ <p>No finished jobs yet.</p>
47
+ </div>
48
+ <% end %>
data/config/importmap.rb CHANGED
@@ -1,2 +1,7 @@
1
+ pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.23/dist/turbo.es2017-esm.js"
2
+ pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js"
1
3
  pin "solid_stack_web", to: "solid_stack_web/application.js"
4
+ pin "solid_stack_web/refresh_controller", to: "solid_stack_web/refresh_controller.js"
5
+ pin "solid_stack_web/search_controller", to: "solid_stack_web/search_controller.js"
2
6
  pin "solid_stack_web/selection_controller", to: "solid_stack_web/selection_controller.js"
7
+ pin "solid_stack_web/sparkline_tooltip_controller", to: "solid_stack_web/sparkline_tooltip_controller.js"
data/config/routes.rb CHANGED
@@ -31,7 +31,11 @@ SolidStackWeb::Engine.routes.draw do
31
31
 
32
32
  resources :processes, only: [:index]
33
33
 
34
+ get "metrics", to: "metrics#index", as: :metrics
35
+ get "stats", to: "stats#index", as: :stats
34
36
  get "history", to: "history#index", as: :history
35
37
  get "cache", to: "cache#index", as: :cache
38
+ resources :cache_entries, only: [:index, :show, :destroy], path: "cache/entries"
39
+ resource :cache_flush, only: [:destroy], path: "cache/flush", controller: "cache/flushes"
36
40
  get "cable", to: "cable#index", as: :cable
37
41
  end
@@ -1,3 +1,3 @@
1
1
  module SolidStackWeb
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -3,7 +3,11 @@ require "solid_stack_web/engine"
3
3
 
4
4
  module SolidStackWeb
5
5
  class << self
6
- attr_writer :page_size, :connects_to
6
+ attr_writer :page_size, :connects_to, :slow_job_threshold,
7
+ :alert_webhook_url, :alert_webhook_cooldown,
8
+ :alert_failure_threshold, :alert_queue_thresholds,
9
+ :dashboard_refresh_interval, :default_refresh_interval,
10
+ :search_results_limit, :allow_value_preview
7
11
 
8
12
  def page_size
9
13
  @page_size || 25
@@ -13,6 +17,42 @@ module SolidStackWeb
13
17
  @connects_to
14
18
  end
15
19
 
20
+ def slow_job_threshold
21
+ @slow_job_threshold
22
+ end
23
+
24
+ def alert_webhook_url
25
+ @alert_webhook_url
26
+ end
27
+
28
+ def alert_webhook_cooldown
29
+ @alert_webhook_cooldown || 3600
30
+ end
31
+
32
+ def alert_failure_threshold
33
+ @alert_failure_threshold
34
+ end
35
+
36
+ def alert_queue_thresholds
37
+ @alert_queue_thresholds || {}
38
+ end
39
+
40
+ def dashboard_refresh_interval
41
+ @dashboard_refresh_interval || 5_000
42
+ end
43
+
44
+ def default_refresh_interval
45
+ @default_refresh_interval || 10_000
46
+ end
47
+
48
+ def search_results_limit
49
+ @search_results_limit || 25
50
+ end
51
+
52
+ def allow_value_preview
53
+ @allow_value_preview || false
54
+ end
55
+
16
56
  def configure
17
57
  yield self
18
58
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_stack_web
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -145,7 +145,9 @@ files:
145
145
  - app/assets/stylesheets/solid_stack_web/application.css
146
146
  - app/controllers/solid_stack_web/application_controller.rb
147
147
  - app/controllers/solid_stack_web/cable_controller.rb
148
+ - app/controllers/solid_stack_web/cache/flushes_controller.rb
148
149
  - app/controllers/solid_stack_web/cache_controller.rb
150
+ - app/controllers/solid_stack_web/cache_entries_controller.rb
149
151
  - app/controllers/solid_stack_web/dashboard_controller.rb
150
152
  - app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb
151
153
  - app/controllers/solid_stack_web/failed_jobs/selections_controller.rb
@@ -153,19 +155,34 @@ files:
153
155
  - app/controllers/solid_stack_web/history_controller.rb
154
156
  - app/controllers/solid_stack_web/jobs/selections_controller.rb
155
157
  - app/controllers/solid_stack_web/jobs_controller.rb
158
+ - app/controllers/solid_stack_web/metrics_controller.rb
156
159
  - app/controllers/solid_stack_web/processes_controller.rb
157
160
  - app/controllers/solid_stack_web/queues/pauses_controller.rb
158
161
  - app/controllers/solid_stack_web/queues_controller.rb
159
162
  - app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb
160
163
  - app/controllers/solid_stack_web/recurring_tasks_controller.rb
161
164
  - app/controllers/solid_stack_web/scheduled_jobs_controller.rb
165
+ - app/controllers/solid_stack_web/stats_controller.rb
162
166
  - app/helpers/solid_stack_web/application_helper.rb
163
167
  - app/javascript/solid_stack_web/application.js
168
+ - app/javascript/solid_stack_web/refresh_controller.js
169
+ - app/javascript/solid_stack_web/search_controller.js
164
170
  - app/javascript/solid_stack_web/selection_controller.js
171
+ - app/javascript/solid_stack_web/sparkline_tooltip_controller.js
172
+ - app/models/solid_stack_web/alert_webhook.rb
173
+ - app/models/solid_stack_web/cable_stats.rb
174
+ - app/models/solid_stack_web/cache_size_stats.rb
175
+ - app/models/solid_stack_web/cache_stats.rb
176
+ - app/models/solid_stack_web/cache_timeline.rb
165
177
  - app/models/solid_stack_web/job.rb
178
+ - app/models/solid_stack_web/queue_depth_sparkline.rb
179
+ - app/models/solid_stack_web/queue_stats.rb
180
+ - app/models/solid_stack_web/throughput_sparkline.rb
166
181
  - app/views/layouts/solid_stack_web/application.html.erb
167
182
  - app/views/solid_stack_web/cable/index.html.erb
168
183
  - app/views/solid_stack_web/cache/index.html.erb
184
+ - app/views/solid_stack_web/cache_entries/index.html.erb
185
+ - app/views/solid_stack_web/cache_entries/show.html.erb
169
186
  - app/views/solid_stack_web/dashboard/index.html.erb
170
187
  - app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb
171
188
  - app/views/solid_stack_web/failed_jobs/index.html.erb
@@ -180,6 +197,7 @@ files:
180
197
  - app/views/solid_stack_web/queues/show.html.erb
181
198
  - app/views/solid_stack_web/recurring_tasks/index.html.erb
182
199
  - app/views/solid_stack_web/scheduled_jobs/update.turbo_stream.erb
200
+ - app/views/solid_stack_web/stats/index.html.erb
183
201
  - config/importmap.rb
184
202
  - config/routes.rb
185
203
  - lib/solid_stack_web.rb