rails_error_dashboard 0.5.8 → 0.5.10

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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -8
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +51 -17
  4. data/app/controllers/rails_error_dashboard/webhooks_controller.rb +2 -1
  5. data/app/views/layouts/rails_error_dashboard.html.erb +5 -0
  6. data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +1 -1
  7. data/app/views/rails_error_dashboard/errors/_discussion.html.erb +76 -43
  8. data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +78 -24
  9. data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +1 -0
  10. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +88 -71
  11. data/app/views/rails_error_dashboard/errors/releases.html.erb +284 -0
  12. data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +15 -0
  13. data/app/views/rails_error_dashboard/errors/settings.html.erb +32 -0
  14. data/app/views/rails_error_dashboard/errors/show.html.erb +10 -8
  15. data/config/routes.rb +1 -1
  16. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +32 -2
  17. data/lib/rails_error_dashboard/configuration.rb +10 -13
  18. data/lib/rails_error_dashboard/queries/release_timeline.rb +181 -0
  19. data/lib/rails_error_dashboard/services/codeberg_issue_client.rb +23 -0
  20. data/lib/rails_error_dashboard/services/github_issue_client.rb +23 -0
  21. data/lib/rails_error_dashboard/services/gitlab_issue_client.rb +23 -0
  22. data/lib/rails_error_dashboard/services/issue_tracker_client.rb +6 -0
  23. data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +4 -6
  24. data/lib/rails_error_dashboard/version.rb +1 -1
  25. data/lib/rails_error_dashboard.rb +1 -0
  26. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc2ad274b6cf17149a3d60eecb3c6cde4280379e8c74c870be26530641267f8b
4
- data.tar.gz: b8bb6d9e448631bb294663b73d33379c7da33e0cba5ab97d79949b1860070cc3
3
+ metadata.gz: f3004ee5b49b622c064ceb8103c6abb2212f1a0cc269471a7281b49338213240
4
+ data.tar.gz: a8e27db79b78052909acc3ce04ab4875b31148b6e50b631b46bfe67eb0562fd3
5
5
  SHA512:
6
- metadata.gz: 2f1b87689dc7d3be38a036ead3d2e63236265ebe6a7135503e6cff3231a97f87b5203bc9f6b8da86e23735401f26616688adbe43bf2e225f6f3fce6cd808f0d3
7
- data.tar.gz: da68e7b4b68b14f9a8dd349f7c5ccd0704844a4da2d77f97556e9cf3ebda1577d59ba54acb844a3d8de59704c16bf494065e6d23d83abfee07b6ca1a28b7afd8
6
+ metadata.gz: c2dfe54a84c8a7049e6b51956d3512c6c18f8d37cabdd5f655bbdb046b6ed7ce2b287886f80a36a7bba85839090309eb81e823e29b5b766036f9b7ba726f7fef
7
+ data.tar.gz: fdcf6aea6b7f568da4a525330e63de6121aa087926ffd8982c631ec90efa5282081d428c7fd9069b4ed923f31a1c1445483c443f4b845d4677648b10c4d56fc4
data/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
  [![Downloads](https://img.shields.io/gem/dt/rails_error_dashboard)](https://rubygems.org/gems/rails_error_dashboard)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Tests](https://github.com/AnjanJ/rails_error_dashboard/workflows/Tests/badge.svg)](https://github.com/AnjanJ/rails_error_dashboard/actions)
7
+ [![Sponsor](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-ea4aaa?logo=githubsponsors)](https://github.com/sponsors/AnjanJ)
7
8
  [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow?logo=buymeacoffee)](https://buymeacoffee.com/anjanj)
8
9
 
9
10
  **Self-hosted Rails error monitoring — free, forever.**
@@ -149,23 +150,43 @@ config.enable_activestorage_tracking = true # requires enable_breadcrumbs = tru
149
150
  <details>
150
151
  <summary><strong>Issue Tracking — GitHub, GitLab, Codeberg</strong></summary>
151
152
 
152
- Create, link, and manage issues directly from error detail pages. Supports GitHub, GitLab, and Codeberg/Gitea/Forgejo with provider auto-detection.
153
+ One switch connects errors to your issue tracker. Platform becomes the source of truth — status, assignees, labels, and comments are mirrored live in the dashboard.
153
154
 
154
- - **Manual:** "Create Issue" button + "Link Existing Issue" URL input
155
- - **Auto-create:** On first occurrence and/or severity threshold — configurable
156
- - **Lifecycle sync:** Resolve → close issue, recur → reopen + comment (throttled)
157
- - **Two-way webhooks:** Issue closed/reopened on platform syncs to dashboard
155
+ - **Create & link:** "Create Issue" button or paste an existing URL
156
+ - **Auto-create:** New errors auto-create issues. Critical/high severity always creates
157
+ - **Lifecycle sync:** Resolve → close, recur → reopen + comment, all via background jobs
158
+ - **Platform mirror:** Issue state, assignees (with avatars), labels (with colors), and comments displayed in the dashboard. Workflow controls (Resolve, Assign, Priority) replaced by platform state
159
+ - **Two-way webhooks:** Issue closed/reopened on platform syncs back to dashboard
158
160
  - **RED branding:** Issues show "Created by RED (Rails Error Dashboard)"
159
161
 
160
162
  ```ruby
161
163
  config.enable_issue_tracking = true
162
164
  config.issue_tracker_token = ENV["RED_BOT_TOKEN"]
163
- # Provider and repo auto-detected from git_repository_url
165
+ # That's it — provider and repo auto-detected from git_repository_url
164
166
  ```
165
167
 
166
168
  [Complete documentation →](docs/guides/CONFIGURATION.md)
167
169
  </details>
168
170
 
171
+ <details>
172
+ <summary><strong>Release Tracking</strong></summary>
173
+
174
+ Dedicated Releases page at `/errors/releases` shows a timeline of all deploys/versions with health stats. Answers: "Did this deploy introduce new errors?" and "Is this release stable?"
175
+
176
+ - **Release timeline:** Every version seen, sorted newest-first, with error counts, unique types, and time range
177
+ - **"New in this release":** Errors whose fingerprint first appeared in each version — flagged with a red badge
178
+ - **Stability indicators:** Green (at or below average), yellow (1-2x), red (>2x average error rate)
179
+ - **Release comparison:** Delta and percentage change vs the previous release
180
+ - **Current release:** Highlighted card with live health stats
181
+ - **Zero config:** Works automatically when `app_version` or `git_sha` is set (via config, `APP_VERSION`, `GIT_SHA`, `HEROKU_SLUG_COMMIT`, or `RENDER_GIT_COMMIT` env vars)
182
+
183
+ ```ruby
184
+ config.app_version = "1.2.0" # or set APP_VERSION env var
185
+ config.git_sha = ENV["GIT_SHA"] # auto-detected on Heroku/Render
186
+ config.git_repository_url = "https://github.com/user/repo" # enables SHA links
187
+ ```
188
+ </details>
189
+
169
190
  <details>
170
191
  <summary><strong>Source Code Integration + Git Blame</strong></summary>
171
192
 
@@ -479,9 +500,9 @@ Special thanks to [@bonniesimon](https://github.com/bonniesimon), [@gundestrup](
479
500
 
480
501
  ## Support
481
502
 
482
- If this gem saves you some headaches (or some money on error tracking SaaS), consider buying me a coffee. It keeps the project going and lets me know people are finding it useful.
503
+ If this gem saves you some headaches (or some money on error tracking SaaS), consider sponsoring the project. It keeps RED going and lets me know people are finding it useful.
483
504
 
484
- <a href="https://www.buymeacoffee.com/anjanj" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="200"></a>
505
+ <a href="https://github.com/sponsors/AnjanJ" target="_blank"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-ea4aaa?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Sponsor on GitHub"></a>&nbsp;&nbsp;<a href="https://www.buymeacoffee.com/anjanj" target="_blank"><img src="https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buymeacoffee&logoColor=black" alt="Buy Me A Coffee"></a>
485
506
 
486
507
  ---
487
508
 
@@ -81,6 +81,10 @@ module RailsErrorDashboard
81
81
  @related_errors = @error.related_errors(limit: 5, application_id: @current_application_id)
82
82
  @error_markdown = Services::MarkdownErrorFormatter.call(@error, related_errors: @related_errors)
83
83
 
84
+ # Fetch platform issue state and comments if linked
85
+ @platform_issue = fetch_platform_issue(@error)
86
+ @platform_comments = fetch_platform_comments(@error)
87
+
84
88
  # Dispatch plugin event for error viewed
85
89
  RailsErrorDashboard::PluginRegistry.dispatch(:on_error_viewed, @error)
86
90
  end
@@ -144,11 +148,12 @@ module RailsErrorDashboard
144
148
  result = Commands::CreateIssue.call(params[:id], dashboard_url: dashboard_url)
145
149
 
146
150
  if result[:success]
147
- flash[:notice] = "Issue created: #{result[:issue_url]}"
151
+ flash[:notice] = "Issue created successfully"
152
+ flash[:new_issue_url] = result[:issue_url]
148
153
  else
149
154
  flash[:alert] = "Failed to create issue: #{result[:error]}"
150
155
  end
151
- redirect_to error_path(params[:id])
156
+ redirect_to error_path(params[:id], anchor: "issue-tracking")
152
157
  end
153
158
 
154
159
  def link_issue
@@ -159,21 +164,7 @@ module RailsErrorDashboard
159
164
  else
160
165
  flash[:alert] = "Failed to link issue: #{result[:error]}"
161
166
  end
162
- redirect_to error_path(params[:id])
163
- end
164
-
165
- def unlink_issue
166
- error = ErrorLog.find(params[:id])
167
- error.update!(
168
- external_issue_url: nil,
169
- external_issue_number: nil,
170
- external_issue_provider: nil
171
- )
172
- flash[:notice] = "Issue unlinked"
173
- redirect_to error_path(error)
174
- rescue => e
175
- flash[:alert] = "Failed to unlink issue: #{e.message}"
176
- redirect_to error_path(params[:id])
167
+ redirect_to error_path(params[:id], anchor: "issue-tracking")
177
168
  end
178
169
 
179
170
  def analytics
@@ -285,6 +276,16 @@ module RailsErrorDashboard
285
276
  @platform_specific_errors = correlation.platform_specific_errors
286
277
  end
287
278
 
279
+ def releases
280
+ days = (params[:days] || 30).to_i
281
+ @days = days
282
+ result = Queries::ReleaseTimeline.call(days, application_id: @current_application_id)
283
+ all_releases = result[:releases]
284
+ @summary = result[:summary]
285
+
286
+ @pagy, @releases = pagy(:offset, all_releases, limit: params[:per_page] || 25)
287
+ end
288
+
288
289
  def deprecations
289
290
  unless RailsErrorDashboard.configuration.enable_breadcrumbs
290
291
  flash[:alert] = "Breadcrumbs are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
@@ -563,6 +564,39 @@ module RailsErrorDashboard
563
564
  @applications = Application.ordered_by_name.pluck(:name, :id)
564
565
  end
565
566
 
567
+ def fetch_platform_issue(error)
568
+ return nil unless error.external_issue_url.present? && error.external_issue_number.present?
569
+ return nil unless RailsErrorDashboard.configuration.enable_issue_tracking
570
+
571
+ cache_key = "red/issue_state/#{error.external_issue_provider}/#{error.external_issue_number}"
572
+ Rails.cache.fetch(cache_key, expires_in: 60.seconds) do
573
+ client = Services::IssueTrackerClient.from_config
574
+ return nil unless client
575
+
576
+ result = client.fetch_issue(number: error.external_issue_number)
577
+ result[:success] ? result : nil
578
+ end
579
+ rescue => e
580
+ nil
581
+ end
582
+
583
+ def fetch_platform_comments(error)
584
+ return [] unless error.external_issue_url.present? && error.external_issue_number.present?
585
+ return [] unless RailsErrorDashboard.configuration.enable_issue_tracking
586
+
587
+ # Cache for 60 seconds to avoid API hammering on page refreshes
588
+ cache_key = "red/issue_comments/#{error.external_issue_provider}/#{error.external_issue_number}"
589
+ Rails.cache.fetch(cache_key, expires_in: 60.seconds) do
590
+ client = Services::IssueTrackerClient.from_config
591
+ return [] unless client
592
+
593
+ result = client.fetch_comments(number: error.external_issue_number, per_page: 20)
594
+ result[:success] ? result[:comments] : []
595
+ end
596
+ rescue => e
597
+ []
598
+ end
599
+
566
600
  def check_default_credentials
567
601
  @default_credentials_warning = RailsErrorDashboard.configuration.default_credentials?
568
602
  end
@@ -43,7 +43,8 @@ module RailsErrorDashboard
43
43
  private
44
44
 
45
45
  def verify_webhook_enabled
46
- unless RailsErrorDashboard.configuration.enable_issue_webhooks
46
+ config = RailsErrorDashboard.configuration
47
+ unless config.enable_issue_tracking && config.issue_webhook_secret.present?
47
48
  head :not_found
48
49
  end
49
50
  end
@@ -1651,6 +1651,11 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1651
1651
  <i class="bi bi-diagram-3"></i> Correlation
1652
1652
  <% end %>
1653
1653
  </li>
1654
+ <li class="nav-item">
1655
+ <%= link_to releases_errors_path(nav_params), class: "nav-link #{request.path == releases_errors_path ? 'active' : ''}" do %>
1656
+ <i class="bi bi-rocket-takeoff"></i> Releases
1657
+ <% end %>
1658
+ </li>
1654
1659
  <li class="nav-item">
1655
1660
  <%= link_to settings_path(nav_params), class: "nav-link #{request.path == settings_path ? 'active' : ''}" do %>
1656
1661
  <i class="bi bi-gear"></i> Settings
@@ -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,66 +1,99 @@
1
1
  <% has_linked_issue = error.respond_to?(:external_issue_url) && error.external_issue_url.present? %>
2
- <% has_comments = error.respond_to?(:comments) && error.comments.any? %>
2
+ <% has_audit_comments = error.respond_to?(:comments) && error.comments.any? %>
3
+ <% platform_comments = @platform_comments || [] %>
3
4
  <% issue_tracking_enabled = RailsErrorDashboard.configuration.enable_issue_tracking %>
4
5
 
5
- <% if has_linked_issue || has_comments || issue_tracking_enabled %>
6
+ <% if has_linked_issue && (platform_comments.any? || has_audit_comments) %>
6
7
  <div class="card mb-4" id="section-discussion">
7
- <div class="card-header bg-white">
8
+ <div class="card-header bg-white d-flex justify-content-between align-items-center">
8
9
  <h5 class="mb-0">
9
10
  <i class="bi bi-chat-dots"></i> Discussion
10
- <% if has_comments %>
11
- <span class="badge bg-secondary ms-2"><%= error.comments.count %></span>
11
+ <% if platform_comments.any? %>
12
+ <span class="badge bg-secondary ms-2"><%= platform_comments.size %></span>
12
13
  <% end %>
13
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>
14
18
  </div>
15
- <div class="card-body">
16
- <% if has_linked_issue %>
17
- <!-- Platform discussion link -->
18
- <% provider = error.external_issue_provider&.capitalize || "Platform" %>
19
- <% icon = case error.external_issue_provider
20
- when "github" then "bi-github"
21
- when "gitlab" then "bi-gitlab"
22
- when "codeberg" then "bi-git"
23
- else "bi-chat-dots"
24
- end %>
25
- <div class="text-center py-3 mb-3">
26
- <a href="<%= error.external_issue_url %>" target="_blank" rel="noopener" class="btn btn-primary">
27
- <i class="bi <%= icon %> me-1"></i>
28
- Discuss on <%= provider %> #<%= error.external_issue_number || "?" %>
29
- <i class="bi bi-box-arrow-up-right ms-1" style="font-size: 0.8em;"></i>
30
- </a>
31
- </div>
32
- <% elsif issue_tracking_enabled %>
33
- <div class="text-center py-3 mb-3">
34
- <p class="text-muted small mb-0">
35
- <i class="bi bi-link-45deg"></i>
36
- Link or create an issue above to start a discussion on your issue tracker.
37
- </p>
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>
33
+ </div>
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>
38
+ </div>
39
+ <% end %>
38
40
  </div>
39
41
  <% end %>
40
42
 
41
- <% if has_comments %>
42
- <!-- Audit trail — read-only workflow comments (snooze, mute, status changes) -->
43
- <% if has_linked_issue || issue_tracking_enabled %>
44
- <hr class="my-3">
45
- <h6 class="text-muted mb-3"><i class="bi bi-clock-history"></i> Activity Log</h6>
46
- <% end %>
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>
47
48
  <% error.comments.recent_first.each_with_index do |comment, index| %>
48
- <div class="<%= index < error.comments.count - 1 ? 'border-bottom' : '' %> pb-3 mb-3">
49
- <div class="d-flex justify-content-between align-items-start mb-2">
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">
50
51
  <div>
51
- <strong class="text-primary">
52
- <i class="bi bi-person-circle"></i> <%= comment.author_name %>
53
- </strong>
52
+ <small class="text-primary fw-bold"><i class="bi bi-person-circle"></i> <%= comment.author_name %></small>
54
53
  <small class="text-muted ms-2"><%= comment.formatted_time %></small>
55
- <% if comment.recent? %>
56
- <span class="badge bg-info ms-1">New</span>
57
- <% end %>
58
54
  </div>
59
55
  </div>
60
- <p class="mb-0 text-break"><%= simple_format(h(comment.body), {}, wrapper_tag: "span") %></p>
56
+ <small class="text-muted text-break"><%= simple_format(h(comment.body), {}, wrapper_tag: "span") %></small>
61
57
  </div>
62
58
  <% end %>
63
59
  <% end %>
64
60
  </div>
65
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>
93
+ </div>
94
+ <small class="text-muted text-break"><%= simple_format(h(comment.body), {}, wrapper_tag: "span") %></small>
95
+ </div>
96
+ <% end %>
97
+ </div>
98
+ </div>
66
99
  <% end %>
@@ -1,38 +1,84 @@
1
1
  <% if RailsErrorDashboard.configuration.enable_issue_tracking %>
2
2
  <div class="card mb-4" id="issue-tracking">
3
- <div class="card-header bg-white">
3
+ <div class="card-header bg-white d-flex justify-content-between align-items-center">
4
4
  <h5 class="mb-0">
5
5
  <i class="bi bi-link-45deg me-2"></i>
6
6
  Issue Tracker
7
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 %>
8
13
  </div>
9
14
  <div class="card-body">
10
15
  <% if @error.external_issue_url.present? %>
11
- <!-- Linked issue display -->
12
- <div class="d-flex align-items-center justify-content-between">
13
- <div>
14
- <% provider = @error.external_issue_provider %>
15
- <% icon = case provider
16
- when "github" then "bi-github"
17
- when "gitlab" then "bi-gitlab"
18
- when "codeberg" then "bi-git"
19
- else "bi-link-45deg"
20
- end %>
21
- <span class="badge bg-success me-2">
22
- <i class="bi <%= icon %> me-1"></i>
23
- <%= provider&.capitalize || "Linked" %> #<%= @error.external_issue_number || "?" %>
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 %>
24
37
  </span>
25
- <a href="<%= @error.external_issue_url %>" target="_blank" rel="noopener" class="text-decoration-none">
26
- View Issue <i class="bi bi-box-arrow-up-right" style="font-size: 0.8em;"></i>
27
- </a>
28
- </div>
29
- <div>
30
- <a href="<%= @error.external_issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary me-1">
31
- <i class="bi bi-chat-dots"></i> Discuss on <%= provider&.capitalize || "Platform" %>
32
- </a>
33
- <%= button_to "Unlink", unlink_issue_error_path(@error), method: :post, class: "btn btn-sm btn-outline-danger", data: { confirm: "Remove issue link?" } %>
34
- </div>
38
+ <% end %>
35
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
+
36
82
  <% else %>
37
83
  <!-- No linked issue — show create/link options -->
38
84
  <div class="row">
@@ -64,4 +110,12 @@
64
110
  <% end %>
65
111
  </div>
66
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 %>
67
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' }