maquina-generators 0.6.0 → 0.7.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -0
  3. data/lib/generators/maquina/clave/clave_generator.rb +48 -0
  4. data/lib/generators/maquina/clave/templates/app/controllers/registration/verifications_controller.rb.tt +10 -1
  5. data/lib/generators/maquina/clave/templates/app/controllers/session/verifications_controller.rb.tt +10 -1
  6. data/lib/generators/maquina/clave/templates/app/models/email_verification.rb.tt +18 -0
  7. data/lib/generators/maquina/clave/templates/config/locales/clave.en.yml +2 -0
  8. data/lib/generators/maquina/clave/templates/config/locales/clave.es.yml +2 -0
  9. data/lib/generators/maquina/csp_reports/USAGE +18 -0
  10. data/lib/generators/maquina/csp_reports/csp_reports_generator.rb +231 -0
  11. data/lib/generators/maquina/csp_reports/templates/app/controllers/csp_reports_controller.rb +24 -0
  12. data/lib/generators/maquina/csp_reports/templates/app/models/security/csp_violation.rb +89 -0
  13. data/lib/generators/maquina/csp_reports/templates/app/models/security/csp_violation_report.rb +30 -0
  14. data/lib/generators/maquina/csp_reports/templates/app/views/backstage/security/_csp_violations.html.erb +42 -0
  15. data/lib/generators/maquina/csp_reports/templates/db/create_csp_violations.rb.tt +23 -0
  16. data/lib/maquina_generators/version.rb +1 -1
  17. data/test/generators/maquina/clave_generator_test.rb +74 -0
  18. data/test/generators/maquina/csp_reports_generator_test.rb +151 -0
  19. data/test/tmp/.gitignore +5 -0
  20. data/test/tmp/.rubocop.yml +11 -0
  21. data/test/tmp/.standard.yml +1 -0
  22. data/test/tmp/Gemfile +13 -1
  23. data/test/tmp/Procfile.dev +3 -0
  24. data/test/tmp/README.md +89 -0
  25. data/test/tmp/app/controllers/home_controller.rb +6 -0
  26. data/test/tmp/app/javascript/application.js +1 -0
  27. data/test/tmp/app/views/home/index.html.erb +204 -0
  28. data/test/tmp/app/views/layouts/application.html.erb +10 -0
  29. data/test/tmp/config/application.rb +15 -0
  30. data/test/tmp/config/database.yml +90 -0
  31. data/test/tmp/config/database.yml.example +90 -0
  32. data/test/tmp/config/environments/development.rb +8 -0
  33. data/test/tmp/config/environments/production.rb +6 -0
  34. data/test/tmp/config/initializers/generators.rb +4 -0
  35. data/test/tmp/config/routes.rb +1 -2
  36. metadata +27 -14
  37. data/test/tmp/app/controllers/application_controller.rb +0 -2
  38. data/test/tmp/app/controllers/registrations_controller.rb +0 -37
  39. data/test/tmp/app/models/account.rb +0 -5
  40. data/test/tmp/app/models/current.rb +0 -5
  41. data/test/tmp/app/models/user.rb +0 -12
  42. data/test/tmp/app/views/registrations/new.html.erb +0 -72
  43. data/test/tmp/app/views/sessions/new.html.erb +0 -46
  44. data/test/tmp/config/locales/registration.en.yml +0 -44
  45. data/test/tmp/config/locales/registration.es.yml +0 -44
  46. data/test/tmp/db/migrate/20260927024163_create_accounts.rb +0 -9
  47. data/test/tmp/db/migrate/20260927024164_add_account_fields_to_users.rb +0 -7
