solid_stack_web 1.2.0 → 1.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3dbb76a6507f2e2a0480bafe3b9e18ac8f34777563c853911dfaf7b7f549f23
4
- data.tar.gz: 49f69c487ad4580d704ce122e46024ba96ff05c0c4b25166d64148128a28949d
3
+ metadata.gz: f5c82dfa829278df1d7fe7b59637d2b7c7b2ef31ea8ec9fb7ff2fe1234133658
4
+ data.tar.gz: eb1e7519de019b3921c5ef3536b806b902790a51eac340d94084e59e1ef4650c
5
5
  SHA512:
6
- metadata.gz: aa1598c4f1ee1a527ccc5b50240e5c866274e848ad2e7245dfe02ef6f05a6b1b889a7eb88ce80cad46f72f1b7f533a858b47cb1a3d130fd300c8bb32698d9842
7
- data.tar.gz: 752a9f8176b029fa54f7c0dcef93081d4a6b25bc75dccebab67ae7561854ff3c5d1f9ae96804ac290841afbc38630b7329d0472b45fbaf7cbf95d3f342797b0b
6
+ metadata.gz: 33821254738d6f8c2ee5cc177ef01f600988d905ccf7020285b4a3368b29db7c5f19811aabaf45ade463a433304e70639047bf8120bc0718ee2c9442a93686a7
7
+ data.tar.gz: 780cbf594e35533f5ee0374e41945a6a1319c22151c6fb2b9b7a82f1aaf256651b6e836777e3939b305bcff510144a5d3d84de014f6e8bc67cab67959f761725
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/eclectic-coding/solid_stack_web/actions/workflows/ci.yml/badge.svg)](https://github.com/eclectic-coding/solid_stack_web/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://badge.fury.io/rb/solid_stack_web.svg)](https://badge.fury.io/rb/solid_stack_web)
5
+ [![Downloads](https://img.shields.io/gem/dt/solid_stack_web.svg)](https://rubygems.org/gems/solid_stack_web)
5
6
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.3-ruby)](https://www.ruby-lang.org)
6
7
  [![codecov](https://codecov.io/gh/eclectic-coding/solid_stack_web/branch/main/graph/badge.svg)](https://codecov.io/gh/eclectic-coding/solid_stack_web)
7
8
 
@@ -22,7 +22,7 @@ module SolidStackWeb
22
22
 
23
23
  def current_section
24
24
  case controller_name
25
- when "jobs", "failed_jobs", "queues", "processes", "history", "scheduled_jobs", "recurring_tasks" then :queue
25
+ when "jobs", "failed_jobs", "queues", "processes", "history", "scheduled_jobs", "recurring_tasks", "audit" then :queue
26
26
  when "cache", "cache_entries" then :cache
27
27
  when "cable" then :cable
28
28
  else :overview
@@ -51,6 +51,28 @@ module SolidStackWeb
51
51
  request_http_basic_authentication("Solid Stack Dashboard")
52
52
  end
53
53
 
54
+ def record_audit(action, job_class: nil, queue_name: nil, item_count: 1)
55
+ AuditEvent.create!(
56
+ action: action,
57
+ actor: resolve_current_actor,
58
+ job_class: job_class,
59
+ queue_name: queue_name,
60
+ item_count: item_count
61
+ )
62
+ rescue => e
63
+ Rails.logger.error("[SolidStackWeb] Audit log failed: #{e.message}")
64
+ end
65
+
66
+ def resolve_current_actor
67
+ block = SolidStackWeb.current_actor
68
+ return nil unless block
69
+
70
+ instance_exec(&block)
71
+ rescue => e
72
+ Rails.logger.error("[SolidStackWeb] current_actor resolution failed: #{e.message}")
73
+ nil
74
+ end
75
+
54
76
  def render_not_found
55
77
  render "solid_stack_web/errors/not_found", status: :not_found
56
78
  end
@@ -0,0 +1,49 @@
1
+ module SolidStackWeb
2
+ class AuditController < ApplicationController
3
+ def index
4
+ unless AuditEvent.table_exists?
5
+ redirect_to root_path,
6
+ alert: "Audit log requires running `rails solid_stack_web:install:migrations && rails db:migrate`."
7
+ return
8
+ end
9
+
10
+ set_filters
11
+ scope = audit_scope
12
+
13
+ respond_to do |format|
14
+ format.html { @pagy, @events = pagy(scope) }
15
+ format.csv do
16
+ send_data audit_csv(scope),
17
+ filename: "audit-log-#{Date.today}.csv",
18
+ type: "text/csv", disposition: "attachment"
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def set_filters
26
+ @action_filter = params[:audit_action].presence_in(AuditEvent::ACTIONS)
27
+ @actor_filter = params[:actor].presence
28
+ @queue_filter = params[:queue].presence
29
+ end
30
+
31
+ def audit_scope
32
+ scope = AuditEvent.recent
33
+ scope = scope.where(action: @action_filter) if @action_filter
34
+ scope = scope.where(actor: @actor_filter) if @actor_filter
35
+ scope = scope.where(queue_name: @queue_filter) if @queue_filter
36
+ scope
37
+ end
38
+
39
+ def audit_csv(scope)
40
+ CSV.generate(headers: true) do |csv|
41
+ csv << %w[id action actor job_class queue_name item_count created_at]
42
+ scope.each do |event|
43
+ csv << [event.id, event.action, event.actor, event.job_class,
44
+ event.queue_name, event.item_count, event.created_at.iso8601]
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -6,6 +6,7 @@ module SolidStackWeb
6
6
  @cable_stats = CableStats.new.to_h
7
7
  @throughput = ThroughputSparkline.new
8
8
  @failures = FailedJobSparkline.new
9
+ AlertWebhook.check(@queue_stats)
9
10
  end
10
11
  end
11
12
  end
@@ -6,6 +6,7 @@ module SolidStackWeb
6
6
  def create
7
7
  count = @ids.size
8
8
  SolidQueue::FailedExecution.where(id: @ids).each(&:retry)
9
+ record_audit("failed_jobs_retried", item_count: count)
9
10
  redirect_to failed_jobs_path, notice: "#{count} #{count == 1 ? "job" : "jobs"} retried."
10
11
  rescue => e
11
12
  redirect_to failed_jobs_path, alert: "Could not retry jobs: #{e.message}"
@@ -14,6 +15,7 @@ module SolidStackWeb
14
15
  def destroy
15
16
  job_ids = SolidQueue::FailedExecution.where(id: @ids).pluck(:job_id)
16
17
  count = SolidQueue::Job.where(id: job_ids).destroy_all.size
18
+ record_audit("failed_jobs_discarded", item_count: count)
17
19
  redirect_to failed_jobs_path, notice: "#{count} #{count == 1 ? "job" : "jobs"} discarded."
18
20
  rescue => e
19
21
  redirect_to failed_jobs_path, alert: "Could not discard jobs: #{e.message}"
@@ -28,7 +28,10 @@ module SolidStackWeb
28
28
 
29
29
  def destroy
30
30
  @execution = ::SolidQueue::FailedExecution.find(params[:id])
31
+ job_class = @execution.job.class_name
32
+ queue_name = @execution.job.queue_name
31
33
  @execution.job.destroy!
34
+ record_audit("failed_job_discarded", job_class: job_class, queue_name: queue_name)
32
35
  @executions_remain = ::SolidQueue::FailedExecution.exists?
33
36
  @notice = "Job discarded."
34
37
 
@@ -40,6 +43,7 @@ module SolidStackWeb
40
43
 
41
44
  def retry
42
45
  execution = ::SolidQueue::FailedExecution.find(params[:id])
46
+ record_audit("failed_job_retried", job_class: execution.job.class_name, queue_name: execution.job.queue_name)
43
47
  execution.retry
44
48
  redirect_to failed_jobs_path, notice: "Job retried."
45
49
  end
@@ -8,6 +8,7 @@ module SolidStackWeb
8
8
  ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
9
9
  job_ids = Job::EXECUTION_MODELS[status].where(id: ids).pluck(:job_id)
10
10
  count = SolidQueue::Job.where(id: job_ids).destroy_all.size
11
+ record_audit("jobs_discarded", item_count: count)
11
12
 
12
13
  redirect_to jobs_path(
13
14
  status: status,
@@ -31,7 +31,10 @@ module SolidStackWeb
31
31
  def destroy
32
32
  if params[:id]
33
33
  @execution = Job::EXECUTION_MODELS[@status].find(params[:id])
34
+ job_class = @execution.job.class_name
35
+ queue_name = @execution.job.queue_name
34
36
  @execution.job.destroy!
37
+ record_audit("job_discarded", job_class: job_class, queue_name: queue_name)
35
38
  @executions_remain = Job::EXECUTION_MODELS[@status].exists?
36
39
  @notice = "Job discarded."
37
40
 
@@ -42,6 +45,7 @@ module SolidStackWeb
42
45
  else
43
46
  job_ids = filtered_scope.pluck(:job_id)
44
47
  count = SolidQueue::Job.where(id: job_ids).destroy_all.size
48
+ record_audit("jobs_discarded", item_count: count)
45
49
  redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction),
46
50
  notice: "#{count} #{count == 1 ? "job" : "jobs"} discarded."
47
51
  end
@@ -82,10 +86,14 @@ module SolidStackWeb
82
86
 
83
87
  def jobs_csv
84
88
  CSV.generate(headers: true) do |csv|
85
- csv << %w[id class_name queue_name status priority enqueued_at]
89
+ headers = %w[id class_name queue_name status priority enqueued_at]
90
+ headers << "wait_time_seconds" if @status == "claimed"
91
+ csv << headers
86
92
  filtered_scope.each do |execution|
87
93
  job = execution.job
88
- csv << [job.id, job.class_name, job.queue_name, @status, job.priority, job.created_at.iso8601]
94
+ row = [job.id, job.class_name, job.queue_name, @status, job.priority, job.created_at.iso8601]
95
+ row << (execution.created_at - job.created_at).to_i if @status == "claimed"
96
+ csv << row
89
97
  end
90
98
  end
91
99
  end
@@ -2,11 +2,13 @@ module SolidStackWeb
2
2
  class Queues::PausesController < ApplicationController
3
3
  def create
4
4
  ::SolidQueue::Pause.find_or_create_by!(queue_name: params[:queue_id])
5
+ record_audit("queue_paused", queue_name: params[:queue_id])
5
6
  redirect_back_or_to queues_path
6
7
  end
7
8
 
8
9
  def destroy
9
10
  ::SolidQueue::Pause.find_by(queue_name: params[:queue_id])&.destroy
11
+ record_audit("queue_resumed", queue_name: params[:queue_id])
10
12
  redirect_back_or_to queues_path
11
13
  end
12
14
  end
@@ -87,6 +87,15 @@ module SolidStackWeb
87
87
  end
88
88
  end
89
89
 
90
+ def audit_action_badge_class(action)
91
+ case action
92
+ when /discard/ then "sqw-badge--failed"
93
+ when /retry/ then "sqw-badge--scheduled"
94
+ when "queue_paused" then "sqw-badge--blocked"
95
+ when "queue_resumed" then "sqw-badge--ready"
96
+ end
97
+ end
98
+
90
99
  def sort_header_th(label, col, url_proc, current_sort:, current_dir:)
91
100
  is_active = current_sort == col
92
101
  next_dir = (is_active && current_dir == "desc") ? "asc" : "desc"
@@ -39,6 +39,16 @@ module SolidStackWeb
39
39
  alerts << { type: "queue_depth", queue: queue_name.to_s, count: count, threshold: threshold } if count >= threshold
40
40
  end
41
41
 
42
+ if (job_threshold = SolidStackWeb.slow_job_threshold) && (count_threshold = SolidStackWeb.alert_slow_job_count_threshold)
43
+ count = ::SolidQueue::ClaimedExecution.where("created_at <= ?", job_threshold.seconds.ago).count
44
+ alerts << { type: "slow_jobs", count: count, threshold: count_threshold } if count >= count_threshold
45
+ end
46
+
47
+ if (threshold = SolidStackWeb.alert_stale_process_threshold)
48
+ count = @queue_stats[:processes_stale]
49
+ alerts << { type: "stale_processes", count: count, threshold: threshold } if count >= threshold
50
+ end
51
+
42
52
  alerts
43
53
  end
44
54
 
@@ -0,0 +1,17 @@
1
+ module SolidStackWeb
2
+ class AuditEvent < ActiveRecord::Base
3
+ self.table_name = "solid_stack_web_audit_events"
4
+
5
+ ACTIONS = %w[
6
+ job_discarded jobs_discarded
7
+ failed_job_retried failed_jobs_retried
8
+ failed_job_discarded failed_jobs_discarded
9
+ queue_paused queue_resumed
10
+ ].freeze
11
+
12
+ validates :action, presence: true, inclusion: { in: ACTIONS }
13
+ validates :item_count, numericality: { greater_than: 0 }
14
+
15
+ scope :recent, -> { order(created_at: :desc) }
16
+ end
17
+ end
@@ -65,6 +65,8 @@
65
65
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "history"}" %>
66
66
  <%= link_to "Processes", processes_path,
67
67
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "processes"}" %>
68
+ <%= link_to "Audit", audit_path,
69
+ class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "audit"}" %>
68
70
  </div>
69
71
  </nav>
70
72
  <% end %>
@@ -0,0 +1,86 @@
1
+ <div class="sqw-page-header sqw-page-header--split">
2
+ <h1 class="sqw-page-title">Audit Log</h1>
3
+ <% if @events&.any? %>
4
+ <%= link_to "Export CSV",
5
+ audit_path(format: :csv, audit_action: @action_filter, actor: @actor_filter, queue: @queue_filter),
6
+ class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
7
+ <% end %>
8
+ </div>
9
+
10
+ <form class="sqw-filters" action="<%= audit_path %>" method="get">
11
+ <select name="audit_action" class="sqw-select" aria-label="Filter by action"
12
+ onchange="this.form.submit()">
13
+ <option value="">All actions</option>
14
+ <% SolidStackWeb::AuditEvent::ACTIONS.each do |action| %>
15
+ <option value="<%= action %>" <%= "selected" if @action_filter == action %>><%= action.tr("_", " ") %></option>
16
+ <% end %>
17
+ </select>
18
+
19
+ <% if @actor_filter.present? %>
20
+ <span class="sqw-badge">
21
+ Actor: <%= @actor_filter %>
22
+ <%= link_to "×", audit_path(audit_action: @action_filter, queue: @queue_filter), class: "sqw-muted", aria: { label: "Clear actor filter" } %>
23
+ </span>
24
+ <% end %>
25
+
26
+ <% if @queue_filter.present? %>
27
+ <span class="sqw-badge">
28
+ Queue: <%= @queue_filter %>
29
+ <%= link_to "×", audit_path(audit_action: @action_filter, actor: @actor_filter), class: "sqw-muted", aria: { label: "Clear queue filter" } %>
30
+ </span>
31
+ <% end %>
32
+
33
+ <% if @action_filter.present? || @actor_filter.present? || @queue_filter.present? %>
34
+ <%= link_to "Clear all", audit_path, class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
35
+ <% end %>
36
+ </form>
37
+
38
+ <% if @events.any? %>
39
+ <table class="sqw-table">
40
+ <thead>
41
+ <tr>
42
+ <th scope="col">Time</th>
43
+ <th scope="col">Action</th>
44
+ <th scope="col">Actor</th>
45
+ <th scope="col">Job Class</th>
46
+ <th scope="col">Queue</th>
47
+ <th scope="col">Count</th>
48
+ </tr>
49
+ </thead>
50
+ <tbody>
51
+ <% @events.each do |event| %>
52
+ <tr>
53
+ <td class="sqw-muted"><%= local_time(event.created_at, format: :long) %></td>
54
+ <td>
55
+ <span class="sqw-badge <%= audit_action_badge_class(event.action) %>">
56
+ <%= event.action.tr("_", " ") %>
57
+ </span>
58
+ </td>
59
+ <td>
60
+ <% if event.actor.present? %>
61
+ <%= link_to event.actor, audit_path(audit_action: @action_filter, actor: event.actor, queue: @queue_filter) %>
62
+ <% else %>
63
+ <span class="sqw-muted">—</span>
64
+ <% end %>
65
+ </td>
66
+ <td class="sqw-monospace"><%= event.job_class.presence || content_tag(:span, "—", class: "sqw-muted") %></td>
67
+ <td>
68
+ <% if event.queue_name.present? %>
69
+ <span class="sqw-badge sqw-badge--queue">
70
+ <%= link_to event.queue_name, audit_path(audit_action: @action_filter, actor: @actor_filter, queue: event.queue_name) %>
71
+ </span>
72
+ <% else %>
73
+ <span class="sqw-muted">—</span>
74
+ <% end %>
75
+ </td>
76
+ <td><%= event.item_count %></td>
77
+ </tr>
78
+ <% end %>
79
+ </tbody>
80
+ </table>
81
+ <%== @pagy.series_nav(aria_label: "Pagination") if @pagy.pages > 1 %>
82
+ <% else %>
83
+ <div class="sqw-empty" id="sqd-empty">
84
+ <p class="sqw-muted">No audit events recorded yet.</p>
85
+ </div>
86
+ <% end %>
@@ -112,6 +112,7 @@
112
112
  <%= sort_header_th("Priority", "priority", sort_url, current_sort: @sort, current_dir: @direction) %>
113
113
  <%= sort_header_th("Enqueued At", "created_at", sort_url, current_sort: @sort, current_dir: @direction) %>
114
114
  <% if @status == "scheduled" %><th scope="col">Scheduled At</th><% end %>
115
+ <% if @status == "claimed" %><th scope="col">Wait Time</th><% end %>
115
116
  <th scope="col"><span class="sqw-sr-only">Actions</span></th>
116
117
  </tr>
117
118
  </thead>
@@ -132,6 +133,9 @@
132
133
  <% if @status == "scheduled" %>
133
134
  <td id="scheduled_at_<%= execution.id %>" class="sqw-muted"><%= local_time(execution.scheduled_at) %></td>
134
135
  <% end %>
136
+ <% if @status == "claimed" %>
137
+ <td class="sqw-monospace"><%= format_duration(execution.created_at - execution.job.created_at) %></td>
138
+ <% end %>
135
139
  <td class="sqw-actions">
136
140
  <% if @status == "scheduled" %>
137
141
  <%= button_to "Run Now", scheduled_job_path(execution),
data/config/importmap.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.23/dist/turbo.es2017-esm.js"
2
2
  pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js"
3
3
  pin "solid_stack_web", to: "solid_stack_web/application.js"
4
+ pin "solid_stack_web/filter_persist_controller", to: "solid_stack_web/filter_persist_controller.js"
4
5
  pin "solid_stack_web/refresh_controller", to: "solid_stack_web/refresh_controller.js"
5
6
  pin "solid_stack_web/search_controller", to: "solid_stack_web/search_controller.js"
6
7
  pin "solid_stack_web/selection_controller", to: "solid_stack_web/selection_controller.js"
data/config/routes.rb CHANGED
@@ -36,6 +36,7 @@ SolidStackWeb::Engine.routes.draw do
36
36
  get "metrics", to: "metrics#index", as: :metrics
37
37
  get "stats", to: "stats#index", as: :stats
38
38
  get "history", to: "history#index", as: :history
39
+ get "audit", to: "audit#index", as: :audit
39
40
  get "cache", to: "cache#index", as: :cache
40
41
  resources :cache_entries, only: [:index, :show, :destroy], path: "cache/entries"
41
42
  resource :cache_flush, only: [:destroy], path: "cache/flush", controller: "cache/flushes"
@@ -0,0 +1,24 @@
1
+ require "rails/generators/base"
2
+ require "rails/generators/migration"
3
+
4
+ module SolidStackWeb
5
+ module Generators
6
+ class MigrationsGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Generates the solid_stack_web_audit_events migration"
12
+
13
+ def self.next_migration_number(dirname)
14
+ next_migration_number = current_migration_number(dirname) + 1
15
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
16
+ end
17
+
18
+ def create_migration_file
19
+ migration_template "create_solid_stack_web_audit_events.rb.tt",
20
+ "db/migrate/create_solid_stack_web_audit_events.rb"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ class CreateSolidStackWebAuditEvents < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :solid_stack_web_audit_events do |t|
4
+ t.string :action, null: false
5
+ t.string :actor
6
+ t.string :job_class
7
+ t.string :queue_name
8
+ t.integer :item_count, null: false, default: 1
9
+ t.datetime :created_at, null: false
10
+ end
11
+
12
+ add_index :solid_stack_web_audit_events, :created_at
13
+ add_index :solid_stack_web_audit_events, :action
14
+ add_index :solid_stack_web_audit_events, :actor
15
+ end
16
+ end
@@ -41,11 +41,20 @@ SolidStackWeb.configure do |config|
41
41
  # Alert webhook — POST to this URL when a threshold is breached.
42
42
  # Delivery failures are silently swallowed; configure a cooldown to avoid storms.
43
43
  #
44
- # config.alert_webhook_url = "https://hooks.example.com/my-alert"
45
- # config.alert_failure_threshold = 10 # fire when failed jobs >= this
46
- # config.alert_queue_thresholds = { # fire when a queue's ready depth >= value
44
+ # config.alert_webhook_url = "https://hooks.example.com/my-alert"
45
+ # config.alert_failure_threshold = 10 # fire when failed jobs >= this
46
+ # config.alert_queue_thresholds = { # fire when a queue's ready depth >= value
47
47
  # "critical" => 50,
48
48
  # "default" => 500
49
49
  # }
50
- # config.alert_webhook_cooldown = 3600 # seconds between repeat alerts
50
+ # Audit log actor — block runs in controller context, return a string identifying the current user.
51
+ # Requires running `rails solid_stack_web:install:migrations && rails db:migrate` first.
52
+ #
53
+ # config.current_actor do
54
+ # current_user&.email
55
+ # end
56
+
57
+ # config.alert_slow_job_count_threshold = 3 # fire when N+ claimed jobs exceed slow_job_threshold duration
58
+ # config.alert_stale_process_threshold = 1 # fire when N+ workers have a stale heartbeat (>5 min old)
59
+ # config.alert_webhook_cooldown = 3600 # seconds between repeat alerts
51
60
  end
@@ -1,3 +1,3 @@
1
1
  module SolidStackWeb
2
- VERSION = "1.2.0"
2
+ VERSION = "1.4.0"
3
3
  end
@@ -6,6 +6,7 @@ module SolidStackWeb
6
6
  attr_writer :page_size, :connects_to, :slow_job_threshold,
7
7
  :alert_webhook_url, :alert_webhook_cooldown,
8
8
  :alert_failure_threshold, :alert_queue_thresholds,
9
+ :alert_slow_job_count_threshold, :alert_stale_process_threshold,
9
10
  :dashboard_refresh_interval, :default_refresh_interval,
10
11
  :search_results_limit, :allow_value_preview
11
12
 
@@ -37,6 +38,14 @@ module SolidStackWeb
37
38
  @alert_queue_thresholds || {}
38
39
  end
39
40
 
41
+ def alert_slow_job_count_threshold
42
+ @alert_slow_job_count_threshold
43
+ end
44
+
45
+ def alert_stale_process_threshold
46
+ @alert_stale_process_threshold
47
+ end
48
+
40
49
  def dashboard_refresh_interval
41
50
  @dashboard_refresh_interval || 5_000
42
51
  end
@@ -75,6 +84,11 @@ module SolidStackWeb
75
84
  @authenticate
76
85
  end
77
86
 
87
+ def current_actor(&block)
88
+ @current_actor = block if block_given?
89
+ @current_actor
90
+ end
91
+
78
92
  def deprecator
79
93
  @deprecator ||= ActiveSupport::Deprecation.new("1.0", "SolidStackWeb")
80
94
  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: 1.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -151,6 +151,7 @@ files:
151
151
  - app/assets/stylesheets/solid_stack_web/_12_dark_mode.css
152
152
  - app/assets/stylesheets/solid_stack_web/application.css
153
153
  - app/controllers/solid_stack_web/application_controller.rb
154
+ - app/controllers/solid_stack_web/audit_controller.rb
154
155
  - app/controllers/solid_stack_web/cable/channel_purges_controller.rb
155
156
  - app/controllers/solid_stack_web/cable/purges_controller.rb
156
157
  - app/controllers/solid_stack_web/cable_controller.rb
@@ -184,6 +185,7 @@ files:
184
185
  - app/javascript/solid_stack_web/theme_controller.js
185
186
  - app/javascript/solid_stack_web/timestamp_controller.js
186
187
  - app/models/solid_stack_web/alert_webhook.rb
188
+ - app/models/solid_stack_web/audit_event.rb
187
189
  - app/models/solid_stack_web/cable_stats.rb
188
190
  - app/models/solid_stack_web/cable_timeline.rb
189
191
  - app/models/solid_stack_web/cache_size_stats.rb
@@ -196,6 +198,7 @@ files:
196
198
  - app/models/solid_stack_web/queue_stats.rb
197
199
  - app/models/solid_stack_web/throughput_sparkline.rb
198
200
  - app/views/layouts/solid_stack_web/application.html.erb
201
+ - app/views/solid_stack_web/audit/index.html.erb
199
202
  - app/views/solid_stack_web/cable/index.html.erb
200
203
  - app/views/solid_stack_web/cable_messages/index.html.erb
201
204
  - app/views/solid_stack_web/cache/index.html.erb
@@ -224,6 +227,8 @@ files:
224
227
  - config/routes.rb
225
228
  - docs/screenshots/demo.gif
226
229
  - lib/generators/solid_stack_web/install/install_generator.rb
230
+ - lib/generators/solid_stack_web/install/migrations_generator.rb
231
+ - lib/generators/solid_stack_web/install/templates/create_solid_stack_web_audit_events.rb.tt
227
232
  - lib/generators/solid_stack_web/install/templates/initializer.rb
228
233
  - lib/solid_stack_web.rb
229
234
  - lib/solid_stack_web/engine.rb