rails_error_dashboard 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -3
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +81 -4
  4. data/app/controllers/rails_error_dashboard/webhooks_controller.rb +192 -0
  5. data/app/jobs/rails_error_dashboard/add_issue_recurrence_comment_job.rb +71 -0
  6. data/app/jobs/rails_error_dashboard/close_linked_issue_job.rb +43 -0
  7. data/app/jobs/rails_error_dashboard/create_issue_job.rb +68 -0
  8. data/app/jobs/rails_error_dashboard/reopen_linked_issue_job.rb +44 -0
  9. data/app/models/rails_error_dashboard/error_log.rb +2 -1
  10. data/app/views/layouts/rails_error_dashboard.html.erb +19 -6
  11. data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +1 -1
  12. data/app/views/rails_error_dashboard/errors/_discussion.html.erb +92 -100
  13. data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +121 -0
  14. data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +1 -0
  15. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +77 -73
  16. data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +148 -0
  17. data/app/views/rails_error_dashboard/errors/show.html.erb +13 -9
  18. data/config/routes.rb +6 -1
  19. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +5 -0
  20. data/db/migrate/20260326000001_add_issue_tracking_to_error_logs.rb +15 -0
  21. data/lib/generators/rails_error_dashboard/install/install_generator.rb +12 -4
  22. data/lib/rails_error_dashboard/commands/create_issue.rb +59 -0
  23. data/lib/rails_error_dashboard/commands/link_existing_issue.rb +65 -0
  24. data/lib/rails_error_dashboard/configuration.rb +99 -0
  25. data/lib/rails_error_dashboard/engine.rb +39 -0
  26. data/lib/rails_error_dashboard/queries/active_storage_summary.rb +101 -0
  27. data/lib/rails_error_dashboard/services/codeberg_issue_client.rb +122 -0
  28. data/lib/rails_error_dashboard/services/github_issue_client.rb +117 -0
  29. data/lib/rails_error_dashboard/services/github_link_generator.rb +19 -1
  30. data/lib/rails_error_dashboard/services/gitlab_issue_client.rb +121 -0
  31. data/lib/rails_error_dashboard/services/issue_body_formatter.rb +132 -0
  32. data/lib/rails_error_dashboard/services/issue_tracker_client.rb +168 -0
  33. data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +12 -0
  34. data/lib/rails_error_dashboard/subscribers/active_storage_subscriber.rb +112 -0
  35. data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +71 -0
  36. data/lib/rails_error_dashboard/version.rb +1 -1
  37. data/lib/rails_error_dashboard.rb +11 -1
  38. metadata +21 -3
@@ -1,7 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <title><%= content_for?(:page_title) ? "#{content_for(:page_title)} | " : "" %><%= Rails.application.class.module_parent_name %> - Error Dashboard</title>
4
+ <title><%= content_for?(:page_title) ? "#{content_for(:page_title)} | " : "" %><%= Rails.application.class.module_parent_name %> - RED</title>
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <meta name="turbo-visit-control" content="reload">
7
7
  <% if respond_to?(:csrf_meta_tags) %>
@@ -1547,18 +1547,24 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1547
1547
  end
1548
1548
  %>
1549
1549
  <% if has_root %>
1550
- <a class="navbar-brand" href="<%= root_url %>" title="Back to <%= Rails.application.class.module_parent_name %>">
1550
+ <a class="navbar-brand d-flex align-items-center" href="<%= root_url %>" title="Back to <%= Rails.application.class.module_parent_name %>">
1551
1551
  <i class="bi bi-bug-fill"></i>
1552
1552
  <span class="d-none d-sm-inline"><%= Rails.application.class.module_parent_name %></span>
1553
1553
  <span class="d-none d-md-inline text-white-50 mx-2">|</span>
1554
- <span class="d-none d-md-inline">Error Dashboard</span>
1554
+ <span class="d-none d-md-inline">
1555
+ <strong>RED</strong>
1556
+ <small class="text-white-50 ms-1" style="font-size: 0.65em; vertical-align: middle;">Rails Error Dashboard</small>
1557
+ </span>
1555
1558
  </a>
