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
data/README.md CHANGED
@@ -21,37 +21,28 @@ Rails teams use PostgreSQL triggers for data integrity, performance, and billing
21
21
  - UI control
22
22
  - Emergency SQL escape hatches
23
23
 
24
- ## Installation
24
+ ## Requirements
25
25
 
26
- Add this line to your application's Gemfile:
26
+ - **Ruby 3.0+**
27
+ - **Rails 6.1+**
28
+ - **PostgreSQL** (any supported version)
27
29
 
28
- ```ruby
29
- gem 'pg_sql_triggers'
30
- ```
30
+ ## Quick Start
31
31
 
32
- And then execute:
32
+ ### Installation
33
33
 
34
- ```bash
35
- $ bundle install
34
+ ```ruby
35
+ # Gemfile
36
+ gem 'pg_sql_triggers'
36
37
  ```
37
38
 
38
- Run the installer:
39
-
40
39
  ```bash
41
- $ rails generate pg_sql_triggers:install
42
- $ rails db:migrate
40
+ bundle install
41
+ rails generate pg_sql_triggers:install
42
+ rails db:migrate
43
43
  ```
44
44
 
45
- This will:
46
- 1. Create an initializer at `config/initializers/pg_sql_triggers.rb`
47
- 2. Create migrations for registry table
48
- 3. Mount the engine at `/pg_sql_triggers`
49
-
50
- ## Usage
51
-
52
- ### 1. Declaring Triggers
53
-
54
- Create trigger definitions using the Ruby DSL:
45
+ ### Define a Trigger
55
46
 
56
47
  ```ruby
57
48
  # app/triggers/users_email_validation.rb
@@ -59,222 +50,59 @@ PgSqlTriggers::DSL.pg_sql_trigger "users_email_validation" do
59
50
  table :users
60
51
  on :insert, :update
61
52
  function :validate_user_email
62
-
63
53
  version 1
64
54
  enabled false
65
-
66
55
  when_env :production
67
56
  end
68
57
  ```
69
58
 
70
- ### 2. Trigger Migrations
71
-
72
- Generate and run trigger migrations similar to Rails schema migrations:
59
+ ### Create and Run Migration
73
60
 
74
61
  ```bash
75
- # Generate a new trigger migration
76
- rails generate trigger:migration add_validation_trigger
77
-
78
- # Run pending trigger migrations
62
+ rails generate trigger:migration add_email_validation
79
63
  rake trigger:migrate
