findbug 0.3.4 → 0.4.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.
@@ -0,0 +1,127 @@
1
+ <h1>Alerts</h1>
2
+ <p class="page-description">Manage alert channels and send test notifications.</p>
3
+
4
+ <%# Summary stats %>
5
+ <div class="stats-grid" style="grid-template-columns: repeat(3, 1fr);">
6
+ <div class="stat-card">
7
+ <div class="stat-label">Total Channels</div>
8
+ <div class="stat-value" style="font-size: 1.5rem;"><%= @channels.size %></div>
9
+ <div class="stat-change"><%= @enabled_count %> enabled</div>
10
+ </div>
11
+
12
+ <div class="stat-card">
13
+ <div class="stat-label">Throttle Period</div>
14
+ <div class="stat-value" style="font-size: 1.5rem;"><%= Findbug.config.alerts.throttle_period %>s</div>
15
+ <div class="stat-change"><%= Findbug.config.alerts.throttle_period / 60 %> minutes between alerts per error</div>
16
+ </div>
17
+
18
+ <div class="stat-card">
19
+ <div class="stat-label">Alert Status</div>
20
+ <div style="margin-top: 0.5rem;">
21
+ <% if @enabled_count > 0 %>
22
+ <span class="status-dot success"></span>
23
+ <span class="text-sm">Active</span>
24
+ <% else %>
25
+ <span class="status-dot error"></span>
26
+ <span class="text-sm">No channels enabled</span>
27
+ <% end %>
28
+ </div>
29
+ <div class="stat-change">
30
+ <% if @enabled_count > 0 %>
31
+ Alerts will be sent when errors occur
32
+ <% else %>
33
+ Configure a channel below to start receiving alerts
34
+ <% end %>
35
+ </div>
36
+ </div>
37
+ </div>
38
+
39
+ <%# Add channel button %>
40
+ <div style="display: flex; justify-content: flex-end; margin-bottom: 1rem;">
41
+ <a href="<%= findbug.new_alert_path %>" class="btn btn-primary btn-sm">
42
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 0.375rem;">
43
+ <line x1="12" y1="5" x2="12" y2="19"/>
44
+ <line x1="5" y1="12" x2="19" y2="12"/>
45
+ </svg>
46
+ Add Alert Channel
47
+ </a>
48
+ </div>
49
+
50
+ <%# Channel list %>
51
+ <% if @channels.any? %>
52
+ <div class="card">
53
+ <table class="table">
54
+ <thead>
55
+ <tr>
56
+ <th>Channel</th>
57
+ <th>Config</th>
58
+ <th style="text-align: center;">Status</th>
59
+ <th style="text-align: right;">Actions</th>
60
+ </tr>
61
+ </thead>
62
+ <tbody>
63
+ <% @channels.each do |channel| %>
64
+ <tr>
65
+ <td>
66
+ <strong><%= channel.name %></strong>
67
+ <span class="badge badge-info" style="margin-left: 0.5rem;"><%= channel.display_type %></span>
68
+ </td>
69
+ <td class="text-muted text-sm font-mono">
70
+ <% masked = channel.masked_config %>
71
+ <%= masked.values.first if masked.any? %>
72
+ </td>
73
+ <td style="text-align: center;">
74
+ <%= form_tag findbug.toggle_alert_path(channel), method: :post, style: "display: inline;" do %>
75
+ <label class="toggle" title="<%= channel.enabled? ? 'Disable' : 'Enable' %>">
76
+ <input type="checkbox" onchange="this.form.submit()" <%= "checked" if channel.enabled? %>>
77
+ <span class="toggle-slider"></span>
78
+ </label>
79
+ <% end %>
80
+ </td>
81
+ <td>
82
+ <div style="display: flex; gap: 0.25rem; justify-content: flex-end; align-items: center;">
83
+ <%# Test %>
84
+ <% if channel.enabled? %>
85
+ <%= button_to findbug.test_alert_path(channel), method: :post, class: "btn-icon", title: "Send test alert" do %>
86
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
87
+ <path d="M22 2 11 13"/><path d="m22 2-7 20-4-9-9-4 20-7z"/>
88
+ </svg>
89
+ <% end %>
90
+ <% end %>
91
+ <%# Edit %>
92
+ <a href="<%= findbug.edit_alert_path(channel) %>" class="btn-icon" title="Edit">
93
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
94
+ <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
95
+ <path d="m15 5 4 4"/>
96
+ </svg>
97
+ </a>
98
+ <%# Delete %>
99
+ <%= button_to findbug.alert_path(channel), method: :delete, class: "btn-icon destructive", title: "Delete" do %>
100
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
101
+ <path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
102
+ <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
103
+ </svg>
104
+ <% end %>
105
+ </div>
106
+ </td>
107
+ </tr>
108
+ <% end %>
109
+ </tbody>
110
+ </table>
111
+ </div>
112
+ <% else %>
113
+ <div class="card">
114
+ <div class="empty-state">
115
+ <div class="empty-state-icon">
116
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
117
+ <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
118
+ <path d="M13.73 21a2 2 0 0 1-3.46 0"/>
119
+ </svg>
120
+ </div>
121
+ <p>No alert channels configured yet.</p>
122
+ <p style="margin-top: 0.75rem;">
123
+ <a href="<%= findbug.new_alert_path %>" class="btn btn-primary btn-sm">Add your first channel</a>
124
+ </p>
125
+ </div>
126
+ </div>
127
+ <% end %>
@@ -0,0 +1,154 @@
1
+ <h1>New Alert Channel</h1>
2
+ <p class="page-description">Configure a new alert channel to receive error notifications.</p>
3
+
4
+ <div class="card" style="max-width: 560px;">
5
+ <div class="card-content">
6
+ <%= form_tag findbug.alerts_path, method: :post do %>
7
+ <div style="display: flex; flex-direction: column; gap: 1.5rem;">
8
+
9
+ <%# Channel Type %>
10
+ <div class="form-group">
11
+ <label>Channel Type</label>
12
+ <%= select_tag "alert_channel[channel_type]",
13
+ options_for_select([
14
+ ["Select a channel type...", ""],
15
+ ["Email", "email"],
16
+ ["Slack", "slack"],
17
+ ["Discord", "discord"],
18
+ ["Webhook", "webhook"]
19
+ ], @channel.channel_type),
20
+ id: "channel_type_select",
21
+ onchange: "toggleConfigFields(this.value)" %>
22
+ </div>
23
+
24
+ <%# Name %>
25
+ <div class="form-group">
26
+ <label>Name</label>
27
+ <input type="text" name="alert_channel[name]" value="<%= @channel.name %>"
28
+ placeholder="e.g., Production Slack, Dev Team Email">
29
+ </div>
30
+
31
+ <%# Enabled %>
32
+ <label style="display: flex; align-items: center; gap: 0.75rem; cursor: pointer;">
33
+ <input type="hidden" name="alert_channel[enabled]" value="0">
34
+ <span class="toggle">
35
+ <input type="checkbox" name="alert_channel[enabled]" value="1" <%= "checked" if @channel.enabled? %>>
36
+ <span class="toggle-slider"></span>
37
+ </span>
38
+ <span>Enable this channel</span>
39
+ </label>
40
+
41
+ <div class="separator"></div>
42
+
43
+ <%# Email config fields %>
44
+ <div id="config_email" class="channel-config" style="display: none;">
45
+ <h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 1.25rem; color: hsl(var(--muted-foreground));">Email Configuration</h3>
46
+ <div style="display: flex; flex-direction: column; gap: 1.25rem;">
47
+ <div class="form-group">
48
+ <label>Recipients</label>
49
+ <textarea name="config[recipients]" rows="3" placeholder="One email per line&#10;team@example.com&#10;oncall@example.com" class="font-mono"><%= Array(@channel.config["recipients"]).join("\n") %></textarea>
50
+ <span class="form-hint">One email address per line</span>
51
+ </div>
52
+ <div class="form-group">
53
+ <label>From Address</label>
54
+ <input type="text" name="config[from]" value="<%= @channel.config["from"] %>" placeholder="findbug@example.com">
55
+ <span class="form-hint">Optional. Defaults to findbug@localhost</span>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <%# Slack config fields %>
61
+ <div id="config_slack" class="channel-config" style="display: none;">
62
+ <h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 1.25rem; color: hsl(var(--muted-foreground));">Slack Configuration</h3>
63
+ <div style="display: flex; flex-direction: column; gap: 1.25rem;">
64
+ <div class="form-group">
65
+ <label>Webhook URL</label>
66
+ <input type="text" name="config[webhook_url]" value="<%= @channel.config["webhook_url"] %>" placeholder="https://hooks.slack.com/services/..." class="font-mono">
67
+ </div>
68
+ <div class="form-group">
69
+ <label>Channel</label>
70
+ <input type="text" name="config[channel]" value="<%= @channel.config["channel"] %>" placeholder="#errors">
71
+ <span class="form-hint">Optional. Overrides the webhook's default channel</span>
72
+ </div>
73
+ <div class="form-group">
74
+ <label>Username</label>
75
+ <input type="text" name="config[username]" value="<%= @channel.config["username"] %>" placeholder="Findbug">
76
+ <span class="form-hint">Optional. Defaults to Findbug</span>
77
+ </div>
78
+ <div class="form-group">
79
+ <label>Icon Emoji</label>
80
+ <input type="text" name="config[icon_emoji]" value="<%= @channel.config["icon_emoji"] %>" placeholder=":bug:">
81
+ <span class="form-hint">Optional. Defaults to :bug:</span>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <%# Discord config fields %>
87
+ <div id="config_discord" class="channel-config" style="display: none;">
88
+ <h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 1.25rem; color: hsl(var(--muted-foreground));">Discord Configuration</h3>
89
+ <div style="display: flex; flex-direction: column; gap: 1.25rem;">
90
+ <div class="form-group">
91
+ <label>Webhook URL</label>
92
+ <input type="text" name="config[webhook_url]" value="<%= @channel.config["webhook_url"] %>" placeholder="https://discord.com/api/webhooks/..." class="font-mono">
93
+ </div>
94
+ <div class="form-group">
95
+ <label>Username</label>
96
+ <input type="text" name="config[username]" value="<%= @channel.config["username"] %>" placeholder="Findbug">
97
+ <span class="form-hint">Optional. Defaults to Findbug</span>
98
+ </div>
99
+ <div class="form-group">
100
+ <label>Avatar URL</label>
101
+ <input type="text" name="config[avatar_url]" value="<%= @channel.config["avatar_url"] %>" placeholder="https://example.com/avatar.png">
102
+ <span class="form-hint">Optional. URL to a custom avatar image</span>
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ <%# Webhook config fields %>
108
+ <div id="config_webhook" class="channel-config" style="display: none;">
109
+ <h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 1.25rem; color: hsl(var(--muted-foreground));">Webhook Configuration</h3>
110
+ <div style="display: flex; flex-direction: column; gap: 1.25rem;">
111
+ <div class="form-group">
112
+ <label>URL</label>
113
+ <input type="text" name="config[url]" value="<%= @channel.config["url"] %>" placeholder="https://your-service.com/findbug-webhook" class="font-mono">
114
+ </div>
115
+ <div class="form-group">
116
+ <label>HTTP Method</label>
117
+ <%= select_tag "config[method]",
118
+ options_for_select([["POST", "POST"], ["PUT", "PUT"]], @channel.config["method"] || "POST") %>
119
+ </div>
120
+ <div class="form-group">
121
+ <label>Custom Headers</label>
122
+ <textarea name="config[headers]" rows="3" placeholder="Authorization: Bearer your-token&#10;X-Custom-Header: value" class="font-mono"><%= (@channel.config["headers"] || {}).map { |k, v| "#{k}: #{v}" }.join("\n") %></textarea>
123
+ <span class="form-hint">One header per line in "Key: Value" format</span>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <div class="separator"></div>
129
+
130
+ <%# Actions %>
131
+ <div style="display: flex; gap: 0.75rem;">
132
+ <button type="submit" class="btn btn-primary">Create Channel</button>
133
+ <a href="<%= findbug.alerts_path %>" class="btn btn-ghost">Cancel</a>
134
+ </div>
135
+ </div>
136
+ <% end %>
137
+ </div>
138
+ </div>
139
+
140
+ <script>
141
+ function toggleConfigFields(channelType) {
142
+ document.querySelectorAll('.channel-config').forEach(function(el) {
143
+ el.style.display = 'none';
144
+ });
145
+ if (channelType) {
146
+ var section = document.getElementById('config_' + channelType);
147
+ if (section) section.style.display = 'block';
148
+ }
149
+ }
150
+ (function() {
151
+ var select = document.getElementById('channel_type_select');
152
+ if (select && select.value) toggleConfigFields(select.value);
153
+ })();
154
+ </script>
@@ -292,7 +292,61 @@
292
292
  transition: border-color 0.15s;
