pg_sql_triggers 1.0.0 → 1.0.1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.erb_lint.yml +47 -0
  3. data/.rubocop.yml +4 -1
  4. data/CHANGELOG.md +29 -1
  5. data/Goal.md +408 -123
  6. data/README.md +47 -215
  7. data/app/controllers/pg_sql_triggers/application_controller.rb +46 -0
  8. data/app/controllers/pg_sql_triggers/generator_controller.rb +10 -4
  9. data/app/controllers/pg_sql_triggers/migrations_controller.rb +18 -0
  10. data/app/models/pg_sql_triggers/trigger_registry.rb +20 -2
  11. data/app/views/layouts/pg_sql_triggers/application.html.erb +34 -1
  12. data/app/views/pg_sql_triggers/dashboard/index.html.erb +70 -30
  13. data/app/views/pg_sql_triggers/generator/new.html.erb +4 -4
  14. data/app/views/pg_sql_triggers/generator/preview.html.erb +14 -6
  15. data/app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb +189 -0
  16. data/app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb +40 -0
  17. data/app/views/pg_sql_triggers/tables/index.html.erb +0 -2
  18. data/app/views/pg_sql_triggers/tables/show.html.erb +3 -4
  19. data/db/migrate/20251222000001_create_pg_sql_triggers_tables.rb +1 -1
  20. data/docs/README.md +66 -0
  21. data/docs/api-reference.md +663 -0
  22. data/docs/configuration.md +541 -0
  23. data/docs/getting-started.md +135 -0
  24. data/docs/kill-switch.md +586 -0
  25. data/docs/screenshots/.gitkeep +1 -0
  26. data/docs/screenshots/Generate Trigger.png +0 -0
  27. data/docs/screenshots/Triggers Page.png +0 -0
  28. data/docs/screenshots/kill error.png +0 -0
  29. data/docs/screenshots/kill modal for migration down.png +0 -0
  30. data/docs/usage-guide.md +420 -0
  31. data/docs/web-ui.md +339 -0
  32. data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +1 -1
  33. data/lib/generators/pg_sql_triggers/templates/initializer.rb +36 -2
  34. data/lib/pg_sql_triggers/generator/service.rb +1 -1
  35. data/lib/pg_sql_triggers/migration.rb +1 -1
  36. data/lib/pg_sql_triggers/migrator.rb +27 -3
  37. data/lib/pg_sql_triggers/registry/manager.rb +6 -6
  38. data/lib/pg_sql_triggers/sql/kill_switch.rb +300 -0
  39. data/lib/pg_sql_triggers/testing/dry_run.rb +5 -7
  40. data/lib/pg_sql_triggers/testing/safe_executor.rb +23 -11
  41. data/lib/pg_sql_triggers/version.rb +1 -1
  42. data/lib/pg_sql_triggers.rb +12 -0
  43. data/lib/tasks/trigger_migrations.rake +33 -0
  44. metadata +35 -5
@@ -73,7 +73,7 @@
73
73
  <!-- Migration Management Section -->
74
74
  <div style="margin-top: 3rem;">
75
75
  <h3>Trigger Migrations</h3>
76
-
76
+
77
77
  <div style="background: white; padding: 1.5rem; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem;">
78
78
  <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.5rem;">
79
79
  <div style="padding: 1rem; background: #f8f9fa; border-radius: 4px;">
@@ -97,25 +97,43 @@
97
97
  <!-- Migration Action Buttons -->
98
98
  <div style="display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap;">
99
99
  <% if @pending_migrations.any? %>
