roundhouse_ui 0.9.1 → 0.11.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +620 -46
  3. data/app/controllers/concerns/roundhouse_ui/job_set_browsing.rb +246 -12
  4. data/app/controllers/roundhouse_ui/application_controller.rb +74 -0
  5. data/app/controllers/roundhouse_ui/audit_controller.rb +1 -0
  6. data/app/controllers/roundhouse_ui/busy_controller.rb +25 -5
  7. data/app/controllers/roundhouse_ui/dashboard_controller.rb +27 -3
  8. data/app/controllers/roundhouse_ui/dead_controller.rb +27 -10
  9. data/app/controllers/roundhouse_ui/errors_controller.rb +60 -1
  10. data/app/controllers/roundhouse_ui/jobs_controller.rb +5 -1
  11. data/app/controllers/roundhouse_ui/queues_controller.rb +57 -6
  12. data/app/controllers/roundhouse_ui/recurring_controller.rb +12 -0
  13. data/app/controllers/roundhouse_ui/retries_controller.rb +34 -10
  14. data/app/controllers/roundhouse_ui/scheduled_controller.rb +4 -7
  15. data/app/controllers/roundhouse_ui/settings_controller.rb +15 -0
  16. data/app/controllers/roundhouse_ui/snapshots_controller.rb +5 -6
  17. data/app/controllers/roundhouse_ui/workers_controller.rb +2 -7
  18. data/app/helpers/roundhouse_ui/application_helper.rb +293 -0
  19. data/app/helpers/roundhouse_ui/nav_helper.rb +3 -3
  20. data/app/helpers/roundhouse_ui/observability_helper.rb +121 -12
  21. data/app/helpers/roundhouse_ui/tags_helper.rb +128 -0
  22. data/app/views/layouts/roundhouse_ui/application.html.erb +1277 -84
  23. data/app/views/roundhouse_ui/busy/index.html.erb +30 -6
  24. data/app/views/roundhouse_ui/dashboard/show.html.erb +62 -17
  25. data/app/views/roundhouse_ui/dead/index.html.erb +54 -32
  26. data/app/views/roundhouse_ui/errors/index.html.erb +51 -11
  27. data/app/views/roundhouse_ui/jobs/show.html.erb +8 -3
  28. data/app/views/roundhouse_ui/metrics/show.html.erb +26 -3
  29. data/app/views/roundhouse_ui/queues/index.html.erb +54 -10
  30. data/app/views/roundhouse_ui/queues/show.html.erb +55 -0
  31. data/app/views/roundhouse_ui/recurring/index.html.erb +71 -0
  32. data/app/views/roundhouse_ui/retries/index.html.erb +30 -17
  33. data/app/views/roundhouse_ui/scheduled/index.html.erb +17 -11
  34. data/app/views/roundhouse_ui/settings/show.html.erb +91 -0
  35. data/app/views/roundhouse_ui/shared/_filter_error.html.erb +23 -0
  36. data/app/views/roundhouse_ui/shared/_pager.html.erb +2 -2
  37. data/app/views/roundhouse_ui/shared/_search_bar.html.erb +55 -0
  38. data/app/views/roundhouse_ui/shared/_search_help.html.erb +37 -0
  39. data/app/views/roundhouse_ui/shared/_tag_filter.html.erb +28 -0
  40. data/app/views/roundhouse_ui/shared/bulk_preview.html.erb +69 -0
  41. data/config/routes.rb +9 -0
  42. data/lib/roundhouse_ui/backends/sidekiq.rb +124 -5
  43. data/lib/roundhouse_ui/backends/solid_queue.rb +54 -3
  44. data/lib/roundhouse_ui/demo.rb +130 -0
  45. data/lib/roundhouse_ui/error_groups.rb +25 -4
  46. data/lib/roundhouse_ui/filter_query.rb +504 -0
  47. data/lib/roundhouse_ui/health.rb +43 -4
  48. data/lib/roundhouse_ui/history.rb +54 -0
  49. data/lib/roundhouse_ui/icons.rb +104 -0
  50. data/lib/roundhouse_ui/marks/datadog-lockup-white.svg +30 -0
  51. data/lib/roundhouse_ui/marks/datadog-lockup.svg +41 -0
  52. data/lib/roundhouse_ui/observability.rb +55 -7
  53. data/lib/roundhouse_ui/pause.rb +6 -11
  54. data/lib/roundhouse_ui/queue_summary.rb +11 -0
  55. data/lib/roundhouse_ui/recurring.rb +137 -0
  56. data/lib/roundhouse_ui/runbooks.rb +75 -0
  57. data/lib/roundhouse_ui/snapshots.rb +13 -2
  58. data/lib/roundhouse_ui/tags.rb +112 -0
  59. data/lib/roundhouse_ui/theme.rb +298 -0
  60. data/lib/roundhouse_ui/version.rb +1 -1
  61. data/lib/roundhouse_ui.rb +235 -5
  62. data/lib/tasks/roundhouse_ui_tasks.rake +84 -4
  63. metadata +32 -7
