newshound 0.2.7 → 1.0.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: 58c2540992cef9f0214865a6d365d0e2b2e98e6f7a9d186df9c50affbc254498
4
- data.tar.gz: e1db27edd5f1f665bb30154ce0fe2aea85781397e6caaf71f171d2c5e5a4fc8c
3
+ metadata.gz: c59db21db9a10838f80bf529e985a4cb1af723bcf86ba4893db0d597b1a10059
4
+ data.tar.gz: 66a6aa516fc12f6c7003ed286c0ffaef9fc7112c2d659b96a927b09de53c0a54
5
5
  SHA512:
6
- metadata.gz: fb50eb837a41ceafd5e5b47a5a90762d7e04b5642c520e24a103202a87eead65d241dcb9674939d1cef34fdb9a1fb186d885cd19cdd154ac9d0049f9bb6ccefd
7
- data.tar.gz: 5344958684e704bba2f18ef8bce4bccd04ad954119ed8f0b5e57f6d2e6b1af422ac70642e32eceb7af488946102b0e71474f540db38030b930222b88f8ca97da
6
+ metadata.gz: 210e8a10e784ab3a72a34291465faf6c2f213a332eca30d392561609a0316a7c28e30ae532a814bf4eca2f42fb630ed43ce8dcc4c2db458d05267fc14419c5ac
7
+ data.tar.gz: 6de9641e2cfb691ccb94c4185c54dabaa78d9d92ac57e173eed5087ebb5c0dd5e468c81f79d2ca83b20376bc2d29e9137e766354028493c568c0d24eca615b07
data/CHANGELOG.md CHANGED
@@ -5,14 +5,30 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.2.7] - 2026-01-30
8
+ ## [0.2.8] - 2026-02-10
9
+
10
+ ### Changed
9
11
 
10
- ## [] -
12
+ - Ruby support to 3.3+ and added Ruby 4.0 to CI matrix (24e5b86)
13
+ - Job monitoring uses configurable adapter pattern via config.job_source (8d33d30)
11
14
 
12
- ###
15
+ ### Removed
13
16
 
14
- - [0.2.6] -2025-01-13
17
+ - Ruby 3.1 and 3.2 support (24e5b86)
18
+ - Hard dependency on que gem (8d33d30)
19
+ - QueReporter class (replaced by JobReporter + Jobs adapters) Version: major (8d33d30)
15
20
 
16
21
  ### Fixed
17
22
 
18
- - Solid Errors title is able to present in banner.
23
+ - Set the correct location for the repository on the web. (cb7bb88)
24
+ - test_exceptions rake task calling report instead of banner_data (e440188)
25
+ - Banner overlaying content in apps with !important body padding-top rules (16f22f1)
26
+
27
+ ### Added
28
+
29
+ - Jobs adapter pattern with Base class and registry (c50ba9c)
30
+ - Jobs::Que adapter for Que job backend (c50ba9c)
31
+ - JobReporter that delegates to a configurable job source adapter (c50ba9c)
32
+ - ExceptionReporter#formatted_exception_count and #exception_summary helpers (e440188)
33
+
34
+ ## [0.2.7] - 2026-01-30
@@ -31,7 +31,7 @@ module Newshound
31
31
  say " rake newshound:test_jobs"
32
32
  say ""
33
33
  say " For more information, visit:", :blue
34
- say " https://github.com/salbanez/newshound"
34
+ say " https://github.com/SOFware/newshound"
35
35
  say "===============================================================================", :green
36
36
  say ""
37
37
  end
@@ -14,6 +14,11 @@ Newshound.configure do |config|
14
14
  # Default is :exception_track
15
15
  config.exception_source = :exception_track # or :solid_errors
16
16
 
17
+ # Job source adapter for monitoring background jobs
18
+ # Uncomment and set to enable job monitoring in the banner
19
+ # config.job_source = :que # or a custom adapter instance
20
+ # See Newshound::Jobs::Base for the adapter interface
21
+
17
22
  # User roles that are authorized to view the Newshound banner