100
- <%= form_with url: up_migrations_path, method: :post, local: true, style: "margin: 0;" do |f| %>
101
- <%= f.submit "Apply All Pending Migrations",
102
- style: "padding: 0.75rem 1.5rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;",
103
- data: { confirm: "Are you sure you want to apply #{@pending_migrations.count} pending migration(s)?" } %>
100
+ <%= form_with url: up_migrations_path, method: :post, local: true, id: "migration-up-all-form", style: "margin: 0;" do |f| %>
101
+ <button type="button" onclick="showKillSwitchModal('migration-up-all-form')"
102
+ style="padding: 0.75rem 1.5rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
103
+ Apply All Pending Migrations
104
+ </button>
104
105
  <% end %>
106
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
107
+ operation: :ui_migration_up,
108
+ form_id: 'migration-up-all-form',
109
+ title: 'Apply All Pending Migrations',
110
+ message: "Are you sure you want to apply #{@pending_migrations.count} pending migration(s)?" %>
105
111
  <% end %>
106
-
112
+
107
113
  <% if @current_migration_version > 0 %>
108
- <%= form_with url: down_migrations_path, method: :post, local: true, style: "margin: 0;" do |f| %>
109
- <%= f.submit "Rollback Last Migration",
110
- style: "padding: 0.75rem 1.5rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;",
111
- data: { confirm: "Are you sure you want to rollback the last migration?" } %>
114
+ <%= form_with url: down_migrations_path, method: :post, local: true, id: "migration-down-form", style: "margin: 0;" do |f| %>
115
+ <button type="button" onclick="showKillSwitchModal('migration-down-form')"
116
+ style="padding: 0.75rem 1.5rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
117
+ Rollback Last Migration
118
+ </button>
112
119
  <% end %>
113
-
114
- <%= form_with url: redo_migrations_path, method: :post, local: true, style: "margin: 0;" do |f| %>
115
- <%= f.submit "Redo Last Migration",
116
- style: "padding: 0.75rem 1.5rem; background: #ffc107; color: #212529; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;",
117
- data: { confirm: "Are you sure you want to redo the last migration?" } %>
120
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
121
+ operation: :ui_migration_down,
122
+ form_id: 'migration-down-form',
123
+ title: 'Rollback Last Migration',
124
+ message: 'Are you sure you want to rollback the last migration?' %>
125
+
126
+ <%= form_with url: redo_migrations_path, method: :post, local: true, id: "migration-redo-form", style: "margin: 0;" do |f| %>
127
+ <button type="button" onclick="showKillSwitchModal('migration-redo-form')"
128
+ style="padding: 0.75rem 1.5rem; background: #ffc107; color: #212529; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: 600;">
129
+ Redo Last Migration
130
+ </button>
118
131
  <% end %>
132
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
133
+ operation: :ui_migration_redo,
134
+ form_id: 'migration-redo-form',
135
+ title: 'Redo Last Migration',
136
+ message: 'Are you sure you want to redo the last migration?' %>
119
137
  <% end %>
120
138
  </div>
121
139
 
@@ -161,25 +179,47 @@
161
179
  <td>
162
180
  <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
163
181
  <% if migration[:status] == "down" %>
164
- <%= form_with url: up_migrations_path, method: :post, local: true, style: "margin: 0;" do |f| %>
182
+ <% form_id = "migration-up-#{migration[:version]}-form" %>
183
+ <%= form_with url: up_migrations_path, method: :post, local: true, id: form_id, style: "margin: 0;" do |f| %>
165
184
  <%= f.hidden_field :version, value: migration[:version] %>
166
- <%= f.submit "Up",
167
- style: "padding: 0.25rem 0.75rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;",
168
- data: { confirm: "Apply migration #{migration[:version]}?" } %>
185
+ <button type="button" onclick="showKillSwitchModal('<%= form_id %>')"
186
+ style="padding: 0.25rem 0.75rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
187
+ Up
188
+ </button>
169
189
  <% end %>
190
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
191
+ operation: :ui_migration_up,
192
+ form_id: form_id,
193
+ title: 'Apply Migration',
194
+ message: "Are you sure you want to apply migration #{migration[:version]}?" %>
170
195
  <% else %>