1556
1559
  <% else %>
1557
- <span class="navbar-brand">
1560
+ <span class="navbar-brand d-flex align-items-center">
1558
1561
  <i class="bi bi-bug-fill"></i>
1559
1562
  <span class="d-none d-sm-inline"><%= Rails.application.class.module_parent_name %></span>
1560
1563
  <span class="d-none d-md-inline text-white-50 mx-2">|</span>
1561
- <span class="d-none d-md-inline">Error Dashboard</span>
1564
+ <span class="d-none d-md-inline">
1565
+ <strong>RED</strong>
1566
+ <small class="text-white-50 ms-1" style="font-size: 0.65em; vertical-align: middle;">Rails Error Dashboard</small>
1567
+ </span>
1562
1568
  </span>
1563
1569
  <% end %>
1564
1570
  </div>
@@ -1681,6 +1687,13 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1681
1687
  <% end %>
1682
1688
  </li>
1683
1689
  <% end %>
1690
+ <% if RailsErrorDashboard.configuration.enable_activestorage_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
1691
+ <li class="nav-item">
1692
+ <%= link_to activestorage_health_summary_errors_path(nav_params), class: "nav-link #{request.path == activestorage_health_summary_errors_path ? 'active' : ''}" do %>
1693
+ <i class="bi bi-cloud-arrow-up"></i> ActiveStorage
1694
+ <% end %>
1695
+ </li>
1696
+ <% end %>
1684
1697
  <% if RailsErrorDashboard.configuration.enable_system_health %>
1685
1698
  <li class="nav-item">
1686
1699
  <%= link_to job_health_summary_errors_path(nav_params), class: "nav-link #{request.path == job_health_summary_errors_path ? 'active' : ''}" do %>
@@ -1820,7 +1833,7 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1820
1833
  <div class="container">
1821
1834
  <p class="text-muted mb-1">
1822
1835
  <i class="bi bi-bug-fill text-primary"></i>
1823
- Built with <a href="https://github.com/AnjanJ/rails_error_dashboard" target="_blank" class="text-decoration-none">Rails Error Dashboard</a>
1836
+ Built with <a href="https://github.com/AnjanJ/rails_error_dashboard" target="_blank" class="text-decoration-none"><strong>RED</strong> — Rails Error Dashboard</a>
1824
1837
  </p>
1825
1838
  <p class="text-muted small mb-0">
1826
1839
  Created by <a href="https://www.anjan.dev/" target="_blank" class="text-decoration-none">Anjan Jagirdar</a>
@@ -196,7 +196,7 @@
196
196
  </h5>
197
197
  <small class="text-muted">Activity trail leading up to this error</small>
198
198
  </div>
199
- <div class="card-body p-0">
199
+ <div class="card-body p-0" style="max-height: 400px; overflow-y: auto;">
200
200
  <div class="table-responsive">
201
201
  <table class="table table-sm table-hover mb-0">
202
202
  <thead class="table-light">