293
293
  }
294
294
 
295
- select:focus, input:focus { outline: none; border-color: hsl(var(--ring)); box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2); }
295
+ textarea {
296
+ padding: 0.5rem 0.75rem;
297
+ font-size: 0.875rem;
298
+ background-color: hsl(var(--background));
299
+ border: 1px solid hsl(var(--input));
300
+ border-radius: var(--radius);
301
+ color: hsl(var(--foreground));
302
+ transition: border-color 0.15s;
303
+ resize: vertical;
304
+ line-height: 1.5;
305
+ }
306
+
307
+ input[type="checkbox"] {
308
+ width: 1rem;
309
+ height: 1rem;
310
+ accent-color: hsl(var(--primary));
311
+ cursor: pointer;
312
+ }
313
+
314
+ /* Toggle switch */
315
+ .toggle { position: relative; display: inline-block; width: 2rem; height: 1.125rem; flex-shrink: 0; }
316
+ .toggle input { opacity: 0; width: 0; height: 0; position: absolute; }
317
+ .toggle-slider {
318
+ position: absolute; cursor: pointer; inset: 0;
319
+ background-color: hsl(var(--error) / 0.5);
320
+ border-radius: 1rem;
321
+ transition: background-color 0.2s;
322
+ }
323
+ .toggle-slider::before {
324
+ content: "";
325
+ position: absolute; left: 2px; top: 50%; transform: translateY(-50%);
326
+ width: 0.875rem; height: 0.875rem;
327
+ background-color: hsl(var(--foreground));
328
+ border-radius: 50%;
329
+ transition: transform 0.2s;
330
+ }
331
+ .toggle input:checked + .toggle-slider { background-color: hsl(var(--success)); }
332
+ .toggle input:checked + .toggle-slider::before { transform: translateY(-50%) translateX(0.875rem); }
333
+ .toggle input:focus-visible + .toggle-slider { box-shadow: 0 0 0 2px hsl(var(--ring) / 0.3); }
334
+
335
+ /* Icon button */
336
+ .btn-icon {
337
+ display: inline-flex; align-items: center; justify-content: center;
338
+ width: 1.75rem; height: 1.75rem; padding: 0; border: none; border-radius: var(--radius);
339
+ background: transparent; color: hsl(var(--muted-foreground)); cursor: pointer;
340
+ transition: background-color 0.15s, color 0.15s;
341
+ }
342
+ .btn-icon:hover { background-color: hsl(var(--muted)); color: hsl(var(--foreground)); }
343
+ .btn-icon.destructive:hover { color: hsl(var(--error)); }
344
+
345
+ select:focus, input:focus, textarea:focus { outline: none; border-color: hsl(var(--ring)); box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2); }
346
+
347
+ .form-group { display: flex; flex-direction: column; gap: 0.375rem; }
348
+ .form-group label { font-size: 0.875rem; font-weight: 500; color: hsl(var(--foreground)); }
349
+ .form-hint { font-size: 0.75rem; color: hsl(var(--muted-foreground)); }
296
350
 
297
351
  select {
298
352
  cursor: pointer;
@@ -566,6 +620,7 @@
566
620
  <a href="<%= findbug.root_path %>" class="<%= 'active' if controller_name == 'dashboard' %>">Dashboard</a>
567
621
  <a href="<%= findbug.errors_path %>" class="<%= 'active' if controller_name == 'errors' %>">Errors</a>
568
622
  <a href="<%= findbug.performance_index_path %>" class="<%= 'active' if controller_name == 'performance' %>">Performance</a>
623
+ <a href="<%= findbug.alerts_path %>" class="<%= 'active' if controller_name == 'alerts' %>">Alerts</a>
569
624
  </nav>
570
625
  <div class="header-right">
571
626
  <div class="health-indicator">