171
- <%= form_with url: down_migrations_path, method: :post, local: true, style: "margin: 0;" do |f| %>
196
+ <% form_id_down = "migration-down-#{migration[:version]}-form" %>
197
+ <%= form_with url: down_migrations_path, method: :post, local: true, id: form_id_down, style: "margin: 0;" do |f| %>
172
198
  <%= f.hidden_field :version, value: migration[:version] %>
173
- <%= f.submit "Down",
174
- style: "padding: 0.25rem 0.75rem; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;",
175
- data: { confirm: "Rollback to version #{migration[:version]}?" } %>
199
+ <button type="button" onclick="showKillSwitchModal('<%= form_id_down %>')"
200
+ style="padding: 0.25rem 0.75rem; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
201
+ Down
202
+ </button>
176
203
  <% end %>
177
- <%= form_with url: redo_migrations_path, method: :post, local: true, style: "margin: 0;" do |f| %>
204
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
205
+ operation: :ui_migration_down,
206
+ form_id: form_id_down,
207
+ title: 'Rollback Migration',
208
+ message: "Are you sure you want to rollback to version #{migration[:version]}?" %>
209
+
210
+ <% form_id_redo = "migration-redo-#{migration[:version]}-form" %>
211
+ <%= form_with url: redo_migrations_path, method: :post, local: true, id: form_id_redo, style: "margin: 0;" do |f| %>
178
212
  <%= f.hidden_field :version, value: migration[:version] %>
179
- <%= f.submit "Redo",
180
- style: "padding: 0.25rem 0.75rem; background: #ffc107; color: #212529; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;",
181
- data: { confirm: "Redo migration #{migration[:version]}?" } %>
213
+ <button type="button" onclick="showKillSwitchModal('<%= form_id_redo %>')"
214
+ style="padding: 0.25rem 0.75rem; background: #ffc107; color: #212529; border: none; border-radius: 3px; cursor: pointer; font-size: 0.75rem;">
215
+ Redo
216
+ </button>
182
217
  <% end %>
218
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
219
+ operation: :ui_migration_redo,
220
+ form_id: form_id_redo,
221
+ title: 'Redo Migration',
222
+ message: "Are you sure you want to redo migration #{migration[:version]}?" %>
183
223
  <% end %>
184
224
  </div>
185
225
  </td>
@@ -187,13 +227,13 @@
187
227
  <% end %>
188
228
  </tbody>
189
229
  </table>
190
-
230
+
191
231
  <!-- Pagination Controls -->
192
232
  <% if @total_pages > 1 %>
193
233
  <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #dee2e6;">
194
234
  <div>
195
235
  <% if @page > 1 %>
196
- <%= link_to "← Previous", dashboard_path(page: @page - 1, per_page: @per_page),
236
+ <%= link_to "← Previous", dashboard_path(page: @page - 1, per_page: @per_page),
197
237
  style: "padding: 0.5rem 1rem; background: #007bff; color: white; text-decoration: none; border-radius: 4px; margin-right: 0.5rem;" %>
198
238
  <% end %>
199
239
  <% if @page < @total_pages %>
@@ -206,7 +246,7 @@
206
246
  </div>
207
247
  <div style="display: flex; align-items: center; gap: 0.5rem;">
208
248
  <label style="color: #6c757d; font-size: 0.875rem;">Per page:</label>
209
- <select onchange="window.location.href='<%= dashboard_path %>?page=1&per_page=' + this.value"
249
+ <select onchange="window.location.href='<%= dashboard_path %>?page=1&per_page=' + this.value"
210
250
  style="padding: 0.25rem 0.5rem; border: 1px solid #ced4da; border-radius: 4px;">
211
251
  <option value="10" <%= 'selected' if @per_page == 10 %>>10</option>
212
252
  <option value="20" <%= 'selected' if @per_page == 20 %>>20</option>
@@ -64,7 +64,7 @@
64
64
 
