error_radar 0.9.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/app/controllers/error_radar/errors_controller.rb +87 -13
- data/app/models/error_radar/error_activity.rb +25 -0
- data/app/models/error_radar/error_comment.rb +11 -0
- data/app/models/error_radar/error_log.rb +8 -0
- data/app/views/error_radar/errors/show.html.erb +224 -0
- data/config/routes.rb +9 -6
- data/lib/error_radar/version.rb +1 -1
- data/lib/generators/error_radar/install/templates/initializer.rb +5 -0
- data/lib/generators/error_radar/upgrade_v100/templates/upgrade_to_v100.rb.tt +31 -0
- data/lib/generators/error_radar/upgrade_v100/upgrade_v100_generator.rb +20 -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: 01352c602172578657721bce465d06d1db4f3c077cedcecbeb9742eae276764e
|
|
4
|
+
data.tar.gz: e9a7d7c7be46a629b96d65cdbfec77bbb3b3a25d23acc07c8f52b45ea8ba9d0a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bfcf06383fe9f3fa619b7ed10e958db142c2f2eee52ce68e01b5ebc0d7af92ce7a706cb1bb851247da1590be49d77e6558ccd89f456985eb8dc67b5bf2dc6f99
|
|
7
|
+
data.tar.gz: 858acdf7ce7ae9ca9be1ac55f13d4120b7a9ba5460f605bad529c46ef235920faa31a43bdbb0a088ac41ba0a09a342fa4298601f1b53249a72d1b142e4601c28
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.0.0] - 2026-07-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Error assignment**: assign any error to a team member from the detail page.
|
|
9
|
+
Stored in the new `assigned_to` column on `error_radar_error_logs`.
|
|
10
|
+
- **Comment thread**: add, view, and delete comments on each error. Comments
|
|
11
|
+
are stored in the new `error_radar_comments` table with `author` and `body`.
|
|
12
|
+
- **Activity log**: every status change, assignment, and comment is recorded in
|
|
13
|
+
`error_radar_activities` (columns: `actor`, `action`, `detail`, `created_at`).
|
|
14
|
+
The timeline is shown on the error detail page (last 50 events).
|
|
15
|
+
- **`error_radar:upgrade_v100` generator**: one migration that adds `assigned_to`
|
|
16
|
+
to `error_radar_error_logs` and creates both `error_radar_comments` and
|
|
17
|
+
`error_radar_activities` tables.
|
|
18
|
+
- **`PATCH /errors/:id/assign`**: JSON endpoint to assign/unassign an error.
|
|
19
|
+
- **`POST /errors/:id/comments`**: JSON endpoint to add a comment.
|
|
20
|
+
- **`DELETE /errors/:id/comments/:cid`**: JSON endpoint to remove a comment.
|
|
21
|
+
- **`ErrorRadar::ErrorActivity`** model with `icon` helper mapping action names
|
|
22
|
+
to display glyphs (✓ resolved, ↩ reopened, 💬 commented, → assigned, …).
|
|
23
|
+
- **`ErrorRadar::ErrorComment`** model with `chronological` scope.
|
|
24
|
+
- All new features are **backward-compatible** — existing apps without the
|
|
25
|
+
migration see a panel with the migration command to run.
|
|
26
|
+
|
|
27
|
+
### Notes
|
|
28
|
+
- Activity logging is best-effort: if the activities table does not exist yet,
|
|
29
|
+
the action still succeeds and the log write is silently skipped.
|
|
30
|
+
- The `current_user` proc (`config.current_user`) is used as the default actor;
|
|
31
|
+
clients can also pass `author` in the comment POST body.
|
|
32
|
+
|
|
5
33
|
## [0.9.0] - 2026-07-03
|
|
6
34
|
|
|
7
35
|
### Added
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module ErrorRadar
|
|
4
4
|
class ErrorsController < ApplicationController
|
|
5
5
|
before_action :authenticate_request!
|
|
6
|
-
before_action :set_error, only: %i[show update_status destroy create_github_issue]
|
|
6
|
+
before_action :set_error, only: %i[show update_status destroy create_github_issue assign comment destroy_comment]
|
|
7
7
|
|
|
8
8
|
rescue_from ActiveRecord::RecordNotFound do
|
|
9
9
|
redirect_to errors_path, alert: 'Error not found.'
|
|
@@ -24,20 +24,81 @@ module ErrorRadar
|
|
|
24
24
|
|
|
25
25
|
def show
|
|
26
26
|
@occurrences = []
|
|
27
|
-
|
|
27
|
+
if ErrorRadar.config.track_occurrences
|
|
28
|
+
begin
|
|
29
|
+
@occ_page = [params[:occ_page].to_i, 1].max
|
|
30
|
+
occ_per_page = 20
|
|
31
|
+
@occ_total = @error.occurrences.count
|
|
32
|
+
@occ_total_pages = [(@occ_total.to_f / occ_per_page).ceil, 1].max
|
|
33
|
+
@occ_page = [@occ_page, @occ_total_pages].min
|
|
34
|
+
@occurrences = @error.occurrences.recent
|
|
35
|
+
.limit(occ_per_page)
|
|
36
|
+
.offset((@occ_page - 1) * occ_per_page)
|
|
37
|
+
rescue ActiveRecord::StatementInvalid
|
|
38
|
+
@occurrences = []
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@has_v100 = @error.class.column_names.include?('assigned_to')
|
|
43
|
+
@comments = []
|
|
44
|
+
@activities = []
|
|
45
|
+
if @has_v100
|
|
46
|
+
begin
|
|
47
|
+
@comments = @error.comments.chronological
|
|
48
|
+
@activities = @error.activities.recent.limit(50)
|
|
49
|
+
rescue ActiveRecord::StatementInvalid
|
|
50
|
+
@has_v100 = false
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def assign
|
|
56
|
+
unless @error.class.column_names.include?('assigned_to')
|
|
57
|
+
return render json: { ok: false, error: 'Run upgrade_v100 migration first' }, status: :unprocessable_entity
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
assignee = params[:assigned_to].to_s.strip.truncate(200)
|
|
61
|
+
@error.update!(assigned_to: assignee.presence)
|
|
62
|
+
|
|
63
|
+
action_label = assignee.present? ? "assigned → #{assignee}" : 'unassigned'
|
|
64
|
+
log_activity(@error, action: action_label)
|
|
65
|
+
|
|
66
|
+
render json: { ok: true, assigned_to: @error.assigned_to }
|
|
67
|
+
end
|
|
28
68
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@occ_total_pages = [(@occ_total.to_f / occ_per_page).ceil, 1].max
|
|
34
|
-
@occ_page = [@occ_page, @occ_total_pages].min
|
|
35
|
-
@occurrences = @error.occurrences.recent
|
|
36
|
-
.limit(occ_per_page)
|
|
37
|
-
.offset((@occ_page - 1) * occ_per_page)
|
|
38
|
-
rescue ActiveRecord::StatementInvalid
|
|
39
|
-
@occurrences = []
|
|
69
|
+
def comment
|
|
70
|
+
body = params[:body].to_s.strip
|
|
71
|
+
if body.blank?
|
|
72
|
+
return render json: { ok: false, error: 'Comment cannot be blank' }, status: :unprocessable_entity
|
|
40
73
|
end
|
|
74
|
+
|
|
75
|
+
author = (error_radar_current_user.presence || params[:author].to_s.strip.presence || 'Anonymous').truncate(200)
|
|
76
|
+
cmt = ErrorComment.create!(error_log: @error, author: author, body: body)
|
|
77
|
+
|
|
78
|
+
log_activity(@error, action: 'commented', actor: author, detail: body.truncate(120))
|
|
79
|
+
|
|
80
|
+
render json: {
|
|
81
|
+
ok: true,
|
|
82
|
+
id: cmt.id,
|
|
83
|
+
author: cmt.author,
|
|
84
|
+
body: cmt.body,
|
|
85
|
+
created_at: cmt.created_at.strftime('%Y-%m-%d %H:%M')
|
|
86
|
+
}
|
|
87
|
+
rescue ActiveRecord::StatementInvalid
|
|
88
|
+
render json: { ok: false, error: 'Run upgrade_v100 migration first' }, status: :unprocessable_entity
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def destroy_comment
|
|
92
|
+
cmt = ErrorComment.find(params[:cid])
|
|
93
|
+
return head :not_found unless cmt.error_log_id == @error.id
|
|
94
|
+
|
|
95
|
+
cmt.destroy!
|
|
96
|
+
log_activity(@error, action: 'comment_deleted')
|
|
97
|
+
render json: { ok: true }
|
|
98
|
+
rescue ActiveRecord::RecordNotFound
|
|
99
|
+
head :not_found
|
|
100
|
+
rescue ActiveRecord::StatementInvalid
|
|
101
|
+
head :unprocessable_entity
|
|
41
102
|
end
|
|
42
103
|
|
|
43
104
|
def update_status
|
|
@@ -52,6 +113,7 @@ module ErrorRadar
|
|
|
52
113
|
@error.update!(status: new_status, resolved_at: nil)
|
|
53
114
|
end
|
|
54
115
|
|
|
116
|
+
log_activity(@error, action: new_status, detail: params[:note].presence)
|
|
55
117
|
render json: { ok: true, id: @error.id, status: @error.status }
|
|
56
118
|
end
|
|
57
119
|
|
|
@@ -164,6 +226,18 @@ module ErrorRadar
|
|
|
164
226
|
ErrorRadar.config.github_token.present? && ErrorRadar.config.github_repo.present?
|
|
165
227
|
end
|
|
166
228
|
|
|
229
|
+
def log_activity(error_log, action:, actor: nil, detail: nil)
|
|
230
|
+
return unless defined?(ErrorRadar::ErrorActivity)
|
|
231
|
+
ErrorRadar::ErrorActivity.create!(
|
|
232
|
+
error_log: error_log,
|
|
233
|
+
actor: (actor || error_radar_current_user).to_s.presence,
|
|
234
|
+
action: action.to_s,
|
|
235
|
+
detail: detail.to_s.truncate(500).presence
|
|
236
|
+
)
|
|
237
|
+
rescue StandardError
|
|
238
|
+
# silently skip if table not yet created or any other error
|
|
239
|
+
end
|
|
240
|
+
|
|
167
241
|
def active_filter_params
|
|
168
242
|
params.permit(:q, :status, :severity, :category, :from, :to, :sort, :order).to_h
|
|
169
243
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ErrorRadar
|
|
4
|
+
class ErrorActivity < ApplicationRecord
|
|
5
|
+
belongs_to :error_log
|
|
6
|
+
|
|
7
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
8
|
+
|
|
9
|
+
ACTION_ICONS = {
|
|
10
|
+
'resolved' => '✓',
|
|
11
|
+
'reopened' => '↩',
|
|
12
|
+
'in_progress' => '▶',
|
|
13
|
+
'open' => '○',
|
|
14
|
+
'ignored' => '–',
|
|
15
|
+
'assigned' => '→',
|
|
16
|
+
'unassigned' => '×',
|
|
17
|
+
'commented' => '💬',
|
|
18
|
+
'comment_deleted'=> '🗑'
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def icon
|
|
22
|
+
ACTION_ICONS.find { |k, _| action.to_s.start_with?(k) }&.last || '·'
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -20,6 +20,14 @@ module ErrorRadar
|
|
|
20
20
|
foreign_key: :error_log_id,
|
|
21
21
|
dependent: :delete_all
|
|
22
22
|
|
|
23
|
+
has_many :comments, class_name: 'ErrorRadar::ErrorComment',
|
|
24
|
+
foreign_key: :error_log_id,
|
|
25
|
+
dependent: :delete_all
|
|
26
|
+
|
|
27
|
+
has_many :activities, class_name: 'ErrorRadar::ErrorActivity',
|
|
28
|
+
foreign_key: :error_log_id,
|
|
29
|
+
dependent: :delete_all
|
|
30
|
+
|
|
23
31
|
validates :fingerprint, presence: true, uniqueness: true
|
|
24
32
|
validates :first_seen_at, :last_seen_at, presence: true
|
|
25
33
|
|
|
@@ -220,6 +220,113 @@
|
|
|
220
220
|
</div>
|
|
221
221
|
<% end %>
|
|
222
222
|
|
|
223
|
+
<%# ── Assignment, Comments & Activity ── %>
|
|
224
|
+
<% if @has_v100 %>
|
|
225
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px">
|
|
226
|
+
|
|
227
|
+
<%# Assignment %>
|
|
228
|
+
<div class="panel" id="assignment-panel">
|
|
229
|
+
<h2>Assignment</h2>
|
|
230
|
+
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
|
231
|
+
<div id="assignee-display" style="font-size:14px;color:#374151">
|
|
232
|
+
<% if @error.assigned_to.present? %>
|
|
233
|
+
Assigned to <strong><%= @error.assigned_to %></strong>
|
|
234
|
+
<% else %>
|
|
235
|
+
<span style="color:#9ca3af">Unassigned</span>
|
|
236
|
+
<% end %>
|
|
237
|
+
</div>
|
|
238
|
+
<button onclick="toggleAssignForm()"
|
|
239
|
+
style="padding:4px 12px;font-size:12px;border:1px solid #d1d5db;border-radius:6px;background:#f9fafb;cursor:pointer">
|
|
240
|
+
Change
|
|
241
|
+
</button>
|
|
242
|
+
</div>
|
|
243
|
+
<div id="assign-form" style="display:none;margin-top:10px">
|
|
244
|
+
<input id="assignee-input" type="text" value="<%= @error.assigned_to %>"
|
|
245
|
+
placeholder="Name or email…"
|
|
246
|
+
style="padding:6px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;width:200px">
|
|
247
|
+
<button onclick="doAssign()"
|
|
248
|
+
style="margin-left:6px;padding:6px 14px;background:#2563eb;color:#fff;border:0;border-radius:6px;font-size:13px;cursor:pointer">
|
|
249
|
+
Save
|
|
250
|
+
</button>
|
|
251
|
+
<button onclick="toggleAssignForm()"
|
|
252
|
+
style="margin-left:4px;padding:6px 10px;background:#f3f4f6;color:#374151;border:1px solid #d1d5db;border-radius:6px;font-size:13px;cursor:pointer">
|
|
253
|
+
Cancel
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<%# Activity timeline %>
|
|
259
|
+
<div class="panel" id="activity-panel">
|
|
260
|
+
<h2>Activity <span style="font-size:12px;font-weight:400;color:#9ca3af">(<%= @activities.size %> events)</span></h2>
|
|
261
|
+
<% if @activities.empty? %>
|
|
262
|
+
<p style="font-size:13px;color:#9ca3af;margin:0">No activity yet.</p>
|
|
263
|
+
<% else %>
|
|
264
|
+
<div style="max-height:280px;overflow-y:auto">
|
|
265
|
+
<% @activities.each do |act| %>
|
|
266
|
+
<div style="display:flex;gap:8px;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:12px">
|
|
267
|
+
<span style="font-size:14px;min-width:20px;text-align:center"><%= act.icon %></span>
|
|
268
|
+
<div style="flex:1">
|
|
269
|
+
<span style="font-weight:600;color:#374151"><%= act.action %></span>
|
|
270
|
+
<% if act.detail.present? %>
|
|
271
|
+
<span style="color:#6b7280"> — <%= act.detail.truncate(80) %></span>
|
|
272
|
+
<% end %>
|
|
273
|
+
<div style="color:#9ca3af;font-size:11px;margin-top:1px">
|
|
274
|
+
<% if act.actor.present? %><%= act.actor %> · <% end %>
|
|
275
|
+
<%= act.created_at&.strftime('%Y-%m-%d %H:%M') %>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
<% end %>
|
|
280
|
+
</div>
|
|
281
|
+
<% end %>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<%# Comments %>
|
|
286
|
+
<div class="panel" style="margin-top:16px" id="comments-panel">
|
|
287
|
+
<h2>Comments <span style="font-size:12px;font-weight:400;color:#9ca3af">(<%= @comments.size %>)</span></h2>
|
|
288
|
+
|
|
289
|
+
<div id="comments-list">
|
|
290
|
+
<% @comments.each do |cmt| %>
|
|
291
|
+
<div class="er-comment" id="comment-<%= cmt.id %>"
|
|
292
|
+
style="border:1px solid #e5e7eb;border-radius:8px;padding:12px 14px;margin-bottom:10px">
|
|
293
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:6px">
|
|
294
|
+
<div style="font-size:12px;font-weight:600;color:#374151"><%= cmt.author.presence || 'Anonymous' %></div>
|
|
295
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
296
|
+
<span style="font-size:11px;color:#9ca3af"><%= cmt.created_at&.strftime('%Y-%m-%d %H:%M') %></span>
|
|
297
|
+
<button onclick="deleteComment(<%= cmt.id %>)"
|
|
298
|
+
style="font-size:11px;color:#dc3545;background:none;border:0;cursor:pointer;padding:0">✕</button>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
<div style="font-size:13px;color:#1f2937;white-space:pre-wrap"><%= cmt.body %></div>
|
|
302
|
+
</div>
|
|
303
|
+
<% end %>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<div style="margin-top:12px">
|
|
307
|
+
<textarea id="comment-body" rows="3" placeholder="Add a comment…"
|
|
308
|
+
style="width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:13px;resize:vertical;box-sizing:border-box"></textarea>
|
|
309
|
+
<div style="margin-top:6px;display:flex;justify-content:flex-end">
|
|
310
|
+
<button onclick="submitComment()"
|
|
311
|
+
style="padding:7px 18px;background:#2563eb;color:#fff;border:0;border-radius:8px;font-size:13px;cursor:pointer">
|
|
312
|
+
Add comment
|
|
313
|
+
</button>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<% else %>
|
|
319
|
+
<div class="panel" style="margin-top:16px">
|
|
320
|
+
<h2>Assignment, Comments & Activity (v1.0.0)</h2>
|
|
321
|
+
<p style="font-size:13px;color:#6b7280;margin:0">
|
|
322
|
+
Run the upgrade migration to enable these features:
|
|
323
|
+
</p>
|
|
324
|
+
<code style="display:block;margin-top:8px;background:#f3f4f6;padding:10px 14px;border-radius:8px;font-size:12px">
|
|
325
|
+
bin/rails generate error_radar:upgrade_v100 && bin/rails db:migrate
|
|
326
|
+
</code>
|
|
327
|
+
</div>
|
|
328
|
+
<% end %>
|
|
329
|
+
|
|
223
330
|
<script>
|
|
224
331
|
window.toggleOcc = function (id) {
|
|
225
332
|
var el = document.getElementById(id);
|
|
@@ -276,6 +383,123 @@
|
|
|
276
383
|
});
|
|
277
384
|
};
|
|
278
385
|
|
|
386
|
+
// ── Assignment ──
|
|
387
|
+
var ASSIGN_URL = '<%= respond_to?(:error_assign_path) ? error_assign_path(@error) : "" %>';
|
|
388
|
+
var COMMENT_URL = '<%= respond_to?(:error_comments_path) ? error_comments_path(@error) : "" %>';
|
|
389
|
+
|
|
390
|
+
window.toggleAssignForm = function () {
|
|
391
|
+
var f = document.getElementById('assign-form');
|
|
392
|
+
if (f) f.style.display = f.style.display === 'none' ? 'block' : 'none';
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
window.doAssign = function () {
|
|
396
|
+
var val = document.getElementById('assignee-input').value.trim();
|
|
397
|
+
fetch(ASSIGN_URL, {
|
|
398
|
+
method: 'PATCH',
|
|
399
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': CSRF },
|
|
400
|
+
body: JSON.stringify({ assigned_to: val })
|
|
401
|
+
}).then(function (r) { return r.json(); })
|
|
402
|
+
.then(function (d) {
|
|
403
|
+
if (d.ok) {
|
|
404
|
+
var disp = document.getElementById('assignee-display');
|
|
405
|
+
disp.innerHTML = d.assigned_to
|
|
406
|
+
? 'Assigned to <strong>' + d.assigned_to + '</strong>'
|
|
407
|
+
: '<span style="color:#9ca3af">Unassigned</span>';
|
|
408
|
+
toggleAssignForm();
|
|
409
|
+
appendActivity({ icon: '→', action: d.assigned_to ? 'assigned → ' + d.assigned_to : 'unassigned' });
|
|
410
|
+
showToast(d.assigned_to ? 'Assigned to ' + d.assigned_to : 'Unassigned', true);
|
|
411
|
+
} else {
|
|
412
|
+
showToast(d.error || 'Failed', false);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// ── Comments ──
|
|
418
|
+
window.submitComment = function () {
|
|
419
|
+
var body = document.getElementById('comment-body').value.trim();
|
|
420
|
+
if (!body) { showToast('Comment cannot be blank', false); return; }
|
|
421
|
+
|
|
422
|
+
fetch(COMMENT_URL, {
|
|
423
|
+
method: 'POST',
|
|
424
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': CSRF },
|
|
425
|
+
body: JSON.stringify({ body: body })
|
|
426
|
+
}).then(function (r) { return r.json(); })
|
|
427
|
+
.then(function (d) {
|
|
428
|
+
if (d.ok) {
|
|
429
|
+
document.getElementById('comment-body').value = '';
|
|
430
|
+
var list = document.getElementById('comments-list');
|
|
431
|
+
list.insertAdjacentHTML('beforeend',
|
|
432
|
+
'<div class="er-comment" id="comment-' + d.id + '" style="border:1px solid #e5e7eb;border-radius:8px;padding:12px 14px;margin-bottom:10px">' +
|
|
433
|
+
'<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:6px">' +
|
|
434
|
+
'<div style="font-size:12px;font-weight:600;color:#374151">' + (d.author || 'Anonymous') + '</div>' +
|
|
435
|
+
'<div style="display:flex;gap:8px;align-items:center">' +
|
|
436
|
+
'<span style="font-size:11px;color:#9ca3af">' + d.created_at + '</span>' +
|
|
437
|
+
'<button onclick="deleteComment(' + d.id + ')" style="font-size:11px;color:#dc3545;background:none;border:0;cursor:pointer;padding:0">✕</button>' +
|
|
438
|
+
'</div></div>' +
|
|
439
|
+
'<div style="font-size:13px;color:#1f2937;white-space:pre-wrap">' + escHtml(d.body) + '</div>' +
|
|
440
|
+
'</div>'
|
|
441
|
+
);
|
|
442
|
+
appendActivity({ icon: '💬', action: 'commented', detail: d.body.substring(0, 80) });
|
|
443
|
+
updateCommentCount(1);
|
|
444
|
+
showToast('Comment added', true);
|
|
445
|
+
} else {
|
|
446
|
+
showToast(d.error || 'Failed', false);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
window.deleteComment = function (id) {
|
|
452
|
+
if (!confirm('Delete this comment?')) return;
|
|
453
|
+
fetch('<%= respond_to?(:error_comment_path) ? error_path(@error) : "" %>/comments/' + id, {
|
|
454
|
+
method: 'DELETE',
|
|
455
|
+
headers: { 'X-CSRF-Token': CSRF }
|
|
456
|
+
}).then(function (r) {
|
|
457
|
+
if (r.ok) {
|
|
458
|
+
var el = document.getElementById('comment-' + id);
|
|
459
|
+
if (el) el.remove();
|
|
460
|
+
appendActivity({ icon: '🗑', action: 'comment_deleted' });
|
|
461
|
+
updateCommentCount(-1);
|
|
462
|
+
showToast('Comment deleted', true);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
function updateCommentCount(delta) {
|
|
468
|
+
var h2 = document.querySelector('#comments-panel h2');
|
|
469
|
+
if (!h2) return;
|
|
470
|
+
var match = h2.textContent.match(/(\d+)/);
|
|
471
|
+
if (match) {
|
|
472
|
+
var n = parseInt(match[1], 10) + delta;
|
|
473
|
+
h2.innerHTML = 'Comments <span style="font-size:12px;font-weight:400;color:#9ca3af">(' + n + ')</span>';
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function appendActivity(act) {
|
|
478
|
+
var list = document.querySelector('#activity-panel [style*="max-height"]');
|
|
479
|
+
var empty = document.querySelector('#activity-panel p');
|
|
480
|
+
if (empty) { empty.remove(); }
|
|
481
|
+
if (!list) {
|
|
482
|
+
var panel = document.querySelector('#activity-panel');
|
|
483
|
+
if (!panel) return;
|
|
484
|
+
list = document.createElement('div');
|
|
485
|
+
list.style.cssText = 'max-height:280px;overflow-y:auto';
|
|
486
|
+
panel.appendChild(list);
|
|
487
|
+
}
|
|
488
|
+
var now = new Date();
|
|
489
|
+
var ts = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0') + ' ' + String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');
|
|
490
|
+
list.insertAdjacentHTML('afterbegin',
|
|
491
|
+
'<div style="display:flex;gap:8px;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:12px">' +
|
|
492
|
+
'<span style="font-size:14px;min-width:20px;text-align:center">' + (act.icon || '·') + '</span>' +
|
|
493
|
+
'<div style="flex:1"><span style="font-weight:600;color:#374151">' + act.action + '</span>' +
|
|
494
|
+
(act.detail ? '<span style="color:#6b7280"> — ' + escHtml(act.detail) + '</span>' : '') +
|
|
495
|
+
'<div style="color:#9ca3af;font-size:11px;margin-top:1px">' + ts + '</div></div></div>'
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function escHtml(str) {
|
|
500
|
+
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
501
|
+
}
|
|
502
|
+
|
|
279
503
|
window.createGithubIssue = function () {
|
|
280
504
|
var btn = document.getElementById('github-btn');
|
|
281
505
|
var status = document.getElementById('github-status');
|
data/config/routes.rb
CHANGED
|
@@ -10,12 +10,15 @@ ErrorRadar::Engine.routes.draw do
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
# Web UI
|
|
13
|
-
get 'errors',
|
|
14
|
-
post 'errors/bulk',
|
|
15
|
-
post 'errors/:id/github_issue',
|
|
16
|
-
get 'errors/:id',
|
|
17
|
-
patch 'errors/:id/status',
|
|
18
|
-
|
|
13
|
+
get 'errors', to: 'errors#index', as: :errors
|
|
14
|
+
post 'errors/bulk', to: 'errors#bulk', as: :errors_bulk
|
|
15
|
+
post 'errors/:id/github_issue', to: 'errors#create_github_issue', as: :error_github_issue
|
|
16
|
+
get 'errors/:id', to: 'errors#show', as: :error
|
|
17
|
+
patch 'errors/:id/status', to: 'errors#update_status', as: :error_status
|
|
18
|
+
patch 'errors/:id/assign', to: 'errors#assign', as: :error_assign
|
|
19
|
+
post 'errors/:id/comments', to: 'errors#comment', as: :error_comments
|
|
20
|
+
delete 'errors/:id/comments/:cid', to: 'errors#destroy_comment', as: :error_comment
|
|
21
|
+
delete 'errors/:id', to: 'errors#destroy'
|
|
19
22
|
|
|
20
23
|
post 'maintenance/purge', to: 'dashboard#purge', as: :maintenance_purge
|
|
21
24
|
end
|
data/lib/error_radar/version.rb
CHANGED
|
@@ -93,6 +93,11 @@ ErrorRadar.configure do |config|
|
|
|
93
93
|
# config.github_token = ENV['GITHUB_TOKEN'] # PAT with repo scope
|
|
94
94
|
# config.github_repo = 'myorg/myapp' # "owner/repo" format
|
|
95
95
|
|
|
96
|
+
# --- Assignment & Comments (v1.0.0) ---
|
|
97
|
+
# Requires running: bin/rails generate error_radar:upgrade_v100 && bin/rails db:migrate
|
|
98
|
+
# Adds assigned_to column, error_radar_comments and error_radar_activities tables.
|
|
99
|
+
# No config needed — use the UI to assign errors and add comments.
|
|
100
|
+
|
|
96
101
|
# --- Digest Email ---
|
|
97
102
|
# Sends a periodic summary of errors via email. Requires ActionMailer.
|
|
98
103
|
# Run via cron / Heroku Scheduler:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class UpgradeErrorRadarToV100 < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
4
|
+
def change
|
|
5
|
+
# Assignment field on the error log
|
|
6
|
+
add_column :error_radar_error_logs, :assigned_to, :string
|
|
7
|
+
|
|
8
|
+
# Comment thread per error
|
|
9
|
+
create_table :error_radar_comments do |t|
|
|
10
|
+
t.references :error_log, null: false,
|
|
11
|
+
foreign_key: { to_table: :error_radar_error_logs }
|
|
12
|
+
t.string :author
|
|
13
|
+
t.text :body, null: false
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Activity / audit log per error
|
|
18
|
+
create_table :error_radar_activities do |t|
|
|
19
|
+
t.references :error_log, null: false,
|
|
20
|
+
foreign_key: { to_table: :error_radar_error_logs },
|
|
21
|
+
index: false
|
|
22
|
+
t.string :actor # who performed the action
|
|
23
|
+
t.string :action, null: false # "resolved", "assigned → alice", "commented", ...
|
|
24
|
+
t.string :detail, limit: 500 # extra context (note preview, old→new value)
|
|
25
|
+
t.timestamps
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
add_index :error_radar_activities, %i[error_log_id created_at],
|
|
29
|
+
name: 'idx_er_activities_log_time'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators/active_record'
|
|
4
|
+
|
|
5
|
+
module ErrorRadar
|
|
6
|
+
module Generators
|
|
7
|
+
class UpgradeV100Generator < ActiveRecord::Generators::Base
|
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
|
9
|
+
|
|
10
|
+
argument :name, type: :string, default: 'upgrade_v100'
|
|
11
|
+
|
|
12
|
+
desc 'Creates tables for assignment, comments and activity log (v1.0.0).'
|
|
13
|
+
|
|
14
|
+
def create_migration
|
|
15
|
+
migration_template 'upgrade_to_v100.rb.tt',
|
|
16
|
+
'db/migrate/upgrade_error_radar_to_v100.rb'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: error_radar
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- chienbn9x
|
|
@@ -66,6 +66,8 @@ files:
|
|
|
66
66
|
- app/mailers/error_radar/digest_mailer.rb
|
|
67
67
|
- app/mailers/error_radar/error_mailer.rb
|
|
68
68
|
- app/models/error_radar/application_record.rb
|
|
69
|
+
- app/models/error_radar/error_activity.rb
|
|
70
|
+
- app/models/error_radar/error_comment.rb
|
|
69
71
|
- app/models/error_radar/error_log.rb
|
|
70
72
|
- app/models/error_radar/error_occurrence.rb
|
|
71
73
|
- app/views/error_radar/dashboard/index.html.erb
|
|
@@ -106,6 +108,8 @@ files:
|
|
|
106
108
|
- lib/generators/error_radar/upgrade_v050/upgrade_v050_generator.rb
|
|
107
109
|
- lib/generators/error_radar/upgrade_v060/templates/create_error_radar_occurrences.rb.tt
|
|
108
110
|
- lib/generators/error_radar/upgrade_v060/upgrade_v060_generator.rb
|
|
111
|
+
- lib/generators/error_radar/upgrade_v100/templates/upgrade_to_v100.rb.tt
|
|
112
|
+
- lib/generators/error_radar/upgrade_v100/upgrade_v100_generator.rb
|
|
109
113
|
- lib/tasks/error_radar.rake
|
|
110
114
|
homepage: https://github.com/chienbn9x/error_radar
|
|
111
115
|
licenses:
|