lcp 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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/lcp-custom-field/SKILL.md +10 -1
  3. data/.claude/skills/lcp-custom-field/agents/openai.yaml +4 -0
  4. data/.claude/skills/lcp-getting-started/SKILL.md +1 -1
  5. data/.claude/skills/lcp-getting-started/agents/openai.yaml +4 -0
  6. data/.claude/skills/lcp-host-binding/agents/openai.yaml +4 -0
  7. data/.claude/skills/lcp-model/agents/openai.yaml +4 -0
  8. data/.claude/skills/lcp-permissions/agents/openai.yaml +4 -0
  9. data/.claude/skills/lcp-presenter/agents/openai.yaml +4 -0
  10. data/.claude/skills/lcp-workflow/SKILL.md +10 -2
  11. data/.claude/skills/lcp-workflow/agents/openai.yaml +4 -0
  12. data/CHANGELOG.md +59 -1
  13. data/app/assets/javascripts/lcp_ruby/dev_toolbar.js +5 -6
  14. data/app/controllers/concerns/lcp_ruby/zone_resolution.rb +14 -1
  15. data/app/helpers/lcp_ruby/display/card_helper.rb +71 -16
  16. data/app/helpers/lcp_ruby/display_helper.rb +30 -3
  17. data/app/helpers/lcp_ruby/display_template_helper.rb +3 -3
  18. data/app/helpers/lcp_ruby/layout_helper.rb +49 -8
  19. data/app/helpers/lcp_ruby/tree_helper.rb +24 -3
  20. data/app/models/lcp_ruby/user.rb +10 -0
  21. data/app/views/layouts/lcp_ruby/application.html.erb +42 -20
  22. data/app/views/layouts/lcp_ruby/auth.html.erb +6 -1
  23. data/app/views/lcp_ruby/resources/_show_sections.html.erb +24 -17
  24. data/app/views/lcp_ruby/resources/_table_index.html.erb +12 -17
  25. data/app/views/lcp_ruby/widgets/_markdown.html.erb +13 -0
  26. data/app/views/lcp_ruby/widgets/_presenter_zone.html.erb +23 -7
  27. data/app/views/lcp_ruby/widgets/_rich_text.html.erb +25 -0
  28. data/docs/README.md +3 -0
  29. data/docs/feature-catalog.md +5 -2
  30. data/docs/feature-catalog.yml +89 -9
  31. data/docs/getting-started.md +38 -22
  32. data/docs/guides/dashboards.md +44 -1
  33. data/docs/guides/display-types.md +6 -0
  34. data/docs/guides/host-application.md +1 -1
  35. data/docs/guides/windows-setup.md +211 -0
  36. data/docs/guides/workflow.md +1 -1
  37. data/docs/reference/asset-pipeline.md +79 -0
  38. data/docs/reference/pages.md +50 -1
  39. data/docs/reference/presenters.md +6 -0
  40. data/docs/reference/workflow.md +2 -2
  41. data/examples/crm/Gemfile +3 -0
  42. data/examples/crm/Gemfile.lock +14 -9
  43. data/examples/crm/app/assets/config/manifest.js +4 -0
  44. data/examples/hr/Gemfile +3 -0
  45. data/examples/hr/Gemfile.lock +14 -9
  46. data/examples/hr/app/assets/config/manifest.js +4 -0
  47. data/examples/showcase/Gemfile +3 -0
  48. data/examples/showcase/Gemfile.lock +12 -9
  49. data/examples/showcase/app/assets/config/manifest.js +4 -0
  50. data/examples/showcase/config/lcp_ruby/pages/main_dashboard.yml +17 -2
  51. data/examples/showcase/config/locales/cs.yml +8 -0
  52. data/examples/showcase/config/locales/en.yml +8 -0
  53. data/examples/todo/Gemfile +3 -0
  54. data/examples/todo/Gemfile.lock +14 -9
  55. data/examples/todo/app/assets/config/manifest.js +4 -0
  56. data/exe/lcp +1 -1
  57. data/lib/generators/lcp_ruby/agent_setup_generator.rb +12 -9
  58. data/lib/generators/lcp_ruby/claude_skills_generator.rb +23 -19
  59. data/lib/generators/lcp_ruby/install_generator.rb +171 -3
  60. data/lib/generators/lcp_ruby/templates/agent_setup/agents_md.md +2 -1
  61. data/lib/generators/lcp_ruby/templates/agent_setup/claude_md.md +3 -2
  62. data/lib/generators/lcp_ruby/templates/install/menu.yml.tt +4 -0
  63. data/lib/generators/lcp_ruby/templates/monitoring/page.yml +5 -0
  64. data/lib/lcp_ruby/app_template.rb +37 -1
  65. data/lib/lcp_ruby/array_query.rb +33 -10
  66. data/lib/lcp_ruby/cli/new_command.rb +9 -4
  67. data/lib/lcp_ruby/cli/run_command.rb +98 -11
  68. data/lib/lcp_ruby/cli/skills_command.rb +27 -15
  69. data/lib/lcp_ruby/cli.rb +5 -1
  70. data/lib/lcp_ruby/configuration.rb +13 -2
  71. data/lib/lcp_ruby/custom_fields/applicator.rb +8 -0
  72. data/lib/lcp_ruby/custom_fields/query.rb +28 -20
  73. data/lib/lcp_ruby/display/markdown_sanitize.rb +36 -0
  74. data/lib/lcp_ruby/display/renderers/markdown.rb +6 -13
  75. data/lib/lcp_ruby/engine.rb +185 -1
  76. data/lib/lcp_ruby/export/data_generator.rb +5 -1
  77. data/lib/lcp_ruby/export/value_formatter.rb +34 -0
  78. data/lib/lcp_ruby/grouped_query/builder.rb +21 -0
  79. data/lib/lcp_ruby/metadata/configuration_validator.rb +5 -5
  80. data/lib/lcp_ruby/metadata/field_path.rb +57 -0
  81. data/lib/lcp_ruby/metadata/loader.rb +33 -1
  82. data/lib/lcp_ruby/metadata/menu_definition.rb +32 -2
  83. data/lib/lcp_ruby/metadata/menu_item.rb +53 -4
  84. data/lib/lcp_ruby/metadata/model_definition.rb +9 -0
  85. data/lib/lcp_ruby/metadata/zone_definition.rb +12 -5
  86. data/lib/lcp_ruby/metrics/json_query.rb +5 -0
  87. data/lib/lcp_ruby/model_factory/builder.rb +5 -0
  88. data/lib/lcp_ruby/model_factory/json_type_applicator.rb +35 -0
  89. data/lib/lcp_ruby/model_factory/label_method_builder.rb +3 -19
  90. data/lib/lcp_ruby/model_factory/schema_manager.rb +19 -0
  91. data/lib/lcp_ruby/pages/definition_validator.rb +13 -0
  92. data/lib/lcp_ruby/presenter/field_value_resolver.rb +36 -19
  93. data/lib/lcp_ruby/schemas/page.json +33 -1
  94. data/lib/lcp_ruby/search/custom_field_filter.rb +5 -0
  95. data/lib/lcp_ruby/skills_installer.rb +15 -2
  96. data/lib/lcp_ruby/tasks/doctor.rb +13 -8
  97. data/lib/lcp_ruby/version.rb +1 -1
  98. data/lib/lcp_ruby/widgets/data_resolver.rb +42 -1
  99. data/lib/lcp_ruby/widgets/date_grouper.rb +19 -0
  100. data/lib/lcp_ruby/workflow/contract_validator.rb +1 -1
  101. data/lib/lcp_ruby/workflow/model_source.rb +2 -2
  102. data/lib/lcp_ruby.rb +18 -0
  103. data/lib/tasks/lcp_ruby_db.rake +8 -1
  104. data/lib/tasks/lcp_ruby_i18n_check.rake +8 -0
  105. metadata +34 -24
  106. data/examples/crm/erd.md +0 -163
  107. data/examples/hr/erd.md +0 -396
  108. data/examples/showcase/erd.md +0 -567
  109. data/examples/todo/erd.md +0 -21
