maquina-generators 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/lib/generators/maquina/app/app_generator.rb +66 -15
- data/lib/generators/maquina/app/templates/.rubocop.yml +6 -0
- data/lib/generators/maquina/clave/clave_generator.rb +3 -0
- data/lib/generators/maquina/clave/templates/app/controllers/registration/verifications_controller.rb.tt +9 -4
- data/lib/generators/maquina/clave/templates/app/models/account.rb.tt +5 -0
- data/lib/generators/maquina/clave/templates/app/models/current.rb.tt +1 -0
- data/lib/generators/maquina/clave/templates/app/models/user.rb.tt +4 -0
- data/lib/generators/maquina/clave/templates/migration_create_accounts.rb.tt +9 -0
- data/lib/generators/maquina/clave/templates/migration_create_users.rb.tt +2 -0
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/jobs/_general_information.html.erb +1 -1
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/jobs/failed/_job.html.erb +2 -2
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/shared/_job.html.erb +1 -1
- data/lib/generators/maquina/registration/templates/app/models/account.rb.tt +1 -1
- data/lib/generators/maquina/registration/templates/app/models/user.rb.tt +1 -1
- data/lib/generators/maquina/registration/templates/app/views/registrations/new.html.erb.tt +2 -2
- data/lib/generators/maquina/registration/templates/config/locales/registration.en.yml +1 -0
- data/lib/generators/maquina/registration/templates/config/locales/registration.es.yml +1 -0
- data/lib/generators/maquina/solid_errors/USAGE +24 -0
- data/lib/generators/maquina/solid_errors/solid_errors_generator.rb +139 -13
- data/lib/generators/maquina/solid_errors/templates/app/agents/error_redactor.rb +34 -0
- data/lib/generators/maquina/solid_errors/templates/app/agents/errors_query.rb +84 -0
- data/lib/generators/maquina/solid_errors/templates/app/agents/failure_triage.rb +21 -0
- data/lib/generators/maquina/solid_errors/templates/app/agents/get_exception_tool.rb +26 -0
- data/lib/generators/maquina/solid_errors/templates/app/agents/list_failures_tool.rb +20 -0
- data/lib/generators/maquina/solid_errors/templates/app/agents/top_exceptions_tool.rb +18 -0
- data/lib/generators/maquina/solid_errors/templates/app/controllers/failures_mcp_controller.rb.tt +65 -0
- data/lib/generators/maquina/solid_errors/templates/bin/failures +37 -0
- data/lib/generators/maquina/solid_errors/templates/bin/failures-mcp +21 -0
- data/lib/maquina_generators/version.rb +1 -1
- data/test/generators/maquina/app_generator_test.rb +121 -5
- data/test/generators/maquina/clave_generator_test.rb +10 -1
- data/test/generators/maquina/failures_mcp_tools_test.rb +37 -0
- data/test/generators/maquina/registration_generator_test.rb +2 -2
- data/test/generators/maquina/solid_errors_generator_test.rb +140 -0
- data/test/tmp/Gemfile +3 -1
- data/test/tmp/app/agents/error_redactor.rb +34 -0
- data/test/tmp/app/agents/errors_query.rb +84 -0
- data/test/tmp/app/agents/failure_triage.rb +21 -0
- data/test/tmp/app/agents/get_exception_tool.rb +26 -0
- data/test/tmp/app/agents/list_failures_tool.rb +20 -0
- data/test/tmp/app/agents/top_exceptions_tool.rb +18 -0
- data/test/tmp/app/controllers/backstage_controller.rb +8 -0
- data/test/tmp/app/helpers/solid_errors_helper.rb +9 -0
- data/test/tmp/app/javascript/controllers/backtrace_filter_controller.js +27 -0
- data/test/tmp/app/javascript/controllers/clipboard_controller.js +36 -0
- data/test/tmp/app/views/layouts/_admin_navigation.html.erb +28 -0
- data/test/tmp/app/views/layouts/solid_errors/application.html.erb +23 -0
- data/test/tmp/app/views/solid_errors/errors/_actions.html.erb +22 -0
- data/test/tmp/app/views/solid_errors/errors/_delete_button.html.erb +10 -0
- data/test/tmp/app/views/solid_errors/errors/_error_card.html.erb +63 -0
- data/test/tmp/app/views/solid_errors/errors/_resolve_button.html.erb +11 -0
- data/test/tmp/app/views/solid_errors/errors/index.html.erb +37 -0
- data/test/tmp/app/views/solid_errors/errors/show/_actions.html.erb +33 -0
- data/test/tmp/app/views/solid_errors/errors/show/_error_details.html.erb +95 -0
- data/test/tmp/app/views/solid_errors/errors/show/_header.html.erb +18 -0
- data/test/tmp/app/views/solid_errors/errors/show/_properties.html.erb +65 -0
- data/test/tmp/app/views/solid_errors/errors/show.html.erb +23 -0
- data/test/tmp/app/views/solid_errors/occurrences/_backtrace_line.html.erb +27 -0
- data/test/tmp/app/views/solid_errors/occurrences/_collection.html.erb +80 -0
- data/test/tmp/app/views/solid_errors/occurrences/_occurrence.html.erb +129 -0
- data/test/tmp/bin/failures +37 -0
- data/test/tmp/bin/failures-mcp +21 -0
- data/test/tmp/config/initializers/solid_errors.rb +12 -0
- data/test/tmp/config/routes.rb +3 -0
- metadata +57 -5
- data/test/tmp/Procfile.dev +0 -3
- data/test/tmp/config/application.rb +0 -12
- data/test/tmp/config/solid_queue.yml +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79618453431401c828cd9f89037c0111810fe7ff1b3328986fb99fea3282e010
|
|
4
|
+
data.tar.gz: e33ebfe0accac9a78432b1548648fce1ca8de0176370673f84665a7928eefeef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5c84a544b6295a6dfd32833b8d65febdb7e9d1de3e79da20c67e55cc7d5102c585a61b76f640ae3eb01cd1e59a57172c7930a90ea86f47bbcf7e49583709cf6
|
|
7
|
+
data.tar.gz: 27361ba6b7f8c51778eee636c43715ce8aa6dcce88c5a6c929c4715d4980b27db240fdbaba8139b3859d920976ad8fc1655881cf965c891099063c63cf88b635
|
data/README.md
CHANGED
|
@@ -10,13 +10,13 @@ A collection of Rails generators from the Maquina umbrella. Each generator produ
|
|
|
10
10
|
|
|
11
11
|
#### What it generates
|
|
12
12
|
|
|
13
|
-
- **Models:** `User`, `Session`, `EmailVerification`, `Current`
|
|
13
|
+
- **Models:** `Account`, `User`, `Session`, `EmailVerification`, `Current`
|
|
14
14
|
- **Controllers:** Sign-in/sign-up flows with email code verification
|
|
15
15
|
- **Views:** Minimal, responsive forms styled with Tailwind CSS
|
|
16
16
|
- **Mailer:** Verification code emails (HTML + text)
|
|
17
17
|
- **Job:** Cleanup job for expired sessions and verifications
|
|
18
18
|
- **Locale files:** English and Spanish translations
|
|
19
|
-
- **Migrations:**
|
|
19
|
+
- **Migrations:** 4 migrations (accounts, users, sessions, email_verifications)
|
|
20
20
|
- **Test helper:** `sign_in_as(user)` and `sign_out` for integration tests
|
|
21
21
|
|
|
22
22
|
#### Installation
|
|
@@ -20,7 +20,16 @@ module Maquina
|
|
|
20
20
|
content = File.read(gemfile_path)
|
|
21
21
|
|
|
22
22
|
remove_gems = ["rubocop-rails-omakase"]
|
|
23
|
-
|
|
23
|
+
# standard >= 1.54: avoids the inoperative placeholder releases
|
|
24
|
+
# (1.34.0.1 / 1.35.0.1) whose too-loose `rubocop ~> 1.62` requirement
|
|
25
|
+
# makes bundler backtrack onto them and pair them with an incompatible
|
|
26
|
+
# rubocop, so the `standard` binary refuses to run.
|
|
27
|
+
#
|
|
28
|
+
# standard.rb runs through the generated .rubocop.yml (see
|
|
29
|
+
# create_config_files) — that file is standard.rb's runner bridge, not
|
|
30
|
+
# custom RuboCop rules, so it ships even though guidance says "no
|
|
31
|
+
# .rubocop.yml".
|
|
32
|
+
dev_gems = {"brakeman" => nil, "bundle-audit" => nil, "letter_opener" => nil, "standard" => ">= 1.54"}
|
|
24
33
|
runtime_gems = {"rails-i18n" => nil, "maquina-components" => nil}
|
|
25
34
|
production_gems = {"aws-sdk-s3" => nil}
|
|
26
35
|
|
|
@@ -28,22 +37,19 @@ module Maquina
|
|
|
28
37
|
gsub_file "Gemfile", /^\s*gem\s+["']#{gem_name}["'].*\n/, ""
|
|
29
38
|
end
|
|
30
39
|
|
|
31
|
-
dev_gems.each do |gem_name,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
end
|
|
40
|
+
dev_gems.each do |gem_name, version|
|
|
41
|
+
next if content.include?("gem \"#{gem_name}\"")
|
|
42
|
+
append_to_file "Gemfile", "\n#{gem_line(gem_name, version, group: :development)}\n"
|
|
35
43
|
end
|
|
36
44
|
|
|
37
|
-
runtime_gems.each do |gem_name,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
end
|
|
45
|
+
runtime_gems.each do |gem_name, version|
|
|
46
|
+
next if content.include?("gem \"#{gem_name}\"")
|
|
47
|
+
append_to_file "Gemfile", "\n#{gem_line(gem_name, version)}\n"
|
|
41
48
|
end
|
|
42
49
|
|
|
43
|
-
production_gems.each do |gem_name,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
end
|
|
50
|
+
production_gems.each do |gem_name, version|
|
|
51
|
+
next if content.include?("gem \"#{gem_name}\"")
|
|
52
|
+
append_to_file "Gemfile", "\n#{gem_line(gem_name, version, group: :production)}\n"
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
return unless rails_app?
|
|
@@ -255,7 +261,45 @@ module Maquina
|
|
|
255
261
|
end
|
|
256
262
|
end
|
|
257
263
|
|
|
258
|
-
# 18.
|
|
264
|
+
# 18. Restore database.yml from its example in bin/setup. config/database.yml
|
|
265
|
+
# is gitignored (it differs per environment), so a fresh clone has only
|
|
266
|
+
# the committed example — bin/setup must copy it before db:prepare.
|
|
267
|
+
def configure_bin_setup
|
|
268
|
+
setup_file = File.join(destination_root, "bin/setup")
|
|
269
|
+
return unless File.exist?(setup_file)
|
|
270
|
+
return if File.read(setup_file).include?("config/database.yml.example")
|
|
271
|
+
|
|
272
|
+
inject_into_file "bin/setup",
|
|
273
|
+
after: %r{system\("bundle check"\) \|\| system!\("bundle install"\)\n} do
|
|
274
|
+
<<~RUBY.indent(2)
|
|
275
|
+
|
|
276
|
+
# config/database.yml is gitignored; restore it from the committed example
|
|
277
|
+
unless File.exist?("config/database.yml")
|
|
278
|
+
puts "\\n== Copying config/database.yml =="
|
|
279
|
+
FileUtils.cp "config/database.yml.example", "config/database.yml"
|
|
280
|
+
end
|
|
281
|
+
RUBY
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# 19. Restore database.yml from its example in CI. Without it, the test job
|
|
286
|
+
# boots with no database.yml and every Rails task fails. Only touched
|
|
287
|
+
# when the host app already has a GitHub Actions workflow.
|
|
288
|
+
def configure_ci
|
|
289
|
+
ci_file = File.join(destination_root, ".github/workflows/ci.yml")
|
|
290
|
+
return unless File.exist?(ci_file)
|
|
291
|
+
return if File.read(ci_file).include?("config/database.yml.example")
|
|
292
|
+
|
|
293
|
+
inject_into_file ".github/workflows/ci.yml",
|
|
294
|
+
before: /^ - name: Run tests$/ do
|
|
295
|
+
<<~YAML.indent(6)
|
|
296
|
+
- name: Prepare database config
|
|
297
|
+
run: cp config/database.yml.example config/database.yml
|
|
298
|
+
YAML
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# 20. Run all migrations
|
|
259
303
|
def run_migrations
|
|
260
304
|
return unless rails_app?
|
|
261
305
|
|
|
@@ -264,7 +308,7 @@ module Maquina
|
|
|
264
308
|
end
|
|
265
309
|
end
|
|
266
310
|
|
|
267
|
-
#
|
|
311
|
+
# 21. Post-install message
|
|
268
312
|
def show_post_install
|
|
269
313
|
say ""
|
|
270
314
|
say "=" * 60, :green
|
|
@@ -298,6 +342,13 @@ module Maquina
|
|
|
298
342
|
|
|
299
343
|
private
|
|
300
344
|
|
|
345
|
+
def gem_line(name, version, group: nil)
|
|
346
|
+
line = "gem \"#{name}\""
|
|
347
|
+
line << ", \"#{version}\"" if version
|
|
348
|
+
line << ", group: :#{group}" if group
|
|
349
|
+
line
|
|
350
|
+
end
|
|
351
|
+
|
|
301
352
|
def rails_app?
|
|
302
353
|
File.exist?(File.join(destination_root, "bin/rails"))
|
|
303
354
|
end
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# standard.rb's runner bridge, not custom RuboCop rules.
|
|
2
|
+
#
|
|
3
|
+
# This is the entry point RuboCop (and editors/LSP) need to run the Standard
|
|
4
|
+
# ruleset — all style comes from the `standard` gem via config/base.yml below.
|
|
5
|
+
# Keep this file even though general guidance says "no .rubocop.yml": for
|
|
6
|
+
# standard.rb this IS the runner config, not a custom rule set.
|
|
1
7
|
require:
|
|
2
8
|
- standard
|
|
3
9
|
|
|
@@ -34,6 +34,7 @@ module Maquina
|
|
|
34
34
|
|
|
35
35
|
# 1. Models
|
|
36
36
|
def create_models
|
|
37
|
+
template "app/models/account.rb.tt", "app/models/account.rb"
|
|
37
38
|
template "app/models/current.rb.tt", "app/models/current.rb"
|
|
38
39
|
template "app/models/session.rb.tt", "app/models/session.rb"
|
|
39
40
|
template "app/models/email_verification.rb.tt", "app/models/email_verification.rb"
|
|
@@ -146,6 +147,8 @@ module Maquina
|
|
|
146
147
|
|
|
147
148
|
# 12. Migrations
|
|
148
149
|
def add_migrations
|
|
150
|
+
migration_template "migration_create_accounts.rb.tt",
|
|
151
|
+
"db/migrate/create_accounts.rb"
|
|
149
152
|
migration_template "migration_create_users.rb.tt",
|
|
150
153
|
"db/migrate/create_users.rb"
|
|
151
154
|
migration_template "migration_create_sessions.rb.tt",
|
|
@@ -34,10 +34,15 @@ class Registration::VerificationsController < ApplicationController
|
|
|
34
34
|
return
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
user =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
user = nil
|
|
38
|
+
ActiveRecord::Base.transaction do
|
|
39
|
+
account = Account.create!
|
|
40
|
+
user = account.users.create!(
|
|
41
|
+
email_address: @verification.email,
|
|
42
|
+
locale: @verification.locale || I18n.default_locale.to_s,
|
|
43
|
+
role: :admin
|
|
44
|
+
)
|
|
45
|
+
end
|
|
41
46
|
|
|
42
47
|
@verification.mark_verified!
|
|
43
48
|
session.delete(:pending_verification_id)
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
class User < ApplicationRecord
|
|
2
|
+
belongs_to :account
|
|
2
3
|
has_many :sessions, dependent: :destroy
|
|
3
4
|
|
|
5
|
+
enum :role, {member: "member", admin: "admin"}, default: "member"
|
|
6
|
+
|
|
4
7
|
normalizes :email_address, with: ->(e) { e.strip.downcase }
|
|
5
8
|
|
|
6
9
|
validates :email_address, uniqueness: true
|
|
10
|
+
validates :name, allow_blank: true, length: { maximum: 100 }
|
|
7
11
|
validate :email_cannot_contain_plus
|
|
8
12
|
|
|
9
13
|
scope :active, -> { where(blocked_at: nil) }
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
class CreateUsers < ActiveRecord::Migration<%= migration_version %>
|
|
2
2
|
def change
|
|
3
3
|
create_table :users do |t|
|
|
4
|
+
t.references :account, null: false, foreign_key: true
|
|
4
5
|
t.string :email_address, null: false
|
|
5
6
|
t.string :name
|
|
7
|
+
t.string :role, null: false, default: "member"
|
|
6
8
|
t.string :locale
|
|
7
9
|
t.datetime :blocked_at
|
|
8
10
|
t.text :blocked_reason
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<div class="space-y-4">
|
|
8
8
|
<div>
|
|
9
9
|
<span class="text-sm text-muted-foreground">Arguments</span>
|
|
10
|
-
<div class="mt-1 text-sm font-mono text-foreground break-words"><%= job_arguments(job) %></div>
|
|
10
|
+
<div class="mt-1 text-sm font-mono text-foreground break-words overflow-wrap-anywhere"><%= job_arguments(job) %></div>
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
13
|
<div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%= render "components/table/cell" do %>
|
|
2
|
-
<div>
|
|
2
|
+
<div class="min-w-0">
|
|
3
3
|
<%= link_to failed_job_error(job), application_job_path(@application, job.job_id, anchor: "error"),
|
|
4
|
-
class: "text-sm text-foreground hover:text-primary transition-colors" %>
|
|
4
|
+
class: "block text-sm text-foreground hover:text-primary transition-colors break-words overflow-wrap-anywhere line-clamp-2" %>
|
|
5
5
|
<div class="text-xs text-muted-foreground mt-1"><%= time_distance_in_words_with_title(job.failed_at) %> ago</div>
|
|
6
6
|
</div>
|
|
7
7
|
<% end %>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<% end %>
|
|
13
13
|
<%= render "components/table/cell" do %>
|
|
14
14
|
<% if job.serialized_arguments.present? %>
|
|
15
|
-
<span class="text-xs font-mono text-muted-foreground"><%= job_arguments(job) %></span>
|
|
15
|
+
<span class="block min-w-0 text-xs font-mono text-muted-foreground break-words overflow-wrap-anywhere line-clamp-2"><%= job_arguments(job) %></span>
|
|
16
16
|
<% end %>
|
|
17
17
|
<% end %>
|
|
18
18
|
<%= render "components/table/cell" do %>
|
|
@@ -6,7 +6,7 @@ class User < ApplicationRecord
|
|
|
6
6
|
|
|
7
7
|
normalizes :email_address, with: ->(e) { e.strip.downcase }
|
|
8
8
|
|
|
9
|
-
validates :name,
|
|
9
|
+
validates :name, allow_blank: true, length: { maximum: 100 }
|
|
10
10
|
|
|
11
11
|
enum :role, {member: "member", admin: "admin"}, default: "member"
|
|
12
12
|
end
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<%%= form_with url: registrations_path, method: :post, class: "space-y-6" do |form| %>
|
|
9
9
|
<div>
|
|
10
10
|
<%%= form.label :account_name, t(".account_name"), class: "block text-sm font-medium text-foreground mb-2", data: { component: "label" } %>
|
|
11
|
+
<span class="text-xs text-muted-foreground"><%%= t(".optional") %></span>
|
|
11
12
|
<%%= form.text_field :account_name,
|
|
12
|
-
required: true,
|
|
13
13
|
autofocus: true,
|
|
14
14
|
value: @account_name,
|
|
15
15
|
placeholder: t(".account_name_placeholder"),
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
|
|
20
20
|
<div>
|
|
21
21
|
<%%= form.label :name, t(".name"), class: "block text-sm font-medium text-foreground mb-2", data: { component: "label" } %>
|
|
22
|
+
<span class="text-xs text-muted-foreground"><%%= t(".optional") %></span>
|
|
22
23
|
<%%= form.text_field :name,
|
|
23
|
-
required: true,
|
|
24
24
|
value: @user.name,
|
|
25
25
|
placeholder: t(".name_placeholder"),
|
|
26
26
|
data: { component: "input" },
|
|
@@ -15,6 +15,7 @@ es:
|
|
|
15
15
|
submit: "Registrarse"
|
|
16
16
|
have_account: "Ya tienes cuenta?"
|
|
17
17
|
sign_in: "Inicia sesion"
|
|
18
|
+
optional: "(opcional)"
|
|
18
19
|
create:
|
|
19
20
|
success: "Tu cuenta ha sido creada. Bienvenido!"
|
|
20
21
|
rate_limited: "Demasiados intentos. Por favor, espera unos minutos e intenta de nuevo."
|
|
@@ -7,9 +7,33 @@ Description:
|
|
|
7
7
|
Authentication tries Rails credentials (backstage.username/password)
|
|
8
8
|
first, then falls back to environment variables.
|
|
9
9
|
|
|
10
|
+
Regardless of options, ApplicationJob is patched to tag reported errors
|
|
11
|
+
with the job class, arguments, and id, so background-job failures appear
|
|
12
|
+
in the Solid Errors dashboard with replayable context.
|
|
13
|
+
|
|
14
|
+
With --agent, it also generates read-only AI-agent triage tooling: query
|
|
15
|
+
and triage objects over the errors store (with a redaction pass), a
|
|
16
|
+
bin/failures runner, and a stdio MCP server (bin/failures-mcp, gem "mcp").
|
|
17
|
+
The agent tooling never writes — resolution stays in the dashboards.
|
|
18
|
+
|
|
19
|
+
With --mcp-http (implies --agent), it additionally generates an
|
|
20
|
+
MCP-over-HTTP endpoint at <prefix>/failures/mcp, behind the same backstage
|
|
21
|
+
Basic Auth as the dashboards, so a developer can query a DEPLOYED app from
|
|
22
|
+
their machine over HTTPS. It self-disables until backstage credentials are
|
|
23
|
+
set. It serves redacted-but-sensitive data over the internet, so enable it
|
|
24
|
+
only where intended and put it behind an IP allowlist/VPN.
|
|
25
|
+
|
|
26
|
+
To query a deployed app without exposing HTTP, run the stdio server where the
|
|
27
|
+
errors DB lives and tunnel its stdio (the post-install prints ssh/docker/kamal
|
|
28
|
+
examples), or copy storage/<env>_errors.sqlite3 down for offline analysis.
|
|
29
|
+
|
|
10
30
|
Examples:
|
|
11
31
|
rails g maquina:solid_errors --prefix /admin
|
|
12
32
|
|
|
13
33
|
rails g maquina:solid_errors --prefix /backstage \
|
|
14
34
|
--user-env-var ADMIN_USER \
|
|
15
35
|
--password-env-var ADMIN_PASSWORD
|
|
36
|
+
|
|
37
|
+
rails g maquina:solid_errors --prefix /admin --agent
|
|
38
|
+
|
|
39
|
+
rails g maquina:solid_errors --prefix /admin --mcp-http
|
|
@@ -13,6 +13,10 @@ module Maquina
|
|
|
13
13
|
desc: "Environment variable for HTTP auth password"
|
|
14
14
|
class_option :copy_views, type: :boolean, default: true,
|
|
15
15
|
desc: "Copy custom Solid Errors views to the host app"
|
|
16
|
+
class_option :agent, type: :boolean, default: false,
|
|
17
|
+
desc: "Generate read-only AI-agent triage tooling (bin/failures + stdio MCP server)"
|
|
18
|
+
class_option :mcp_http, type: :boolean, default: false,
|
|
19
|
+
desc: "Also expose the MCP server over HTTP behind backstage auth (implies --agent; internet-exposed)"
|
|
16
20
|
class_option :quiet, type: :boolean, default: false,
|
|
17
21
|
desc: "Suppress post-install instructions"
|
|
18
22
|
|
|
@@ -36,25 +40,62 @@ module Maquina
|
|
|
36
40
|
"config/initializers/solid_errors.rb"
|
|
37
41
|
end
|
|
38
42
|
|
|
39
|
-
# 4.
|
|
43
|
+
# 4. Enrich job failures with job context (always — benefits the dashboard
|
|
44
|
+
# too, not just the agent). On Solid Queue >= 1.0.2 a failed job's
|
|
45
|
+
# exception already flows to the Rails error reporter that Solid Errors
|
|
46
|
+
# subscribes to; this only attaches the job class/arguments/id to that
|
|
47
|
+
# report so a job failure is distinguishable and replayable.
|
|
48
|
+
def configure_application_job
|
|
49
|
+
job_path = "app/jobs/application_job.rb"
|
|
50
|
+
full_path = File.join(destination_root, job_path)
|
|
51
|
+
return unless File.exist?(full_path)
|
|
52
|
+
return if File.read(full_path).include?("Rails.error.set_context")
|
|
53
|
+
|
|
54
|
+
inject_into_class job_path, "ApplicationJob", <<~RUBY
|
|
55
|
+
# Attach job context to errors reported to Rails.error so background-job
|
|
56
|
+
# failures are distinguishable from request failures in Solid Errors,
|
|
57
|
+
# and carry replayable arguments. Sets context only and lets Solid
|
|
58
|
+
# Queue's built-in reporting forward the exception (reported once).
|
|
59
|
+
#
|
|
60
|
+
# If your Solid Queue version does not propagate this context to its
|
|
61
|
+
# report, fall back to rescuing in the block, reporting explicitly with
|
|
62
|
+
# `Rails.error.report(e, handled: false)`, and RE-RAISING (re-raising is
|
|
63
|
+
# required: it preserves the failed_execution record and retries).
|
|
64
|
+
around_perform do |job, block|
|
|
65
|
+
Rails.error.set_context(
|
|
66
|
+
active_job: job.class.name,
|
|
67
|
+
arguments: job.arguments,
|
|
68
|
+
job_id: job.job_id,
|
|
69
|
+
queue_name: job.queue_name
|
|
70
|
+
)
|
|
71
|
+
block.call
|
|
72
|
+
end
|
|
73
|
+
RUBY
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# 5. Add gem to Gemfile
|
|
40
77
|
def add_gem
|
|
41
78
|
gemfile_path = File.join(destination_root, "Gemfile")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
79
|
+
return unless File.exist?(gemfile_path)
|
|
80
|
+
|
|
81
|
+
content = File.read(gemfile_path)
|
|
82
|
+
unless content.include?('gem "solid_errors"')
|
|
83
|
+
append_to_file "Gemfile", "\ngem \"solid_errors\"\n"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if agent? && !content.include?('gem "mcp"')
|
|
87
|
+
append_to_file "Gemfile", "\ngem \"mcp\"\n"
|
|
47
88
|
end
|
|
48
89
|
end
|
|
49
90
|
|
|
50
|
-
#
|
|
91
|
+
# 6. Routes
|
|
51
92
|
def add_route
|
|
52
93
|
mount_path = "#{options[:prefix]}/solid_errors"
|
|
53
94
|
|
|
54
95
|
route "mount SolidErrors::Engine, at: \"#{mount_path}\""
|
|
55
96
|
end
|
|
56
97
|
|
|
57
|
-
#
|
|
98
|
+
# 7. Admin navigation
|
|
58
99
|
def create_admin_navigation
|
|
59
100
|
nav_path = "app/views/layouts/_admin_navigation.html.erb"
|
|
60
101
|
return if File.exist?(File.join(destination_root, nav_path))
|
|
@@ -62,13 +103,13 @@ module Maquina
|
|
|
62
103
|
template "app/views/layouts/_admin_navigation.html.erb.tt", nav_path
|
|
63
104
|
end
|
|
64
105
|
|
|
65
|
-
#
|
|
106
|
+
# 8. Layout
|
|
66
107
|
def copy_layout
|
|
67
108
|
copy_file "app/views/layouts/solid_errors/application.html.erb",
|
|
68
109
|
"app/views/layouts/solid_errors/application.html.erb"
|
|
69
110
|
end
|
|
70
111
|
|
|
71
|
-
#
|
|
112
|
+
# 9. Stimulus controllers
|
|
72
113
|
def copy_stimulus_controllers
|
|
73
114
|
copy_file "app/javascript/controllers/clipboard_controller.js",
|
|
74
115
|
"app/javascript/controllers/clipboard_controller.js"
|
|
@@ -76,7 +117,7 @@ module Maquina
|
|
|
76
117
|
"app/javascript/controllers/backtrace_filter_controller.js"
|
|
77
118
|
end
|
|
78
119
|
|
|
79
|
-
#
|
|
120
|
+
# 10. Custom views
|
|
80
121
|
def copy_views
|
|
81
122
|
return unless options[:copy_views]
|
|
82
123
|
|
|
@@ -85,7 +126,36 @@ module Maquina
|
|
|
85
126
|
end
|
|
86
127
|
end
|
|
87
128
|
|
|
88
|
-
#
|
|
129
|
+
# 11. AI-agent triage tooling (opt-in). Read-only query/triage layer over
|
|
130
|
+
# the errors store, a bin/ runner, and a stdio MCP server. Resolution
|
|
131
|
+
# stays a human action in the Backstage dashboards.
|
|
132
|
+
def create_agent_files
|
|
133
|
+
return unless agent?
|
|
134
|
+
|
|
135
|
+
agent_files.each do |file|
|
|
136
|
+
copy_file file, file
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
copy_file "bin/failures", "bin/failures"
|
|
140
|
+
copy_file "bin/failures-mcp", "bin/failures-mcp"
|
|
141
|
+
chmod "bin/failures", 0o755
|
|
142
|
+
chmod "bin/failures-mcp", 0o755
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# 12. MCP-over-HTTP endpoint (opt-in, internet-exposed). Mounts the same
|
|
146
|
+
# read-only tool surface behind the existing backstage Basic Auth so a
|
|
147
|
+
# developer can query a deployed app's failures from their machine.
|
|
148
|
+
def create_mcp_http_controller
|
|
149
|
+
return unless options[:mcp_http]
|
|
150
|
+
|
|
151
|
+
template "app/controllers/failures_mcp_controller.rb.tt",
|
|
152
|
+
"app/controllers/failures_mcp_controller.rb"
|
|
153
|
+
|
|
154
|
+
route %(match "#{options[:prefix]}/failures/mcp", ) +
|
|
155
|
+
%(to: "failures_mcp#handle", via: %i[get post delete])
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# 13. Bundle install
|
|
89
159
|
def run_bundle_install
|
|
90
160
|
return unless rails_app?
|
|
91
161
|
|
|
@@ -94,7 +164,7 @@ module Maquina
|
|
|
94
164
|
end
|
|
95
165
|
end
|
|
96
166
|
|
|
97
|
-
#
|
|
167
|
+
# 14. Post-install message
|
|
98
168
|
def show_post_install
|
|
99
169
|
return if options[:quiet]
|
|
100
170
|
|
|
@@ -113,10 +183,59 @@ module Maquina
|
|
|
113
183
|
say " password: your_password"
|
|
114
184
|
say " - Or set ENV vars: #{options[:user_env_var]}, #{options[:password_env_var]}"
|
|
115
185
|
say ""
|
|
186
|
+
say "Job failures: app/jobs/application_job.rb now tags reported errors with", :yellow
|
|
187
|
+
say " job class, arguments, and id so background-job failures show up in the"
|
|
188
|
+
say " Solid Errors dashboard with replayable context (Solid Queue >= 1.0.2)."
|
|
189
|
+
say ""
|
|
190
|
+
|
|
191
|
+
show_agent_post_install if agent?
|
|
192
|
+
show_mcp_http_post_install if options[:mcp_http]
|
|
116
193
|
end
|
|
117
194
|
|
|
118
195
|
private
|
|
119
196
|
|
|
197
|
+
def agent?
|
|
198
|
+
options[:agent] || options[:mcp_http]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def show_agent_post_install
|
|
202
|
+
say "AI-agent triage tooling (read-only):", :green
|
|
203
|
+
say ""
|
|
204
|
+
say " bin/failures overview # unresolved errors as JSON"
|
|
205
|
+
say " bin/failures top [limit] # most frequent unresolved"
|
|
206
|
+
say " bin/failures exception <fingerprint> # full detail + backtrace"
|
|
207
|
+
say ""
|
|
208
|
+
say " Register the local stdio MCP server with Claude Code (app root):", :yellow
|
|
209
|
+
say " claude mcp add failures -- bin/failures-mcp"
|
|
210
|
+
say ""
|
|
211
|
+
say " Query a DEPLOYED app from your machine (server runs where the", :yellow
|
|
212
|
+
say " errors DB lives — pick what matches your deploy):"
|
|
213
|
+
say " ssh: claude mcp add failures -- ssh user@host 'cd /app && bin/failures-mcp'"
|
|
214
|
+
say " docker: claude mcp add failures -- ssh user@host 'docker exec -i <container> bin/failures-mcp'"
|
|
215
|
+
say " kamal: claude mcp add failures -- kamal app exec -i --reuse \"bin/failures-mcp\""
|
|
216
|
+
say " offline: copy storage/production_errors.sqlite3 down, run bin/failures locally"
|
|
217
|
+
say ""
|
|
218
|
+
say " Review app/agents/error_redactor.rb — it masks sensitive keys", :yellow
|
|
219
|
+
say " (passwords, tokens, email, ...) before any data reaches the agent."
|
|
220
|
+
say ""
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def show_mcp_http_post_install
|
|
224
|
+
say "MCP-over-HTTP endpoint (internet-exposed, read-only):", :green
|
|
225
|
+
say ""
|
|
226
|
+
say " Mounted at #{options[:prefix]}/failures/mcp behind backstage Basic Auth."
|
|
227
|
+
say " It self-disables (503) until backstage credentials are set."
|
|
228
|
+
say ""
|
|
229
|
+
say " Register from your machine:", :yellow
|
|
230
|
+
say " claude mcp add --transport http failures \\"
|
|
231
|
+
say " https://yourapp.com#{options[:prefix]}/failures/mcp \\"
|
|
232
|
+
say " --header \"Authorization: Basic $(echo -n user:pass | base64)\""
|
|
233
|
+
say ""
|
|
234
|
+
say " SECURITY: this serves redacted-but-sensitive failure data over the", :red
|
|
235
|
+
say " internet. Enable only where intended and put it behind an IP allowlist/VPN."
|
|
236
|
+
say ""
|
|
237
|
+
end
|
|
238
|
+
|
|
120
239
|
def rails_app?
|
|
121
240
|
File.exist?(File.join(destination_root, "bin/rails"))
|
|
122
241
|
end
|
|
@@ -127,6 +246,13 @@ module Maquina
|
|
|
127
246
|
File.join("app/views/solid_errors", file)
|
|
128
247
|
end
|
|
129
248
|
end
|
|
249
|
+
|
|
250
|
+
def agent_files
|
|
251
|
+
agents_dir = File.join(self.class.source_root, "app/agents")
|
|
252
|
+
Dir.glob("**/*.rb", base: agents_dir).map do |file|
|
|
253
|
+
File.join("app/agents", file)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
130
256
|
end
|
|
131
257
|
end
|
|
132
258
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Redacts sensitive values out of error context and job arguments before they
|
|
2
|
+
# leave the query layer for an AI agent (or any log). Job arguments and request
|
|
3
|
+
# context routinely carry passwords, tokens, and PII; this is the one place that
|
|
4
|
+
# stops them from reaching the agent.
|
|
5
|
+
#
|
|
6
|
+
# Tune SENSITIVE_KEY_PATTERN for your app. Keys that match are masked; everything
|
|
7
|
+
# else passes through untouched. Nested hashes and arrays are walked recursively.
|
|
8
|
+
class ErrorRedactor
|
|
9
|
+
SENSITIVE_KEY_PATTERN = /pass(word)?|secret|token|api[_-]?key|auth|credential|ssn|card|cvv|email/i
|
|
10
|
+
MASK = "[REDACTED]"
|
|
11
|
+
|
|
12
|
+
def self.redact(value)
|
|
13
|
+
new.redact(value)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def redact(value)
|
|
17
|
+
case value
|
|
18
|
+
when Hash
|
|
19
|
+
value.each_with_object({}) do |(key, val), acc|
|
|
20
|
+
acc[key] = sensitive?(key) ? MASK : redact(val)
|
|
21
|
+
end
|
|
22
|
+
when Array
|
|
23
|
+
value.map { |item| redact(item) }
|
|
24
|
+
else
|
|
25
|
+
value
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def sensitive?(key)
|
|
32
|
+
SENSITIVE_KEY_PATTERN.match?(key.to_s)
|
|
33
|
+
end
|
|
34
|
+
end
|