catpm 0.4.0 → 0.6.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/controllers/catpm/endpoints_controller.rb +56 -1
  4. data/app/controllers/catpm/events_controller.rb +75 -9
  5. data/app/controllers/catpm/samples_controller.rb +11 -0
  6. data/app/controllers/catpm/status_controller.rb +13 -2
  7. data/app/models/catpm/endpoint_pref.rb +14 -0
  8. data/app/models/catpm/event_pref.rb +14 -0
  9. data/app/views/catpm/endpoints/ignored.html.erb +57 -0
  10. data/app/views/catpm/endpoints/show.html.erb +7 -3
  11. data/app/views/catpm/events/ignored.html.erb +52 -0
  12. data/app/views/catpm/events/index.html.erb +15 -1
  13. data/app/views/catpm/events/show.html.erb +13 -4
  14. data/app/views/catpm/samples/show.html.erb +12 -7
  15. data/app/views/catpm/shared/_segments_waterfall.html.erb +5 -0
  16. data/app/views/catpm/status/index.html.erb +18 -2
  17. data/app/views/layouts/catpm/application.html.erb +130 -0
  18. data/config/routes.rb +8 -2
  19. data/db/migrate/20250601000001_create_catpm_tables.rb +22 -0
  20. data/lib/catpm/adapter/base.rb +8 -4
  21. data/lib/catpm/call_tracer.rb +85 -0
  22. data/lib/catpm/collector.rb +76 -19
  23. data/lib/catpm/configuration.rb +22 -1
  24. data/lib/catpm/fingerprint.rb +2 -2
  25. data/lib/catpm/flusher.rb +45 -32
  26. data/lib/catpm/middleware.rb +7 -0
  27. data/lib/catpm/request_segments.rb +2 -2
  28. data/lib/catpm/segment_subscribers.rb +4 -2
  29. data/lib/catpm/stack_sampler.rb +5 -3
  30. data/lib/catpm/trace.rb +9 -1
  31. data/lib/catpm/version.rb +1 -1
  32. data/lib/catpm.rb +1 -0
  33. data/lib/generators/catpm/templates/initializer.rb.tt +69 -57
  34. data/lib/tasks/catpm_tasks.rake +28 -0
  35. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30a794032ccb0fcb32152d5ae6c7285758260c6a80c5241c217924a887040885
4
- data.tar.gz: 7691f6480aa62e4efd41d657fda9e1501a2765584a3d926a5d9ac3a8b6ac0c98
3
+ metadata.gz: 72b1f8ad18b14d62b8ace369a8d064396ff4fc2988f1dc4eb911c71278c05766
4
+ data.tar.gz: f6153a851adc6ff5dd810cf648b069b512117bd25ae87f6e7715382ea6c43960
5
5
  SHA512:
6
- metadata.gz: c902a837f12312f1f856d232b056eb3669c8648f5907e5088ced55972888d64682749f937a994d79b040688d1f59d929dccd11a1d28ffdcfe4e25f2458c02f49
7
- data.tar.gz: 73dd36a0cb7ea32b405388bb62f54e9f327f30155b3b30794101feb056abe70b7b747cd5dda3dc67e6669e4289392964b37ae76385e6cb680c96e5a5bb550d55
6
+ metadata.gz: 485696711db8a1ed56901065ee90b151292a466c4a21bf6585e63e36a0ee6f873a14584b229f1963bd9fa14ac5143d4fbc3641763177dec0ec2fc5c5421ba11d
7
+ data.tar.gz: c6e0dd6391af5921e4fa79ad77a69fe7d637adf77f714cf19eb66182dbac128c7c45d75ee1ac7365620824afc34d6672d788169c230c72d8ac0af27460167eff
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  gem build catpm.gemspec
2
- gem push catpm-0.1.2.gem
2
+ gem push catpm-0.5.0.gem
3
3
 
4
4
  # Catpm
5
5
 
@@ -85,6 +85,7 @@ module Catpm
85
85
  @samples = endpoint_samples.where(sample_type: 'random').order(recorded_at: :desc).limit(10)
86
86
  @error_samples = endpoint_samples.where(sample_type: 'error').order(recorded_at: :desc).limit(10)
87
87
 
88
+ @pref = Catpm::EndpointPref.find_by(kind: @kind, target: @target, operation: @operation)
88
89
  @active_error_count = Catpm::ErrorRecord.unresolved.count