@@ -8,12 +8,16 @@ All examples use the **Ruby DSL** as the primary format. YAML equivalents are pr
8
8
 
9
9
  | Requirement | Version |
10
10
  |-------------|---------|
11
- | Ruby | >= 3.1 |
11
+ | Ruby | >= 3.2 |
12
12
  | Rails | >= 8.1 (for `lcp new`) or >= 7.1 (manual setup) |
13
13
  | Database | SQLite (development), PostgreSQL (production recommended) |
14
14
 
15
15
  LCP Ruby works with any Rails-supported database. SQLite is fine for development and this tutorial.
16
16
 
17
+ > **On Windows?** A clean Windows box needs a working native-compilation
18
+ > toolchain before `gem install lcp` and `lcp run` succeed. See the
19
+ > [Windows Setup guide](guides/windows-setup.md) for the seven-step path.
20
+
17
21
  ## Quick Start with `lcp new`
18
22
 
19
23
  The fastest way to create a new LCP Ruby application:
@@ -30,7 +34,7 @@ This launches an interactive wizard that asks about:
30
34
  Feature presets:
31
35
  | Preset | Includes |
32
36
  |--------|----------|
33
- | **minimal** | Core platform only (sample model + presenter + permissions) |
37
+ | **minimal** | Core platform only (sample `item` model + presenter + permissions) |
34
38
  | **standard** | Core + authentication + auditing + saved filters + export + import |
35
39
  | **full** | All 17 feature generators |
36
40
  | **custom** | Interactive selection of individual features |
@@ -44,6 +48,14 @@ rails s
44
48
 
45
49
  Visit `http://localhost:3000/items` to see the sample application.
46
50
 
51
+ The sample `item` entity is generated by default so a fresh install has something to click. Each generated file (`models/item`, `presenters/items`, `permissions/item`, `views/items`) carries a `DELETE ME` header pointing at all four siblings; delete them once you've added your own entities via `rails g lcp_ruby:entity`. To skip the sample entirely:
52
+
53
+ ```bash
54
+ lcp new my_app --skip-sample
55
+ # or in an existing Rails app:
56
+ rails generate lcp_ruby:install --skip-sample
57
+ ```
58
+
47
59
  To skip the interactive wizard and use defaults (sqlite3, DSL, minimal):
48
60
 
