rails_tool_views 0.1.0 → 0.3.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/CHANGELOG.md +17 -0
- data/README.md +14 -70
- data/app/controllers/rails_tool_views/surfaces_controller.rb +24 -4
- data/app/helpers/rails_tool_views/surfaces_helper.rb +4 -0
- data/app/models/rails_tool_views/agent_interaction.rb +2 -0
- data/app/models/rails_tool_views/surface.rb +17 -1
- data/app/views/layouts/rails_tool_views/application.html.erb +24 -0
- data/app/views/rails_tool_views/surfaces/_closed.html.erb +2 -0
- data/app/views/rails_tool_views/surfaces/_surface.html.erb +1 -1
- data/app/views/rails_tool_views/surfaces/page.html.erb +9 -0
- data/db/migrate/20261019000000_add_rails_tool_views_lifecycle.rb +23 -0
- data/lib/generators/rails_tool_views/component/component_generator.rb +47 -0
- data/lib/generators/rails_tool_views/component/templates/component_test.rb +15 -0
- data/lib/generators/rails_tool_views/component/templates/partial.html.erb +6 -0
- data/lib/generators/rails_tool_views/component/templates/registration.rb +24 -0
- data/lib/generators/rails_tool_views/install/install_generator.rb +1 -0
- data/lib/generators/rails_tool_views/install/templates/add_rails_tool_views_lifecycle.rb +23 -0
- data/lib/generators/rails_tool_views/install/templates/rails_tool_views.rb +2 -1
- data/lib/rails_tool_views/core.rb +6 -1
- data/lib/rails_tool_views/lifecycle.rb +92 -0
- data/lib/rails_tool_views/runtime.rb +33 -17
- data/lib/rails_tool_views/version.rb +1 -1
- data/lib/rails_tool_views.rb +1 -0
- metadata +12 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f0b45e8093744ab3053cb621e1b8118ef6c618b98f907e8e88b9b16f5b36fba
|
|
4
|
+
data.tar.gz: '05959be24f98969c883260f0b06cef6815d812393bf7c250d0d1dea1cc552e7b'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57ab0c782013453cc510f47402fcdae2b934a06fe69760b45411466b543c851d7b8b84e653bcef7824f3267f97319783e712754b7c321b9bd844826645f94afe
|
|
7
|
+
data.tar.gz: 2d741cf1b44e389bb3cc7a00bcf1de0da70ef3377077950fed1c4a166d9a62d7ac3ae339550779616b2c414ed23b8612531b2d2fda01f10e462cbc40d7ba6b5b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-10-19
|
|
4
|
+
|
|
5
|
+
- Opt-in scoped `purge_closed` preview/apply with deadlines, bounded batches, atomic receipt cleanup and durable correlation tombstones. Host business records stay intact.
|
|
6
|
+
|
|
7
|
+
- Add explicit host-authorized `close` and `abandon`, with read-only closed rendering, preserved exact action replay and first-termination retention deadlines.
|
|
8
|
+
- Add lifecycle snapshot metadata and an additive migration. Stop old workers before upgrading; rollback refuses once a Surface was closed.
|
|
9
|
+
- Coordinate delivery claims and lifecycle changes through the Surface lock while keeping callbacks outside transactions. Quarantine ambiguous lease takeovers and legacy multiple-attempt histories from cleanup.
|
|
10
|
+
|
|
11
|
+
## [0.2.0] - 2026-10-18
|
|
12
|
+
|
|
13
|
+
- Add `rails_tool_views:component NAME` for a Partial, closed registration schemas and a Minitest scaffold. Component and action policies deny by default. Existing paths, unsafe names and symlink escapes are refused before writing.
|
|
14
|
+
- Render complete Engine HTML read/error pages with accessible document structure, authorized correction fields and a saved-view reload link. Embedded helpers and JSON/Turbo retain their formats.
|
|
15
|
+
- Fix redirects after correcting a non-Turbo error when the referrer is a POST-only Engine route.
|
|
16
|
+
- Add real Chrome E2E tests, including JavaScript-disabled mobile correction, fixture screenshots and a browser CI job. Keep browser dependencies in a development-only bundle.
|
|
17
|
+
- Exercise the installed gem and generated scaffold in a temporary Rails host, both with developer dependencies and an isolated network-resolved GEM_HOME.
|
|
18
|
+
- No database migration or new runtime dependency.
|
|
19
|
+
|
|
3
20
|
## [0.1.0] - 2026-10-18
|
|
4
21
|
|
|
5
22
|
First public release. Earlier version labels below describe unpublished development milestones.
|
data/README.md
CHANGED
|
@@ -1,90 +1,34 @@
|
|
|
1
1
|
# RailsToolViews
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Turn trusted Rails Partials and ViewComponents into persistent, interactive tools. Your Rails application owns authentication, permissions and business data. RubyLLM integration is optional.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Documentation](https://rails-tool-views.wangyaodi.com) · [Getting started](https://rails-tool-views.wangyaodi.com/getting-started/) · [API](https://rails-tool-views.wangyaodi.com/api/) · [Roadmap](ROADMAP.md)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Install
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Version **0.1.0** is the first public release.
|
|
12
|
-
|
|
13
|
-
## Try the workbench
|
|
14
|
-
|
|
15
|
-
Requires Ruby 3.3+ and Bundler. From this checkout:
|
|
16
|
-
|
|
17
|
-
```sh
|
|
18
|
-
bundle install
|
|
19
|
-
bin/demo
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Open **http://127.0.0.1:3100**. Stop with `Ctrl-C` when finished.
|
|
23
|
-
|
|
24
|
-
| Workflow | What to try |
|
|
25
|
-
| --- | --- |
|
|
26
|
-
| Component playground | Present a Partial, chat card or ViewComponent. Save a choice and refresh to restore it. |
|
|
27
|
-
| Customer intake | Submit an invalid email, correct it and create a customer with field-level feedback. |
|
|
28
|
-
| Support workspace | Find a ticket, inspect its account, review a draft reply and confirm the local outbox. |
|
|
29
|
-
| Content review | Edit, regenerate, accept a version and download its text export. |
|
|
30
|
-
|
|
31
|
-
Switch synthetic identities to explore two tenants and two roles. Fixtures run locally, with no provider calls or external messages. The identity switch is a testing aid, not production authentication. See the [demo walkthrough](docs/site/_getting_started/demo.md) for test scenarios and continuation failure/retry.
|
|
32
|
-
|
|
33
|
-
## Add it to a Rails application
|
|
34
|
-
|
|
35
|
-
Add the gem:
|
|
36
|
-
|
|
37
|
-
```ruby
|
|
38
|
-
# Gemfile
|
|
39
|
-
gem "rails_tool_views", "~> 0.1.0"
|
|
40
|
-
```
|
|
9
|
+
Requires Ruby 3.3 or later and a patched Rails release.
|
|
41
10
|
|
|
42
11
|
```sh
|
|
43
|
-
bundle
|
|
12
|
+
bundle add rails_tool_views
|
|
44
13
|
bin/rails generate rails_tool_views:install
|
|
45
14
|
bin/rails db:migrate
|
|
46
15
|
```
|
|
47
16
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
```ruby
|
|
51
|
-
scope = RailsToolViews::Scope.new(actor: current_user.id.to_s, tenant: current_account.id.to_s)
|
|
52
|
-
surface = RailsToolViews.present(:choice_card, props: approved_props, scope: scope)
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
```erb
|
|
56
|
-
<%= rails_tool_views_surface(surface, scope: scope) %>
|
|
57
|
-
```
|
|
17
|
+
Continue with [Getting Started](https://rails-tool-views.wangyaodi.com/getting-started/) to configure authenticated scope and register your first component. Component and action policies deny access until you configure them.
|
|
58
18
|
|
|
59
|
-
|
|
19
|
+
## Try the demo
|
|
60
20
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
Read the [searchable guides](https://rails-tool-views.wangyaodi.com). Preview locally with `docs/bin/serve.sh`; setup and build instructions are in [docs/README.md](docs/README.md).
|
|
64
|
-
|
|
65
|
-
- [Getting Started](docs/site/_getting_started/getting-started.md): installation and a working first component.
|
|
66
|
-
- [Configuration](docs/site/_getting_started/configuration.md): host identity and engine settings.
|
|
67
|
-
- [Core guides](docs/site/_core_features): components, surfaces, actions and validation.
|
|
68
|
-
- [Advanced guides](docs/site/_advanced): optional RubyLLM, continuation and production boundaries.
|
|
69
|
-
- [API reference](docs/site/_reference/api-reference.md): generated Ruby API documentation.
|
|
70
|
-
- [Upgrading](docs/site/_reference/upgrading.md): moving an unpublished development host to the first public version.
|
|
71
|
-
|
|
72
|
-
The dummy host in [`test/dummy`](test/dummy) contains the complete runnable examples and is excluded from the gem runtime. OddlyDock is a planned reference host, not a dependency.
|
|
73
|
-
|
|
74
|
-
## Development
|
|
21
|
+
From this checkout:
|
|
75
22
|
|
|
76
23
|
```sh
|
|
77
|
-
bundle
|
|
78
|
-
|
|
79
|
-
# Optional adapter tests:
|
|
80
|
-
BUNDLE_GEMFILE=gemfiles/adapter.gemfile bundle install
|
|
81
|
-
BUNDLE_GEMFILE=gemfiles/adapter.gemfile bundle exec rake test:adapter
|
|
24
|
+
bundle install
|
|
25
|
+
bin/demo
|
|
82
26
|
```
|
|
83
27
|
|
|
84
|
-
|
|
28
|
+
Open http://127.0.0.1:3100 and stop with Ctrl-C. The demo covers choices, customer forms, support and content review using offline fixtures. See the [walkthrough](https://rails-tool-views.wangyaodi.com/demo/).
|
|
85
29
|
|
|
86
|
-
##
|
|
30
|
+
## Contribute
|
|
87
31
|
|
|
88
|
-
|
|
32
|
+
Read [Contributing](CONTRIBUTING.md) for development and documentation commands. Report vulnerabilities through the [private security channel](SECURITY.md).
|
|
89
33
|
|
|
90
|
-
RailsToolViews is an unofficial community project. Rails marks belong to David Heinemeier Hansson
|
|
34
|
+
Released under the [MIT license](LICENSE.txt). RailsToolViews is an unofficial community project, without affiliation or endorsement. Rails marks belong to David Heinemeier Hansson and are exclusively licensed to the Rails Foundation; see the [trademark policy](https://rubyonrails.org/trademarks).
|
|
@@ -23,7 +23,7 @@ module RailsToolViews
|
|
|
23
23
|
surface = RailsToolViews.restore(params[:id], scope: current_scope)
|
|
24
24
|
respond_to do |format|
|
|
25
25
|
format.json { render json: surface.snapshot }
|
|
26
|
-
format.html {
|
|
26
|
+
format.html { render_surface_page(surface) }
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -88,9 +88,29 @@ module RailsToolViews
|
|
|
88
88
|
format.turbo_stream { render turbo_stream: turbo_stream.replace(surface.dom_id, RailsToolViews.render(surface, view_context: view_context, scope: current_scope)), status: status }
|
|
89
89
|
# Without Turbo, return to the page that embeds the Surface; it re-renders saved state.
|
|
90
90
|
# The Engine's own helper keeps whatever mount path and name the host chose.
|
|
91
|
-
format.html {
|
|
91
|
+
format.html { redirect_after_action(surface) }
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
|
+
def redirect_after_action(surface)
|
|
95
|
+
# After correcting a full-page error, Referer can be a POST-only Engine
|
|
96
|
+
# route. Redirect to the readable Surface instead of GET /actions (404).
|
|
97
|
+
referrer_path = begin
|
|
98
|
+
URI.parse(request.referer.to_s).path.to_s
|
|
99
|
+
rescue URI::InvalidURIError
|
|
100
|
+
""
|
|
101
|
+
end
|
|
102
|
+
if referrer_path.start_with?("#{surface_path(surface)}/")
|
|
103
|
+
redirect_to surface_path(surface), status: :see_other
|
|
104
|
+
else
|
|
105
|
+
redirect_back_or_to surface_path(surface), allow_other_host: false, status: :see_other
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
def render_surface_page(surface, error: nil)
|
|
109
|
+
html = surface && RailsToolViews.render(surface, view_context: view_context, scope: current_scope, error: error)
|
|
110
|
+
render template: "rails_tool_views/surfaces/page", layout: "rails_tool_views/application",
|
|
111
|
+
locals: { surface_html: html, reload_path: surface && surface_path(surface), error: error },
|
|
112
|
+
status: error ? error.status : :ok
|
|
113
|
+
end
|
|
94
114
|
def reject_request(error)
|
|
95
115
|
respond_to do |format|
|
|
96
116
|
format.json { render json: error.to_h, status: error.status }
|
|
@@ -107,9 +127,9 @@ module RailsToolViews
|
|
|
107
127
|
format.html do
|
|
108
128
|
begin
|
|
109
129
|
surface = RailsToolViews.restore(params[:id], scope: current_scope)
|
|
110
|
-
|
|
130
|
+
render_surface_page(surface, error: error)
|
|
111
131
|
rescue RailsToolViews::Error
|
|
112
|
-
|
|
132
|
+
render_surface_page(nil, error: error)
|
|
113
133
|
end
|
|
114
134
|
end
|
|
115
135
|
end
|
|
@@ -4,6 +4,9 @@ module RailsToolViews
|
|
|
4
4
|
module SurfacesHelper
|
|
5
5
|
# Engine-owned copy. Translate or reword any of it under rails_tool_views.surface.<key>.
|
|
6
6
|
TEXT = {
|
|
7
|
+
page_title: "Interactive view", page_error_title: "Unable to complete this request", reload: "Reload saved view",
|
|
8
|
+
closed_title: "View closed.", abandoned_title: "View abandoned.", closed_help: "Saved actions are read-only. Start a new view to continue.",
|
|
9
|
+
agent_abandoned: "AI follow-up abandoned by the application.",
|
|
7
10
|
ready: "View ready.", saved: "Changes saved.", revision: "Saved revision %{revision}",
|
|
8
11
|
saving: "Saving changes…", save_failed: "Changes could not be saved. Review the message and try again.",
|
|
9
12
|
agent_pending: "AI follow-up pending.", agent_completed: "AI follow-up completed.",
|
|
@@ -55,6 +58,7 @@ module RailsToolViews
|
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
def rails_tool_views_body(surface, definition, error: nil) # :nodoc:
|
|
61
|
+
return render(partial: "rails_tool_views/surfaces/closed", formats: [:html], locals: { surface: surface }) if surface.closed?
|
|
58
62
|
html = if definition.partial
|
|
59
63
|
render(partial: definition.partial, locals: { props: surface.props, state: surface.state, surface: surface, error: error })
|
|
60
64
|
else
|
|
@@ -6,6 +6,8 @@ module RailsToolViews
|
|
|
6
6
|
belongs_to :surface
|
|
7
7
|
belongs_to :action_execution
|
|
8
8
|
self.filter_attributes += %i[payload]
|
|
9
|
+
# Rails 7.2 cannot infer a SQLite boolean default written by Rails 8.1.
|
|
10
|
+
attribute :delivery_uncertain, :boolean, default: false
|
|
9
11
|
attribute :payload, RedactedJson.new
|
|
10
12
|
# Returns the stable host deduplication key across delivery retries.
|
|
11
13
|
def delivery_key
|
|
@@ -15,9 +15,25 @@ module RailsToolViews
|
|
|
15
15
|
def dom_id
|
|
16
16
|
"rails_tool_views_surface_#{id}"
|
|
17
17
|
end
|
|
18
|
+
# True once the host closes or abandons the view. Closed views cannot reopen.
|
|
19
|
+
def closed?
|
|
20
|
+
closed_at.present?
|
|
21
|
+
end
|
|
22
|
+
# True when payload cleanup has retained only the durable tombstone.
|
|
23
|
+
def purged?
|
|
24
|
+
purged_at.present?
|
|
25
|
+
end
|
|
26
|
+
# Returns the lifecycle independently of domain record or delivery status.
|
|
27
|
+
def lifecycle_status
|
|
28
|
+
return "purged" if purged?
|
|
29
|
+
return "abandoned" if abandoned_at
|
|
30
|
+
closed? ? "closed" : "active"
|
|
31
|
+
end
|
|
18
32
|
# Returns the UI projection; it is not a permission grant.
|
|
19
33
|
def snapshot
|
|
20
|
-
{ id: id, component: component_key, component_digest: component_digest, props: props, state: state, revision: revision
|
|
34
|
+
{ id: id, component: component_key, component_digest: component_digest, props: props, state: state, revision: revision,
|
|
35
|
+
lifecycle: { status: lifecycle_status, closed_at: closed_at&.iso8601(6),
|
|
36
|
+
retain_until: retain_until&.iso8601(6), purged_at: purged_at&.iso8601(6) } }
|
|
21
37
|
end
|
|
22
38
|
end
|
|
23
39
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="<%= I18n.locale %>">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title><%= rails_tool_views_text(error ? :page_error_title : :page_title) %></title>
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<style>
|
|
9
|
+
.rails-tool-views-page { box-sizing: border-box; max-width: 48rem; margin: 2rem auto; padding: 0 1rem; font: 1rem/1.6 system-ui, sans-serif; overflow-wrap: anywhere; }
|
|
10
|
+
.rails-tool-views-page * { box-sizing: border-box; }
|
|
11
|
+
.rails-tool-views-page input, .rails-tool-views-page textarea, .rails-tool-views-page select { max-width: 100%; font: inherit; }
|
|
12
|
+
.rails-tool-views-page label { display: block; }
|
|
13
|
+
.rails-tool-views-page fieldset { min-width: 0; }
|
|
14
|
+
.rails-tool-views-page button, .rails-tool-views-page input[type=submit] { padding: .5rem 1rem; font: inherit; cursor: pointer; }
|
|
15
|
+
.rails-tool-views-page [role=alert], .rails-tool-views-page [aria-invalid=true] { color: #a51d1d; }
|
|
16
|
+
.rails-tool-views-page :focus-visible { outline: 3px solid #215dc4; outline-offset: 3px; }
|
|
17
|
+
</style>
|
|
18
|
+
</head>
|
|
19
|
+
<body class="rails-tool-views-page">
|
|
20
|
+
<main aria-labelledby="rails-tool-views-page-title">
|
|
21
|
+
<%= yield %>
|
|
22
|
+
</main>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
</p>
|
|
9
9
|
<% interaction = surface.agent_interactions.order(:id).last %>
|
|
10
10
|
<% if interaction && interaction.status != "skipped" %>
|
|
11
|
-
<p role="status"><%= rails_tool_views_text({ "pending" => :agent_pending, "delivering" => :agent_pending, "completed" => :agent_completed, "failed" => :agent_failed }.fetch(interaction.status, :agent_unavailable)) %></p>
|
|
11
|
+
<p role="status"><%= rails_tool_views_text({ "pending" => :agent_pending, "delivering" => :agent_pending, "completed" => :agent_completed, "failed" => :agent_failed, "abandoned" => :agent_abandoned }.fetch(interaction.status, :agent_unavailable)) %></p>
|
|
12
12
|
<% if interaction.status == "failed" %>
|
|
13
13
|
<%= form_with url: rails_tool_views_routes.surface_continuation_path(surface, interaction_id: interaction.id), method: :post do %>
|
|
14
14
|
<button type="submit"><%= rails_tool_views_text(:agent_retry) %></button>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<h1 id="rails-tool-views-page-title"><%= rails_tool_views_text(error ? :page_error_title : :page_title) %></h1>
|
|
2
|
+
<% if surface_html %>
|
|
3
|
+
<%= surface_html %>
|
|
4
|
+
<% elsif error %>
|
|
5
|
+
<%= render partial: "rails_tool_views/surfaces/error", locals: { error: error } %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% if error && reload_path %>
|
|
8
|
+
<p><%= link_to rails_tool_views_text(:reload), reload_path %></p>
|
|
9
|
+
<% end %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
class AddRailsToolViewsLifecycle < ActiveRecord::Migration[7.2]
|
|
3
|
+
def up
|
|
4
|
+
add_column :rails_tool_views_surfaces, :closed_at, :datetime
|
|
5
|
+
add_column :rails_tool_views_surfaces, :abandoned_at, :datetime
|
|
6
|
+
add_column :rails_tool_views_surfaces, :retain_until, :datetime
|
|
7
|
+
add_column :rails_tool_views_surfaces, :purged_at, :datetime
|
|
8
|
+
add_index :rails_tool_views_surfaces, [:tenant_ref, :actor_ref, :closed_at], name: "index_rails_tool_views_retention_scope"
|
|
9
|
+
add_column :rails_tool_views_agent_interactions, :delivery_uncertain, :boolean, default: false, null: false
|
|
10
|
+
# Old rows do not distinguish explicit failed retries from lease takeovers.
|
|
11
|
+
# Preserve uncertainty instead of assuming an old callback has stopped.
|
|
12
|
+
execute "UPDATE rails_tool_views_agent_interactions SET delivery_uncertain = #{connection.quote(true)} WHERE attempts > 1 OR status = 'delivering'"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def down
|
|
16
|
+
if select_value("SELECT COUNT(*) FROM rails_tool_views_surfaces WHERE closed_at IS NOT NULL").to_i.positive?
|
|
17
|
+
raise ActiveRecord::IrreversibleMigration, "Closed or purged Surfaces cannot be safely reopened by an older runtime. Restore a pre-upgrade backup instead."
|
|
18
|
+
end
|
|
19
|
+
remove_column :rails_tool_views_agent_interactions, :delivery_uncertain
|
|
20
|
+
remove_index :rails_tool_views_surfaces, name: "index_rails_tool_views_retention_scope"
|
|
21
|
+
%i[closed_at abandoned_at retain_until purged_at].each { |column| remove_column :rails_tool_views_surfaces, column }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "rails/generators"
|
|
3
|
+
|
|
4
|
+
module RailsToolViews
|
|
5
|
+
module Generators
|
|
6
|
+
# Generates a trusted Partial definition with closed schemas and denied policies.
|
|
7
|
+
class ComponentGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
argument :name, type: :string, required: true, desc: "Flat snake_case component key"
|
|
10
|
+
desc "Generate a Partial, registration and Minitest scaffold. Policies deny access until configured."
|
|
11
|
+
|
|
12
|
+
# Preflights all destinations, then writes the registration, Partial and test.
|
|
13
|
+
def create_component
|
|
14
|
+
unless name.match?(/\A[a-z][a-z0-9_]{0,63}\z/)
|
|
15
|
+
raise Thor::Error, "Use a flat snake_case name of 1 to 64 characters, starting with a letter."
|
|
16
|
+
end
|
|
17
|
+
outputs = {
|
|
18
|
+
"registration.rb" => "config/rails_tool_views/components/#{name}.rb",
|
|
19
|
+
"partial.html.erb" => "app/views/rails_tool_views/components/_#{name}.html.erb",
|
|
20
|
+
"component_test.rb" => "test/rails_tool_views/components/#{name}_test.rb"
|
|
21
|
+
}
|
|
22
|
+
# Preflight every output before writing any file. Even --force must not
|
|
23
|
+
# destroy host code or follow a symlink outside the destination root.
|
|
24
|
+
root = File.realpath(destination_root)
|
|
25
|
+
outputs.each_value do |relative|
|
|
26
|
+
path = File.join(root, relative)
|
|
27
|
+
if File.exist?(path) || File.symlink?(path)
|
|
28
|
+
raise Thor::Error, "Destination exists: #{relative}. Resolve the conflict before generating."
|
|
29
|
+
end
|
|
30
|
+
parent = File.dirname(path)
|
|
31
|
+
parent = File.dirname(parent) until File.exist?(parent) || File.symlink?(parent)
|
|
32
|
+
begin
|
|
33
|
+
real = File.realpath(parent)
|
|
34
|
+
rescue Errno::ENOENT, Errno::ELOOP
|
|
35
|
+
raise Thor::Error, "Invalid destination parent: #{relative}"
|
|
36
|
+
end
|
|
37
|
+
unless File.directory?(real) && (real == root || real.start_with?(root + File::SEPARATOR))
|
|
38
|
+
raise Thor::Error, "Destination escapes the application: #{relative}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
outputs.each { |source, target| template(source, target) }
|
|
42
|
+
say "Load config/rails_tool_views/components/*.rb inside your shared to_prepare block after reset_registry!."
|
|
43
|
+
say "Replace BOTH denied policies with host authorization before presenting this component."
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "test_helper"
|
|
3
|
+
|
|
4
|
+
class <%= name.camelize %>ComponentTest < ActiveSupport::TestCase
|
|
5
|
+
test "<%= name %> denies presentation until host policies are configured" do
|
|
6
|
+
error = assert_raises(RailsToolViews::Error) do
|
|
7
|
+
RailsToolViews.present(:<%= name %>, props: { title: "Review" },
|
|
8
|
+
scope: RailsToolViews::Scope.new(actor: "test-user", tenant: "test-tenant"))
|
|
9
|
+
end
|
|
10
|
+
assert_equal "NOT_ALLOWED", error.code
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Replace the denial test when you configure host policies. Add an allowed
|
|
14
|
+
# actor/tenant, a denied actor/tenant, action validation and persisted-state tests.
|
|
15
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Load from the host's shared to_prepare block. Do not reset the registry here.
|
|
3
|
+
RailsToolViews.register(:<%= name %>,
|
|
4
|
+
description: "<%= name.humanize %>",
|
|
5
|
+
partial: "rails_tool_views/components/<%= name %>",
|
|
6
|
+
schema: {
|
|
7
|
+
type: "object", additionalProperties: false, required: ["title"],
|
|
8
|
+
properties: { title: { type: "string", maxLength: 120 } }
|
|
9
|
+
},
|
|
10
|
+
# Replace with host authorization using access.actor and access.tenant.
|
|
11
|
+
policy: ->(_access) { false },
|
|
12
|
+
actions: {
|
|
13
|
+
set_value: {
|
|
14
|
+
schema: {
|
|
15
|
+
type: "object", additionalProperties: false, required: ["value"],
|
|
16
|
+
properties: { value: { type: "string", maxLength: 120 } }
|
|
17
|
+
},
|
|
18
|
+
# Recheck host authorization and trusted allowed values on every action.
|
|
19
|
+
policy: ->(_access) { false },
|
|
20
|
+
handler: ->(access) {
|
|
21
|
+
RailsToolViews::Transition.new(state: { value: access.arguments.fetch("value") })
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})
|
|
@@ -12,6 +12,7 @@ module RailsToolViews
|
|
|
12
12
|
end
|
|
13
13
|
def install
|
|
14
14
|
migration_template "create_rails_tool_views.rb", "db/migrate/create_rails_tool_views.rb"
|
|
15
|
+
migration_template "add_rails_tool_views_lifecycle.rb", "db/migrate/add_rails_tool_views_lifecycle.rb"
|
|
15
16
|
template "rails_tool_views.rb", "config/initializers/rails_tool_views.rb"
|
|
16
17
|
route 'mount RailsToolViews::Engine => "/rails_tool_views", as: :rails_tool_views'
|
|
17
18
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
class AddRailsToolViewsLifecycle < ActiveRecord::Migration[7.2]
|
|
3
|
+
def up
|
|
4
|
+
add_column :rails_tool_views_surfaces, :closed_at, :datetime
|
|
5
|
+
add_column :rails_tool_views_surfaces, :abandoned_at, :datetime
|
|
6
|
+
add_column :rails_tool_views_surfaces, :retain_until, :datetime
|
|
7
|
+
add_column :rails_tool_views_surfaces, :purged_at, :datetime
|
|
8
|
+
add_index :rails_tool_views_surfaces, [:tenant_ref, :actor_ref, :closed_at], name: "index_rails_tool_views_retention_scope"
|
|
9
|
+
add_column :rails_tool_views_agent_interactions, :delivery_uncertain, :boolean, default: false, null: false
|
|
10
|
+
# Old rows do not distinguish explicit failed retries from lease takeovers.
|
|
11
|
+
# Preserve uncertainty instead of assuming an old callback has stopped.
|
|
12
|
+
execute "UPDATE rails_tool_views_agent_interactions SET delivery_uncertain = #{connection.quote(true)} WHERE attempts > 1 OR status = 'delivering'"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def down
|
|
16
|
+
if select_value("SELECT COUNT(*) FROM rails_tool_views_surfaces WHERE closed_at IS NOT NULL").to_i.positive?
|
|
17
|
+
raise ActiveRecord::IrreversibleMigration, "Closed or purged Surfaces cannot be safely reopened by an older runtime. Restore a pre-upgrade backup instead."
|
|
18
|
+
end
|
|
19
|
+
remove_column :rails_tool_views_agent_interactions, :delivery_uncertain
|
|
20
|
+
remove_index :rails_tool_views_surfaces, name: "index_rails_tool_views_retention_scope"
|
|
21
|
+
%i[closed_at abandoned_at retain_until purged_at].each { |column| remove_column :rails_tool_views_surfaces, column }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -9,4 +9,5 @@ RailsToolViews.configure do |config|
|
|
|
9
9
|
# config.parent_controller = "::ApplicationController"
|
|
10
10
|
end
|
|
11
11
|
# Register trusted components and explicit policies in Rails.application.config.to_prepare.
|
|
12
|
-
#
|
|
12
|
+
# Generate a safe scaffold: bin/rails generate rails_tool_views:component review_card
|
|
13
|
+
# See https://rails-tool-views.wangyaodi.com/component-generator/ for registration and policies.
|
|
@@ -13,6 +13,11 @@ module RailsToolViews
|
|
|
13
13
|
MESSAGES = {
|
|
14
14
|
"NOT_FOUND" => "This view is unavailable.", "NOT_ALLOWED" => "You do not have permission to use this view.",
|
|
15
15
|
"CONFLICT" => "This view changed. Refresh to continue.", "INVALID_ARGUMENTS" => "Check the submitted values and try again.",
|
|
16
|
+
"SURFACE_CLOSED" => "This view is closed. Its saved actions cannot be changed.",
|
|
17
|
+
"SURFACE_PURGED" => "This view has been cleaned up and cannot be restored.",
|
|
18
|
+
"LIFECYCLE_BUSY" => "This view has unfinished or uncertain delivery. Resolve it before closing.",
|
|
19
|
+
"INVALID_RETENTION_REQUEST" => "Choose an explicit past cutoff and a batch size from 1 to 1000.",
|
|
20
|
+
"INVALID_RETENTION_WINDOW" => "Choose a future retention deadline or keep this view indefinitely.",
|
|
16
21
|
"VALIDATION_FAILED" => "Check the highlighted fields and try again.",
|
|
17
22
|
"INCOMPATIBLE_COMPONENT" => "This view's contract changed. Ask the application owner to migrate or recreate it.",
|
|
18
23
|
"RENDER_FAILED" => "This view cannot be rendered. Your saved state is preserved.",
|
|
@@ -249,7 +254,7 @@ module RailsToolViews
|
|
|
249
254
|
# Process-local host hooks, request limits, and delivery settings.
|
|
250
255
|
class Configuration
|
|
251
256
|
attr_accessor :scope_resolver, :agent_adapter, :max_payload_bytes, :max_depth, :max_catalog_components, :max_actions_per_minute, :auto_deliver,
|
|
252
|
-
:parent_controller, :delivery_lease
|
|
257
|
+
:parent_controller, :delivery_lease, :retention_policy
|
|
253
258
|
# Initializes conservative request limits with no host identity or agent adapter.
|
|
254
259
|
def initialize
|
|
255
260
|
@max_payload_bytes, @max_depth, @max_catalog_components, @max_actions_per_minute = 32_768, 16, 20, 60
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module RailsToolViews
|
|
3
|
+
class << self
|
|
4
|
+
# Closes a view after host authorization. Outstanding continuation work blocks
|
|
5
|
+
# closure. nil retain_until keeps data indefinitely; the first closure wins.
|
|
6
|
+
def close(id, scope:, retain_until: nil)
|
|
7
|
+
finish_surface(id, scope: scope, retain_until: retain_until, abandon: false)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Explicitly abandons pending/failed continuation work before closing a view.
|
|
11
|
+
# In-flight or historically uncertain delivery is never abandoned implicitly.
|
|
12
|
+
def abandon(id, scope:, retain_until: nil)
|
|
13
|
+
finish_surface(id, scope: scope, retain_until: retain_until, abandon: true)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Scrubs eligible closed payloads in one bounded batch, preserving replay
|
|
17
|
+
# tombstones. Requires an explicit scope, cutoff and host retention policy.
|
|
18
|
+
# Defaults to a read-only preview. Each applied Surface commits atomically.
|
|
19
|
+
def purge_closed(scope:, closed_before:, batch_size: 100, dry_run: true)
|
|
20
|
+
check_scope!(scope)
|
|
21
|
+
valid_time = closed_before.is_a?(Time) || closed_before.is_a?(ActiveSupport::TimeWithZone)
|
|
22
|
+
unless valid_time && closed_before <= Time.current && batch_size.is_a?(Integer) && batch_size.between?(1, 1000) && [true, false].any? { |v| dry_run.equal?(v) }
|
|
23
|
+
raise Error.new("INVALID_RETENTION_REQUEST")
|
|
24
|
+
end
|
|
25
|
+
policy = configuration.retention_policy
|
|
26
|
+
raise Error.new("NOT_ALLOWED", status: 403) unless policy.respond_to?(:call)
|
|
27
|
+
now = Time.current
|
|
28
|
+
candidates = Surface.where(actor_ref: scope.actor, tenant_ref: scope.tenant, purged_at: nil)
|
|
29
|
+
.where("closed_at < ? AND retain_until <= ?", closed_before, now).order(:closed_at, :id).limit(batch_size).pluck(:id)
|
|
30
|
+
counts = { examined: 0, eligible: 0, purged: 0, protected: 0, dry_run: dry_run }
|
|
31
|
+
candidates.each do |id|
|
|
32
|
+
surface = Surface.find_by(id: id)
|
|
33
|
+
next unless surface
|
|
34
|
+
surface.with_lock do
|
|
35
|
+
counts[:examined] += 1
|
|
36
|
+
eligible = exact_match?(surface, actor_ref: scope.actor, tenant_ref: scope.tenant) &&
|
|
37
|
+
!surface.purged? && surface.closed? && surface.closed_at < closed_before &&
|
|
38
|
+
surface.retain_until && surface.retain_until <= now
|
|
39
|
+
if eligible
|
|
40
|
+
request = access(scope, :purge, surface: surface, props: surface.props, context_ref: surface.context_ref)
|
|
41
|
+
eligible = policy.call(request) == true && !protected_interactions?(surface, abandon: false)
|
|
42
|
+
end
|
|
43
|
+
unless eligible
|
|
44
|
+
counts[:protected] += 1
|
|
45
|
+
next
|
|
46
|
+
end
|
|
47
|
+
counts[:eligible] += 1
|
|
48
|
+
next if dry_run
|
|
49
|
+
# The Surface lock excludes action commits, delivery claims and other
|
|
50
|
+
# cleanup workers. Only known terminal children are removed together.
|
|
51
|
+
surface.agent_interactions.delete_all
|
|
52
|
+
surface.action_executions.delete_all
|
|
53
|
+
surface.update!(props: {}, state: {}, context_ref: nil, purged_at: now)
|
|
54
|
+
counts[:purged] += 1
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
counts
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
def finish_surface(id, scope:, retain_until:, abandon:)
|
|
62
|
+
validate_retention_deadline!(retain_until, future: false)
|
|
63
|
+
surface = restore(id, scope: scope)
|
|
64
|
+
surface.with_lock do
|
|
65
|
+
ensure_not_purged!(surface)
|
|
66
|
+
component = compatible_component!(surface)
|
|
67
|
+
component.authorize!(access(scope, :read, surface: surface, props: surface.props, context_ref: surface.context_ref))
|
|
68
|
+
component.authorize!(access(scope, abandon ? :abandon : :close, surface: surface, props: surface.props, context_ref: surface.context_ref))
|
|
69
|
+
next if surface.closed?
|
|
70
|
+
validate_retention_deadline!(retain_until, future: true)
|
|
71
|
+
interactions = surface.agent_interactions
|
|
72
|
+
raise Error.new("LIFECYCLE_BUSY", status: 409) if protected_interactions?(surface, abandon: abandon)
|
|
73
|
+
if abandon
|
|
74
|
+
interactions.where(status: %w[pending failed]).update_all(status: "abandoned", claimed_at: nil, error_code: nil, updated_at: Time.current)
|
|
75
|
+
end
|
|
76
|
+
now = Time.current
|
|
77
|
+
surface.update!(closed_at: now, abandoned_at: abandon ? now : nil, retain_until: retain_until, revision: surface.revision + 1)
|
|
78
|
+
end
|
|
79
|
+
surface
|
|
80
|
+
end
|
|
81
|
+
def protected_interactions?(surface, abandon:)
|
|
82
|
+
interactions = surface.agent_interactions
|
|
83
|
+
allowed = abandon ? %w[completed skipped pending failed abandoned] : %w[completed skipped abandoned]
|
|
84
|
+
interactions.where(delivery_uncertain: true).exists? || interactions.where.not(status: allowed).exists?
|
|
85
|
+
end
|
|
86
|
+
def validate_retention_deadline!(deadline, future:)
|
|
87
|
+
return if deadline.nil?
|
|
88
|
+
valid_type = deadline.is_a?(Time) || deadline.is_a?(ActiveSupport::TimeWithZone)
|
|
89
|
+
raise Error.new("INVALID_RETENTION_WINDOW") unless valid_type && (!future || deadline > Time.current)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -44,6 +44,7 @@ module RailsToolViews
|
|
|
44
44
|
# match may stand in for this scope's Surface, or a lookup could disclose
|
|
45
45
|
# another actor's or tenant's record.
|
|
46
46
|
raise Error.new("NOT_FOUND", status: 404) unless surface && exact_match?(surface, id: id.to_s, actor_ref: scope.actor, tenant_ref: scope.tenant)
|
|
47
|
+
ensure_not_purged!(surface)
|
|
47
48
|
component = compatible_component!(surface)
|
|
48
49
|
component.authorize!(access(scope, :read, surface: surface, props: surface.props, context_ref: surface.context_ref))
|
|
49
50
|
surface
|
|
@@ -62,6 +63,7 @@ module RailsToolViews
|
|
|
62
63
|
request_digest = Data.digest(action: definition.key, arguments: arguments, base_revision: base_revision)
|
|
63
64
|
result = nil
|
|
64
65
|
surface.with_lock do
|
|
66
|
+
ensure_not_purged!(surface)
|
|
65
67
|
compatible_component!(surface).authorize!(access(scope, :read, surface: surface, props: surface.props, context_ref: surface.context_ref))
|
|
66
68
|
request = access(scope, :action, surface: surface, props: surface.props, action: definition.key,
|
|
67
69
|
arguments: arguments, context_ref: surface.context_ref)
|
|
@@ -74,6 +76,7 @@ module RailsToolViews
|
|
|
74
76
|
result = prior.result.symbolize_keys
|
|
75
77
|
next
|
|
76
78
|
end
|
|
79
|
+
raise Error.new("SURFACE_CLOSED", status: 410) if surface.closed?
|
|
77
80
|
raise Error.new("CONFLICT", status: 409) unless surface.revision == base_revision
|
|
78
81
|
if surface.action_executions.where("created_at >= ?", 1.minute.ago).count >= configuration.max_actions_per_minute
|
|
79
82
|
raise Error.new("RATE_LIMITED", status: 429)
|
|
@@ -114,25 +117,35 @@ module RailsToolViews
|
|
|
114
117
|
interaction = AgentInteraction.find_by(id: id)
|
|
115
118
|
raise Error.new("NOT_FOUND", status: 404) unless interaction
|
|
116
119
|
surface = restore(interaction.surface_id, scope: scope)
|
|
117
|
-
definition = compatible_component!(surface).actions.fetch(interaction.payload.fetch("action")) { raise Error.new("INCOMPATIBLE_COMPONENT", status: 422) }
|
|
118
|
-
arguments = definition.schema.validate!(interaction.payload.fetch("values"), code: "INVALID_ARGUMENTS")
|
|
119
|
-
request = access(scope, :action, surface: surface, props: surface.props, action: definition.key, arguments: arguments, context_ref: surface.context_ref)
|
|
120
|
-
raise Error.new("NOT_ALLOWED", status: 403) unless definition.policy&.call(request) == true
|
|
121
120
|
claimed = false
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
121
|
+
# Coordinate the claim with close/abandon/purge, using the same lock order
|
|
122
|
+
# as actions. Never hold a database transaction during the adapter call.
|
|
123
|
+
surface.with_lock do
|
|
124
|
+
ensure_not_purged!(surface)
|
|
125
|
+
component = compatible_component!(surface)
|
|
126
|
+
component.authorize!(access(scope, :read, surface: surface, props: surface.props, context_ref: surface.context_ref))
|
|
127
|
+
interaction.with_lock do
|
|
128
|
+
definition = component.actions.fetch(interaction.payload.fetch("action")) { raise Error.new("INCOMPATIBLE_COMPONENT", status: 422) }
|
|
129
|
+
arguments = definition.schema.validate!(interaction.payload.fetch("values"), code: "INVALID_ARGUMENTS")
|
|
130
|
+
request = access(scope, :action, surface: surface, props: surface.props, action: definition.key, arguments: arguments, context_ref: surface.context_ref)
|
|
131
|
+
raise Error.new("NOT_ALLOWED", status: 403) unless definition.policy&.call(request) == true
|
|
132
|
+
next if %w[completed skipped abandoned].include?(interaction.status)
|
|
133
|
+
raise Error.new("SURFACE_CLOSED", status: 410) if surface.closed?
|
|
134
|
+
if interaction.status == "delivering" && interaction.claimed_at && interaction.claimed_at > Time.current - configuration.delivery_lease
|
|
135
|
+
next
|
|
136
|
+
end
|
|
137
|
+
next if interaction.status == "failed" && !retry_failed
|
|
138
|
+
# Reclaiming a lease does not prove that the previous callback ended.
|
|
139
|
+
# This durable flag cannot be cleared by a stale worker's completion.
|
|
140
|
+
uncertain = interaction.delivery_uncertain? || interaction.status == "delivering"
|
|
141
|
+
if adapter.nil?
|
|
142
|
+
interaction.update!(status: "skipped", claimed_at: nil, delivery_uncertain: uncertain)
|
|
143
|
+
next
|
|
144
|
+
end
|
|
145
|
+
interaction.update!(status: "delivering", claimed_at: Time.current, attempts: interaction.attempts + 1,
|
|
146
|
+
error_code: nil, delivery_uncertain: uncertain)
|
|
147
|
+
claimed = true
|
|
133
148
|
end
|
|
134
|
-
interaction.update!(status: "delivering", claimed_at: Time.current, attempts: interaction.attempts + 1, error_code: nil)
|
|
135
|
-
claimed = true
|
|
136
149
|
end
|
|
137
150
|
return interaction.reload unless claimed
|
|
138
151
|
begin
|
|
@@ -190,6 +203,9 @@ module RailsToolViews
|
|
|
190
203
|
end
|
|
191
204
|
|
|
192
205
|
private
|
|
206
|
+
def ensure_not_purged!(surface)
|
|
207
|
+
raise Error.new("SURFACE_PURGED", status: 410) if surface.purged?
|
|
208
|
+
end
|
|
193
209
|
def check_scope!(scope)
|
|
194
210
|
raise Error.new("INVALID_SCOPE", status: 403) unless scope.is_a?(Scope)
|
|
195
211
|
end
|
data/lib/rails_tool_views.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_tool_views
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Wang
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -178,17 +178,27 @@ files:
|
|
|
178
178
|
- app/models/rails_tool_views/presentation.rb
|
|
179
179
|
- app/models/rails_tool_views/redacted_json.rb
|
|
180
180
|
- app/models/rails_tool_views/surface.rb
|
|
181
|
+
- app/views/layouts/rails_tool_views/application.html.erb
|
|
182
|
+
- app/views/rails_tool_views/surfaces/_closed.html.erb
|
|
181
183
|
- app/views/rails_tool_views/surfaces/_error.html.erb
|
|
182
184
|
- app/views/rails_tool_views/surfaces/_surface.html.erb
|
|
185
|
+
- app/views/rails_tool_views/surfaces/page.html.erb
|
|
183
186
|
- config/routes.rb
|
|
184
187
|
- db/migrate/20261010000000_create_rails_tool_views.rb
|
|
188
|
+
- db/migrate/20261019000000_add_rails_tool_views_lifecycle.rb
|
|
189
|
+
- lib/generators/rails_tool_views/component/component_generator.rb
|
|
190
|
+
- lib/generators/rails_tool_views/component/templates/component_test.rb
|
|
191
|
+
- lib/generators/rails_tool_views/component/templates/partial.html.erb
|
|
192
|
+
- lib/generators/rails_tool_views/component/templates/registration.rb
|
|
185
193
|
- lib/generators/rails_tool_views/install/install_generator.rb
|
|
194
|
+
- lib/generators/rails_tool_views/install/templates/add_rails_tool_views_lifecycle.rb
|
|
186
195
|
- lib/generators/rails_tool_views/install/templates/create_rails_tool_views.rb
|
|
187
196
|
- lib/generators/rails_tool_views/install/templates/rails_tool_views.rb
|
|
188
197
|
- lib/rails_tool_views.rb
|
|
189
198
|
- lib/rails_tool_views/adapters/ruby_llm.rb
|
|
190
199
|
- lib/rails_tool_views/core.rb
|
|
191
200
|
- lib/rails_tool_views/engine.rb
|
|
201
|
+
- lib/rails_tool_views/lifecycle.rb
|
|
192
202
|
- lib/rails_tool_views/runtime.rb
|
|
193
203
|
- lib/rails_tool_views/version.rb
|
|
194
204
|
homepage: https://github.com/yorzi/rails_tool_views
|