rails_pulse 0.2.5 → 0.2.7

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: 930db997d251ac06c0b09a03e69f8960407af2f113374e1b6a914a308c8dfe1b
4
- data.tar.gz: e12bcdf3dccdfa5bb245de515837a15c8da2f5f1229339d3c61f29d44980cd39
3
+ metadata.gz: fa27bb069bc0caa54e5fda780981de2b934e74159bcfb56022c4faa56936700f
4
+ data.tar.gz: 1e9964ae760f5ddb6608a7e759877467ffd3511a416cf21bc3c896fd4b8cb452
5
5
  SHA512:
6
- metadata.gz: b4c15fb0f71df5460885934b8da074bc2cc6f3bcd57a99da411cfd47e30df93e2f6117fe3f59b7eda85b6e98c3a41fbd096a4ce3c85b6cc601ba3f6cc0c9fca7
7
- data.tar.gz: b8c4b0f672cdb1706cfb31ca36273ad18b95dfd69b041358077908c39e9eed8f5e305cb0296cbc5311e6c76823d2d12e777ef45a95fe374100d47d41b1dcccb7
6
+ metadata.gz: 81dbeb827db18b2eb34173c8eb673611dc190ca918b4269068829c8596d6f6fe7c73b66f8275efc7cfc4332838cc46dc48d2764979bab3faaaeb9fec245ee571
7
+ data.tar.gz: d8b6535688dd1ed2ce627f868fd4f992965efc59bfffdaa4666e9b6fe32830a22f2f8ff0ee55348bbea4e87a0b94529558c393ecb8140a0c9d62e9b962c14261
@@ -1,31 +1,40 @@
1
1
  module RailsPulse
2
2
  module BreadcrumbsHelper
3
3
  def breadcrumbs
