pg_sql_triggers 1.1.0 → 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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -0
  3. data/CHANGELOG.md +61 -0
  4. data/COVERAGE.md +32 -28
  5. data/README.md +31 -2
  6. data/RELEASE.md +1 -1
  7. data/app/controllers/pg_sql_triggers/application_controller.rb +1 -1
  8. data/app/controllers/pg_sql_triggers/dashboard_controller.rb +10 -0
  9. data/app/controllers/pg_sql_triggers/migrations_controller.rb +62 -10
  10. data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +161 -0
  11. data/app/controllers/pg_sql_triggers/triggers_controller.rb +165 -0
  12. data/app/models/pg_sql_triggers/trigger_registry.rb +123 -0
  13. data/app/views/pg_sql_triggers/dashboard/index.html.erb +40 -2
  14. data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +81 -0
  15. data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +85 -0
  16. data/app/views/pg_sql_triggers/tables/show.html.erb +36 -2
  17. data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +129 -0
  18. data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +136 -0
  19. data/app/views/pg_sql_triggers/triggers/show.html.erb +186 -0
  20. data/config/routes.rb +9 -0
  21. data/docs/README.md +2 -2
  22. data/docs/api-reference.md +252 -4
  23. data/docs/getting-started.md +1 -1
  24. data/docs/kill-switch.md +3 -3
  25. data/docs/web-ui.md +82 -17
  26. data/lib/generators/pg_sql_triggers/templates/README +1 -1
  27. data/lib/pg_sql_triggers/registry/manager.rb +28 -13
  28. data/lib/pg_sql_triggers/registry.rb +41 -0
  29. data/lib/pg_sql_triggers/sql/capsule.rb +79 -0
  30. data/lib/pg_sql_triggers/sql/executor.rb +200 -0
  31. data/lib/pg_sql_triggers/version.rb +1 -1
  32. metadata +18 -12
@@ -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>
@@ -0,0 +1,136 @@
1
+ <% form_id = "trigger-re-execute-#{trigger.id}-form" %>
2
+
3
+ <%= form_with url: re_execute_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="showReExecuteModal('<%= form_id %>')"
7
+ style="padding: 0.75rem 1.5rem; background: #ffc107; color: #000; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
8
+ Re-Execute Trigger
9
+ </button>
10
+ <% end %>
11
+
12
+ <!-- Re-Execute 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: 3% auto; padding: 0; border-radius: 8px; width: 90%; max-width: 800px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
15
+ <!-- Header -->
16
+ <div style="background: #ffc107; color: #000; 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;">🔄 Re-Execute Trigger</h3>
19
+ <button type="button" onclick="closeReExecuteModal('<%= form_id %>')"
20
+ style="background: none; border: none; color: #000; 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;">Notice:</strong>
30
+ <p style="margin: 0.5rem 0 0 0; color: #856404;">
31
+ This will drop and re-create trigger '<strong><%= trigger.trigger_name %></strong>' based on the expected definition from the registry.
32
+ This can fix drift issues.
33
+ </p>
34
+ </div>
35
+
36
+ <!-- Show Drift Diff -->
37
+ <% if drift_info[:expected_sql].present? %>
38
+ <div style="margin-bottom: 1.5rem;">
39
+ <h4 style="margin-bottom: 1rem;">SQL Comparison</h4>
40
+
41
+ <div style="margin-bottom: 1rem;">
42
+ <div style="color: #28a745; font-weight: 600; margin-bottom: 0.5rem;">✓ Expected SQL (will be applied):</div>
43
+ <pre style="background: #d4edda; padding: 1rem; border-radius: 4px; overflow-x: auto; border: 1px solid #c3e6cb; font-size: 0.85rem; max-height: 200px;"><code><%= drift_info[:expected_sql] %></code></pre>
44
+ </div>
45
+
46
+ <div>
47
+ <div style="color: #dc3545; font-weight: 600; margin-bottom: 0.5rem;">✗ Current SQL (will be replaced):</div>
48
+ <pre style="background: #f8d7da; padding: 1rem; border-radius: 4px; overflow-x: auto; border: 1px solid #f5c6cb; font-size: 0.85rem; max-height: 200px;"><code><%= drift_info[:actual_sql] || "Not found in database" %></code></pre>
49
+ </div>
50
+ </div>
51
+ <% end %>
52
+
53
+ <form id="<%= form_id %>-reason-form">
54
+ <div style="margin-bottom: 1rem;">
55
+ <label for="<%= form_id %>-reason" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
56
+ Reason <span style="color: #dc3545;">*</span>
57
+ </label>
58
+ <textarea id="<%= form_id %>-reason" name="reason" required rows="2"
59
+ placeholder="Why are you re-executing this trigger? This will be logged."
60
+ style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; font-family: inherit;"></textarea>
61
+ </div>
62
+
63
+ <% if kill_switch_active? %>
64
+ <div style="margin-bottom: 1rem;">
65
+ <label for="<%= form_id %>-confirmation" style="display: block; font-weight: 600; margin-bottom: 0.5rem;">
66
+ Confirmation Text <span style="color: #dc3545;">*</span>
67
+ </label>
68
+ <input type="text" id="<%= form_id %>-confirmation" name="confirmation_text" required
69
+ placeholder="<%= expected_confirmation_text(:trigger_re_execute) %>"
70
+ autocomplete="off"
71
+ style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; font-family: 'Monaco', 'Menlo', monospace;">
72
+ <small style="color: #6c757d; display: block; margin-top: 0.25rem;">
73
+ Type: <code><%= expected_confirmation_text(:trigger_re_execute) %></code>
74
+ </small>
75
+ </div>
76
+ <% end %>
77
+
78
+ <div style="display: flex; gap: 1rem; justify-content: flex-end;">
79
+ <button type="button" onclick="closeReExecuteModal('<%= form_id %>')"
80
+ style="padding: 0.75rem 1.5rem; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer;">
81
+ Cancel
82
+ </button>
83
+ <button type="button" onclick="submitReExecute('<%= form_id %>')"
84
+ style="padding: 0.75rem 1.5rem; background: #ffc107; color: #000; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">
85
+ Re-Execute Trigger
86
+ </button>
87
+ </div>
88
+ </form>
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ <script>
94
+ function showReExecuteModal(formId) {
95
+ document.getElementById(formId + '-modal').style.display = 'block';
96
+ }
97
+
98
+ function closeReExecuteModal(formId) {
99
+ document.getElementById(formId + '-modal').style.display = 'none';
100
+ }
101
+
102
+ function submitReExecute(formId) {
103
+ const form = document.getElementById(formId);
104
+ const reasonForm = document.getElementById(formId + '-reason-form');
105
+ const reason = document.getElementById(formId + '-reason').value;
106
+
107
+ if (!reason.trim()) {
108
+ alert('Please provide a reason for re-executing this trigger.');
109
+ return;
110
+ }
111
+
112
+ <% if kill_switch_active? %>
113
+ const confirmation = document.getElementById(formId + '-confirmation').value;
114
+ if (!confirmation.trim()) {
115
+ alert('Please provide the confirmation text.');
116
+ return;
117
+ }
118
+
119
+ // Add confirmation to main form
120
+ const confirmationInput = document.createElement('input');
121
+ confirmationInput.type = 'hidden';
122
+ confirmationInput.name = 'confirmation_text';
123
+ confirmationInput.value = confirmation;
124
+ form.appendChild(confirmationInput);
125
+ <% end %>
126
+
127
+ // Add reason to main form
128
+ const reasonInput = document.createElement('input');
129
+ reasonInput.type = 'hidden';
130
+ reasonInput.name = 'reason';
131
+ reasonInput.value = reason;
132
+ form.appendChild(reasonInput);
133
+
134
+ form.submit();
135
+ }
136
+ </script>
@@ -0,0 +1,186 @@
1
+ <div style="margin-bottom: 2rem;">
2
+ <h2>Trigger Details: <%= @trigger.trigger_name %></h2>
3
+ <%= link_to "← Back to Dashboard", dashboard_path, class: "btn", style: "background: #6c757d; color: white; text-decoration: none; margin-right: 1rem;" %>
4
+ <%= link_to "View Table", table_path(@trigger.table_name), class: "btn", style: "background: #007bff; color: white; text-decoration: none;" %>
5
+ </div>
6
+
7
+ <!-- Drift Warning -->
8
+ <% if @drift_info[:has_drift] %>
9
+ <div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 1.5rem; border-radius: 4px; margin-bottom: 2rem;">
10
+ <h3 style="margin-top: 0; color: #856404;">⚠ Drift Detected</h3>
11
+ <p style="color: #856404; margin-bottom: 0;">
12
+ This trigger has drifted from its expected state. Type: <strong><%= @drift_info[:drift_type] %></strong>
13
+ </p>
14
+ </div>
15
+ <% end %>
16
+
17
+ <!-- Trigger Summary -->
18
+ <div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
19
+ <h3 style="margin-top: 0;">Summary</h3>
20
+
21
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem; margin-bottom: 1.5rem;">
22
+ <div>
23
+ <div style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.25rem;">Status</div>
24
+ <div>
25
+ <% if @trigger.enabled %>
26
+ <span class="badge badge-success">Enabled</span>
27
+ <% else %>
28
+ <span class="badge badge-danger">Disabled</span>
29
+ <% end %>
30
+ </div>
31
+ </div>
32
+
33
+ <div>
34
+ <div style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.25rem;">Table</div>
35
+ <div><strong><%= @trigger.table_name %></strong></div>
36
+ </div>
37
+
38
+ <div>
39
+ <div style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.25rem;">Version</div>
40
+ <div><strong><%= @trigger.version %></strong></div>
41
+ </div>
42
+
43
+ <div>
44
+ <div style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.25rem;">Source</div>
45
+ <div><span class="badge badge-info"><%= @trigger.source %></span></div>
46
+ </div>
47
+ </div>
48
+
49
+ <% if @trigger.environment.present? %>
50
+ <div style="margin-bottom: 1rem;">
51
+ <div style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.25rem;">Environment</div>
52
+ <div><strong><%= @trigger.environment %></strong></div>
53
+ </div>
54
+ <% end %>
55
+
56
+ <% if @trigger.installed_at.present? %>
57
+ <div style="margin-bottom: 1rem;">
58
+ <div style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.25rem;">Installed At</div>
59
+ <div><%= @trigger.installed_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
60
+ </div>
61
+ <% end %>
62
+
63
+ <div>
64
+ <div style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.25rem;">Created At</div>
65
+ <div><%= @trigger.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></div>
66
+ </div>
67
+ </div>
68
+
69
+ <!-- Trigger Definition -->
70
+ <% if @trigger.definition.present? %>
71
+ <% definition = JSON.parse(@trigger.definition) rescue {} %>
72
+ <div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
73
+ <h3 style="margin-top: 0;">Trigger Configuration</h3>
74
+
75
+ <div style="margin-bottom: 1rem;">
76
+ <strong>Function:</strong>
77
+ <code style="background: #e9ecef; padding: 0.25rem 0.5rem; border-radius: 2px;"><%= definition["function_name"] || "N/A" %></code>
78
+ </div>
79
+
80
+ <% if definition["timing"].present? %>
81
+ <div style="margin-bottom: 1rem;">
82
+ <strong>Timing:</strong>
83
+ <span class="badge badge-info"><%= definition["timing"].upcase %></span>
84
+ </div>
85
+ <% end %>
86
+
87
+ <% if definition["events"].present? %>
88
+ <div style="margin-bottom: 1rem;">
89
+ <strong>Events:</strong>
90
+ <% definition["events"].each do |event| %>
91
+ <span class="badge badge-info" style="margin-left: 0.25rem;"><%= event.upcase %></span>
92
+ <% end %>
93
+ </div>
94
+ <% end %>
95
+
96
+ <% if definition["condition"].present? %>
97
+ <div style="margin-bottom: 1rem;">
98
+ <strong>Condition (WHEN):</strong>
99
+ <pre style="background: #f8f9fa; padding: 0.5rem; border-radius: 4px; margin-top: 0.25rem; overflow-x: auto;"><code><%= definition["condition"] %></code></pre>
100
+ </div>
101
+ <% end %>
102
+
103
+ <% if definition["environments"].present? %>
104
+ <div>
105
+ <strong>Environments:</strong>
106
+ <% definition["environments"].each do |env| %>
107
+ <span class="badge badge-secondary" style="margin-left: 0.25rem;"><%= env %></span>
108
+ <% end %>
109
+ </div>
110
+ <% end %>
111
+ </div>
112
+ <% end %>
113
+
114
+ <!-- SQL Diff (if drift detected) -->
115
+ <% if @drift_info[:has_drift] && @drift_info[:expected_sql].present? %>
116
+ <div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
117
+ <h3 style="margin-top: 0;">SQL Drift Comparison</h3>
118
+
119
+ <div style="margin-bottom: 1.5rem;">
120
+ <h4 style="color: #28a745; margin-bottom: 0.5rem;">Expected SQL (from DSL)</h4>
121
+ <pre style="background: #d4edda; padding: 1rem; border-radius: 4px; overflow-x: auto; border: 1px solid #c3e6cb;"><code><%= @drift_info[:expected_sql] %></code></pre>
122
+ </div>
123
+
124
+ <div>
125
+ <h4 style="color: #dc3545; margin-bottom: 0.5rem;">Actual SQL (from database)</h4>
126
+ <pre style="background: #f8d7da; padding: 1rem; border-radius: 4px; overflow-x: auto; border: 1px solid #f5c6cb;"><code><%= @drift_info[:actual_sql] || "Not found in database" %></code></pre>
127
+ </div>
128
+ </div>
129
+ <% end %>
130
+
131
+ <!-- Function Body -->
132
+ <% if @trigger.function_body.present? %>
133
+ <div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
134
+ <h3 style="margin-top: 0;">Function Body</h3>
135
+ <pre style="background: #f8f9fa; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><%= @trigger.function_body %></code></pre>
136
+ </div>
137
+ <% end %>
138
+
139
+ <!-- Action Buttons -->
140
+ <% if PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger) %>
141
+ <div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
142
+ <h3 style="margin-top: 0;">Actions</h3>
143
+
144
+ <div style="display: flex; gap: 1rem; flex-wrap: wrap;">
145
+ <% if @trigger.enabled %>
146
+ <% form_id = "trigger-detail-disable-form" %>
147
+ <%= form_with url: disable_trigger_path(@trigger), method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
148
+ <%= f.hidden_field :redirect_to, value: trigger_path(@trigger) %>
149
+ <button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
150
+ style="padding: 0.75rem 1.5rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
151
+ Disable Trigger
152
+ </button>
153
+ <% end %>
154
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
155
+ operation: :ui_trigger_disable,
156
+ form_id: form_id,
157
+ title: 'Disable Trigger',
158
+ message: "Are you sure you want to disable trigger '#{@trigger.trigger_name}'?" %>
159
+ <% else %>
160
+ <% form_id = "trigger-detail-enable-form" %>
161
+ <%= form_with url: enable_trigger_path(@trigger), method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
162
+ <%= f.hidden_field :redirect_to, value: trigger_path(@trigger) %>
163
+ <button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
164
+ style="padding: 0.75rem 1.5rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
165
+ Enable Trigger
166
+ </button>
167
+ <% end %>
168
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
169
+ operation: :ui_trigger_enable,
170
+ form_id: form_id,
171
+ title: 'Enable Trigger',
172
+ message: "Are you sure you want to enable trigger '#{@trigger.trigger_name}'?" %>
173
+ <% end %>
174
+
175
+ <!-- Re-execute button (for drifted triggers) -->
176
+ <% if @drift_info[:has_drift] && PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger) %>
177
+ <%= render 'pg_sql_triggers/triggers/re_execute_modal', trigger: @trigger, drift_info: @drift_info %>
178
+ <% end %>
179
+
180
+ <!-- Drop button (Admin only) -->
181
+ <% if PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger) %>
182
+ <%= render 'pg_sql_triggers/triggers/drop_modal', trigger: @trigger %>
183
+ <% end %>
184
+ </div>
185
+ </div>
186
+ <% end %>
data/config/routes.rb CHANGED
@@ -28,6 +28,15 @@ begin
28
28
  post :redo
29
29
  end
30
30
  end
31
+
32
+ resources :triggers, only: [:show] do
33
+ member do
34
+ post :enable
35
+ post :disable
36
+ post :drop
37
+ post :re_execute
38
+ end
39
+ end
31
40
  end
32
41
  rescue ArgumentError => e
33
42
  # Ignore duplicate route errors (routes may already be drawn in tests)
data/docs/README.md CHANGED
@@ -61,6 +61,6 @@ When updating documentation:
61
61
 
62
62
  ## External Resources
63
63
 
64
- - [GitHub Repository](https://github.com/samaswin87/pg_sql_triggers)
65
- - [Example Application](https://github.com/samaswin87/pg_triggers_example)
64
+ - [GitHub Repository](https://github.com/samaswin/pg_sql_triggers)
65
+ - [Example Application](https://github.com/samaswin/pg_triggers_example)
66
66
  - [RubyGems](https://rubygems.org/gems/pg_sql_triggers)