65
65
  <div style="margin-bottom: 1rem;">
66
66
  <%= f.label :function_body, "Function Body *", style: "display: block; font-weight: 500; margin-bottom: 0.25rem;" %>
67
- <%
67
+ <%
68
68
  default_function_body = @form.function_body.presence || @form.default_function_body
69
69
  %>
70
70
  <%= f.text_area :function_body,
@@ -282,10 +282,10 @@ document.getElementById('function-name-input')?.addEventListener('input', functi
282
282
  const functionBodyTextarea = document.getElementById('function-body-textarea');
283
283
  if (functionBodyTextarea) {
284
284
  const newTemplate = generateFunctionBody(functionName);
285
-
285
+
286
286
  // Update placeholder
287
287
  functionBodyTextarea.placeholder = newTemplate;
288
-
288
+
289
289
  // Update value only if textarea is empty or matches template pattern
290
290
  if (!functionBodyTextarea.value || isTemplateValue(functionBodyTextarea.value)) {
291
291
  functionBodyTextarea.value = newTemplate;
@@ -320,7 +320,7 @@ document.getElementById('table-name-select')?.addEventListener('change', functio
320
320
  .then(data => {
321
321
  if (data.valid) {
322
322
  messageDiv.innerHTML = '<span style="color: #28a745;">✓ Table exists (' + data.column_count + ' columns)</span>';
323
-
323
+
324
324
  // Try to fetch existing triggers for this table
325
325
  if (infoDiv && triggersList) {
326
326
  fetch('<%= tables_path %>/' + encodeURIComponent(tableName) + '.json', {
@@ -19,7 +19,7 @@
19
19
  </div>
20
20
 
21
21
  <!-- Actions -->
22
- <%= form_with url: generator_index_path, method: :post, scope: :pg_sql_triggers_generator_form do |f| %>
22
+ <%= form_with url: generator_index_path, method: :post, scope: :pg_sql_triggers_generator_form, id: "generator-create-form" do |f| %>
23
23
  <!-- SQL Validation Result -->
24
24
  <% if @sql_validation %>
25
25
  <div style="margin-bottom: 2rem; padding: 1rem; border-radius: 4px; <%= @sql_validation[:valid] ? 'background: #d4edda; border-left: 4px solid #28a745;' : 'background: #f8d7da; border-left: 4px solid #dc3545;' %>">
@@ -48,7 +48,7 @@
48
48
  <p style="color: #6c757d; margin-bottom: 0.5rem; font-size: 0.875rem;">
49
49
  You can edit the function body below before generating the trigger files.
50
50
  </p>
51
- <%= f.text_area :function_body,
51
+ <%= f.text_area :function_body,
52
52
  value: @function_content,
53
53
  rows: 20,
54
54
  required: true,
@@ -69,9 +69,17 @@
69
69
  <% end %>
70
70
 
71
71
  <div style="display: flex; gap: 1rem;">
72
- <%= f.submit "Generate Files", class: "btn btn-success",
73
- data: { confirm: "This will create files on disk and register the trigger. Continue?" } %>
74
- <%= link_to "Back to Edit", new_generator_path, class: "btn", style: "background: #6c757d; color: white; text-decoration: none;" %>
75
- <%= link_to "Cancel", root_path, class: "btn", style: "background: #6c757d; color: white; text-decoration: none;" %>
72
+ <button type="button" onclick="showKillSwitchModal('generator-create-form')" class="btn btn-success"
73
+ style="padding: 0.75rem 1.5rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">
74
+ Generate Files
75
+ </button>
76
+ <%= link_to "Back to Edit", new_generator_path, class: "btn", style: "background: #6c757d; color: white; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 4px;" %>
77
+ <%= link_to "Cancel", root_path, class: "btn", style: "background: #6c757d; color: white; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 4px;" %>
76
78
  </div>
77
79
  <% end %>
80
+
81
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
82
+ operation: :ui_trigger_generate,
83
+ form_id: 'generator-create-form',
84
+ title: 'Generate Trigger Files',
85
+ message: 'This will create files on disk and register the trigger. Continue?' %>
@@ -0,0 +1,189 @@
1
+ <%# Confirmation Modal for Dangerous Operations %>
2
+ <%#
3
+ Usage:
4
+ <%= render 'pg_sql_triggers/shared/confirmation_modal',
5
+ operation: :trigger_migrate_up,
6
+ form_id: 'migration-form',
7
+ title: 'Confirm Migration',
8
+ message: 'Are you sure you want to run this migration?'
9
+ %>
10
+
11
+ <%
12
+ operation = local_assigns[:operation] || :unknown_operation
13
+ form_id = local_assigns[:form_id] || 'confirmation-form'
14
+ title = local_assigns[:title] || 'Confirm Action'
15
+ message = local_assigns[:message] || 'This action requires confirmation.'
16
+
17
+ # Generate expected confirmation text
18
+ if PgSqlTriggers.respond_to?(:kill_switch_confirmation_pattern) &&
19
+ PgSqlTriggers.kill_switch_confirmation_pattern.respond_to?(:call)
20
+ expected_confirmation = PgSqlTriggers.kill_switch_confirmation_pattern.call(operation)
21
+ else
22
+ expected_confirmation = "EXECUTE #{operation.to_s.upcase}"
23
+ end
24
+ %>
25
+
26
+ <div id="kill-switch-modal-<%= form_id %>" class="kill-switch-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);">
27
+ <div class="modal-content" style="background-color: #fefefe; margin: 10% auto; padding: 20px; border: 1px solid #888; border-radius: 8px; width: 80%; max-width: 600px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); box-sizing: border-box;">
28
+ <div class="modal-header" style="border-bottom: 1px solid #dee2e6; padding-bottom: 12px; margin-bottom: 16px;">
29
+ <h3 style="margin: 0; color: #dc3545; display: flex; align-items: center; gap: 8px;">
30
+ <span style="font-size: 24px;">⚠️</span>
31
+ <%= title %>
32
+ </h3>
33
+ </div>
34
+
35
+ <div class="modal-body" style="margin-bottom: 20px; box-sizing: border-box;">
36
+ <p style="color: #495057; line-height: 1.6;"><%= message %></p>
37
+
38
+ <% if kill_switch_active? %>
39
+ <div style="background-color: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 12px; margin: 16px 0; box-sizing: border-box;">
40
+ <p style="margin: 0 0 8px 0; font-weight: bold; color: #856404;">
41
+ 🛡️ Kill Switch Protection Active
42
+ </p>
43
+ <p style="margin: 0 0 12px 0; color: #856404; font-size: 14px;">
44
+ You must enter the exact confirmation text below to proceed:
45
+ </p>
46
+ <div style="background-color: #f8f9fa; border: 2px solid #ffc107; border-radius: 4px; padding: 8px; font-family: monospace; font-size: 14px; font-weight: bold; text-align: center; color: #856404; word-break: break-word; box-sizing: border-box;">
47
+ <%= expected_confirmation %>
48
+ </div>
49
+ </div>
50
+
51
+ <div style="margin-top: 16px; box-sizing: border-box;">
52
+ <label for="confirmation-text-<%= form_id %>" style="display: block; margin-bottom: 8px; font-weight: bold; color: #495057;">
53
+ Confirmation Text:
54
+ </label>
55
+ <input
56
+ type="text"
57
+ id="confirmation-text-<%= form_id %>"
58
+ name="confirmation_text"
59
+ class="form-control"
60
+ placeholder="Type the confirmation text above"
61
+ style="width: 100%; max-width: 100%; padding: 8px 12px; border: 1px solid #ced4da; border-radius: 4px; font-family: monospace; box-sizing: border-box;"
62
+ autocomplete="off">
63
+ <small style="display: block; margin-top: 4px; color: #6c757d;">
64
+ Case-sensitive. Must match exactly.
65
+ </small>
66
+ </div>
67
+ <% end %>
68
+ </div>
69
+
70
+ <div class="modal-footer" style="border-top: 1px solid #dee2e6; padding-top: 12px; display: flex; gap: 8px; justify-content: flex-end;">
71
+ <button
72
+ type="button"
73
+ class="btn-cancel"
74
+ onclick="closeKillSwitchModal('<%= form_id %>')"
75
+ style="padding: 8px 16px; border: 1px solid #6c757d; background-color: #6c757d; color: white; border-radius: 4px; cursor: pointer;">
76
+ Cancel
77
+ </button>
78
+
79
+ <% if kill_switch_active? %>
80
+ <button
81
+ type="button"
82
+ class="btn-confirm"
83
+ onclick="submitWithConfirmation('<%= form_id %>', '<%= expected_confirmation %>')"
84
+ style="padding: 8px 16px; border: 1px solid #dc3545; background-color: #dc3545; color: white; border-radius: 4px; cursor: pointer;">
85
+ Confirm & Proceed
86
+ </button>
87
+ <% else %>
88
+ <button
89
+ type="button"
90
+ class="btn-confirm"
91
+ onclick="submitWithoutConfirmation('<%= form_id %>')"
92
+ style="padding: 8px 16px; border: 1px solid #007bff; background-color: #007bff; color: white; border-radius: 4px; cursor: pointer;">
93
+ Proceed
94
+ </button>
95
+ <% end %>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <script>
101
+ // Only define functions once
102
+ if (typeof window.showKillSwitchModal === 'undefined') {
103
+ window.showKillSwitchModal = function(formId) {
104
+ const modal = document.getElementById('kill-switch-modal-' + formId);
105
+ if (modal) {
106
+ modal.style.display = 'block';
107
+ // Focus on confirmation input if it exists
108
+ const confirmationInput = document.getElementById('confirmation-text-' + formId);
109
+ if (confirmationInput) {
110
+ setTimeout(() => confirmationInput.focus(), 100);
111
+ }
112
+ }
113
+ };
114
+
115
+ window.closeKillSwitchModal = function(formId) {
116
+ const modal = document.getElementById('kill-switch-modal-' + formId);
117
+ if (modal) {
118
+ modal.style.display = 'none';
119
+ // Clear confirmation input
120
+ const confirmationInput = document.getElementById('confirmation-text-' + formId);
121
+ if (confirmationInput) {
122
+ confirmationInput.value = '';
123
+ }
124
+ }
125
+ };
126
+
127
+ window.submitWithConfirmation = function(formId, expectedConfirmation) {
128
+ const confirmationInput = document.getElementById('confirmation-text-' + formId);
129
+ const confirmationText = confirmationInput ? confirmationInput.value.trim() : '';
130
+
131
+ if (confirmationText !== expectedConfirmation) {
132
+ alert('Invalid confirmation text. Please type exactly: ' + expectedConfirmation);
133
+ return;
134
+ }
135
+
136
+ // Add confirmation text to the form
137
+ const form = document.getElementById(formId);
138
+ if (form) {
139
+ // Remove any existing confirmation_text input
140
+ const existingInput = form.querySelector('input[name="confirmation_text"]');
141
+ if (existingInput && existingInput !== confirmationInput) {
142
+ existingInput.remove();
143
+ }
144
+
145
+ // Create a hidden input with the confirmation text
146
+ const hiddenInput = document.createElement('input');
147
+ hiddenInput.type = 'hidden';
148
+ hiddenInput.name = 'confirmation_text';
149
+ hiddenInput.value = confirmationText;
150
+ form.appendChild(hiddenInput);
151
+
152
+ form.submit();
153
+ }
154
+
155
+ window.closeKillSwitchModal(formId);
156
+ };
157
+
158
+ window.submitWithoutConfirmation = function(formId) {
159
+ const form = document.getElementById(formId);
160
+ if (form) {
161
+ form.submit();
162
+ }
163
+ window.closeKillSwitchModal(formId);
164
+ };
165
+
166
+ // Close modal when clicking outside of it (only add listener once)
167
+ if (!window._killSwitchModalListenersAttached) {
168
+ window.onclick = function(event) {
169
+ if (event.target.className === 'kill-switch-modal') {
170
+ event.target.style.display = 'none';
171
+ }
172
+ };
173
+
174
+ // Close modal on Escape key
175
+ document.addEventListener('keydown', function(event) {
176
+ if (event.key === 'Escape') {
177
+ const modals = document.querySelectorAll('.kill-switch-modal');
178
+ modals.forEach(modal => {
179
+ if (modal.style.display === 'block') {
180
+ modal.style.display = 'none';
181
+ }
182
+ });
183
+ }
184
+ });
185
+
186
+ window._killSwitchModalListenersAttached = true;
187
+ }
188
+ }
189
+ </script>
@@ -0,0 +1,40 @@
1
+ <%# Kill Switch Status Indicator %>
2
+ <%# Displays the current kill switch status and environment warning if applicable %>
3
+
4
+ <%
5
+ # Debug info (can be removed in production)
6
+ env = current_environment
7
+ is_active = kill_switch_active?
8
+ protected_envs = PgSqlTriggers.respond_to?(:kill_switch_environments) ? PgSqlTriggers.kill_switch_environments : %i[production staging]
9
+
10
+ # Log for debugging (remove in production)
11
+ Rails.logger.debug "[KILL_SWITCH_VIEW] is_active=#{is_active} env=#{env} protected_envs=#{Array(protected_envs).map(&:to_s).join(',')}"
12
+ %>
13
+
14
+ <% if is_active %>
15
+ <div class="kill-switch-status active" style="background-color: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 12px; margin-bottom: 16px;">
16
+ <div style="display: flex; align-items: center; gap: 8px;">
17
+ <span style="font-size: 20px;">🛡️</span>
18
+ <div>
19
+ <strong style="color: #856404;">Kill Switch Active</strong>
20
+ <p style="margin: 4px 0 0 0; font-size: 14px; color: #856404;">
21
+ Dangerous operations in <strong><%= env %></strong> require confirmation.
22
+ All operations are logged and audited.
23
+ </p>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ <% else %>
28
+ <div class="kill-switch-status inactive" style="background-color: #d1ecf1; border: 1px solid #bee5eb; border-radius: 4px; padding: 12px; margin-bottom: 16px;">
29
+ <div style="display: flex; align-items: center; gap: 8px;">
30
+ <span style="font-size: 20px;">ℹ️</span>
31
+ <div>
32
+ <strong style="color: #0c5460;">Kill Switch Inactive</strong>
33
+ <p style="margin: 4px 0 0 0; font-size: 14px; color: #0c5460;">
34
+ Kill switch is disabled in <strong><%= env %></strong>.
35
+ Operations can be performed without confirmation.
36
+ </p>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ <% end %>
@@ -101,5 +101,3 @@
101
101
  <%= link_to "Generate New Trigger", new_generator_path, class: "btn btn-primary" %>
102
102
  </div>
103
103
  <% end %>
104
-
105
-
@@ -63,14 +63,14 @@
63
63
  </p>
64
64
  </div>
65
65
  </div>
66
-
66
+
67
67
  <% if trigger.definition.present? %>
68
68
  <% definition = JSON.parse(trigger.definition) rescue {} %>
69
69
  <div style="margin-bottom: 1rem;">
70
70
  <strong>Function:</strong> <code style="background: #e9ecef; padding: 0.25rem 0.5rem; border-radius: 2px;"><%= definition["function_name"] || "N/A" %></code>
71
71
  <% if definition["events"].present? %>
72
72
  <span style="margin-left: 1rem;">
73
- <strong>Events:</strong>
73
+ <strong>Events:</strong>
74
74
  <% definition["events"].each do |event| %>
75
75
  <span class="badge badge-info" style="margin-left: 0.25rem;"><%= event %></span>
76
76
  <% end %>
@@ -78,7 +78,7 @@
78
78
  <% end %>
79
79
  </div>
80
80
  <% end %>
81
-
81
+
82
82
  <% if trigger.function_body.present? %>
83
83
  <details style="margin-top: 1rem;">
84
84
  <summary style="cursor: pointer; font-weight: 600; color: #495057;">View Function Body</summary>
@@ -123,4 +123,3 @@
123
123
  </div>
124
124
  </div>
125
125
  <% end %>
126
-
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreatePgSqlTriggersTables < ActiveRecord::Migration[6.0]
3
+ class CreatePgSqlTriggersTables < ActiveRecord::Migration[6.1]
4
4
  def change
5
5
  # Registry table - source of truth for all triggers
6
6
  create_table :pg_sql_triggers_registry do |t|
data/docs/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # PgSqlTriggers Documentation
2
+
3
+ Welcome to the PgSqlTriggers documentation. This directory contains comprehensive guides and references for all features.
4
+
5
+ ## Documentation Index
6
+
7
+ ### Getting Started
8
+ - **[Getting Started Guide](getting-started.md)** - Installation, setup, and your first trigger
9
+
10
+ ### Core Guides
11
+ - **[Usage Guide](usage-guide.md)** - DSL syntax, migrations, and drift detection
12
+ - **[Web UI Guide](web-ui.md)** - Using the web dashboard
13
+ - **[Kill Switch Guide](kill-switch.md)** - Production safety features
14
+
15
+ ### Reference
16
+ - **[Configuration Reference](configuration.md)** - Complete configuration options
17
+ - **[API Reference](api-reference.md)** - Console API and programmatic usage
18
+
19
+ ## Quick Links
20
+
21
+ ### I want to...
22
+
23
+ #### Install PgSqlTriggers
24
+ Start with [Getting Started](getting-started.md)
25
+
26
+ #### Learn the DSL syntax
27
+ See [Usage Guide - Declaring Triggers](usage-guide.md#declaring-triggers)
28
+
29
+ #### Understand migrations
30
+ See [Usage Guide - Trigger Migrations](usage-guide.md#trigger-migrations)
31
+
32
+ #### Use the web dashboard
33
+ See [Web UI Documentation](web-ui.md)
34
+
35
+ #### Protect production
36
+ See [Kill Switch Documentation](kill-switch.md)
37
+
38
+ #### Configure permissions
39
+ See [Configuration - Permission System](configuration.md#permission-system)
40
+
41
+ #### Use the console API
42
+ See [API Reference](api-reference.md)
43
+
44
+ #### Handle drift detection
45
+ See [Usage Guide - Drift Detection](usage-guide.md#drift-detection)
46
+
47
+ ## Screenshots
48
+
49
+ Screenshots referenced in the documentation are stored in the [screenshots](screenshots/) directory.
50
+
51
+ ## Contributing to Documentation
52
+
53
+ When updating documentation:
54
+
55
+ 1. Keep examples realistic and practical
56
+ 2. Include both basic and advanced usage
57
+ 3. Add cross-references between related topics
58
+ 4. Update this index when adding new pages
59
+ 5. Place screenshots in the `screenshots/` directory
60
+ 6. Use relative links for internal documentation
61
+
62
+ ## External Resources
63
+
64
+ - [GitHub Repository](https://github.com/samaswin87/pg_sql_triggers)
65
+ - [Example Application](https://github.com/samaswin87/pg_triggers_example)
66
+ - [RubyGems](https://rubygems.org/gems/pg_sql_triggers)