plutonium 0.55.0 → 0.56.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/.claude/skills/plutonium-resource/SKILL.md +21 -2
- data/.claude/skills/plutonium-ui/SKILL.md +15 -2
- data/CHANGELOG.md +31 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +94 -26
- data/app/assets/plutonium.js.map +2 -2
- data/app/assets/plutonium.min.js +9 -9
- data/app/assets/plutonium.min.js.map +3 -3
- data/config/initializers/rabl.rb +16 -0
- data/docs/.vitepress/config.ts +1 -0
- data/docs/public/templates/lite.rb +10 -0
- data/docs/reference/generators/lite.md +65 -0
- data/docs/reference/resource/definition.md +18 -2
- data/docs/reference/ui/assets.md +14 -0
- data/docs/reference/ui/displays.md +27 -1
- data/docs/reference/ui/forms.md +2 -1
- data/docs/reference/ui/layouts.md +33 -0
- data/docs/superpowers/plans/2026-06-04-sqlite-tune-maintenance-generators.md +857 -0
- data/docs/superpowers/plans/2026-06-04-sqlite-tune-maintenance-generators.md.tasks.json +45 -0
- data/docs/superpowers/specs/2026-06-04-sqlite-tune-maintenance-generators-design.md +238 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +4 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/configures_recurring.rb +89 -0
- data/lib/generators/pu/lite/maintenance/maintenance_generator.rb +45 -0
- data/lib/generators/pu/lite/maintenance/templates/app/jobs/sqlite_maintenance_job.rb.tt +60 -0
- data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +4 -51
- data/lib/generators/pu/lite/rails_pulse/templates/config/initializers/rails_pulse.rb.tt +1 -1
- data/lib/generators/pu/lite/tune/tune_generator.rb +105 -0
- data/lib/plutonium/models/has_cents.rb +10 -0
- data/lib/plutonium/resource/controllers/interactive_actions.rb +19 -2
- data/lib/plutonium/routing/mapper_extensions.rb +5 -0
- data/lib/plutonium/ui/display/base.rb +9 -0
- data/lib/plutonium/ui/display/components/badge.rb +83 -0
- data/lib/plutonium/ui/display/components/boolean.rb +28 -6
- data/lib/plutonium/ui/display/components/currency.rb +50 -0
- data/lib/plutonium/ui/display/options/inferred_types.rb +13 -0
- data/lib/plutonium/ui/display/theme.rb +5 -0
- data/lib/plutonium/ui/form/base.rb +5 -0
- data/lib/plutonium/ui/form/components/toggle.rb +14 -0
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +14 -25
- data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +67 -0
- data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +5 -38
- data/lib/plutonium/ui/form/interaction.rb +7 -2
- data/lib/plutonium/ui/form/options/inferred_types.rb +2 -0
- data/lib/plutonium/ui/form/resource.rb +1 -0
- data/lib/plutonium/ui/form/theme.rb +12 -0
- data/lib/plutonium/ui/grid/card.rb +58 -21
- data/lib/plutonium/ui/layout/icon_rail.rb +29 -9
- data/lib/plutonium/ui/sidebar_menu.rb +29 -0
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/plutonium.gemspec +5 -4
- data/src/css/components.css +126 -0
- data/src/js/controllers/dirty_form_guard_controller.js +55 -4
- data/src/js/controllers/nested_resource_form_fields_controller.js +35 -12
- data/src/js/controllers/resource_drop_down_controller.js +49 -14
- metadata +19 -6
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"planPath": "docs/superpowers/plans/2026-06-04-sqlite-tune-maintenance-generators.md",
|
|
3
|
+
"tasks": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"subject": "Task 1: Extract ConfiguresRecurring concern + refactor rails_pulse",
|
|
7
|
+
"status": "completed",
|
|
8
|
+
"description": "Move env-aware recurring.yml injection into a reusable, unit-testable ConfiguresRecurring concern (pure nested RecurringYAML); rails_pulse keeps identical behavior.\n\n```json:metadata\n{\"files\": [\"lib/generators/pu/lib/plutonium_generators/concerns/configures_recurring.rb\", \"lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb\", \"test/generators/configures_recurring_test.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/generators/configures_recurring_test.rb test/generators/lite_generators_test.rb\", \"acceptanceCriteria\": [\"ConfiguresRecurring autoloads and exposes add_recurring_tasks(tasks_yaml, marker:)\", \"RecurringYAML tested for env-scoped and flat layouts\", \"rails_pulse uses concern, old private method removed, tests still pass\"], \"requiresUserVerification\": false}\n```"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": 2,
|
|
12
|
+
"subject": "Task 2: pu:lite:tune generator (SQLite pragmas)",
|
|
13
|
+
"status": "completed",
|
|
14
|
+
"description": "Generator that inserts tuned, version-aware performance pragmas into config/database.yml default block, idempotently.\n\n```json:metadata\n{\"files\": [\"lib/generators/pu/lite/tune/tune_generator.rb\", \"test/generators/lite_generators_test.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/generators/lite_generators_test.rb\", \"acceptanceCriteria\": [\"TuneGenerator < Rails::Generators::Base, includes Generator\", \"Rails 8.1+ block = four deltas, no baseline, no busy_timeout\", \"Rails <8.1 also includes baseline pragmas\", \"idempotent re-run\"], \"requiresUserVerification\": false}\n```"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": 3,
|
|
18
|
+
"subject": "Task 3: pu:lite:maintenance generator (job + schedule)",
|
|
19
|
+
"status": "completed",
|
|
20
|
+
"blockedBy": [1],
|
|
21
|
+
"description": "Generator that installs app/jobs/sqlite_maintenance_job.rb and schedules it in config/recurring.yml via ConfiguresRecurring.\n\n```json:metadata\n{\"files\": [\"lib/generators/pu/lite/maintenance/maintenance_generator.rb\", \"lib/generators/pu/lite/maintenance/templates/app/jobs/sqlite_maintenance_job.rb.tt\", \"test/generators/lite_generators_test.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/generators/lite_generators_test.rb\", \"acceptanceCriteria\": [\"MaintenanceGenerator includes Generator + ConfiguresRecurring\", \"--schedule option defaults to 'every day at 3:30am'\", \"job template defines MaintenanceConnection/OPTIMIZE_DBS/VACUUM_DBS, PRAGMA optimize + VACUUM, Rails.error.report\"], \"requiresUserVerification\": false}\n```"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": 4,
|
|
25
|
+
"subject": "Task 4: Wire generators into lite.rb app template",
|
|
26
|
+
"status": "completed",
|
|
27
|
+
"blockedBy": [2, 3],
|
|
28
|
+
"description": "lite app template runs pu:lite:tune after setup and pu:lite:maintenance after the solid/rails_pulse stack, each with a commit guard. Only edit public source.\n\n```json:metadata\n{\"files\": [\"docs/public/templates/lite.rb\"], \"verifyCommand\": \"ruby -c docs/public/templates/lite.rb\", \"acceptanceCriteria\": [\"tune generated after setup with commit guard\", \"maintenance generated after rails_pulse with commit guard\", \"only public/templates/lite.rb edited\"], \"requiresUserVerification\": false}\n```"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": 5,
|
|
32
|
+
"subject": "Task 5: Docs page + sidebar entry",
|
|
33
|
+
"status": "completed",
|
|
34
|
+
"description": "Add docs/reference/generators/lite.md documenting both generators and link it in the VitePress sidebar.\n\n```json:metadata\n{\"files\": [\"docs/reference/generators/lite.md\", \"docs/.vitepress/config.ts\"], \"verifyCommand\": \"yarn docs:build\", \"acceptanceCriteria\": [\"lite.md documents both generators with rationale\", \"sidebar links /reference/generators/lite\", \"docs build passes with no broken links\"], \"requiresUserVerification\": false}\n```"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": 6,
|
|
38
|
+
"subject": "Task 6: Full suite + single final commit",
|
|
39
|
+
"status": "pending",
|
|
40
|
+
"blockedBy": [1, 2, 3, 4, 5],
|
|
41
|
+
"description": "Run full generator test suite, then make ONE commit with all changes (per user's 'commit at the end').\n\n```json:metadata\n{\"files\": [], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/generators/lite_generators_test.rb test/generators/configures_recurring_test.rb\", \"acceptanceCriteria\": [\"generator tests pass on Rails 8.1\", \"single commit contains all generators, concern, refactor, template, docs\"], \"requiresUserVerification\": false}\n```"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"lastUpdated": "2026-06-04"
|
|
45
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# SQLite Tuning & Maintenance Generators — Design
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-06-04
|
|
4
|
+
**Status:** Approved (design); pending implementation plan
|
|
5
|
+
|
|
6
|
+
## Summary
|
|
7
|
+
|
|
8
|
+
Port two production-proven SQLite improvements from `radioactive_labs/universal_chatbot`
|
|
9
|
+
into the Plutonium generator template:
|
|
10
|
+
|
|
11
|
+
1. **Config tuning** — performance pragmas in `config/database.yml`.
|
|
12
|
+
2. **Maintenance** — a scheduled `SqliteMaintenanceJob` (`PRAGMA optimize` everywhere,
|
|
13
|
+
`VACUUM` on safe databases only).
|
|
14
|
+
|
|
15
|
+
Both ship as **two new generators** under the existing `pu:lite` namespace:
|
|
16
|
+
`pu:lite:tune` and `pu:lite:maintenance`. The existing `pu:lite:setup` generator is
|
|
17
|
+
left untouched.
|
|
18
|
+
|
|
19
|
+
## Background
|
|
20
|
+
|
|
21
|
+
### What the reference project does
|
|
22
|
+
|
|
23
|
+
`config/database.yml` `default: &default` block carries tuned pragmas:
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
pragmas:
|
|
27
|
+
cache_size: -64000 # 64 MB page cache (default ~2 MB is too small)
|
|
28
|
+
temp_store: 2 # MEMORY — sorts/temp indexes stay off disk
|
|
29
|
+
mmap_size: 536870912 # 512 MB (override the 128 MB default)
|
|
30
|
+
wal_autocheckpoint: 10000 # checkpoint every ~40 MB of WAL, fewer pauses
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
It deliberately does **not** set `busy_timeout` — Rails routes the `timeout:` key to
|
|
34
|
+
the sqlite3 gem's constant-poll `busy_handler_timeout`, which has better tail-latency
|
|
35
|
+
than SQLite's internal backoff. Adding `busy_timeout` to pragmas would replace that with
|
|
36
|
+
the worse handler.
|
|
37
|
+
|
|
38
|
+
`app/jobs/sqlite_maintenance_job.rb` runs nightly via `config/recurring.yml`:
|
|
39
|
+
|
|
40
|
+
- Uses an isolated abstract connection class (`MaintenanceConnection`) so it never
|
|
41
|
+
mutates the global primary connection that sibling jobs rely on.
|
|
42
|
+
- `PRAGMA optimize` on all databases (cheap, brief shared lock).
|
|
43
|
+
- `VACUUM` only on databases without live 24/7 writers. Excludes `queue`/`cache`/`cable`
|
|
44
|
+
because SolidQueue / Solid Cache / Solid Cable write to them constantly, and VACUUM's
|
|
45
|
+
global exclusive lock stalls and errors those processes (e.g. SolidQueue process
|
|
46
|
+
deregistration hitting "database is locked"). This is the `2b813bdd` fix
|
|
47
|
+
("stop nightly VACUUM from locking the live queue DB").
|
|
48
|
+
- Errors are reported via `Rails.error.report` and the connection is always removed in
|
|
49
|
+
an `ensure` block.
|
|
50
|
+
|
|
51
|
+
### Current Plutonium state
|
|
52
|
+
|
|
53
|
+
- `pu:lite:setup` only ensures the `sqlite3` gem version and adds the Rails 7 enhanced
|
|
54
|
+
adapter. **No pragma tuning.**
|
|
55
|
+
- `pu:lite:*` generators (solid_queue, solid_cache, solid_cable, solid_errors,
|
|
56
|
+
rails_pulse, litestream) wire up extra databases via the `ConfiguresSqlite` concern.
|
|
57
|
+
- `rails_pulse_generator.rb` already contains working, env-aware `config/recurring.yml`
|
|
58
|
+
injection logic (handles both env-scoped and flat recurring files).
|
|
59
|
+
- **No pragma tuning and no maintenance job exist in the template today.**
|
|
60
|
+
|
|
61
|
+
## Design Decisions (confirmed)
|
|
62
|
+
|
|
63
|
+
| Decision | Choice |
|
|
64
|
+
|---|---|
|
|
65
|
+
| Packaging | Two new generators: `pu:lite:tune`, `pu:lite:maintenance`. `pu:lite:setup` untouched. |
|
|
66
|
+
| Pragma values | Port reference values **verbatim**. |
|
|
67
|
+
| VACUUM target selection | Editable list in the generated job; runtime confirms each DB exists (`configs_for ... return unless config`); user can extend the list. |
|
|
68
|
+
| `recurring.yml` editing | Extract the rails_pulse injection logic into a shared `ConfiguresRecurring` concern; rails_pulse refactored to use it. |
|
|
69
|
+
| Generator names | `pu:lite:tune` and `pu:lite:maintenance` (confirmed). |
|
|
70
|
+
|
|
71
|
+
## Component 1: `pu:lite:tune`
|
|
72
|
+
|
|
73
|
+
**File:** `lib/generators/pu/lite/tune/tune_generator.rb`
|
|
74
|
+
|
|
75
|
+
**Purpose:** Insert tuned performance pragmas into `config/database.yml`'s
|
|
76
|
+
`default: &default` block.
|
|
77
|
+
|
|
78
|
+
**Behavior:**
|
|
79
|
+
|
|
80
|
+
- Locate the `default: &default` mapping in `config/database.yml`.
|
|
81
|
+
- If a `pragmas:` key already exists under `default`, merge our keys, skipping any key
|
|
82
|
+
already present (no clobbering user values).
|
|
83
|
+
- If no `pragmas:` key exists, insert a new `pragmas:` block containing the four reference
|
|
84
|
+
deltas plus the reference's explanatory comments, including the `busy_timeout` note.
|
|
85
|
+
- **Version-aware pragma set:**
|
|
86
|
+
- Rails **8.1+**: write only the four deltas (`cache_size`, `temp_store`, `mmap_size`,
|
|
87
|
+
`wal_autocheckpoint`) — Rails already sets WAL / synchronous=NORMAL / foreign_keys /
|
|
88
|
+
mmap=128MB / journal_size_limit by default.
|
|
89
|
+
- Rails **< 8.1**: also emit the baseline set (`journal_mode: WAL`,
|
|
90
|
+
`synchronous: NORMAL`, `foreign_keys: true`, `journal_size_limit`) since these are not
|
|
91
|
+
guaranteed there. The Rails 7 path already pulls in
|
|
92
|
+
`activerecord-enhancedsqlite3-adapter` via `pu:lite:setup`, which supports pragmas.
|
|
93
|
+
- **Idempotent:** re-running skips keys already present; a marker (the comment header or
|
|
94
|
+
the presence of our keys) prevents duplicate insertion.
|
|
95
|
+
- Emits `say_status` lines for each key added.
|
|
96
|
+
|
|
97
|
+
**Reuse:** YAML location/insertion can lean on the existing `ConfiguresSqlite` concern's
|
|
98
|
+
file-manipulation helpers (`insert_into_file`, regex anchors) and `file_includes?`. The
|
|
99
|
+
default-block anchor is matched the same way the concern matches `default: &default`.
|
|
100
|
+
|
|
101
|
+
## Component 2: `pu:lite:maintenance`
|
|
102
|
+
|
|
103
|
+
**File:** `lib/generators/pu/lite/maintenance/maintenance_generator.rb`
|
|
104
|
+
**Template:** `lib/generators/pu/lite/maintenance/templates/app/jobs/sqlite_maintenance_job.rb.tt`
|
|
105
|
+
|
|
106
|
+
**Purpose:** Install `SqliteMaintenanceJob` and schedule it in `config/recurring.yml`.
|
|
107
|
+
|
|
108
|
+
**Behavior:**
|
|
109
|
+
|
|
110
|
+
- `template` the job to `app/jobs/sqlite_maintenance_job.rb`, ported from the reference:
|
|
111
|
+
- Isolated `MaintenanceConnection < ActiveRecord::Base` (`abstract_class = true`).
|
|
112
|
+
- `OPTIMIZE_DBS` and `VACUUM_DBS = %w[primary errors rails_pulse]` as **editable
|
|
113
|
+
constants**, with comments instructing the user to add their own DB names.
|
|
114
|
+
- At runtime, `configs_for(env_name:, name:, include_hidden: true)` returns the config
|
|
115
|
+
or nil; `return unless config` silently skips DBs that don't exist — this is the
|
|
116
|
+
"keep a list, confirm they exist" behavior.
|
|
117
|
+
- `PRAGMA optimize` on every `OPTIMIZE_DBS` entry; `VACUUM` only on `VACUUM_DBS`.
|
|
118
|
+
- Preserve the live-writer exclusion rationale (queue/cache/cable) as comments.
|
|
119
|
+
- `rescue => e` → `Rails.error.report(e, context: {...})`; `ensure` removes the
|
|
120
|
+
connection.
|
|
121
|
+
- Add the recurring entry to `config/recurring.yml` using the shared
|
|
122
|
+
`ConfiguresRecurring` concern (see below):
|
|
123
|
+
```yaml
|
|
124
|
+
sqlite_maintenance:
|
|
125
|
+
class: SqliteMaintenanceJob
|
|
126
|
+
queue: default
|
|
127
|
+
schedule: every day at 3:30am
|
|
128
|
+
description: "VACUUM + PRAGMA optimize across SQLite databases"
|
|
129
|
+
```
|
|
130
|
+
- **Gating:** if solid_queue is not installed or `config/recurring.yml` is absent, still
|
|
131
|
+
write the job file but log that no scheduler was found (mirrors rails_pulse's
|
|
132
|
+
`solid_queue_installed?` gate).
|
|
133
|
+
- **Idempotent:** skip the recurring injection if `sqlite_maintenance` is already present;
|
|
134
|
+
`template` with conflict handling for the job file.
|
|
135
|
+
|
|
136
|
+
## Component 3: `ConfiguresRecurring` concern (refactor)
|
|
137
|
+
|
|
138
|
+
**File:** `lib/generators/pu/lib/plutonium_generators/concerns/configures_recurring.rb`
|
|
139
|
+
|
|
140
|
+
Extract the env-aware `recurring.yml` injection currently private to
|
|
141
|
+
`rails_pulse_generator.rb` into a reusable concern. Concerns are autoloaded by Zeitwerk
|
|
142
|
+
(`Zeitwerk::Loader.for_gem` in `lib/generators/pu/lib/plutonium_generators.rb`), so a new
|
|
143
|
+
file at the conventional path with module nesting
|
|
144
|
+
`PlutoniumGenerators::Concerns::ConfiguresRecurring` is picked up automatically — no
|
|
145
|
+
manual `require` needed. Public surface:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
# Inject one or more recurring task blocks into config/recurring.yml,
|
|
149
|
+
# handling both env-scoped (production:/development:) and flat layouts.
|
|
150
|
+
add_recurring_tasks(tasks_yaml, marker:)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- `tasks_yaml`: the YAML body for the task(s), without leading indentation (the concern
|
|
154
|
+
re-indents per environment, as the existing code does).
|
|
155
|
+
- `marker`: a string used for the `file_includes?` idempotency guard (e.g.
|
|
156
|
+
`"rails_pulse"`, `"sqlite_maintenance"`).
|
|
157
|
+
- Internals are the existing `inject_*_under_envs` / indent-detection logic, generalized
|
|
158
|
+
to accept arbitrary task YAML instead of hardcoded rails_pulse tasks.
|
|
159
|
+
|
|
160
|
+
`rails_pulse_generator.rb` is refactored to call `add_recurring_tasks` with its existing
|
|
161
|
+
task YAML and `marker: "rails_pulse"`. No behavior change.
|
|
162
|
+
|
|
163
|
+
## Testing
|
|
164
|
+
|
|
165
|
+
Add to `test/generators/` (alongside `lite_generators_test.rb`,
|
|
166
|
+
`configures_sqlite_test.rb`):
|
|
167
|
+
|
|
168
|
+
- **`pu:lite:tune`:**
|
|
169
|
+
- Inserts the `pragmas:` block with the four delta keys into the `default` anchor.
|
|
170
|
+
- Re-running is idempotent (no duplicate keys).
|
|
171
|
+
- Merges into an existing `pragmas:` block without clobbering user-set keys.
|
|
172
|
+
- (If feasible in the harness) version-aware: Rails < 8.1 emits the baseline set.
|
|
173
|
+
- **`pu:lite:maintenance`:**
|
|
174
|
+
- Creates `app/jobs/sqlite_maintenance_job.rb` with the expected constants.
|
|
175
|
+
- Injects the `sqlite_maintenance` entry under each environment in an env-scoped
|
|
176
|
+
`recurring.yml`; idempotent on re-run.
|
|
177
|
+
- When `recurring.yml` is absent, writes the job and logs the missing-scheduler notice.
|
|
178
|
+
- **`ConfiguresRecurring`:** the extracted concern behaves identically to the old
|
|
179
|
+
rails_pulse path (regression guard) — verify via the existing rails_pulse test still
|
|
180
|
+
passing plus a focused concern test.
|
|
181
|
+
|
|
182
|
+
## App template integration (`lite.rb`)
|
|
183
|
+
|
|
184
|
+
`docs/public/templates/lite.rb` is the SQLite-stack app template that chains the
|
|
185
|
+
`pu:lite:*` generators `after_bundle` (setup → solid_queue/cache/cable/errors →
|
|
186
|
+
litestream → rails_pulse), each followed by a conditional git commit.
|
|
187
|
+
|
|
188
|
+
Wire **both** new generators into this template, matching the existing pattern (each with
|
|
189
|
+
its own `git add` / `git commit` guard):
|
|
190
|
+
|
|
191
|
+
- `pu:lite:tune` — immediately after `pu:lite:setup` (pragmas belong with the base SQLite
|
|
192
|
+
config, before the extra DBs are added).
|
|
193
|
+
- `pu:lite:maintenance` — after the solid stack and rails_pulse, so the extra databases
|
|
194
|
+
exist and `solid_queue` is present for scheduling.
|
|
195
|
+
|
|
196
|
+
Only edit the source template at `docs/public/templates/lite.rb`; the copies under
|
|
197
|
+
`docs/dist/` and `docs/.vitepress/dist/` are build artifacts regenerated by
|
|
198
|
+
`yarn docs:build`.
|
|
199
|
+
|
|
200
|
+
## Documentation
|
|
201
|
+
|
|
202
|
+
- New standalone page **`docs/reference/generators/lite.md`** describing the `pu:lite:*`
|
|
203
|
+
generators, with full sections for `pu:lite:tune` and `pu:lite:maintenance` (purpose,
|
|
204
|
+
options, idempotency, the `busy_timeout` rationale, and the VACUUM live-writer
|
|
205
|
+
exclusion rationale). Documenting the other existing `pu:lite:*` generators on this page
|
|
206
|
+
is a nice-to-have but the two new ones are required.
|
|
207
|
+
- Add the new page to the VitePress sidebar in `docs/.vitepress/config.ts` (near the
|
|
208
|
+
existing `/reference/app/generators` entry) so it is reachable.
|
|
209
|
+
- Skills require a gem release to take effect; a skill update is **out of scope** for this
|
|
210
|
+
change (docs page + sidebar only).
|
|
211
|
+
|
|
212
|
+
## Out of scope (YAGNI)
|
|
213
|
+
|
|
214
|
+
- App-specific extras from the reference (`sqlite_vec` extension loading, per-app
|
|
215
|
+
cleanup jobs, Rails Pulse summary/cleanup jobs) — those belong to their own generators
|
|
216
|
+
or the app, not this port.
|
|
217
|
+
- Changing `pu:lite:setup` behavior.
|
|
218
|
+
- Litestream / backup concerns (separate `pu:lite:litestream` generator already exists).
|
|
219
|
+
|
|
220
|
+
## File change list
|
|
221
|
+
|
|
222
|
+
| Action | Path |
|
|
223
|
+
|---|---|
|
|
224
|
+
| Create | `lib/generators/pu/lite/tune/tune_generator.rb` |
|
|
225
|
+
| Create | `lib/generators/pu/lite/maintenance/maintenance_generator.rb` |
|
|
226
|
+
| Create | `lib/generators/pu/lite/maintenance/templates/app/jobs/sqlite_maintenance_job.rb.tt` |
|
|
227
|
+
| Create | `lib/generators/pu/lib/plutonium_generators/concerns/configures_recurring.rb` |
|
|
228
|
+
| Modify | `lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb` (use shared concern) |
|
|
229
|
+
| Modify | `docs/public/templates/lite.rb` (chain `pu:lite:tune` + `pu:lite:maintenance`) |
|
|
230
|
+
| Create | `test/generators/tune_generator_test.rb` (or extend `lite_generators_test.rb`) |
|
|
231
|
+
| Create | `test/generators/maintenance_generator_test.rb` (or extend `lite_generators_test.rb`) |
|
|
232
|
+
| Create | `docs/reference/generators/lite.md` |
|
|
233
|
+
| Modify | `docs/.vitepress/config.ts` (sidebar entry for the new page) |
|
|
234
|
+
|
|
235
|
+
> Zeitwerk autoloads concerns by path, so no edit to
|
|
236
|
+
> `lib/generators/pu/lib/plutonium_generators.rb` is required for the new concern.
|
|
237
|
+
> The `docs/dist/` and `docs/.vitepress/dist/` copies of `lite.rb` are build artifacts —
|
|
238
|
+
> do not hand-edit them.
|
|
@@ -23,7 +23,10 @@ module Pu
|
|
|
23
23
|
return unless File.file?(Rails.root.join(".claude", "skills", "plutonium", "SKILL.md"))
|
|
24
24
|
|
|
25
25
|
say_status :update, "Syncing Plutonium Claude skills...", :green
|
|
26
|
-
|
|
26
|
+
# Shell out to a fresh process so the sync reads the newly-installed
|
|
27
|
+
# gem's skills. Invoking in-process would reuse the already-loaded
|
|
28
|
+
# (pre-update) gem, whose Plutonium.root still points at the old skills.
|
|
29
|
+
run "bin/rails generate pu:skills:sync"
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
def update_gem
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlutoniumGenerators
|
|
4
|
+
module Concerns
|
|
5
|
+
module ConfiguresRecurring
|
|
6
|
+
ENV_KEYS = %w[production development staging test].freeze
|
|
7
|
+
|
|
8
|
+
# Pure transform of a config/recurring.yml string. No file IO — testable
|
|
9
|
+
# in isolation, mirroring ConfiguresSqlite::DatabaseYAML.
|
|
10
|
+
class RecurringYAML
|
|
11
|
+
# Returns new content with `tasks_yaml` injected. If the file is
|
|
12
|
+
# env-scoped (has top-level production:/development:/... keys), the
|
|
13
|
+
# tasks are inserted under each environment at the siblings' indent.
|
|
14
|
+
# Otherwise they are appended at column 0.
|
|
15
|
+
def inject(content, tasks_yaml)
|
|
16
|
+
if env_scoped?(content)
|
|
17
|
+
inject_under_envs(content, tasks_yaml)
|
|
18
|
+
else
|
|
19
|
+
content.rstrip + "\n\n" + indent(tasks_yaml, 0)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def env_scoped?(content)
|
|
26
|
+
content.lines.any? { |l| l.match?(env_re) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def env_re
|
|
30
|
+
/^(#{ENV_KEYS.join("|")}):\s*$/
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def indent(yaml, n)
|
|
34
|
+
pad = " " * n
|
|
35
|
+
yaml.gsub(/^(?=.)/, pad)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def inject_under_envs(content, tasks_yaml)
|
|
39
|
+
lines = content.lines
|
|
40
|
+
env_starts = lines.each_with_index.select { |l, _| env_re.match?(l) }.map(&:last)
|
|
41
|
+
|
|
42
|
+
env_starts.reverse_each do |start|
|
|
43
|
+
end_idx = lines.length
|
|
44
|
+
((start + 1)...lines.length).each do |i|
|
|
45
|
+
if lines[i].match?(/^[^\s#]/)
|
|
46
|
+
end_idx = i
|
|
47
|
+
break
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
child_indent = 2
|
|
52
|
+
((start + 1)...end_idx).each do |i|
|
|
53
|
+
if (m = lines[i].match(/^(\s+)\S/))
|
|
54
|
+
child_indent = m[1].length
|
|
55
|
+
break
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
insert_at = end_idx
|
|
60
|
+
while insert_at > start + 1 && lines[insert_at - 1].strip.empty?
|
|
61
|
+
insert_at -= 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
lines.insert(insert_at, "\n", indent(tasks_yaml, child_indent))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
lines.join
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Inject recurring task YAML into config/recurring.yml. Returns true when
|
|
74
|
+
# written, false when the file is missing or the marker already present
|
|
75
|
+
# (idempotent). `marker` is matched with file_includes? to avoid dupes.
|
|
76
|
+
def add_recurring_tasks(tasks_yaml, marker:)
|
|
77
|
+
recurring_file = "config/recurring.yml"
|
|
78
|
+
full_path = File.expand_path(recurring_file, destination_root)
|
|
79
|
+
return false unless File.exist?(full_path)
|
|
80
|
+
return false if file_includes?(recurring_file, marker)
|
|
81
|
+
|
|
82
|
+
new_content = RecurringYAML.new.inject(File.read(full_path), tasks_yaml)
|
|
83
|
+
create_file recurring_file, new_content, force: true
|
|
84
|
+
say_status :recurring, "#{marker} (config/recurring.yml)"
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../lib/plutonium_generators"
|
|
4
|
+
|
|
5
|
+
module Pu
|
|
6
|
+
module Lite
|
|
7
|
+
class MaintenanceGenerator < Rails::Generators::Base
|
|
8
|
+
include PlutoniumGenerators::Generator
|
|
9
|
+
include PlutoniumGenerators::Concerns::ConfiguresRecurring
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Install a nightly SqliteMaintenanceJob (PRAGMA optimize + VACUUM)"
|
|
14
|
+
|
|
15
|
+
class_option :schedule, type: :string, default: "every day at 3:30am",
|
|
16
|
+
desc: "Cron-style schedule for the maintenance job"
|
|
17
|
+
|
|
18
|
+
def start
|
|
19
|
+
template "app/jobs/sqlite_maintenance_job.rb"
|
|
20
|
+
|
|
21
|
+
if gem_in_bundle?("solid_queue")
|
|
22
|
+
unless add_recurring_tasks(maintenance_task_yaml, marker: "sqlite_maintenance")
|
|
23
|
+
log :skip, "could not schedule (config/recurring.yml missing or already scheduled)"
|
|
24
|
+
end
|
|
25
|
+
else
|
|
26
|
+
log :info, "solid_queue not found — job installed but not scheduled. Add a 'sqlite_maintenance' entry to your scheduler."
|
|
27
|
+
end
|
|
28
|
+
rescue => e
|
|
29
|
+
exception "#{self.class} failed:", e
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def maintenance_task_yaml
|
|
35
|
+
<<~YAML
|
|
36
|
+
sqlite_maintenance:
|
|
37
|
+
class: SqliteMaintenanceJob
|
|
38
|
+
queue: default
|
|
39
|
+
schedule: "#{options[:schedule]}"
|
|
40
|
+
description: "VACUUM + PRAGMA optimize across SQLite databases"
|
|
41
|
+
YAML
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
class SqliteMaintenanceJob < ApplicationJob
|
|
2
|
+
queue_as :default
|
|
3
|
+
|
|
4
|
+
# Isolated connection for maintenance. Establishing on this dedicated
|
|
5
|
+
# abstract class (instead of ActiveRecord::Base) means we never mutate
|
|
6
|
+
# the global primary connection — a sibling job on the other worker
|
|
7
|
+
# thread keeps talking to the right database.
|
|
8
|
+
class MaintenanceConnection < ActiveRecord::Base
|
|
9
|
+
self.abstract_class = true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Names match the keys in config/database.yml. Add your own database
|
|
13
|
+
# names here if you run extra SQLite databases.
|
|
14
|
+
#
|
|
15
|
+
# PRAGMA optimize is cheap (just refreshes query-planner stats, brief
|
|
16
|
+
# shared lock) so it runs everywhere. Full VACUUM rewrites the file
|
|
17
|
+
# under a global *exclusive* lock for its whole duration, so it only
|
|
18
|
+
# runs on databases without live 24/7 writers.
|
|
19
|
+
OPTIMIZE_DBS = %w[primary queue cache cable errors rails_pulse].freeze
|
|
20
|
+
|
|
21
|
+
# queue/cable/cache are deliberately excluded: SolidQueue, Solid Cable
|
|
22
|
+
# and Solid Cache write to them constantly, and a VACUUM lock there
|
|
23
|
+
# stalls (and errors out) those processes — e.g. SolidQueue's process
|
|
24
|
+
# deregistration hitting "database is locked". They also barely benefit:
|
|
25
|
+
# in WAL mode deleted pages land on the freelist and get reused, so a
|
|
26
|
+
# churning DB sits at a steady-state size without nightly reclamation.
|
|
27
|
+
VACUUM_DBS = %w[primary errors rails_pulse].freeze
|
|
28
|
+
|
|
29
|
+
def perform
|
|
30
|
+
OPTIMIZE_DBS.each { |db_name| run_maintenance(db_name) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def run_maintenance(db_name)
|
|
36
|
+
config = ActiveRecord::Base.configurations.configs_for(
|
|
37
|
+
env_name: Rails.env,
|
|
38
|
+
name: db_name,
|
|
39
|
+
include_hidden: true
|
|
40
|
+
)
|
|
41
|
+
return unless config
|
|
42
|
+
|
|
43
|
+
MaintenanceConnection.establish_connection(config)
|
|
44
|
+
MaintenanceConnection.connection_pool.with_connection do |conn|
|
|
45
|
+
Rails.logger.info { "[SqliteMaintenance] PRAGMA optimize on #{db_name}" }
|
|
46
|
+
conn.execute("PRAGMA optimize")
|
|
47
|
+
|
|
48
|
+
next unless VACUUM_DBS.include?(db_name)
|
|
49
|
+
|
|
50
|
+
Rails.logger.info { "[SqliteMaintenance] VACUUM on #{db_name}" }
|
|
51
|
+
started = Time.current
|
|
52
|
+
conn.execute("VACUUM")
|
|
53
|
+
Rails.logger.info { "[SqliteMaintenance] VACUUM on #{db_name} done in #{(Time.current - started).round(2)}s" }
|
|
54
|
+
end
|
|
55
|
+
rescue => e
|
|
56
|
+
Rails.error.report(e, context: {db: db_name, action: "sqlite_maintenance"})
|
|
57
|
+
ensure
|
|
58
|
+
MaintenanceConnection.remove_connection
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -7,6 +7,7 @@ module Pu
|
|
|
7
7
|
class RailsPulseGenerator < Rails::Generators::Base
|
|
8
8
|
include PlutoniumGenerators::Generator
|
|
9
9
|
include PlutoniumGenerators::Concerns::ConfiguresSqlite
|
|
10
|
+
include PlutoniumGenerators::Concerns::ConfiguresRecurring
|
|
10
11
|
include PlutoniumGenerators::Concerns::MountsEngines
|
|
11
12
|
|
|
12
13
|
source_root File.expand_path("templates", __dir__)
|
|
@@ -69,25 +70,11 @@ module Pu
|
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
def setup_recurring_tasks
|
|
72
|
-
|
|
73
|
-
full_path = File.expand_path(recurring_file, destination_root)
|
|
74
|
-
return unless File.exist?(full_path)
|
|
75
|
-
return if file_includes?(recurring_file, "rails_pulse")
|
|
76
|
-
|
|
77
|
-
content = File.read(full_path)
|
|
78
|
-
env_keys = %w[production development staging test]
|
|
79
|
-
env_scoped = content.lines.any? { |l| l.match?(/^(#{env_keys.join("|")}):\s*$/) }
|
|
80
|
-
|
|
81
|
-
if env_scoped
|
|
82
|
-
create_file recurring_file, inject_rails_pulse_under_envs(content, env_keys), force: true
|
|
83
|
-
else
|
|
84
|
-
append_to_file recurring_file, "\n" + rails_pulse_tasks_yaml(0)
|
|
85
|
-
end
|
|
73
|
+
add_recurring_tasks(rails_pulse_tasks_yaml, marker: "rails_pulse")
|
|
86
74
|
end
|
|
87
75
|
|
|
88
|
-
def rails_pulse_tasks_yaml
|
|
89
|
-
|
|
90
|
-
<<~YAML.gsub(/^(?=.)/, pad)
|
|
76
|
+
def rails_pulse_tasks_yaml
|
|
77
|
+
<<~YAML
|
|
91
78
|
rails_pulse_summary:
|
|
92
79
|
class: RailsPulse::SummaryJob
|
|
93
80
|
queue: default
|
|
@@ -101,40 +88,6 @@ module Pu
|
|
|
101
88
|
description: "Archive/purge old Rails Pulse data"
|
|
102
89
|
YAML
|
|
103
90
|
end
|
|
104
|
-
|
|
105
|
-
def inject_rails_pulse_under_envs(content, env_keys)
|
|
106
|
-
lines = content.lines
|
|
107
|
-
env_re = /^(#{env_keys.join("|")}):\s*$/
|
|
108
|
-
|
|
109
|
-
env_starts = lines.each_with_index.select { |l, _| env_re.match?(l) }.map(&:last)
|
|
110
|
-
|
|
111
|
-
env_starts.reverse_each do |start|
|
|
112
|
-
end_idx = lines.length
|
|
113
|
-
((start + 1)...lines.length).each do |i|
|
|
114
|
-
if lines[i].match?(/^[^\s#]/)
|
|
115
|
-
end_idx = i
|
|
116
|
-
break
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
indent = 2
|
|
121
|
-
((start + 1)...end_idx).each do |i|
|
|
122
|
-
if (m = lines[i].match(/^(\s+)\S/))
|
|
123
|
-
indent = m[1].length
|
|
124
|
-
break
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
insert_at = end_idx
|
|
129
|
-
while insert_at > start + 1 && lines[insert_at - 1].strip.empty?
|
|
130
|
-
insert_at -= 1
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
lines.insert(insert_at, "\n", rails_pulse_tasks_yaml(indent))
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
lines.join
|
|
137
|
-
end
|
|
138
91
|
end
|
|
139
92
|
end
|
|
140
93
|
end
|
|
@@ -27,6 +27,6 @@ RailsPulse.configure do |config|
|
|
|
27
27
|
<%- if options[:database] -%>
|
|
28
28
|
|
|
29
29
|
# Use separate database for performance data
|
|
30
|
-
config.connects_to = {database: {writing: :<%= options[:database] %>}}
|
|
30
|
+
config.connects_to = {database: {writing: :<%= options[:database] %>, reading: :<%= options[:database] %>}}
|
|
31
31
|
<%- end -%>
|
|
32
32
|
end
|