@@ -1,107 +1,99 @@
1
- <!-- Phase 3: Comment Threads -->
2
- <% if error.respond_to?(:comments) %>
3
- <div class="card mb-4" id="section-discussion">
4
- <div class="card-header bg-white">
5
- <h5 class="mb-0">
6
- <i class="bi bi-chat-dots"></i> Discussion
7
- <span class="badge bg-secondary ms-2"><%= error.comments.count %></span>
8
- </h5>
9
- </div>
10
- <div class="card-body">
11
- <!-- Existing Comments -->
12
- <% if error.comments.recent_first.any? %>
13
- <div class="mb-4">
14
- <% error.comments.recent_first.each_with_index do |comment, index| %>
15
- <div class="<%= index < error.comments.count - 1 ? 'border-bottom' : '' %> pb-3 mb-3">
16
- <div class="d-flex justify-content-between align-items-start mb-2">
17
- <div>
18
- <strong class="text-primary">
19
- <i class="bi bi-person-circle"></i> <%= comment.author_name %>
20
- </strong>
21
- <% if comment.recent? %>
22
- <span class="badge bg-success ms-2">New</span>
23
- <% end %>
24
- </div>
25
- <small class="text-muted">
26
- <%= local_time(comment.created_at, format: :datetime) %>
27
- <span class="ms-1 text-muted">(<%= local_time_ago(comment.created_at) %>)</span>
28
- </small>
29
- </div>
30
- <div class="text-break">
31
- <%= auto_link_urls(comment.body, error: error).html_safe %>
32
- </div>
33
- </div>
34
- <% end %>
35
- </div>
36
- <% else %>
37
- <p class="text-muted mb-4">
38
- <i class="bi bi-info-circle"></i> No comments yet. Start the discussion below.
39
- </p>
40
- <% end %>
41
-
42
- <!-- Add Comment Form -->
43
- <div class="<%= error.comments.any? ? '' : 'border-top' %> pt-3">
44
- <h6 class="mb-3">
45
- <i class="bi bi-plus-circle"></i> Add Comment
46
- </h6>
47
- <%= form_with url: add_comment_error_path(error), method: :post do |f| %>
48
- <div class="mb-3">
49
- <label for="author_name" class="form-label">Your Name <span class="text-danger">*</span></label>
50
- <%= text_field_tag :author_name, error.assigned_to, class: "form-control", placeholder: "e.g., John Doe", required: true %>
51
- </div>
52
- <div class="mb-3">
53
- <label for="body" class="form-label">Comment <span class="text-danger">*</span></label>
1
+ <% has_linked_issue = error.respond_to?(:external_issue_url) && error.external_issue_url.present? %>
2
+ <% has_audit_comments = error.respond_to?(:comments) && error.comments.any? %>
3
+ <% platform_comments = @platform_comments || [] %>
4
+ <% issue_tracking_enabled = RailsErrorDashboard.configuration.enable_issue_tracking %>
54
5
 
55
- <!-- Quick Templates -->
56
- <div class="mb-2">
57
- <small class="metadata-label d-block mb-1">
58
- <i class="bi bi-lightning-fill"></i> Quick templates:
59
- </small>
60
- <div class="d-flex flex-wrap gap-1">
61
- <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('investigating')">
62
- <i class="bi bi-search"></i> Investigating
63
- </button>
64
- <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('found_fix')">
65
- <i class="bi bi-wrench"></i> Found Fix
66
- </button>
67
- <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('need_info')">
68
- <i class="bi bi-question-circle"></i> Need Info
69
- </button>
70
- <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('duplicate')">
71
- <i class="bi bi-files"></i> Duplicate
72
- </button>
73
- <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('cannot_reproduce')">
74
- <i class="bi bi-x-circle"></i> Cannot Reproduce
75
- </button>
76
- </div>
77
- </div>
78
-
79
- <%= text_area_tag :body, nil, class: "form-control", rows: 4, placeholder: "Share your thoughts, findings, or updates...", required: true, id: "comment_body" %>
6
+ <% if has_linked_issue && (platform_comments.any? || has_audit_comments) %>
7
+ <div class="card mb-4" id="section-discussion">
8
+ <div class="card-header bg-white d-flex justify-content-between align-items-center">
9
+ <h5 class="mb-0">
10
+ <i class="bi bi-chat-dots"></i> Discussion
11
+ <% if platform_comments.any? %>
12
+ <span class="badge bg-secondary ms-2"><%= platform_comments.size %></span>
13
+ <% end %>
14
+ </h5>
15
+ <a href="<%= error.external_issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
16
+ <i class="bi bi-box-arrow-up-right me-1"></i> Reply on <%= error.external_issue_provider&.capitalize || "Platform" %>
17
+ </a>
18
+ </div>
19
+ <div class="card-body p-0">
20
+ <% if platform_comments.any? %>
21
+ <div style="max-height: 400px; overflow-y: auto;">
22
+ <% platform_comments.each_with_index do |comment, index| %>
23
+ <div class="px-3 py-3 <%= index < platform_comments.size - 1 ? 'border-bottom' : '' %>">
24
+ <div class="d-flex align-items-start gap-2 mb-2">
25
+ <% if comment[:avatar_url].present? %>
26
+ <img src="<%= comment[:avatar_url] %>" alt="<%= comment[:author] %>" width="24" height="24" class="rounded-circle" style="flex-shrink: 0;">
27
+ <% else %>
28
+ <i class="bi bi-person-circle" style="font-size: 1.3em; flex-shrink: 0;"></i>
29
+ <% end %>
30
+ <div>
31
+ <strong class="text-primary"><%= comment[:author] || "Unknown" %></strong>
32
+ <small class="text-muted ms-2"><%= comment[:created_at] ? Time.parse(comment[:created_at]).strftime("%b %d, %Y %H:%M") : "" %></small>
80
33
  </div>