49
61
  ```bash
@@ -61,7 +73,7 @@ continue, then run `bundle exec rake lcp_ruby:doctor` from the new
61
73
  app to see exactly what is broken and how to fix it. See
62
74
  [reference/doctor.md](reference/doctor.md) for the full diagnostic.
63
75
 
64
- `lcp new` also installs [Claude Code skills](#claude-code-skills-ai-assisted-development) into `.claude/skills/` automatically — see the dedicated section below.
76
+ `lcp new` also installs [AI agent skills](#ai-agent-skills-ai-assisted-development) into `.claude/skills/` and `.codex/skills/` automatically — see the dedicated section below.
65
77
 
66
78
  ### Installing from a local checkout
67
79
 
@@ -145,25 +157,25 @@ missing, the generator aborts up-front with a structured
145
157
  Run `bundle exec rake lcp_ruby:doctor` to see the full feature
146
158
  dependency graph.
147
159
 
148
- ## Claude Code Skills (AI-Assisted Development)
160
+ ## AI Agent Skills (AI-Assisted Development)
149
161
 
150
- LCP Ruby ships a set of [Claude Code](https://claude.ai/code) skills (`lcp-model`, `lcp-presenter`, `lcp-permissions`, `lcp-workflow`, `lcp-custom-field`, `lcp-host-binding`) that auto-trigger when you're authoring the corresponding YAML/DSL configuration. Each skill briefs Claude on the 80/20 syntax + decision points + pitfalls and points at the relevant reference doc for the rest. The result: less time grepping for "how do I declare a `belongs_to` with a business-key join?" and fewer round-trips on common authoring tasks.
162
+ LCP Ruby ships a set of AI authoring skills (`lcp-getting-started`, `lcp-model`, `lcp-presenter`, `lcp-permissions`, `lcp-workflow`, `lcp-custom-field`, `lcp-host-binding`) that auto-trigger in Claude Code and Codex when you're authoring the corresponding YAML/DSL configuration. Each skill briefs the agent on the 80/20 syntax + decision points + pitfalls and points at the relevant reference doc for the rest. The result: less time grepping for "how do I declare a `belongs_to` with a business-key join?" and fewer round-trips on common authoring tasks.
151
163
 
152
- **Auto-installed by `lcp new`** — you don't need to do anything. The skills land in `.claude/skills/` of the new app. Open the project in Claude Code (CLI, VS Code, or any other client) and skills activate automatically based on context.
164
+ **Auto-installed by `lcp new`** — you don't need to do anything. The skills land in `.claude/skills/` and `.codex/skills/` of the new app. Open the project in Claude Code or Codex and skills activate automatically based on context.
153
165
 
154
166
  **Manual install / refresh after gem upgrade:**
155
167
 
156
168
  ```bash
157
- rails generate lcp_ruby:claude_skills
169
+ rails generate lcp_ruby:agent_setup
158
170
  ```
159
171
 
160
- This copies `.claude/skills/lcp-*/` from the gem path into the host application, overwriting the existing copies (use `--force=false` to skip existing files). Re-run after `bundle update lcp_ruby` to pick up skills updated for the new gem version — keeps Claude's instructions in sync with the DSL/schema changes you just bundled.
172
+ This copies `.claude/skills/lcp-*/` from the gem path into both `.claude/skills/` and `.codex/skills/`, overwriting existing `lcp-*` copies and pruning removed ones. Re-run after `bundle update lcp_ruby` to pick up skills updated for the new gem version — keeps agent instructions in sync with the DSL/schema changes you just bundled.
161
173
 
162
- **Verify**: list installed skills via `/skills` inside Claude Code, or `ls .claude/skills/` on disk.
174
+ **Verify**: list installed skills via `/skills` inside Claude Code, or inspect `.claude/skills/` and `.codex/skills/` on disk.
163
175
 
164
- **For host-app developers**: commit `.claude/skills/lcp-*` to git so the whole team benefits. (`.claude/settings.local.json` is per-user and stays gitignored.)
176
+ **For host-app developers**: commit `.claude/skills/lcp-*` and `.codex/skills/lcp-*` to git so the whole team benefits. (`.claude/settings.local.json` is per-user and stays gitignored.)
165
177
 
166
- **For platform contributors** (working on `lcp_ruby` itself): the canonical skill files live in `.claude/skills/` of the gem repo and are loaded automatically when you open the gem in Claude Code. The generator copies these same files — single source of truth.
178
+ **For platform contributors** (working on `lcp_ruby` itself): the canonical skill files live in `.claude/skills/` of the gem repo; `.codex/skills/lcp-*` is a symlink mirror for Codex. The generator copies the canonical files — single source of truth.
167
179
 
168
180
  ## Installation (step-by-step)
169
181
 
@@ -175,24 +187,28 @@ Add LCP Ruby to your Gemfile:
175
187
  gem "lcp"
176
188
  ```
177
189
 
178
- ### Asset Pipeline (Sprockets)
190
+ ### Asset Pipeline (handled automatically)
179
191
 
180
- LCP Ruby bundles its JavaScript and CSS via Sprockets. If your host app uses **Propshaft** (the Rails 8 default), you must also add `sprockets-rails` — otherwise the engine's JS (Stimulus controllers, row-click navigation, advanced filters, form handling) will not load:
192
+ LCP Ruby bundles its JavaScript and CSS via Sprockets. On Rails 8 (Propshaft default), the engine needs `sprockets-rails` as a compatibility shim — otherwise the engine's JS (Stimulus controllers, row-click, advanced filters, form widgets) silently fails to load.
181
193
 
182
- ```ruby
183
- gem "sprockets-rails"
194
+ **You don't need to do anything**: `lcp new` and `rails generate lcp_ruby:install` both add `sprockets-rails` to your Gemfile and create `app/assets/config/manifest.js` with the right link directives automatically. The engine also has a boot-time check that raises `LcpRuby::AssetPipelineError` with a fix recipe if the shim is missing, so silent failure mode is impossible.
195
+
196
+ If you've wired your own ESM bundler (esbuild, importmap with custom pins, etc.) and don't want the shim, opt out:
197
+
198
+ ```bash
199
+ lcp new my_app --skip-asset-pipeline
200
+ # or for an existing app:
201
+ rails generate lcp_ruby:install --skip-asset-pipeline
184
202
  ```
185
203
 
186
- Then create `app/assets/config/manifest.js` to link the engine assets:
204
+ …and silence the boot check:
187
205
 
188
- ```js
189
- //= link lcp_ruby/application.js
190
- //= link lcp_ruby/application.css
191
- //= link lcp_ruby/tom-select.css
192
- //= link lcp_ruby/tom-select.complete.min.js
206
+ ```ruby
207
+ # config/initializers/lcp_ruby.rb
208
+ LcpRuby.configure { |c| c.skip_asset_pipeline_check = true }
193
209
  ```