@@ -0,0 +1,42 @@
1
+ <%# locals: (report:) %>
2
+ <%# Content Security Policy violations browsers reported to /csp-reports
3
+ (maquina:csp_reports), grouped so each row is one thing to fix. %>
4
+ <%= render "components/card" do %>
5
+ <%= render "components/card/header" do %>
6
+ <%= render "components/card/title", text: "Content Security Policy violations" %>
7
+ <%= render "components/card/description",
8
+ text: "#{number_with_delimiter(report.total)} reported in the last #{report.window.inspect}, kept for #{Security::CspViolation::RETENTION.inspect}. Most frequent first." %>
9
+ <% end %>
10
+ <%= render "components/card/content", spacing: :full do %>
11
+ <% if report.empty? %>
12
+ <%= empty_state(title: "No violation was reported.", icon: :check_circle, size: :compact) %>
13
+ <% else %>
14
+ <div class="overflow-x-auto">
15
+ <%= render "components/table", container: false, "aria-label": "Content Security Policy violations" do %>
16
+ <%= render "components/table/header" do %>
17
+ <%= render "components/table/row" do %>
18
+ <%= render "components/table/head", text: "Directive" %>
19
+ <%= render "components/table/head", text: "Blocked" %>
20
+ <%= render "components/table/head", text: "Page" %>
21
+ <%= render "components/table/head", text: "Reports", css_classes: "text-right" %>
22
+ <%= render "components/table/head", text: "Last seen" %>
23
+ <% end %>
24
+ <% end %>
25
+ <%= render "components/table/body" do %>
26
+ <% report.top.each do |group| %>
27
+ <%= render "components/table/row" do %>
28
+ <%= render "components/table/cell", css_classes: "font-mono", text: group.effective_directive %>
29
+ <%= render "components/table/cell", css_classes: "font-mono whitespace-normal break-all", text: group.blocked_uri.presence || "—" %>
30
+ <%= render "components/table/cell", css_classes: "font-mono whitespace-normal break-all", text: group.document_uri.presence || "—" %>
31
+ <%= render "components/table/cell", css_classes: "text-right tabular-nums", text: number_with_delimiter(group.count) %>
32
+ <%= render "components/table/cell" do %>
33
+ <%= render "backstage/security/utc_time", time: group.last_seen %>
34
+ <% end %>
35
+ <% end %>
36
+ <% end %>
37
+ <% end %>
38
+ <% end %>
39
+ </div>
40
+ <% end %>
41
+ <% end %>
42
+ <% end %>
@@ -0,0 +1,23 @@
1
+ # Content Security Policy violation reports (maquina:csp_reports). Lives in the
2
+ # `security` database next to abuse_events; db/security_schema.rb carries the same
3
+ # table for a fresh database, this migration adds it to an existing one.
4
+ class CreateCspViolations < ActiveRecord::Migration<%= migration_version %>
5
+ def change
6
+ create_table :csp_violations do |t|
7
+ t.string :effective_directive, null: false
8
+ t.string :blocked_uri
9
+ t.string :document_uri
10
+ t.string :source_file
11
+ t.integer :line_number
12
+ t.integer :column_number
13
+ t.string :disposition
14
+ t.string :sample
15
+ t.string :ip
16
+ t.string :user_agent
17
+ t.datetime :created_at, null: false
18
+ end
19
+
20
+ add_index :csp_violations, :created_at
21
+ add_index :csp_violations, [:effective_directive, :created_at]
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module MaquinaGenerators
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -181,9 +181,83 @@ class Maquina::Generators::ClaveGeneratorTest < Rails::Generators::TestCase
181
181
  end
182
182
  end
183
183
 
