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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3337550bf775b5f8898cd083bf92b679e913a68166e03064614a16949808bbd
4
- data.tar.gz: dd37cf03eaa472383509efb76aa77e9760dcbb38a06bf9eb380b73080888671f
3
+ metadata.gz: 01352c602172578657721bce465d06d1db4f3c077cedcecbeb9742eae276764e
4
+ data.tar.gz: e9a7d7c7be46a629b96d65cdbfec77bbb3b3a25d23acc07c8f52b45ea8ba9d0a
5
5
  SHA512:
6
- metadata.gz: 872ec9263dffcdb9c4df391c4259577a0a15805a54cc9e0f8116f0942bee6b05e289b072764e950028dc4fc5dfb786831e864c86d7284f2f88d1b4b71bb20c93
7
- data.tar.gz: 7e23681c9968c2b2248e3f7ee93e487f2570cce77139acff21e7e1f6f5d2236c58cc09506fff434dd6cec7401cdce5cc26bd9af3321aa5cc43ad3371e12f12b9
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
- return unless ErrorRadar.config.track_occurrences
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
- begin
30
- @occ_page = [params[:occ_page].to_i, 1].max
31
- occ_per_page = 20
32
- @occ_total = @error.occurrences.count
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErrorRadar
4
+ class ErrorComment < ApplicationRecord
5
+ belongs_to :error_log
6
+
7
+ validates :body, presence: true
8
+
9
+ scope :chronological, -> { order(created_at: :asc) }
10
+ end
11
+ 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 &amp; 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 &amp;&amp; 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
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', 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
- delete 'errors/:id', to: 'errors#destroy'
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ErrorRadar
4
- VERSION = '0.9.0'
4
+ VERSION = '1.0.0'
5
5
  end
@@ -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.9.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: