pg_sql_triggers 1.1.1 → 1.2.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.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgSqlTriggers
4
+ # rubocop:disable Metrics/ClassLength
4
5
  class TriggerRegistry < PgSqlTriggers::ApplicationRecord
5
6
  self.table_name = "pg_sql_triggers_registry"
6
7
 
@@ -150,6 +151,51 @@ module PgSqlTriggers
150
151
  end
151
152
  end
152
153
 
154
+ def drop!(reason:, confirmation: nil, actor: nil)
155
+ # Check kill switch before dropping trigger
156
+ PgSqlTriggers::SQL::KillSwitch.check!(
157
+ operation: :trigger_drop,
158
+ environment: Rails.env,
159
+ confirmation: confirmation,
160
+ actor: actor || { type: "Console", id: "TriggerRegistry#drop!" }
161
+ )
162
+
163
+ # Validate reason is provided
164
+ raise ArgumentError, "Reason is required" if reason.nil? || reason.to_s.strip.empty?
165
+
166
+ log_drop_attempt(reason)
167
+
168
+ # Execute DROP TRIGGER in transaction
169
+ ActiveRecord::Base.transaction do
170
+ drop_trigger_from_database
171
+ destroy!
172
+ log_drop_success
173
+ end
174
+ end
175
+
176
+ def re_execute!(reason:, confirmation: nil, actor: nil)
177
+ # Check kill switch before re-executing trigger
178
+ PgSqlTriggers::SQL::KillSwitch.check!(
179
+ operation: :trigger_re_execute,
180
+ environment: Rails.env,
181
+ confirmation: confirmation,
182
+ actor: actor || { type: "Console", id: "TriggerRegistry#re_execute!" }
183
+ )
184
+
185
+ # Validate reason is provided
186
+ raise ArgumentError, "Reason is required" if reason.nil? || reason.to_s.strip.empty?
187
+ raise StandardError, "Cannot re-execute: missing function_body" if function_body.blank?
188
+
189
+ log_re_execute_attempt(reason)
190
+
191
+ # Execute the trigger creation/update in transaction
192
+ ActiveRecord::Base.transaction do
193
+ drop_existing_trigger_for_re_execute
194
+ recreate_trigger
195
+ update_registry_after_re_execute
196
+ end
197
+ end
198
+
153
199
  private
154
200
 
155
201
  def quote_identifier(identifier)
@@ -170,5 +216,82 @@ module PgSqlTriggers
170
216
  def verify!
171
217
  update!(last_verified_at: Time.current)
172
218
  end