data/lib/roundhouse_ui.rb CHANGED
@@ -7,6 +7,14 @@ require "roundhouse_ui/snapshots"
7
7
  require "roundhouse_ui/observability"
8
8
  require "roundhouse_ui/audit"
9
9
  require "roundhouse_ui/redaction"
10
+ require "roundhouse_ui/queue_summary"
11
+ require "roundhouse_ui/theme"
12
+ require "roundhouse_ui/tags"
13
+ require "roundhouse_ui/filter_query"
14
+ require "roundhouse_ui/runbooks"
15
+ require "roundhouse_ui/icons"
16
+ require "roundhouse_ui/history"
17
+ require "roundhouse_ui/recurring"
10
18
  require "roundhouse_ui/cancellation"
11
19
  require "roundhouse_ui/cancel_middleware"
12
20
  require "roundhouse_ui/metrics"
@@ -19,6 +27,14 @@ require "roundhouse_ui/backends/solid_queue"
19
27
  # Brand name is "Roundhouse"; the gem and Ruby namespace are RoundhouseUi
20
28
  # (matching the published gem name `roundhouse_ui`).
21
29
  module RoundhouseUi
30
+ # A well-formed Ruby constant path, and a sane bound on its length. Used to
31
+ # decide what may reach String#safe_constantize — see .job_class.
32
+ JOB_CLASS_NAME = /\A[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*\z/
33
+ MAX_JOB_CLASS_NAME = 200
34
+
35
+ # How many distinct warning messages .warn_once remembers before resetting.
36
+ WARN_MEMO_CAP = 200
37
+
22
38
  class << self
23
39
  # When true, destructive actions (purge, retry, delete, …) are disabled.
24
40
  # Mount Roundhouse read-only where operators should only observe.
@@ -56,6 +72,39 @@ module RoundhouseUi
56
72
  # e.g. RoundhouseUi.redact_args = %w[password token secret]. Default: none.
57
73
  attr_accessor :redact_args
58
74
 
75
+ # Host-defined job tags, resolved at read time (see ADR 0002): a callable
76
+ # given the job's class name and payload, returning a Hash of tags or nil.
77
+ # `klass` is always the real job class (the ActiveJob adapter wrapper is
78
+ # unwrapped first). For the class-constant convention there's a shorthand:
79
+ #
80
+ # RoundhouseUi.job_tags = RoundhouseUi::Tags.from_constant(:OWNER, as: :squad)
81
+ # # equivalent to:
82
+ # RoundhouseUi.job_tags = ->(klass:, item:) {
83
+ # k = klass.safe_constantize
84
+ # { squad: k.const_get(:OWNER) } if k&.const_defined?(:OWNER)
85
+ # }
86
+ #
87
+ # Tag values render in the UI and pass through redact_args masking (by tag
88
+ # key). Must be cheap: by default it's memoized per class per request and
89
+ # called with item: nil. Default: nil (no tags anywhere).
90
+ attr_accessor :job_tags
91
+
92
+ # Set true when job_tags derives tags from the payload (args, tenant, …):
93
+ # the resolver is then called once per job with the full item, and nothing
94
+ # is cached — a 1,000-entry scan means 1,000 calls, so keep it fast. Leave
95
+ # false for class-derived tags (OWNER-style constants). Default: false.
96
+ attr_accessor :job_tags_per_job
97
+
98
+ # Optional declared filter vocabulary, so tag filter dropdowns are stable
99
+ # instead of discovered from whatever jobs happen to be visible:
100
+ #
101
+ # RoundhouseUi.tag_filters = { squad: %w[core training growth platform ops ai] }
102
+ #
103
+ # Values (or the whole setting) may be callables for dynamic vocabularies.
104
+ # When set, filtering by an undeclared key matches nothing (fail-closed).
105
+ # Default: nil — the filter UI discovers values from the entries it scans.
106
+ attr_accessor :tag_filters
107
+
59
108
  # Opt-in: fold failures recorded by the `sidekiq-failures` gem (its `failed`
60
109
  # sorted set) into the grouped Errors view. Off by default, and a no-op
61
110
  # unless sidekiq-failures is loaded. Jobs with `retry: false` never enter
@@ -73,13 +122,60 @@ module RoundhouseUi
73
122
  # load. Default 5s; raise it if polling shows up in your traces.
74
123
  attr_accessor :poll_interval
75
124
 
76
- # Queue pause/resume is only enforced when RoundhouseUi::Fetch is installed
77
- # as the server's fetch strategy. If you run reliable fetch (Sidekiq
78
- # Pro/Enterprise super_fetch) you can't also run our fetcher, so pause can't
79
- # be enforced — set this to false to hide the pause controls and the
80
- # "not enforced" warning entirely. Default: true.
125
+ # On OSS Sidekiq, pause is enforced only with RoundhouseUi::Fetch installed as the
126
+ # server's fetch strategy; the UI warns until it is. Sidekiq Pro and Solid Queue
127
+ # enforce it natively with no fetcher, so leave this on there. Default: true.
81
128
  attr_accessor :pause_enabled
82
129
 
130
+ # Cancellation only does something if the host opted into it — either by
131
+ # installing RoundhouseUi::CancelMiddleware as server middleware, or by
132
+ # having long-running jobs poll RoundhouseUi.cancelled?(jid) themselves.
133
+ # With neither, cancel! writes a JID that nothing ever reads, so the Busy
134
+ # page's button would be inert. Default false: the button appears once you
135
+ # say the check exists. Also requires a backend that supports :cancel.
136
+ attr_accessor :cancel_enabled
137
+
138
+ # Override any of the UI's colour tokens, as pure CSS custom properties:
139
+ #
140
+ # RoundhouseUi.theme = { accent: "#FF2BD1", accent_2: "#00E5FF" }
141
+ # RoundhouseUi.theme = { dark: { bg: "#0A0511" }, light: { bg: "#FFF7FB" } }
142
+ # RoundhouseUi.theme = RoundhouseUi::Theme::PRESETS[:cyberpunk]
143
+ #
144
+ # Unset tokens keep their shipped values, so a partial theme is fine. Only
145
+ # known tokens are emitted and values are shape-checked — this is
146
+ # interpolated into a stylesheet, where escaping does not make arbitrary
147
+ # input safe. Default: nil.
148
+ attr_accessor :theme
149
+
150
+ # Named palettes a viewer can choose between on the Settings page:
151
+ #
152
+ # RoundhouseUi.themes = { cyberpunk: RoundhouseUi::Theme::PRESETS[:cyberpunk] }
153
+ #
154
+ # `theme` sets the default for everyone; this is the menu each person picks
155
+ # from in their own browser. Default: the shipped presets.
156
+ attr_accessor :themes
157
+
158
+ # How icons render. `:svg` (default) inlines them — no font, no request, no
159
+ # CSP change. `:font_awesome` or a Hash of `{ name => "class names" }` emits
160
+ # class names instead, for hosts that already ship an icon font; Roundhouse
161
+ # never loads one itself either way.
162
+ attr_accessor :icons
163
+
164
+ # Where the runbook for a job class lives. A callable, or a Hash keyed by
165
+ # class name:
166
+ #
167
+ # c.job_runbooks = RoundhouseUi::Runbooks.from_constant(:RUNBOOK)
168
+ # c.job_runbooks = { "Billing::SyncWorker" => "https://wiki/billing" }
169
+ #
170
+ # Resolved at read time like job_tags, so it applies to jobs already in the
171
+ # sets. Only http(s) URLs are rendered. Default: nil.
172
+ attr_accessor :job_runbooks
173
+
174
+ # Set false where an operator should not be able to recolour a production
175
+ # console. Settings then hides palette selection and everyone keeps the
176
+ # host's `theme`. Default: true.
177
+ attr_accessor :allow_theme_selection
178
+
83
179
  # Configure in an initializer:
84
180
  #
85
181
  # RoundhouseUi.configure do |c|
@@ -98,6 +194,135 @@ module RoundhouseUi
98
194
  @backend ||= Backends::Sidekiq.new
99
195
  end
100
196
 
197
+ # ActiveJob-on-Sidekiq stores the adapter's JobWrapper in item["class"] and
198
+ # the real job class in item["wrapped"]. Solid Queue and raw Sidekiq workers
199
+ # put the real class in klass, and Solid Queue's synthetic item never carries
200
+ # a "wrapped" key — so this is a no-op there and safe to call unconditionally.
201
+ #
202
+ # Reading item["wrapped"] rather than matching on the wrapper's name also
203
+ # covers Sidekiq 7+/8's native Sidekiq::ActiveJob::Wrapper for free.
204
+ #
205
+ # Use for display, search, grouping and APM links, so all four agree on one
206
+ # string. NOT for re-enqueue: a payload pushed back to Redis must keep
207
+ # item["class"] exactly as Sidekiq stored it, or the job is re-created as a
208
+ # raw worker and fails on every attempt.
209
+ def unwrapped_class(klass, item)
210
+ wrapped = item["wrapped"] if item.is_a?(Hash)
211
+ (wrapped || klass)&.to_s
212
+ end
213
+
214
+ # When a job was enqueued, as a Time, or nil if the payload does not say.
215
+ #
216
+ # Two formats exist three orders of magnitude apart: Sidekiq 8 writes integer
217
+ # epoch milliseconds, 6.5 and 7 write float epoch seconds. Reading one as the
218
+ # other does not give a slightly wrong time, it gives 1970 or the year 58000.
219
+ def enqueued_at(item)
220
+ raw = item["enqueued_at"] || item["created_at"] if item.is_a?(Hash)
221
+ return nil unless raw
222
+
223
+ raw.is_a?(Float) ? Time.at(raw) : Time.at(raw / 1000.0)
224
+ rescue StandardError
225
+ nil
226
+ end
227
+
228
+ # Optional allowlist for constant resolution. When set, only these
229
+ # namespaces may be resolved from a job payload:
230
+ #
231
+ # c.job_class_namespaces = %w[Workers Jobs Billing]
232
+ #
233
+ # This is the only control here that actually restricts what can be loaded.
234
+ # Shape checks do not: Ruby rejects a malformed constant path before it
235
+ # attempts any lookup, so a name like "../../etc/passwd" never reaches
236
+ # autoloading in the first place — only well-formed names do, and those are
237
+ # exactly what a shape check permits.
238
+ #
239
+ # Default nil (no restriction), because most apps do not need it: production
240
+ # Rails sets `eager_load = true`, so every app constant is already loaded and
241
+ # resolving one executes no new file. Set it where the job payload is not
242
+ # fully trusted and you would rather bound the blast radius anyway.
243
+ attr_accessor :job_class_namespaces
244
+
245
+ # Resolve a job class name to its Class, for resolvers that read constants
246
+ # off the class (Tags.from_constant, Runbooks.from_constant).
247
+ #
248
+ # The name comes from the job payload — `item["class"]` or `item["wrapped"]`
249
+ # — which is data, not code. Three narrowings, and it is worth being precise
250
+ # about which of them is a security control and which are not:
251
+ #
252
+ # * The namespace allowlist, when configured, IS one. It runs before the
253
+ # constant lookup and is the only thing here that decides what may be
254
+ # resolved.
255
+ # * The shape and length checks are NOT. They reject names Ruby would
256
+ # reject anyway, before it attempts a lookup; they are here so the
257
+ # rejection is explicit and a pathological string is bounded early.
258
+ # * Requiring a Module back is robustness: a name resolving to an Array
259
+ # would otherwise be sent `const_defined?`, which it does not answer.
260
+ #
261
+ # What nothing at this layer can do is stop a chosen name for a class that
262
+ # genuinely exists — reading a constant requires loading the class. Hosts who
263
+ # want no constant resolution at all should supply a resolver that does none:
264
+ # a Hash keyed by class name serves both tags and runbooks and never
265
+ # constantizes.
266
+ def job_class(name)
267
+ str = name.to_s
268
+ return nil unless str.length <= MAX_JOB_CLASS_NAME && str.match?(JOB_CLASS_NAME)
269
+ return nil unless namespace_allowed?(str)
270
+
271
+ klass = str.safe_constantize
272
+ klass if klass.is_a?(Module)
273
+ end
274
+
275
+ def namespace_allowed?(str)
276
+ allowed = job_class_namespaces
277
+ return true if allowed.nil? || Array(allowed).empty?
278
+
279
+ Array(allowed).any? { |ns| str == ns.to_s || str.start_with?("#{ns}::") }
280
+ end
281
+
282
+ # Durations, in one place. This lived in a view helper, which meant anything
283
+ # in lib/ that wanted to print a duration had to reinvent it — and did, five
284
+ # different ways, including the health signal that reported an hour-old queue
285
+ # as "3616s" (#31). Views reach this through the `duration` helper.
286
+ def duration(seconds)
287
+ return "—" if seconds.nil?
288
+
289
+ secs = seconds.to_f.abs
290
+ # Sub-minute keeps a decimal: 0.4s and 12s are a real distinction here.
291
+ return "#{secs.round(1)}s" if secs < 60
292
+ return "#{(secs / 60).floor}m #{(secs % 60).round}s" if secs < 3_600
293
+ return "#{(secs / 3_600).floor}h #{((secs % 3_600) / 60).round}m" if secs < 86_400
294
+
295
+ "#{(secs / 86_400).floor}d #{((secs % 86_400) / 3_600).round}h"
296
+ end
297
+
298
+ def duration_ms(ms)
299
+ return "—" if ms.nil?
300
+ return "#{ms.to_f.abs.round}ms" if ms.to_f.abs < 1_000
301
+
302
+ duration(ms.to_f / 1_000)
303
+ end
304
+
305
+ # Log a resolver failure at most once per process per distinct message.
306
+ #
307
+ # It was named warn_once and warned every time. A resolver that raises raises
308
+ # for every entry in a scan, so one broken job_tags lambda wrote a line per
309
+ # job — a thousand identical lines in the log for a single page render, which
310
+ # is how a warning stops being read at all.
311
+ #
312
+ # Bounded by WARN_MEMO_CAP, because the message carries the class name and the
313
+ # exception's own text: a resolver whose message varies could otherwise grow
314
+ # this without limit. At the cap it resets rather than leaking or going silent.
315
+ def warn_once(message)
316
+ return unless defined?(Rails)
317
+
318
+ @warned ||= {}
319
+ return if @warned.key?(message)
320
+
321
+ @warned.clear if @warned.size >= WARN_MEMO_CAP
322
+ @warned[message] = true
323
+ Rails.logger&.warn("[roundhouse] #{message}")
324
+ end
325
+
101
326
  # Cooperative cancellation check for long-running jobs:
102
327
  # raise SomeStop if RoundhouseUi.cancelled?(jid)
103
328
  def cancelled?(jid)
@@ -110,6 +335,11 @@ module RoundhouseUi
110
335
  self.redact_args = []
111
336
  self.show_sidekiq_failures = false
112
337
  self.pause_enabled = true
338
+ self.cancel_enabled = false
113
339
  self.poll_interval = 5
114
340
  self.collect_durations = false
341
+ self.job_tags_per_job = false
342
+ self.allow_theme_selection = true
343
+ self.icons = :svg
344
+ self.themes = Theme::PRESETS
115
345
  end
@@ -1,4 +1,84 @@
1
- # desc "Explaining what the task does"
2
- # task :roundhouse_ui do
3
- # # Task goes here
4
- # end
1
+ # frozen_string_literal: true
2
+
3
+ namespace :roundhouse_ui do
4
+ namespace :demo do
5
+ desc "Enqueue demo jobs for N minutes (default 10, hard cap 20): rake roundhouse_ui:demo:load[10]"
6
+ task :load, [ :minutes ] => :environment do |_task, args|
7
+ require "roundhouse_ui/demo"
8
+
9
+ abort "refusing to run in #{Rails.env}" unless Rails.env.development?
10
+
11
+ # Ask the connection where it actually is. Reading configuration cannot
12
+ # answer that, and Sidekiq falls back to database 0 when nothing set it.
13
+ db = Sidekiq.redis { |c| c.call("CLIENT", "INFO") }.to_s[/\bdb=(\d+)/, 1]
14
+ abort "refusing to enqueue against Redis database 0" if db.nil? || db == "0"
15
+
16
+ # Capped, and capped in one place. A load generator you have to remember to
17
+ # stop is a load generator that runs all night.
18
+ minutes = [ (args[:minutes] || 10).to_f, 20.0 ].min
19
+ minutes = 1.0 if minutes <= 0
20
+
21
+ mono = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
22
+ started = mono.call
23
+ deadline = started + (minutes * 60)
24
+ enqueued = Hash.new(0)
25
+ tick = 0
26
+
27
+ puts "Enqueueing to Redis db=#{db} for #{minutes.round(1)} min " \
28
+ "(#{RoundhouseUi::Demo::ALL.size} worker classes). Ctrl-C to stop early."
29
+
30
+ begin
31
+ while mono.call < deadline
32
+ tick += 1
33
+ # A rate that moves, so the dashboard's throughput line and the drain
34
+ # forecast have something to say. Peaks roughly every two minutes.
35
+ phase = Math.sin(tick / 12.0)
36
+ batch = (6 + (phase.abs * 18)).round
37
+
38
+ batch.times do
39
+ worker = RoundhouseUi::Demo::ALL.sample
40
+ worker.set(queue: worker::QUEUE).perform_async(
41
+ "account_id" => 40_000 + rand(600),
42
+ "user_id" => 900_000 + rand(9_000),
43
+ # Deliberately named to be masked by redact_args in the UI.
44
+ "api_token" => "sk_live_#{SecureRandom.hex(8)}"
45
+ )
46
+ enqueued[worker.name.split("::").last] += 1
47
+ end
48
+
49
+ if (tick % 10).zero?
50
+ left = ((deadline - mono.call) / 60).round(1)
51
+ puts " +#{enqueued.values.sum} enqueued, #{left} min left"
52
+ end
53
+ sleep 1.5
54
+ end
55
+ rescue Interrupt
56
+ puts "\n stopped early"
57
+ end
58
+
59
+ puts "\nDone after #{((mono.call - started) / 60).round(1)} min:"
60
+ enqueued.sort_by { |_k, v| -v }.each { |k, v| puts format(" %-22s %5d", k, v) }
61
+ puts format(" %-22s %5d", "TOTAL", enqueued.values.sum)
62
+ end
63
+
64
+ desc "Remove everything the demo seeded or enqueued"
65
+ task clean: :environment do
66
+ require "roundhouse_ui/demo"
67
+ abort "refusing to run in #{Rails.env}" unless Rails.env.development?
68
+ removed = 0
69
+ %w[retry dead schedule].each do |set|
70
+ Sidekiq.redis do |conn|
71
+ conn.call("ZRANGE", set, "0", "-1").each do |raw|
72
+ job = JSON.parse(raw) rescue next
73
+ next unless job["rh_demo"] || job["class"].to_s.start_with?("RoundhouseUi::Demo::")
74
+
75
+ conn.call("ZREM", set, raw)
76
+ removed += 1
77
+ end
78
+ end
79
+ end
80
+ RoundhouseUi::Demo::ALL.each { |w| Sidekiq::Queue.new(w::QUEUE).clear }
81
+ puts "removed #{removed} demo entries from the retry/dead/scheduled sets"
82
+ end
83
+ end
84
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roundhouse_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - R.J. Robinson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-31 00:00:00.000000000 Z
11
+ date: 2026-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -38,11 +38,12 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '6.5'
41
- description: 'A mountable Rails engine that replaces the Sidekiq Web UI with a real-time
42
- control plane: live queues, search, bulk actions, stuck-queue detection, snapshots,
43
- and pluggable observability.'
41
+ description: 'Mountable Rails engine for Sidekiq or Solid Queue: grouped errors, argument
42
+ search, filter-scoped bulk actions, enforced pause, snapshots, audit log. Mounts
43
+ alongside or instead of Sidekiq::Web. Not affiliated with or endorsed by Contributed
44
+ Systems LLC; Sidekiq, Sidekiq Pro and Sidekiq Enterprise are their trademarks.'
44
45
  email:
45
- - rj@trainual.com
46
+ - robertj.robinson@gmail.com
46
47
  executables: []
47
48
  extensions: []
48
49
  extra_rdoc_files: []
@@ -64,14 +65,17 @@ files:
64
65
  - app/controllers/roundhouse_ui/jobs_controller.rb
65
66
  - app/controllers/roundhouse_ui/metrics_controller.rb
66
67
  - app/controllers/roundhouse_ui/queues_controller.rb
68
+ - app/controllers/roundhouse_ui/recurring_controller.rb
67
69
  - app/controllers/roundhouse_ui/redis_controller.rb
68
70
  - app/controllers/roundhouse_ui/retries_controller.rb
69
71
  - app/controllers/roundhouse_ui/scheduled_controller.rb
72
+ - app/controllers/roundhouse_ui/settings_controller.rb
70
73
  - app/controllers/roundhouse_ui/snapshots_controller.rb
71
74
  - app/controllers/roundhouse_ui/workers_controller.rb
72
75
  - app/helpers/roundhouse_ui/application_helper.rb
73
76
  - app/helpers/roundhouse_ui/nav_helper.rb
74
77
  - app/helpers/roundhouse_ui/observability_helper.rb
78
+ - app/helpers/roundhouse_ui/tags_helper.rb
75
79
  - app/views/layouts/roundhouse_ui/application.html.erb
76
80
  - app/views/roundhouse_ui/audit/index.html.erb
77
81
  - app/views/roundhouse_ui/busy/index.html.erb
@@ -85,10 +89,18 @@ files:
85
89
  - app/views/roundhouse_ui/jobs/show.html.erb
86
90
  - app/views/roundhouse_ui/metrics/show.html.erb
87
91
  - app/views/roundhouse_ui/queues/index.html.erb
92
+ - app/views/roundhouse_ui/queues/show.html.erb
93
+ - app/views/roundhouse_ui/recurring/index.html.erb
88
94
  - app/views/roundhouse_ui/redis/show.html.erb
89
95
  - app/views/roundhouse_ui/retries/index.html.erb
90
96
  - app/views/roundhouse_ui/scheduled/index.html.erb
97
+ - app/views/roundhouse_ui/settings/show.html.erb
98
+ - app/views/roundhouse_ui/shared/_filter_error.html.erb
91
99
  - app/views/roundhouse_ui/shared/_pager.html.erb
100
+ - app/views/roundhouse_ui/shared/_search_bar.html.erb
101
+ - app/views/roundhouse_ui/shared/_search_help.html.erb
102
+ - app/views/roundhouse_ui/shared/_tag_filter.html.erb
103
+ - app/views/roundhouse_ui/shared/bulk_preview.html.erb
92
104
  - app/views/roundhouse_ui/snapshots/index.html.erb
93
105
  - app/views/roundhouse_ui/workers/index.html.erb
94
106
  - config/routes.rb
@@ -98,16 +110,27 @@ files:
98
110
  - lib/roundhouse_ui/backends/solid_queue.rb
99
111
  - lib/roundhouse_ui/cancel_middleware.rb
100
112
  - lib/roundhouse_ui/cancellation.rb
113
+ - lib/roundhouse_ui/demo.rb
101
114
  - lib/roundhouse_ui/duration_collector.rb
102
115
  - lib/roundhouse_ui/engine.rb
103
116
  - lib/roundhouse_ui/error_groups.rb
104
117
  - lib/roundhouse_ui/fetch.rb
118
+ - lib/roundhouse_ui/filter_query.rb
105
119
  - lib/roundhouse_ui/health.rb
120
+ - lib/roundhouse_ui/history.rb
121
+ - lib/roundhouse_ui/icons.rb
122
+ - lib/roundhouse_ui/marks/datadog-lockup-white.svg
123
+ - lib/roundhouse_ui/marks/datadog-lockup.svg
106
124
  - lib/roundhouse_ui/metrics.rb
107
125
  - lib/roundhouse_ui/observability.rb
108
126
  - lib/roundhouse_ui/pause.rb
127
+ - lib/roundhouse_ui/queue_summary.rb
128
+ - lib/roundhouse_ui/recurring.rb
109
129
  - lib/roundhouse_ui/redaction.rb
130
+ - lib/roundhouse_ui/runbooks.rb
110
131
  - lib/roundhouse_ui/snapshots.rb
132
+ - lib/roundhouse_ui/tags.rb
133
+ - lib/roundhouse_ui/theme.rb
111
134
  - lib/roundhouse_ui/version.rb
112
135
  - lib/tasks/roundhouse_ui_tasks.rake
113
136
  homepage: https://github.com/rjrobinson/roundhouse_ui
@@ -116,6 +139,8 @@ licenses:
116
139
  metadata:
117
140
  source_code_uri: https://github.com/rjrobinson/roundhouse_ui
118
141
  changelog_uri: https://github.com/rjrobinson/roundhouse_ui/blob/main/CHANGELOG.md
142
+ bug_tracker_uri: https://github.com/rjrobinson/roundhouse_ui/issues
143
+ documentation_uri: https://github.com/rjrobinson/roundhouse_ui#readme
119
144
  rubygems_mfa_required: 'true'
120
145
  post_install_message:
121
146
  rdoc_options: []
@@ -135,5 +160,5 @@ requirements: []
135
160
  rubygems_version: 3.5.22
136
161
  signing_key:
137
162
  specification_version: 4
138
- summary: Roundhouse — a modern, real-time web UI for Sidekiq.
163
+ summary: Roundhouse — a real-time ops UI for Sidekiq and Solid Queue.
139
164
  test_files: []