maquina-generators 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +30 -0
- data/lib/generators/maquina/rack_attack/USAGE +15 -0
- data/lib/generators/maquina/rack_attack/rack_attack_generator.rb +53 -0
- data/lib/generators/maquina/rack_attack/templates/config/initializers/rack_attack.rb.tt +95 -0
- data/lib/generators/maquina/solid_errors/solid_errors_generator.rb +21 -3
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/_actions.html.erb +39 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/_delete_button.html.erb +24 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/_error_card.html.erb +106 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/_resolve_button.html.erb +23 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/index.html.erb +74 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/show/_actions.html.erb +60 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/show/_error_details.html.erb +132 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/show/_header.html.erb +32 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/show/_properties.html.erb +77 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/show.html.erb +23 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/occurrences/_backtrace_line.html.erb +27 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/occurrences/_collection.html.erb +124 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/occurrences/_occurrence.html.erb +175 -0
- data/lib/maquina_generators/version.rb +1 -1
- data/test/generators/maquina/rack_attack_generator_test.rb +122 -0
- data/test/generators/maquina/solid_errors_generator_test.rb +25 -0
- data/test/tmp/Gemfile +1 -1
- data/test/tmp/config/initializers/rack_attack.rb +95 -0
- data/test/tmp/config/routes.rb +0 -1
- metadata +19 -3
- data/test/tmp/app/controllers/backstage_controller.rb +0 -4
- data/test/tmp/config/initializers/solid_errors.rb +0 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce5b50ab1d409ff8e66103ffd248866536fb97868694f514fc63e622a091d3e9
|
|
4
|
+
data.tar.gz: 991f0a0f392196b14285b68ec027cf120f4b610f172c6772e095343ff032bf1d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f151ba3435ab5cb9be0cf0ddbe8d864b18d3f59f2c403e08a9653998fbe95b384947d315f99bc155d5337603d931b839d1408aa2b227eb41562490e07082d87
|
|
7
|
+
data.tar.gz: 1caf6b0eb21b9eefb9ada1806e6af396fd435224edce1bf7dd1a1899a7243164b1baa599eebb28d91c2d142e26e19bcbe66da8db2032abea937804698ca46c26
|
data/README.md
CHANGED
|
@@ -69,11 +69,13 @@ All generated code lives in your app -- edit it directly:
|
|
|
69
69
|
- **BackstageController:** Inherits from `ActionController::Base` (bypasses app's ApplicationController concerns)
|
|
70
70
|
- **Initializer:** Credentials-first auth with ENV variable fallback, database connection config
|
|
71
71
|
- **Route:** Mounts `SolidErrors::Engine` under a configurable prefix
|
|
72
|
+
- **Custom views:** Optionally copies custom Tailwind-styled views to override the gem defaults
|
|
72
73
|
|
|
73
74
|
#### Usage
|
|
74
75
|
|
|
75
76
|
```bash
|
|
76
77
|
rails g maquina:solid_errors --prefix /admin
|
|
78
|
+
rails g maquina:solid_errors --prefix /admin --copy-views # Include custom views
|
|
77
79
|
```
|
|
78
80
|
|
|
79
81
|
The generator automatically runs `bundle install` and `solid_errors:install`. After running, execute `bin/rails db:migrate`.
|
|
@@ -82,6 +84,7 @@ The generator automatically runs `bundle install` and `solid_errors:install`. Af
|
|
|
82
84
|
|
|
83
85
|
```bash
|
|
84
86
|
rails g maquina:solid_errors --prefix /admin # Default env vars
|
|
87
|
+
rails g maquina:solid_errors --prefix /admin --copy-views # With custom views
|
|
85
88
|
rails g maquina:solid_errors --prefix /backstage \
|
|
86
89
|
--user-env-var ADMIN_USER --password-env-var ADMIN_PASSWORD # Custom env vars
|
|
87
90
|
```
|
|
@@ -130,6 +133,33 @@ Credentials are resolved in order:
|
|
|
130
133
|
|
|
131
134
|
---
|
|
132
135
|
|
|
136
|
+
### Rack Attack -- Request Protection
|
|
137
|
+
|
|
138
|
+
**Rack Attack** installs the [rack-attack](https://github.com/rack/rack-attack) gem with default security rules to block common vulnerability scans and throttle abusive requests.
|
|
139
|
+
|
|
140
|
+
#### What it generates
|
|
141
|
+
|
|
142
|
+
- **Initializer:** `config/initializers/rack_attack.rb` with blocklists, safelists, and throttles
|
|
143
|
+
|
|
144
|
+
#### Usage
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
rails g maquina:rack_attack
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The generator automatically runs `bundle install`.
|
|
151
|
+
|
|
152
|
+
#### Default Protections
|
|
153
|
+
|
|
154
|
+
- **Blocklists:** PHP files (`*.php`), WordPress paths (`wp-admin`, `wp-login`, etc.), sensitive files (`.env`, `.git`, `/etc/passwd`, etc.), scanner targets (`phpmyadmin`, `cgi-bin`, etc.)
|
|
155
|
+
- **Safelists:** Localhost (`127.0.0.1`, `::1`)
|
|
156
|
+
- **Throttles:** 300 requests/5min per IP (general), 5 login attempts/20s per IP
|
|
157
|
+
- **Responses:** 403 Forbidden for blocklisted, 429 Too Many Requests for throttled
|
|
158
|
+
|
|
159
|
+
Customize rules in `config/initializers/rack_attack.rb`.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
133
163
|
## Adding New Generators
|
|
134
164
|
|
|
135
165
|
Create a new folder under `lib/generators/maquina/`:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Installs Rack::Attack with default security rules to block common
|
|
3
|
+
vulnerability scans and throttle abusive requests.
|
|
4
|
+
|
|
5
|
+
Default protections:
|
|
6
|
+
- Blocks PHP file requests (*.php)
|
|
7
|
+
- Blocks WordPress paths (wp-admin, wp-login, wp-content, etc.)
|
|
8
|
+
- Blocks sensitive file access (.env, .git, /etc/passwd, etc.)
|
|
9
|
+
- Blocks common scanner targets (phpmyadmin, cgi-bin, etc.)
|
|
10
|
+
- Safelists localhost
|
|
11
|
+
- Throttles general requests (300/5min per IP)
|
|
12
|
+
- Throttles login attempts (5/20s per IP)
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
rails g maquina:rack_attack
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module Maquina
|
|
4
|
+
module Generators
|
|
5
|
+
class RackAttackGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
# 1. Initializer
|
|
9
|
+
def create_initializer
|
|
10
|
+
template "config/initializers/rack_attack.rb.tt",
|
|
11
|
+
"config/initializers/rack_attack.rb"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# 2. Add gem to Gemfile
|
|
15
|
+
def add_gem
|
|
16
|
+
gemfile_path = File.join(destination_root, "Gemfile")
|
|
17
|
+
if File.exist?(gemfile_path)
|
|
18
|
+
content = File.read(gemfile_path)
|
|
19
|
+
unless content.include?('gem "rack-attack"')
|
|
20
|
+
append_to_file "Gemfile", "\ngem \"rack-attack\"\n"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# 3. Bundle install
|
|
26
|
+
def run_bundle_install
|
|
27
|
+
return unless rails_app?
|
|
28
|
+
|
|
29
|
+
run "bundle install", capture: true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 4. Post-install message
|
|
33
|
+
def show_post_install
|
|
34
|
+
say ""
|
|
35
|
+
say "Rack::Attack has been installed!", :green
|
|
36
|
+
say ""
|
|
37
|
+
say "Default protections enabled:", :yellow
|
|
38
|
+
say " - Blocklist: PHP files, WordPress paths, sensitive files, scanner targets"
|
|
39
|
+
say " - Safelist: localhost (127.0.0.1, ::1)"
|
|
40
|
+
say " - Throttle: 300 req/5min per IP, 5 login attempts/20s per IP"
|
|
41
|
+
say ""
|
|
42
|
+
say "Customize rules in config/initializers/rack_attack.rb"
|
|
43
|
+
say ""
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def rails_app?
|
|
49
|
+
File.exist?(File.join(destination_root, "bin/rails"))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rack::Attack configuration
|
|
4
|
+
# https://github.com/rack/rack-attack
|
|
5
|
+
|
|
6
|
+
# Safelist localhost
|
|
7
|
+
Rack::Attack.safelist("allow-localhost") do |req|
|
|
8
|
+
req.ip == "127.0.0.1" || req.ip == "::1"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Blocklists — common vulnerability scanner paths
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
# Block PHP file requests
|
|
16
|
+
Rack::Attack.blocklist("block-php") do |req|
|
|
17
|
+
req.path.match?(/\.php\d?$/i)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Block WordPress paths
|
|
21
|
+
WORDPRESS_PATHS = %w[
|
|
22
|
+
/wp-admin
|
|
23
|
+
/wp-login
|
|
24
|
+
/wp-content
|
|
25
|
+
/wp-includes
|
|
26
|
+
/xmlrpc.php
|
|
27
|
+
/wp-config
|
|
28
|
+
/wp-cron
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
Rack::Attack.blocklist("block-wordpress") do |req|
|
|
32
|
+
WORDPRESS_PATHS.any? { |path| req.path.downcase.start_with?(path) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Block sensitive file access
|
|
36
|
+
SENSITIVE_PATTERNS = %w[
|
|
37
|
+
/.env
|
|
38
|
+
/.git
|
|
39
|
+
/.htaccess
|
|
40
|
+
/.htpasswd
|
|
41
|
+
/.aws
|
|
42
|
+
/.ssh
|
|
43
|
+
/.svn
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
Rack::Attack.blocklist("block-sensitive-files") do |req|
|
|
47
|
+
path = req.path.downcase
|
|
48
|
+
SENSITIVE_PATTERNS.any? { |pattern| path.start_with?(pattern) } ||
|
|
49
|
+
path.include?("/etc/passwd") ||
|
|
50
|
+
path.include?("/etc/shadow")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Block common scanner targets
|
|
54
|
+
SCANNER_PATHS = %w[
|
|
55
|
+
/cgi-bin
|
|
56
|
+
/phpmyadmin
|
|
57
|
+
/pma
|
|
58
|
+
/myadmin
|
|
59
|
+
/phpinfo
|
|
60
|
+
/mysqladmin
|
|
61
|
+
/admin/config.php
|
|
62
|
+
/solr/
|
|
63
|
+
/actuator
|
|
64
|
+
/telescope/requests
|
|
65
|
+
/debug
|
|
66
|
+
/_ignition
|
|
67
|
+
/vendor/phpunit
|
|
68
|
+
].freeze
|
|
69
|
+
|
|
70
|
+
Rack::Attack.blocklist("block-scanner-targets") do |req|
|
|
71
|
+
SCANNER_PATHS.any? { |path| req.path.downcase.start_with?(path) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Throttles
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
# General rate limit: 300 requests per 5 minutes per IP
|
|
79
|
+
Rack::Attack.throttle("req/ip", limit: 300, period: 5.minutes) do |req|
|
|
80
|
+
req.ip unless req.path.start_with?("/assets")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Login throttle: 5 attempts per 20 seconds per IP
|
|
84
|
+
Rack::Attack.throttle("login/ip", limit: 5, period: 20.seconds) do |req|
|
|
85
|
+
req.ip if req.path == "/session" && req.post?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Custom responses
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
# Return 403 Forbidden for blocklisted requests
|
|
93
|
+
Rack::Attack.blocklisted_responder = lambda do |_request|
|
|
94
|
+
[403, {"content-type" => "text/plain"}, ["Forbidden\n"]]
|
|
95
|
+
end
|
|
@@ -11,6 +11,8 @@ module Maquina
|
|
|
11
11
|
desc: "Environment variable for HTTP auth username"
|
|
12
12
|
class_option :password_env_var, type: :string, default: "SOLID_ERRORS_PASSWORD",
|
|
13
13
|
desc: "Environment variable for HTTP auth password"
|
|
14
|
+
class_option :copy_views, type: :boolean, default: false,
|
|
15
|
+
desc: "Copy custom Solid Errors views to the host app"
|
|
14
16
|
|
|
15
17
|
# 1. BackstageController
|
|
16
18
|
def create_backstage_controller
|
|
@@ -44,21 +46,30 @@ module Maquina
|
|
|
44
46
|
route "mount SolidErrors::Engine, at: \"#{mount_path}\""
|
|
45
47
|
end
|
|
46
48
|
|
|
47
|
-
# 5.
|
|
49
|
+
# 5. Custom views
|
|
50
|
+
def copy_views
|
|
51
|
+
return unless options[:copy_views]
|
|
52
|
+
|
|
53
|
+
view_files.each do |view|
|
|
54
|
+
copy_file view, view
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 6. Bundle install
|
|
48
59
|
def run_bundle_install
|
|
49
60
|
return unless rails_app?
|
|
50
61
|
|
|
51
62
|
run "bundle install", capture: true
|
|
52
63
|
end
|
|
53
64
|
|
|
54
|
-
#
|
|
65
|
+
# 7. Run solid_errors:install
|
|
55
66
|
def run_gem_install
|
|
56
67
|
return unless rails_app?
|
|
57
68
|
|
|
58
69
|
run "bin/rails generate solid_errors:install", capture: true
|
|
59
70
|
end
|
|
60
71
|
|
|
61
|
-
#
|
|
72
|
+
# 8. Post-install message
|
|
62
73
|
def show_post_install
|
|
63
74
|
say ""
|
|
64
75
|
say "Solid Errors has been installed!", :green
|
|
@@ -80,6 +91,13 @@ module Maquina
|
|
|
80
91
|
def rails_app?
|
|
81
92
|
File.exist?(File.join(destination_root, "bin/rails"))
|
|
82
93
|
end
|
|
94
|
+
|
|
95
|
+
def view_files
|
|
96
|
+
views_dir = File.join(self.class.source_root, "app/views/solid_errors")
|
|
97
|
+
Dir.glob("**/*.erb", base: views_dir).map do |file|
|
|
98
|
+
File.join("app/views/solid_errors", file)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
83
101
|
end
|
|
84
102
|
end
|
|
85
103
|
end
|
data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/_actions.html.erb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<%# locals: (error:) %>
|
|
2
|
+
|
|
3
|
+
<%= render "components/card" do %>
|
|
4
|
+
<div class="p-6 pb-2">
|
|
5
|
+
<h3 class="text-lg font-medium">Actions</h3>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="p-6 pt-0 space-y-3">
|
|
9
|
+
<%= link_to solid_errors.root_path, class: "button w-full justify-center" do %>
|
|
10
|
+
<svg
|
|
11
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
12
|
+
width="16"
|
|
13
|
+
height="16"
|
|
14
|
+
viewBox="0 0 24 24"
|
|
15
|
+
fill="none"
|
|
16
|
+
stroke="currentColor"
|
|
17
|
+
stroke-width="2"
|
|
18
|
+
stroke-linecap="round"
|
|
19
|
+
stroke-linejoin="round"
|
|
20
|
+
class="lucide"
|
|
21
|
+
>
|
|
22
|
+
<path d="m12 19-7-7 7-7" />
|
|
23
|
+
<path d="M19 12H5" />
|
|
24
|
+
</svg>
|
|
25
|
+
|
|
26
|
+
<span>Back to Errors</span>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
29
|
+
<% if error.resolved? %>
|
|
30
|
+
<%= render "solid_errors/errors/delete_button",
|
|
31
|
+
error: error,
|
|
32
|
+
class: "w-full justify-center" %>
|
|
33
|
+
<% else %>
|
|
34
|
+
<%= render "solid_errors/errors/resolve_button",
|
|
35
|
+
error: error,
|
|
36
|
+
class: "w-full justify-center" %>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<%# locals: (error:) %>
|
|
2
|
+
|
|
3
|
+
<%= button_to solid_errors.error_path(error),
|
|
4
|
+
method: :delete,
|
|
5
|
+
class: "button button-danger w-full justify-center inline-flex items-center gap-2 py-2 px-4 transition-all hover:scale-[1.02] active:scale-95",
|
|
6
|
+
data: { turbo_confirm: "Are you sure you want to delete this error?" } do %>
|
|
7
|
+
<svg
|
|
8
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
+
width="16"
|
|
10
|
+
height="16"
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
stroke-width="2"
|
|
15
|
+
stroke-linecap="round"
|
|
16
|
+
stroke-linejoin="round"
|
|
17
|
+
class="lucide"
|
|
18
|
+
>
|
|
19
|
+
<path d="M3 6h18" />
|
|
20
|
+
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
21
|
+
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
22
|
+
</svg>
|
|
23
|
+
<span>Delete Error</span>
|
|
24
|
+
<% end %>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<%# locals: (error:) %>
|
|
2
|
+
|
|
3
|
+
<%= render "components/card", css_classes: "hover:shadow-md transition-shadow" do %>
|
|
4
|
+
<div class="p-6">
|
|
5
|
+
<div class="flex items-start justify-between gap-4">
|
|
6
|
+
<div class="flex-1 min-w-0">
|
|
7
|
+
<!-- Severity & Status -->
|
|
8
|
+
<div class="flex items-center gap-2 mb-2">
|
|
9
|
+
<%= render "components/severity_badge", severity: error.severity %>
|
|
10
|
+
<%= render "components/error_status_badge", resolved: error.resolved? %>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Error Title -->
|
|
14
|
+
<%= link_to solid_errors.error_path(error),
|
|
15
|
+
class: "text-lg font-semibold text-foreground hover:text-primary transition-colors" do %>
|
|
16
|
+
<code class="break-words"><%= error.exception_class %></code>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<div class="mt-1 text-sm text-muted-foreground">
|
|
20
|
+
from <em><code><%= error.source %></code></em>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- Message with overflow handling -->
|
|
24
|
+
<pre
|
|
25
|
+
class="
|
|
26
|
+
mt-3 text-sm text-foreground whitespace-pre-wrap font-mono bg-muted/50 p-3
|
|
27
|
+
rounded-md overflow-auto max-h-32 break-words
|
|
28
|
+
"
|
|
29
|
+
><%= error.message %></pre>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Action Button -->
|
|
33
|
+
<div class="flex-shrink-0">
|
|
34
|
+
<% if error.resolved? %>
|
|
35
|
+
<%= button_to solid_errors.error_path(error),
|
|
36
|
+
method: :delete,
|
|
37
|
+
form: { data: { turbo: false } },
|
|
38
|
+
data: { turbo_confirm: "Are you sure you want to delete this error?" },
|
|
39
|
+
class: "button button-danger inline-flex items-center gap-2 py-2 px-4 transition-all hover:scale-[1.02] active:scale-95" do %>
|
|
40
|
+
<svg
|
|
41
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
42
|
+
width="16"
|
|
43
|
+
height="16"
|
|
44
|
+
viewBox="0 0 24 24"
|
|
45
|
+
fill="none"
|
|
46
|
+
stroke="currentColor"
|
|
47
|
+
stroke-width="2"
|
|
48
|
+
stroke-linecap="round"
|
|
49
|
+
stroke-linejoin="round"
|
|
50
|
+
class="lucide"
|
|
51
|
+
>
|
|
52
|
+
<path d="M3 6h18" />
|
|
53
|
+
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
54
|
+
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
55
|
+
</svg>
|
|
56
|
+
|
|
57
|
+
<span>Delete</span>
|
|
58
|
+
<% end %>
|
|
59
|
+
<% else %>
|
|
60
|
+
<%= button_to solid_errors.error_path(error),
|
|
61
|
+
method: :patch,
|
|
62
|
+
form: { data: { turbo: false } },
|
|
63
|
+
params: { error: { resolved_at: Time.now } },
|
|
64
|
+
class: "button button-primary inline-flex items-center gap-2 py-2 px-4 transition-all hover:scale-[1.02] active:scale-95" do %>
|
|
65
|
+
<svg
|
|
66
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
67
|
+
width="16"
|
|
68
|
+
height="16"
|
|
69
|
+
viewBox="0 0 24 24"
|
|
70
|
+
fill="none"
|
|
71
|
+
stroke="currentColor"
|
|
72
|
+
stroke-width="2"
|
|
73
|
+
stroke-linecap="round"
|
|
74
|
+
stroke-linejoin="round"
|
|
75
|
+
class="lucide"
|
|
76
|
+
>
|
|
77
|
+
<circle cx="12" cy="12" r="10" />
|
|
78
|
+
<path d="m9 12 2 2 4-4" />
|
|
79
|
+
</svg>
|
|
80
|
+
|
|
81
|
+
<span>Resolve</span>
|
|
82
|
+
<% end %>
|
|
83
|
+
<% end %>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Footer Stats -->
|
|
88
|
+
<div
|
|
89
|
+
class="
|
|
90
|
+
mt-4 flex items-center gap-6 text-sm text-muted-foreground border-t
|
|
91
|
+
border-border pt-4
|
|
92
|
+
"
|
|
93
|
+
>
|
|
94
|
+
<span>
|
|
95
|
+
<strong class="text-foreground"><%= error.occurrences_count %></strong>
|
|
96
|
+
occurrences
|
|
97
|
+
</span>
|
|
98
|
+
|
|
99
|
+
<span>
|
|
100
|
+
Last seen
|
|
101
|
+
<strong class="text-foreground"><%= time_ago_in_words(error.recent_occurrence) %></strong>
|
|
102
|
+
ago
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<% end %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<%# locals: (error:) %>
|
|
2
|
+
|
|
3
|
+
<%= button_to solid_errors.error_path(error),
|
|
4
|
+
method: :patch,
|
|
5
|
+
params: { error: { resolved_at: Time.now } },
|
|
6
|
+
class: "button button-primary w-full justify-center inline-flex items-center gap-2 py-2 px-4 transition-all hover:scale-[1.02] active:scale-95" do %>
|
|
7
|
+
<svg
|
|
8
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
+
width="16"
|
|
10
|
+
height="16"
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
stroke-width="2"
|
|
15
|
+
stroke-linecap="round"
|
|
16
|
+
stroke-linejoin="round"
|
|
17
|
+
class="lucide"
|
|
18
|
+
>
|
|
19
|
+
<circle cx="12" cy="12" r="10" />
|
|
20
|
+
<path d="m9 12 2 2 4-4" />
|
|
21
|
+
</svg>
|
|
22
|
+
<span>Resolve Error</span>
|
|
23
|
+
<% end %>
|
data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/errors/index.html.erb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<div class="container mx-auto px-4 py-8">
|
|
2
|
+
<!-- Header with tabs -->
|
|
3
|
+
<div class="mb-6 border-b border-border">
|
|
4
|
+
<div class="flex items-center justify-between mb-4">
|
|
5
|
+
<h1 class="text-2xl font-bold text-foreground">Errors</h1>
|
|
6
|
+
|
|
7
|
+
<!-- Delete All Resolved Button - Only on Resolved tab -->
|
|
8
|
+
<% if error_scope.resolved? %>
|
|
9
|
+
<% resolved_count = SolidErrors::Error.resolved.count %>
|
|
10
|
+
<% if resolved_count > 0 %>
|
|
11
|
+
<%= button_to main_app.solid_errors_resolved_errors_path,
|
|
12
|
+
method: :delete,
|
|
13
|
+
form: { data: { turbo: false } },
|
|
14
|
+
data: {
|
|
15
|
+
turbo_confirm: "This will queue deletion of all #{resolved_count} resolved error#{resolved_count == 1 ? '' : 's'}. The process will run in the background. Are you sure?"
|
|
16
|
+
},
|
|
17
|
+
class: "button button-danger inline-flex items-center gap-2 py-2 px-4 text-sm transition-all hover:scale-[1.02] active:scale-95" do %>
|
|
18
|
+
<svg
|
|
19
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
20
|
+
width="16"
|
|
21
|
+
height="16"
|
|
22
|
+
viewBox="0 0 24 24"
|
|
23
|
+
fill="none"
|
|
24
|
+
stroke="currentColor"
|
|
25
|
+
stroke-width="2"
|
|
26
|
+
stroke-linecap="round"
|
|
27
|
+
stroke-linejoin="round"
|
|
28
|
+
class="lucide"
|
|
29
|
+
>
|
|
30
|
+
<path d="M3 6h18" />
|
|
31
|
+
|
|
32
|
+
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
33
|
+
|
|
34
|
+
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
35
|
+
|
|
36
|
+
<line
|
|
37
|
+
x1="10"
|
|
38
|
+
x2="10"
|
|
39
|
+
y1="11"
|
|
40
|
+
y2="17"
|
|
41
|
+
/>
|
|
42
|
+
|
|
43
|
+
<line
|
|
44
|
+
x1="14"
|
|
45
|
+
x2="14"
|
|
46
|
+
y1="11"
|
|
47
|
+
y2="17"
|
|
48
|
+
/>
|
|
49
|
+
</svg>
|
|
50
|
+
<span>Delete All Resolved</span>
|
|
51
|
+
<% end %>
|
|
52
|
+
<% end %>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Tab Navigation -->
|
|
57
|
+
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
|
|
58
|
+
<%= link_to solid_errors.root_path(scope: :unresolved),
|
|
59
|
+
class: "inline-flex items-center border-b-2 px-1 py-4 text-sm font-medium #{ error_scope.unresolved? ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:border-border hover:text-foreground' }" do %>
|
|
60
|
+
<span>⏳ Unresolved</span>
|
|
61
|
+
<% end %>
|
|
62
|
+
|
|
63
|
+
<%= link_to solid_errors.root_path(scope: :resolved),
|
|
64
|
+
class: "inline-flex items-center border-b-2 px-1 py-4 text-sm font-medium #{ error_scope.resolved? ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:border-border hover:text-foreground' }" do %>
|
|
65
|
+
<span>✅ Resolved</span>
|
|
66
|
+
<% end %>
|
|
67
|
+
</nav>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Error Grid -->
|
|
71
|
+
<div class="grid grid-cols-1 gap-4">
|
|
72
|
+
<%= render partial: "solid_errors/errors/error_card", collection: @errors, as: :error %>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<%# locals: (error:) %>
|
|
2
|
+
|
|
3
|
+
<%= render "components/card" do %>
|
|
4
|
+
<div class="p-6 pb-2">
|
|
5
|
+
<h3 class="text-lg font-medium text-foreground">Actions</h3>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="p-6 pt-0 space-y-3">
|
|
9
|
+
<!-- Clean Old Occurrences Button - FIRST (only if more than 10) -->
|
|
10
|
+
<% if error.occurrences.count > 10 %>
|
|
11
|
+
<%= button_to main_app.solid_errors_error_occurrences_path(error),
|
|
12
|
+
method: :delete,
|
|
13
|
+
form: { data: { turbo: false } },
|
|
14
|
+
data: {
|
|
15
|
+
turbo_confirm: "This will delete #{error.occurrences.count - 10} old occurrence#{'s' if error.occurrences.count - 10 != 1}, keeping only the latest 10. Are you sure?"
|
|
16
|
+
},
|
|
17
|
+
class: "button w-full justify-center inline-flex items-center gap-2 py-2 px-4 bg-yellow-100 text-yellow-800 hover:bg-yellow-200 border-yellow-300 transition-all hover:scale-[1.02] active:scale-95" do %>
|
|
18
|
+
<svg
|
|
19
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
20
|
+
width="16"
|
|
21
|
+
height="16"
|
|
22
|
+
viewBox="0 0 24 24"
|
|
23
|
+
fill="none"
|
|
24
|
+
stroke="currentColor"
|
|
25
|
+
stroke-width="2"
|
|
26
|
+
stroke-linecap="round"
|
|
27
|
+
stroke-linejoin="round"
|
|
28
|
+
class="lucide"
|
|
29
|
+
>
|
|
30
|
+
<path d="M3 6h18" />
|
|
31
|
+
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
32
|
+
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
33
|
+
|
|
34
|
+
<line
|
|
35
|
+
x1="10"
|
|
36
|
+
x2="10"
|
|
37
|
+
y1="11"
|
|
38
|
+
y2="17"
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
<line
|
|
42
|
+
x1="14"
|
|
43
|
+
x2="14"
|
|
44
|
+
y1="11"
|
|
45
|
+
y2="17"
|
|
46
|
+
/>
|
|
47
|
+
</svg>
|
|
48
|
+
|
|
49
|
+
<span>Clean Old Occurrences</span>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% end %>
|
|
52
|
+
|
|
53
|
+
<!-- Resolve/Delete Button - SECOND -->
|
|
54
|
+
<% if error.resolved? %>
|
|
55
|
+
<%= render "solid_errors/errors/delete_button", error: error %>
|
|
56
|
+
<% else %>
|
|
57
|
+
<%= render "solid_errors/errors/resolve_button", error: error %>
|
|
58
|
+
<% end %>
|
|
59
|
+
</div>
|
|
60
|
+
<% end %>
|