219
+
220
+ # Drop trigger helpers
221
+ def log_drop_attempt(reason)
222
+ return unless defined?(Rails.logger)
223
+
224
+ Rails.logger.info "[TRIGGER_DROP] Dropping: #{trigger_name} on #{table_name}"
225
+ Rails.logger.info "[TRIGGER_DROP] Reason: #{reason}"
226
+ end
227
+
228
+ def log_drop_success
229
+ return unless defined?(Rails.logger)
230
+
231
+ Rails.logger.info "[TRIGGER_DROP] Successfully removed from registry"
232
+ end
233
+
234
+ def drop_trigger_from_database
235
+ trigger_exists = check_trigger_exists
236
+ return unless trigger_exists
237
+
238
+ execute_drop_sql
239
+ end
240
+
241
+ def check_trigger_exists
242
+ introspection = PgSqlTriggers::DatabaseIntrospection.new
243
+ introspection.trigger_exists?(trigger_name)
244
+ rescue StandardError => e
245
+ Rails.logger.warn("Could not check trigger existence: #{e.message}") if defined?(Rails.logger)
246
+ false
247
+ end
248
+
249
+ def execute_drop_sql
250
+ quoted_table = quote_identifier(table_name)
251
+ quoted_trigger = quote_identifier(trigger_name)
252
+ sql = "DROP TRIGGER IF EXISTS #{quoted_trigger} ON #{quoted_table};"
253
+ ActiveRecord::Base.connection.execute(sql)
254
+ Rails.logger.info "[TRIGGER_DROP] Dropped from database" if defined?(Rails.logger)
255
+ rescue ActiveRecord::StatementInvalid, StandardError => e
256
+ Rails.logger.error("[TRIGGER_DROP] Failed: #{e.message}") if defined?(Rails.logger)
257
+ raise
258
+ end
259
+
260
+ # Re-execute trigger helpers
261
+ def log_re_execute_attempt(reason)
262
+ return unless defined?(Rails.logger)
263
+
264
+ Rails.logger.info "[TRIGGER_RE_EXECUTE] Re-executing: #{trigger_name} on #{table_name}"
265
+ Rails.logger.info "[TRIGGER_RE_EXECUTE] Reason: #{reason}"
266
+ drift = drift_result
267
+ Rails.logger.info "[TRIGGER_RE_EXECUTE] Current state: #{drift[:state]}"
268
+ end
269
+
270
+ def drop_existing_trigger_for_re_execute
271
+ introspection = PgSqlTriggers::DatabaseIntrospection.new
272
+ return unless introspection.trigger_exists?(trigger_name)
273
+
274
+ quoted_table = quote_identifier(table_name)
275
+ quoted_trigger = quote_identifier(trigger_name)
276
+ drop_sql = "DROP TRIGGER IF EXISTS #{quoted_trigger} ON #{quoted_table};"
277
+ ActiveRecord::Base.connection.execute(drop_sql)
278
+ Rails.logger.info "[TRIGGER_RE_EXECUTE] Dropped existing" if defined?(Rails.logger)
279
+ rescue StandardError => e
280
+ Rails.logger.warn("[TRIGGER_RE_EXECUTE] Drop failed: #{e.message}") if defined?(Rails.logger)
281
+ end
282
+
283
+ def recreate_trigger
284
+ ActiveRecord::Base.connection.execute(function_body)
285
+ Rails.logger.info "[TRIGGER_RE_EXECUTE] Re-created trigger" if defined?(Rails.logger)
286
+ rescue ActiveRecord::StatementInvalid, StandardError => e
287
+ Rails.logger.error("[TRIGGER_RE_EXECUTE] Failed: #{e.message}") if defined?(Rails.logger)
288
+ raise
289
+ end
290
+
291
+ def update_registry_after_re_execute
292
+ update!(enabled: true, last_executed_at: Time.current)
293
+ Rails.logger.info "[TRIGGER_RE_EXECUTE] Updated registry" if defined?(Rails.logger)
294
+ end
173
295
  end
296
+ # rubocop:enable Metrics/ClassLength
174
297
  end
@@ -46,7 +46,9 @@
46
46
  <tbody>
47
47
  <% @triggers.limit(10).each do |trigger| %>
48
48
  <tr>
49
- <td><strong><%= trigger.trigger_name %></strong></td>
49
+ <td>
50
+ <%= link_to trigger.trigger_name, trigger_path(trigger), style: "color: #007bff; text-decoration: none; font-weight: 600;" %>
51
+ </td>
50
52
  <td><%= trigger.table_name %></td>
51
53
  <td><%= trigger.version %></td>
52
54
  <td>
@@ -57,7 +59,43 @@
57
59
  <% end %>
58
60
  </td>
59
61
  <td><span class="badge badge-info"><%= trigger.source %></span></td>
60
- <td>—</td>
62
+ <td>
63
+ <% if PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger) %>
64
+ <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
65
+ <% if trigger.enabled %>
66
+ <% form_id = "trigger-disable-#{trigger.id}-form" %>
67
+ <%= form_with url: disable_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
68
+ <%= f.hidden_field :redirect_to, value: dashboard_path %>
69
+ <button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
70
+ style="padding: 0.25rem 0.75rem; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
71
+ Disable
72
+ </button>
73
+ <% end %>
74
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
75
+ operation: :ui_trigger_disable,
76
+ form_id: form_id,
77
+ title: 'Disable Trigger',
78
+ message: "Are you sure you want to disable trigger '#{trigger.trigger_name}'?" %>
79
+ <% else %>
80
+ <% form_id = "trigger-enable-#{trigger.id}-form" %>
81
+ <%= form_with url: enable_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
82
+ <%= f.hidden_field :redirect_to, value: dashboard_path %>
83
+ <button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
84
+ style="padding: 0.25rem 0.75rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
85
+ Enable
86
+ </button>
87
+ <% end %>
88
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
89
+ operation: :ui_trigger_enable,
90
+ form_id: form_id,
91
+ title: 'Enable Trigger',
92
+ message: "Are you sure you want to enable trigger '#{trigger.trigger_name}'?" %>
93
+ <% end %>
94
+ </div>
95
+ <% else %>
96
+
97
+ <% end %>
98
+ </td>
61
99
  </tr>
62
100
  <% end %>
63
101
  </tbody>
@@ -0,0 +1,81 @@
1
+ <div style="max-width: 900px; margin: 0 auto;">
2
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
3
+ <h2 style="margin: 0;">Create SQL Capsule</h2>
4
+ <%= link_to "← Back to Dashboard", dashboard_path, style: "color: #007bff; text-decoration: none;" %>
5
+ </div>
6
+
7
+ <div style="background: #fff3cd; border: 1px solid #ffc107; padding: 1rem; border-radius: 4px; margin-bottom: 2rem;">
8
+ <strong>⚠️ Warning:</strong> SQL Capsules allow direct SQL execution. Use with extreme caution.
9
+ This feature is intended for emergency operations only.
10
+ </div>
11
+
12
+ <%= form_with url: sql_capsules_path, method: :post, local: true do |f| %>
13
+ <div style="background: white; padding: 2rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
14
+
15
+ <div style="margin-bottom: 1.5rem;">
16
+ <label for="name" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
17
+ Capsule Name <span style="color: #dc3545;">*</span>
18
+ </label>
19
+ <%= text_field_tag :name, @capsule_name,
20
+ placeholder: "e.g., fix_user_permissions",
21
+ required: true,
22
+ pattern: "[a-zA-Z0-9_-]+",
23
+ autocomplete: "off",
24
+ style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" %>
25
+ <small style="color: #6c757d; display: block; margin-top: 0.25rem;">
26
+ Letters, numbers, underscores, and hyphens only
27
+ </small>
28
+ </div>
29
+
30
+ <div style="margin-bottom: 1.5rem;">
31
+ <label for="environment" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
32
+ Environment <span style="color: #dc3545;">*</span>
33
+ </label>
34
+ <%= text_field_tag :environment, @environment,
35
+ placeholder: "e.g., production",
36
+ required: true,
37
+ autocomplete: "off",
38
+ style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;" %>
39
+ <small style="color: #6c757d; display: block; margin-top: 0.25rem;">
40
+ The environment this capsule is intended for
41
+ </small>
42
+ </div>
43
+
44
+ <div style="margin-bottom: 1.5rem;">
45
+ <label for="purpose" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
46
+ Purpose <span style="color: #dc3545;">*</span>
47
+ </label>
48
+ <%= text_area_tag :purpose, @purpose,
49
+ placeholder: "Describe what this SQL does and why it's needed...",
50
+ required: true,
51
+ rows: 3,
52
+ style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; font-family: inherit;" %>
53
+ <small style="color: #6c757d; display: block; margin-top: 0.25rem;">
54
+ Detailed description of the operation and its purpose
55
+ </small>
56
+ </div>
57
+
58
+ <div style="margin-bottom: 1.5rem;">
59
+ <label for="sql" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
60
+ SQL Statement <span style="color: #dc3545;">*</span>
61
+ </label>
62
+ <%= text_area_tag :sql, @sql,
63
+ placeholder: "-- Enter your SQL statement here\nSELECT * FROM users WHERE ...",
64
+ required: true,
65
+ rows: 12,
66
+ style: "width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; font-family: 'Monaco', 'Menlo', 'Courier New', monospace; font-size: 0.9rem;" %>
67
+ <small style="color: #6c757d; display: block; margin-top: 0.25rem;">
68
+ The SQL to execute. Review carefully before saving.
69
+ </small>
70
+ </div>
71
+
72
+ <div style="display: flex; gap: 1rem; justify-content: flex-end;">
73
+ <%= link_to "Cancel", dashboard_path,
74
+ style: "padding: 0.75rem 1.5rem; background: #6c757d; color: white; text-decoration: none; border-radius: 4px;" %>
75
+ <%= button_tag "Create Capsule",
76
+ type: "submit",
77
+ style: "padding: 0.75rem 1.5rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem;" %>
78
+ </div>
79
+ </div>
80
+ <% end %>
81
+ </div>
@@ -0,0 +1,85 @@
1
+ <div style="max-width: 900px; margin: 0 auto;">
2
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
3
+ <h2 style="margin: 0;">SQL Capsule: <%= @capsule.name %></h2>
4
+ <%= link_to "← Back to Dashboard", dashboard_path, style: "color: #007bff; text-decoration: none;" %>
5
+ </div>
6
+
7
+ <div style="background: white; padding: 2rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
8
+ <h3 style="margin-top: 0; margin-bottom: 1rem; color: #495057;">Capsule Details</h3>
9
+
10
+ <div style="display: grid; grid-template-columns: 200px 1fr; gap: 1rem; margin-bottom: 1rem;">
11
+ <div style="font-weight: 600; color: #6c757d;">Name:</div>
12
+ <div><%= @capsule.name %></div>
13
+
14
+ <div style="font-weight: 600; color: #6c757d;">Environment:</div>
15
+ <div><span class="badge badge-info"><%= @capsule.environment %></span></div>
16
+
17
+ <div style="font-weight: 600; color: #6c757d;">Created:</div>
18
+ <div><%= @capsule.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
19
+
20
+ <div style="font-weight: 600; color: #6c757d;">Checksum:</div>
21
+ <div style="font-family: 'Monaco', 'Menlo', 'Courier New', monospace; font-size: 0.85rem; color: #6c757d;">
22
+ <%= @checksum %>
23
+ </div>
24
+ </div>
25
+
26
+ <div style="margin-top: 1.5rem;">
27
+ <div style="font-weight: 600; color: #6c757d; margin-bottom: 0.5rem;">Purpose:</div>
28
+ <div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; border-left: 3px solid #007bff;">
29
+ <%= @capsule.purpose %>
30
+ </div>
31
+ </div>
32
+
33
+ <div style="margin-top: 1.5rem;">
34
+ <div style="font-weight: 600; color: #6c757d; margin-bottom: 0.5rem;">SQL Statement:</div>
35
+ <div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; border-left: 3px solid #28a745; overflow-x: auto;">
36
+ <pre style="margin: 0; font-family: 'Monaco', 'Menlo', 'Courier New', monospace; font-size: 0.9rem; white-space: pre-wrap; word-wrap: break-word;"><%= @capsule.sql %></pre>
37
+ </div>
38
+ </div>
39
+ </div>
40
+
41
+ <% if @can_execute %>
42
+ <div style="background: #fff3cd; border: 1px solid #ffc107; padding: 1.5rem; border-radius: 4px; margin-bottom: 2rem;">
43
+ <h4 style="margin-top: 0; color: #856404;">⚠️ Execution Warning</h4>
44
+ <p style="margin-bottom: 1rem; color: #856404;">
45
+ This will execute the SQL statement directly against the database.
46
+ This operation cannot be undone. Make sure you understand the implications.
47
+ </p>
48
+
49
+ <% if kill_switch_active? %>
50
+ <p style="margin-bottom: 1rem; color: #856404;">
51
+ <strong>Kill Switch is ACTIVE for <%= current_environment %> environment.</strong><br>
52
+ You must provide confirmation text to proceed.
53
+ </p>
54
+ <% end %>
55
+
56
+ <% form_id = "execute-capsule-form" %>
57
+ <%= form_with url: execute_sql_capsule_path(@capsule.name), method: :post, local: true, id: form_id do |f| %>
58
+ <div style="display: flex; gap: 1rem; align-items: flex-end;">
59
+ <div style="flex: 1;">
60
+ <button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
61
+ style="width: 100%; padding: 0.75rem 1.5rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; font-weight: 600;">
62
+ Execute SQL Capsule
63
+ </button>
64
+ </div>
65
+ </div>
66
+ <% end %>
67
+
68
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
69
+ operation: :execute_sql_capsule,
70
+ form_id: form_id,
71
+ title: 'Execute SQL Capsule',
72
+ message: "Are you sure you want to execute SQL capsule '#{@capsule.name}'? This will run the SQL statement directly against the database." %>
73
+ </div>
74
+ <% else %>
75
+ <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 1rem; border-radius: 4px; color: #721c24;">
76
+ <strong>Insufficient Permissions:</strong> You need Admin role to execute SQL capsules.
77
+ </div>
78
+ <% end %>
79
+
80
+ <div style="margin-top: 2rem;">
81
+ <%= link_to "Create Another Capsule", new_sql_capsule_path,
82
+ class: "btn btn-primary",
83
+ style: "padding: 0.75rem 1.5rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px; display: inline-block;" %>
84
+ </div>
85
+ </div>
@@ -46,8 +46,8 @@
46
46
  <div style="border: 1px solid #dee2e6; border-radius: 4px; padding: 1rem; background: #f8f9fa;">
47
47
  <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
48
48
  <div>
49
- <h4 style="margin: 0; color: #007bff;">
50
- <%= trigger.trigger_name %>
49
+ <h4 style="margin: 0;">
50
+ <%= link_to trigger.trigger_name, trigger_path(trigger), style: "color: #007bff; text-decoration: none;" %>
51
51
  <% if trigger.enabled %>
52
52
  <span class="badge badge-success">Enabled</span>
53
53
  <% else %>
@@ -64,6 +64,40 @@
64
64
  </div>
65
65
  </div>
66
66
 
67
+ <% if PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger) %>
68
+ <div style="margin-bottom: 1rem; display: flex; gap: 0.5rem;">
69
+ <% if trigger.enabled %>
70
+ <% form_id = "table-trigger-disable-#{trigger.id}-form" %>
71
+ <%= form_with url: disable_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
72
+ <%= f.hidden_field :redirect_to, value: table_path(@table_info[:table_name]) %>
73
+ <button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
74
+ style="padding: 0.5rem 1rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
75
+ Disable Trigger
76
+ </button>
77
+ <% end %>
78
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
79
+ operation: :ui_trigger_disable,
80
+ form_id: form_id,
81
+ title: 'Disable Trigger',
82
+ message: "Are you sure you want to disable trigger '#{trigger.trigger_name}' on table '#{@table_info[:table_name]}'?" %>
83
+ <% else %>
84
+ <% form_id = "table-trigger-enable-#{trigger.id}-form" %>
85
+ <%= form_with url: enable_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
86
+ <%= f.hidden_field :redirect_to, value: table_path(@table_info[:table_name]) %>
87
+ <button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
88
+ style="padding: 0.5rem 1rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
89
+ Enable Trigger
90
+ </button>
91
+ <% end %>
92
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
93
+ operation: :ui_trigger_enable,
94
+ form_id: form_id,
95
+ title: 'Enable Trigger',
96
+ message: "Are you sure you want to enable trigger '#{trigger.trigger_name}' on table '#{@table_info[:table_name]}'?" %>
97
+ <% end %>
98
+ </div>
99
+ <% end %>
100
+
67
101
  <% if trigger.definition.present? %>
