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. a bare tabs['Cron'] = 'cron' — must not produce a duplicate nav item, so custom_tabs filters them out. One an extension answers stays: sidekiq-cron's tab shows that gem's jobs, which the native page knows nothing about.

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

Locale tags the shipped SPA bundle carries copy for, in menu order. Mirrors availableLocales in frontend/src/i18n/index.ts — the pair is pinned by WebConfigTest, since a bundle added on one side only would make the server negotiate a locale the SPA can't render (or refuse one it can).

%w[en es fr de pt-BR ja zh-CN ar].freeze
THEMES =

Preferences the SPA's theme runtime understands (frontend/src/theme.ts). 'system' follows the visitor's OS; the palettes themselves are the two data-theme values.

%w[light dark system].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize ⇒ Config

Returns a new instance of Config.



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

def initialize
  reset!
end

Instance Attribute Details

#app_url ⇒ Object

Returns the value of attribute app_url.



201
202
203
# File 'lib/wurk/web/config.rb', line 201

def app_url
  @app_url
end

#assets_path ⇒ Object

Returns the value of attribute assets_path.



201
202
203
# File 'lib/wurk/web/config.rb', line 201

def assets_path
  @assets_path
end

#custom_job_info_rows ⇒ Object

Returns the value of attribute custom_job_info_rows.



201
202
203
# File 'lib/wurk/web/config.rb', line 201

def custom_job_info_rows
  @custom_job_info_rows
end

#default_locale ⇒ Object

Dashboard language. offered_locales is the set the server negotiates a request's Accept-Language against before emitting the SPA's data-locale hint (see Wurk::Web::LocaleNegotiator): it starts as the locales the bundle ships and may be narrowed, or widened with one the host supplies copy for through translations. default_locale is the hint used when the header asks for nothing offered — nil emits no hint at all, leaving the browser's own preference list to decide.

Not to be confused with locales above: that is the extension renderer's locale-directory list, a separate i18n system for server-rendered third-party tabs.



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

def default_locale
  @default_locale
end

#default_theme ⇒ Object

Dashboard theme hint. The SPA resolves 'light', 'dark' or 'system' (the default) out of localStorage; this is what it falls back to for a visitor who has never picked one. Injected ahead of the shell's pre-paint script so a host default paints with everything else instead of flashing the other palette. nil emits nothing. Initialization and reset! sample WURK_WEB_DEFAULT_THEME through the validating writer; later explicit assignments (including nil) override that seed.



136
137
138
# File 'lib/wurk/web/config.rb', line 136

def default_theme
  @default_theme
end

#extensions ⇒ Object (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.



196
197
198
# File 'lib/wurk/web/config.rb', line 196

def extensions
  @extensions
end

#locales ⇒ Object (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.



196
197
198
# File 'lib/wurk/web/config.rb', line 196

def locales
  @locales
end

#max_streams ⇒ Object

Cap on concurrent live-update (SSE) streams per web process. Each open stream pins a server thread, so nil (the default) derives the cap from the server's thread count, leaving the rest for ordinary requests; 0 turns streaming off, and the SPA falls back to polling.



151
152
153
# File 'lib/wurk/web/config.rb', line 151

def max_streams
  @max_streams
end

#middlewares ⇒ Object (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.



88
89
90
# File 'lib/wurk/web/config.rb', line 88

def middlewares
  @middlewares
end

#offered_locales ⇒ Object

Dashboard language. offered_locales is the set the server negotiates a request's Accept-Language against before emitting the SPA's data-locale hint (see Wurk::Web::LocaleNegotiator): it starts as the locales the bundle ships and may be narrowed, or widened with one the host supplies copy for through translations. default_locale is the hint used when the header asks for nothing offered — nil emits no hint at all, leaving the browser's own preference list to decide.

Not to be confused with locales above: that is the extension renderer's locale-directory list, a separate i18n system for server-rendered third-party tabs.



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

def offered_locales
  @offered_locales
end

#read_only_message ⇒ Object

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."



114
115
116
# File 'lib/wurk/web/config.rb', line 114

def read_only_message
  @read_only_message