18
23
  # These should match the role values in your User model
19
24
  # Default is [:developer, :super_user]
@@ -4,7 +4,7 @@ module Newshound
4
4
  class Configuration
5
5
  attr_accessor :exception_limit, :enabled, :authorized_roles,
6
6
  :current_user_method, :authorization_block, :exception_source,
7
- :warning_source, :warning_limit
7
+ :warning_source, :warning_limit, :job_source
8
8
 
9
9
  def initialize
10
10
  @exception_limit = 10
@@ -15,6 +15,7 @@ module Newshound
15
15
  @exception_source = :exception_track
16
16
  @warning_source = nil
17
17
  @warning_limit = 10
18
+ @job_source = nil
18
19
  end
19
20
 
20
21
  # Allow custom authorization logic
@@ -29,10 +29,24 @@ module Newshound
29
29
  # Returns data formatted for the banner UI
30
30
  def banner_data
31
31
  {
32
- exceptions: recent_exceptions.map { |exception| exception_source.format_for_banner(exception) }
32
+ exceptions: recent_exceptions.map { |exception|
33
+ exception_source.format_for_banner(exception)
34
+ }
33
35
  }
34
36
  end
35
37
 
38
+ def formatted_exception_count(
39
+ format = "Total exceptions: %s"
40
+ )
41
+ format % recent_exceptions.length
42
+ end
43
+
44
+ def exception_summary
45
+ recent_exceptions.map.with_index(1) do |ex, i|
46
+ exception_source.format_for_report(ex, i)
47
+ end
48
+ end
49
+
36
50
  private
37
51
 