80
-
81
- # Rollback last trigger migration
82
- rake trigger:rollback
83
-
84
- # Rollback multiple steps
85
- rake trigger:rollback STEP=3
86
-
87
- # Check migration status
88
- rake trigger:migrate:status
89
-
90
- # Run a specific migration up
91
- rake trigger:migrate:up VERSION=20231215120000
92
-
93
- # Run a specific migration down
94
- rake trigger:migrate:down VERSION=20231215120000
95
-
96
- # Redo last migration
97
- rake trigger:migrate:redo
98
- ```
99
-
100
- **Web UI Migration Management:**
101
-
102
- You can also manage migrations directly from the web dashboard:
103
-
104
- - **Apply All Pending Migrations**: Click the "Apply All Pending Migrations" button to run all pending migrations at once
105
- - **Rollback Last Migration**: Use the "Rollback Last Migration" button to undo the most recent migration
106
- - **Redo Last Migration**: Click "Redo Last Migration" to rollback and re-apply the last migration
107
- - **Individual Migration Actions**: Each migration in the status table has individual "Up", "Down", or "Redo" buttons for granular control
108
-
109
- All migration actions include confirmation dialogs and provide feedback via flash messages.
110
-
111
- Trigger migrations are stored in `db/triggers/` and follow the same naming convention as Rails migrations (`YYYYMMDDHHMMSS_name.rb`).
112
-
113
- Example trigger migration:
114
-
115
- ```ruby
116
- # db/triggers/20231215120000_add_validation_trigger.rb
117
- class AddValidationTrigger < PgSqlTriggers::Migration
118
- def up
119
- execute <<-SQL
120
- CREATE OR REPLACE FUNCTION validate_user_email()
121
- RETURNS TRIGGER AS $$
122
- BEGIN
123
- IF NEW.email !~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$' THEN
124
- RAISE EXCEPTION 'Invalid email format';
125
- END IF;
126
- RETURN NEW;
127
- END;
128
- $$ LANGUAGE plpgsql;
129
-
130
- CREATE TRIGGER user_email_validation
131
- BEFORE INSERT OR UPDATE ON users
132
- FOR EACH ROW
133
- EXECUTE FUNCTION validate_user_email();
134
- SQL
135
- end
136
-
137
- def down
138
- execute <<-SQL
139
- DROP TRIGGER IF EXISTS user_email_validation ON users;
140
- DROP FUNCTION IF EXISTS validate_user_email();
141
- SQL
142
- end
143
- end
144
- ```
145
-
146
- ### 3. Combined Schema and Trigger Migrations
147
-
148
- Run both schema and trigger migrations together:
149
-
150
- ```bash
151
- # Run both schema and trigger migrations
152
- rake db:migrate:with_triggers
153
-
154
- # Rollback both (rolls back the most recent migration)
155
- rake db:rollback:with_triggers
156
-
157
- # Check status of both
158
- rake db:migrate:status:with_triggers
159
-
160
- # Get versions of both
161
- rake db:version:with_triggers
162
64
  ```
163
65
 
164
- ### 4. Console Introspection
165
-
166
- Access trigger information from the Rails console:
167
-
168
- ```ruby
169
- # List all triggers
170
- PgSqlTriggers::Registry.list
171
-
172
- # List enabled triggers
173
- PgSqlTriggers::Registry.enabled
174
-
175
- # List disabled triggers
176
- PgSqlTriggers::Registry.disabled
177
-
178
- # Get triggers for a specific table
179
- PgSqlTriggers::Registry.for_table(:users)
180
-
181
- # Check for drift
182
- PgSqlTriggers::Registry.diff
183
-
184
- # Validate all triggers
185
- PgSqlTriggers::Registry.validate!
186
- ```
187
-
188
- ### 5. Web UI
189
-
190
- Access the web UI at `http://localhost:3000/pg_sql_triggers` to:
191
-
192
- - View all triggers and their status
193
- - Enable/disable triggers
194
- - View drift states
195
- - Execute SQL capsules
196
- - Manage trigger lifecycle
197
- - **Run trigger migrations** (up/down/redo) directly from the dashboard
198
- - Apply all pending migrations with a single click
199
- - Rollback the last migration
200
- - Redo the last migration
201
- - Individual migration controls for each migration in the status table
202
-
203
- <img width="3360" height="2506" alt="screencapture-localhost-3000-pg-triggers-2025-12-27-17_04_29" src="https://github.com/user-attachments/assets/a7f5904b-1172-41fc-ba3f-c05587cb1fe8" />
66
+ ### Access the Web UI
204
67
 
205
- <img width="3360" height="3420" alt="screencapture-localhost-3000-pg-triggers-generator-new-2025-12-27-17_04_49" src="https://github.com/user-attachments/assets/fc9e53f2-f540-489d-8e41-6075dab8d731" />
68
+ Navigate to `http://localhost:3000/pg_sql_triggers` to manage triggers visually.
206
69
 
70
+ Screenshots are available in the [docs/screenshots](docs/screenshots/) directory.
207
71
 
208
- ### 6. Permissions
72
+ ## Documentation
209
73
 
210
- PgSqlTriggers supports three permission levels:
74
+ Comprehensive documentation is available in the [docs](docs/) directory:
211
75
 
212
- - **Viewer**: Read-only access (view triggers, diffs)
213
- - **Operator**: Can enable/disable triggers, apply generated triggers
214
- - **Admin**: Full access including dropping triggers and executing SQL
76
+ - **[Getting Started](docs/getting-started.md)** - Installation and basic setup
77
+ - **[Usage Guide](docs/usage-guide.md)** - DSL syntax, migrations, and drift detection
78
+ - **[Web UI](docs/web-ui.md)** - Using the web dashboard
79
+ - **[Kill Switch](docs/kill-switch.md)** - Production safety features
80
+ - **[Configuration](docs/configuration.md)** - Complete configuration reference
81
+ - **[API Reference](docs/api-reference.md)** - Console API and programmatic access
215
82
 
216
- Configure custom permission checking:
83
+ ## Key Features
217
84
 
218
- ```ruby
219
- # config/initializers/pg_sql_triggers.rb
220
- PgSqlTriggers.configure do |config|
221
- config.permission_checker = ->(actor, action, environment) {
222
- # Your custom permission logic
223
- user = User.find(actor[:id])
224
- user.has_permission?(action)
225
- }
226
- end
227
- ```
85
+ ### Trigger DSL
86
+ Define triggers using a Rails-native Ruby DSL with versioning and environment control.
228
87
 
229
- ### 7. Drift Detection
88
+ ### Migration System
89
+ Manage trigger functions and definitions with a migration system similar to Rails schema migrations.
230
90
 