89
90
  end
90
91
 
@@ -94,7 +95,61 @@ module Catpm
94
95
  operation = params[:operation].presence || ''
95
96
 
96
97
  Catpm::Bucket.where(kind: kind, target: target, operation: operation).destroy_all
97
- redirect_to catpm.status_index_path, notice: 'Endpoint deleted'
98
+ Catpm::EndpointPref.find_by(kind: kind, target: target, operation: operation)&.destroy
99
+ if request.xhr?
100
+ render json: { deleted: true }
101
+ else
102
+ redirect_to catpm.status_index_path, notice: 'Endpoint deleted'
103
+ end
104
+ end
105
+
106
+ def toggle_pin
107
+ pref = Catpm::EndpointPref.lookup(params[:kind], params[:target], params[:operation])
108
+ pref.pinned = !pref.pinned
109
+ pref.save!
110
+ if request.xhr?
111
+ render json: { pinned: pref.pinned }
112
+ else
113
+ redirect_back fallback_location: catpm.endpoint_path(kind: params[:kind], target: params[:target], operation: params[:operation])
114
+ end
115
+ end
116
+
117
+ def toggle_ignore
118
+ pref = Catpm::EndpointPref.lookup(params[:kind], params[:target], params[:operation])
119
+ pref.ignored = !pref.ignored
120
+ pref.save!
121
+ if request.xhr?
122
+ render json: { ignored: pref.ignored }
123
+ else
124
+ redirect_back fallback_location: catpm.status_index_path
125
+ end
126
+ end
127
+
128
+ def ignored
129
+ @range, period, _bucket_seconds = helpers.parse_range(remembered_range)
130
+ ignored_prefs = Catpm::EndpointPref.ignored
131
+
132
+ scope = @range == 'all' ? Catpm::Bucket.all : Catpm::Bucket.recent(period)
133
+ grouped = scope.group_by { |b| [b.kind, b.target, b.operation] }
134
+
135
+ ignored_keys = ignored_prefs.map { |p| [p.kind, p.target, p.operation] }.to_set
136
+
137
+ @ignored_endpoints = ignored_prefs.map do |pref|
138
+ key = [pref.kind, pref.target, pref.operation]
139
+ bs = grouped[key]
140
+ total_count = bs ? bs.sum(&:count) : 0
141
+ {
142
+ kind: pref.kind,
143
+ target: pref.target,
144
+ operation: pref.operation,
145
+ total_count: total_count,
146
+ avg_duration: total_count > 0 ? bs.sum(&:duration_sum) / total_count : 0.0,
147
+ last_seen: bs&.map(&:bucket_start)&.max
148
+ }
149
+ end
150
+
151
+ @active_endpoint_count = grouped.keys.count { |k| !ignored_keys.include?(k) }
152
+ @active_error_count = Catpm::ErrorRecord.unresolved.count
98
153
  end
99
154
  end
100
155
  end
@@ -30,40 +30,48 @@ module Catpm
30
30
  grouped = recent_buckets.group_by(&:name)
31
31
  @unique_names = grouped.keys.size
32
32
 
33
+ # Load event preferences
34
+ prefs = Catpm::EventPref.where('pinned = ? OR ignored = ?', true, true).index_by(&:name)
35
+
36
+ now_slot = (Time.current.to_i / bucket_seconds) * bucket_seconds
37
+ @sparkline_times = 60.times.map { |i| Time.at(now_slot - (59 - i) * bucket_seconds).strftime('%H:%M') }
38
+
33
39
  events_list = grouped.map do |name, bs|
34
40
  total_count = bs.sum(&:count)
35
41
 
36
- # Sparkline data for this name
37
42
  slots = {}
38
43
  bs.each do |b|
39
44
  slot_key = (b.bucket_start.to_i / bucket_seconds) * bucket_seconds
40
45
  slots[slot_key] = (slots[slot_key] || 0) + b.count
41
46
  end
42
- now_slot = (Time.current.to_i / bucket_seconds) * bucket_seconds
43
47
  sparkline = 60.times.map { |i| slots[now_slot - (59 - i) * bucket_seconds] || 0 }
44
48
 
