catpm 0.5.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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/controllers/catpm/endpoints_controller.rb +56 -1
- data/app/controllers/catpm/events_controller.rb +75 -9
- data/app/controllers/catpm/samples_controller.rb +11 -0
- data/app/controllers/catpm/status_controller.rb +13 -2
- data/app/models/catpm/endpoint_pref.rb +14 -0
- data/app/models/catpm/event_pref.rb +14 -0
- data/app/views/catpm/endpoints/ignored.html.erb +57 -0
- data/app/views/catpm/endpoints/show.html.erb +7 -3
- data/app/views/catpm/events/ignored.html.erb +52 -0
- data/app/views/catpm/events/index.html.erb +15 -1
- data/app/views/catpm/events/show.html.erb +13 -4
- data/app/views/catpm/samples/show.html.erb +12 -7
- data/app/views/catpm/status/index.html.erb +18 -2
- data/app/views/layouts/catpm/application.html.erb +130 -0
- data/config/routes.rb +8 -2
- data/db/migrate/20250601000001_create_catpm_tables.rb +22 -0
- data/lib/catpm/version.rb +1 -1
- data/lib/tasks/catpm_tasks.rake +28 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72b1f8ad18b14d62b8ace369a8d064396ff4fc2988f1dc4eb911c71278c05766
|
|
4
|
+
data.tar.gz: f6153a851adc6ff5dd810cf648b069b512117bd25ae87f6e7715382ea6c43960
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 485696711db8a1ed56901065ee90b151292a466c4a21bf6585e63e36a0ee6f873a14584b229f1963bd9fa14ac5143d4fbc3641763177dec0ec2fc5c5421ba11d
|
|
7
|
+
data.tar.gz: c6e0dd6391af5921e4fa79ad77a69fe7d637adf77f714cf19eb66182dbac128c7c45d75ee1ac7365620824afc34d6672d788169c230c72d8ac0af27460167eff
|
data/README.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
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 = [
|
|
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">⋮</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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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">⋮</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] ? "★".html_safe : "☆".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">⋮</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
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
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
|
· <%= format_duration(@sample.duration) %>
|
|
7
7
|
<% end %>
|
|
8
8
|
|
|
9
|
-
<div class="breadcrumbs">
|
|
10
|
-
<
|
|
11
|
-
|
|
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
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 ─── %>
|
|
@@ -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
|
|
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] ? "★".html_safe : "☆".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">⋮</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
|
+
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<title>catpm<%= " — #{yield :title}" if content_for?(:title) %></title>
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
6
7
|
<%= yield :head_extra if content_for?(:head_extra) %>
|
|
7
8
|
<style>
|
|
8
9
|
/* ─── Design Tokens — Light Theme ─── */
|
|
@@ -199,6 +200,22 @@
|
|
|
199
200
|
.btn-primary:hover { background: var(--border); }
|
|
200
201
|
.btn-danger { background: var(--bg-0); color: var(--text-2); border-color: var(--border); }
|
|
201
202
|
.btn-danger:hover { background: #ffebe9; color: var(--red); border-color: #ffcecb; }
|
|
203
|
+
|
|
204
|
+
/* ─── Star Button ─── */
|
|
205
|
+
.star-btn { background: none; border: none; cursor: pointer; font-size: 16px; color: var(--text-2); padding: 0; line-height: 1; transition: color 0.15s; }
|
|
206
|
+
.star-btn:hover { color: #e3b341; }
|
|
207
|
+
.star-btn.pinned { color: #e3b341; }
|
|
208
|
+
|
|
209
|
+
/* ─── Action Menu (3-dot) ─── */
|
|
210
|
+
.action-menu-btn { background: none; border: none; cursor: pointer; font-size: 18px; color: var(--text-2); padding: 2px 6px; line-height: 1; border-radius: 4px; transition: background 0.1s, color 0.1s; }
|
|
211
|
+
.action-menu-btn:hover, .action-menu-btn.active { color: var(--text-0); background: var(--bg-2); }
|
|
212
|
+
.action-menu { display: none; }
|
|
213
|
+
.action-menu-overlay { position: fixed; background: var(--bg-0); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.15), 0 1px 4px rgba(0,0,0,0.08); z-index: 9999; min-width: 160px; padding: 4px 0; }
|
|
214
|
+
.action-menu-overlay button { display: block; width: 100%; text-align: left; padding: 8px 14px; border: none; background: none; cursor: pointer; font-size: 13px; color: var(--text-0); transition: background 0.1s; }
|
|
215
|
+
.action-menu-overlay button:hover { background: var(--bg-1); }
|
|
216
|
+
.action-menu-overlay .menu-danger { color: var(--red); }
|
|
217
|
+
.action-menu-overlay .menu-danger:hover { background: #ffebe9; }
|
|
218
|
+
|
|
202
219
|
.copy-btn { background: var(--bg-1); border: 1px solid var(--border); color: var(--text-2); padding: 3px 8px; border-radius: 3px; font-size: 11px; cursor: pointer; }
|
|
203
220
|
.copy-btn:hover { color: var(--text-0); background: var(--bg-2); }
|
|
204
221
|
|
|
@@ -422,6 +439,119 @@
|
|
|
422
439
|
if (search) search.focus();
|
|
423
440
|
}
|
|
424
441
|
});
|
|
442
|
+
|
|
443
|
+
/* ─── AJAX Helpers ─── */
|
|
444
|
+
function csrfToken() {
|
|
445
|
+
var meta = document.querySelector('meta[name="csrf-token"]');
|
|
446
|
+
return meta ? meta.getAttribute('content') : '';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function apiRequest(url, method) {
|
|
450
|
+
return fetch(url, {
|
|
451
|
+
method: method,
|
|
452
|
+
headers: { 'X-CSRF-Token': csrfToken(), 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
|
|
453
|
+
credentials: 'same-origin'
|
|
454
|
+
}).then(function(r) { return r.json(); });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function fadeOutRow(row) {
|
|
458
|
+
row.style.transition = 'opacity 0.3s';
|
|
459
|
+
row.style.opacity = '0';
|
|
460
|
+
setTimeout(function() { row.remove(); }, 300);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
var _actionMenu = null;
|
|
464
|
+
function closeActionMenu() {
|
|
465
|
+
if (_actionMenu) {
|
|
466
|
+
_actionMenu.overlay.remove();
|
|
467
|
+
_actionMenu.btn.classList.remove('active');
|
|
468
|
+
_actionMenu = null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
document.addEventListener('click', function(e) {
|
|
473
|
+
/* Star (pin) toggle */
|
|
474
|
+
var starBtn = e.target.closest('.star-btn');
|
|
475
|
+
if (starBtn) {
|
|
476
|
+
e.stopPropagation();
|
|
477
|
+
e.preventDefault();
|
|
478
|
+
apiRequest(starBtn.dataset.pinUrl, 'PATCH').then(function(data) {
|
|
479
|
+
starBtn.classList.toggle('pinned', data.pinned);
|
|
480
|
+
starBtn.innerHTML = data.pinned ? '\u2605' : '\u2606';
|
|
481
|
+
});
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* Pin text button (endpoint page) */
|
|
486
|
+
var pinTextBtn = e.target.closest('[data-pin-text]');
|
|
487
|
+
if (pinTextBtn) {
|
|
488
|
+
e.preventDefault();
|
|
489
|
+
apiRequest(pinTextBtn.dataset.pinUrl, 'PATCH').then(function(data) {
|
|
490
|
+
pinTextBtn.textContent = data.pinned ? 'Unpin' : 'Pin';
|
|
491
|
+
pinTextBtn.classList.toggle('btn-primary', data.pinned);
|
|
492
|
+
});
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/* 3-dot menu toggle — portal overlay on body */
|
|
497
|
+
var menuBtn = e.target.closest('.action-menu-btn');
|
|
498
|
+
if (menuBtn) {
|
|
499
|
+
e.stopPropagation();
|
|
500
|
+
closeActionMenu();
|
|
501
|
+
var source = menuBtn.nextElementSibling;
|
|
502
|
+
if (!source) return;
|
|
503
|
+
var row = menuBtn.closest('tr');
|
|
504
|
+
var rect = menuBtn.getBoundingClientRect();
|
|
505
|
+
|
|
506
|
+
var overlay = document.createElement('div');
|
|
507
|
+
overlay.className = 'action-menu-overlay';
|
|
508
|
+
source.querySelectorAll('button').forEach(function(src) {
|
|
509
|
+
var item = document.createElement('button');
|
|
510
|
+
item.textContent = src.textContent;
|
|
511
|
+
if (src.classList.contains('menu-danger')) item.className = 'menu-danger';
|
|
512
|
+
item.dataset.action = src.dataset.action;
|
|
513
|
+
item.dataset.url = src.dataset.url;
|
|
514
|
+
overlay.appendChild(item);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
document.body.appendChild(overlay);
|
|
518
|
+
menuBtn.classList.add('active');
|
|
519
|
+
_actionMenu = { overlay: overlay, row: row, btn: menuBtn };
|
|
520
|
+
|
|
521
|
+
overlay.style.right = (window.innerWidth - rect.right) + 'px';
|
|
522
|
+
overlay.style.top = (rect.bottom + 4) + 'px';
|
|
523
|
+
requestAnimationFrame(function() {
|
|
524
|
+
var mr = overlay.getBoundingClientRect();
|
|
525
|
+
if (mr.bottom > window.innerHeight - 8) {
|
|
526
|
+
overlay.style.top = (rect.top - mr.height - 4) + 'px';
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* Menu overlay action clicks */
|
|
533
|
+
var overlayBtn = e.target.closest('.action-menu-overlay button');
|
|
534
|
+
if (overlayBtn) {
|
|
535
|
+
e.stopPropagation();
|
|
536
|
+
var action = overlayBtn.dataset.action;
|
|
537
|
+
var url = overlayBtn.dataset.url;
|
|
538
|
+
var row = _actionMenu ? _actionMenu.row : null;
|
|
539
|
+
closeActionMenu();
|
|
540
|
+
|
|
541
|
+
if (action === 'ignore') {
|
|
542
|
+
apiRequest(url, 'PATCH').then(function(data) { if (data.ignored && row) fadeOutRow(row); });
|
|
543
|
+
} else if (action === 'delete') {
|
|
544
|
+
if (!confirm('Delete this endpoint and all its data?')) return;
|
|
545
|
+
apiRequest(url, 'DELETE').then(function(data) { if (data.deleted && row) fadeOutRow(row); });
|
|
546
|
+
} else if (action === 'unignore') {
|
|
547
|
+
apiRequest(url, 'PATCH').then(function(data) { if (!data.ignored && row) fadeOutRow(row); });
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/* Close menu on outside click */
|
|
553
|
+
closeActionMenu();
|
|
554
|
+
});
|
|
425
555
|
</script>
|
|
426
556
|
</body>
|
|
427
557
|
</html>
|
data/config/routes.rb
CHANGED
|
@@ -6,8 +6,14 @@ Catpm::Engine.routes.draw do
|
|
|
6
6
|
resources :system, only: [:index]
|
|
7
7
|
get 'endpoint', to: 'endpoints#show', as: :endpoint
|
|
8
8
|
delete 'endpoint', to: 'endpoints#destroy'
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
patch 'endpoint/pin', to: 'endpoints#toggle_pin', as: :endpoint_pin
|
|
10
|
+
patch 'endpoint/ignore', to: 'endpoints#toggle_ignore', as: :endpoint_ignore
|
|
11
|
+
get 'endpoints/ignored', to: 'endpoints#ignored', as: :ignored_endpoints
|
|
12
|
+
resources :samples, only: [:show, :destroy]
|
|
13
|
+
resources :events, only: [:index, :show, :destroy], param: :name
|
|
14
|
+
patch 'events/:name/pin', to: 'events#toggle_pin', as: :event_pin
|
|
15
|
+
patch 'events/:name/ignore', to: 'events#toggle_ignore', as: :event_ignore
|
|
16
|
+
get 'events_ignored', to: 'events#ignored', as: :ignored_events
|
|
11
17
|
resources :errors, only: [:index, :show, :destroy] do
|
|
12
18
|
collection do
|
|
13
19
|
post :resolve_all
|
|
@@ -74,6 +74,26 @@ class CreateCatpmTables < ActiveRecord::Migration[8.0]
|
|
|
74
74
|
add_index :catpm_event_samples, [:name, :recorded_at], name: 'idx_catpm_event_samples_name_time'
|
|
75
75
|
add_index :catpm_event_samples, :recorded_at, name: 'idx_catpm_event_samples_time'
|
|
76
76
|
|
|
77
|
+
create_table :catpm_endpoint_prefs do |t|
|
|
78
|
+
t.string :kind, null: false
|
|
79
|
+
t.string :target, null: false
|
|
80
|
+
t.string :operation, null: false, default: ''
|
|
81
|
+
t.boolean :pinned, null: false, default: false
|
|
82
|
+
t.boolean :ignored, null: false, default: false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
add_index :catpm_endpoint_prefs, [:kind, :target, :operation],
|
|
86
|
+
unique: true, name: 'idx_catpm_endpoint_prefs_unique'
|
|
87
|
+
|
|
88
|
+
create_table :catpm_event_prefs do |t|
|
|
89
|
+
t.string :name, null: false
|
|
90
|
+
t.boolean :pinned, null: false, default: false
|
|
91
|
+
t.boolean :ignored, null: false, default: false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
add_index :catpm_event_prefs, :name,
|
|
95
|
+
unique: true, name: 'idx_catpm_event_prefs_unique'
|
|
96
|
+
|
|
77
97
|
if postgresql?
|
|
78
98
|
execute <<~SQL
|
|
79
99
|
CREATE OR REPLACE FUNCTION catpm_merge_jsonb_sums(a jsonb, b jsonb)
|
|
@@ -92,6 +112,8 @@ class CreateCatpmTables < ActiveRecord::Migration[8.0]
|
|
|
92
112
|
execute 'DROP FUNCTION IF EXISTS catpm_merge_jsonb_sums(jsonb, jsonb);'
|
|
93
113
|
end
|
|
94
114
|
|
|
115
|
+
drop_table :catpm_event_prefs, if_exists: true
|
|
116
|
+
drop_table :catpm_endpoint_prefs, if_exists: true
|
|
95
117
|
drop_table :catpm_event_samples, if_exists: true
|
|
96
118
|
drop_table :catpm_event_buckets, if_exists: true
|
|
97
119
|
drop_table :catpm_errors, if_exists: true
|
data/lib/catpm/version.rb
CHANGED
data/lib/tasks/catpm_tasks.rake
CHANGED
|
@@ -19,5 +19,33 @@ namespace :catpm do
|
|
|
19
19
|
else
|
|
20
20
|
puts '[catpm] catpm_errors.occurrence_buckets already exists, skipping'
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
unless connection.table_exists?(:catpm_endpoint_prefs)
|
|
24
|
+
connection.create_table :catpm_endpoint_prefs do |t|
|
|
25
|
+
t.string :kind, null: false
|
|
26
|
+
t.string :target, null: false
|
|
27
|
+
t.string :operation, null: false, default: ''
|
|
28
|
+
t.boolean :pinned, null: false, default: false
|
|
29
|
+
t.boolean :ignored, null: false, default: false
|
|
30
|
+
end
|
|
31
|
+
connection.add_index :catpm_endpoint_prefs, [:kind, :target, :operation],
|
|
32
|
+
unique: true, name: 'idx_catpm_endpoint_prefs_unique'
|
|
33
|
+
puts '[catpm] Created catpm_endpoint_prefs table'
|
|
34
|
+
else
|
|
35
|
+
puts '[catpm] catpm_endpoint_prefs table already exists, skipping'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
unless connection.table_exists?(:catpm_event_prefs)
|
|
39
|
+
connection.create_table :catpm_event_prefs do |t|
|
|
40
|
+
t.string :name, null: false
|
|
41
|
+
t.boolean :pinned, null: false, default: false
|
|
42
|
+
t.boolean :ignored, null: false, default: false
|
|
43
|
+
end
|
|
44
|
+
connection.add_index :catpm_event_prefs, :name,
|
|
45
|
+
unique: true, name: 'idx_catpm_event_prefs_unique'
|
|
46
|
+
puts '[catpm] Created catpm_event_prefs table'
|
|
47
|
+
else
|
|
48
|
+
puts '[catpm] catpm_event_prefs table already exists, skipping'
|
|
49
|
+
end
|
|
22
50
|
end
|
|
23
51
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: catpm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ''
|
|
@@ -46,14 +46,18 @@ files:
|
|
|
46
46
|
- app/mailers/catpm/application_mailer.rb
|
|
47
47
|
- app/models/catpm/application_record.rb
|
|
48
48
|
- app/models/catpm/bucket.rb
|
|
49
|
+
- app/models/catpm/endpoint_pref.rb
|
|
49
50
|
- app/models/catpm/error_record.rb
|
|
50
51
|
- app/models/catpm/event_bucket.rb
|
|
52
|
+
- app/models/catpm/event_pref.rb
|
|
51
53
|
- app/models/catpm/event_sample.rb
|
|
52
54
|
- app/models/catpm/sample.rb
|
|
53
55
|
- app/views/catpm/endpoints/_sample_table.html.erb
|
|
56
|
+
- app/views/catpm/endpoints/ignored.html.erb
|
|
54
57
|
- app/views/catpm/endpoints/show.html.erb
|
|
55
58
|
- app/views/catpm/errors/index.html.erb
|
|
56
59
|
- app/views/catpm/errors/show.html.erb
|
|
60
|
+
- app/views/catpm/events/ignored.html.erb
|
|
57
61
|
- app/views/catpm/events/index.html.erb
|
|
58
62
|
- app/views/catpm/events/show.html.erb
|
|
59
63
|
- app/views/catpm/samples/show.html.erb
|