231
- PgSqlTriggers automatically detects drift between your DSL definitions and the actual database state:
91
+ ### Drift Detection
92
+ Automatically detect when database triggers drift from your DSL definitions.
232
93
 
233
- - **Managed & In Sync**: Trigger matches DSL definition
234
- - **Managed & Drifted**: Trigger exists but doesn't match DSL
235
- - **Manual Override**: Trigger was modified outside of PgSqlTriggers
236
- - **Disabled**: Trigger is disabled
237
- - **Dropped**: Trigger was dropped but still in registry
238
- - **Unknown**: Trigger exists in DB but not in registry
94
+ ### Production Kill Switch
95
+ Multi-layered safety mechanism preventing accidental destructive operations in production environments.
239
96
 
240
- ### 8. Production Kill Switch
97
+ ### Web Dashboard
98
+ Visual interface for managing triggers, running migrations, and executing SQL capsules.
241
99
 
242
- By default, PgSqlTriggers blocks destructive operations in production:
100
+ ### Permissions
101
+ Three-tier permission system (Viewer, Operator, Admin) with customizable authorization.
243
102
 
244
- ```ruby
245
- # config/initializers/pg_sql_triggers.rb
246
- PgSqlTriggers.configure do |config|
247
- # Enable production kill switch (default: true)
248
- config.kill_switch_enabled = true
249
- end
250
- ```
103
+ ## Examples
251
104
 