49
+ pref = prefs[name]
45
50
  {
46
51
  name: name,
47
52
  total_count: total_count,
48
53
  sparkline: sparkline,
49
- last_seen: bs.map(&:bucket_start).max
54
+ last_seen: bs.map(&:bucket_start).max,
55
+ pinned: pref&.pinned || false,
56
+ ignored: pref&.ignored || false
50
57
  }
51
58
  end
52
59
 
53
- # Sort
60
+ # Separate ignored events
61
+ @ignored_events = events_list.select { |e| e[:ignored] }
62
+ events_list = events_list.reject { |e| e[:ignored] }
63
+
64
+ # Sort (pinned always on top)
54
65
  @sort = %w[name total_count last_seen].include?(params[:sort]) ? params[:sort] : 'total_count'
55
66
  @dir = params[:dir] == 'asc' ? 'asc' : 'desc'
56
67
  events_list = events_list.sort_by { |e| e[@sort.to_sym] || '' }
57
68
  events_list = events_list.reverse if @dir == 'desc'
69
+ events_list = events_list.sort_by { |e| e[:pinned] ? 0 : 1 }
58
70
 
59
71
  @total_event_names = events_list.size
60
72
 
61
- # Sparkline times for tooltips
62
- now_slot = (Time.current.to_i / bucket_seconds) * bucket_seconds
63
- @sparkline_times = 60.times.map { |i| Time.at(now_slot - (59 - i) * bucket_seconds).strftime('%H:%M') }
64
-
65
73
  # Pagination
66
- @page = [ params[:page].to_i, 1 ].max
74
+ @page = [params[:page].to_i, 1].max
67
75
  @events = events_list.drop((@page - 1) * PER_PAGE).first(PER_PAGE)
68
76
 
69
77
  @active_error_count = Catpm::ErrorRecord.unresolved.count
@@ -107,6 +115,64 @@ module Catpm
107
115
  # Recent samples
108
116
  @samples = Catpm::EventSample.by_name(@name).order(recorded_at: :desc).limit(Catpm.config.events_max_samples_per_name)
109
117
 
118
+ @pref = Catpm::EventPref.find_by(name: @name)
119
+ @active_error_count = Catpm::ErrorRecord.unresolved.count
120
+ end
121
+
122
+ def destroy
123
+ name = params[:name]
124
+ Catpm::EventBucket.where(name: name).destroy_all
125
+ Catpm::EventSample.where(name: name).destroy_all
126
+ Catpm::EventPref.find_by(name: name)&.destroy
127
+ if request.xhr?
128
+ render json: { deleted: true }
129
+ else
130
+ redirect_to catpm.events_path, notice: 'Event deleted'
131
+ end
132
+ end
133
+
134
+ def toggle_pin
135
+ pref = Catpm::EventPref.lookup(params[:name])
136
+ pref.pinned = !pref.pinned
137
+ pref.save!
138
+ if request.xhr?
139
+ render json: { pinned: pref.pinned }
140
+ else
141
+ redirect_back fallback_location: catpm.event_path(name: params[:name])
142
+ end
143
+ end
144
+
145
+ def toggle_ignore
146
+ pref = Catpm::EventPref.lookup(params[:name])
147
+ pref.ignored = !pref.ignored
148
+ pref.save!
149
+ if request.xhr?
150
+ render json: { ignored: pref.ignored }
151
+ else
152
+ redirect_back fallback_location: catpm.events_path
153
+ end
154
+ end
155
+
156
+ def ignored
157
+ @range, period, _bucket_seconds = helpers.parse_range(remembered_range)
158
+ ignored_prefs = Catpm::EventPref.ignored
159
+
160
+ scope = @range == 'all' ? Catpm::EventBucket.all : Catpm::EventBucket.recent(period)
161
+ grouped = scope.group_by(&:name)
162
+
163
+ ignored_keys = ignored_prefs.map(&:name).to_set
164
+
165
+ @ignored_events = ignored_prefs.map do |pref|
166
+ bs = grouped[pref.name]
167
+ total_count = bs ? bs.sum(&:count) : 0
168
+ {
169
+ name: pref.name,
170
+ total_count: total_count,
171
+ last_seen: bs&.map(&:bucket_start)&.max
172
+ }
173
+ end
174
+
175
+ @active_event_count = grouped.keys.count { |k| !ignored_keys.include?(k) }
110
176
  @active_error_count = Catpm::ErrorRecord.unresolved.count
