rails_tool_views 0.1.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 +7 -0
- data/CHANGELOG.md +54 -0
- data/LICENSE.txt +21 -0
- data/README.md +90 -0
- data/SECURITY.md +11 -0
- data/app/assets/javascripts/rails_tool_views_controller.js +41 -0
- data/app/controllers/rails_tool_views/surfaces_controller.rb +118 -0
- data/app/helpers/rails_tool_views/surfaces_helper.rb +78 -0
- data/app/models/rails_tool_views/action_execution.rb +11 -0
- data/app/models/rails_tool_views/agent_interaction.rb +15 -0
- data/app/models/rails_tool_views/presentation.rb +10 -0
- data/app/models/rails_tool_views/redacted_json.rb +20 -0
- data/app/models/rails_tool_views/surface.rb +23 -0
- data/app/views/rails_tool_views/surfaces/_error.html.erb +1 -0
- data/app/views/rails_tool_views/surfaces/_surface.html.erb +19 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20261010000000_create_rails_tool_views.rb +56 -0
- data/lib/generators/rails_tool_views/install/install_generator.rb +20 -0
- data/lib/generators/rails_tool_views/install/templates/create_rails_tool_views.rb +56 -0
- data/lib/generators/rails_tool_views/install/templates/rails_tool_views.rb +12 -0
- data/lib/rails_tool_views/adapters/ruby_llm.rb +75 -0
- data/lib/rails_tool_views/core.rb +288 -0
- data/lib/rails_tool_views/engine.rb +11 -0
- data/lib/rails_tool_views/runtime.rb +221 -0
- data/lib/rails_tool_views/version.rb +4 -0
- data/lib/rails_tool_views.rb +6 -0
- metadata +223 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ba277c8c403681504a1c5c2d154df3c7814952735ca5a10ba6981f7c50109a2d
|
|
4
|
+
data.tar.gz: a6ebf8a7b47065ffaf8585d5f57b2aff864d181fe0e7deb0085c26500b640ef4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 276b0f741dc507376c153e900a0b20468e64cf55f66073be587eb4a0d4705bc0d8a4b2d75b5531382e80011a0bf193510acb143fbe2e6864028881422509e461
|
|
7
|
+
data.tar.gz: e216675b6918e72cdbaf75a4404e7729bac2186260cbc06a500a8130c7cacaa1312d2e436bcbe085b159534a60adc6c1898673319fd718b456fab32ddd9a8f38
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-10-18
|
|
4
|
+
|
|
5
|
+
First public release. Earlier version labels below describe unpublished development milestones.
|
|
6
|
+
|
|
7
|
+
- Register trusted Rails Partials and ViewComponents with closed schemas and explicit policies.
|
|
8
|
+
- Persist and restore scoped Surfaces; execute authorized actions with revisions, transactions and durable replay receipts.
|
|
9
|
+
- Render JSON/Turbo responses, accessible action forms and bounded validation feedback.
|
|
10
|
+
- Add optional RubyLLM presentation and durable host-managed continuation without a core AI dependency.
|
|
11
|
+
- Install the engine into Rails 7.2 and 8.1 on Ruby 3.3 or later; one `rails_tool_views` identity throughout.
|
|
12
|
+
- Publish versioned guides and an API reference on GitHub Pages; release gems through GitHub OIDC.
|
|
13
|
+
- Require patched CGI, ERB, Resolv, URI and Zlib gems so Ruby 3.3 default gems cannot silently satisfy the runtime dependencies. Test the Rails 7.2 line on patched 7.2.3.2.
|
|
14
|
+
- MySQL, live-provider behavior, independent security review and external adoption remain unverified.
|
|
15
|
+
|
|
16
|
+
## 0.3.0.pre.v3 — single identity, 2026-10-18 (unpublished)
|
|
17
|
+
|
|
18
|
+
Breaking for 0.2 development hosts; see the Upgrading guide and ADR 006.
|
|
19
|
+
|
|
20
|
+
- Removed: the `ToolViews` constant, the `toolviews`, `tool_views`, `tool_views/core` and `tool_views/adapters/ruby_llm` entrypoints, the `tool_views:install` generator, the `tool_views_*` view helpers and the `:tool_views` route-proxy fallback.
|
|
21
|
+
- Changed: tables and custom indexes use the `rails_tool_views_` prefix; the interaction/execution index has an explicit 44-byte name so it fits PostgreSQL. The install migration is `CreateRailsToolViews` and the initializer `config/initializers/rails_tool_views.rb`.
|
|
22
|
+
- Changed: I18n keys move to `rails_tool_views.*`, notifications to `*.rails_tool_views`, the `Rails.error` source to `rails_tool_views`, Surface DOM ids to `rails_tool_views_surface_<id>`, CSS classes to `rails-tool-views-*`, the Stimulus asset to `rails_tool_views_controller.js` (`railsToolViewsController`, identifier `rails-tool-views`) and delivery keys to `rails_tool_views:interaction:<id>`.
|
|
23
|
+
- Unchanged: component contracts and digests, Surface IDs, revisions, serialized data, error codes and replay behavior.
|
|
24
|
+
|
|
25
|
+
## Unreleased — audit hardening, 2026-10-17
|
|
26
|
+
|
|
27
|
+
Fixed: the bundled migration created the five-column presentation unique index with unbounded `varchar` columns, so `CREATE TABLE` failed on MySQL/InnoDB under utf8mb4 (5 x 255 x 4 = 5100 bytes against a 3072-byte limit). Identifiers are now bounded to 150 bytes, so the widest index needs 3000 bytes. Changed: stored actor, tenant, context, adapter, call and idempotency references are byte-capped in `RailsToolViews::Data.identifier`; a longer value is rejected (`INVALID_SCOPE`, `INVALID_CONTEXT`, `INVALID_CORRELATION`, `INVALID_IDEMPOTENCY_KEY`) instead of overflowing a column. Descriptions and versions keep the wider 255-byte cap. Fixed: a non-binary database collation could resolve another actor's or tenant's Surface, another correlation's presentation, or another idempotency key's action receipt; every lookup now confirms a byte-exact match before it stands in for the request. Added regression tests for index-key width, the identifier cap, and collation-safe identity matching. No data migration: existing rows are unchanged and the runtime bound is enforced in code.
|
|
28
|
+
|
|
29
|
+
## Unreleased — audit hardening and component authoring, 2026-10-13
|
|
30
|
+
|
|
31
|
+
Fixed: nested schemas that were untyped, empty or arrays without `items` registered successfully and accepted arbitrary JSON inside a closed object; they are now rejected with `INVALID_SCHEMA`. Fixed: a native form for an action without arguments was rejected with `INVALID_ARGUMENTS` because it posts no `arguments` field. Changed: log redaction is scoped to the Engine's controller and models; installing the gem no longer adds `props`, `state`, `payload`, `result`, `arguments` and `values` to the host's `filter_parameters`, which had redacted matching host parameters and columns. Changed: renderer and continuation-adapter exceptions are reported to `Rails.error`, logged, and emitted as `surface.render_failed` / `agent_failed` events instead of being swallowed; `surface.rendered` fires only for a successful render. Added: `message:` on `ToolViews::Error` and `ValidationError`, I18n lookup under `tool_views.errors.*`, and overridable Engine copy under `tool_views.surface.*`. Changed: the status line no longer interprets a `selected` state key; handlers set `state["message"]`. Removed from core: the reference host's `ALREADY_SUBMITTED`, `ALREADY_ACCEPTED` and `REVIEW_REQUIRED` wording, now in its own locale file. No database change.
|
|
32
|
+
|
|
33
|
+
Added: `tool_views_action_form`, `tool_views_action_button` and `tool_views_action_fields` view helpers. Fixed: Rails' own `form.submit` and `form.button` were rejected as `UNKNOWN_FIELD`; `commit` and `button` are now accepted and ignored. Added: a ViewComponent whose initializer names `surface:`, `state:` or `error:` receives them and can be interactive. Added: `config.parent_controller` and `config.delivery_lease`. Changed: without Turbo, a committed HTML action redirects back to the same-host embedding page instead of the Engine's bare Surface fragment. Changed: a database deadlock, serialization failure or lock timeout returns `CONFLICT` instead of an unhandled error. Verified locally on PostgreSQL 17 (`gemfiles/postgres.gemfile`, `DATABASE_URL`); CI job added, not yet run.
|
|
34
|
+
|
|
35
|
+
## 0.2.0.pre.v1 — 2026-10-11 (unpublished)
|
|
36
|
+
|
|
37
|
+
Host-owned customer form, scoped support workflows and versioned result review reference demos (UC-02..05). Add bounded validation feedback with retained form values, authorized HTML error rendering, host ApplicationController inheritance and general action announcements. Demo services independently enforce role/tenant/domain versions, human review before a local outbox, immutable accepted artifacts, authorized export and durable continuation receipts. No new gem database tables or core RubyLLM dependency. Local matrix, packaged install, privacy, recovery and browser evidence: docs/evidence/V1_VALIDATION.md. Manual review and release gates remain open.
|
|
38
|
+
|
|
39
|
+
## 0.1.0.pre.v0 — 2026-10-10 (unpublished)
|
|
40
|
+
|
|
41
|
+
Strict registry and trusted Partial/ViewComponent rendering; scoped ActiveRecord Surfaces; atomic authorized actions with revision/replay receipts; JSON/Turbo controller and optional Stimulus focus/loading support; optional RubyLLM present_ui and structured continuation; installer and offline ChoiceCard demo. Local integration/security/concurrency/package/browser verification is recorded in docs/evidence/VALIDATION.md. Release and adoption gates remain open.
|
|
42
|
+
|
|
43
|
+
## Unreleased — first host integration fixes (2026-10-17)
|
|
44
|
+
|
|
45
|
+
- Fix: without Turbo and without a same-host referrer, a committed HTML action redirected through a hard-coded `tool_views` route proxy. Under the installer's `rails_tool_views` mount it raised `NameError` (500). It now uses the Engine's own route helper and keeps any mount path and name.
|
|
46
|
+
- Add: `rails_tool_views_component_name` / `tool_views_component_name`. The Surface's accessible name is looked up under `tool_views.components.<key>` on every render and falls back to the registered description.
|
|
47
|
+
- Docs: renderers and parent-controller filters run inside the Engine's controller and call host routes through `main_app`.
|
|
48
|
+
- No database change.
|
|
49
|
+
|
|
50
|
+
## Unreleased — RailsToolViews identity (2026-10-16)
|
|
51
|
+
|
|
52
|
+
- Rename the package to `rails_tool_views` and the canonical namespace to `RailsToolViews`.
|
|
53
|
+
- Preserve legacy Ruby requires/constants/helpers/mounts, database and wire identifiers.
|
|
54
|
+
- Add independent packaged-host and pre-rename persisted-data compatibility verification. No RubyGems release.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andy Wang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# RailsToolViews
|
|
2
|
+
|
|
3
|
+
**Turn trusted Rails views into persistent, interactive tools.**
|
|
4
|
+
|
|
5
|
+
Register an existing Partial or ViewComponent, present it with validated data, and let users act through authorized Rails handlers. Each view keeps its state across refreshes. Your application owns authentication, tenant boundaries and business records.
|
|
6
|
+
|
|
7
|
+
The core runs without an AI provider. An optional RubyLLM adapter lets agents present registered components and continue after a user action. RailsToolViews does not generate executable UI code.
|
|
8
|
+
|
|
9
|
+
[Documentation](https://rails-tool-views.wangyaodi.com) · [RubyGems](https://rubygems.org/gems/rails_tool_views) · [API reference](https://rails-tool-views.wangyaodi.com/api/)
|
|
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
|
+
```
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
bundle install
|
|
44
|
+
bin/rails generate rails_tool_views:install
|
|
45
|
+
bin/rails db:migrate
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Follow [Getting Started](docs/site/_getting_started/getting-started.md) to configure authenticated scope resolution and register a component. Once registered, present and render it:
|
|
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
|
+
```
|
|
58
|
+
|
|
59
|
+
Component and action policies deny by default. The host must authorize domain records and allowed values independently of proposed props. External side effects require host idempotency or an outbox. Read [Security](SECURITY.md) before integration.
|
|
60
|
+
|
|
61
|
+
## Documentation
|
|
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
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
bundle exec rake test zeitwerk:check
|
|
78
|
+
bundle exec ruby scripts/package_smoke.rb
|
|
79
|
+
# Optional adapter tests:
|
|
80
|
+
BUNDLE_GEMFILE=gemfiles/adapter.gemfile bundle install
|
|
81
|
+
BUNDLE_GEMFILE=gemfiles/adapter.gemfile bundle exec rake test:adapter
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Tests use offline responses and block TCP connections. Supported dependency bounds are in [the gemspec](rails_tool_views.gemspec); tested combinations and limitations are recorded in [validation evidence](docs/evidence). MySQL and live-provider behavior remain unverified. See [the release checklist](docs/operations/02_RELEASE_CHECKLIST.md) for remaining release gates.
|
|
85
|
+
|
|
86
|
+
## Project status and attribution
|
|
87
|
+
|
|
88
|
+
Released under the [MIT license](LICENSE.txt).
|
|
89
|
+
|
|
90
|
+
RailsToolViews is an unofficial community project. Rails marks belong to David Heinemeier Hansson, with exclusive licensing to the Rails Foundation. Framework names identify compatibility; neither the owner nor the Foundation endorses or is affiliated with this project. See the [Rails trademark policy](https://rubyonrails.org/trademarks).
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
The 0.1.x release line receives security fixes. This project has not received independent security review. Report suspected vulnerabilities through [GitHub private vulnerability reporting](https://github.com/yorzi/rails_tool_views/security/advisories/new). Do not post secrets, customer data, raw payloads or exploit instructions in a public issue. The maintainer reviews reports on a best-effort basis; no response-time guarantee is offered.
|
|
4
|
+
|
|
5
|
+
The host must resolve actor and tenant from authenticated server state, provide deny-by-default component/action policies, authorize domain records independently of props, keep CSRF enabled and project data before persistence. RailsToolViews rejects unregistered renderers/actions, extra parameters, client policy text, forged sources/targets, stale revisions and mismatched replays. The only extra parameters tolerated are `commit` and `button`, which Rails' submit helpers post; their values are never read. The non-Turbo redirect after a committed action follows `Referer` only to the same host. Native escaping depends on trusted host templates. Registration rejects any nested schema that is untyped, empty, open or an array without `items`, so a closed props or arguments object cannot be reopened from the inside. Engine-scoped request and SQL bind redaction and metadata-only notifications reduce logging exposure without altering host logs; host SQL/SDK logging and retention still need review.
|
|
6
|
+
|
|
7
|
+
The gem stores approved display props/state and structured interaction values. It does not store transcripts, API credentials or raw business-record dumps by itself. Hosts control export/deletion and must not place sensitive data into these fields. Database handlers share Surface's transaction; external calls require host idempotency/outbox. Agent deliveries use a lease and stable delivery key, with explicit retry; a host must deduplicate across uncertain remote outcomes.
|
|
8
|
+
|
|
9
|
+
Supported transports are JSON and Turbo. ActionCable is absent. No executable model-generated HTML, JS, Ruby, arbitrary remote schema resolution or URL fetch is supported. Security coverage maps SEC-01..18 in the acceptance matrix; channel-specific checks remain N/A while broadcasting is disabled.
|
|
10
|
+
|
|
11
|
+
Mounted controllers inherit the host's ApplicationController; review inherited authentication filters and helpers. ValidationError returns bounded approved field feedback, so handlers must avoid returning private values. Engine request logs mask every field except routing metadata, and Engine models redact their JSON columns in SQL binds and `#inspect`; custom host attributes/SDK logs require separate review. Renderer and adapter exceptions are host exceptions: they are passed to `Rails.error` and logged with class and message, so host code must not put secrets in exception messages. V1 query/resolver/dispatcher independently authorize current role, tenant and associations, and compare host versions across Surfaces. The synthetic identity selector and local reply outbox are reference fixtures, not production authentication or external delivery.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Host imports and registers this factory with its own Stimulus application.
|
|
2
|
+
// No provider, generated code, or browser-owned policy participates in actions.
|
|
3
|
+
export default function railsToolViewsController(Controller) {
|
|
4
|
+
return class extends Controller {
|
|
5
|
+
static targets = ["status", "error"];
|
|
6
|
+
// The server-rendered Surface supplies translated copy; these are fallbacks.
|
|
7
|
+
static values = {
|
|
8
|
+
revision: Number,
|
|
9
|
+
saving: { type: String, default: "Saving changes…" },
|
|
10
|
+
failed: { type: String, default: "Changes could not be saved. Review the message and try again." }
|
|
11
|
+
};
|
|
12
|
+
connect() {
|
|
13
|
+
this.start = () => {
|
|
14
|
+
this.element.setAttribute("aria-busy", "true");
|
|
15
|
+
if (this.hasStatusTarget) this.statusTarget.textContent = this.savingValue;
|
|
16
|
+
};
|
|
17
|
+
this.end = (event) => {
|
|
18
|
+
this.element.removeAttribute("aria-busy");
|
|
19
|
+
if (!event.detail.success && this.hasStatusTarget) this.statusTarget.textContent = this.failedValue;
|
|
20
|
+
};
|
|
21
|
+
this.beforeStream = (event) => {
|
|
22
|
+
const stream = event.target;
|
|
23
|
+
if (stream.getAttribute("target") !== this.element.id) return;
|
|
24
|
+
const incoming = stream.querySelector("template")?.content.firstElementChild;
|
|
25
|
+
const revision = Number(incoming?.getAttribute("data-rails-tool-views-revision-value"));
|
|
26
|
+
if (revision && revision < this.revisionValue) event.preventDefault();
|
|
27
|
+
};
|
|
28
|
+
this.element.ownerDocument.addEventListener("turbo:before-stream-render", this.beforeStream);
|
|
29
|
+
this.element.addEventListener("turbo:submit-start", this.start);
|
|
30
|
+
this.element.addEventListener("turbo:submit-end", this.end);
|
|
31
|
+
// Replacement happens after a committed result; move focus to its announcement.
|
|
32
|
+
if (this.hasErrorTarget) this.errorTarget.focus({ preventScroll: true });
|
|
33
|
+
else if (this.revisionValue > 1 && this.hasStatusTarget) this.statusTarget.focus({ preventScroll: true });
|
|
34
|
+
}
|
|
35
|
+
disconnect() {
|
|
36
|
+
this.element.ownerDocument.removeEventListener("turbo:before-stream-render", this.beforeStream);
|
|
37
|
+
this.element.removeEventListener("turbo:submit-start", this.start);
|
|
38
|
+
this.element.removeEventListener("turbo:submit-end", this.end);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module RailsToolViews
|
|
3
|
+
# Preserve the host's authentication methods, filters and view helpers.
|
|
4
|
+
class SurfacesController < RailsToolViews.parent_controller
|
|
5
|
+
# Request logs for these routes keep routing metadata only. Everything else,
|
|
6
|
+
# including unknown injected fields, is masked here rather than in the host's
|
|
7
|
+
# global filter list, which would also redact the host's own parameters.
|
|
8
|
+
LOGGED_PARAMETERS = %w[controller action id interaction_id format action_name base_revision idempotency_key source_component target].freeze
|
|
9
|
+
UNLOGGED_PARAMETER = /\A(?!(?:#{LOGGED_PARAMETERS.join('|')})\z)/
|
|
10
|
+
# Rails' own form and submit helpers post these names; their values are never read.
|
|
11
|
+
FORM_FURNITURE = %w[authenticity_token commit button].freeze
|
|
12
|
+
|
|
13
|
+
protect_from_forgery with: :exception
|
|
14
|
+
wrap_parameters false
|
|
15
|
+
before_action :limit_payload
|
|
16
|
+
helper RailsToolViews::SurfacesHelper
|
|
17
|
+
rescue_from RailsToolViews::Error, with: :reject_request
|
|
18
|
+
rescue_from ActionController::InvalidAuthenticityToken do
|
|
19
|
+
reject_request(Error.new("INVALID_CSRF", status: 403))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def show
|
|
23
|
+
surface = RailsToolViews.restore(params[:id], scope: current_scope)
|
|
24
|
+
respond_to do |format|
|
|
25
|
+
format.json { render json: surface.snapshot }
|
|
26
|
+
format.html { render html: RailsToolViews.render(surface, view_context: view_context, scope: current_scope) }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def act
|
|
31
|
+
reject_unknown_fields(%w[action action_name arguments base_revision idempotency_key source_component target])
|
|
32
|
+
result = RailsToolViews.dispatch(params[:id], scope: current_scope, action: input["action"] || input["action_name"],
|
|
33
|
+
# A native form for an action without arguments cannot post an empty object.
|
|
34
|
+
arguments: input.fetch("arguments", {}),
|
|
35
|
+
base_revision: parse_revision(input["base_revision"]), idempotency_key: input["idempotency_key"],
|
|
36
|
+
source_component: input["source_component"], target: input["target"])
|
|
37
|
+
if result[:interaction_id] && RailsToolViews.configuration.agent_adapter && RailsToolViews.configuration.auto_deliver
|
|
38
|
+
RailsToolViews.deliver(result[:interaction_id], scope: current_scope)
|
|
39
|
+
end
|
|
40
|
+
respond_with_surface(result)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def retry_agent
|
|
44
|
+
reject_unknown_fields(%w[])
|
|
45
|
+
surface = RailsToolViews.restore(params[:id], scope: current_scope)
|
|
46
|
+
interaction = surface.agent_interactions.find_by(id: params[:interaction_id])
|
|
47
|
+
raise Error.new("NOT_FOUND", status: 404) unless interaction
|
|
48
|
+
RailsToolViews.deliver(interaction.id, scope: current_scope, retry_failed: true)
|
|
49
|
+
respond_with_surface(status: "completed", surface_id: surface.id, revision: surface.revision, interaction_id: interaction.id)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
# Runs before Rails reads the filtered parameters for its request log.
|
|
54
|
+
def process_action(*)
|
|
55
|
+
request.set_header("action_dispatch.parameter_filter", Array(request.get_header("action_dispatch.parameter_filter")) + [UNLOGGED_PARAMETER])
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
def current_scope
|
|
59
|
+
@current_scope ||= RailsToolViews.configuration.scope_resolver&.call(self)
|
|
60
|
+
raise Error.new("NOT_ALLOWED", status: 403) unless @current_scope.is_a?(Scope)
|
|
61
|
+
@current_scope
|
|
62
|
+
end
|
|
63
|
+
def limit_payload
|
|
64
|
+
raise Error.new("PAYLOAD_TOO_LARGE", status: 413) if request.content_length.to_i > RailsToolViews.configuration.max_payload_bytes || request.raw_post.to_s.bytesize > RailsToolViews.configuration.max_payload_bytes
|
|
65
|
+
end
|
|
66
|
+
def reject_unknown_fields(allowed)
|
|
67
|
+
extra = input.keys - allowed - FORM_FURNITURE
|
|
68
|
+
raise Error.new("UNKNOWN_FIELD") unless extra.empty?
|
|
69
|
+
raise Error.new("UNKNOWN_FIELD") if input.key?("action") && input.key?("action_name")
|
|
70
|
+
end
|
|
71
|
+
def input
|
|
72
|
+
@input ||= Data.bounded(request.request_parameters)
|
|
73
|
+
end
|
|
74
|
+
def parse_revision(value)
|
|
75
|
+
return value if value.is_a?(Integer)
|
|
76
|
+
raise Error.new("INVALID_REVISION") unless value.is_a?(String) && value.match?(/\A[1-9][0-9]{0,9}\z/)
|
|
77
|
+
value.to_i
|
|
78
|
+
end
|
|
79
|
+
def respond_with_surface(result)
|
|
80
|
+
surface = RailsToolViews.restore(result[:surface_id], scope: current_scope)
|
|
81
|
+
interaction = result[:interaction_id] && surface.agent_interactions.find_by(id: result[:interaction_id])
|
|
82
|
+
response = result.merge(current_revision: surface.revision)
|
|
83
|
+
response[:agent_status] = interaction.status if interaction
|
|
84
|
+
response[:status] = "pending" if interaction && %w[pending delivering].include?(interaction.status)
|
|
85
|
+
status = response[:status] == "pending" ? 202 : 200
|
|
86
|
+
respond_to do |format|
|
|
87
|
+
format.json { render json: response, status: status }
|
|
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
|
+
# Without Turbo, return to the page that embeds the Surface; it re-renders saved state.
|
|
90
|
+
# The Engine's own helper keeps whatever mount path and name the host chose.
|
|
91
|
+
format.html { redirect_back_or_to surface_path(surface), allow_other_host: false, status: :see_other }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
def reject_request(error)
|
|
95
|
+
respond_to do |format|
|
|
96
|
+
format.json { render json: error.to_h, status: error.status }
|
|
97
|
+
format.turbo_stream do
|
|
98
|
+
# Failed authorization never renders props, and supplied targets never reach Turbo.
|
|
99
|
+
begin
|
|
100
|
+
surface = RailsToolViews.restore(params[:id], scope: current_scope)
|
|
101
|
+
html = RailsToolViews.render(surface, view_context: view_context, scope: current_scope, error: error)
|
|
102
|
+
render turbo_stream: turbo_stream.replace(surface.dom_id, html), status: error.status
|
|
103
|
+
rescue RailsToolViews::Error
|
|
104
|
+
render plain: error.message, status: error.status
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
format.html do
|
|
108
|
+
begin
|
|
109
|
+
surface = RailsToolViews.restore(params[:id], scope: current_scope)
|
|
110
|
+
render html: RailsToolViews.render(surface, view_context: view_context, scope: current_scope, error: error), status: error.status
|
|
111
|
+
rescue RailsToolViews::Error
|
|
112
|
+
render partial: "rails_tool_views/surfaces/error", locals: { error: error }, status: error.status
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module RailsToolViews
|
|
3
|
+
# Rails view helpers for authorized rendering and action forms.
|
|
4
|
+
module SurfacesHelper
|
|
5
|
+
# Engine-owned copy. Translate or reword any of it under rails_tool_views.surface.<key>.
|
|
6
|
+
TEXT = {
|
|
7
|
+
ready: "View ready.", saved: "Changes saved.", revision: "Saved revision %{revision}",
|
|
8
|
+
saving: "Saving changes…", save_failed: "Changes could not be saved. Review the message and try again.",
|
|
9
|
+
agent_pending: "AI follow-up pending.", agent_completed: "AI follow-up completed.",
|
|
10
|
+
agent_failed: "Your changes were saved, but the AI follow-up could not complete.",
|
|
11
|
+
agent_unavailable: "AI follow-up unavailable.", agent_retry: "Retry AI follow-up"
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
# Renders the Surface with a fresh scope and optional field feedback.
|
|
15
|
+
def rails_tool_views_surface(surface, scope:, error: nil)
|
|
16
|
+
RailsToolViews.render(surface, view_context: self, scope: scope, error: error)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def rails_tool_views_text(key, **values)
|
|
20
|
+
t(key, scope: %i[rails_tool_views surface], default: TEXT.fetch(key), **values)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The accessible name of a registered component: the translation under
|
|
24
|
+
# rails_tool_views.components.<key>, or its registered description. Looked up
|
|
25
|
+
# per render, so it follows the request's locale.
|
|
26
|
+
def rails_tool_views_component_name(key)
|
|
27
|
+
t(key.to_s, scope: %i[rails_tool_views components], default: RailsToolViews.registry.fetch(key).description)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Form for one registered action of this Surface. Fields built from the
|
|
31
|
+
# yielded builder are named +arguments[...]+, ids are unique per Surface, and
|
|
32
|
+
# the action, revision, replay key and source fields are filled in.
|
|
33
|
+
def rails_tool_views_action_form(surface, action, **options, &block)
|
|
34
|
+
fields = rails_tool_views_action_fields(surface, action)
|
|
35
|
+
form_with(url: rails_tool_views_routes.surface_actions_path(surface), method: :post, scope: :arguments, namespace: surface.dom_id, **options) do |form|
|
|
36
|
+
safe_join([fields, (capture(form, &block) if block)])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# One-button form for an action whose arguments, if any, are fixed.
|
|
41
|
+
def rails_tool_views_action_button(surface, action, label, arguments: {}, form: {}, **button)
|
|
42
|
+
rails_tool_views_action_form(surface, action, **form) do |builder|
|
|
43
|
+
safe_join(arguments.map { |name, value| builder.hidden_field(name, value: value, id: nil) } << builder.button(label, name: nil, **button))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The routing fields alone, for a form the host builds itself.
|
|
48
|
+
def rails_tool_views_action_fields(surface, action)
|
|
49
|
+
key = action.to_s
|
|
50
|
+
unless RailsToolViews.registry.fetch(surface.component_key).actions.key?(key)
|
|
51
|
+
raise ArgumentError, "#{surface.component_key} does not register the action #{key.inspect}"
|
|
52
|
+
end
|
|
53
|
+
safe_join({ action_name: key, base_revision: surface.revision, idempotency_key: SecureRandom.uuid, source_component: surface.component_key }
|
|
54
|
+
.map { |name, value| hidden_field_tag(name, value, id: nil) })
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def rails_tool_views_body(surface, definition, error: nil) # :nodoc:
|
|
58
|
+
html = if definition.partial
|
|
59
|
+
render(partial: definition.partial, locals: { props: surface.props, state: surface.state, surface: surface, error: error })
|
|
60
|
+
else
|
|
61
|
+
context = { surface: surface, state: surface.state, error: error }.slice(*definition.context)
|
|
62
|
+
render(definition.component.new(**surface.props.symbolize_keys, **context))
|
|
63
|
+
end
|
|
64
|
+
RailsToolViews.notify("surface.rendered", surface_id: surface.id, component: definition.key, revision: surface.revision)
|
|
65
|
+
html
|
|
66
|
+
rescue StandardError => failure
|
|
67
|
+
# A host renderer may reject deliberately; anything else is a host bug to report.
|
|
68
|
+
rejection = [failure, failure.cause].find { |candidate| candidate.is_a?(RailsToolViews::Error) }
|
|
69
|
+
RailsToolViews.report(failure, "surface.render_failed", surface_id: surface.id, component: definition.key) unless rejection
|
|
70
|
+
render(partial: "rails_tool_views/surfaces/error", formats: [:html], locals: { error: rejection || RailsToolViews::Error.new("RENDER_FAILED", status: 422) })
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def rails_tool_views_routes # :nodoc:
|
|
74
|
+
view = respond_to?(:rails_tool_views) || respond_to?(:main_app) ? self : helpers
|
|
75
|
+
view.respond_to?(:rails_tool_views) ? view.rails_tool_views : view.main_app.rails_tool_views
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module RailsToolViews
|
|
3
|
+
# A committed action receipt used to replay requests without repeating handlers.
|
|
4
|
+
class ActionExecution < ActiveRecord::Base
|
|
5
|
+
self.table_name = "rails_tool_views_action_executions"
|
|
6
|
+
belongs_to :surface
|
|
7
|
+
has_one :agent_interaction, dependent: :destroy
|
|
8
|
+
self.filter_attributes += %i[result]
|
|
9
|
+
attribute :result, RedactedJson.new
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module RailsToolViews
|
|
3
|
+
# A durable continuation request delivered after its action commits.
|
|
4
|
+
class AgentInteraction < ActiveRecord::Base
|
|
5
|
+
self.table_name = "rails_tool_views_agent_interactions"
|
|
6
|
+
belongs_to :surface
|
|
7
|
+
belongs_to :action_execution
|
|
8
|
+
self.filter_attributes += %i[payload]
|
|
9
|
+
attribute :payload, RedactedJson.new
|
|
10
|
+
# Returns the stable host deduplication key across delivery retries.
|
|
11
|
+
def delivery_key
|
|
12
|
+
"rails_tool_views:interaction:#{id}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module RailsToolViews
|
|
3
|
+
# A durable acknowledgment correlating one scoped tool call with a Surface.
|
|
4
|
+
class Presentation < ActiveRecord::Base
|
|
5
|
+
self.table_name = "rails_tool_views_presentations"
|
|
6
|
+
belongs_to :surface
|
|
7
|
+
self.filter_attributes += %i[result]
|
|
8
|
+
attribute :result, RedactedJson.new
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module RailsToolViews
|
|
3
|
+
# JSON column type whose database bind hides itself from SQL logs. Rails
|
|
4
|
+
# filters binds by attribute name alone, so listing `state` or `result` in the
|
|
5
|
+
# host-wide filter would also redact every host column that shares the name.
|
|
6
|
+
class RedactedJson < ActiveRecord::Type::Json
|
|
7
|
+
class Bind < String
|
|
8
|
+
def inspect
|
|
9
|
+
"[FILTERED]"
|
|
10
|
+
end
|
|
11
|
+
def as_json(*)
|
|
12
|
+
"[FILTERED]"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
def serialize(value)
|
|
16
|
+
json = super
|
|
17
|
+
json && Bind.new(json)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module RailsToolViews
|
|
3
|
+
# A persisted UI snapshot whose read and action access is checked by RailsToolViews.
|
|
4
|
+
class Surface < ActiveRecord::Base
|
|
5
|
+
self.table_name = "rails_tool_views_surfaces"
|
|
6
|
+
self.filter_attributes += %i[props state]
|
|
7
|
+
attribute :props, RedactedJson.new, default: -> { {} }
|
|
8
|
+
attribute :state, RedactedJson.new, default: -> { {} }
|
|
9
|
+
has_many :presentations, dependent: :destroy
|
|
10
|
+
has_many :action_executions, dependent: :destroy
|
|
11
|
+
has_many :agent_interactions, dependent: :destroy
|
|
12
|
+
before_create { self.id ||= SecureRandom.uuid }
|
|
13
|
+
validates :actor_ref, :tenant_ref, :component_key, :component_digest, presence: true
|
|
14
|
+
# Returns the stable browser target identifier.
|
|
15
|
+
def dom_id
|
|
16
|
+
"rails_tool_views_surface_#{id}"
|
|
17
|
+
end
|
|
18
|
+
# Returns the UI projection; it is not a permission grant.
|
|
19
|
+
def snapshot
|
|
20
|
+
{ id: id, component: component_key, component_digest: component_digest, props: props, state: state, revision: revision }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="rails-tool-views-error" role="alert" tabindex="-1" data-error-code="<%= error.code %>"><%= error.message %></div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<section id="<%= surface.dom_id %>" class="rails-tool-views-surface" data-controller="rails-tool-views" data-rails-tool-views-revision-value="<%= surface.revision %>" data-rails-tool-views-saving-value="<%= rails_tool_views_text(:saving) %>" data-rails-tool-views-failed-value="<%= rails_tool_views_text(:save_failed) %>" aria-label="<%= rails_tool_views_component_name(definition.key) %>" aria-describedby="<%= surface.dom_id %>_status">
|
|
2
|
+
<%= rails_tool_views_body(surface, definition, error: error) %>
|
|
3
|
+
<% if error %>
|
|
4
|
+
<p id="<%= surface.dom_id %>_error" role="alert" tabindex="-1" data-rails-tool-views-target="error"><%= error.message %></p>
|
|
5
|
+
<% end %>
|
|
6
|
+
<p id="<%= surface.dom_id %>_status" role="status" aria-live="polite" tabindex="-1" data-rails-tool-views-target="status">
|
|
7
|
+
<%= surface.state["message"].is_a?(String) ? surface.state["message"] : rails_tool_views_text(surface.revision > 1 ? :saved : :ready) %>
|
|
8
|
+
</p>
|
|
9
|
+
<% interaction = surface.agent_interactions.order(:id).last %>
|
|
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>
|
|
12
|
+
<% if interaction.status == "failed" %>
|
|
13
|
+
<%= form_with url: rails_tool_views_routes.surface_continuation_path(surface, interaction_id: interaction.id), method: :post do %>
|
|
14
|
+
<button type="submit"><%= rails_tool_views_text(:agent_retry) %></button>
|
|
15
|
+
<% end %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% end %>
|
|
18
|
+
<p class="rails-tool-views-revision"><%= rails_tool_views_text(:revision, revision: surface.revision) %></p>
|
|
19
|
+
</section>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
RailsToolViews::Engine.routes.draw do
|
|
2
|
+
get "surfaces/:id", to: "surfaces#show", as: :surface
|
|
3
|
+
post "surfaces/:id/actions", to: "surfaces#act", as: :surface_actions
|
|
4
|
+
post "surfaces/:id/continuations/:interaction_id", to: "surfaces#retry_agent", as: :surface_continuation
|
|
5
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
class CreateRailsToolViews < ActiveRecord::Migration[7.2]
|
|
3
|
+
# Identifier columns are bounded to 150 bytes in RailsToolViews::Data, and the
|
|
4
|
+
# value is stored in a byte-limited column. This keeps every index key within
|
|
5
|
+
# utf8mb4's 3072-byte limit on MySQL/InnoDB: the widest index is the five
|
|
6
|
+
# presentation columns below (5 * 150 * 4 = 3000 bytes).
|
|
7
|
+
def change
|
|
8
|
+
create_table :rails_tool_views_surfaces, id: :string do |t|
|
|
9
|
+
t.string :actor_ref, null: false, limit: 150
|
|
10
|
+
t.string :tenant_ref, null: false, limit: 150
|
|
11
|
+
t.string :context_ref, limit: 150
|
|
12
|
+
t.string :component_key, null: false
|
|
13
|
+
t.string :component_digest, null: false
|
|
14
|
+
t.integer :contract_version, default: 1, null: false
|
|
15
|
+
t.json :props, null: false
|
|
16
|
+
t.json :state, null: false
|
|
17
|
+
t.integer :revision, default: 1, null: false
|
|
18
|
+
t.integer :lock_version, default: 0, null: false
|
|
19
|
+
t.timestamps
|
|
20
|
+
end
|
|
21
|
+
add_index :rails_tool_views_surfaces, [:tenant_ref, :actor_ref]
|
|
22
|
+
create_table :rails_tool_views_presentations do |t|
|
|
23
|
+
t.references :surface, type: :string, foreign_key: { to_table: :rails_tool_views_surfaces }, null: false
|
|
24
|
+
t.string :actor_ref, null: false, limit: 150
|
|
25
|
+
t.string :tenant_ref, null: false, limit: 150
|
|
26
|
+
t.string :adapter, null: false, limit: 150
|
|
27
|
+
t.string :call_id, null: false, limit: 150
|
|
28
|
+
t.string :context_ref, null: false, limit: 150
|
|
29
|
+
t.string :request_digest, null: false
|
|
30
|
+
t.json :result, null: false
|
|
31
|
+
t.timestamps
|
|
32
|
+
end
|
|
33
|
+
add_index :rails_tool_views_presentations, [:tenant_ref, :actor_ref, :adapter, :call_id, :context_ref], unique: true, name: "index_rails_tool_views_call_scope"
|
|
34
|
+
create_table :rails_tool_views_action_executions do |t|
|
|
35
|
+
t.references :surface, type: :string, foreign_key: { to_table: :rails_tool_views_surfaces }, null: false
|
|
36
|
+
t.string :actor_ref, null: false, limit: 150
|
|
37
|
+
t.string :action_key, null: false
|
|
38
|
+
t.string :idempotency_key, null: false, limit: 150
|
|
39
|
+
t.string :request_digest, null: false
|
|
40
|
+
t.integer :base_revision, null: false
|
|
41
|
+
t.json :result, null: false
|
|
42
|
+
t.timestamps
|
|
43
|
+
end
|
|
44
|
+
add_index :rails_tool_views_action_executions, [:surface_id, :idempotency_key], unique: true, name: "index_rails_tool_views_action_replay"
|
|
45
|
+
create_table :rails_tool_views_agent_interactions do |t|
|
|
46
|
+
t.references :surface, type: :string, foreign_key: { to_table: :rails_tool_views_surfaces }, null: false
|
|
47
|
+
t.references :action_execution, foreign_key: { to_table: :rails_tool_views_action_executions }, null: false, index: { unique: true, name: "index_rails_tool_views_interaction_execution" }
|
|
48
|
+
t.json :payload, null: false
|
|
49
|
+
t.string :status, null: false, default: "pending"
|
|
50
|
+
t.string :error_code
|
|
51
|
+
t.datetime :claimed_at
|
|
52
|
+
t.integer :attempts, null: false, default: 0
|
|
53
|
+
t.timestamps
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "rails/generators"
|
|
3
|
+
require "rails/generators/migration"
|
|
4
|
+
module RailsToolViews
|
|
5
|
+
module Generators
|
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
|
7
|
+
include Rails::Generators::Migration
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
def self.next_migration_number(dirname)
|
|
10
|
+
next_migration_number = current_migration_number(dirname) + 1
|
|
11
|
+
[Time.now.utc.strftime("%Y%m%d%H%M%S"), format("%014d", next_migration_number)].max
|
|
12
|
+
end
|
|
13
|
+
def install
|
|
14
|
+
migration_template "create_rails_tool_views.rb", "db/migrate/create_rails_tool_views.rb"
|
|
15
|
+
template "rails_tool_views.rb", "config/initializers/rails_tool_views.rb"
|
|
16
|
+
route 'mount RailsToolViews::Engine => "/rails_tool_views", as: :rails_tool_views'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|