Class: Wurk::Web::Config

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/wurk/web/config.rb

Overview

Web UI configuration. Holds the authorization callback documented in docs/target/sidekiq-ent.md §9.2 — a Rack-level hook called with (env, method, path) per request. Truthy return proceeds; falsey short-circuits to 403.

Wurk ships the Ent feature in the free gem. No license check; the block runs unconditionally when present.

Example:

Wurk::Web.configure do |c|
c.authorization do |env, method, _path|
  user = env['warden']&.user
  method == 'GET' ? user&.support? || user&.admin? : user&.admin?
end
end

When no block is registered, every request is authorized (matches Sidekiq's default — no auth until the user opts in).

Constant Summary collapse

FALSEY_STRINGS =

String forms that mean "off" — so config.web.read_only = ENV[...] doesn't flip on when the env var is "0"/"false"/empty.

['', '0', 'false', 'no', 'off'].freeze
PROFILE_VIEW_URL =

Firefox-profiler endpoints for the Profiles pane (spec §25.2). The dashboard uploads a stored profile to profile_store_url and redirects the operator to profile_view_url % <returned-hash>. Overridable for self-hosted profiler instances.

'https://profiler.firefox.com/public/%s'
PROFILE_STORE_URL =
'https://api.profiler.firefox.com/compressed-store'
OPTIONS =

Hash-style settings, same surface as Sidekiq::Web::Config (#204): gems and apps write Sidekiq::Web.configure { |c| c[:csrf] = false }. Seeded like upstream's OPTIONS; unknown keys are stored verbatim so a setting wurk doesn't consume still round-trips (e.g. :csrf — wurk's extension POST guard is the Sec-Fetch-Site check in ExtensionsController, spec §25.1, not a token).

{
  profile_view_url: PROFILE_VIEW_URL,
  profile_store_url: PROFILE_STORE_URL
}.freeze
DEFAULT_TABS =

Sidekiq's built-in dashboard tabs (spec §25.3). The tabs hash starts as a copy of this; extensions add to it via register_extension or by mutating tabs directly — the same surface third-party gems use.

{
  'Dashboard' => '', 'Busy' => 'busy', 'Queues' => 'queues',
  'Retries' => 'retries', 'Scheduled' => 'scheduled', 'Dead' => 'morgue',
  'Metrics' => 'metrics', 'Profiles' => 'profiles'
}.freeze
NATIVE_TAB_PATHS =

Tab paths the SPA already renders natively (Sidekiq DEFAULT_TABS plus wurk's Pro/Ent extras). A gem re-registering one of these — e.g. sidekiq-cron's "cron" — must not produce a duplicate nav item, so custom_tabs filters them out.

%w[
  busy queues retries scheduled dead morgue metrics profiles
  batches limiters cron search
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConfig

Returns a new instance of Config.



77
78
79
80
81
82
83
84
85
# File 'lib/wurk/web/config.rb', line 77

def initialize
  @options = OPTIONS.dup
  @authorization = nil
  @read_only = env_read_only?
  @read_only_message = nil
  @middlewares = []
  @rack_app = nil
  init_extensions!
end

Instance Attribute Details

#app_urlObject

Returns the value of attribute app_url.



126
127
128
# File 'lib/wurk/web/config.rb', line 126

def app_url
  @app_url
end

#assets_pathObject

Returns the value of attribute assets_path.



126
127
128
# File 'lib/wurk/web/config.rb', line 126

def assets_path
  @assets_path
end

#custom_job_info_rowsObject

Returns the value of attribute custom_job_info_rows.



126
127
128
# File 'lib/wurk/web/config.rb', line 126

def custom_job_info_rows
  @custom_job_info_rows
end

#extensionsObject (readonly)

Web-extension surface (spec §25.2). Third-party gems (sidekiq-cron, sidekiq-unique-jobs, sidekiq-status, …) register dashboard tabs at load time. tabs is a mutable name→path hash seeded from DEFAULT_TABS; custom_job_info_rows collects callables that add rows to the job detail view; app_url / assets_path mirror Sidekiq's accessors. locales is the mutable locale-directory list extensions append to (Sidekiq::Web.configure.locales << dir) — the Extension renderer's t() reads en.yml from every listed dir. Wurk's own SPA i18n is separate, so it starts empty.



125
126
127
# File 'lib/wurk/web/config.rb', line 125

def extensions
  @extensions
end

#localesObject (readonly)

Web-extension surface (spec §25.2). Third-party gems (sidekiq-cron, sidekiq-unique-jobs, sidekiq-status, …) register dashboard tabs at load time. tabs is a mutable name→path hash seeded from DEFAULT_TABS; custom_job_info_rows collects callables that add rows to the job detail view; app_url / assets_path mirror Sidekiq's accessors. locales is the mutable locale-directory list extensions append to (Sidekiq::Web.configure.locales << dir) — the Extension renderer's t() reads en.yml from every listed dir. Wurk's own SPA i18n is separate, so it starts empty.



125
126
127
# File 'lib/wurk/web/config.rb', line 125

def locales
  @locales
end

#middlewaresObject (readonly)

Host-app Rack middleware stacked in front of the dashboard, newest last. Each entry is [middleware, args, block]. The LIVE array, like Sidekiq::Web::Config#middlewares — callers mutate it directly (sidekiq-cron's tests do c.middlewares.clear), so #rack_app detects drift instead of this returning a frozen copy.



75
76
77
# File 'lib/wurk/web/config.rb', line 75

def middlewares
  @middlewares
end

#read_only_messageObject

Optional banner copy shown by the dashboard in read-only mode. Nil → the SPA falls back to its localized default ("Read-only mode"). Lets a host explain why it's read-only — e.g. the public demo sets "This is a public demo — actions are disabled."



107
108
109
# File 'lib/wurk/web/config.rb', line 107

def read_only_message
  @read_only_message
end

#tabsObject (readonly)

Web-extension surface (spec §25.2). Third-party gems (sidekiq-cron, sidekiq-unique-jobs, sidekiq-status, …) register dashboard tabs at load time. tabs is a mutable name→path hash seeded from DEFAULT_TABS; custom_job_info_rows collects callables that add rows to the job detail view; app_url / assets_path mirror Sidekiq's accessors. locales is the mutable locale-directory list extensions append to (Sidekiq::Web.configure.locales << dir) — the Extension renderer's t() reads en.yml from every listed dir. Wurk's own SPA i18n is separate, so it starts empty.



125
126
127
# File 'lib/wurk/web/config.rb', line 125

def tabs
  @tabs
end

Instance Method Details

#authorization(&block) ⇒ Object

Registers a (env, method, path) -> truthy/falsey block. Re-calling overwrites; the spec exposes a single hook, not a chain.



111
112
113
114
# File 'lib/wurk/web/config.rb', line 111

def authorization(&block)
  @authorization = block if block
  @authorization
end

#authorized?(env, method, path) ⇒ Boolean

Returns true when no block is registered, otherwise the block's truthiness. The path argument is the engine-relative path so a consumer mounting under /wurk sees /api/stats, not the host's absolute path — that matches Sidekiq's contract.

Returns:

  • (Boolean)


242
243
244
245
246
# File 'lib/wurk/web/config.rb', line 242

def authorized?(env, method, path)
  return true if @authorization.nil?

  !!@authorization.call(env, method, path)
end

#custom_tabsObject

Tabs the SPA should render in the nav: everything registered beyond the Sidekiq defaults and wurk's own native pages, as { name:, path:, ext_name: }. ext_name ties the tab back to a registered extension so the Extension page can fetch its server-rendered view from ext/:ext_name/*; nil for tabs added by bare tabs[]= mutation (the SPA falls back to the iframe embed for those).



159
160
161
162
163
164
165
166
167
# File 'lib/wurk/web/config.rb', line 159

def custom_tabs
  @tabs.filter_map do |name, path|
    # Same trailing-slash normalization as extension_for_index, so a
    # native path registered as "cron/" doesn't duplicate the native tab.
    next if DEFAULT_TABS.key?(name) || NATIVE_TAB_PATHS.include?(path.to_s.delete_suffix('/'))

    { name: name, path: path.to_s, ext_name: extension_for_index(path)&.dig(:name)&.to_s }
  end
end

#extension_for_index(path) ⇒ Object

The registered extension whose index covers path ("locks/" and "locks" are the same index).



171
172
173
174
# File 'lib/wurk/web/config.rb', line 171

def extension_for_index(path)
  norm = path.to_s.delete_suffix('/')
  @extensions.find { |e| Array(e[:index]).any? { |i| i.to_s.delete_suffix('/') == norm } }
end

#job_info_pairs(job) ⇒ Object

Evaluate the registered custom_job_info_rows against a job (spec §25.2), returning [[label, value], …] for the SPA's job-detail modal. Each row is a callable (call(job)) or a Sidekiq-style add_pair(job) object; a row that raises or returns a non-pair is skipped so one bad extension can't break the job views.



181
182
183
184
185
# File 'lib/wurk/web/config.rb', line 181

def job_info_pairs(job)
  return [] if @custom_job_info_rows.empty?

  @custom_job_info_rows.filter_map { |row| job_info_pair(row, job) }
end

#profile_store_urlObject



93
# File 'lib/wurk/web/config.rb', line 93

def profile_store_url = @options[:profile_store_url]

#profile_store_url=(value) ⇒ Object



99
100
101
# File 'lib/wurk/web/config.rb', line 99

def profile_store_url=(value)
  @options[:profile_store_url] = value
end

#profile_view_urlObject

Firefox-profiler URLs — named views over the same @options keys the bracket surface exposes, so c[:profile_view_url] = … and c.profile_view_url = … can't drift apart.



92
# File 'lib/wurk/web/config.rb', line 92

def profile_view_url  = @options[:profile_view_url]

#profile_view_url=(value) ⇒ Object



95
96
97
# File 'lib/wurk/web/config.rb', line 95

def profile_view_url=(value)
  @options[:profile_view_url] = value
end

#rack_app(inner) ⇒ Object

Builds the host-middleware chain wrapping inner, memoized against the middleware list it was built from — middlewares is the live array (upstream surface), so direct mutation after the first request triggers a rebuild instead of silently serving the stale chain. Production builds exactly once at boot; the per-request comparison is an == over a handful of entries.



204
205
206
207
208
209
210
211
212
213
# File 'lib/wurk/web/config.rb', line 204

def rack_app(inner)
  return @rack_app if @rack_app && @rack_app_stack == @middlewares

  @rack_app_stack = @middlewares.dup
  stack = @middlewares
  @rack_app = ::Rack::Builder.new do
    stack.each { |middleware, args, block| use(middleware, *args, &block) }
    run inner
  end.to_app
end

#read_only=(value) ⇒ Object

Read-only mode. When on, the Authorization middleware blocks every non-GET request (retry/kill/requeue/delete/pause/resume/clear) with 403, and the SPA hides destructive actions via the /api/meta flag. Defaults from WURK_WEB_READ_ONLY=1 so a viewer-only deploy (e.g. the public demo) needs no Ruby config.



220
221
222
# File 'lib/wurk/web/config.rb', line 220

def read_only=(value)
  @read_only = value.is_a?(String) ? !FALSEY_STRINGS.include?(value.strip.downcase) : !!value
end

#read_only?Boolean

Returns:

  • (Boolean)


224
225
226
# File 'lib/wurk/web/config.rb', line 224

def read_only?
  @read_only
end

#register_extension(extension, name:, tab:, index:, root_dir: nil, cache_for: 86_400, asset_paths: nil) {|_self| ... } ⇒ Object Also known as: register

Matches Sidekiq::Web::Config#register_extension (aliased register, spec §25.2): tab is the label(s), index the path(s), name the asset namespace. tab/index are zipped into the tabs hash (label => path) so the tab surfaces in the SPA nav via /api/meta, and the extension's registered(app) routes/ERB views are served by Wurk::Web::Extension::Renderer under ext/:name/* (#187) — the SPA's Extension page embeds the rendered HTML. Yields self to an optional block for further config, and returns self. rubocop:disable Metrics/ParameterLists -- signature matches Sidekiq::Web::Config#register_extension (spec §25.2)

Yields:

  • (_self)

Yield Parameters:



137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/wurk/web/config.rb', line 137

def register_extension(extension, name:, tab:, index:, root_dir: nil,
                       cache_for: 86_400, asset_paths: nil)
  Array(tab).zip(Array(index)).each { |label, path| @tabs[label] = path if label }
  # Upstream registers root_dir/locales automatically; extensions
  # without a root_dir append their dir to `locales` themselves.
  @locales << ::File.join(root_dir, 'locales') if root_dir
  @extensions << {
    extension: extension, name: name, tab: tab, index: index,
    root_dir: root_dir, cache_for: cache_for, asset_paths: asset_paths
  }
  yield self if block_given?
  self
end

#reset!Object



228
229
230
231
232
233
234
235
236
# File 'lib/wurk/web/config.rb', line 228

def reset!
  @options = OPTIONS.dup
  @authorization = nil
  @read_only = env_read_only?
  @read_only_message = nil
  @middlewares = []
  @rack_app = nil
  init_extensions!
end

#use(middleware, *args, &block) ⇒ Object

Sidekiq-compatible (Sidekiq::Web.use). Registers a Rack middleware that wraps the dashboard, in front of the authorization hook, so a host app can gate the UI with Devise/Warden/Sorcery/Rack::Auth::Basic without writing its own middleware. args and an optional block pass straight through to the middleware's new. Call before the first request (i.e. from an initializer) — the chain is built once.



193
194
195
196
# File 'lib/wurk/web/config.rb', line 193

def use(middleware, *args, &block)
  @middlewares << [middleware, args, block]
  @rack_app = nil
end