194
210
 
195
- > **Note:** If your host app already uses Sprockets, skip this step — the engine assets are picked up automatically.
211
+ See [Asset pipeline reference](reference/asset-pipeline.md) for the full background and the future ESM-native roadmap.
196
212
 
197
213
  Run `bundle install`.
198
214
 
@@ -128,7 +128,7 @@ Displays an aggregate value (count, sum, avg, min, max) from a model.
128
128
 
129
129
  ### Text
130
130
 
131
- Displays static i18n content.
131
+ Displays static i18n content as plain text (HTML-escaped).
132
132
 
133
133
  ```yaml
134
134
  - name: welcome
@@ -139,6 +139,49 @@ Displays static i18n content.
139
139
  position: { row: 2, col: 1, width: 12, height: 1 }
140
140
  ```
141
141
 
142
+ For formatted blurbs (headings, links, lists, emphasis) pick `rich_text` or `markdown` instead. All three share the same `content_key:` API.
143
+
144
+ ### Rich Text
145
+
146
+ i18n content emitted as raw HTML — locale string can carry `<h2>`, `<a>`, `<strong>`, etc.
147
+
148
+ ```yaml
149
+ - name: announcement
150
+ type: widget
151
+ widget:
152
+ type: rich_text
153
+ content_key: lcp_ruby.dashboard.announcement_html # i18n key holds HTML
154
+ position: { row: 2, col: 1, width: 12, height: 1 }
155
+ ```
156
+
157
+ Trust model: same level as any engine view that uses `raw`. Use when translations live alongside code and go through code review.
158
+
159
+ ### Markdown
160
+
161
+ i18n source is parsed as CommonMark (via Commonmarker, with table / tasklist / strikethrough / autolink extensions) and the rendered HTML is sanitized against an explicit tag allow-list. Raw HTML in the markdown source is stripped (`unsafe:` is deliberately omitted from Commonmarker options).
162
+
163
+ ```yaml
164
+ - name: welcome
165
+ type: widget
166
+ widget:
167
+ type: markdown
168
+ content_key: lcp_ruby.dashboard.welcome_md # i18n key holds Markdown
169
+ position: { row: 2, col: 1, width: 12, height: 1 }
170
+ ```
171
+
172
+ ```yaml
173
+ # en.yml
174
+ en:
175
+ lcp_ruby:
176
+ dashboard:
177
+ welcome_md: |
178
+ ## Welcome
179
+
180
+ Mix **KPIs**, lists, charts, and text widgets on one page.
181
+ ```
182
+
183
+ Markdown is the friendlier authoring path for hand-written blurbs and the safer trust posture when translations come from an external pipeline. Mirrors the model-level `:markdown` renderer (`display/renderers/markdown.rb`) so on-page and in-table content render identically.
184
+
142
185
  ### List
143
186
 
144
187
  Displays a limited list of records from a model.
@@ -195,6 +195,12 @@ end
195
195
 
196
196
  **Appearance:** A rounded pill with a colored background and white or dark text. Values not present in `color_map` fall back to a neutral gray badge.
197
197
 
198
+ > **`color_map` keys are raw enum keys**, e.g. `draft` / `active` /
199
+ > `paused` — not their humanized labels (`Draft`, `Active`, …) and not
200
+ > localized translations. The badge's display text is humanized
201
+ > automatically from the enum's `label:` / i18n entry; `color_map`
202
+ > operates on the raw stored value.
203
+
198
204
  ---
199
205
 
200
206
  ### `workflow_badge`
@@ -4,7 +4,7 @@ This guide walks through creating a new Rails application powered by the LCP Rub
4
4
 
5
5
  ## Prerequisites
6
6
 
7
- - Ruby >= 3.1
7
+ - Ruby >= 3.2
8
8
  - Rails >= 7.1
9
9
  - The `lcp_ruby` gem (local path or published)
10
10
 