81
- <%= submit_tag "Post Comment", class: "btn btn-primary" %>
82
- <% end %>
34
+ </div>
35
+ <div class="ms-4 text-break" style="font-size: 0.9em;">
36
+ <%= simple_format(h(comment[:body].to_s.truncate(500)), {}, wrapper_tag: "span") %>
37
+ </div>
83
38
  </div>
39
+ <% end %>
40
+ </div>
41
+ <% end %>
84
42
 
85
- <script>
86
- function insertTemplate(templateType) {
87
- const textarea = document.getElementById('comment_body');
88
- const templates = {
89
- investigating: "🔍 Investigating this issue now. Will update with findings.",
90
- found_fix: "✅ Found the fix!\n\nRoot cause: \nSolution: \nPR: ",
91
- need_info: "ℹ️ Need more information:\n\n- \n- \n\nPlease provide details to help debug this issue.",
92
- duplicate: "📋 This appears to be a duplicate of error #\n\nClosing as duplicate.",
93
- cannot_reproduce: "❌ Cannot reproduce this issue.\n\nAttempted:\n- \n- \n\nNeed more details or steps to reproduce."
94
- };
95
-
96
- const template = templates[templateType];
97
- if (template) {
98
- textarea.value = template;
99
- textarea.focus();
100
- // Move cursor to end
101
- textarea.setSelectionRange(textarea.value.length, textarea.value.length);
102
- }
103
- }
104
- </script>
43
+ <% if has_audit_comments %>
44
+ <!-- Audit trail — workflow actions -->
45
+ <div class="px-3 py-2 bg-light border-top">
46
+ <small class="text-muted fw-bold"><i class="bi bi-clock-history"></i> Activity Log</small>
47
+ </div>
48
+ <% error.comments.recent_first.each_with_index do |comment, index| %>
49
+ <div class="px-3 py-2 <%= index < error.comments.count - 1 ? 'border-bottom' : '' %>">
50
+ <div class="d-flex justify-content-between align-items-start">
51
+ <div>
52
+ <small class="text-primary fw-bold"><i class="bi bi-person-circle"></i> <%= comment.author_name %></small>
53
+ <small class="text-muted ms-2"><%= comment.formatted_time %></small>
54
+ </div>
55
+ </div>
56
+ <small class="text-muted text-break"><%= simple_format(h(comment.body), {}, wrapper_tag: "span") %></small>
57
+ </div>
58
+ <% end %>
59
+ <% end %>
60
+ </div>
61
+ </div>
62
+ <% elsif has_linked_issue %>
63
+ <!-- Issue linked but no comments yet -->
64
+ <div class="card mb-4" id="section-discussion">
65
+ <div class="card-header bg-white d-flex justify-content-between align-items-center">
66
+ <h5 class="mb-0">
67
+ <i class="bi bi-chat-dots"></i> Discussion
68
+ </h5>
69
+ <a href="<%= error.external_issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
70
+ <i class="bi bi-box-arrow-up-right me-1"></i> Start discussion on <%= error.external_issue_provider&.capitalize || "Platform" %>
71
+ </a>
72
+ </div>
73
+ <div class="card-body text-center py-3">
74
+ <p class="text-muted small mb-0">No comments yet. Be the first to discuss this error on <%= error.external_issue_provider&.capitalize || "the platform" %>.</p>
75
+ </div>
76
+ </div>
77
+ <% elsif has_audit_comments %>
78
+ <!-- No issue linked, but has audit trail -->
79
+ <div class="card mb-4" id="section-discussion">
80
+ <div class="card-header bg-white">
81
+ <h5 class="mb-0">
82
+ <i class="bi bi-clock-history"></i> Activity Log
83
+ </h5>
84
+ </div>
85
+ <div class="card-body p-0">
86
+ <% error.comments.recent_first.each_with_index do |comment, index| %>
87
+ <div class="px-3 py-2 <%= index < error.comments.count - 1 ? 'border-bottom' : '' %>">
88
+ <div class="d-flex justify-content-between align-items-start">
89
+ <div>
90
+ <small class="text-primary fw-bold"><i class="bi bi-person-circle"></i> <%= comment.author_name %></small>
91
+ <small class="text-muted ms-2"><%= comment.formatted_time %></small>
92
+ </div>
105
93
  </div>