38
52
  def resolve_exception_source(source)
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Newshound
4
+ class JobReporter
5
+ attr_reader :job_source, :configuration, :logger
6
+
7
+ def initialize(job_source: nil, configuration: nil, logger: nil)
8
+ @configuration = configuration || Newshound.configuration
9
+ @job_source = resolve_job_source(job_source)
10
+ @logger = logger || (defined?(Rails) ? Rails.logger : Logger.new($stdout))
11
+ end
12
+
13
+ def generate_report
14
+ return no_jobs_block unless job_source
15
+
16
+ [
17
+ {
18
+ type: "section",
19
+ text: {
20
+ type: "mrkdwn",
21
+ text: "*📊 Job Queue Status*"
22
+ }
23
+ },
24
+ job_counts_section,
25
+ queue_health_section
26
+ ].compact
27
+ end
28
+ alias_method :report, :generate_report
29
+
30
+ def banner_data
31
+ return {queue_stats: {}} unless job_source
32
+
33
+ job_source.format_for_banner
34
+ end
35
+
36
+ private
37
+
38
+ def resolve_job_source(source)
39
+ source ||= @configuration.job_source
40
+ return nil if source.nil?
41
+
42
+ source.is_a?(Symbol) ? Jobs.source(source) : source
43
+ end
44
+
45
+ def job_counts_section
46
+ counts = job_source.job_counts_by_type
47
+
48
+ return no_jobs_section if counts.empty?
49
+
50
+ {
51
+ type: "section",
52
+ text: {
53
+ type: "mrkdwn",
54
+ text: format_job_counts(counts)
55
+ }
56
+ }
57
+ end
58
+
59
+ def format_job_counts(counts)
60
+ lines = ["*Job Counts by Type:*"]
61
+
62
+ counts.each do |job_class, stats|
63
+ status_emoji = (stats[:failed] > 0) ? "⚠️" : "✅"
64
+ lines << "• #{status_emoji} *#{job_class}*: #{stats[:total]} total (#{stats[:success]} success, #{stats[:failed]} failed)"
65
+ end
66
+
67
+ lines.join("\n")
68
+ end
69
+
70
+ def queue_health_section
71
+ stats = job_source.queue_statistics
72
+
73
+ {
74
+ type: "section",
75
+ text: {
76
+ type: "mrkdwn",
77
+ text: format_queue_health(stats)
78
+ }
79
+ }
80
+ end
81
+
82
+ def format_queue_health(stats)
83
+ health_emoji = if stats[:failed] > 10
84
+ "🔴"
85
+ else
86
+ (stats[:failed] > 5) ? "🟡" : "🟢"
87
+ end
88
+
89
+ <<~TEXT
90
+ *Queue Health #{health_emoji}*
91
+ • *Ready to Run:* #{stats[:ready]}
92
+ • *Scheduled:* #{stats[:scheduled]}
93
+ • *Failed (Retry Queue):* #{stats[:failed]}
94
+ • *Completed Today:* #{stats[:finished_today]}
95
+ TEXT
96
+ end
97
+
98
+ def no_jobs_section
99
+ {
100
+ type: "section",
101
+ text: {
102
+ type: "mrkdwn",
103
+ text: "*No jobs found in the queue*"
104
+ }
105
+ }
106
+ end
107
+
108
+ def no_jobs_block
109
+ [
110
+ {
111
+ type: "section",
112
+ text: {
113
+ type: "mrkdwn",
114
+ text: "*✅ No Job Source Configured*"
115
+ }
116
+ }
117
+ ]
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Newshound
4
+ module Jobs
5
+ # Base class for job source adapters
6
+ # Each adapter is responsible for:
7
+ # 1. Fetching queue statistics from its specific job backend
8
+ # 2. Fetching job counts grouped by type
9
+ # 3. Formatting job data for banner display
10
+ #
11
+ # Subclasses must implement:
12
+ # - #queue_statistics - Returns a hash of queue stats
13
+ # - #job_counts_by_type - Returns a hash of job class => counts
14
+ #
15
+ # Unlike Warnings::Base and Exceptions::Base (which format individual records),
16
+ # #format_for_banner takes no arguments and returns aggregate queue statistics.
17
+ # A default implementation is provided that delegates to #queue_statistics.
18
+ class Base
19
+ # Returns queue-level statistics
20
+ #
21
+ # @return [Hash] with keys :ready, :scheduled, :failed, :finished_today
22
+ def queue_statistics
23
+ raise NotImplementedError, "#{self.class} must implement #queue_statistics"
24
+ end
25
+
26
+ # Returns job counts grouped by job class
27
+ #
28
+ # @return [Hash] job_class => { success:, failed:, total: }
29
+ def job_counts_by_type
30
+ raise NotImplementedError, "#{self.class} must implement #job_counts_by_type"
31
+ end
32
+
33
+ # Returns data formatted for the banner UI
34
+ #
35
+ # @return [Hash] with key :queue_stats containing ready_to_run, scheduled, failed, completed_today
36
+ def format_for_banner
37
+ stats = queue_statistics
38
+
39
+ {
40
+ queue_stats: {
41
+ ready_to_run: stats[:ready],
42
+ scheduled: stats[:scheduled],
43
+ failed: stats[:failed],
44
+ completed_today: stats[:finished_today]
45
+ }
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Newshound
4
+ module Jobs
5
+ class Que < Base
6
+ attr_reader :logger
7
+
8
+ def initialize(logger: nil)
9
+ @logger = logger || (defined?(Rails) ? Rails.logger : Logger.new($stdout))
10
+ end
11
+
12
+ def queue_statistics
13
+ conn = ActiveRecord::Base.connection
14
+ current_time = conn.quote(Time.now)
15
+ beginning_of_day = conn.quote(Date.today.to_time)
16
+
17
+ {
18
+ ready: count_jobs("finished_at IS NULL AND expired_at IS NULL AND run_at <= #{current_time}"),
19
+ scheduled: count_jobs("finished_at IS NULL AND expired_at IS NULL AND run_at > #{current_time}"),
20
+ failed: count_jobs("error_count > 0 AND finished_at IS NULL"),
21
+ finished_today: count_jobs("finished_at >= #{beginning_of_day}")
22
+ }
23
+ rescue => e
24
+ logger.error "Failed to fetch Que statistics: #{e.message}"
25
+ {ready: 0, scheduled: 0, failed: 0, finished_today: 0}
26
+ end
27
+
28
+ def job_counts_by_type
29
+ results = ActiveRecord::Base.connection.execute(<<~SQL)
30
+ SELECT job_class, error_count, COUNT(*) as count
31
+ FROM que_jobs
32
+ WHERE finished_at IS NULL
33
+ GROUP BY job_class, error_count
34
+ ORDER BY job_class
35
+ SQL
36
+
37
+ results.each_with_object({}) do |row, hash|
38
+ job_class = row["job_class"]
39
+ error_count = row["error_count"].to_i
40
+ count = row["count"].to_i
41
+
42
+ hash[job_class] ||= {success: 0, failed: 0, total: 0}
43
+
44
+ if error_count.zero?
45
+ hash[job_class][:success] += count
46
+ else
47
+ hash[job_class][:failed] += count
48
+ end
49
+
50
+ hash[job_class][:total] += count
51
+ end
52
+ rescue => e
53
+ logger.error "Failed to fetch job counts: #{e.message}"
54
+ {}
55
+ end
56
+
57
+ private
58
+
59
+ def count_jobs(where_clause)
60
+ ActiveRecord::Base.connection.select_value(
61
+ "SELECT COUNT(*) FROM que_jobs WHERE #{where_clause}"
62
+ ).to_i
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.join(__dir__, "jobs", "*.rb")].each { |file| require file }
4
+
5
+ module Newshound
6
+ module Jobs
7
+ @registry = {}
8
+
9
+ class << self
10
+ attr_reader :registry
11
+
12
+ # Register a job adapter class with a symbolic name
13
+ #
14
+ # @param name [Symbol, String] The name to register the adapter under
15
+ # @param adapter_class [Class] The adapter class (must inherit from Jobs::Base)
16
+ def register(name, adapter_class)
17
+ @registry[name.to_sym] = adapter_class
18
+ end
19
+
20
+ # Get a job source adapter instance
21
+ #
22
+ # @param source [Symbol, Object] Either a symbolic name to look up, or an adapter instance
23
+ # @return [Jobs::Base] An instance of the job adapter
24
+ # @raise [RuntimeError] If the source symbol cannot be resolved
25
+ def source(source)
26
+ return source unless source.is_a?(Symbol)
27
+
28
+ if @registry.key?(source)
29
+ return @registry[source].new
30
+ end
31
+
32
+ constant = constants.find { |c| c.to_s.gsub(/(?<!^)([A-Z])/, "_\\1").downcase == source.to_s }
33
+ raise "Invalid job source: #{source}" unless constant
34
+
35
+ const_get(constant).new
36
+ end
37
+
38
+ def clear_registry!
39
+ @registry = {}
40
+ end
41
+ end
42
+ end
43
+ end
@@ -55,11 +55,11 @@ module Newshound
55
55
 