@@ -0,0 +1,211 @@
1
+ # Windows Setup
2
+
3
+ LCP Ruby runs on Windows, but a clean Windows box needs a working Ruby
4
+ native-compilation toolchain before `gem install lcp`, `lcp skills install`,
5
+ and `lcp run <example>` succeed. Most of the friction is **environment-level**
6
+ (how RubyInstaller / rbenv-for-windows ship MSYS2), not LCP itself — but it
7
+ trips up new users, so this page documents the known-good path end to end.
8
+
9
+ > **Reality check.** On a clean Windows 11 box, budget ~1 hour the first time,
10
+ > mostly toolchain troubleshooting. Once the toolchain is fixed, it stays
11
+ > fixed — subsequent `lcp new` / `lcp run` runs are normal.
12
+
13
+ This recipe was field-verified on:
14
+
15
+ | | |
16
+ |---|---|
17
+ | OS | Windows 11 Enterprise (26200) |
18
+ | Ruby | 3.3.5 (RubyInstaller via rbenv-for-windows) |
19
+ | Shell | PowerShell 5.1, non-elevated |
20
+
21
+ Other Ruby/RubyInstaller versions follow the same shape; exact paths and which
22
+ MSYS2 libs you need may differ.
23
+
24
+ ## TL;DR — the seven steps
25
+
26
+ 1. **Install rbenv-for-windows + Ruby 3.3.5.**
27
+
28
+ ```powershell
29
+ iwr -useb https://github.com/RubyMetric/rbenv-for-windows/raw/main/tool/install.ps1 | iex
30
+ rbenv install 3.3.5
31
+ rbenv global 3.3.5
32
+ ```
33
+
34
+ 2. **Fix PATH and environment** (user-level env vars). Add to user `PATH`:
35
+
36
+ ```
37
+ C:\Ruby-on-Windows\msys64\ucrt64\bin
38
+ C:\Ruby-on-Windows\rbenv\bin
39
+ C:\Ruby-on-Windows\shims
40
+ ```
41
+
42
+ and set `RBENV_ROOT=C:\Ruby-on-Windows`. (Adjust the drive/folder to wherever
43
+ rbenv-for-windows installed; `RBENV_ROOT` is the root of that tree.) See
44
+ [gotcha C](#c-ucrt64bin-missing-from-path) for why the `ucrt64\bin` entry
45
+ matters.
46
+
47
+ 3. **Enable rbenv in your PowerShell profile** (`$PROFILE`):
48
+
49
+ ```powershell
50
+ & "$env:RBENV_ROOT\rbenv\bin\rbenv.ps1" init | Out-Null
51
+ ```
52
+
53
+ 4. **Patch `rbconfig.rb` so native gems compile** — append
54
+ `-Wno-error=incompatible-pointer-types` to `CONFIG["CFLAGS"]`. See
55
+ [gotcha A](#a-gcc-14-treats-incompatible-pointer-types-as-an-error).
56
+
57
+ 5. **Overwrite the stale bundled `libwinpthread-1.dll`** with the MSYS2 ucrt64
58
+ copy. See [gotcha D](#d-stale-bundled-libwinpthread-1dll-breaks-require-date).
59
+
60
+ 6. **Install the one MSYS2 system library LCP needs that isn't bundled** —
61
+ `libyaml` (required by `psych`). See [gotcha B](#b-msys2-keyring-uninitialized)
62
+ for the keyring workaround.
63
+
64
+ ```powershell
65
+ # after the SigLevel workaround in gotcha B:
66
+ ridk exec pacman -Sy --noconfirm
67
+ ridk exec pacman -S --noconfirm mingw-w64-ucrt-x86_64-libyaml
68
+ ```
69
+
70
+ 7. **Install LCP and run an example.**
71
+
72
+ ```powershell
73
+ gem install lcp
74
+ lcp skills install --global
75
+ lcp run showcase
76
+ ```
77
+
78
+ ## Environment gotchas (not LCP bugs)
79
+
80
+ These stem from how rbenv-for-windows ships MSYS2 + GCC, not from LCP. They
81
+ affect **every** Ruby project with native-extension gems
82
+ (`websocket-driver`, `bcrypt`, `puma`, `psych`, `io-console`, `stringio`,
83
+ `date`, …) — LCP just happens to depend on several of them.
84
+
85
+ ### A. GCC 14+ treats `incompatible-pointer-types` as an error
86
+
87
+ MSYS2's GCC 14 defaults `-Wincompatible-pointer-types` to **error**, which
88
+ breaks the native compile of many common gems on Ruby 3.3.5.
89
+
90
+ A `.gemrc` `:build_args:` workaround fixes `gem install` but **does not
91
+ propagate through Bundler** — so `bundle install` inside an example app still
92
+ fails. The reliable fix is to relax the flag at the compiler-config level:
93
+
94
+ Edit `<RBENV_ROOT>\rubies\<version>\lib\ruby\3.3.0\<arch>\rbconfig.rb` and
95
+ append to `CONFIG["CFLAGS"]`:
96
+
97
+ ```
98
+ -Wno-error=incompatible-pointer-types
99
+ ```
100
+
101
+ > Patch **`CFLAGS`**, not `warnflags` — the Makefile template the gems use
102
+ > doesn't reference `warnflags`, so a change there has no effect.
103
+
104
+ ### B. MSYS2 keyring uninitialized
105
+
106
+ After an rbenv-for-windows install, `pacman -S` fails with
107
+ `key "..." is unknown` / `keyring is not writable`, and the usual
108
+ `pacman-key --init && --populate msys2` repair isn't available on the
109
+ `ridk exec` PATH.
110
+
111
+ **Workaround** — temporarily disable signature checking, install, then restore:
112
+
113
+ 1. Edit `<RBENV_ROOT>\msys64\etc\pacman.conf` and set:
114
+
115
+ ```ini
116
+ SigLevel = Never
117
+ LocalFileSigLevel = Never
118
+ ```
119
+
120
+ 2. Sync and install the package(s) you need:
121
+
122
+ ```powershell
123
+ ridk exec pacman -Sy --noconfirm
124
+ ridk exec pacman -S --noconfirm mingw-w64-ucrt-x86_64-libyaml
125
+ ```
126
+
127
+ 3. **Restore** the original `SigLevel` / `LocalFileSigLevel` lines in
128
+ `pacman.conf`.
129
+
130
+ `libyaml` is the one library LCP indirectly needs (via `psych`) that MSYS2
131
+ doesn't bundle. Other native gems may pull in other MSYS2 libs — install them
132
+ the same way.
133
+
134
+ ### C. `ucrt64\bin` missing from PATH
135
+
136
+ Runtime DLLs (`libgmp`, `libssl`, `libwinpthread`, `libyaml`, …) live in
137
+ `<RBENV_ROOT>\msys64\ucrt64\bin`, but the installer doesn't add it to user
138
+ `PATH`. Without it, even `require 'date'` fails with
139
+ `127: procedure could not be found` once a native extension is built. Adding
140
+ the directory in [step 2](#tldr--the-seven-steps) fixes this.
141
+
142
+ ### D. Stale bundled `libwinpthread-1.dll` breaks `require 'date'`
143
+
144
+ The biggest gotcha. The `date` gem's native extension imports
145
+ `clock_gettime64` from `libwinpthread-1.dll`. The copy Ruby ships in
146
+ `<RBENV_ROOT>\rubies\<version>\bin\ruby_builtin_dlls\` is too old — it only
147
+ exports `clock_gettime`, no `_64` variant. Worse, Ruby's `AddDllDirectory`
148
+ loads `ruby_builtin_dlls\` **ahead** of `ucrt64\bin`, so fixing PATH
149
+ ([gotcha C](#c-ucrt64bin-missing-from-path)) alone isn't enough — the wrong
150
+ DLL still wins.
151
+
152
+ **Fix** — overwrite the bundled copy with the MSYS2 ucrt64 one:
153
+
154
+ ```powershell
155
+ Copy-Item "$env:RBENV_ROOT\msys64\ucrt64\bin\libwinpthread-1.dll" `
156
+ "$env:RBENV_ROOT\rubies\<version>\bin\ruby_builtin_dlls\libwinpthread-1.dll" -Force
157
+ ```
158
+
159
+ > This is an upstream packaging mismatch (RubyInstaller ships an older
160
+ > `libwinpthread` than the MSYS2 it bundles); the overwrite is a local
161
+ > workaround until the toolchain ships a matched pair.
162
+
163
+ ## LCP-specific notes on Windows
164
+
165
+ The toolchain gotchas above aside, two Windows concerns are handled by LCP
166
+ itself:
167
+
168
+ - **Timezone data is bundled.** Windows has no system `zoneinfo` database, so
169
+ TZInfo needs the pure-Ruby fallback or Rails boot fails with
170
+ `TZInfo::DataSources::ZoneinfoDirectoryNotFound`. The bundled example apps
171
+ (`crm`, `hr`, `showcase`, `todo`) include
172
+ `gem "tzinfo-data", platforms: %i[ windows jruby ]`, and apps generated by
173
+ `lcp new` get it from the Rails 8 generator automatically. No action needed.
174
+
175
+ - **The asset pipeline is wired automatically.** On a Rails 8 (Propshaft-only)
176
+ host, LCP's Sprockets-based JS bundle won't load without a compatibility
177
+ shim. Both `lcp new` and `rails generate lcp_ruby:install` add
178
+ `gem "sprockets-rails"` and create `app/assets/config/manifest.js` by default,
179
+ and the engine fails loudly at boot if the shim is missing — see the
180
+ [Asset Pipeline reference](../reference/asset-pipeline.md). No Windows-specific
181
+ action needed.
182
+
183
+ - **The first-request `Errno::EACCES` is handled.** Sprockets' asset-cache
184
+ write uses a non-atomic `File.rename` that races with Windows file locking,
185
+ and LCP's 12+ precompile entries all hit that cache in parallel on the first
186
+ request — so a fresh `rails server` used to crash its very first page with
187
+ `Permission denied @ rb_file_s_rename` (reload always worked). LCP now swaps
188
+ the file-backed assets cache for a `null_store` on Windows in
189
+ **non-production** (development and test both live-compile assets and race the
190
+ same way; production precompiles at deploy, and Unix is untouched), removing
191
+ the rename race. No action needed; if you ever want the cache back, re-set
192
+ `env.cache` in your own `config.assets.configure` block.
193
+
194
+ ## Re-running an example
195
+
196
+ `lcp run <name>` is idempotent: the first run materializes the bundled example
197
+ into a fresh directory and boots it (`bundle install` → `db:setup` → server);
198
+ running the same command again detects the already-materialized directory (via
199
+ the `.lcp-run` marker it dropped after the first successful setup) and just
200
+ re-boots it — `bundle install` → `db:prepare` → server — so your data from the
201
+ previous session survives. (If you dropped the database in the meantime, the
202
+ re-run falls back to `db:setup` and re-seeds.) No flag, no second command:
203
+
204
+ ```powershell
205
+ lcp run showcase # first time: materialize + boot
206
+ lcp run showcase # again: re-boot the same app, data preserved
207
+ ```
208
+
209
+ Pointing `lcp run` at a non-empty directory that LCP did **not** create (no
210
+ `.lcp-run` marker) still aborts with `already exists and is not empty`, so it
211
+ never clobbers your own files. In that case pass a different `DIR`.
@@ -518,7 +518,7 @@ LcpRuby.configure do |config|
518
518
  end
519
519
  ```
520
520
 
521
- Create the model with required fields (name, model_name, field_name, states, transitions, version, active). The `states` and `transitions` fields should be JSON columns containing the same structure as the YAML.
521
+ Create the model with required fields (name, target_model, field_name, states, transitions, version, active). The `states` and `transitions` fields should be JSON columns containing the same structure as the YAML.
522
522
 
523
523
  Static (YAML/DSL) workflows are always loaded if files exist. DB workflows are additive. On name conflicts, static wins.
524
524
 
@@ -0,0 +1,79 @@
1
+ # Asset pipeline
2
+
3
+ LCP Ruby ships its UI as ~50 JavaScript files (45 Stimulus controllers plus helpers, vendored Stimulus 3.2 UMD bundle, and a few utility scripts) and a single 1900-line CSS file. The JS is glued together by Sprockets `//= require` directives in `app/assets/javascripts/lcp_ruby/application.js`.
4
+
5
+ ## Why a Sprockets compatibility shim is required
6
+
7
+ Rails 8 ships **Propshaft** as the default asset pipeline. Propshaft is intentionally simple — it serves static assets and does not understand Sprockets' `//= require` directive syntax. Without `sprockets-rails`, two failure modes both produce a silently-broken UI:
8
+
9
+ | Failure mode | Cause | Symptom in browser |
10
+ |---|---|---|
11
+ | **A — no sprockets-rails at all** | The `<script>` tag renders (the old `respond_to?(:assets)` gate is true on Propshaft too, so it never actually suppressed it), but Propshaft has no compiled bundle to serve at that logical path | `<script>` tag present, request for LCP JS 404s; Stimulus never loads; top nav stays hidden behind the FOUC mask |
12
+ | **B — sprockets-rails present, manifest.js missing** | Propshaft serves `application.js` as the raw 58-line manifest with `//= require` comments instead of the 11000-line compiled bundle | `<script>` tag present, file fetched 200 OK, body is comments only, no Stimulus loads, top nav stays hidden |
13
+
14
+ Both modes ship no console errors and no server-side warnings. Audit #16 cost ~hours of debugging before identifying the cause — these are the worst kind of failures.
15
+
16
+ ## The fix (Phase 1, current)
17
+
18
+ `lcp new` and `rails generate lcp_ruby:install` both:
19
+
20
+ 1. Add `gem "sprockets-rails"` to the host's `Gemfile` (idempotent — re-runs don't duplicate)
21
+ 2. Create `app/assets/config/manifest.js` with the load-bearing link directives:
22
+
23
+ ```js
24
+ //= link lcp_ruby/application.js
25
+ //= link lcp_ruby/application.css
26
+ //= link lcp_ruby/tom-select.css
27
+ //= link lcp_ruby/tom-select.complete.min.js
28
+ ```
29
+
30
+ If the file already exists (e.g. host added their own `Chart.bundle.js` link), only the missing LCP lines are appended — host entries are preserved.
31
+
32
+ The engine adds a **boot-time check** (`lcp_ruby.asset_pipeline_check` initializer → `LcpRuby::Engine.check_asset_pipeline_compat!`) that raises `LcpRuby::AssetPipelineError` if Propshaft is loaded without sprockets-rails. The check skips:
33
+
34
+ - during `rails generate lcp_ruby:install` (chicken-and-egg — the generator is what adds the shim)
35
+ - when `LcpRuby.configuration.skip_asset_pipeline_check = true` (explicit opt-out)
36
+
37
+ So the silent failure mode is impossible — you either get the working UI or a loud actionable error.
38
+
39
+ ## Opting out (advanced)
40
+
41
+ If you've wired your own ESM bundler (esbuild, importmap-rails with custom pins, etc.) and serve the engine's JS your own way:
42
+
43
+ ```bash
44
+ # Skip the shim during install
45
+ lcp new my_app --skip-asset-pipeline
46
+ # or for an existing app:
47
+ rails generate lcp_ruby:install --skip-asset-pipeline
48
+ ```
49
+
50
+ Silence the boot check:
51
+
52
+ ```ruby
53
+ # config/initializers/lcp_ruby.rb
54
+ LcpRuby.configure do |config|
55
+ config.skip_asset_pipeline_check = true
56
+ end
57
+ ```
58
+
59
+ You take responsibility for compiling and serving the engine's JS bundle yourself. The 50 controllers register as `window.StimulusApp.register("lcp-foo", class extends Stimulus.Controller {...})` globals; your bundler needs to load them in the right order (Stimulus core first, then `stimulus_bootstrap.js`, then controllers — see `app/assets/javascripts/lcp_ruby/application.js` for the canonical order).
60
+
61
+ ## Phase 2 — ESM-native roadmap (not yet built)
62
+
63
+ The current architecture predates Rails 8's pivot to Propshaft. Long-term the engine should ship a pre-bundled ESM artifact that works on Propshaft natively, without the Sprockets shim. Sketch of the plan:
64
+
65
+ 1. Convert all 45 Stimulus controllers from `window.StimulusApp.register(...)` globals to ESM `export default class Foo extends Controller {}`
66
+ 2. Add a `package.json` + esbuild build step to the engine repo
67
+ 3. Commit the pre-bundled output (or distribute via `gemspec.files` so `bundle install` lays it down)
68
+ 4. Layout dual-mode: if the pre-bundled file exists, use it; else fall back to the Sprockets path
69
+
70
+ This is a substantial change (~2000 LOC) with breaking-change implications for hosts that have custom Stimulus controllers reading from `window.StimulusApp`. It is intentionally **not** in scope for the current fix — Phase 1 unblocks every new user today without forcing the bigger migration.
71
+
72
+ ## Files
73
+
74
+ - `lib/lcp_ruby/engine.rb` — `check_asset_pipeline_compat!` class method + `lcp_ruby.asset_pipeline_check` initializer
75
+ - `lib/generators/lcp_ruby/install_generator.rb` — `install_asset_pipeline_compat` step + `add_sprockets_rails_to_gemfile!` / `ensure_sprockets_manifest!` helpers
76
+ - `lib/lcp_ruby/app_template.rb` — top-level `gem "sprockets-rails"` so `lcp new` bundles it before the install generator boots Rails
77
+ - `lib/lcp_ruby/configuration.rb` — `skip_asset_pipeline_check` reader/writer (default false)
78
+ - `app/views/layouts/lcp_ruby/application.html.erb` (and `auth.html.erb`) — engine asset includes are gated on the `lcp_emit_engine_assets?` helper (= `!skip_asset_pipeline_check`). The old `respond_to?(:assets)` gate was removed; the new gate suppresses the pipeline-dependent tags only when the host opted out (own bundler), so it doesn't 500 on Propshaft
79
+ - `script/smoke_e2e.sh` — CI smoke test that probes `/assets/lcp_ruby/application.js` and verifies it's the compiled bundle, not the manifest stub
@@ -172,7 +172,7 @@ Displays an aggregate value (count, sum, avg, min, max) over a model's records,
172
172
 
173
173
  ### text
174
174
 
175
- Displays translated static content.
175
+ Displays translated static content as plain (HTML-escaped) text.
176
176
 
177
177
  | Attribute | Type | Required | Description |
178
178
  |-----------|------|----------|-------------|
@@ -188,6 +188,55 @@ Displays translated static content.
188
188
  position: { row: 2, col: 1, width: 12, height: 1 }
189
189
  ```
190
190
 
191
+ For formatted content use the `rich_text` or `markdown` widget instead — both share the same `content_key:` API and differ only in how the resolved string is rendered.
192
+
193
+ ### rich_text
194
+
195
+ Same `content_key:` lookup as `text`, but the resolved string is emitted via `raw` — so the translation can carry `<h2>`, `<a>`, `<strong>`, etc., and they render as tags. Trust model: locale files are author-controlled, the same level as any engine view that uses `raw`. When translations might originate outside code review (external translator pipeline), prefer `markdown` — its sanitize step makes the trust boundary explicit.
196
+
197
+ | Attribute | Type | Required | Description |
198
+ |-----------|------|----------|-------------|
199
+ | `type` | string | yes | Must be `rich_text`. |
200
+ | `content_key` | string | yes | i18n key whose value is HTML. |
201
+
202
+ ```yaml
203
+ - name: announcement
204
+ type: widget
205
+ widget:
206
+ type: rich_text
207
+ content_key: lcp_ruby.dashboard.announcement_html
208
+ position: { row: 2, col: 1, width: 12, height: 1 }
209
+ ```
210
+
211
+ ### markdown
212
+
213
+ i18n source is parsed by [Commonmarker](https://github.com/gjtorikian/commonmarker) (CommonMark + GFM tables, tasklists, strikethrough, autolinks) and the rendered HTML is sanitized against the same tag/attribute allow-list as the model-level `:markdown` renderer. `unsafe:` is **deliberately omitted** from the Commonmarker options, so raw HTML embedded in the source is stripped at parse time.
214
+
215
+ | Attribute | Type | Required | Description |
216
+ |-----------|------|----------|-------------|
217
+ | `type` | string | yes | Must be `markdown`. |
218
+ | `content_key` | string | yes | i18n key whose value is Markdown source. |
219
+
220
+ ```yaml
221
+ - name: welcome
222
+ type: widget
223
+ widget:
224
+ type: markdown
225
+ content_key: lcp_ruby.dashboard.welcome_md
226
+ position: { row: 2, col: 1, width: 12, height: 1 }
227
+ ```
228
+
229
+ ```yaml
230
+ # en.yml
231
+ en:
232
+ lcp_ruby:
233
+ dashboard:
234
+ welcome_md: |
235
+ ## Welcome
236
+
237
+ Mix **KPIs**, lists, charts, and *text-style* widgets on one page.
238
+ ```
239
+
191
240
  ### list
192
241
 
193
242
  Displays recent records from a model.
@@ -1242,6 +1242,12 @@ Renders the value as a colored badge. Useful for enum and status fields.
1242
1242
 
1243
1243
  Available colors: `green`, `red`, `blue`, `yellow`, `orange`, `purple`, `gray`, `teal`, `cyan`, `pink`.
1244
1244
 
1245
+ **`color_map` keys are raw enum keys, not humanized labels.** For an enum
1246
+ field with values `active`, `pending`, `suspended`, write
1247
+ `active: green` — not `Active: green` and not the localized label. The
1248
+ displayed text comes from the enum's `label:` / i18n translation
1249
+ automatically; `color_map` operates on the underlying stored value.
1250
+
1245
1251
  ```yaml
1246
1252
  - field: status
1247
1253
  renderer: badge
@@ -538,7 +538,7 @@ LcpRuby.configure do |config|
538
538
  # DB model source
539
539
  config.workflow_model = "workflow_definition"
540
540
  config.workflow_model_fields = {
541
- name: "name", model_name: "model_name", field_name: "field_name",
541
+ name: "name", target_model: "target_model", field_name: "field_name",
542
542
  states: "states", transitions: "transitions",
543
543
  version: "version", active: "active", audit_log: "audit_log"
544
544
  }
@@ -575,7 +575,7 @@ The DB model must have these fields:
575
575
  | Logical Name | Type | Description |
576
576
  |---|---|---|
577
577
  | `name` | string | Unique workflow name |
578
- | `model_name` | string | Target model name |
578
+ | `target_model` | string | Target model name |
579
579
  | `field_name` | string | Enum field name |
580
580
  | `states` | json | States hash |
581
581
  | `transitions` | json | Transitions hash |
data/examples/crm/Gemfile CHANGED
@@ -4,6 +4,9 @@ gem "rails", "~> 7.1"
4
4
  gem "sqlite3", "~> 1.4"
5
5
  gem "puma", "~> 6.0"
6
6
  gem "sprockets-rails"
7
+
8
+ # Windows has no system zoneinfo database; bundle the pure-Ruby fallback.
9
+ gem "tzinfo-data", platforms: %i[ windows jruby ]
7
10
  gem "image_processing", "~> 1.2"
8
11
  gem "chartkick"
9
12
  gem "lcp", path: "../.."