4
- # Get the engine's mount point by removing the leading slash and splitting
5
- mount_point = RailsPulse::Engine.routes.find_script_name({}).sub(/^\//, "")
4
+ # Get the engine's mount point segments
5
+ mount_segments = RailsPulse::Engine.routes.find_script_name({}).split("/").reject(&:empty?)
6
6
 
7
7
  # Split the full path and remove empty segments
8
8
  path_segments = request.path.split("/").reject(&:empty?)
9
9
 
10
- # Find the index of the mount point in the path segments
11
- mount_point_index = path_segments.index(mount_point)
10
+ # Find where the mount point segments end in the request path
11
+ mount_end_index = nil
12
+ (0..path_segments.length - mount_segments.length).each do |i|
13
+ if path_segments[i, mount_segments.length] == mount_segments
14
+ mount_end_index = i + mount_segments.length - 1
15
+ break
16
+ end
17
+ end
12
18
 
13
19
  # If we can't find the mount point or it's the last segment, return empty
14
- return [] if mount_point_index.nil? || mount_point_index == path_segments.length - 1
20
+ return [] if mount_end_index.nil? || mount_end_index == path_segments.length - 1
15
21
 
16
22
  # Only keep segments after the mount point
17
- path_segments = path_segments[(mount_point_index + 1)..-1]
23
+ path_segments = path_segments[(mount_end_index + 1)..-1]
24
+
25
+ # Build the engine root path directly from mount segments (avoids relying on named route helper)
26
+ engine_root = "/" + mount_segments.join("/")
18
27
 
19
28
  # Start with the Home link
20
29
  crumbs = [ {
21
30
  title: "Home",
22
- path: main_app.rails_pulse_path,
31
+ path: engine_root,
23
32
  current: path_segments.empty?
24
33
  } ]
25
34
 
26
35
  return crumbs if path_segments.empty?
27
36
 
28
- current_path = main_app.rails_pulse_path.chomp("/")
37
+ current_path = engine_root
29
38
 
30
39
  path_segments.each_with_index do |segment, index|
31
40
  current_path += "/#{segment}"
@@ -58,7 +67,7 @@ module RailsPulse
58
67
  path_segments[index + 1] =~ /^\d+$/
59
68
  # This is a nested collection (e.g., /jobs/5/runs/291)
60
69
  # Link to parent show page (e.g., /jobs/5)
61
- path_segments[0..index-1].inject(main_app.rails_pulse_path.chomp("/")) { |path, seg| path + "/#{seg}" }
70
+ path_segments[0..index-1].inject(engine_root) { |path, seg| path + "/#{seg}" }
62
71
  else
63
72
  current_path
64
73
  end
@@ -62,12 +62,12 @@ module RailsPulse
62
62
  return false unless File.exist?(config_path)
63
63
 
64
64
  require "yaml"
65
- db_config = YAML.load_file(config_path)
65
+ db_config = YAML.safe_load(File.read(config_path), aliases: true)
66
66
 
67
67
  # Check if any environment has a rails_pulse database configuration
68
68
  db_config.values.any? { |env| env.is_a?(Hash) && env.key?("rails_pulse") }
69
- rescue => e
70
- # If we can't read the file, assume single database
69
+ rescue Psych::SyntaxError, Psych::AliasesNotEnabled, Errno::ENOENT
70
+ # If we can't read or parse the file, assume single database
71
71
  false
72
72
  end
73
73
 
@@ -85,32 +85,24 @@ module RailsPulse
85
85
  def cleanup_queries_by_time(cutoff_time)
86
86
  return 0 unless defined?(RailsPulse::Query)
87
87
 
88
- # Only delete queries that have no associated operations
89
- query_ids_with_operations = RailsPulse::Operation.distinct.pluck(:query_id).compact
90
- count = RailsPulse::Query
88
+ # Only delete queries that have no associated operations (atomic subquery avoids race condition)
89
+ scope = RailsPulse::Query
91
90
  .where("created_at < ?", cutoff_time)
92
- .where.not(id: query_ids_with_operations)
93
- .count
94
- RailsPulse::Query
95
- .where("created_at < ?", cutoff_time)
96
- .where.not(id: query_ids_with_operations)
97
- .delete_all
91
+ .where("id NOT IN (SELECT DISTINCT query_id FROM rails_pulse_operations WHERE query_id IS NOT NULL)")
92
+ count = scope.count
93
+ scope.delete_all
98
94
  count
99
95
  end
100
96
 
101
97
  def cleanup_routes_by_time(cutoff_time)
102
98
  return 0 unless defined?(RailsPulse::Route)
103
99
 
104
- # Only delete routes that have no associated requests
105
- route_ids_with_requests = RailsPulse::Request.distinct.pluck(:route_id).compact
106
- count = RailsPulse::Route
107
- .where("created_at < ?", cutoff_time)
108
- .where.not(id: route_ids_with_requests)
109
- .count
110
- RailsPulse::Route
100
+ # Only delete routes that have no associated requests (atomic subquery avoids race condition)
101
+ scope = RailsPulse::Route
111
102
  .where("created_at < ?", cutoff_time)
112
- .where.not(id: route_ids_with_requests)
113
- .delete_all
103
+ .where("id NOT IN (SELECT DISTINCT route_id FROM rails_pulse_requests WHERE route_id IS NOT NULL)")
104
+ count = scope.count
105
+ scope.delete_all
114
106
  count
115
107
  end
116
108
 
@@ -126,12 +118,12 @@ module RailsPulse
126
118
  def cleanup_jobs_by_time(cutoff_time)
127
119
  return 0 unless defined?(RailsPulse::Job)
128
120
 
129
- job_ids_with_runs = RailsPulse::JobRun.distinct.pluck(:job_id).compact
130
- jobs = RailsPulse::Job
121
+ # Only delete jobs that have no associated runs (atomic subquery avoids race condition)
122
+ scope = RailsPulse::Job
131
123
  .where("created_at < ?", cutoff_time)
132
- .where.not(id: job_ids_with_runs)
133
- count = jobs.count
134
- jobs.delete_all
124
+ .where("id NOT IN (SELECT DISTINCT job_id FROM rails_pulse_job_runs WHERE job_id IS NOT NULL)")
125
+ count = scope.count
126
+ scope.delete_all
135
127
  count
136
128
  end
137
129
 
@@ -183,9 +175,10 @@ module RailsPulse
183
175
  max_records = @config.max_table_records[:rails_pulse_queries]
184
176
  return 0 unless max_records
185
177
 
186
- # Only consider queries that have no associated operations
187
- query_ids_with_operations = RailsPulse::Operation.distinct.pluck(:query_id).compact
188
- available_queries = RailsPulse::Query.where.not(id: query_ids_with_operations)
178
+ # Only consider queries that have no associated operations (atomic subquery avoids race condition)
179
+ available_queries = RailsPulse::Query.where(
180
+ "id NOT IN (SELECT DISTINCT query_id FROM rails_pulse_operations WHERE query_id IS NOT NULL)"
181
+ )
189
182
  current_count = available_queries.count
190
183
  return 0 if current_count <= max_records
191
184
 
@@ -205,9 +198,10 @@ module RailsPulse
205
198
  max_records = @config.max_table_records[:rails_pulse_routes]
206
199
  return 0 unless max_records
207
200
 
208
- # Only consider routes that have no associated requests
209
- route_ids_with_requests = RailsPulse::Request.distinct.pluck(:route_id).compact
210
- available_routes = RailsPulse::Route.where.not(id: route_ids_with_requests)
201
+ # Only consider routes that have no associated requests (atomic subquery avoids race condition)
202
+ available_routes = RailsPulse::Route.where(
203
+ "id NOT IN (SELECT DISTINCT route_id FROM rails_pulse_requests WHERE route_id IS NOT NULL)"
204
+ )
211
205
  current_count = available_routes.count
212
206
  return 0 if current_count <= max_records
213
207
 
@@ -246,8 +240,10 @@ module RailsPulse
246
240
  max_records = @config.max_table_records[:rails_pulse_jobs]
247
241
  return 0 unless max_records
248
242
 
249
- job_ids_with_runs = RailsPulse::JobRun.distinct.pluck(:job_id).compact
250
- available_jobs = RailsPulse::Job.where.not(id: job_ids_with_runs)
243
+ # Only consider jobs that have no associated runs (atomic subquery avoids race condition)
244
+ available_jobs = RailsPulse::Job.where(
245
+ "id NOT IN (SELECT DISTINCT job_id FROM rails_pulse_job_runs WHERE job_id IS NOT NULL)"
246
+ )
251
247
  current_count = available_jobs.count
252
248
  return 0 if current_count <= max_records
253
249
 
@@ -1,3 +1,3 @@
1
1
  module RailsPulse
2
- VERSION = "0.2.5"
2
+ VERSION = "0.2.7"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_pulse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Pulse
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-14 00:00:00.000000000 Z
11
+ date: 2026-04-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -373,11 +373,8 @@ files:
373
373
  - lib/tasks/rails_pulse_tasks.rake
374
374
  - public/rails-pulse-assets/csp-test.js
375
375
  - public/rails-pulse-assets/rails-pulse-icons.js
376
- - public/rails-pulse-assets/rails-pulse-icons.js.map
377
376
  - public/rails-pulse-assets/rails-pulse.css
378
- - public/rails-pulse-assets/rails-pulse.css.map
379
377
  - public/rails-pulse-assets/rails-pulse.js
380
- - public/rails-pulse-assets/rails-pulse.js.map
381
378
  - public/rails-pulse-assets/search.svg
382
379
  - vendor/VENDORING.md
383
380
  - vendor/assets/javascripts/rails-pulse-icons.js
@@ -1,13 +0,0 @@
1
- {
2
- "version": 3,
3
- "file": "rails-pulse-icons.js",
4
- "sourceRoot": "",
5
- "sources": [
6
- "rails-pulse-icons.js"
7
- ],
8
- "names": [],
9
- "mappings": "",
10
- "sourcesContent": [
11
- "// Rails Pulse Icons Bundle - Auto-generated\n// Contains 35 SVG icons for Rails Pulse\n\n(function() {\n 'use strict';\n\n // Icon data\n const icons = {\n \"menu\": \"<path d=\\\"M4 5h16\\\" /><path d=\\\"M4 12h16\\\" /><path d=\\\"M4 19h16\\\" />\",\n \"sun\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"4\\\" /><path d=\\\"M12 2v2\\\" /><path d=\\\"M12 20v2\\\" /><path d=\\\"m4.93 4.93 1.41 1.41\\\" /><path d=\\\"m17.66 17.66 1.41 1.41\\\" /><path d=\\\"M2 12h2\\\" /><path d=\\\"M20 12h2\\\" /><path d=\\\"m6.34 17.66-1.41 1.41\\\" /><path d=\\\"m19.07 4.93-1.41 1.41\\\" />\",\n \"moon\": \"<path d=\\\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\\\" />\",\n \"chevron-right\": \"<path d=\\\"m9 18 6-6-6-6\\\" />\",\n \"chevron-left\": \"<path d=\\\"m15 18-6-6 6-6\\\" />\",\n \"chevron-down\": \"<path d=\\\"m6 9 6 6 6-6\\\" />\",\n \"chevron-up\": \"<path d=\\\"m18 15-6-6-6 6\\\" />\",\n \"chevrons-left\": \"<path d=\\\"m11 17-5-5 5-5\\\" /><path d=\\\"m18 17-5-5 5-5\\\" />\",\n \"chevrons-right\": \"<path d=\\\"m6 17 5-5-5-5\\\" /><path d=\\\"m13 17 5-5-5-5\\\" />\",\n \"loader-circle\": \"<path d=\\\"M12 2v4\\\" /><path d=\\\"m16.2 7.8 2.9-2.9\\\" /><path d=\\\"M18 12h4\\\" /><path d=\\\"m16.2 16.2 2.9 2.9\\\" /><path d=\\\"M12 18v4\\\" /><path d=\\\"m4.9 19.1 2.9-2.9\\\" /><path d=\\\"M2 12h4\\\" /><path d=\\\"m4.9 4.9 2.9 2.9\\\" />\",\n \"search\": \"<path d=\\\"m21 21-4.34-4.34\\\" /><circle cx=\\\"11\\\" cy=\\\"11\\\" r=\\\"8\\\" />\",\n \"list-filter\": \"<path d=\\\"M2 5h20\\\" /><path d=\\\"M6 12h12\\\" /><path d=\\\"M9 19h6\\\" />\",\n \"list-filter-plus\": \"<path d=\\\"M12 5H2\\\" /><path d=\\\"M6 12h12\\\" /><path d=\\\"M9 19h6\\\" /><path d=\\\"M16 5h6\\\" /><path d=\\\"M19 8V2\\\" />\",\n \"x\": \"<path d=\\\"M18 6 6 18\\\" /><path d=\\\"m6 6 12 12\\\" />\",\n \"x-circle\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><path d=\\\"m15 9-6 6\\\" /><path d=\\\"m9 9 6 6\\\" />\",\n \"check\": \"<path d=\\\"M20 6 9 17l-5-5\\\" />\",\n \"alert-circle\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><line x1=\\\"12\\\" x2=\\\"12\\\" y1=\\\"8\\\" y2=\\\"12\\\" /><line x1=\\\"12\\\" x2=\\\"12.01\\\" y1=\\\"16\\\" y2=\\\"16\\\" />\",\n \"alert-triangle\": \"<path d=\\\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\\\" /><path d=\\\"M12 9v4\\\" /><path d=\\\"M12 17h.01\\\" />\",\n \"info\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><path d=\\\"M12 16v-4\\\" /><path d=\\\"M12 8h.01\\\" />\",\n \"external-link\": \"<path d=\\\"M15 3h6v6\\\" /><path d=\\\"M10 14 21 3\\\" /><path d=\\\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\\\" />\",\n \"download\": \"<path d=\\\"M12 15V3\\\" /><path d=\\\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\\\" /><path d=\\\"m7 10 5 5 5-5\\\" />\",\n \"refresh-cw\": \"<path d=\\\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\\\" /><path d=\\\"M21 3v5h-5\\\" /><path d=\\\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\\\" /><path d=\\\"M8 16H3v5\\\" />\",\n \"clock\": \"<path d=\\\"M12 6v6l4 2\\\" /><circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" />\",\n \"database\": \"<ellipse cx=\\\"12\\\" cy=\\\"5\\\" rx=\\\"9\\\" ry=\\\"3\\\" /><path d=\\\"M3 5V19A9 3 0 0 0 21 19V5\\\" /><path d=\\\"M3 12A9 3 0 0 0 21 12\\\" />\",\n \"server\": \"<rect width=\\\"20\\\" height=\\\"8\\\" x=\\\"2\\\" y=\\\"2\\\" rx=\\\"2\\\" ry=\\\"2\\\" /><rect width=\\\"20\\\" height=\\\"8\\\" x=\\\"2\\\" y=\\\"14\\\" rx=\\\"2\\\" ry=\\\"2\\\" /><line x1=\\\"6\\\" x2=\\\"6.01\\\" y1=\\\"6\\\" y2=\\\"6\\\" /><line x1=\\\"6\\\" x2=\\\"6.01\\\" y1=\\\"18\\\" y2=\\\"18\\\" />\",\n \"activity\": \"<path d=\\\"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2\\\" />\",\n \"layout-dashboard\": \"<rect width=\\\"7\\\" height=\\\"9\\\" x=\\\"3\\\" y=\\\"3\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"5\\\" x=\\\"14\\\" y=\\\"3\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"9\\\" x=\\\"14\\\" y=\\\"12\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"5\\\" x=\\\"3\\\" y=\\\"16\\\" rx=\\\"1\\\" />\",\n \"audio-lines\": \"<path d=\\\"M2 10v3\\\" /><path d=\\\"M6 6v11\\\" /><path d=\\\"M10 3v18\\\" /><path d=\\\"M14 8v7\\\" /><path d=\\\"M18 5v13\\\" /><path d=\\\"M22 10v3\\\" />\",\n \"message-circle-question\": \"<path d=\\\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719\\\" /><path d=\\\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\\\" /><path d=\\\"M12 17h.01\\\" />\",\n \"route\": \"<circle cx=\\\"6\\\" cy=\\\"19\\\" r=\\\"3\\\" /><path d=\\\"M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15\\\" /><circle cx=\\\"18\\\" cy=\\\"5\\\" r=\\\"3\\\" />\",\n \"trending-up\": \"<path d=\\\"M16 7h6v6\\\" /><path d=\\\"m22 7-8.5 8.5-5-5L2 17\\\" />\",\n \"trending-down\": \"<path d=\\\"M16 17h6v-6\\\" /><path d=\\\"m22 17-8.5-8.5-5 5L2 7\\\" />\",\n \"move-right\": \"<path d=\\\"M18 8L22 12L18 16\\\" /><path d=\\\"M2 12H22\\\" />\",\n \"eye\": \"<path d=\\\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\\\" /><circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"3\\\" />\",\n \"zap\": \"<path d=\\\"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z\\\" />\"\n};\n\n // Global icon registry\n window.RailsPulseIcons = {\n icons: icons,\n\n // Get icon SVG content\n get: function(name) {\n return icons[name] || null;\n },\n\n // Check if icon exists\n has: function(name) {\n return name in icons;\n },\n\n // Get all icon names\n list: function() {\n return Object.keys(icons);\n },\n\n // Render icon to element (CSP-safe)\n render: function(name, element, options = {}) {\n const svgContent = this.get(name);\n if (!svgContent || !element) return false;\n\n // Create SVG element\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', options.width || '24');\n svg.setAttribute('height', options.height || '24');\n svg.setAttribute('viewBox', '0 0 24 24');\n svg.setAttribute('fill', 'none');\n svg.setAttribute('stroke', 'currentColor');\n svg.setAttribute('stroke-width', '2');\n svg.setAttribute('stroke-linecap', 'round');\n svg.setAttribute('stroke-linejoin', 'round');\n\n // Add icon content\n svg.innerHTML = svgContent;\n\n // Replace element content\n element.innerHTML = '';\n element.appendChild(svg);\n\n return true;\n }\n };\n})();\n"
12
- ]
13
- }