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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca9a3e20d87733caff1807d5b89935d349a316cddf15ee2e7565f3b861cafc10
|
|
4
|
+
data.tar.gz: d135aae0a004b454448d2688e04cf22f6121ed3d64b743baf7663466ef5894b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 00d61fb0d96254b5195951d9c55a0c459f896ad61b3f813ed1fbeb817856b186ee8ceac24d5d659934e3909cfaa9bb901f38cd3fc3bc68e8aa5df4d7e04c3627
|
|
7
|
+
data.tar.gz: 01c241d176885ea96d700623048bf1f7d8af8832057b722b6d23c8a079dedc61410322abb897313a4e7a3d937a0ce6197c248b6438197725ab6656333f1dc311
|
data/README.md
CHANGED
|
@@ -142,34 +142,11 @@ Findbug.configure do |config|
|
|
|
142
142
|
# ===================
|
|
143
143
|
# Alerts (Optional)
|
|
144
144
|
# ===================
|
|
145
|
+
# Alert channels (Email, Slack, Discord, Webhook) are configured via the
|
|
146
|
+
# dashboard UI at /findbug/alerts — no code changes needed.
|
|
147
|
+
|
|
145
148
|
config.alerts do |alerts|
|
|
146
149
|
alerts.throttle_period = 5.minutes
|
|
147
|
-
|
|
148
|
-
# Slack
|
|
149
|
-
# alerts.slack(
|
|
150
|
-
# enabled: true,
|
|
151
|
-
# webhook_url: ENV["SLACK_WEBHOOK_URL"],
|
|
152
|
-
# channel: "#errors"
|
|
153
|
-
# )
|
|
154
|
-
|
|
155
|
-
# Email
|
|
156
|
-
# alerts.email(
|
|
157
|
-
# enabled: true,
|
|
158
|
-
# recipients: ["team@example.com"]
|
|
159
|
-
# )
|
|
160
|
-
|
|
161
|
-
# Discord
|
|
162
|
-
# alerts.discord(
|
|
163
|
-
# enabled: true,
|
|
164
|
-
# webhook_url: ENV["DISCORD_WEBHOOK_URL"]
|
|
165
|
-
# )
|
|
166
|
-
|
|
167
|
-
# Custom Webhook
|
|
168
|
-
# alerts.webhook(
|
|
169
|
-
# enabled: true,
|
|
170
|
-
# url: "https://your-service.com/webhook",
|
|
171
|
-
# headers: { "Authorization" => "Bearer token" }
|
|
172
|
-
# )
|
|
173
150
|
end
|
|
174
151
|
end
|
|
175
152
|
```
|
|
@@ -311,6 +288,7 @@ Apartment.configure do |config|
|
|
|
311
288
|
# Your existing excluded models...
|
|
312
289
|
Findbug::ErrorEvent
|
|
313
290
|
Findbug::PerformanceEvent
|
|
291
|
+
Findbug::AlertChannel
|
|
314
292
|
]
|
|
315
293
|
end
|
|
316
294
|
```
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ostruct"
|
|
4
|
+
|
|
5
|
+
module Findbug
|
|
6
|
+
# AlertsController manages alert channel configuration via the dashboard.
|
|
7
|
+
#
|
|
8
|
+
# Users can create, edit, enable/disable, delete, and test alert channels
|
|
9
|
+
# directly from the UI instead of editing the Rails initializer.
|
|
10
|
+
#
|
|
11
|
+
class AlertsController < ApplicationController
|
|
12
|
+
before_action :set_alert_channel, only: [:edit, :update, :destroy, :toggle, :test]
|
|
13
|
+
|
|
14
|
+
# GET /findbug/alerts
|
|
15
|
+
#
|
|
16
|
+
# List all configured alert channels.
|
|
17
|
+
#
|
|
18
|
+
def index
|
|
19
|
+
@channels = Findbug::AlertChannel.order(created_at: :asc)
|
|
20
|
+
@enabled_count = @channels.count(&:enabled?)
|
|
21
|
+
|
|
22
|
+
render template: "findbug/alerts/index", layout: "findbug/application"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# GET /findbug/alerts/new
|
|
26
|
+
#
|
|
27
|
+
# Form to create a new alert channel.
|
|
28
|
+
#
|
|
29
|
+
def new
|
|
30
|
+
@channel = Findbug::AlertChannel.new
|
|
31
|
+
render template: "findbug/alerts/new", layout: "findbug/application"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# POST /findbug/alerts
|
|
35
|
+
#
|
|
36
|
+
# Save a new alert channel.
|
|
37
|
+
#
|
|
38
|
+
def create
|
|
39
|
+
@channel = Findbug::AlertChannel.new(channel_params)
|
|
40
|
+
@channel.config = build_config_from_params
|
|
41
|
+
|
|
42
|
+
if @channel.save
|
|
43
|
+
flash_success "#{@channel.display_type} alert channel created"
|
|
44
|
+
redirect_to findbug.alerts_path
|
|
45
|
+
else
|
|
46
|
+
flash_error @channel.errors.full_messages.join(", ")
|
|
47
|
+
render template: "findbug/alerts/new", layout: "findbug/application", status: :unprocessable_entity
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# GET /findbug/alerts/:id/edit
|
|
52
|
+
#
|
|
53
|
+
# Form to edit an existing alert channel.
|
|
54
|
+
#
|
|
55
|
+
def edit
|
|
56
|
+
render template: "findbug/alerts/edit", layout: "findbug/application"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# PATCH /findbug/alerts/:id
|
|
60
|
+
#
|
|
61
|
+
# Update an existing alert channel.
|
|
62
|
+
#
|
|
63
|
+
def update
|
|
64
|
+
@channel.assign_attributes(channel_params)
|
|
65
|
+
@channel.config = build_config_from_params
|
|
66
|
+
|
|
67
|
+
if @channel.save
|
|
68
|
+
flash_success "#{@channel.display_type} alert channel updated"
|
|
69
|
+
redirect_to findbug.alerts_path
|
|
70
|
+
else
|
|
71
|
+
flash_error @channel.errors.full_messages.join(", ")
|
|
72
|
+
render template: "findbug/alerts/edit", layout: "findbug/application", status: :unprocessable_entity
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# DELETE /findbug/alerts/:id
|
|
77
|
+
#
|
|
78
|
+
# Delete an alert channel.
|
|
79
|
+
#
|
|
80
|
+
def destroy
|
|
81
|
+
name = @channel.name
|
|
82
|
+
@channel.destroy
|
|
83
|
+
flash_success "Alert channel \"#{name}\" deleted"
|
|
84
|
+
redirect_to findbug.alerts_path
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# POST /findbug/alerts/:id/toggle
|
|
88
|
+
#
|
|
89
|
+
# Toggle enable/disable for an alert channel.
|
|
90
|
+
#
|
|
91
|
+
def toggle
|
|
92
|
+
@channel.enabled = !@channel.enabled?
|
|
93
|
+
|
|
94
|
+
if @channel.save
|
|
95
|
+
status = @channel.enabled? ? "enabled" : "disabled"
|
|
96
|
+
flash_success "#{@channel.name} #{status}"
|
|
97
|
+
else
|
|
98
|
+
flash_error @channel.errors.full_messages.join(", ")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
redirect_to findbug.alerts_path
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# POST /findbug/alerts/:id/test
|
|
105
|
+
#
|
|
106
|
+
# Send a test alert to this channel.
|
|
107
|
+
#
|
|
108
|
+
# Creates a synthetic error event (not persisted to DB) and sends it
|
|
109
|
+
# directly to the channel, bypassing throttling.
|
|
110
|
+
#
|
|
111
|
+
def test
|
|
112
|
+
unless @channel.enabled?
|
|
113
|
+
flash_error "Cannot test a disabled channel. Enable it first."
|
|
114
|
+
redirect_to findbug.alerts_path and return
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
error_event = build_test_error_event
|
|
118
|
+
channel_instance = @channel.channel_class.new(@channel.config.symbolize_keys)
|
|
119
|
+
|
|
120
|
+
begin
|
|
121
|
+
channel_instance.send_alert(error_event)
|
|
122
|
+
flash_success "Test alert sent to #{@channel.name} successfully!"
|
|
123
|
+
rescue StandardError => e
|
|
124
|
+
flash_error "Failed to send test alert: #{e.message}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
redirect_to findbug.alerts_path
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def set_alert_channel
|
|
133
|
+
@channel = Findbug::AlertChannel.find(params[:id])
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def channel_params
|
|
137
|
+
params.require(:alert_channel).permit(:name, :channel_type, :enabled)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Build the config hash from channel-type-specific form params
|
|
141
|
+
#
|
|
142
|
+
# Each channel type has different fields. We extract them from
|
|
143
|
+
# params[:config] and build a clean hash.
|
|
144
|
+
#
|
|
145
|
+
def build_config_from_params
|
|
146
|
+
config_params = params[:config] || {}
|
|
147
|
+
channel_type = params.dig(:alert_channel, :channel_type) || @channel&.channel_type
|
|
148
|
+
|
|
149
|
+
case channel_type
|
|
150
|
+
when "email"
|
|
151
|
+
recipients = (config_params[:recipients] || "").split(/[\n,]/).map(&:strip).compact_blank
|
|
152
|
+
{
|
|
153
|
+
"recipients" => recipients,
|
|
154
|
+
"from" => config_params[:from].presence
|
|
155
|
+
}.compact
|
|
156
|
+
when "slack"
|
|
157
|
+
{
|
|
158
|
+
"webhook_url" => config_params[:webhook_url],
|
|
159
|
+
"channel" => config_params[:channel].presence,
|
|
160
|
+
"username" => config_params[:username].presence,
|
|
161
|
+
"icon_emoji" => config_params[:icon_emoji].presence
|
|
162
|
+
}.compact
|
|
163
|
+
when "discord"
|
|
164
|
+
{
|
|
165
|
+
"webhook_url" => config_params[:webhook_url],
|
|
166
|
+
"username" => config_params[:username].presence,
|
|
167
|
+
"avatar_url" => config_params[:avatar_url].presence
|
|
168
|
+
}.compact
|
|
169
|
+
when "webhook"
|
|
170
|
+
headers = parse_headers(config_params[:headers])
|
|
171
|
+
{
|
|
172
|
+
"url" => config_params[:url],
|
|
173
|
+
"method" => config_params[:method].presence || "POST",
|
|
174
|
+
"headers" => headers.presence
|
|
175
|
+
}.compact
|
|
176
|
+
else
|
|
177
|
+
{}
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Parse headers from textarea format ("Key: Value" per line) into a hash
|
|
182
|
+
def parse_headers(raw)
|
|
183
|
+
return {} if raw.blank?
|
|
184
|
+
|
|
185
|
+
raw.split("\n").each_with_object({}) do |line, hash|
|
|
186
|
+
key, value = line.split(":", 2).map(&:strip)
|
|
187
|
+
hash[key] = value if key.present? && value.present?
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Build a synthetic error event for testing alerts
|
|
192
|
+
#
|
|
193
|
+
# Uses OpenStruct to duck-type ErrorEvent without touching the database.
|
|
194
|
+
# Includes all attributes that channel implementations access.
|
|
195
|
+
#
|
|
196
|
+
def build_test_error_event
|
|
197
|
+
now = Time.current
|
|
198
|
+
|
|
199
|
+
OpenStruct.new(
|
|
200
|
+
id: 0,
|
|
201
|
+
fingerprint: "findbug-test-alert-#{now.to_i}",
|
|
202
|
+
exception_class: "Findbug::TestAlert",
|
|
203
|
+
message: "This is a test alert from the Findbug dashboard. If you see this, your alert channel is working correctly!",
|
|
204
|
+
severity: "error",
|
|
205
|
+
status: "unresolved",
|
|
206
|
+
handled: false,
|
|
207
|
+
occurrence_count: 1,
|
|
208
|
+
first_seen_at: now,
|
|
209
|
+
last_seen_at: now,
|
|
210
|
+
environment: Findbug.config.environment || "production",
|
|
211
|
+
release_version: Findbug::VERSION,
|
|
212
|
+
backtrace_lines: [
|
|
213
|
+
"app/controllers/findbug/alerts_controller.rb:42:in `test'",
|
|
214
|
+
"lib/findbug/alerts/dispatcher.rb:57:in `send_alerts'",
|
|
215
|
+
"lib/findbug/alerts/channels/base.rb:34:in `send_alert'"
|
|
216
|
+
],
|
|
217
|
+
context: {},
|
|
218
|
+
user: nil,
|
|
219
|
+
request: { "method" => "POST", "path" => "/findbug/alerts/test" },
|
|
220
|
+
tags: { "source" => "test_alert" }
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../lib/findbug/alerts/channels/base"
|
|
4
|
+
require_relative "../../../lib/findbug/alerts/channels/email"
|
|
5
|
+
require_relative "../../../lib/findbug/alerts/channels/slack"
|
|
6
|
+
require_relative "../../../lib/findbug/alerts/channels/discord"
|
|
7
|
+
require_relative "../../../lib/findbug/alerts/channels/webhook"
|
|
8
|
+
|
|
9
|
+
module Findbug
|
|
10
|
+
# AlertChannel stores alert channel configurations in the database.
|
|
11
|
+
#
|
|
12
|
+
# DATABASE SCHEMA
|
|
13
|
+
# ===============
|
|
14
|
+
#
|
|
15
|
+
# This model expects a table created by the install generator:
|
|
16
|
+
#
|
|
17
|
+
# create_table :findbug_alert_channels do |t|
|
|
18
|
+
# t.string :channel_type, null: false
|
|
19
|
+
# t.string :name, null: false
|
|
20
|
+
# t.boolean :enabled, default: false
|
|
21
|
+
# t.text :config_data
|
|
22
|
+
# t.timestamps
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# WHY DB INSTEAD OF INITIALIZER?
|
|
26
|
+
# ==============================
|
|
27
|
+
#
|
|
28
|
+
# Storing alert config in the database lets users create, edit, and
|
|
29
|
+
# manage alert channels from the dashboard UI without code changes
|
|
30
|
+
# or redeployment.
|
|
31
|
+
#
|
|
32
|
+
# WHY TEXT + SERIALIZE INSTEAD OF JSONB?
|
|
33
|
+
# ======================================
|
|
34
|
+
#
|
|
35
|
+
# ActiveRecord::Encryption works on text columns. We serialize the
|
|
36
|
+
# config hash as JSON and encrypt the entire blob at rest, so webhook
|
|
37
|
+
# URLs and tokens are never stored in plain text.
|
|
38
|
+
#
|
|
39
|
+
class AlertChannel < ActiveRecord::Base
|
|
40
|
+
self.table_name = "findbug_alert_channels"
|
|
41
|
+
|
|
42
|
+
# Channel types
|
|
43
|
+
CHANNEL_TYPES = %w[email slack discord webhook].freeze
|
|
44
|
+
|
|
45
|
+
# Check if Rails encryption is configured
|
|
46
|
+
def self.encryption_available?
|
|
47
|
+
return false unless defined?(ActiveRecord::Encryption)
|
|
48
|
+
|
|
49
|
+
ActiveRecord::Encryption.config.primary_key.present?
|
|
50
|
+
rescue StandardError
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Serialize config as JSON
|
|
55
|
+
serialize :config_data, coder: JSON
|
|
56
|
+
|
|
57
|
+
# Encrypt config at rest if Rails encryption is configured
|
|
58
|
+
encrypts :config_data if encryption_available?
|
|
59
|
+
|
|
60
|
+
# Validations
|
|
61
|
+
validates :name, presence: true
|
|
62
|
+
validates :channel_type, presence: true, inclusion: { in: CHANNEL_TYPES }
|
|
63
|
+
validate :validate_required_config
|
|
64
|
+
|
|
65
|
+
# Scopes
|
|
66
|
+
scope :enabled, -> { where(enabled: true) }
|
|
67
|
+
scope :by_type, ->(type) { where(channel_type: type) }
|
|
68
|
+
|
|
69
|
+
# Convenience accessor for config
|
|
70
|
+
def config
|
|
71
|
+
config_data || {}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def config=(value)
|
|
75
|
+
self.config_data = value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns the channel class for sending alerts
|
|
79
|
+
#
|
|
80
|
+
# Maps channel_type to the corresponding Alerts::Channels class.
|
|
81
|
+
#
|
|
82
|
+
def channel_class
|
|
83
|
+
case channel_type
|
|
84
|
+
when "email" then Findbug::Alerts::Channels::Email
|
|
85
|
+
when "slack" then Findbug::Alerts::Channels::Slack
|
|
86
|
+
when "discord" then Findbug::Alerts::Channels::Discord
|
|
87
|
+
when "webhook" then Findbug::Alerts::Channels::Webhook
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Human-readable channel type
|
|
92
|
+
def display_type
|
|
93
|
+
channel_type&.titleize
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns config with sensitive values masked for display
|
|
97
|
+
#
|
|
98
|
+
# Shows scheme + host for URLs, masks everything else.
|
|
99
|
+
# Email recipients are shown in full (not sensitive).
|
|
100
|
+
#
|
|
101
|
+
def masked_config
|
|
102
|
+
masked = {}
|
|
103
|
+
|
|
104
|
+
case channel_type
|
|
105
|
+
when "email"
|
|
106
|
+
masked["Recipients"] = Array(config["recipients"]).join(", ").presence || "None"
|
|
107
|
+
masked["From"] = config["from"] || "findbug@localhost"
|
|
108
|
+
when "slack"
|
|
109
|
+
masked["Webhook URL"] = mask_url(config["webhook_url"])
|
|
110
|
+
masked["Channel"] = config["channel"] || "Default"
|
|
111
|
+
masked["Username"] = config["username"] || "Findbug"
|
|
112
|
+
when "discord"
|
|
113
|
+
masked["Webhook URL"] = mask_url(config["webhook_url"])
|
|
114
|
+
masked["Username"] = config["username"] || "Findbug"
|
|
115
|
+
when "webhook"
|
|
116
|
+
masked["URL"] = mask_url(config["url"])
|
|
117
|
+
masked["Method"] = (config["method"] || "POST").upcase
|
|
118
|
+
headers_count = (config["headers"] || {}).size
|
|
119
|
+
masked["Custom Headers"] = "#{headers_count} configured" if headers_count > 0
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
masked
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
# Mask a URL for safe display
|
|
128
|
+
#
|
|
129
|
+
# Shows scheme + host but masks the path (which typically contains
|
|
130
|
+
# secret tokens in webhook URLs).
|
|
131
|
+
#
|
|
132
|
+
def mask_url(url)
|
|
133
|
+
return "Not configured" if url.blank?
|
|
134
|
+
|
|
135
|
+
uri = URI.parse(url)
|
|
136
|
+
path = uri.path.to_s
|
|
137
|
+
masked_path = path.length > 8 ? "#{path[0..7]}********" : "********"
|
|
138
|
+
"#{uri.scheme}://#{uri.host}#{masked_path}"
|
|
139
|
+
rescue URI::InvalidURIError
|
|
140
|
+
"#{url[0..15]}********"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Validate that required config fields are present for each channel type
|
|
144
|
+
def validate_required_config
|
|
145
|
+
return if config.blank? && !enabled?
|
|
146
|
+
|
|
147
|
+
case channel_type
|
|
148
|
+
when "email"
|
|
149
|
+
if enabled? && Array(config["recipients"]).compact_blank.empty?
|
|
150
|
+
errors.add(:base, "Email channel requires at least one recipient")
|
|
151
|
+
end
|
|
152
|
+
when "slack"
|
|
153
|
+
if enabled? && config["webhook_url"].blank?
|
|
154
|
+
errors.add(:base, "Slack channel requires a webhook URL")
|
|
155
|
+
end
|
|
156
|
+
when "discord"
|
|
157
|
+
if enabled? && config["webhook_url"].blank?
|
|
158
|
+
errors.add(:base, "Discord channel requires a webhook URL")
|
|
159
|
+
end
|
|
160
|
+
when "webhook"
|
|
161
|
+
if enabled? && config["url"].blank?
|
|
162
|
+
errors.add(:base, "Webhook channel requires a URL")
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<h1>Edit Alert Channel</h1>
|
|
2
|
+
<p class="page-description">Update configuration for <strong><%= @channel.name %></strong>.</p>
|
|
3
|
+
|
|
4
|
+
<div class="card" style="max-width: 560px;">
|
|
5
|
+
<div class="card-content">
|
|
6
|
+
<%= form_tag findbug.alert_path(@channel), method: :patch do %>
|
|
7
|
+
<div style="display: flex; flex-direction: column; gap: 1.5rem;">
|
|
8
|
+
|
|
9
|
+
<%# Channel Type (locked on edit) %>
|
|
10
|
+
<div class="form-group">
|
|
11
|
+
<label>Channel Type</label>
|
|
12
|
+
<div style="display: flex; align-items: center; gap: 0.5rem; height: 2rem;">
|
|
13
|
+
<span class="badge badge-info"><%= @channel.display_type %></span>
|
|
14
|
+
<span class="form-hint">Cannot be changed after creation</span>
|
|
15
|
+
</div>
|
|
16
|
+
<input type="hidden" name="alert_channel[channel_type]" value="<%= @channel.channel_type %>">
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<%# Name %>
|
|
20
|
+
<div class="form-group">
|
|
21
|
+
<label>Name</label>
|
|
22
|
+
<input type="text" name="alert_channel[name]" value="<%= @channel.name %>"
|
|
23
|
+
placeholder="e.g., Production Slack, Dev Team Email">
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<%# Enabled %>
|
|
27
|
+
<label style="display: flex; align-items: center; gap: 0.75rem; cursor: pointer;">
|
|
28
|
+
<input type="hidden" name="alert_channel[enabled]" value="0">
|
|
29
|
+
<span class="toggle">
|
|
30
|
+
<input type="checkbox" name="alert_channel[enabled]" value="1" <%= "checked" if @channel.enabled? %>>
|
|
31
|
+
<span class="toggle-slider"></span>
|
|
32
|
+
</span>
|
|
33
|
+
<span>Enable this channel</span>
|
|
34
|
+
</label>
|
|
35
|
+
|
|
36
|
+
<div class="separator"></div>
|
|
37
|
+
|
|
38
|
+
<%# Email config fields %>
|
|
39
|
+
<% if @channel.channel_type == "email" %>
|
|
40
|
+
<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 1.25rem; color: hsl(var(--muted-foreground));">Email Configuration</h3>
|
|
41
|
+
<div style="display: flex; flex-direction: column; gap: 1.25rem;">
|
|
42
|
+
<div class="form-group">
|
|
43
|
+
<label>Recipients</label>
|
|
44
|
+
<textarea name="config[recipients]" rows="3" placeholder="One email per line" class="font-mono"><%= Array(@channel.config["recipients"]).join("\n") %></textarea>
|
|
45
|
+
<span class="form-hint">One email address per line</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="form-group">
|
|
48
|
+
<label>From Address</label>
|
|
49
|
+
<input type="text" name="config[from]" value="<%= @channel.config["from"] %>" placeholder="findbug@example.com">
|
|
50
|
+
<span class="form-hint">Optional. Defaults to findbug@localhost</span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<% end %>
|
|
54
|
+
|
|
55
|
+
<%# Slack config fields %>
|
|
56
|
+
<% if @channel.channel_type == "slack" %>
|
|
57
|
+
<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 1.25rem; color: hsl(var(--muted-foreground));">Slack Configuration</h3>
|
|
58
|
+
<div style="display: flex; flex-direction: column; gap: 1.25rem;">
|
|
59
|
+
<div class="form-group">
|
|
60
|
+
<label>Webhook URL</label>
|
|
61
|
+
<input type="text" name="config[webhook_url]" value="<%= @channel.config["webhook_url"] %>" placeholder="https://hooks.slack.com/services/..." class="font-mono">
|
|
62
|
+
</div>
|
|
63
|
+
<div class="form-group">
|
|
64
|
+
<label>Channel</label>
|
|
65
|
+
<input type="text" name="config[channel]" value="<%= @channel.config["channel"] %>" placeholder="#errors">
|
|
66
|
+
<span class="form-hint">Optional. Overrides the webhook's default channel</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="form-group">
|
|
69
|
+
<label>Username</label>
|
|
70
|
+
<input type="text" name="config[username]" value="<%= @channel.config["username"] %>" placeholder="Findbug">
|
|
71
|
+
<span class="form-hint">Optional. Defaults to Findbug</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="form-group">
|
|
74
|
+
<label>Icon Emoji</label>
|
|
75
|
+
<input type="text" name="config[icon_emoji]" value="<%= @channel.config["icon_emoji"] %>" placeholder=":bug:">
|
|
76
|
+
<span class="form-hint">Optional. Defaults to :bug:</span>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<% end %>
|
|
80
|
+
|
|
81
|
+
<%# Discord config fields %>
|
|
82
|
+
<% if @channel.channel_type == "discord" %>
|
|
83
|
+
<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 1.25rem; color: hsl(var(--muted-foreground));">Discord Configuration</h3>
|
|
84
|
+
<div style="display: flex; flex-direction: column; gap: 1.25rem;">
|
|
85
|
+
<div class="form-group">
|
|
86
|
+
<label>Webhook URL</label>
|
|
87
|
+
<input type="text" name="config[webhook_url]" value="<%= @channel.config["webhook_url"] %>" placeholder="https://discord.com/api/webhooks/..." class="font-mono">
|
|
88
|
+
</div>
|
|
89
|
+
<div class="form-group">
|
|
90
|
+
<label>Username</label>
|
|
91
|
+
<input type="text" name="config[username]" value="<%= @channel.config["username"] %>" placeholder="Findbug">
|
|
92
|
+
<span class="form-hint">Optional. Defaults to Findbug</span>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="form-group">
|
|
95
|
+
<label>Avatar URL</label>
|
|
96
|
+
<input type="text" name="config[avatar_url]" value="<%= @channel.config["avatar_url"] %>" placeholder="https://example.com/avatar.png">
|
|
97
|
+
<span class="form-hint">Optional. URL to a custom avatar image</span>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
<% end %>
|
|
101
|
+
|
|
102
|
+
<%# Webhook config fields %>
|
|
103
|
+
<% if @channel.channel_type == "webhook" %>
|
|
104
|
+
<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 1.25rem; color: hsl(var(--muted-foreground));">Webhook Configuration</h3>
|
|
105
|
+
<div style="display: flex; flex-direction: column; gap: 1.25rem;">
|
|
106
|
+
<div class="form-group">
|
|
107
|
+
<label>URL</label>
|
|
108
|
+
<input type="text" name="config[url]" value="<%= @channel.config["url"] %>" placeholder="https://your-service.com/findbug-webhook" class="font-mono">
|
|
109
|
+
</div>
|
|
110
|
+
<div class="form-group">
|
|
111
|
+
<label>HTTP Method</label>
|
|
112
|
+
<%= select_tag "config[method]",
|
|
113
|
+
options_for_select([["POST", "POST"], ["PUT", "PUT"]], @channel.config["method"] || "POST") %>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="form-group">
|
|
116
|
+
<label>Custom Headers</label>
|
|
117
|
+
<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>
|
|
118
|
+
<span class="form-hint">One header per line in "Key: Value" format</span>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<% end %>
|
|
122
|
+
|
|
123
|
+
<div class="separator"></div>
|
|
124
|
+
|
|
125
|
+
<%# Actions %>
|
|
126
|
+
<div style="display: flex; gap: 0.75rem;">
|
|
127
|
+
<button type="submit" class="btn btn-primary">Update Channel</button>
|
|
128
|
+
<a href="<%= findbug.alerts_path %>" class="btn btn-ghost">Cancel</a>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<% end %>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|