94
+ <small class="text-muted text-break"><%= simple_format(h(comment.body), {}, wrapper_tag: "span") %></small>
106
95
  </div>
107
96
  <% end %>
97
+ </div>
98
+ </div>
99
+ <% end %>
@@ -0,0 +1,121 @@
1
+ <% if RailsErrorDashboard.configuration.enable_issue_tracking %>
2
+ <div class="card mb-4" id="issue-tracking">
3
+ <div class="card-header bg-white d-flex justify-content-between align-items-center">
4
+ <h5 class="mb-0">
5
+ <i class="bi bi-link-45deg me-2"></i>
6
+ Issue Tracker
7
+ </h5>
8
+ <% if @error.external_issue_url.present? %>
9
+ <a href="<%= @error.external_issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
10
+ <i class="bi bi-box-arrow-up-right me-1"></i> View Issue
11
+ </a>
12
+ <% end %>
13
+ </div>
14
+ <div class="card-body">
15
+ <% if @error.external_issue_url.present? %>
16
+ <% provider = @error.external_issue_provider %>
17
+ <% icon = case provider
18
+ when "github" then "bi-github"
19
+ when "gitlab" then "bi-gitlab"
20
+ when "codeberg" then "bi-git"
21
+ else "bi-link-45deg"
22
+ end %>
23
+ <% issue = @platform_issue %>
24
+
25
+ <!-- Issue badge + status -->
26
+ <div class="d-flex align-items-center gap-2 mb-3">
27
+ <span class="badge bg-success">
28
+ <i class="bi <%= icon %> me-1"></i>
29
+ <%= provider&.capitalize || "Linked" %> #<%= @error.external_issue_number || "?" %>
30
+ </span>
31
+ <% if issue %>
32
+ <% state_color = issue[:state] == "open" ? "success" : "secondary" %>
33
+ <% state_icon = issue[:state] == "open" ? "bi-circle-fill" : "bi-check-circle-fill" %>
34
+ <span class="badge bg-<%= state_color %>">
35
+ <i class="bi <%= state_icon %> me-1" style="font-size: 0.6em;"></i>
36
+ <%= issue[:state]&.capitalize %>
37
+ </span>
38
+ <% end %>
39
+ </div>
40
+
41
+ <% if issue %>
42
+ <div class="row">
43
+ <!-- Assignees -->
44
+ <div class="col-md-6 mb-2">
45
+ <small class="text-muted d-block mb-1">Assignees</small>
46
+ <% if issue[:assignees].present? && issue[:assignees].any? %>
47
+ <div class="d-flex align-items-center gap-2 flex-wrap">
48
+ <% issue[:assignees].each do |assignee| %>
49
+ <span class="d-inline-flex align-items-center gap-1 border rounded-pill px-2 py-1" style="font-size: 0.85em;">
50
+ <% if assignee[:avatar_url].present? %>
51
+ <img src="<%= assignee[:avatar_url] %>" width="18" height="18" class="rounded-circle">
52
+ <% end %>
53
+ <%= assignee[:login] %>
54
+ </span>
55
+ <% end %>
56
+ </div>
57
+ <% else %>
58
+ <span class="text-muted" style="font-size: 0.85em;">Unassigned</span>
59
+ <% end %>
60
+ </div>
61
+
62
+ <!-- Labels -->
63
+ <div class="col-md-6 mb-2">
64
+ <small class="text-muted d-block mb-1">Labels</small>
65
+ <% if issue[:labels].present? && issue[:labels].any? %>
66
+ <div class="d-flex align-items-center gap-1 flex-wrap">
67
+ <% issue[:labels].each do |label| %>
68
+ <% bg_color = label[:color].present? ? "##{label[:color]}" : "#6c757d" %>
69
+ <% text_color = label[:color].present? && label[:color].scan(/../).map { |c| c.to_i(16) }.sum > 382 ? "#000" : "#fff" %>
70
+ <span class="badge rounded-pill" style="background-color: <%= bg_color %>; color: <%= text_color %>; font-size: 0.78em;">
71
+ <%= label[:name] %>
72
+ </span>
73
+ <% end %>
74
+ </div>
75
+ <% else %>
76
+ <span class="text-muted" style="font-size: 0.85em;">No labels</span>
77
+ <% end %>
78
+ </div>
79
+ </div>
80
+ <% end %>
81
+
82
+ <% else %>
83
+ <!-- No linked issue — show create/link options -->
84
+ <div class="row">
85
+ <div class="col-md-6">
86
+ <h6 class="text-muted mb-2">Create New Issue</h6>
87
+ <% provider = RailsErrorDashboard.configuration.effective_issue_tracker_provider %>
88
+ <% if provider && RailsErrorDashboard.configuration.effective_issue_tracker_token %>
89
+ <%= button_to create_issue_error_path(@error), method: :post, class: "btn btn-sm btn-primary" do %>
90
+ <i class="bi bi-plus-circle me-1"></i>
91
+ Create <%= provider.to_s.capitalize %> Issue
92
+ <% end %>
93
+ <% else %>
94
+ <p class="text-muted small mb-0">
95
+ <i class="bi bi-info-circle"></i>
96
+ Configure <code>issue_tracker_token</code> and <code>git_repository_url</code> in your initializer to enable issue creation.
97
+ </p>
98
+ <% end %>
99
+ </div>
100
+ <div class="col-md-6">
101
+ <h6 class="text-muted mb-2">Link Existing Issue</h6>
102
+ <%= form_tag link_issue_error_path(@error), method: :post, class: "d-flex gap-2" do %>
103
+ <%= text_field_tag :issue_url, nil, placeholder: "https://github.com/user/repo/issues/42", class: "form-control form-control-sm", required: true %>
104
+ <button type="submit" class="btn btn-sm btn-outline-secondary text-nowrap">
105
+ <i class="bi bi-link"></i> Link
106
+ </button>
107
+ <% end %>
108
+ </div>
109
+ </div>
110
+ <% end %>
111
+ </div>
112
+ </div>
113
+
114
+ <% if flash[:new_issue_url].present? %>
115
+ <script>
116
+ (function() {
117
+ window.open('<%= j flash[:new_issue_url] %>', '_blank');
118
+ })();
119
+ </script>
120
+ <% end %>
121
+ <% end %>
@@ -66,6 +66,7 @@
66
66
  { id: 'section-similar-errors', icon: 'bi-diagram-3', label: 'Similar' },
