findbug 0.3.5 → 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.
- checksums.yaml +4 -4
- data/README.md +4 -26
- data/app/controllers/findbug/alerts_controller.rb +224 -0
- data/app/models/findbug/alert_channel.rb +167 -0
- data/app/views/findbug/alerts/edit.html.erb +133 -0
- data/app/views/findbug/alerts/index.html.erb +127 -0
- data/app/views/findbug/alerts/new.html.erb +154 -0
- data/app/views/layouts/findbug/application.html.erb +56 -1
- data/docs/index.html +222 -14
- data/lib/findbug/alerts/dispatcher.rb +18 -32
- data/lib/findbug/configuration.rb +17 -56
- data/lib/findbug/engine.rb +9 -0
- data/lib/findbug/railtie.rb +8 -0
- data/lib/findbug/version.rb +1 -1
- data/lib/generators/findbug/install_generator.rb +6 -0
- data/lib/generators/findbug/templates/POST_INSTALL +5 -8
- data/lib/generators/findbug/templates/create_findbug_alert_channels.rb +24 -0
- data/lib/generators/findbug/templates/initializer.rb +3 -26
- data/lib/generators/findbug/upgrade_generator.rb +79 -0
- metadata +9 -2
|
@@ -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 team@example.com 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 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
|
-
|
|
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">
|
data/docs/index.html
CHANGED
|
@@ -1453,29 +1453,189 @@
|
|
|
1453
1453
|
/* CTA Section */
|
|
1454
1454
|
.cta {
|
|
1455
1455
|
padding: 5rem 0;
|
|
1456
|
-
|
|
1456
|
+
background: hsl(0 0% 2%);
|
|
1457
1457
|
}
|
|
1458
1458
|
|
|
1459
|
-
.cta
|
|
1460
|
-
|
|
1459
|
+
.cta-box {
|
|
1460
|
+
display: grid;
|
|
1461
|
+
grid-template-columns: 1fr 1.2fr;
|
|
1462
|
+
border-radius: 1.5rem;
|
|
1463
|
+
overflow: hidden;
|
|
1464
|
+
border: 1px solid hsl(var(--border));
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
.cta-content {
|
|
1468
|
+
display: flex;
|
|
1469
|
+
flex-direction: column;
|
|
1470
|
+
justify-content: center;
|
|
1471
|
+
padding: 3rem;
|
|
1472
|
+
position: relative;
|
|
1473
|
+
z-index: 2;
|
|
1474
|
+
background: linear-gradient(135deg,
|
|
1475
|
+
hsl(145 70% 22%) 0%,
|
|
1476
|
+
hsl(170 65% 30%) 50%,
|
|
1477
|
+
hsl(195 75% 40%) 100%
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
.cta-content::before {
|
|
1482
|
+
content: '';
|
|
1483
|
+
position: absolute;
|
|
1484
|
+
inset: 0;
|
|
1485
|
+
background:
|
|
1486
|
+
radial-gradient(ellipse 80% 50% at 20% 40%, hsl(145 100% 45% / 0.3), transparent),
|
|
1487
|
+
radial-gradient(ellipse 60% 80% at 80% 80%, hsl(195 100% 55% / 0.35), transparent),
|
|
1488
|
+
radial-gradient(ellipse 50% 50% at 50% 20%, hsl(170 100% 45% / 0.2), transparent);
|
|
1489
|
+
z-index: -1;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
.cta-content h2 {
|
|
1493
|
+
font-size: 2.25rem;
|
|
1461
1494
|
font-weight: 700;
|
|
1462
1495
|
letter-spacing: -0.025em;
|
|
1463
|
-
margin-bottom:
|
|
1496
|
+
margin-bottom: 1rem;
|
|
1497
|
+
line-height: 1.2;
|
|
1498
|
+
color: hsl(var(--foreground));
|
|
1464
1499
|
}
|
|
1465
1500
|
|
|
1466
|
-
.cta p {
|
|
1501
|
+
.cta-content p {
|
|
1467
1502
|
font-size: 1rem;
|
|
1468
|
-
color:
|
|
1503
|
+
color: white;
|
|
1469
1504
|
margin-bottom: 1.5rem;
|
|
1505
|
+
max-width: 320px;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
.cta-preview {
|
|
1509
|
+
position: relative;
|
|
1510
|
+
display: flex;
|
|
1511
|
+
align-items: center;
|
|
1512
|
+
justify-content: center;
|
|
1513
|
+
padding: 2rem;
|
|
1514
|
+
background: hsl(0 0% 2%);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
.cta-preview-window {
|
|
1518
|
+
width: 100%;
|
|
1470
1519
|
max-width: 400px;
|
|
1471
|
-
|
|
1472
|
-
|
|
1520
|
+
background: hsl(var(--card));
|
|
1521
|
+
border: 1px solid hsl(var(--border));
|
|
1522
|
+
border-radius: 12px;
|
|
1523
|
+
overflow: hidden;
|
|
1524
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
.cta-preview-titlebar {
|
|
1528
|
+
display: flex;
|
|
1529
|
+
align-items: center;
|
|
1530
|
+
gap: 0.5rem;
|
|
1531
|
+
padding: 0.75rem 1rem;
|
|
1532
|
+
background: hsl(var(--secondary));
|
|
1533
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
.cta-preview-dot {
|
|
1537
|
+
width: 10px;
|
|
1538
|
+
height: 10px;
|
|
1539
|
+
border-radius: 50%;
|
|
1540
|
+
background: hsl(var(--muted-foreground) / 0.3);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
.cta-preview-content {
|
|
1544
|
+
padding: 1.25rem;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
.cta-preview-header {
|
|
1548
|
+
display: flex;
|
|
1549
|
+
align-items: center;
|
|
1550
|
+
gap: 0.75rem;
|
|
1551
|
+
margin-bottom: 1rem;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
.cta-preview-logo {
|
|
1555
|
+
width: 32px;
|
|
1556
|
+
height: 32px;
|
|
1557
|
+
background: hsl(var(--destructive) / 0.15);
|
|
1558
|
+
border-radius: 8px;
|
|
1559
|
+
display: flex;
|
|
1560
|
+
align-items: center;
|
|
1561
|
+
justify-content: center;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
.cta-preview-logo svg {
|
|
1565
|
+
width: 18px;
|
|
1566
|
+
height: 18px;
|
|
1567
|
+
color: hsl(var(--destructive));
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
.cta-preview-title {
|
|
1571
|
+
font-weight: 600;
|
|
1572
|
+
font-size: 0.9375rem;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
.cta-preview-stats {
|
|
1576
|
+
display: grid;
|
|
1577
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1578
|
+
gap: 0.75rem;
|
|
1579
|
+
margin-bottom: 1rem;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
.cta-stat-card {
|
|
1583
|
+
background: hsl(var(--secondary));
|
|
1584
|
+
border-radius: 8px;
|
|
1585
|
+
padding: 0.75rem;
|
|
1586
|
+
text-align: center;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
.cta-stat-value {
|
|
1590
|
+
font-size: 1.25rem;
|
|
1591
|
+
font-weight: 700;
|
|
1592
|
+
color: hsl(var(--foreground));
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
.cta-stat-label {
|
|
1596
|
+
font-size: 0.6875rem;
|
|
1597
|
+
color: hsl(var(--muted-foreground));
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
.cta-preview-chart {
|
|
1601
|
+
height: 60px;
|
|
1602
|
+
background: hsl(var(--secondary));
|
|
1603
|
+
border-radius: 8px;
|
|
1604
|
+
display: flex;
|
|
1605
|
+
align-items: flex-end;
|
|
1606
|
+
justify-content: space-around;
|
|
1607
|
+
padding: 0.75rem;
|
|
1608
|
+
gap: 4px;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
.cta-chart-bar {
|
|
1612
|
+
width: 12px;
|
|
1613
|
+
background: linear-gradient(to top, hsl(var(--success)), hsl(var(--success) / 0.5));
|
|
1614
|
+
border-radius: 2px;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
@media (max-width: 900px) {
|
|
1618
|
+
.cta-box {
|
|
1619
|
+
grid-template-columns: 1fr;
|
|
1620
|
+
}
|
|
1621
|
+
.cta-content {
|
|
1622
|
+
padding: 2.5rem 1.5rem;
|
|
1623
|
+
text-align: center;
|
|
1624
|
+
align-items: center;
|
|
1625
|
+
}
|
|
1626
|
+
.cta-content h2 {
|
|
1627
|
+
font-size: 1.75rem;
|
|
1628
|
+
}
|
|
1629
|
+
.cta-preview {
|
|
1630
|
+
padding: 1.5rem;
|
|
1631
|
+
}
|
|
1473
1632
|
}
|
|
1474
1633
|
|
|
1475
1634
|
/* Footer */
|
|
1476
1635
|
footer {
|
|
1477
1636
|
padding: 2rem 0;
|
|
1478
1637
|
text-align: center;
|
|
1638
|
+
background: hsl(0 0% 2%);
|
|
1479
1639
|
}
|
|
1480
1640
|
|
|
1481
1641
|
.footer-links {
|
|
@@ -2261,12 +2421,59 @@
|
|
|
2261
2421
|
<!-- CTA Section -->
|
|
2262
2422
|
<section class="cta">
|
|
2263
2423
|
<div class="container">
|
|
2264
|
-
<
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2424
|
+
<div class="cta-box">
|
|
2425
|
+
<div class="cta-content">
|
|
2426
|
+
<h2>Own Your<br>Error Data</h2>
|
|
2427
|
+
<p>Open source, self-hosted, and forever free. Your errors, your infrastructure, your rules.</p>
|
|
2428
|
+
<a href="https://github.com/ITSSOUMIT/findbug" target="_blank" class="btn btn-primary">
|
|
2429
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
|
2430
|
+
Get Started
|
|
2431
|
+
</a>
|
|
2432
|
+
</div>
|
|
2433
|
+
<div class="cta-preview">
|
|
2434
|
+
<div class="cta-preview-window">
|
|
2435
|
+
<div class="cta-preview-titlebar">
|
|
2436
|
+
<span class="cta-preview-dot"></span>
|
|
2437
|
+
<span class="cta-preview-dot"></span>
|
|
2438
|
+
<span class="cta-preview-dot"></span>
|
|
2439
|
+
</div>
|
|
2440
|
+
<div class="cta-preview-content">
|
|
2441
|
+
<div class="cta-preview-header">
|
|
2442
|
+
<div class="cta-preview-logo">
|
|
2443
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
|
2444
|
+
</div>
|
|
2445
|
+
<span class="cta-preview-title">FindBug Dashboard</span>
|
|
2446
|
+
</div>
|
|
2447
|
+
<div class="cta-preview-stats">
|
|
2448
|
+
<div class="cta-stat-card">
|
|
2449
|
+
<div class="cta-stat-value">12</div>
|
|
2450
|
+
<div class="cta-stat-label">Errors</div>
|
|
2451
|
+
</div>
|
|
2452
|
+
<div class="cta-stat-card">
|
|
2453
|
+
<div class="cta-stat-value">847</div>
|
|
2454
|
+
<div class="cta-stat-label">Requests</div>
|
|
2455
|
+
</div>
|
|
2456
|
+
<div class="cta-stat-card">
|
|
2457
|
+
<div class="cta-stat-value">42ms</div>
|
|
2458
|
+
<div class="cta-stat-label">Avg Time</div>
|
|
2459
|
+
</div>
|
|
2460
|
+
</div>
|
|
2461
|
+
<div class="cta-preview-chart">
|
|
2462
|
+
<div class="cta-chart-bar" style="height: 25%;"></div>
|
|
2463
|
+
<div class="cta-chart-bar" style="height: 45%;"></div>
|
|
2464
|
+
<div class="cta-chart-bar" style="height: 30%;"></div>
|
|
2465
|
+
<div class="cta-chart-bar" style="height: 60%;"></div>
|
|
2466
|
+
<div class="cta-chart-bar" style="height: 80%;"></div>
|
|
2467
|
+
<div class="cta-chart-bar" style="height: 55%;"></div>
|
|
2468
|
+
<div class="cta-chart-bar" style="height: 70%;"></div>
|
|
2469
|
+
<div class="cta-chart-bar" style="height: 90%;"></div>
|
|
2470
|
+
<div class="cta-chart-bar" style="height: 65%;"></div>
|
|
2471
|
+
<div class="cta-chart-bar" style="height: 40%;"></div>
|
|
2472
|
+
</div>
|
|
2473
|
+
</div>
|
|
2474
|
+
</div>
|
|
2475
|
+
</div>
|
|
2476
|
+
</div>
|
|
2270
2477
|
</div>
|
|
2271
2478
|
</section>
|
|
2272
2479
|
|
|
@@ -2284,5 +2491,6 @@
|
|
|
2284
2491
|
</p>
|
|
2285
2492
|
</div>
|
|
2286
2493
|
</footer>
|
|
2494
|
+
|
|
2287
2495
|
</body>
|
|
2288
2496
|
</html>
|