111
177
  end
112
178
  end
@@ -12,5 +12,16 @@ module Catpm
12
12
  Catpm::ErrorRecord.find_by(fingerprint: @sample.error_fingerprint)
13
13
  end
14
14
  end
15
+
16
+ def destroy
17
+ sample = Catpm::Sample.find(params[:id])
18
+ bucket = sample.bucket
19
+ sample.destroy
20
+ if bucket
21
+ redirect_to catpm.endpoint_path(kind: bucket.kind, target: bucket.target, operation: bucket.operation), notice: 'Sample deleted'
22
+ else
23
+ redirect_to catpm.status_index_path, notice: 'Sample deleted'
24
+ end
25
+ end
15
26
  end
16
27
  end
@@ -52,9 +52,13 @@ module Catpm
52
52
  # Endpoints — aggregated from the SAME time range as hero metrics
53
53
  grouped = recent_buckets.group_by { |b| [b.kind, b.target, b.operation] }
54
54
 
55
+ # Load endpoint preferences (pinned/ignored)
56
+ prefs = Catpm::EndpointPref.where('pinned = ? OR ignored = ?', true, true).index_by { |p| [p.kind, p.target, p.operation] }
57
+
55
58
  endpoints = grouped.map do |key, bs|
56
59
  kind, target, operation = key
57
60
  total_count = bs.sum(&:count)
61
+ pref = prefs[key]
58
62
  {
59
63
  kind: kind,
60
64
  target: target,
@@ -63,20 +67,27 @@ module Catpm
63
67
  avg_duration: total_count > 0 ? bs.sum(&:duration_sum) / total_count : 0.0,
64
68
  max_duration: bs.map(&:duration_max).max,
65
69
  total_failures: bs.sum(&:failure_count),
66
- last_seen: bs.map(&:bucket_start).max
70
+ last_seen: bs.map(&:bucket_start).max,
71
+ pinned: pref&.pinned || false,
72
+ ignored: pref&.ignored || false
67
73
  }
68
74
  end
69
75
 
76
+ # Separate ignored endpoints
77
+ @ignored_endpoints = endpoints.select { |e| e[:ignored] }
78
+ endpoints = endpoints.reject { |e| e[:ignored] }
79
+
70
80
  # Kind filter (URL-based)
71
81
  @available_kinds = endpoints.map { |e| e[:kind] }.uniq.sort
72
82
  @kind_filter = params[:kind] if params[:kind].present? && @available_kinds.include?(params[:kind])
73
83
  endpoints = endpoints.select { |e| e[:kind] == @kind_filter } if @kind_filter
74
84
 
75
- # Server-side sort
85
+ # Server-side sort (pinned always on top)
76
86
  @sort = %w[target total_count avg_duration max_duration total_failures last_seen].include?(params[:sort]) ? params[:sort] : 'last_seen'
77
87
  @dir = params[:dir] == 'asc' ? 'asc' : 'desc'
78
88
  endpoints = endpoints.sort_by { |e| e[@sort.to_sym] || '' }
79
89
  endpoints = endpoints.reverse if @dir == 'desc'
90
+ endpoints = endpoints.sort_by { |e| e[:pinned] ? 0 : 1 }
80
91
 
81
92
  @total_endpoint_count = endpoints.size