67
67
  { id: 'section-co-occurring', icon: 'bi-intersect', label: 'Co-occurring' },
68
68
  { id: 'section-timeline', icon: 'bi-clock-history', label: 'Timeline' },
69
+ { id: 'issue-tracking', icon: 'bi-link-45deg', label: 'Issue' },
69
70
  { id: 'section-discussion', icon: 'bi-chat-dots', label: 'Discussion' },
70
71
  { id: 'section-error-cascades', icon: 'bi-share', label: 'Cascades' },
71
72
  { id: 'section-occurrence-patterns', icon: 'bi-graph-up', label: 'Patterns' }
@@ -71,88 +71,92 @@
71
71
  <% end %>
72
72
  </div>
73
73
 
74
- <!-- Phase 3: Workflow Status -->
75
- <div class="mb-3">
76
- <small class="metadata-label d-block mb-1">Workflow Status</small>
77
- <% if error.respond_to?(:status) %>
78
- <% badge_color = error.status_badge_color %>
79
- <% text_dark = %w[info warning light].include?(badge_color) ? 'text-dark' : '' %>
80
- <span class="badge bg-<%= badge_color %> <%= text_dark %> fs-6">
81
- <%= error.status.titleize %>
82
- </span>
83
- <% elsif error.resolved? %>
84
- <span class="badge bg-success">
85
- <i class="bi bi-check-circle"></i> Resolved
86
- </span>
87
- <% if error.resolved_at.present? %>
74
+ <% unless RailsErrorDashboard.configuration.enable_issue_tracking %>
75
+ <!-- Workflow Status (hidden when issue tracking enabled — platform is source of truth) -->
76
+ <div class="mb-3">
77
+ <small class="metadata-label d-block mb-1">Workflow Status</small>
78
+ <% if error.respond_to?(:status) %>
79
+ <% badge_color = error.status_badge_color %>
80
+ <% text_dark = %w[info warning light].include?(badge_color) ? 'text-dark' : '' %>
81
+ <span class="badge bg-<%= badge_color %> <%= text_dark %> fs-6">
82
+ <%= error.status.titleize %>
83
+ </span>
84
+ <% elsif error.resolved? %>
85
+ <span class="badge bg-success">
86
+ <i class="bi bi-check-circle"></i> Resolved
87
+ </span>
88
+ <% if error.resolved_at.present? %>
89
+ <br>
90
+ <small class="text-muted mt-1 d-block">
91
+ <%= local_time(error.resolved_at, format: :full) %>
92
+ </small>
93
+ <% end %>
94
+ <% else %>
95
+ <span class="badge bg-danger">
96
+ <i class="bi bi-exclamation-circle"></i> Unresolved
97
+ </span>
98
+ <% end %>
99
+ </div>
100
+
101
+ <!-- Reopened indicator -->
102
+ <% if error.reopened? %>
103
+ <div class="mb-3">
104
+ <small class="metadata-label d-block mb-1">Reopened</small>
105
+ <span class="badge bg-warning text-dark">
106
+ <i class="bi bi-arrow-counterclockwise"></i> Reopened
107
+ </span>
88
108
  <br>