184
+ test "the sign-in code is encrypted at rest and compared in constant time" do
185
+ run_generator
186
+
187
+ assert_file "app/models/email_verification.rb" do |content|
188
+ assert_match(/encrypts :code/, content)
189
+ assert_match(/def code_matches\?\(candidate\)/, content)
190
+ assert_match(/ActiveSupport::SecurityUtils\.secure_compare/, content)
191
+ end
192
+ %w[session registration].each do |flow|
193
+ assert_file "app/controllers/#{flow}/verifications_controller.rb" do |content|
194
+ assert_match(/unless @verification\.code_matches\?\(params\[:code\]\)/, content)
195
+ assert_no_match(/@verification\.code !=/, content)
196
+ end
197
+ end
198
+ end
199
+
200
+ test "a code dies after five wrong guesses in both verification flows" do
201
+ run_generator
202
+
203
+ assert_file "app/models/email_verification.rb" do |content|
204
+ assert_match(/MAX_ATTEMPTS = 5/, content)
205
+ assert_match(/def locked\?\n attempts >= MAX_ATTEMPTS/, content)
206
+ end
207
+ {"session" => "sessions", "registration" => "registrations"}.each do |flow, scope|
208
+ assert_file "app/controllers/#{flow}/verifications_controller.rb" do |content|
209
+ assert_match(/if @verification\.locked\?\n @verification\.update!\(expires_at: Time\.current\)/, content)
210
+ assert_match(/t\("flash\.#{scope}\.code_locked"\)/, content)
211
+ assert content.index("@verification.locked?") < content.index("code_matches?"),
212
+ "the lock is checked before the guess, so a correct sixth guess is refused too"
213
+ end
214
+ end
215
+ %w[en es].each do |locale|
216
+ assert_file "config/locales/clave.#{locale}.yml" do |content|
217
+ assert_equal 2, content.scan(/^ code_locked: /).size
218
+ end
219
+ end
220
+ end
221
+
222
+ test "writes Active Record encryption keys into the credentials when they are missing" do
223
+ credentials = create_credentials("secret_key_base: abc\n")
224
+
225
+ output = run_generator
226
+
227
+ content = credentials.read
228
+ assert_match(/\Asecret_key_base: abc\n\nactive_record_encryption:\n primary_key: \w{32}\n deterministic_key: \w{32}\n key_derivation_salt: \w{32}\n\z/, content)
229
+ assert_match(/Added Active Record encryption keys/, output)
230
+ end
231
+
232
+ test "leaves existing encryption keys alone" do
233
+ existing = "active_record_encryption:\n primary_key: keep\n"
234
+ credentials = create_credentials(existing)
235
+
236
+ run_generator
237
+
238
+ assert_equal existing, credentials.read
239
+ end
240
+
241
+ test "without a master key it explains how to add the encryption keys" do
242
+ output = run_generator
243
+
244
+ assert_no_file "config/credentials.yml.enc"
245
+ assert_match(/bin\/rails db:encryption:init/, output)
246
+ end
247
+
184
248
  private
185
249
 
186
250
  def mkdir_p(path)
187
251
  FileUtils.mkdir_p(File.join(destination_root, path))
188
252
  end
253
+
254
+ def create_credentials(content)
255
+ key = ActiveSupport::EncryptedConfiguration.generate_key
256
+ File.write(File.join(destination_root, "config/master.key"), key)
257
+ ActiveSupport::EncryptedConfiguration.new(
258
+ config_path: File.join(destination_root, "config/credentials.yml.enc"),
259
+ key_path: File.join(destination_root, "config/master.key"),
260
+ env_key: "RAILS_MASTER_KEY_UNUSED", raise_if_missing_key: true
261
+ ).tap { |credentials| credentials.write(content) }
262
+ end
189
263
  end
@@ -0,0 +1,151 @@
1
+ require "test_helper"
2
+ require "generators/maquina/csp_reports/csp_reports_generator"
3
+
4
+ class Maquina::Generators::CspReportsGeneratorTest < Rails::Generators::TestCase
5
+ tests Maquina::Generators::CspReportsGenerator
6
+ destination File.expand_path("../../tmp", __dir__)
7
+
8
+ SECURITY_SCHEMA = <<~RUBY
9
+ # The `security` database.
10
+
11
+ ActiveRecord::Schema[8.1].define(version: 1) do
12
+ create_table "abuse_events", force: :cascade do |t|
13
+ t.string "ip", null: false
14
+ end
15
+ end
16
+ RUBY
17
+
18
+ CSP_INITIALIZER = <<~RUBY
19
+ Rails.application.configure do
20
+ config.content_security_policy do |policy|
21
+ policy.default_src :self
22
+ end
23
+
24
+ config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
25
+ config.content_security_policy_report_only = true
26
+ end
27
+ RUBY
28
+
29
+ RACK_ATTACK = <<~RUBY
30
+ class Rack::Attack
31
+ def self.counted_ip(req)
32
+ req.ip unless req.path.start_with?("/assets", "/rails/active_storage")
33
+ end
34
+ end
35
+ RUBY
36
+
37
+ SECURITY_PAGE = <<~ERB
38
+ <div class="py-8 space-y-8">
39
+ <% if @report.empty? %>
40
+ empty
41
+ <% end %>
42
+ </div>
43
+ ERB
44
+
45
+ setup do
46
+ prepare_destination
47
+ write "config/routes.rb", "Rails.application.routes.draw do\nend\n"
48
+ write "db/security_schema.rb", SECURITY_SCHEMA
49
+ write "config/initializers/content_security_policy.rb", CSP_INITIALIZER
50
+ write "config/initializers/rack_attack.rb", RACK_ATTACK
51
+ write "config/recurring.yml", "production:\n purge_abuse_events:\n command: \"x\"\n"
52
+ write "app/views/backstage/security/show.html.erb", SECURITY_PAGE
53
+ end
54
+
55
+ test "refuses to run without the security database" do
56
+ File.delete(File.join(destination_root, "db/security_schema.rb"))
57
+
58
+ output = capture(:stderr) { run_generator }
59
+
60
+ assert_match(/Run `rails g maquina:security --prefix \/admin` first/, output)
61
+ assert_no_file "app/controllers/csp_reports_controller.rb"
62
+ end
63
+
64
+ test "installs the endpoint, the models and the route" do
65
+ run_generator
66
+
67
+ assert_file "app/controllers/csp_reports_controller.rb", /class CspReportsController < ActionController::API/
68
+ assert_file "app/models/security/csp_violation.rb", /class Security::CspViolation < Security::Record/
69
+ assert_file "app/models/security/csp_violation_report.rb", /class Security::CspViolationReport/
70
+ assert_file "config/routes.rb", %r{post "/csp-reports", to: "csp_reports#create", as: :csp_reports}
71
+ end
72
+
73
+ test "adds the table as a security migration and to the schema" do
74
+ run_generator
75
+
76
+ assert_migration "db/security_migrate/create_csp_violations.rb", /create_table :csp_violations/
77
+ migration = Dir[File.join(destination_root, "db/security_migrate/*_create_csp_violations.rb")].first
78
+ version = File.basename(migration).split("_").first
79
+ assert_file "db/security_schema.rb" do |content|
80
+ assert_match(/define\(version: #{version.sub(/\A(\d{4})(\d{2})(\d{2})(\d{6})\z/, '\1_\2_\3_\4')}\)/, content)
81
+ assert_match(/create_table "abuse_events".*create_table "csp_violations"/m, content)
82
+ assert_match(/\n end\nend\n\z/, content)
83
+ assert_no_match(/[ \t]+$/, content, "no trailing whitespace")
84
+ end
85
+ end
86
+
87
+ test "points the policy at the endpoint and gives anonymous requests a nonce" do
88
+ run_generator %w[--path /security/csp]
89
+
90
+ assert_file "config/initializers/content_security_policy.rb" do |content|
91
+ assert_match(%r{config\.content_security_policy do \|policy\|\n.*policy\.report_uri "/security/csp"}m, content)
92
+ assert_no_match(/report_to/, content.gsub(/#.*$/, ""))
93
+ assert_match(/request\.session\.id\.to_s\.presence \|\| SecureRandom\.base64\(16\)/, content)
94
+ end
95
+ end
96
+
97
+ test "takes the endpoint out of the general throttle and caps it on its own" do
98
+ run_generator
99
+
100
+ assert_file "config/initializers/rack_attack.rb" do |content|
101
+ assert_match(%r{start_with\?\("/assets", "/rails/active_storage", "/csp-reports"\)}, content)
102
+ assert_match(/Rack::Attack\.throttle\("csp\/ip", limit: 100, period: 5\.minutes\)/, content)
103
+ end
104
+ end
105
+
106
+ test "extends a STATIC_FILE_PREFIXES list when the rules use one" do
107
+ write "config/initializers/rack_attack.rb", "class Rack::Attack\n STATIC_FILE_PREFIXES = %w[/assets /u/].freeze\nend\n"
108
+
109
+ run_generator
110
+
111
+ assert_file "config/initializers/rack_attack.rb", %r{STATIC_FILE_PREFIXES = %w\[/assets /u/ /csp-reports\]}
112
+ end
113
+
114
+ test "purges daily and shows the section on the Security page" do
115
+ run_generator
116
+
117
+ assert_file "config/recurring.yml", /purge_csp_violations:\n command: "Security::CspViolation\.expired\.delete_all"/
118
+ assert_file "app/views/backstage/security/_csp_violations.html.erb", /Content Security Policy violations/
119
+ assert_file "app/views/backstage/security/show.html.erb",
120
+ %r{<% end %>\n\n <%= render "backstage/security/csp_violations", report: Security::CspViolationReport\.new %>\n</div>\n\z}
121
+ end
122
+
123
+ test "running twice changes nothing the second time" do
124
+ run_generator
125
+ snapshot = %w[db/security_schema.rb config/initializers/rack_attack.rb config/initializers/content_security_policy.rb
126
+ config/recurring.yml app/views/backstage/security/show.html.erb config/routes.rb].to_h { |f| [f, File.read(File.join(destination_root, f))] }
127
+
128
+ run_generator
129
+
130
+ snapshot.each { |file, content| assert_equal content, File.read(File.join(destination_root, file)), file }
131
+ assert_equal 1, Dir[File.join(destination_root, "db/security_migrate/*_create_csp_violations.rb")].size
132
+ end
133
+
134
+ test "lists what it could not place instead of forcing it" do
135
+ write "config/initializers/content_security_policy.rb", "# Rails.application.configure do\n# end\n"
136
+ write "config/initializers/rack_attack.rb", "class Rack::Attack\nend\n"
137
+
138
+ output = run_generator
139
+
140
+ assert_match(%r{policy\.report_uri "/csp-reports"}, output)
141
+ assert_match(/Exempt \/csp-reports from the general throttle/, output)
142
+ end
143
+
144
+ private
145
+
146
+ def write(path, content)
147
+ full = File.join(destination_root, path)
148
+ FileUtils.mkdir_p(File.dirname(full))
149
+ File.write(full, content)
150
+ end
151
+ end
@@ -0,0 +1,5 @@
1
+ /log/*
2
+ /tmp/*
3
+
4
+ # Ignore database configuration
5
+ config/database.yml
@@ -0,0 +1,11 @@
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.
7
+ require:
8
+ - standard
9
+
10
+ inherit_gem:
11
+ standard: config/base.yml
@@ -0,0 +1 @@
1
+ ruby_version: 3.2
data/test/tmp/Gemfile CHANGED
@@ -1,3 +1,15 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gem "bcrypt"
3
+ gem "brakeman", group: :development
4
+
5
+ gem "bundle-audit", group: :development
6
+
7
+ gem "letter_opener", group: :development
8
+
9
+ gem "standard", ">= 1.54", group: :development
10
+
11
+ gem "rails-i18n"
12
+
13
+ gem "maquina-components"
14
+
15
+ gem "aws-sdk-s3", group: :production
@@ -0,0 +1,3 @@
1
+ web: bin/rails server -p 3000
2
+ css: bin/rails tailwindcss:watch
3
+ solid_queue: bin/rails solid_queue:start
@@ -0,0 +1,89 @@
1
+ # Tmp
2
+
3
+ Built with [maquina.app](https://maquina.app) — production-tested tools for developers who ship alone.
4
+
5
+ ## Getting Started
6
+
7
+ ### Prerequisites
8
+
9
+ - Ruby 3.2+
10
+ - Rails 8+
11
+ - PostgreSQL (or SQLite for development)
12
+
13
+ ### Setup
14
+
15
+ ```bash
16
+ bin/setup # Install dependencies and prepare databases
17
+ bin/dev # Start all processes (web, css, solid_queue)
18
+ ```
19
+
20
+ Your app will be running at **http://localhost:3000**.
21
+
22
+ ## What's Included
23
+
24
+ | Feature | Description |
25
+ |---------|-------------|
26
+ | **Background Jobs** | Solid Queue with Mission Control Jobs dashboard |
27
+ | **Error Tracking** | Solid Errors with custom UI |
28
+ | **Rate Limiting** | Rack::Attack with sensible defaults |
29
+ | **UI Components** | Maquina Components (ERB partials + Tailwind + Stimulus) |
30
+ | **Caching** | Solid Cache |
31
+ | **WebSockets** | Solid Cable |
32
+ | **Rich Text** | Action Text |
33
+ | **File Storage** | Active Storage (S3-ready in production) |
34
+ | **Multi-database** | Separate databases for queue, cache, cable, errors, and security |
35
+
36
+ ## Development URLs
37
+
38
+ | URL | Description |
39
+ |-----|-------------|
40
+ | http://localhost:3000 | Application |
41
+ | http://localhost:3000/admin | Admin overview |
42
+ | http://localhost:3000/admin/errors | Errors dashboard |
43
+ | http://localhost:3000/admin/jobs | Jobs dashboard |
44
+ | http://localhost:3000/admin/security | Security dashboard |
45
+
46
+ ## Scripts
47
+
48
+ - `bin/setup` — Install dependencies and prepare databases
49
+ - `bin/dev` — Start all processes (web, css, solid_queue)
50
+
51
+ ## Credentials
52
+
53
+ Edit credentials with:
54
+
55
+ ```bash
56
+ bin/rails credentials:edit
57
+ ```
58
+
59
+ Add backstage credentials for the admin dashboards (overview, errors, jobs, security):
60
+
61
+ ```yaml
62
+ backstage:
63
+ username: your_user
64
+ password: your_password
65
+ ```
66
+
67
+ ## Maquina Ecosystem
68
+
69
+ ### UI Components
70
+
71
+ [Maquina Components](https://maquina.app/documentation/components/) — a library of ERB partials, Tailwind CSS styles, and Stimulus controllers for building consistent, accessible UIs in Rails.
72
+
73
+ ### AI Tools
74
+
75
+ Maquina provides a suite of AI-powered tools to accelerate your Rails development:
76
+
77
+ - **Rails MCP Server** — Model Context Protocol server for Rails applications
78
+ - **Rails Simplifier** — simplify and refine Rails code following 37signals patterns
79
+ - **Rails Upgrade Assistant** — comprehensive Rails upgrade reports and migration guides
80
+ - **Maquina UI Standards** — build consistent UIs using maquina_components
81
+ - **Neovim MCP Server** — MCP server for Neovim integration
82
+
83
+ Learn more at [maquina.app/documentation/ai-tools](https://maquina.app/documentation/ai-tools/).
84
+
85
+ ## Learn More
86
+
87
+ Visit [maquina.app](https://maquina.app) for full documentation.
88
+
89
+ Maquina is built for the one-person framework philosophy: production-first, standard Rails, designed to scale with a small team.
@@ -0,0 +1,6 @@
1
+ class HomeController < ApplicationController
2
+ allow_unauthenticated_access if respond_to?(:allow_unauthenticated_access)
3
+
4
+ def index
5
+ end
6
+ end
@@ -0,0 +1 @@
1
+ // Entry point for the build script
@@ -0,0 +1,204 @@
1
+
2
+ <section class="bg-primary/5 border-b border-border">
3
+ <div class="max-w-5xl mx-auto px-5 py-20 sm:py-28">
4
+ <h1 class="text-4xl sm:text-5xl font-bold tracking-tight text-foreground mb-5">
5
+ Tools for Rails developers<br class="hidden sm:block"> who ship alone
6
+ </h1>
7
+ <p class="text-lg text-muted-foreground leading-relaxed max-w-2xl mb-8">
8
+ Maquina is a growing collection of open-source tools extracted from production Rails applications.
9
+ UI components, background jobs, error tracking, and authentication — configured and ready to go.
10
+ </p>
11
+
12
+
13
+ </div>
14
+ </section>
15
+
16
+
17
+
18
+
19
+ <section class="max-w-5xl mx-auto px-5 py-12">
20
+ <p class="text-sm font-semibold uppercase tracking-wider text-primary mb-6">What's Included</p>
21
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
22
+
23
+
24
+
25
+ <%= render "components/card" do %>
26
+ <%= render "components/card/header" do %>
27
+ <%= render "components/card/title", text: "Background Jobs" %>
28
+ <% end %>
29
+ <%= render "components/card/content" do %>
30
+ <p class="text-sm text-muted-foreground">Solid Queue backed by SQLite. Includes Mission Control dashboard for monitoring and managing jobs.</p>
31
+ <% end %>
32
+ <% end %>
33
+
34
+ <%= render "components/card" do %>
35
+ <%= render "components/card/header" do %>
36
+ <%= render "components/card/title", text: "Error Tracking" %>
37
+ <% end %>
38
+ <%= render "components/card/content" do %>
39
+ <p class="text-sm text-muted-foreground">Solid Errors captures exceptions automatically. Browse, resolve, and clean up — no external service needed.</p>
40
+ <% end %>
41
+ <% end %>
42
+
43
+ <%= render "components/card" do %>
44
+ <%= render "components/card/header" do %>
45
+ <%= render "components/card/title", text: "Rate Limiting" %>
46
+ <% end %>
47
+ <%= render "components/card/content" do %>
48
+ <p class="text-sm text-muted-foreground">Rack::Attack bans scanners and floods and throttles sign-in. Every refusal shows on the Security dashboard.</p>
49
+ <% end %>
50
+ <% end %>
51
+
52
+ <%= render "components/card" do %>
53
+ <%= render "components/card/header" do %>
54
+ <%= render "components/card/title", text: "Caching" %>
55
+ <% end %>
56
+ <%= render "components/card/content" do %>
57
+ <p class="text-sm text-muted-foreground">Solid Cache with SQLite backend. No Redis or Memcached to manage.</p>
58
+ <% end %>
59
+ <% end %>
60
+
61
+ <%= render "components/card" do %>
62
+ <%= render "components/card/header" do %>
63
+ <%= render "components/card/title", text: "Real-time" %>
64
+ <% end %>
65
+ <%= render "components/card/content" do %>
66
+ <p class="text-sm text-muted-foreground">Solid Cable for WebSocket support. Action Cable backed by SQLite — zero external dependencies.</p>
67
+ <% end %>
68
+ <% end %>
69
+
70
+ <%= render "components/card" do %>
71
+ <%= render "components/card/header" do %>
72
+ <%= render "components/card/title", text: "UI Components" %>
73
+ <% end %>
74
+ <%= render "components/card/content" do %>
75
+ <p class="text-sm text-muted-foreground">maquina_components — ERB partials styled with Tailwind CSS and Stimulus controllers. No React, no build step.</p>
76
+ <% end %>
77
+ <% end %>
78
+
79
+ <%= render "components/card" do %>
80
+ <%= render "components/card/header" do %>
81
+ <%= render "components/card/title", text: "Standard Rails" %>
82
+ <% end %>
83
+ <%= render "components/card/content" do %>
84
+ <p class="text-sm text-muted-foreground">Importmaps, Turbo, Stimulus. No webpack, no bundler, no extra frameworks to learn.</p>
85
+ <% end %>
86
+ <% end %>
87
+
88
+ </div>
89
+ </section>
90
+
91
+
92
+ <section class="max-w-5xl mx-auto px-5 pb-12">
93
+ <%= render "components/card" do %>
94
+ <%= render "components/card/header" do %>
95
+ <%= render "components/card/title", text: "Maquina Components" %>
96
+ <%= render "components/card/description", text: "UI components for Rails — ERB partials styled with Tailwind CSS 4.0 and Stimulus controllers. Inspired by shadcn/ui, built for the Rails way." %>
97
+ <% end %>
98
+ <%= render "components/card/content" do %>
99
+ <div class="flex flex-wrap gap-2">
100
+ <%= render "components/badge", variant: :outline, size: :sm do %>Card<% end %>
101
+ <%= render "components/badge", variant: :outline, size: :sm do %>Badge<% end %>
102
+ <%= render "components/badge", variant: :outline, size: :sm do %>Table<% end %>
103
+ <%= render "components/badge", variant: :outline, size: :sm do %>Sidebar<% end %>
104
+ <%= render "components/badge", variant: :outline, size: :sm do %>Calendar<% end %>
105
+ <%= render "components/badge", variant: :outline, size: :sm do %>Combobox<% end %>
106
+ <%= render "components/badge", variant: :outline, size: :sm do %>Date Picker<% end %>
107
+ <%= render "components/badge", variant: :outline, size: :sm do %>Toast<% end %>
108
+ <%= render "components/badge", variant: :outline, size: :sm do %>Dropdown Menu<% end %>
109
+ <%= render "components/badge", variant: :outline, size: :sm do %>Toggle Group<% end %>
110
+ <%= render "components/badge", variant: :outline, size: :sm do %>Pagination<% end %>
111
+ <%= render "components/badge", variant: :outline, size: :sm do %>Breadcrumbs<% end %>
112
+ <%= render "components/badge", variant: :outline, size: :sm do %>Alert<% end %>
113
+ <%= render "components/badge", variant: :outline, size: :sm do %>Empty State<% end %>
114
+ <%= render "components/badge", variant: :outline, size: :sm do %>Forms<% end %>
115
+ </div>
116
+ <% end %>
117
+ <%= render "components/card/footer" do %>
118
+ <%= link_to "Browse Components", "https://maquina.app/documentation/components/", target: "_blank", rel: "noopener noreferrer", data: { component: "button", variant: "primary" } %>
119
+ <% end %>
120
+ <% end %>
121
+ </section>
122
+
123
+ <div class="max-w-5xl mx-auto px-5">
124
+ <%= render "components/separator" %>
125
+ </div>
126
+
127
+
128
+ <section class="max-w-5xl mx-auto px-5 py-12">
129
+ <p class="text-sm font-semibold uppercase tracking-wider text-primary mb-6">Ecosystem</p>
130
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
131
+
132
+ <%= render "components/card" do %>
133
+ <%= render "components/card/header" do %>
134
+ <%= render "components/card/title", text: "Rails MCP Server" %>
135
+ <% end %>
136
+ <%= render "components/card/content" do %>
137
+ <p class="text-sm text-muted-foreground">A Model Context Protocol server for Rails projects. Analyze models, routes, schemas, and execute read-only Ruby code in your Rails context.</p>
138
+ <% end %>
139
+ <% end %>
140
+
141
+ <%= render "components/card" do %>
142
+ <%= render "components/card/header" do %>
143
+ <%= render "components/card/title", text: "Neovim MCP Server" %>
144
+ <% end %>
145
+ <%= render "components/card/content" do %>
146
+ <p class="text-sm text-muted-foreground">MCP server for Neovim integration. Read and update buffers, coordinate file changes across your editor and AI assistants.</p>
147
+ <% end %>
148
+ <% end %>
149
+
150
+ <%= render "components/card" do %>
151
+ <%= render "components/card/header" do %>
152
+ <%= render "components/card/title", text: "Rails Upgrade Assistant" %>
153
+ <% end %>
154
+ <%= render "components/card/content" do %>
155
+ <p class="text-sm text-muted-foreground">A Claude Skill that analyzes Rails applications and generates upgrade guides. Covers Rails 7.0 through 8.1 with breaking changes and migration steps.</p>
156
+ <% end %>
157
+ <% end %>
158
+
159
+ <%= render "components/card" do %>
160
+ <%= render "components/card/header" do %>
161
+ <%= render "components/card/title", text: "Rails Simplifier" %>
162
+ <% end %>
163
+ <%= render "components/card/content" do %>
164
+ <p class="text-sm text-muted-foreground">Code simplification following 37signals patterns and the One Person Framework philosophy.</p>
165
+ <% end %>
166
+ <% end %>
167
+
168
+ </div>
169
+ <div class="mt-6 text-center">
170
+ <%= link_to "Explore All Tools", "https://maquina.app/", target: "_blank", rel: "noopener noreferrer", data: { component: "button", variant: "outline" } %>
171
+ </div>
172
+ </section>
173
+
174
+ <div class="max-w-5xl mx-auto px-5">
175
+ <%= render "components/separator" %>
176
+ </div>
177
+
178
+
179
+ <section class="max-w-5xl mx-auto px-5 py-12">
180
+ <p class="text-sm font-semibold uppercase tracking-wider text-primary mb-8 text-center">Philosophy</p>
181
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-10">
182
+ <div>
183
+ <h3 class="font-bold text-lg text-foreground mb-2">Production First</h3>
184
+ <p class="text-sm text-muted-foreground leading-relaxed">Every tool starts in a real application. No theoretical exercises — only code that has shipped.</p>
185
+ </div>
186
+ <div>
187
+ <h3 class="font-bold text-lg text-foreground mb-2">Standard Rails</h3>
188
+ <p class="text-sm text-muted-foreground leading-relaxed">ERB, Tailwind, Stimulus. No new paradigms to learn, no complex build pipelines.</p>
189
+ </div>
190
+ <div>
191
+ <h3 class="font-bold text-lg text-foreground mb-2">One Person Scale</h3>
192
+ <p class="text-sm text-muted-foreground leading-relaxed">Built for developers who ship alone. Simple tools that stay out of your way.</p>
193
+ </div>
194
+ </div>
195
+ </section>
196
+
197
+
198
+ <section class="bg-primary/5 border-t border-border">
199
+ <div class="max-w-5xl mx-auto px-5 py-16 text-center">
200
+ <h2 class="text-2xl font-bold text-foreground mb-4">Start building today</h2>
201
+ <p class="text-muted-foreground mb-6">Visit maquina.app to explore the full ecosystem.</p>
202
+ <%= link_to "Visit maquina.app", "https://maquina.app/", target: "_blank", rel: "noopener noreferrer", data: { component: "button", variant: "primary" } %>
203
+ </div>
204
+ </section>