82
93
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Catpm
4
+ class EndpointPref < ActiveRecord::Base
5
+ self.table_name = 'catpm_endpoint_prefs'
6
+
7
+ scope :pinned, -> { where(pinned: true) }
8
+ scope :ignored, -> { where(ignored: true) }
9
+
10
+ def self.lookup(kind, target, operation)
11
+ find_or_initialize_by(kind: kind, target: target, operation: operation.presence || '')
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Catpm
4
+ class EventPref < ActiveRecord::Base
5
+ self.table_name = 'catpm_event_prefs'
6
+
7
+ scope :pinned, -> { where(pinned: true) }
8
+ scope :ignored, -> { where(ignored: true) }
9
+
10
+ def self.lookup(name)
11
+ find_or_initialize_by(name: name)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,57 @@
1
+ <% content_for :title, "Ignored Endpoints" %>
2
+ <% content_for :subtitle, "Endpoints hidden from the main dashboard" %>
3
+
4
+ <%= render "catpm/shared/page_nav", active: "performance" %>
5
+
6
+ <div class="tabs">
7
+ <a href="<%= catpm.status_index_path %>" class="tab">Active (<%= @active_endpoint_count %>)</a>
8
+ <a href="<%= catpm.ignored_endpoints_path %>" class="tab active">Ignored (<%= @ignored_endpoints.size %>)</a>
9
+ </div>
10
+
11
+ <%# ─── Time Range ─── %>
12
+ <div class="time-range">
13
+ <% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
14
+ <a href="<%= catpm.ignored_endpoints_path(range: r) %>" class="<%= 'active' if @range == r %>"><%= r == "all" ? "All" : r %></a>
15
+ <% end %>
16
+ </div>
17
+
18
+ <% if @ignored_endpoints.any? %>
19
+ <div class="table-scroll">
20
+ <table>
21
+ <thead>
22
+ <tr>
23
+ <th>Kind</th>
24
+ <th>Target</th>
25
+ <th>Count</th>
26
+ <th>Avg</th>
27
+ <th>Last Seen</th>
28
+ <th></th>
29
+ </tr>
30
+ </thead>
31
+ <tbody>
32
+ <% @ignored_endpoints.each do |ep| %>
33
+ <% ep_p = { kind: ep[:kind], target: ep[:target], operation: ep[:operation] } %>
34
+ <tr class="linked">
35
+ <td><a href="<%= catpm.endpoint_path(ep_p) %>" class="row-link"><%= type_badge(ep[:kind]) %></a></td>
36
+ <td class="mono"><%= ep[:target] %><%= " #{ep[:operation]}" if ep[:operation].present? %></td>
37
+ <td><%= ep[:total_count] %></td>
38
+ <td class="mono"><%= format_duration(ep[:avg_duration]) %></td>
39
+ <td><%= ep[:last_seen] ? time_with_tooltip(ep[:last_seen]) : "—" %></td>
40
+ <td style="position:relative; z-index:2; width:28px; padding:4px 2px; text-align:center">
41
+ <button class="action-menu-btn">&#x22EE;</button>
42
+ <div class="action-menu">
43
+ <button data-action="unignore" data-url="<%= catpm.endpoint_ignore_path(ep_p) %>">Unignore</button>
44
+ <button class="menu-danger" data-action="delete" data-url="<%= catpm.endpoint_path(ep_p) %>">Delete</button>
45
+ </div>
46
+ </td>
47
+ </tr>
48
+ <% end %>
49
+ </tbody>
50
+ </table>
51
+ </div>
52
+ <% else %>
53
+ <div class="empty-state">
54
+ <div class="empty-title">No ignored endpoints</div>
55
+ <div class="empty-hint">Endpoints you ignore will appear here.</div>
56
+ </div>
57
+ <% end %>
@@ -14,9 +14,13 @@
14
14
  <span class="sep">/</span>
15
15
  <span><%= @target %></span>
16
16
  </div>
17
- <%= button_to "Delete Endpoint", catpm.endpoint_path(kind: @kind, target: @target, operation: @operation),
18
- method: :delete, class: "btn btn-danger",
19
- data: { confirm: "Delete this endpoint and all its data? This cannot be undone." } %>
17
+ <div style="display:flex; gap:6px; align-items:center">
18
+ <%= button_to @pref&.ignored ? "Unignore" : "Ignore", catpm.endpoint_ignore_path(kind: @kind, target: @target, operation: @operation),
19
+ method: :patch, class: "btn" %>
20
+ <%= button_to "Delete Endpoint", catpm.endpoint_path(kind: @kind, target: @target, operation: @operation),
21
+ method: :delete, class: "btn btn-danger",
22
+ data: { confirm: "Delete this endpoint and all its data? This cannot be undone." } %>
23
+ </div>
20
24
  </div>
21
25
 
22
26
  <% ep_params = { kind: @kind, target: @target, operation: @operation } %>