89
109
  <small class="text-muted mt-1 d-block">
90
- <%= local_time(error.resolved_at, format: :full) %>
110
+ <%= local_time(error.reopened_at, format: :full) %>
91
111
  </small>
92
- <% end %>
93
- <% else %>
94
- <span class="badge bg-danger">
95
- <i class="bi bi-exclamation-circle"></i> Unresolved
96
- </span>
112
+ </div>
97
113
  <% end %>
98
- </div>
99
114
 
100
- <!-- Reopened indicator -->
101
- <% if error.reopened? %>
102
- <div class="mb-3">
103
- <small class="metadata-label d-block mb-1">Reopened</small>
104
- <span class="badge bg-warning text-dark">
105
- <i class="bi bi-arrow-counterclockwise"></i> Reopened
106
- </span>
107
- <br>
108
- <small class="text-muted mt-1 d-block">
109
- <%= local_time(error.reopened_at, format: :full) %>
110
- </small>
111
- </div>
112
- <% end %>
113
-
114
- <!-- Phase 3: Assignment -->
115
- <% if error.respond_to?(:assigned_to) %>
116
- <div class="mb-3">
117
- <small class="metadata-label d-block mb-1">Assigned To</small>
118
- <% if error.assigned? %>
119
- <div class="d-flex align-items-center justify-content-between">
120
- <div>
121
- <i class="bi bi-person-fill text-primary"></i>
122
- <strong><%= error.assigned_to %></strong>
123
- <% if error.assigned_at.present? %>
124
- <br>
125
- <small class="text-muted">
126
- <%= local_time_ago(error.assigned_at) %>
127
- </small>
115
+ <!-- Assignment -->
116
+ <% if error.respond_to?(:assigned_to) %>
117
+ <div class="mb-3">
118
+ <small class="metadata-label d-block mb-1">Assigned To</small>
119
+ <% if error.assigned? %>
120
+ <div class="d-flex align-items-center justify-content-between">
121
+ <div>
122
+ <i class="bi bi-person-fill text-primary"></i>
123
+ <strong><%= error.assigned_to %></strong>
124
+ <% if error.assigned_at.present? %>
125
+ <br>
126
+ <small class="text-muted">
127
+ <%= local_time_ago(error.assigned_at) %>
128
+ </small>
129
+ <% end %>
130
+ </div>
131
+ <%= button_to unassign_error_path(error), method: :post, class: "btn btn-sm btn-outline-secondary",
132
+ data: { turbo_confirm: "Remove assignment?" } do %>
133
+ <i class="bi bi-x"></i>
128
134
  <% end %>