56
56
  def generate_banner_html
57
57
  exception_reporter = Newshound::ExceptionReporter.new
58
- que_reporter = Newshound::QueReporter.new
58
+ job_reporter = Newshound::JobReporter.new
59
59
  warning_reporter = Newshound::WarningReporter.new
60
60
 
61
61
  exception_data = exception_reporter.banner_data
62
- job_data = que_reporter.banner_data
62
+ job_data = job_reporter.banner_data
63
63
  warning_data = warning_reporter.banner_data
64
64
 
65
65
  # Generate HTML from template
@@ -91,25 +91,66 @@ module Newshound
91
91
  <<~JS
92
92
  <script>
93
93
  (function() {
94
+ var cachedPriority, cachedBodyRule;
95
+
96
+ // Detect once whether any non-newshound stylesheet uses !important on body padding-top
97
+ function detectPriority() {
98
+ var styles = document.querySelectorAll('style:not(#newshound-styles)');
99
+
100
+ for (var i = 0; i < styles.length; i++) {
101
+ if (styles[i].textContent.match(/body\\s*{[^}]*padding-top[^;]*!important/s)) {
102
+ return 'important';
103
+ }
104
+ }
105
+
106
+ try {
107
+ for (var s = 0; s < document.styleSheets.length; s++) {
108
+ var sheet = document.styleSheets[s];
109
+ if (sheet.ownerNode && sheet.ownerNode.id === 'newshound-styles') continue;
110
+ var rules = sheet.cssRules || [];
111
+ for (var r = 0; r < rules.length; r++) {
112
+ if (rules[r].selectorText === 'body' &&
113
+ rules[r].style.getPropertyPriority('padding-top') === 'important') {
114
+ return 'important';
115
+ }
116
+ }
117
+ }
118
+ } catch (e) {}
119
+
120
+ return '';
121
+ }
122
+
123
+ // Find the html body rule in the newshound stylesheet once
124
+ function findBodyRule() {
125
+ var styleEl = document.getElementById('newshound-styles');
126
+ if (!styleEl || !styleEl.sheet) return null;
127
+ var rules = styleEl.sheet.cssRules;
128
+ for (var i = 0; i < rules.length; i++) {
129
+ if (rules[i].selectorText === 'html body') return rules[i];
130
+ }
131
+ return null;
132
+ }
133
+
94
134
  window.newshoundUpdatePadding = function() {
95
- // Wait for transition to complete
96
135
  setTimeout(function() {
97
136
  var banner = document.getElementById('newshound-banner');
98
- if (banner) {
99
- var height = banner.offsetHeight;
100
- document.body.style.paddingTop = height + 'px';
101
- }
137
+ if (!banner) return;
138
+
139
+ if (cachedBodyRule === undefined) cachedBodyRule = findBodyRule();
140
+ if (!cachedBodyRule) return;
141
+
142
+ if (cachedPriority === undefined) cachedPriority = detectPriority();
143
+
144
+ cachedBodyRule.style.setProperty('padding-top', banner.offsetHeight + 'px', cachedPriority);
102
145
  }, 300);
103
146
  };
104
147
 
105
- // Set initial padding after page load
106
148
  if (document.readyState === 'loading') {
107
149
  document.addEventListener('DOMContentLoaded', window.newshoundUpdatePadding);
108
150
  } else {
109
151
  window.newshoundUpdatePadding();
110
152
  }
111
153
 
112
- // Also update on window resize
113
154
  window.addEventListener('resize', window.newshoundUpdatePadding);
114
155
  })();