@@ -0,0 +1,52 @@
1
+ <% content_for :title, "Ignored Events" %>
2
+ <% content_for :subtitle, "Events hidden from the main dashboard" %>
3
+
4
+ <%= render "catpm/shared/page_nav", active: "events" %>
5
+
6
+ <div class="tabs">
7
+ <a href="<%= catpm.events_path %>" class="tab">Active (<%= @active_event_count %>)</a>
8
+ <a href="<%= catpm.ignored_events_path %>" class="tab active">Ignored (<%= @ignored_events.size %>)</a>
9
+ </div>
10
+
11
+ <%# ─── Time Range ─── %>
12
+ <div class="time-range">
13
+ <% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
14
+ <a href="<%= catpm.ignored_events_path(range: r) %>" class="<%= 'active' if @range == r %>"><%= r == "all" ? "All" : r %></a>
15
+ <% end %>
16
+ </div>
17
+
18
+ <% if @ignored_events.any? %>
19
+ <div class="table-scroll">
20
+ <table>
21
+ <thead>
22
+ <tr>
23
+ <th>Name</th>
24
+ <th>Count</th>
25
+ <th>Last Seen</th>
26
+ <th></th>
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ <% @ignored_events.each do |ev| %>
31
+ <tr class="linked">
32
+ <td><a href="<%= catpm.event_path(name: ev[:name]) %>" class="row-link"><span class="badge badge-event"><%= ev[:name] %></span></a></td>
33
+ <td><%= ev[:total_count] %></td>
34
+ <td><%= ev[:last_seen] ? time_with_tooltip(ev[:last_seen]) : "—" %></td>
35
+ <td style="position:relative; z-index:2; width:28px; padding:4px 2px; text-align:center">
36
+ <button class="action-menu-btn">&#x22EE;</button>
37
+ <div class="action-menu">
38
+ <button data-action="unignore" data-url="<%= catpm.event_ignore_path(name: ev[:name]) %>">Unignore</button>
39
+ <button class="menu-danger" data-action="delete" data-url="<%= catpm.event_path(name: ev[:name]) %>">Delete</button>
40
+ </div>
41
+ </td>
42
+ </tr>
43
+ <% end %>
44
+ </tbody>
45
+ </table>
46
+ </div>
47
+ <% else %>
48
+ <div class="empty-state">
49
+ <div class="empty-title">No ignored events</div>
50
+ <div class="empty-hint">Events you ignore will appear here.</div>
51
+ </div>
52
+ <% end %>
@@ -3,6 +3,11 @@
3
3
 
4
4
  <%= render "catpm/shared/page_nav", active: "events" %>
5
5
 
6
+ <div class="tabs">
7
+ <a href="<%= catpm.events_path(range: @range) %>" class="tab active">Active (<%= @total_event_names %>)</a>
8
+ <a href="<%= catpm.ignored_events_path %>" class="tab">Ignored (<%= @ignored_events.size %>)</a>
9
+ </div>
10
+
6
11
  <%# ─── Time Range ─── %>
7
12
  <div class="time-range">
8
13
  <% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
@@ -34,22 +39,24 @@
34
39
 
35
40
  <%# ─── Events Table ─── %>
36
41
  <h2>Events <% if @total_event_names > @events.size %><span class="text-muted" style="font-weight:400; font-size:13px">(showing <%= @events.size %> of <%= @total_event_names %>)</span><% end %></h2>
37
- <%= section_description("Events tracked in the selected time range.") %>
38
42
 
39
43
  <% if @events.any? %>
40
44
  <div class="table-scroll">
41
45
  <table>
42
46
  <thead>
43
47
  <tr>
48
+ <th></th>
44
49
  <th><%= sort_header("Name", "name", @sort, @dir, extra_params: { range: @range }) %></th>
45
50
  <th><%= sort_header("Count", "total_count", @sort, @dir, extra_params: { range: @range }) %></th>
46
51
  <th>Trend</th>
47
52
  <th><%= sort_header("Last Seen", "last_seen", @sort, @dir, extra_params: { range: @range }) %></th>
53
+ <th></th>
48
54
  </tr>
49
55
  </thead>
50
56
  <tbody>
51
57
  <% @events.each do |ev| %>
52
58
  <tr class="linked">
59
+ <td style="position:relative; z-index:1; width:28px; padding:4px 2px; text-align:center"><button class="star-btn<%= ' pinned' if ev[:pinned] %>" data-pin-url="<%= catpm.event_pin_path(name: ev[:name]) %>"><%= ev[:pinned] ? "&#x2605;".html_safe : "&#x2606;".html_safe %></button></td>
53
60
  <td>
54
61
  <a href="<%= catpm.event_path(name: ev[:name], range: @range) %>" class="row-link">
