catpm 0.5.0 → 0.6.1
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/assets/images/catpm/favicon-32.png +0 -0
- data/app/assets/images/catpm/favicon-512.png +0 -0
- data/app/assets/images/catpm/favicon.ico +0 -0
- data/app/controllers/catpm/endpoints_controller.rb +56 -1
- data/app/controllers/catpm/errors_controller.rb +26 -4
- data/app/controllers/catpm/events_controller.rb +88 -11
- data/app/controllers/catpm/samples_controller.rb +13 -0
- data/app/controllers/catpm/status_controller.rb +16 -4
- data/app/models/catpm/endpoint_pref.rb +14 -0
- data/app/models/catpm/error_record.rb +1 -0
- data/app/models/catpm/event_pref.rb +14 -0
- data/app/views/catpm/endpoints/_sample_table.html.erb +7 -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/errors/index.html.erb +14 -0
- data/app/views/catpm/errors/show.html.erb +9 -2
- 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 +21 -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 +138 -0
- data/config/routes.rb +13 -2
- data/db/migrate/20250601000001_create_catpm_tables.rb +23 -0
- data/lib/catpm/middleware.rb +1 -0
- data/lib/catpm/version.rb +1 -1
- data/lib/tasks/catpm_tasks.rake +35 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 77d3e9a9e80a64c61ba1cbc9f400f9b53cb774c2eee0cf1fc807952bf1b81c39
|
|
4
|
+
data.tar.gz: 3364e29476e9a6f77494f86c0fb00932c70a84eb90067df86e8455a00608930b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52c5cb2542feabdb2a493390e1fd0c42bef5e5ea8c8d89bb71115f774b790e88b8c8f253402e47835da8190a4113d3ed7302398f59a2ce2841df2576018a20cb
|
|
7
|
+
data.tar.gz: be688f7c4ce207e04d09fb1ac30b390bc7f2ea8b050120bc46929a952a632d17e63ffc110950907ee37e60aa62f8f7400a3d982acfc568dd2392d01fe9841c91
|
data/README.md
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
@@ -28,7 +28,7 @@ module Catpm
|
|
|
28
28
|
|
|
29
29
|
@total_count = scope.count
|
|
30
30
|
@page = [params[:page].to_i, 1].max
|
|
31
|
-
@errors = scope.order(@sort => @dir).offset((@page - 1) * PER_PAGE).limit(PER_PAGE)
|
|
31
|
+
@errors = scope.order(pinned: :desc, @sort => @dir).offset((@page - 1) * PER_PAGE).limit(PER_PAGE)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def show
|
|
@@ -106,19 +106,41 @@ module Catpm
|
|
|
106
106
|
def resolve
|
|
107
107
|
error = Catpm::ErrorRecord.find(params[:id])
|
|
108
108
|
error.resolve!
|
|
109
|
-
|
|
109
|
+
if request.xhr?
|
|
110
|
+
render json: { resolved: true }
|
|
111
|
+
else
|
|
112
|
+
redirect_to catpm.error_path(error), notice: 'Marked as resolved'
|
|
113
|
+
end
|
|
110
114
|
end
|
|
111
115
|
|
|
112
116
|
def unresolve
|
|
113
117
|
error = Catpm::ErrorRecord.find(params[:id])
|
|
114
118
|
error.unresolve!
|
|
115
|
-
|
|
119
|
+
if request.xhr?
|
|
120
|
+
render json: { resolved: false }
|
|
121
|
+
else
|
|
122
|
+
redirect_to catpm.error_path(error), notice: 'Reopened'
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def toggle_pin
|
|
127
|
+
error = Catpm::ErrorRecord.find(params[:id])
|
|
128
|
+
error.update!(pinned: !error.pinned)
|
|
129
|
+
if request.xhr?
|
|
130
|
+
render json: { pinned: error.pinned }
|
|
131
|
+
else
|
|
132
|
+
redirect_back fallback_location: catpm.error_path(error)
|
|
133
|
+
end
|
|
116
134
|
end
|
|
117
135
|
|
|
118
136
|
def destroy
|
|
119
137
|
error = Catpm::ErrorRecord.find(params[:id])
|
|
120
138
|
error.destroy!
|
|
121
|
-
|
|
139
|
+
if request.xhr?
|
|
140
|
+
render json: { deleted: true }
|
|
141
|
+
else
|
|
142
|
+
redirect_to catpm.errors_path, notice: 'Error deleted'
|
|
143
|
+
end
|
|
122
144
|
end
|
|
123
145
|
|
|
124
146
|
def resolve_all
|
|
@@ -30,40 +30,49 @@ 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
|
-
|
|
57
|
-
|
|
67
|
+
sorted = events_list.sort_by { |e| e[@sort.to_sym] || '' }
|
|
68
|
+
sorted = sorted.reverse if @dir == 'desc'
|
|
69
|
+
pinned, unpinned = sorted.partition { |e| e[:pinned] }
|
|
70
|
+
events_list = pinned + unpinned
|
|
58
71
|
|
|
59
72
|
@total_event_names = events_list.size
|
|
60
73
|
|
|
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
74
|
# Pagination
|
|
66
|
-
@page = [
|
|
75
|
+
@page = [params[:page].to_i, 1].max
|
|
67
76
|
@events = events_list.drop((@page - 1) * PER_PAGE).first(PER_PAGE)
|
|
68
77
|
|
|
69
78
|
@active_error_count = Catpm::ErrorRecord.unresolved.count
|
|
@@ -107,6 +116,74 @@ module Catpm
|
|
|
107
116
|
# Recent samples
|
|
108
117
|
@samples = Catpm::EventSample.by_name(@name).order(recorded_at: :desc).limit(Catpm.config.events_max_samples_per_name)
|
|
109
118
|
|
|
119
|
+
@pref = Catpm::EventPref.find_by(name: @name)
|
|
120
|
+
@active_error_count = Catpm::ErrorRecord.unresolved.count
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def destroy
|
|
124
|
+
name = params[:name]
|
|
125
|
+
Catpm::EventBucket.where(name: name).destroy_all
|
|
126
|
+
Catpm::EventSample.where(name: name).destroy_all
|
|
127
|
+
Catpm::EventPref.find_by(name: name)&.destroy
|
|
128
|
+
if request.xhr?
|
|
129
|
+
render json: { deleted: true }
|
|
130
|
+
else
|
|
131
|
+
redirect_to catpm.events_path, notice: 'Event deleted'
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def toggle_pin
|
|
136
|
+
pref = Catpm::EventPref.lookup(params[:name])
|
|
137
|
+
pref.pinned = !pref.pinned
|
|
138
|
+
pref.save!
|
|
139
|
+
if request.xhr?
|
|
140
|
+
render json: { pinned: pref.pinned }
|
|
141
|
+
else
|
|
142
|
+
redirect_back fallback_location: catpm.event_path(name: params[:name])
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def toggle_ignore
|
|
147
|
+
pref = Catpm::EventPref.lookup(params[:name])
|
|
148
|
+
pref.ignored = !pref.ignored
|
|
149
|
+
pref.save!
|
|
150
|
+
if request.xhr?
|
|
151
|
+
render json: { ignored: pref.ignored }
|
|
152
|
+
else
|
|
153
|
+
redirect_back fallback_location: catpm.events_path
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def destroy_sample
|
|
158
|
+
sample = Catpm::EventSample.find(params[:sample_id])
|
|
159
|
+
sample.destroy
|
|
160
|
+
if request.xhr?
|
|
161
|
+
render json: { deleted: true }
|
|
162
|
+
else
|
|
163
|
+
redirect_back fallback_location: catpm.events_path, notice: 'Sample deleted'
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def ignored
|
|
168
|
+
@range, period, _bucket_seconds = helpers.parse_range(remembered_range)
|
|
169
|
+
ignored_prefs = Catpm::EventPref.ignored
|
|
170
|
+
|
|
171
|
+
scope = @range == 'all' ? Catpm::EventBucket.all : Catpm::EventBucket.recent(period)
|
|
172
|
+
grouped = scope.group_by(&:name)
|
|
173
|
+
|
|
174
|
+
ignored_keys = ignored_prefs.map(&:name).to_set
|
|
175
|
+
|
|
176
|
+
@ignored_events = ignored_prefs.map do |pref|
|
|
177
|
+
bs = grouped[pref.name]
|
|
178
|
+
total_count = bs ? bs.sum(&:count) : 0
|
|
179
|
+
{
|
|
180
|
+
name: pref.name,
|
|
181
|
+
total_count: total_count,
|
|
182
|
+
last_seen: bs&.map(&:bucket_start)&.max
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
@active_event_count = grouped.keys.count { |k| !ignored_keys.include?(k) }
|
|
110
187
|
@active_error_count = Catpm::ErrorRecord.unresolved.count
|
|
111
188
|
end
|
|
112
189
|
end
|
|
@@ -12,5 +12,18 @@ 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 request.xhr?
|
|
21
|
+
render json: { deleted: true }
|
|
22
|
+
elsif bucket
|
|
23
|
+
redirect_to catpm.endpoint_path(kind: bucket.kind, target: bucket.target, operation: bucket.operation), notice: 'Sample deleted'
|
|
24
|
+
else
|
|
25
|
+
redirect_to catpm.status_index_path, notice: 'Sample deleted'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
15
28
|
end
|
|
16
29
|
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,28 @@ 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
|
-
|
|
79
|
-
|
|
88
|
+
sorted = endpoints.sort_by { |e| e[@sort.to_sym] || '' }
|
|
89
|
+
sorted = sorted.reverse if @dir == 'desc'
|
|
90
|
+
pinned, unpinned = sorted.partition { |e| e[:pinned] }
|
|
91
|
+
endpoints = pinned + unpinned
|
|
80
92
|
|
|
81
93
|
@total_endpoint_count = endpoints.size
|
|
82
94
|
|
|
@@ -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
|
|
@@ -10,6 +10,7 @@ module Catpm
|
|
|
10
10
|
scope :by_kind, ->(kind) { where(kind: kind) }
|
|
11
11
|
scope :unresolved, -> { where(resolved_at: nil) }
|
|
12
12
|
scope :resolved, -> { where.not(resolved_at: nil) }
|
|
13
|
+
scope :pinned, -> { where(pinned: true) }
|
|
13
14
|
scope :recent, ->(period = 24.hours) { where(last_occurred_at: period.ago..) }
|
|
14
15
|
|
|
15
16
|
def resolved?
|
|
@@ -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
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<th>Duration</th>
|
|
8
8
|
<th>Segments</th>
|
|
9
9
|
<th>Time</th>
|
|
10
|
+
<th></th>
|
|
10
11
|
</tr>
|
|
11
12
|
</thead>
|
|
12
13
|
<tbody>
|
|
@@ -24,6 +25,12 @@
|
|
|
24
25
|
<% end %>
|
|
25
26
|
</td>
|
|
26
27
|
<td><%= time_with_tooltip(s.recorded_at) %></td>
|
|
28
|
+
<td style="position:relative; z-index:2; width:28px; padding:4px 2px; text-align:center">
|
|
29
|
+
<button class="action-menu-btn">⋮</button>
|
|
30
|
+
<div class="action-menu">
|
|
31
|
+
<button class="menu-danger" data-action="delete" data-url="<%= catpm.sample_path(s) %>">Delete</button>
|
|
32
|
+
</div>
|
|
33
|
+
</td>
|
|
27
34
|
</tr>
|
|
28
35
|
<% end %>
|
|
29
36
|
</tbody>
|
|
@@ -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 } %>
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
<table id="errors-table">
|
|
28
28
|
<thead>
|
|
29
29
|
<tr>
|
|
30
|
+
<th></th>
|
|
30
31
|
<th>Kind</th>
|
|
31
32
|
<th><%= sort_header("Error Class", "error_class", @sort, @dir, extra_params: extra) %></th>
|
|
32
33
|
<th>Message</th>
|
|
@@ -34,11 +35,13 @@
|
|
|
34
35
|
<th>Trend</th>
|
|
35
36
|
<th><%= sort_header("Last Seen", "last_occurred_at", @sort, @dir, extra_params: extra) %></th>
|
|
36
37
|
<% if @tab == "resolved" %><th>Resolved</th><% end %>
|
|
38
|
+
<th></th>
|
|
37
39
|
</tr>
|
|
38
40
|
</thead>
|
|
39
41
|
<tbody>
|
|
40
42
|
<% @errors.each do |e| %>
|
|
41
43
|
<tr class="linked">
|
|
44
|
+
<td style="position:relative; z-index:1; width:28px; padding:4px 2px; text-align:center"><button class="star-btn<%= ' pinned' if e.pinned %>" data-pin-url="<%= catpm.toggle_pin_error_path(e) %>"><%= e.pinned ? "★".html_safe : "☆".html_safe %></button></td>
|
|
42
45
|
<td><a href="<%= catpm.error_path(e) %>" class="row-link"><%= type_badge(e.kind) %></a></td>
|
|
43
46
|
<td class="mono"><%= e.error_class %></td>
|
|
44
47
|
<td style="color:var(--text-1)"><%= truncate(e.message, length: 60) %></td>
|
|
@@ -46,6 +49,17 @@
|
|
|
46
49
|
<td><%= trend_indicator(e) %></td>
|
|
47
50
|
<td><%= time_with_tooltip(e.last_occurred_at) %></td>
|
|
48
51
|
<% if @tab == "resolved" %><td><%= time_with_tooltip(e.resolved_at) %></td><% end %>
|
|
52
|
+
<td style="position:relative; z-index:2; width:28px; padding:4px 2px; text-align:center">
|
|
53
|
+
<button class="action-menu-btn">⋮</button>
|
|
54
|
+
<div class="action-menu">
|
|
55
|
+
<% if e.resolved? %>
|
|
56
|
+
<button data-action="unresolve" data-url="<%= catpm.unresolve_error_path(e) %>">Reopen</button>
|
|
57
|
+
<% else %>
|
|
58
|
+
<button data-action="resolve" data-url="<%= catpm.resolve_error_path(e) %>">Resolve</button>
|
|
59
|
+
<% end %>
|
|
60
|
+
<button class="menu-danger" data-action="delete" data-url="<%= catpm.error_path(e) %>">Delete</button>
|
|
61
|
+
</div>
|
|
62
|
+
</td>
|
|
49
63
|
</tr>
|
|
50
64
|
<% end %>
|
|
51
65
|
</tbody>
|
|
@@ -106,17 +106,24 @@
|
|
|
106
106
|
<th>Status</th>
|
|
107
107
|
<th>Target</th>
|
|
108
108
|
<th>Segments</th>
|
|
109
|
+
<th></th>
|
|
109
110
|
</tr>
|
|
110
111
|
</thead>
|
|
111
112
|
<tbody>
|
|
112
113
|
<% @samples.each do |sample| %>
|
|
113
114
|
<% ctx = sample.parsed_context %>
|
|
114
|
-
<tr class="
|
|
115
|
-
<td><%= time_with_tooltip(sample.recorded_at) %></td>
|
|
115
|
+
<tr class="linked">
|
|
116
|
+
<td><a href="<%= catpm.sample_path(sample) %>" class="row-link"><%= time_with_tooltip(sample.recorded_at) %></a></td>
|
|
116
117
|
<td class="mono"><%= format_duration(sample.duration) %></td>
|
|
117
118
|
<td><%= status_badge(ctx["status"] || ctx[:status]) %></td>
|
|
118
119
|
<td class="mono"><%= sample.bucket&.target || "—" %></td>
|
|
119
120
|
<td class="mono text-muted"><%= segment_count_summary(ctx["segment_summary"] || ctx[:segment_summary]).presence || "—" %></td>
|
|
121
|
+
<td style="position:relative; z-index:2; width:28px; padding:4px 2px; text-align:center">
|
|
122
|
+
<button class="action-menu-btn">⋮</button>
|
|
123
|
+
<div class="action-menu">
|
|
124
|
+
<button class="menu-danger" data-action="delete" data-url="<%= catpm.sample_path(sample) %>">Delete</button>
|
|
125
|
+
</div>
|
|
126
|
+
</td>
|
|
120
127
|
</tr>
|
|
121
128
|
<% end %>
|
|
122
129
|
</tbody>
|
|
@@ -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 ─── %>
|
|
@@ -54,11 +63,13 @@
|
|
|
54
63
|
<colgroup>
|
|
55
64
|
<col style="width:120px">
|
|
56
65
|
<col>
|
|
66
|
+
<col style="width:36px">
|
|
57
67
|
</colgroup>
|
|
58
68
|
<thead>
|
|
59
69
|
<tr>
|
|
60
70
|
<th>Recorded At</th>
|
|
61
71
|
<th>Payload</th>
|
|
72
|
+
<th></th>
|
|
62
73
|
</tr>
|
|
63
74
|
</thead>
|
|
64
75
|
<tbody>
|
|
@@ -78,6 +89,12 @@
|
|
|
78
89
|
<span class="text-muted">—</span>
|
|
79
90
|
<% end %>
|
|
80
91
|
</td>
|
|
92
|
+
<td style="position:relative; z-index:2; width:28px; padding:4px 2px; text-align:center; vertical-align:top">
|
|
93
|
+
<button class="action-menu-btn">⋮</button>
|
|
94
|
+
<div class="action-menu">
|
|
95
|
+
<button class="menu-danger" data-action="delete" data-url="<%= catpm.destroy_sample_events_path(sample_id: sample.id) %>">Delete</button>
|
|
96
|
+
</div>
|
|
97
|
+
</td>
|
|
81
98
|
</tr>
|
|
82
99
|
<% end %>
|
|
83
100
|
</tbody>
|
|
@@ -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,11 @@
|
|
|
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
|
+
<link rel="icon" type="image/png" sizes="32x32" href="<%= image_path('catpm/favicon-32.png') %>">
|
|
7
|
+
<link rel="icon" type="image/png" sizes="512x512" href="<%= image_path('catpm/favicon-512.png') %>">
|
|
8
|
+
<link rel="icon" type="image/x-icon" href="<%= image_path('catpm/favicon.ico') %>">
|
|
9
|
+
<link rel="apple-touch-icon" href="<%= image_path('catpm/favicon-512.png') %>">
|
|
10
|
+
<%= csrf_meta_tags %>
|
|
6
11
|
<%= yield :head_extra if content_for?(:head_extra) %>
|
|
7
12
|
<style>
|
|
8
13
|
/* ─── Design Tokens — Light Theme ─── */
|
|
@@ -199,6 +204,22 @@
|
|
|
199
204
|
.btn-primary:hover { background: var(--border); }
|
|
200
205
|
.btn-danger { background: var(--bg-0); color: var(--text-2); border-color: var(--border); }
|
|
201
206
|
.btn-danger:hover { background: #ffebe9; color: var(--red); border-color: #ffcecb; }
|
|
207
|
+
|
|
208
|
+
/* ─── Star Button ─── */
|
|
209
|
+
.star-btn { background: none; border: none; cursor: pointer; font-size: 16px; color: var(--text-2); padding: 0; line-height: 1; transition: color 0.15s; }
|
|
210
|
+
.star-btn:hover { color: #e3b341; }
|
|
211
|
+
.star-btn.pinned { color: #e3b341; }
|
|
212
|
+
|
|
213
|
+
/* ─── Action Menu (3-dot) ─── */
|
|
214
|
+
.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; }
|
|
215
|
+
.action-menu-btn:hover, .action-menu-btn.active { color: var(--text-0); background: var(--bg-2); }
|
|
216
|
+
.action-menu { display: none; }
|
|
217
|
+
.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; }
|
|
218
|
+
.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; }
|
|
219
|
+
.action-menu-overlay button:hover { background: var(--bg-1); }
|
|
220
|
+
.action-menu-overlay .menu-danger { color: var(--red); }
|
|
221
|
+
.action-menu-overlay .menu-danger:hover { background: #ffebe9; }
|
|
222
|
+
|
|
202
223
|
.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
224
|
.copy-btn:hover { color: var(--text-0); background: var(--bg-2); }
|
|
204
225
|
|
|
@@ -422,6 +443,123 @@
|
|
|
422
443
|
if (search) search.focus();
|
|
423
444
|
}
|
|
424
445
|
});
|
|
446
|
+
|
|
447
|
+
/* ─── AJAX Helpers ─── */
|
|
448
|
+
function csrfToken() {
|
|
449
|
+
var meta = document.querySelector('meta[name="csrf-token"]');
|
|
450
|
+
return meta ? meta.getAttribute('content') : '';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function apiRequest(url, method) {
|
|
454
|
+
return fetch(url, {
|
|
455
|
+
method: method,
|
|
456
|
+
headers: { 'X-CSRF-Token': csrfToken(), 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
|
|
457
|
+
credentials: 'same-origin'
|
|
458
|
+
}).then(function(r) { return r.json(); });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function fadeOutRow(row) {
|
|
462
|
+
row.style.transition = 'opacity 0.3s';
|
|
463
|
+
row.style.opacity = '0';
|
|
464
|
+
setTimeout(function() { row.remove(); }, 300);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
var _actionMenu = null;
|
|
468
|
+
function closeActionMenu() {
|
|
469
|
+
if (_actionMenu) {
|
|
470
|
+
_actionMenu.overlay.remove();
|
|
471
|
+
_actionMenu.btn.classList.remove('active');
|
|
472
|
+
_actionMenu = null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
document.addEventListener('click', function(e) {
|
|
477
|
+
/* Star (pin) toggle */
|
|
478
|
+
var starBtn = e.target.closest('.star-btn');
|
|
479
|
+
if (starBtn) {
|
|
480
|
+
e.stopPropagation();
|
|
481
|
+
e.preventDefault();
|
|
482
|
+
apiRequest(starBtn.dataset.pinUrl, 'PATCH').then(function(data) {
|
|
483
|
+
starBtn.classList.toggle('pinned', data.pinned);
|
|
484
|
+
starBtn.innerHTML = data.pinned ? '\u2605' : '\u2606';
|
|
485
|
+
});
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Pin text button (endpoint page) */
|
|
490
|
+
var pinTextBtn = e.target.closest('[data-pin-text]');
|
|
491
|
+
if (pinTextBtn) {
|
|
492
|
+
e.preventDefault();
|
|
493
|
+
apiRequest(pinTextBtn.dataset.pinUrl, 'PATCH').then(function(data) {
|
|
494
|
+
pinTextBtn.textContent = data.pinned ? 'Unpin' : 'Pin';
|
|
495
|
+
pinTextBtn.classList.toggle('btn-primary', data.pinned);
|
|
496
|
+
});
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* 3-dot menu toggle — portal overlay on body */
|
|
501
|
+
var menuBtn = e.target.closest('.action-menu-btn');
|
|
502
|
+
if (menuBtn) {
|
|
503
|
+
e.stopPropagation();
|
|
504
|
+
closeActionMenu();
|
|
505
|
+
var source = menuBtn.nextElementSibling;
|
|
506
|
+
if (!source) return;
|
|
507
|
+
var row = menuBtn.closest('tr');
|
|
508
|
+
var rect = menuBtn.getBoundingClientRect();
|
|
509
|
+
|
|
510
|
+
var overlay = document.createElement('div');
|
|
511
|
+
overlay.className = 'action-menu-overlay';
|
|
512
|
+
source.querySelectorAll('button').forEach(function(src) {
|
|
513
|
+
var item = document.createElement('button');
|
|
514
|
+
item.textContent = src.textContent;
|
|
515
|
+
if (src.classList.contains('menu-danger')) item.className = 'menu-danger';
|
|
516
|
+
item.dataset.action = src.dataset.action;
|
|
517
|
+
item.dataset.url = src.dataset.url;
|
|
518
|
+
overlay.appendChild(item);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
document.body.appendChild(overlay);
|
|
522
|
+
menuBtn.classList.add('active');
|
|
523
|
+
_actionMenu = { overlay: overlay, row: row, btn: menuBtn };
|
|
524
|
+
|
|
525
|
+
overlay.style.right = (window.innerWidth - rect.right) + 'px';
|
|
526
|
+
overlay.style.top = (rect.bottom + 4) + 'px';
|
|
527
|
+
requestAnimationFrame(function() {
|
|
528
|
+
var mr = overlay.getBoundingClientRect();
|
|
529
|
+
if (mr.bottom > window.innerHeight - 8) {
|
|
530
|
+
overlay.style.top = (rect.top - mr.height - 4) + 'px';
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* Menu overlay action clicks */
|
|
537
|
+
var overlayBtn = e.target.closest('.action-menu-overlay button');
|
|
538
|
+
if (overlayBtn) {
|
|
539
|
+
e.stopPropagation();
|
|
540
|
+
var action = overlayBtn.dataset.action;
|
|
541
|
+
var url = overlayBtn.dataset.url;
|
|
542
|
+
var row = _actionMenu ? _actionMenu.row : null;
|
|
543
|
+
closeActionMenu();
|
|
544
|
+
|
|
545
|
+
if (action === 'ignore') {
|
|
546
|
+
apiRequest(url, 'PATCH').then(function(data) { if (data.ignored && row) fadeOutRow(row); });
|
|
547
|
+
} else if (action === 'delete') {
|
|
548
|
+
if (!confirm('Delete this and all its data?')) return;
|
|
549
|
+
apiRequest(url, 'DELETE').then(function(data) { if (data.deleted && row) fadeOutRow(row); });
|
|
550
|
+
} else if (action === 'unignore') {
|
|
551
|
+
apiRequest(url, 'PATCH').then(function(data) { if (!data.ignored && row) fadeOutRow(row); });
|
|
552
|
+
} else if (action === 'resolve') {
|
|
553
|
+
apiRequest(url, 'PATCH').then(function(data) { if (data.resolved && row) fadeOutRow(row); });
|
|
554
|
+
} else if (action === 'unresolve') {
|
|
555
|
+
apiRequest(url, 'PATCH').then(function(data) { if (!data.resolved && row) fadeOutRow(row); });
|
|
556
|
+
}
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/* Close menu on outside click */
|
|
561
|
+
closeActionMenu();
|
|
562
|
+
});
|
|
425
563
|
</script>
|
|
426
564
|
</body>
|
|
427
565
|
</html>
|
data/config/routes.rb
CHANGED
|
@@ -6,8 +6,18 @@ 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 do
|
|
14
|
+
collection do
|
|
15
|
+
delete 'samples/:sample_id', to: 'events#destroy_sample', as: :destroy_sample
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
patch 'events/:name/pin', to: 'events#toggle_pin', as: :event_pin
|
|
19
|
+
patch 'events/:name/ignore', to: 'events#toggle_ignore', as: :event_ignore
|
|
20
|
+
get 'events_ignored', to: 'events#ignored', as: :ignored_events
|
|
11
21
|
resources :errors, only: [:index, :show, :destroy] do
|
|
12
22
|
collection do
|
|
13
23
|
post :resolve_all
|
|
@@ -15,6 +25,7 @@ Catpm::Engine.routes.draw do
|
|
|
15
25
|
member do
|
|
16
26
|
patch :resolve
|
|
17
27
|
patch :unresolve
|
|
28
|
+
patch :toggle_pin
|
|
18
29
|
end
|
|
19
30
|
end
|
|
20
31
|
end
|
|
@@ -50,6 +50,7 @@ class CreateCatpmTables < ActiveRecord::Migration[8.0]
|
|
|
50
50
|
t.json :contexts
|
|
51
51
|
t.json :occurrence_buckets
|
|
52
52
|
t.datetime :resolved_at
|
|
53
|
+
t.boolean :pinned, null: false, default: false
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
add_index :catpm_errors, :fingerprint, unique: true, name: 'idx_catpm_errors_fingerprint'
|
|
@@ -74,6 +75,26 @@ class CreateCatpmTables < ActiveRecord::Migration[8.0]
|
|
|
74
75
|
add_index :catpm_event_samples, [:name, :recorded_at], name: 'idx_catpm_event_samples_name_time'
|
|
75
76
|
add_index :catpm_event_samples, :recorded_at, name: 'idx_catpm_event_samples_time'
|
|
76
77
|
|
|
78
|
+
create_table :catpm_endpoint_prefs do |t|
|
|
79
|
+
t.string :kind, null: false
|
|
80
|
+
t.string :target, null: false
|
|
81
|
+
t.string :operation, null: false, default: ''
|
|
82
|
+
t.boolean :pinned, null: false, default: false
|
|
83
|
+
t.boolean :ignored, null: false, default: false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
add_index :catpm_endpoint_prefs, [:kind, :target, :operation],
|
|
87
|
+
unique: true, name: 'idx_catpm_endpoint_prefs_unique'
|
|
88
|
+
|
|
89
|
+
create_table :catpm_event_prefs do |t|
|
|
90
|
+
t.string :name, null: false
|
|
91
|
+
t.boolean :pinned, null: false, default: false
|
|
92
|
+
t.boolean :ignored, null: false, default: false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
add_index :catpm_event_prefs, :name,
|
|
96
|
+
unique: true, name: 'idx_catpm_event_prefs_unique'
|
|
97
|
+
|
|
77
98
|
if postgresql?
|
|
78
99
|
execute <<~SQL
|
|
79
100
|
CREATE OR REPLACE FUNCTION catpm_merge_jsonb_sums(a jsonb, b jsonb)
|
|
@@ -92,6 +113,8 @@ class CreateCatpmTables < ActiveRecord::Migration[8.0]
|
|
|
92
113
|
execute 'DROP FUNCTION IF EXISTS catpm_merge_jsonb_sums(jsonb, jsonb);'
|
|
93
114
|
end
|
|
94
115
|
|
|
116
|
+
drop_table :catpm_event_prefs, if_exists: true
|
|
117
|
+
drop_table :catpm_endpoint_prefs, if_exists: true
|
|
95
118
|
drop_table :catpm_event_samples, if_exists: true
|
|
96
119
|
drop_table :catpm_event_buckets, if_exists: true
|
|
97
120
|
drop_table :catpm_errors, if_exists: true
|
data/lib/catpm/middleware.rb
CHANGED
data/lib/catpm/version.rb
CHANGED
data/lib/tasks/catpm_tasks.rake
CHANGED
|
@@ -19,5 +19,40 @@ 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.column_exists?(:catpm_errors, :pinned)
|
|
39
|
+
connection.add_column :catpm_errors, :pinned, :boolean, null: false, default: false
|
|
40
|
+
puts '[catpm] Added pinned column to catpm_errors'
|
|
41
|
+
else
|
|
42
|
+
puts '[catpm] catpm_errors.pinned already exists, skipping'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
unless connection.table_exists?(:catpm_event_prefs)
|
|
46
|
+
connection.create_table :catpm_event_prefs do |t|
|
|
47
|
+
t.string :name, null: false
|
|
48
|
+
t.boolean :pinned, null: false, default: false
|
|
49
|
+
t.boolean :ignored, null: false, default: false
|
|
50
|
+
end
|
|
51
|
+
connection.add_index :catpm_event_prefs, :name,
|
|
52
|
+
unique: true, name: 'idx_catpm_event_prefs_unique'
|
|
53
|
+
puts '[catpm] Created catpm_event_prefs table'
|
|
54
|
+
else
|
|
55
|
+
puts '[catpm] catpm_event_prefs table already exists, skipping'
|
|
56
|
+
end
|
|
22
57
|
end
|
|
23
58
|
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.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ''
|
|
@@ -33,6 +33,9 @@ files:
|
|
|
33
33
|
- MIT-LICENSE
|
|
34
34
|
- README.md
|
|
35
35
|
- Rakefile
|
|
36
|
+
- app/assets/images/catpm/favicon-32.png
|
|
37
|
+
- app/assets/images/catpm/favicon-512.png
|
|
38
|
+
- app/assets/images/catpm/favicon.ico
|
|
36
39
|
- app/assets/stylesheets/catpm/application.css
|
|
37
40
|
- app/controllers/catpm/application_controller.rb
|
|
38
41
|
- app/controllers/catpm/endpoints_controller.rb
|
|
@@ -46,14 +49,18 @@ files:
|
|
|
46
49
|
- app/mailers/catpm/application_mailer.rb
|
|
47
50
|
- app/models/catpm/application_record.rb
|
|
48
51
|
- app/models/catpm/bucket.rb
|
|
52
|
+
- app/models/catpm/endpoint_pref.rb
|
|
49
53
|
- app/models/catpm/error_record.rb
|
|
50
54
|
- app/models/catpm/event_bucket.rb
|
|
55
|
+
- app/models/catpm/event_pref.rb
|
|
51
56
|
- app/models/catpm/event_sample.rb
|
|
52
57
|
- app/models/catpm/sample.rb
|
|
53
58
|
- app/views/catpm/endpoints/_sample_table.html.erb
|
|
59
|
+
- app/views/catpm/endpoints/ignored.html.erb
|
|
54
60
|
- app/views/catpm/endpoints/show.html.erb
|
|
55
61
|
- app/views/catpm/errors/index.html.erb
|
|
56
62
|
- app/views/catpm/errors/show.html.erb
|
|
63
|
+
- app/views/catpm/events/ignored.html.erb
|
|
57
64
|
- app/views/catpm/events/index.html.erb
|
|
58
65
|
- app/views/catpm/events/show.html.erb
|
|
59
66
|
- app/views/catpm/samples/show.html.erb
|