129
135
  </div>
130
- <%= button_to unassign_error_path(error), method: :post, class: "btn btn-sm btn-outline-secondary",
131
- data: { turbo_confirm: "Remove assignment?" } do %>
132
- <i class="bi bi-x"></i>
133
- <% end %>
134
- </div>
135
- <% else %>
136
- <button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#assignModal">
137
- <i class="bi bi-person-plus"></i> Assign
138
- </button>
139
- <% end %>
140
- </div>
136
+ <% else %>
137
+ <button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#assignModal">
138
+ <i class="bi bi-person-plus"></i> Assign
139
+ </button>
140
+ <% end %>
141
+ </div>
141
142
 
142
- <!-- Phase 3: Priority -->
143
- <div class="mb-3">
144
- <small class="metadata-label d-block mb-1">Priority</small>
145
- <% if error.respond_to?(:priority_level) %>
146
- <span class="badge bg-<%= error.priority_color %> fs-6">
147
- <%= error.priority_label %>
148
- </span>
149
- <button type="button" class="btn btn-sm btn-outline-secondary ms-2" data-bs-toggle="modal" data-bs-target="#priorityModal">
150
- <i class="bi bi-pencil"></i>
151
- </button>
152
- <% end %>
153
- </div>
143
+ <!-- Priority -->
144
+ <div class="mb-3">
145
+ <small class="metadata-label d-block mb-1">Priority</small>
146
+ <% if error.respond_to?(:priority_level) %>
147
+ <span class="badge bg-<%= error.priority_color %> fs-6">
148
+ <%= error.priority_label %>
149
+ </span>
150
+ <button type="button" class="btn btn-sm btn-outline-secondary ms-2" data-bs-toggle="modal" data-bs-target="#priorityModal">
151
+ <i class="bi bi-pencil"></i>
152
+ </button>
153
+ <% end %>
154
+ </div>
155
+ <% end %>
156
+ <% end %>
154
157
 
155
- <!-- Phase 3: Snooze -->
158
+ <!-- Snooze (always visible — no platform equivalent) -->
159
+ <% if error.respond_to?(:assigned_to) %>
156
160
  <div class="mb-3">
157
161
  <small class="metadata-label d-block mb-1">Snooze</small>
158
162
  <% if error.respond_to?(:snoozed?) && error.snoozed? %>