55
62
  <span class="badge badge-event"><%= ev[:name] %></span>
@@ -58,6 +65,13 @@
58
65
  <td><%= ev[:total_count] %></td>
59
66
  <td><%= sparkline_svg(ev[:sparkline], width: 120, height: 32, color: "var(--accent)", time_labels: @sparkline_times) %></td>
60
67
  <td><%= time_with_tooltip(ev[:last_seen]) %></td>
68
+ <td style="position:relative; z-index:2; width:28px; padding:4px 2px; text-align:center">
69
+ <button class="action-menu-btn">&#x22EE;</button>
70
+ <div class="action-menu">
71
+ <button data-action="ignore" data-url="<%= catpm.event_ignore_path(name: ev[:name]) %>">Ignore</button>
72
+ <button class="menu-danger" data-action="delete" data-url="<%= catpm.event_path(name: ev[:name]) %>">Delete</button>
73
+ </div>
74
+ </td>
61
75
  </tr>
62
76
  <% end %>
63
77
  </tbody>
@@ -3,10 +3,19 @@
3
3
 
4
4
  <%= render "catpm/shared/page_nav", active: "events" %>
5
5
 
6
- <div class="breadcrumbs">
7
- <a href="<%= catpm.events_path(range: @range) %>">Events</a>
8
- <span class="sep">/</span>
9
- <span class="badge badge-event"><%= @name %></span>
6
+ <div class="breadcrumbs" style="display:flex; align-items:center; justify-content:space-between">
7
+ <div>
8
+ <a href="<%= catpm.events_path(range: @range) %>">Events</a>
9
+ <span class="sep">/</span>
10
+ <span class="badge badge-event"><%= @name %></span>
11
+ </div>
12
+ <div style="display:flex; gap:6px; align-items:center">
13
+ <%= button_to @pref&.ignored ? "Unignore" : "Ignore", catpm.event_ignore_path(name: @name),
14
+ method: :patch, class: "btn" %>
15
+ <%= button_to "Delete Event", catpm.event_path(name: @name),
16
+ method: :delete, class: "btn btn-danger",
17
+ data: { confirm: "Delete this event and all its data? This cannot be undone." } %>
18
+ </div>
10
19
  </div>
11
20
 
12
21
  <%# ─── Time Range ─── %>
@@ -6,14 +6,19 @@
6
6
  &middot; <%= format_duration(@sample.duration) %>
7
7
  <% end %>
8
8
 
9
- <div class="breadcrumbs">
10
- <a href="<%= catpm.status_index_path %>">Overview</a>
11
- <% if @bucket %>
9
+ <div class="breadcrumbs" style="display:flex; align-items:center; justify-content:space-between">
10
+ <div>
11
+ <a href="<%= catpm.status_index_path %>">Overview</a>
12
+ <% if @bucket %>
13
+ <span class="sep">/</span>
14
+ <a href="<%= catpm.endpoint_path(kind: @bucket.kind, target: @bucket.target, operation: @bucket.operation) %>"><%= @bucket.target %></a>
15
+ <% end %>
12
16
  <span class="sep">/</span>
13
- <a href="<%= catpm.endpoint_path(kind: @bucket.kind, target: @bucket.target, operation: @bucket.operation) %>"><%= @bucket.target %></a>
14
- <% end %>
15
- <span class="sep">/</span>
16
- <span>Sample #<%= @sample.id %></span>
17
+ <span>Sample #<%= @sample.id %></span>
18
+ </div>
19
+ <%= button_to "Delete Sample", catpm.sample_path(@sample),
20
+ method: :delete, class: "btn btn-danger",
21
+ data: { confirm: "Delete this sample? This cannot be undone." } %>
17
22
  </div>
18
23
 
19
24
  <%# ─── Request Info Bar ─── %>
@@ -17,6 +17,11 @@
17
17
  end
18
18
  end
19
19
 
20
+ # Sort children of each parent by timeline offset so segments appear chronologically
21
+ children.each_value do |kids|
22
+ kids.sort_by! { |i| (segments[i]["offset"] || segments[i][:offset] || 0).to_f }
23
+ end
24
+
20
25
  depth_map = {}
21
26
  ordered = []