252
- Override for specific operations:
253
-
254
- ```ruby
255
- PgSqlTriggers::SQL.override_kill_switch do
256
- # Dangerous operation here
257
- end
258
- ```
259
-
260
- ## Configuration
261
-
262
- ```ruby
263
- # config/initializers/pg_sql_triggers.rb
264
- PgSqlTriggers.configure do |config|
265
- # Kill switch for production (default: true)
266
- config.kill_switch_enabled = true
267
-
268
- # Environment detection (default: -> { Rails.env })
269
- config.default_environment = -> { Rails.env }
270
-
271
- # Custom permission checker
272
- config.permission_checker = ->(actor, action, environment) {
273
- # Return true/false based on your authorization logic
274
- true
275
- }
276
- end
277
- ```
105
+ For working examples and complete demonstrations, check out the [example repository](https://github.com/samaswin87/pg_triggers_example).
278
106
 
279
107
  ## Core Principles
280
108
 
@@ -285,10 +113,14 @@ end
285
113
 
286
114
  ## Development
287
115
 
288
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
116
+ After checking out the repo, run `bin/setup` to install dependencies. Run `rake spec` to run tests. Run `bin/console` for an interactive prompt.
289
117
 
290
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
118
+ To install this gem locally, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and run `bundle exec rake release`.
291
119
 
292
120
  ## Contributing
293
121
 
294
122
  Bug reports and pull requests are welcome on GitHub at https://github.com/samaswin87/pg_sql_triggers.
123
+
124
+ ## License
125
+
126
+ See [LICENSE](LICENSE) file for details.
@@ -9,6 +9,9 @@ module PgSqlTriggers
9
9
 
10
10
  before_action :check_permissions?
11
11
 
12
+ # Helper methods available in views
13
+ helper_method :current_environment, :kill_switch_active?, :expected_confirmation_text
14
+
12
15
  private
13
16
 
14
17
  def check_permissions?
@@ -31,5 +34,48 @@ module PgSqlTriggers
31
34
  def current_user_id
32
35
  "unknown"
33
36
  end
37
+
38
+ # ========== Kill Switch Helpers ==========
39
+
40
+ # Returns the current environment
41
+ def current_environment
42
+ Rails.env
43
+ end
44
+
45
+ # Checks if kill switch is active for the current environment
46
+ def kill_switch_active?
47
+ PgSqlTriggers::SQL::KillSwitch.active?(environment: current_environment)
48
+ end
49
+
50
+ # Checks kill switch before executing a dangerous operation
51
+ # Raises KillSwitchError if the operation is blocked
52
+ #
53
+ # @param operation [Symbol] The operation being performed
54
+ # @param confirmation [String, nil] Optional confirmation text from params
55
+ def check_kill_switch(operation:, confirmation: nil)
56
+ PgSqlTriggers::SQL::KillSwitch.check!(
57
+ operation: operation,
58
+ environment: current_environment,
59
+ confirmation: confirmation,
60
+ actor: current_actor
61
+ )
62
+ end
63
+
64
+ # Before action to require kill switch override for an action
65
+ # Add to specific controller actions that need protection:
66
+ # before_action -> { require_kill_switch_override(:operation_name) }, only: [:dangerous_action]
67
+ def require_kill_switch_override(operation, confirmation: nil)
68
+ check_kill_switch(operation: operation, confirmation: confirmation)
69
+ end
70
+
71
+ # Returns the expected confirmation text for an operation (for use in views)
72
+ def expected_confirmation_text(operation)
73
+ if PgSqlTriggers.respond_to?(:kill_switch_confirmation_pattern) &&
74
+ PgSqlTriggers.kill_switch_confirmation_pattern.respond_to?(:call)
75
+ PgSqlTriggers.kill_switch_confirmation_pattern.call(operation)
76
+ else
77
+ "EXECUTE #{operation.to_s.upcase}"
78
+ end
79
+ end
34
80
  end
35
81
  end
@@ -36,6 +36,9 @@ module PgSqlTriggers
36
36
  # POST /generator/create
37
37
  # Actually create the files and register in TriggerRegistry
38
38
  def create
39
+ # Check kill switch before generating trigger
40
+ check_kill_switch(operation: :ui_trigger_generate, confirmation: params[:confirmation_text])
41
+
39
42
  @form = PgSqlTriggers::Generator::Form.new(generator_params)
40
43
 
41
44
  if @form.valid?
@@ -67,6 +70,9 @@ module PgSqlTriggers
67
70
  @available_tables = fetch_available_tables
68
71
  render :new
69
72
  end
73
+ rescue PgSqlTriggers::KillSwitchError => e
74
+ flash[:error] = e.message
75
+ redirect_to root_path
70
76
  end
71
77
 
72
78
  # POST /generator/validate_table (AJAX)
@@ -96,10 +102,10 @@ module PgSqlTriggers
96
102
  private
97
103
 
98
104
  def generator_params
99
- params.expect(
100
- pg_sql_triggers_generator_form: [:trigger_name, :table_name, :function_name, :version,
101
- :enabled, :condition, :generate_function_stub, :function_body,
102
- { events: [], environments: [] }]
105
+ params.require(:pg_sql_triggers_generator_form).permit(
106
+ :trigger_name, :table_name, :function_name, :version,
107
+ :enabled, :condition, :generate_function_stub, :function_body,
108
+ events: [], environments: []
103
109
  )
104
110
  end
105
111
 
@@ -5,6 +5,9 @@ module PgSqlTriggers
5
5
  # Provides actions to run migrations up, down, and redo
6
6
  class MigrationsController < ApplicationController
7
7
  def up
8
+ # Check kill switch before running migration
9
+ check_kill_switch(operation: :ui_migration_up, confirmation: params[:confirmation_text])
10
+
8
11
  target_version = params[:version]&.to_i
9
12
  PgSqlTriggers::Migrator.ensure_migrations_table!
10
13
 
@@ -22,6 +25,9 @@ module PgSqlTriggers
22
25
  end
23
26
  end
24
27
  redirect_to root_path
28
+ rescue PgSqlTriggers::KillSwitchError => e
29
+ flash[:error] = e.message
30
+ redirect_to root_path
25
31
  rescue StandardError => e
26
32
  Rails.logger.error("Migration up failed: #{e.message}\n#{e.backtrace.join("\n")}")
27
33
  flash[:error] = "Failed to apply migration: #{e.message}"
@@ -29,6 +35,9 @@ module PgSqlTriggers
29
35
  end
30
36
 
31
37
  def down
38
+ # Check kill switch before rolling back migration
39
+ check_kill_switch(operation: :ui_migration_down, confirmation: params[:confirmation_text])
40
+
32
41
  target_version = params[:version]&.to_i
33
42
  PgSqlTriggers::Migrator.ensure_migrations_table!
34
43
 
@@ -48,6 +57,9 @@ module PgSqlTriggers
48
57
  flash[:success] = "Rolled back last migration successfully."
49
58
  end
50
59
  redirect_to root_path
60
+ rescue PgSqlTriggers::KillSwitchError => e
61
+ flash[:error] = e.message
62
+ redirect_to root_path
51
63
  rescue StandardError => e
52
64
  Rails.logger.error("Migration down failed: #{e.message}\n#{e.backtrace.join("\n")}")
53
65
  flash[:error] = "Failed to rollback migration: #{e.message}"
@@ -55,6 +67,9 @@ module PgSqlTriggers
55
67
  end
56
68
 
57
69
  def redo
70
+ # Check kill switch before redoing migration
71
+ check_kill_switch(operation: :ui_migration_redo, confirmation: params[:confirmation_text])
72
+
58
73
  target_version = params[:version]&.to_i
59
74
  PgSqlTriggers::Migrator.ensure_migrations_table!
60
75
 
@@ -75,6 +90,9 @@ module PgSqlTriggers
75
90
  flash[:success] = "Last migration redone successfully."
76
91
  end
77
92
  redirect_to root_path
93
+ rescue PgSqlTriggers::KillSwitchError => e
94
+ flash[:error] = e.message
95
+ redirect_to root_path
78
96
  rescue StandardError => e
79
97
  Rails.logger.error("Migration redo failed: #{e.message}\n#{e.backtrace.join("\n")}")
80
98
  flash[:error] = "Failed to redo migration: #{e.message}"
@@ -24,7 +24,16 @@ module PgSqlTriggers
24
24
  PgSqlTriggers::Drift.detect(trigger_name)
25
25
  end
26
26
 
27
- def enable!
27
+ def enable!(confirmation: nil)
28
+ # Check kill switch before enabling trigger
29
+ # Use Rails.env for kill switch check, not the trigger's environment field
30
+ PgSqlTriggers::SQL::KillSwitch.check!(
31
+ operation: :trigger_enable,
32
+ environment: Rails.env,
33
+ confirmation: confirmation,
34
+ actor: { type: "Console", id: "TriggerRegistry#enable!" }
35
+ )
36
+
28
37
  # Check if trigger exists in database before trying to enable it
29
38
  trigger_exists = false
30
39
  begin
@@ -50,7 +59,16 @@ module PgSqlTriggers
50
59
  update!(enabled: true)
51
60
  end
52
61
 
53
- def disable!
62
+ def disable!(confirmation: nil)
63
+ # Check kill switch before disabling trigger
64
+ # Use Rails.env for kill switch check, not the trigger's environment field
65
+ PgSqlTriggers::SQL::KillSwitch.check!(
66
+ operation: :trigger_disable,
67
+ environment: Rails.env,
68
+ confirmation: confirmation,
69
+ actor: { type: "Console", id: "TriggerRegistry#disable!" }
70
+ )
71
+
54
72
  # Check if trigger exists in database before trying to disable it
55
73
  trigger_exists = false
56
74
  begin
@@ -24,6 +24,8 @@
24
24
  </nav>
25
25
 
26
26
  <main style="max-width: 1200px; margin: 2rem auto; padding: 0 1rem;">
27
+ <%= render 'pg_sql_triggers/shared/kill_switch_status' %>
28
+
27
29
  <% if flash[:success] %>
28
30
  <div style="background: #d4edda; color: #155724; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; border-left: 4px solid #28a745;">
29
31
  <%= flash[:success] %>
@@ -32,7 +34,38 @@
32
34
 
33
35
  <% if flash[:error] %>
34
36
  <div style="background: #f8d7da; color: #721c24; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; border-left: 4px solid #dc3545;">
35
- <%= flash[:error] %>
37
+ <% error_message = flash[:error].to_s %>
38
+ <% if error_message.include?("Kill switch is active") %>
39
+ <%# Format kill switch error messages nicely %>
40
+ <% lines = error_message.split("\n") %>
41
+
42
+ <%# Main error header with clear start %>
43
+ <div style="display: flex; align-items: flex-start; margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 2px solid #f5c6cb;">
44
+ <span style="font-size: 1.8em; margin-right: 0.75rem; line-height: 1.2;">⚠️</span>
45
+ <div style="flex: 1;">
46
+ <div style="font-weight: bold; font-size: 1.15em; margin-bottom: 0.5rem;">
47
+ Operation Blocked
48
+ </div>
49
+ <% lines[0..1].each do |line| %>
50
+ <% next if line.strip.empty? %>
51
+ <div style="margin: 0.25rem 0;"><%= line.strip %></div>
52
+ <% end %>
53
+ </div>
54
+ </div>
55
+
56
+ <%# Instructions section with preserved formatting %>
57
+ <div style="margin-top: 0.5rem;">
58
+ <%# Extract and format the instructions portion %>
59
+ <% instructions_start = error_message.index("To override") || 0 %>
60
+ <% instructions_text = error_message[instructions_start..-1] %>
61
+ <div style="white-space: pre-wrap; word-wrap: break-word; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6;">
62
+ <%= instructions_text.strip %>
63
+ </div>
64
+ </div>
65
+ <% else %>
66
+ <%# Regular error message - preserve formatting %>
67
+ <div style="white-space: pre-wrap; word-wrap: break-word;"><%= error_message %></div>
68
+ <% end %>
36
69
  </div>
37
70
  <% end %>
38
71