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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +37 -0
- data/LICENSE.txt +21 -0
- data/README.md +50 -0
- data/Rakefile +10 -0
- data/lib/side_bro/version.rb +5 -0
- data/lib/side_bro/web/action.rb +115 -0
- data/lib/side_bro/web/application.rb +331 -0
- data/lib/side_bro/web/helpers.rb +176 -0
- data/lib/side_bro/web/router.rb +50 -0
- data/lib/side_bro/web.rb +96 -0
- data/lib/side_bro.rb +8 -0
- data/sig/side_bro.rbs +4 -0
- data/web/assets/images/.keep +0 -0
- data/web/assets/javascripts/application.js +62 -0
- data/web/assets/javascripts/base-charts.js +1 -0
- data/web/assets/javascripts/dashboard-charts.js +1 -0
- data/web/assets/javascripts/dashboard.js +262 -0
- data/web/assets/javascripts/metrics.js +1 -0
- data/web/assets/stylesheets/style.css +636 -0
- data/web/locales/en.yml +88 -0
- data/web/views/_footer.html.erb +3 -0
- data/web/views/_job_info.html.erb +23 -0
- data/web/views/_metrics_period_select.html.erb +5 -0
- data/web/views/_nav.html.erb +76 -0
- data/web/views/_paging.html.erb +11 -0
- data/web/views/_poll_link.html.erb +4 -0
- data/web/views/_summary.html.erb +44 -0
- data/web/views/busy.html.erb +104 -0
- data/web/views/dashboard.html.erb +124 -0
- data/web/views/dead.html.erb +31 -0
- data/web/views/layout.html.erb +61 -0
- data/web/views/metrics.html.erb +56 -0
- data/web/views/metrics_for_job.html.erb +67 -0
- data/web/views/morgue.html.erb +82 -0
- data/web/views/queue.html.erb +190 -0
- data/web/views/queues.html.erb +59 -0
- data/web/views/retries.html.erb +99 -0
- data/web/views/retry.html.erb +32 -0
- data/web/views/scheduled.html.erb +79 -0
- data/web/views/scheduled_job_info.html.erb +31 -0
- metadata +126 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2882e3f5f9428e6068b4a7a96b5ff253454dd60692a11eb53e1054399df0a6f4
|
|
4
|
+
data.tar.gz: 840234c4510a1af907e837e36ab1308419438194b5a8e205a9eb194e5292644e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 74635e64db2372ac7013e346a50361b126e10082da20a1c199c9f476f58f84bde3c2de46e6a7ecfe8ef088eaee94bba198b735f138add225e66fbe8f56fe7d0b
|
|
7
|
+
data.tar.gz: f2bc7ca9cde7caed603b8ffa73abd624ca8e51c2b189145b2c8d063ee5000c8409ee31b56726d296c878a1df077d53b7521caff7d47158ab2587edda11163ea4
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.2.2] - 2026-05-21
|
|
4
|
+
|
|
5
|
+
- Fix table cell overflow on retries/morgue/queues/busy/scheduled pages — removed `white-space: pre-line` override that defeated `.args` truncation
|
|
6
|
+
- Add `overflow: hidden` to `tbody td` and `thead th` so fixed-layout columns never bleed content into adjacent cells
|
|
7
|
+
- Move column widths to CSS (`col.w-xs/sm/md/lg`) so they are inspectable and overridable; add `col-compact` padding class for narrow columns
|
|
8
|
+
- Shorten retries table "Next Retry" → "Next" and "Retry Count" → "#" headers; tighten those column widths
|
|
9
|
+
- Fix refresh button broken by CSP nonce policy — moved `onclick` to `application.js`
|
|
10
|
+
- Remove spurious page auto-refresh on non-dashboard pages introduced by live toggle wiring
|
|
11
|
+
- Hide poll interval ("· 5s") from live toggle button on all pages except the dashboard
|
|
12
|
+
- Template caching now only active when `RACK_ENV=production` so ERB edits are live in development
|
|
13
|
+
|
|
14
|
+
## [0.2.1] - 2026-05-20
|
|
15
|
+
|
|
16
|
+
- Add `Content-Security-Policy` header with per-request nonce on all HTML responses
|
|
17
|
+
- Cache compiled ERB templates at class level to avoid re-reading files on every request
|
|
18
|
+
- Warn at startup when `SIDE_BRO_SESSION_SECRET` is not set
|
|
19
|
+
- Display flash messages (`notice`/`error`) in the layout below the topbar
|
|
20
|
+
- Wire up extension system: load extension locale files and mount extension routes/assets on `register_extension`
|
|
21
|
+
- Fix live toggle double-firing on dashboard (layout inline script removed; `application.js` now owns the button)
|
|
22
|
+
- Auto-refresh non-dashboard pages when live toggle is on; expose `window.SideBroLive` API for dashboard sync
|
|
23
|
+
- Queue job filter now uses substring match instead of exact match for both class name and args
|
|
24
|
+
- Redis memory usage bar now shows actual used/peak percentage instead of hardcoded 30%
|
|
25
|
+
|
|
26
|
+
## [0.2.0] - 2026-05-20
|
|
27
|
+
|
|
28
|
+
- Retries, morgue, scheduled, busy, and queue detail pages
|
|
29
|
+
- Sticky table headers and scrollable job tables
|
|
30
|
+
- Compact sidebar layout with brand mark
|
|
31
|
+
- Standardised `format_args_short` helper across all job list views
|
|
32
|
+
- Throughput chart tooltip smart positioning; dynamic time-window label
|
|
33
|
+
- Dashboard history chart with 1 week / 1 month / 3 month / 6 month range tabs
|
|
34
|
+
|
|
35
|
+
## [0.1.0] - 2026-05-09
|
|
36
|
+
|
|
37
|
+
- Initial release
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cremz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SideBro
|
|
2
|
+
|
|
3
|
+
A Rack-mountable Sidekiq Web UI alternative with a customizable design.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```gem "side_bro"```
|
|
10
|
+
|
|
11
|
+
## Mounting
|
|
12
|
+
|
|
13
|
+
### Rails
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# config/routes.rb
|
|
17
|
+
require "side_bro"
|
|
18
|
+
mount SideBro::Web, at: "/side_bro"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Rack (`config.ru`)
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "side_bro"
|
|
25
|
+
run SideBro::Web
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Authentication
|
|
29
|
+
|
|
30
|
+
SideBro has no built-in authentication. Wrap it with any Rack middleware:
|
|
31
|
+
|
|
32
|
+
### HTTP Basic Auth
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
SideBro::Web.use Rack::Auth::Basic, "SideBro" do |user, password|
|
|
36
|
+
[user, password] == ["admin", ENV["SIDE_BRO_PASSWORD"]]
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Devise (Rails)
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
authenticate :user, ->(u) { u.admin? } do
|
|
44
|
+
mount SideBro::Web, at: "/side_bro"
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Session Secret
|
|
49
|
+
|
|
50
|
+
Set `SIDE_BRO_SESSION_SECRET` env var for a stable session secret across restarts.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module SideBro
|
|
7
|
+
class Web
|
|
8
|
+
class Action
|
|
9
|
+
include SideBro::WebHelpers
|
|
10
|
+
|
|
11
|
+
VIEWS_PATH = File.expand_path("../../../web/views", __dir__)
|
|
12
|
+
TEMPLATE_CACHE = {}
|
|
13
|
+
|
|
14
|
+
attr_reader :env, :request, :response, :nonce
|
|
15
|
+
|
|
16
|
+
def action
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(env)
|
|
21
|
+
@env = env
|
|
22
|
+
@request = Rack::Request.new(env)
|
|
23
|
+
@response = Rack::Response.new
|
|
24
|
+
@response["Content-Type"] = "text/html; charset=utf-8"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def params
|
|
28
|
+
@params ||= begin
|
|
29
|
+
route_params = env[SideBro::Web::Router::ROUTE_PARAMS] || {}
|
|
30
|
+
request.params.merge(route_params)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def session
|
|
35
|
+
request.session
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def flash
|
|
39
|
+
@flash ||= FlashHash.new(session)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def redirect(location)
|
|
43
|
+
response.redirect(location)
|
|
44
|
+
throw :halt, response.finish
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def halt(*resp)
|
|
48
|
+
res = if resp.length == 1 && resp.first.is_a?(Integer)
|
|
49
|
+
Rack::Response.new([], resp.first)
|
|
50
|
+
else
|
|
51
|
+
resp.first
|
|
52
|
+
end
|
|
53
|
+
throw :halt, res.is_a?(Array) ? res : res.finish
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def json(payload)
|
|
57
|
+
response["Content-Type"] = "application/json; charset=utf-8"
|
|
58
|
+
response.body = [JSON.generate(payload)]
|
|
59
|
+
throw :halt, response.finish
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def erb(template_name)
|
|
63
|
+
@nonce = SecureRandom.base64(16)
|
|
64
|
+
env["side_bro.csp_nonce"] = @nonce
|
|
65
|
+
response["Content-Security-Policy"] =
|
|
66
|
+
"default-src 'self'; " \
|
|
67
|
+
"script-src 'nonce-#{@nonce}'; " \
|
|
68
|
+
"style-src 'nonce-#{@nonce}' https://fonts.googleapis.com; " \
|
|
69
|
+
"font-src 'self' https://fonts.gstatic.com; " \
|
|
70
|
+
"img-src 'self' data:; " \
|
|
71
|
+
"connect-src 'self'"
|
|
72
|
+
layout = load_template(:layout)
|
|
73
|
+
content = render_template(template_name)
|
|
74
|
+
response.body = [layout.result_with_hash(content: content, nonce: @nonce, action: self)]
|
|
75
|
+
response.finish
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def render_partial(name)
|
|
79
|
+
load_template(:"_#{name}").result(binding)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def render_template(name)
|
|
85
|
+
load_template(name).result(binding)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def load_template(name)
|
|
89
|
+
tpl = ERB.new(File.read(File.join(VIEWS_PATH, "#{name}.html.erb")), trim_mode: "-")
|
|
90
|
+
return tpl unless ENV["RACK_ENV"] == "production"
|
|
91
|
+
TEMPLATE_CACHE[name] ||= tpl
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class FlashHash
|
|
96
|
+
def initialize(session)
|
|
97
|
+
@session = session
|
|
98
|
+
@session[:flash] ||= {}
|
|
99
|
+
@read = {}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def [](key)
|
|
103
|
+
@read[key] ||= @session[:flash].delete(key.to_s)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def []=(key, value)
|
|
107
|
+
@session[:flash][key.to_s] = value
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def any?
|
|
111
|
+
@session[:flash].any?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sidekiq"
|
|
4
|
+
|
|
5
|
+
module SideBro
|
|
6
|
+
class Web
|
|
7
|
+
class Application
|
|
8
|
+
SAFE_METHODS = %w[GET HEAD OPTIONS TRACE].freeze
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@router = SideBro::Web::Router.new
|
|
12
|
+
register_routes
|
|
13
|
+
register_extension_routes
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(env)
|
|
17
|
+
unless SAFE_METHODS.include?(env["REQUEST_METHOD"])
|
|
18
|
+
unless env["HTTP_SEC_FETCH_SITE"] == "same-origin"
|
|
19
|
+
return [403, {"Content-Type" => "text/plain"}, ["Forbidden"]]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
block = @router.match(env)
|
|
24
|
+
return [404, {"Content-Type" => "text/plain"}, ["Not Found"]] unless block
|
|
25
|
+
|
|
26
|
+
action = SideBro::Web::Action.new(env)
|
|
27
|
+
catch(:halt) do
|
|
28
|
+
action.instance_exec(&block)
|
|
29
|
+
action.response.finish
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def register_routes
|
|
36
|
+
@router.get("/assets/*") do
|
|
37
|
+
asset_path = File.expand_path(File.join(SideBro::Web::ASSETS_PATH, params["splat"].to_s))
|
|
38
|
+
unless asset_path.start_with?(SideBro::Web::ASSETS_PATH) && File.file?(asset_path)
|
|
39
|
+
halt(404)
|
|
40
|
+
end
|
|
41
|
+
content_type = case File.extname(asset_path)
|
|
42
|
+
when ".css" then "text/css"
|
|
43
|
+
when ".js" then "application/javascript"
|
|
44
|
+
when ".png" then "image/png"
|
|
45
|
+
when ".svg" then "image/svg+xml"
|
|
46
|
+
else "application/octet-stream"
|
|
47
|
+
end
|
|
48
|
+
response["Content-Type"] = content_type
|
|
49
|
+
response["Cache-Control"] = "private, max-age=86400"
|
|
50
|
+
response.body = [File.binread(asset_path)]
|
|
51
|
+
response.finish
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
@router.head("/") do
|
|
55
|
+
Sidekiq.redis { |c| c.ping }
|
|
56
|
+
response["Content-Type"] = "text/plain"
|
|
57
|
+
response.body = [""]
|
|
58
|
+
response.finish
|
|
59
|
+
rescue => e
|
|
60
|
+
throw :halt, [500, {"Content-Type" => "text/plain"}, [e.message]]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@router.get("/") do
|
|
64
|
+
@stats = Sidekiq::Stats.new
|
|
65
|
+
days = (params["days"] || 30).to_i.clamp(1, 180)
|
|
66
|
+
@history = Sidekiq::Stats::History.new(days)
|
|
67
|
+
erb :dashboard
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@router.get("/stats") do
|
|
71
|
+
stats = Sidekiq::Stats.new
|
|
72
|
+
redis_info = begin
|
|
73
|
+
Sidekiq.redis { |c| c.info }
|
|
74
|
+
rescue
|
|
75
|
+
{}
|
|
76
|
+
end
|
|
77
|
+
json({
|
|
78
|
+
processed: stats.processed,
|
|
79
|
+
failed: stats.failed,
|
|
80
|
+
busy: stats.workers_size,
|
|
81
|
+
enqueued: stats.enqueued,
|
|
82
|
+
retries: stats.retry_size,
|
|
83
|
+
scheduled: stats.scheduled_size,
|
|
84
|
+
dead: stats.dead_size,
|
|
85
|
+
sidekiq: Sidekiq::VERSION,
|
|
86
|
+
redis: redis_info
|
|
87
|
+
})
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@router.get("/stats/queues") do
|
|
91
|
+
queues = Sidekiq::Queue.all.each_with_object({}) do |q, h|
|
|
92
|
+
h[q.name] = q.size
|
|
93
|
+
end
|
|
94
|
+
json(queues)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@router.get("/busy") do
|
|
98
|
+
@processes = Sidekiq::ProcessSet.new.to_a
|
|
99
|
+
@workers = Sidekiq::WorkSet.new.to_a
|
|
100
|
+
erb :busy
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
@router.post("/busy") do
|
|
104
|
+
if params["quiet"]
|
|
105
|
+
Sidekiq::ProcessSet.new.each(&:quiet!)
|
|
106
|
+
elsif params["stop"]
|
|
107
|
+
Sidekiq::ProcessSet.new.each(&:stop!)
|
|
108
|
+
elsif (identity = params["identity"])
|
|
109
|
+
process = Sidekiq::ProcessSet.new.find { |p| p.identity == identity }
|
|
110
|
+
process&.quiet! if params["quiet_process"]
|
|
111
|
+
process&.stop! if params["stop_process"]
|
|
112
|
+
end
|
|
113
|
+
redirect "#{root_path}busy"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
@router.get("/queues") do
|
|
117
|
+
@queues = Sidekiq::Queue.all
|
|
118
|
+
erb :queues
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
@router.get("/queues/:name") do
|
|
122
|
+
validate_queue_name!(params["name"])
|
|
123
|
+
@queue = Sidekiq::Queue.new(params["name"])
|
|
124
|
+
@page, @per_page = current_page
|
|
125
|
+
@asc = params["direction"] != "desc"
|
|
126
|
+
@filter_job = params["filter_job"].to_s.strip
|
|
127
|
+
@filter_args = params["filter_args"].to_s.strip
|
|
128
|
+
@filtering = !@filter_job.empty? || !@filter_args.empty?
|
|
129
|
+
|
|
130
|
+
if @filtering
|
|
131
|
+
@jobs = @queue.lazy.select { |j|
|
|
132
|
+
(@filter_job.empty? || job_display_class(j).include?(@filter_job)) &&
|
|
133
|
+
(@filter_args.empty? || display_args(j).include?(@filter_args))
|
|
134
|
+
}.first(@per_page)
|
|
135
|
+
@total = nil
|
|
136
|
+
else
|
|
137
|
+
@total = @queue.size
|
|
138
|
+
if @asc
|
|
139
|
+
@jobs = @queue.lazy.drop(@page * @per_page).first(@per_page)
|
|
140
|
+
else
|
|
141
|
+
all = @queue.map { |j| j }
|
|
142
|
+
all.reverse!
|
|
143
|
+
@jobs = all.slice(@page * @per_page, @per_page) || []
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
erb :queue
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
@router.post("/queues/:name") do
|
|
150
|
+
validate_queue_name!(params["name"])
|
|
151
|
+
q = Sidekiq::Queue.new(params["name"])
|
|
152
|
+
if params["pause"]
|
|
153
|
+
q.pause! if q.respond_to?(:pause!)
|
|
154
|
+
elsif params["unpause"]
|
|
155
|
+
q.unpause! if q.respond_to?(:unpause!)
|
|
156
|
+
elsif params["clear"]
|
|
157
|
+
q.clear
|
|
158
|
+
end
|
|
159
|
+
redirect "#{root_path}queues"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
@router.post("/queues/:name/delete_filtered") do
|
|
163
|
+
validate_queue_name!(params["name"])
|
|
164
|
+
filter_job = params["filter_job"].to_s.strip
|
|
165
|
+
filter_args = params["filter_args"].to_s.strip
|
|
166
|
+
q = Sidekiq::Queue.new(params["name"])
|
|
167
|
+
to_delete = q.select { |j|
|
|
168
|
+
(filter_job.empty? || job_display_class(j) == filter_job) &&
|
|
169
|
+
(filter_args.empty? || display_args(j) == filter_args)
|
|
170
|
+
}
|
|
171
|
+
to_delete.each(&:delete)
|
|
172
|
+
redirect "#{root_path}queues/#{params["name"]}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
@router.post("/queues/:name/delete") do
|
|
176
|
+
validate_queue_name!(params["name"])
|
|
177
|
+
q = Sidekiq::Queue.new(params["name"])
|
|
178
|
+
Array(params["key_val"]).each do |jid|
|
|
179
|
+
job = q.find { |j| j.jid == jid }
|
|
180
|
+
job&.delete
|
|
181
|
+
end
|
|
182
|
+
redirect "#{root_path}queues/#{params["name"]}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Retries
|
|
186
|
+
@router.get("/retries") do
|
|
187
|
+
@total = Sidekiq::RetrySet.new.size
|
|
188
|
+
@page, @per_page = current_page
|
|
189
|
+
@jobs = Sidekiq::RetrySet.new.map { |j| j }.slice(@page * @per_page, @per_page) || []
|
|
190
|
+
erb :retries
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
@router.post("/retries/all/:op") do
|
|
194
|
+
case params["op"]
|
|
195
|
+
when "retry" then Sidekiq::RetrySet.new.retry_all
|
|
196
|
+
when "delete" then Sidekiq::RetrySet.new.clear
|
|
197
|
+
when "kill" then Sidekiq::RetrySet.new.kill_all
|
|
198
|
+
end
|
|
199
|
+
redirect "#{root_path}retries"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
@router.get("/retries/:key") do
|
|
203
|
+
@job = Sidekiq::RetrySet.new.find { |j| j.jid == params["key"] }
|
|
204
|
+
halt(404) unless @job
|
|
205
|
+
erb :retry
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
@router.post("/retries") do
|
|
209
|
+
handle_job_action(Sidekiq::RetrySet.new)
|
|
210
|
+
redirect "#{root_path}retries"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Morgue (Dead)
|
|
214
|
+
@router.get("/morgue") do
|
|
215
|
+
@total = Sidekiq::DeadSet.new.size
|
|
216
|
+
@page, @per_page = current_page
|
|
217
|
+
@jobs = Sidekiq::DeadSet.new.map { |j| j }.slice(@page * @per_page, @per_page) || []
|
|
218
|
+
erb :morgue
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
@router.post("/morgue/all/:op") do
|
|
222
|
+
case params["op"]
|
|
223
|
+
when "retry" then Sidekiq::DeadSet.new.retry_all
|
|
224
|
+
when "delete" then Sidekiq::DeadSet.new.clear
|
|
225
|
+
end
|
|
226
|
+
redirect "#{root_path}morgue"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
@router.get("/morgue/:key") do
|
|
230
|
+
@job = Sidekiq::DeadSet.new.find { |j| j.jid == params["key"] }
|
|
231
|
+
halt(404) unless @job
|
|
232
|
+
erb :dead
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
@router.post("/morgue") do
|
|
236
|
+
handle_job_action(Sidekiq::DeadSet.new)
|
|
237
|
+
redirect "#{root_path}morgue"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Scheduled
|
|
241
|
+
@router.get("/scheduled") do
|
|
242
|
+
@total = Sidekiq::ScheduledSet.new.size
|
|
243
|
+
@page, @per_page = current_page
|
|
244
|
+
@jobs = Sidekiq::ScheduledSet.new.map { |j| j }.slice(@page * @per_page, @per_page) || []
|
|
245
|
+
erb :scheduled
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
@router.post("/scheduled/all/:op") do
|
|
249
|
+
case params["op"]
|
|
250
|
+
when "delete" then Sidekiq::ScheduledSet.new.clear
|
|
251
|
+
when "add_to_queue" then Sidekiq::ScheduledSet.new.each(&:add_to_queue)
|
|
252
|
+
end
|
|
253
|
+
redirect "#{root_path}scheduled"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
@router.get("/scheduled/:key") do
|
|
257
|
+
@job = Sidekiq::ScheduledSet.new.find { |j| j.jid == params["key"] }
|
|
258
|
+
halt(404) unless @job
|
|
259
|
+
erb :scheduled_job_info
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
@router.post("/scheduled") do
|
|
263
|
+
handle_job_action(Sidekiq::ScheduledSet.new)
|
|
264
|
+
redirect "#{root_path}scheduled"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Metrics (Sidekiq 7+ only)
|
|
268
|
+
@router.get("/metrics") do
|
|
269
|
+
halt(404) unless metrics_enabled?
|
|
270
|
+
require "sidekiq/metrics/query"
|
|
271
|
+
@period = params["period"] || "1h"
|
|
272
|
+
hours = {"1h" => 1, "8h" => 8, "24h" => 24, "72h" => 72}[@period]
|
|
273
|
+
@metrics = Sidekiq::Metrics::Query.new.top_jobs(hours: hours)
|
|
274
|
+
erb :metrics
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
@router.get("/metrics/:name") do
|
|
278
|
+
halt(404) unless metrics_enabled?
|
|
279
|
+
require "sidekiq/metrics/query"
|
|
280
|
+
@period = params["period"] || "1h"
|
|
281
|
+
hours = {"1h" => 1, "8h" => 8, "24h" => 24, "72h" => 72}[@period]
|
|
282
|
+
@name = params["name"]
|
|
283
|
+
@metrics = Sidekiq::Metrics::Query.new.for_job(@name, hours: hours)
|
|
284
|
+
erb :metrics_for_job
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
@router.post("/change_locale") do
|
|
288
|
+
new_locale = params["locale"].to_s.strip
|
|
289
|
+
session[:locale] = new_locale if SideBro::Web.translations.key?(new_locale)
|
|
290
|
+
redirect(request.env["HTTP_REFERER"] || root_path.to_s)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def register_extension_routes
|
|
295
|
+
SideBro::Web.extensions.each do |ext|
|
|
296
|
+
ext_name = ext[:name]
|
|
297
|
+
ext_class = ext[:class]
|
|
298
|
+
root_dir = ext[:root_dir]
|
|
299
|
+
cache_for = ext[:cache_for] || 86400
|
|
300
|
+
|
|
301
|
+
if root_dir
|
|
302
|
+
assets_base = File.expand_path("assets", root_dir)
|
|
303
|
+
@router.get("/#{ext_name}/assets/*") do
|
|
304
|
+
asset_path = File.expand_path(File.join(assets_base, params["splat"].to_s))
|
|
305
|
+
unless asset_path.start_with?(assets_base) && File.file?(asset_path)
|
|
306
|
+
halt(404)
|
|
307
|
+
end
|
|
308
|
+
content_type = case File.extname(asset_path)
|
|
309
|
+
when ".css" then "text/css"
|
|
310
|
+
when ".js" then "application/javascript"
|
|
311
|
+
when ".png" then "image/png"
|
|
312
|
+
when ".svg" then "image/svg+xml"
|
|
313
|
+
else "application/octet-stream"
|
|
314
|
+
end
|
|
315
|
+
response["Content-Type"] = content_type
|
|
316
|
+
response["Cache-Control"] = "private, max-age=#{cache_for}"
|
|
317
|
+
response.body = [File.binread(asset_path)]
|
|
318
|
+
response.finish
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
next unless ext_class.respond_to?(:call)
|
|
323
|
+
["/#{ext_name}", "/#{ext_name}/*"].each do |path|
|
|
324
|
+
@router.get(path) { throw :halt, ext_class.call(env) }
|
|
325
|
+
@router.post(path) { throw :halt, ext_class.call(env) }
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|