68
102
  <% definition = JSON.parse(trigger.definition) rescue {} %>
69
103
  <div style="margin-bottom: 1rem;">
@@ -0,0 +1,129 @@
1
+ <% form_id = "trigger-drop-#{trigger.id}-form" %>
2
+
3
+ <%= form_with url: drop_trigger_path(trigger), method: :post, local: true, id: form_id, style: "margin: 0; display: inline-block;" do |f| %>
4
+ <%= f.hidden_field :redirect_to, value: params[:redirect_to] || trigger_path(trigger) %>
5
+
6
+ <button type="button" onclick="showDropModal('<%= form_id %>')"
7
+ style="padding: 0.75rem 1.5rem; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
8
+ Drop Trigger
9
+ </button>
10
+ <% end %>
11
+
12
+ <!-- Drop Modal -->
13
+ <div id="<%= form_id %>-modal" style="display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4);">
14
+ <div style="background-color: #fefefe; margin: 5% auto; padding: 0; border-radius: 8px; width: 90%; max-width: 600px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
15
+ <!-- Header -->
16
+ <div style="background: #dc3545; color: white; padding: 1.5rem; border-radius: 8px 8px 0 0;">
17
+ <div style="display: flex; justify-content: space-between; align-items: center;">
18
+ <h3 style="margin: 0;">⚠️ Drop Trigger</h3>
19
+ <button type="button" onclick="closeDropModal('<%= form_id %>')"
20
+ style="background: none; border: none; color: white; font-size: 1.5rem; cursor: pointer; padding: 0; width: 30px; height: 30px;">
21
+ &times;
22
+ </button>
23
+ </div>
24
+ </div>
25
+
26
+ <!-- Body -->
27
+ <div style="padding: 1.5rem;">
28
+ <div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 1rem; margin-bottom: 1.5rem;">
29
+ <strong style="color: #856404;">Warning:</strong>
30
+ <p style="margin: 0.5rem 0 0 0; color: #856404;">
31
+ This will permanently drop trigger '<strong><%= trigger.trigger_name %></strong>' from the database and remove it from the registry.
32
+ This action cannot be undone.
33
+ </p>
34
+ </div>
35
+
36
+ <form id="<%= form_id %>-reason-form">
37
+ <div style="margin-bottom: 1rem;">
38
+ <label for="<%= form_id %>-reason" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
39
+ Reason <span style="color: #dc3545;">*</span>
40
+ </label>
41
+ <textarea id="<%= form_id %>-reason" name="reason" required rows="3"
42
+ placeholder="Why are you dropping this trigger? This will be logged."
43
+ style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; font-family: inherit;"></textarea>
44
+ </div>
45
+
46
+ <% if kill_switch_active? %>
47
+ <div style="margin-bottom: 1rem;">
48
+ <label for="<%= form_id %>-confirmation" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
49
+ Confirmation Text <span style="color: #dc3545;">*</span>
50
+ </label>
51
+ <input type="text" id="<%= form_id %>-confirmation" name="confirmation_text" required
52
+ placeholder="<%= expected_confirmation_text(:trigger_drop) %>"
53
+ autocomplete="off"
54
+ style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; font-family: 'Monaco', 'Menlo', monospace;">
55
+ <small style="color: #6c757d; display: block; margin-top: 0.25rem;">
56
+ Type: <code><%= expected_confirmation_text(:trigger_drop) %></code>
57
+ </small>
58
+ </div>
59
+ <% end %>
60
+
61
+ <div style="display: flex; gap: 1rem; justify-content: flex-end;">
62
+ <button type="button" onclick="closeDropModal('<%= form_id %>')"
63
+ style="padding: 0.75rem 1.5rem; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer;">
64
+ Cancel
65
+ </button>
66
+ <button type="button" onclick="submitDrop('<%= form_id %>')"
67
+ style="padding: 0.75rem 1.5rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">
68
+ Drop Trigger
69
+ </button>
70
+ </div>
71
+ </form>
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <script>
77
+ function showDropModal(formId) {
78
+ document.getElementById(formId + '-modal').style.display = 'block';
79
+ }
80
+
81
+ function closeDropModal(formId) {
82
+ document.getElementById(formId + '-modal').style.display = 'none';
83
+ }
84
+
85
+ function submitDrop(formId) {
86
+ const form = document.getElementById(formId);
87
+ const reasonForm = document.getElementById(formId + '-reason-form');
88
+ const reason = document.getElementById(formId + '-reason').value;
89
+
90
+ if (!reason.trim()) {
91
+ alert('Please provide a reason for dropping this trigger.');
92
+ return;
93
+ }
94
+
95
+ <% if kill_switch_active? %>
96
+ const confirmation = document.getElementById(formId + '-confirmation').value;
97
+ if (!confirmation.trim()) {
98
+ alert('Please provide the confirmation text.');
99
+ return;
100
+ }
101
+
102
+ // Add confirmation to main form
103
+ const confirmationInput = document.createElement('input');
104
+ confirmationInput.type = 'hidden';
105
+ confirmationInput.name = 'confirmation_text';
106
+ confirmationInput.value = confirmation;
107
+ form.appendChild(confirmationInput);
108
+ <% end %>
109
+
110
+ // Add reason to main form
111
+ const reasonInput = document.createElement('input');
112
+ reasonInput.type = 'hidden';
113
+ reasonInput.name = 'reason';
114
+ reasonInput.value = reason;
115
+ form.appendChild(reasonInput);
116
+
117
+ form.submit();
118
+ }
119
+
120
+ // Close modal when clicking outside
121
+ window.onclick = function(event) {
122
+ const modals = document.querySelectorAll('[id$="-modal"]');
123
+ modals.forEach(modal => {
124
+ if (event.target == modal) {
125
+ modal.style.display = 'none';
126
+ }
127
+ });
128
+ }
129
+ </script>