22
27
  build_order = ->(indices, depth) {
@@ -3,6 +3,11 @@
3
3
 
4
4
  <%= render "catpm/shared/page_nav", active: "performance" %>
5
5
 
6
+ <div class="tabs">
7
+ <a href="<%= catpm.status_index_path(range: @range) %>" class="tab active">Active (<%= @total_endpoint_count %>)</a>
8
+ <a href="<%= catpm.ignored_endpoints_path %>" class="tab">Ignored (<%= @ignored_endpoints.size %>)</a>
9
+ </div>
10
+
6
11
  <%# ─── Time Range ─── %>
7
12
  <div class="time-range">
8
13
  <% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
@@ -70,7 +75,6 @@
70
75
 
71
76
  <%# ─── Endpoints ─── %>
72
77
  <h2>Endpoints <% if @total_endpoint_count > @endpoint_count %><span class="text-muted" style="font-weight:400; font-size:13px">(showing <%= @endpoint_count %> of <%= @total_endpoint_count %>)</span><% end %></h2>
73
- <%= section_description("Endpoints active in the selected time range.") %>
74
78
  <% ep_extra = { range: @range }; ep_extra[:kind] = @kind_filter if @kind_filter %>
75
79
  <% if @endpoints.any? || @kind_filter.present? %>
76
80
  <div class="filters">
@@ -84,6 +88,7 @@
84
88
  <table id="endpoints-table">
85
89
  <thead>
86
90
  <tr>
91
+ <th></th>
87
92
  <th>Kind</th>
88
93
  <th><%= sort_header("Target", "target", @sort, @dir, extra_params: ep_extra) %></th>
89
94
  <th><%= sort_header("Count", "total_count", @sort, @dir, extra_params: ep_extra) %></th>
@@ -91,18 +96,28 @@
91
96
  <th><%= sort_header("Max", "max_duration", @sort, @dir, extra_params: ep_extra) %></th>
92
97
  <th><%= sort_header("Fail", "total_failures", @sort, @dir, extra_params: ep_extra) %></th>
93
98
  <th><%= sort_header("Last Seen", "last_seen", @sort, @dir, extra_params: ep_extra) %></th>
99
+ <th></th>
94
100
  </tr>
95
101
  </thead>
96
102
  <tbody>
97
103
  <% @endpoints.each do |ep| %>
104
+ <% ep_p = { kind: ep[:kind], target: ep[:target], operation: ep[:operation] } %>
98
105
  <tr class="linked">
99
- <td><a href="<%= catpm.endpoint_path(kind: ep[:kind], target: ep[:target], operation: ep[:operation]) %>" class="row-link"><%= type_badge(ep[:kind]) %></a></td>
106
+ <td style="position:relative; z-index:1; width:28px; padding:4px 2px; text-align:center"><button class="star-btn<%= ' pinned' if ep[:pinned] %>" data-pin-url="<%= catpm.endpoint_pin_path(ep_p) %>"><%= ep[:pinned] ? "&#x2605;".html_safe : "&#x2606;".html_safe %></button></td>
107
+ <td><a href="<%= catpm.endpoint_path(ep_p) %>" class="row-link"><%= type_badge(ep[:kind]) %></a></td>
100
108
  <td class="mono"><%= ep[:target] %><%= " #{ep[:operation]}" if ep[:operation].present? %></td>
101
109
  <td><%= ep[:total_count] %></td>
102
110
  <td class="mono"><%= format_duration(ep[:avg_duration]) %></td>
103
111
  <td class="mono"><%= format_duration(ep[:max_duration]) %></td>
104
112
  <td><%= ep[:total_failures] %></td>
105
113
  <td><%= time_with_tooltip(ep[:last_seen]) %></td>
114
+ <td style="position:relative; z-index:2; width:28px; padding:4px 2px; text-align:center">
115
+ <button class="action-menu-btn">&#x22EE;</button>
116
+ <div class="action-menu">
117
+ <button data-action="ignore" data-url="<%= catpm.endpoint_ignore_path(ep_p) %>">Ignore</button>
118
+ <button class="menu-danger" data-action="delete" data-url="<%= catpm.endpoint_path(ep_p) %>">Delete</button>
119
+ </div>
120
+ </td>
106
121
  </tr>
107
122
  <% end %>
108
123
  </tbody>
@@ -122,3 +137,4 @@
122
137
  </div>
123
138
  <% end %>
124
139
 
140
+