end

#tabs ⇒ Object (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.



196
197
198
# File 'lib/wurk/web/config.rb', line 196

def tabs
  @tabs
end

#translations ⇒ Object

Host copy overrides for the dashboard, keyed by locale tag and deep-merged over the shipped bundle by the SPA:

c.translations = { 'en' => { 'nav' => { 'queues' => 'Pipelines' } } }

Keyed rather than flat because the negotiated hint is not the last word — a ?locale= link or a stored pick outranks it — so the entry has to be chosen in the browser, against the locale actually rendered.



169
170
171
# File 'lib/wurk/web/config.rb', line 169

def translations
  @translations
end

#views ⇒ Object (readonly)

Upstream's ERB view-directory list. Wurk's dashboard is the SPA, and extension views resolve from their own root_dir, so appending here is accepted and ignored — it exists so gems doing views << dir boot.



200
201
202
# File 'lib/wurk/web/config.rb', line 200

def views
  @views
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.



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

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)


328
329
330
331
332
# File 'lib/wurk/web/config.rb', line 328

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

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

#custom_tabs ⇒ Object

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).



233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/wurk/web/config.rb', line 233

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)

    owner = extension_for_index(path)
    next if owner.nil? && NATIVE_TAB_PATHS.include?(path.to_s.delete_suffix('/'))

    { name: name, path: path.to_s, ext_name: owner&.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).



248
249
250
251
# File 'lib/wurk/web/config.rb', line 248

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.



258
259
260
261
262
# File 'lib/wurk/web/config.rb', line 258

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_url ⇒ Object



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

def profile_store_url = @options[:profile_store_url]

#profile_store_url=(value) ⇒ Object



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

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

#profile_view_url ⇒ Object

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.



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

def profile_view_url  = @options[:profile_view_url]

#profile_view_url=(value) ⇒ Object



102
103
104
# File 'lib/wurk/web/config.rb', line 102

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

#rack_app(inner) ⇒ Object

Builds the host-middleware chain wrapping inner, cached per inner (by identity) against a snapshot of middlewares. Per inner because two callers live side by side — the engine's MiddlewareStack and the standalone Sidekiq::Web.call — and a single slot made each request on one evict the other's chain. The snapshot matters because middlewares is the live array (upstream surface): direct mutation after the first request triggers a rebuild instead of silently serving the stale chain. Copy triples and positional-argument lists so edits cannot mutate the snapshot too; argument objects and blocks retain their identities (their internal mutations are not tracked). Steady state is one Hash lookup plus an == over a handful of entries.



285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/wurk/web/config.rb', line 285

def rack_app(inner)
  snapshot, app = @rack_apps[inner]
  return app if app && snapshot == @middlewares

  stack = middleware_snapshot
  app = ::Rack::Builder.new do
    stack.each { |middleware, args, block| use(middleware, *args, &block) }
    run inner
  end.to_app
  @rack_apps[inner] = [stack, app]
  app
end

#read_only=(value) ⇒ Object

Read-only mode. When on, the Authorization middleware blocks every non-GET request (retry/kill/add_to_queue/delete/pause/unpause/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.



303
304
305
# File 'lib/wurk/web/config.rb', line 303

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

#read_only? ⇒ Boolean

Returns:

  • (Boolean)


307
308
309
# File 'lib/wurk/web/config.rb', line 307

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. The wide keyword signature matches Sidekiq::Web::Config#register_extension (spec §25.2).

Yields:

  • (_self)

Yield Parameters:



212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/wurk/web/config.rb', line 212

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



311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/wurk/web/config.rb', line 311

def reset!
  @options = OPTIONS.dup
  @authorization = nil
  @read_only = env_read_only?
  @read_only_message = nil
  self.default_theme = ENV.fetch('WURK_WEB_DEFAULT_THEME', nil)
  @max_streams = nil
  @middlewares = []
  @rack_apps = {}.compare_by_identity
  init_extensions!
  init_locales!
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. Each inner app's chain is cached until the middleware configuration changes.



270
271
272
# File 'lib/wurk/web/config.rb', line 270

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