115
156
  </script>
@@ -118,8 +159,8 @@ module Newshound
118
159
 
119
160
  def render_styles
120
161
  <<~CSS
121
- <style>
122
- body {
162
+ <style id="newshound-styles">
163
+ html body {
123
164
  padding-top: 50px;
124
165
  transition: padding-top 0.3s ease-out;
125
166
  }
@@ -157,13 +198,13 @@ module Newshound
157
198
  font-weight: 600;
158
199
  background: rgba(255,255,255,0.2);
159
200
  }
160
- .newshound-badge.error {
201
+ .newshound-error {
161
202
  background: #ef4444;
162
203
  }
163
- .newshound-badge.warning {
204
+ .newshound-warning {
164
205
  background: #f59e0b;
165
206
  }
166
- .newshound-badge.success {
207
+ .newshound-success {
167
208
  background: #10b981;
168
209
  }
169
210
  .newshound-toggle {
@@ -245,16 +286,16 @@ module Newshound
245
286
  warning_count = warning_data[:warnings]&.length || 0
246
287
 
247
288
  if exception_count > 0 || failed_jobs > 10
248
- badge_class = "error"
289
+ badge_class = "newshound-error"
249
290
  text = "#{exception_count} exceptions, #{failed_jobs} failed jobs"
250
291
  elsif warning_count > 0 || failed_jobs > 5
251
- badge_class = "warning"
292
+ badge_class = "newshound-warning"
252
293
  parts = []
253
294
  parts << "#{warning_count} warnings" if warning_count > 0
254
295
  parts << "#{failed_jobs} failed jobs" if failed_jobs > 5
255
296
  text = parts.join(", ")
256
297
  else
257
- badge_class = "success"
298
+ badge_class = "newshound-success"
258
299
  text = "All clear"
259
300
  end
260
301
 
@@ -27,23 +27,23 @@ module Newshound
27
27
  desc "Test exception reporter"
28
28
  task test_exceptions: :environment do
29
29
  reporter = Newshound::ExceptionReporter.new
30
- data = reporter.report
31
30
  puts "Exception Report:"
32
- puts " Total exceptions: #{data[:exceptions]&.length || 0}"
33
- data[:exceptions]&.each_with_index do |ex, i|
34
- puts " #{i + 1}. #{ex[:title]} - #{ex[:message]}"
31
+ puts " #{reporter.formatted_exception_count}"
32
+ reporter.exception_summary.each do |line|
33
+ puts " #{line}"
35
34
  end
36
35
  end
37
36
 
38
37
  desc "Test job reporter"
39
38
  task test_jobs: :environment do
40
- reporter = Newshound::QueReporter.new
41
- data = reporter.report
39
+ reporter = Newshound::JobReporter.new
40
+ data = reporter.banner_data
41
+ stats = data[:queue_stats]
42
42
  puts "Job Queue Report:"
43
- puts " Ready to run: #{data.dig(:queue_stats, :ready_to_run)}"
44
- puts " Scheduled: #{data.dig(:queue_stats, :scheduled)}"
45
- puts " Failed: #{data.dig(:queue_stats, :failed)}"
46
- puts " Completed today: #{data.dig(:queue_stats, :completed_today)}"
43
+ puts " Ready to run: #{stats[:ready_to_run] || 0}"
44
+ puts " Scheduled: #{stats[:scheduled] || 0}"
45
+ puts " Failed: #{stats[:failed] || 0}"
46
+ puts " Completed today: #{stats[:completed_today] || 0}"
47
47
  end
48
48
  end
49
49
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Newshound
4
- VERSION = "0.2.7"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/newshound.rb CHANGED
@@ -6,7 +6,8 @@ require_relative "newshound/exceptions"
6
6
  require_relative "newshound/exception_reporter"
7
7
  require_relative "newshound/warnings"
8
8
  require_relative "newshound/warning_reporter"
9
- require_relative "newshound/que_reporter"
9
+ require_relative "newshound/jobs"
10
+ require_relative "newshound/job_reporter"
10
11
  require_relative "newshound/authorization"
11
12
  require_relative "newshound/middleware/banner_injector"
12
13
  require_relative "newshound/railtie" if defined?(Rails)
data/newshound.gemspec CHANGED
@@ -8,11 +8,11 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Savannah Moore"]
9
9
  spec.email = ["savannah.albanez@sofwarellc.com"]
10
10
 
11
- spec.summary = "Real-time web UI banner for monitoring Que jobs and exception tracking"
11
+ spec.summary = "Real-time web UI banner for monitoring background jobs and exception tracking"
12
12
  spec.description = "Newshound displays exceptions and job statuses in a collapsible banner for authorized users in your Rails app"
13
13
  spec.homepage = "https://github.com/SOFware/newshound"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = ">= 3.1.0"
15
+ spec.required_ruby_version = ">= 3.3.0"
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = spec.homepage
@@ -27,7 +27,12 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ["lib"]
29
29
 
30
+ spec.post_install_message = <<~MSG
31
+ To complete installation, run:
32
+
33
+ rails generate newshound:install
34
+ MSG
35
+
30
36
  # Runtime dependencies
31
37
  spec.add_dependency "rails", ">= 6.0"
32
- spec.add_dependency "que", ">= 1.0"
33
38
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: newshound
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Savannah Moore
@@ -23,20 +23,6 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '6.0'
26
- - !ruby/object:Gem::Dependency
27
- name: que
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: '1.0'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: '1.0'
40
26
  description: Newshound displays exceptions and job statuses in a collapsible banner
41
27
  for authorized users in your Rails app
42
28
  email:
@@ -65,8 +51,11 @@ files:
65
51
  - lib/newshound/exceptions/base.rb
66
52
  - lib/newshound/exceptions/exception_track.rb
67
53
  - lib/newshound/exceptions/solid_errors.rb
54
+ - lib/newshound/job_reporter.rb
55
+ - lib/newshound/jobs.rb
56
+ - lib/newshound/jobs/base.rb
57
+ - lib/newshound/jobs/que.rb
68
58
  - lib/newshound/middleware/banner_injector.rb
69
- - lib/newshound/que_reporter.rb
70
59
  - lib/newshound/railtie.rb
71
60
  - lib/newshound/version.rb
72
61
  - lib/newshound/warning_reporter.rb
@@ -80,6 +69,10 @@ metadata:
80
69
  homepage_uri: https://github.com/SOFware/newshound
81
70
  source_code_uri: https://github.com/SOFware/newshound
82
71
  changelog_uri: https://github.com/SOFware/newshound/blob/main/CHANGELOG.md
72
+ post_install_message: |
73
+ To complete installation, run:
74
+
75
+ rails generate newshound:install
83
76
  rdoc_options: []
84
77
  require_paths:
85
78
  - lib
@@ -87,7 +80,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
87
80
  requirements:
88
81
  - - ">="
89
82
  - !ruby/object:Gem::Version
90
- version: 3.1.0
83
+ version: 3.3.0
91
84
  required_rubygems_version: !ruby/object:Gem::Requirement
92
85
  requirements:
93
86
  - - ">="
@@ -96,5 +89,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
89
  requirements: []
97
90
  rubygems_version: 3.6.7
98
91
  specification_version: 4
99
- summary: Real-time web UI banner for monitoring Que jobs and exception tracking
92
+ summary: Real-time web UI banner for monitoring background jobs and exception tracking
100
93
  test_files: []
@@ -1,166 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Newshound
4
- class QueReporter
5
- attr_reader :job_source, :logger
6
-
7
- def initialize(job_source: nil, logger: nil)
8
- @job_source = job_source || (defined?(::Que::Job) ? ::Que::Job : nil)
9
- @logger = logger || (defined?(Rails) ? Rails.logger : Logger.new($stdout))
10
- end
11
-
12
- def generate_report
13
- [
14
- {
15
- type: "section",
16
- text: {
17
- type: "mrkdwn",
18
- text: "*📊 Que Jobs Status*"
19
- }
20
- },
21
- job_counts_section,
22
- queue_health_section
23
- ].compact
24
- end
25
- alias_method :report, :generate_report
26
-
27
- # Returns data formatted for the banner UI
28
- def banner_data
29
- stats = queue_statistics
30
-
31
- {
32
- queue_stats: {
33
- ready_to_run: stats[:ready],
34
- scheduled: stats[:scheduled],
35
- failed: stats[:failed],
36
- completed_today: stats[:finished_today]
37
- }
38
- }
39
- end
40
-
41
- private
42
-
43
- def job_counts_section
44
- counts = job_counts_by_type
45
-
46
- return no_jobs_section if counts.empty?
47
-
48
- {
49
- type: "section",
50
- text: {
51
- type: "mrkdwn",
52
- text: format_job_counts(counts)
53
- }
54
- }
55
- end
56
-
57
- def job_counts_by_type
58
- return {} unless job_source
59
-
60
- # Use raw SQL since Que::Job may not support ActiveRecord's .group method
61
- results = ActiveRecord::Base.connection.execute(<<~SQL)
62
- SELECT job_class, error_count, COUNT(*) as count
63
- FROM que_jobs
64
- WHERE finished_at IS NULL
65
- GROUP BY job_class, error_count
66
- ORDER BY job_class
67
- SQL
68
-
69
- results.each_with_object({}) do |row, hash|
70
- job_class = row["job_class"]
71
- error_count = row["error_count"].to_i
72
- count = row["count"].to_i
73
-
74
- hash[job_class] ||= {success: 0, failed: 0, total: 0}
75
-
76
- if error_count.zero?
77
- hash[job_class][:success] += count
78
- else
79
- hash[job_class][:failed] += count
80
- end
81
-
82
- hash[job_class][:total] += count
83
- end
84
- rescue => e
85
- logger.error "Failed to fetch job counts: #{e.message}"
86
- {}
87
- end
88
-
89
- def format_job_counts(counts)
90
- lines = ["*Job Counts by Type:*"]
91
-
92
- counts.each do |job_class, stats|
93
- status_emoji = (stats[:failed] > 0) ? "⚠️" : "✅"
94
- lines << "• #{status_emoji} *#{job_class}*: #{stats[:total]} total (#{stats[:success]} success, #{stats[:failed]} failed)"
95
- end
96
-
97
- lines.join("\n")
98
- end
99
-
100
- def queue_health_section
101
- stats = queue_statistics
102
-
103
- {
104
- type: "section",
105
- text: {
106
- type: "mrkdwn",
107
- text: format_queue_health(stats)
108
- }
109
- }
110
- end
111
-
112
- def queue_statistics
113
- return default_stats unless job_source
114
-
115
- conn = ActiveRecord::Base.connection
116
- current_time = conn.quote(Time.now)
117
- beginning_of_day = conn.quote(Date.today.to_time)
118
-
119
- {
120
- ready: count_jobs("finished_at IS NULL AND expired_at IS NULL AND run_at <= #{current_time}"),
121
- scheduled: count_jobs("finished_at IS NULL AND expired_at IS NULL AND run_at > #{current_time}"),
122
- failed: count_jobs("error_count > 0 AND finished_at IS NULL"),
123
- finished_today: count_jobs("finished_at >= #{beginning_of_day}")
124
- }
125
- rescue => e
126
- logger.error "Failed to fetch Que statistics: #{e.message}"
127
- default_stats
128
- end
129
-
130
- def count_jobs(where_clause)
131
- ActiveRecord::Base.connection.select_value(
132
- "SELECT COUNT(*) FROM que_jobs WHERE #{where_clause}"
133
- ).to_i
134
- end
135
-
136
- def default_stats
137
- {ready: 0, scheduled: 0, failed: 0, finished_today: 0}
138
- end
139
-
140
- def format_queue_health(stats)
141
- health_emoji = if stats[:failed] > 10
142
- "🔴"
143
- else
144
- (stats[:failed] > 5) ? "🟡" : "🟢"
145
- end
146
-
147
- <<~TEXT
148
- *Queue Health #{health_emoji}*
149
- • *Ready to Run:* #{stats[:ready]}
150
- • *Scheduled:* #{stats[:scheduled]}
151
- • *Failed (Retry Queue):* #{stats[:failed]}
152
- • *Completed Today:* #{stats[:finished_today]}
153
- TEXT
154
- end
155
-
156
- def no_jobs_section
157
- {
158
- type: "section",
159
- text: {
160
- type: "mrkdwn",
161
- text: "*No jobs found in the queue*"
162
- }
163
- }
164
- end
165
- end
166
- end