lean_cms 0.2.12
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 +235 -0
- data/LICENSE +21 -0
- data/README.md +107 -0
- data/app/assets/images/lean_cms/sloth-404.png +0 -0
- data/app/assets/images/lean_cms/sloth-500.png +0 -0
- data/app/assets/images/lean_cms/sloth-favicon-16.png +0 -0
- data/app/assets/images/lean_cms/sloth-favicon-32.png +0 -0
- data/app/assets/images/lean_cms/sloth-favicon-64.png +0 -0
- data/app/assets/images/lean_cms/sloth-logo.png +0 -0
- data/app/assets/lean_cms/actiontext.css +440 -0
- data/app/assets/lean_cms/cms_edit_controls.css +548 -0
- data/app/assets/tailwind/lean_cms/engine.css +14 -0
- data/app/components/lean_cms/base_component.rb +61 -0
- data/app/components/lean_cms/bullets_section_component.html.erb +23 -0
- data/app/components/lean_cms/bullets_section_component.rb +54 -0
- data/app/components/lean_cms/cards_section_component.html.erb +237 -0
- data/app/components/lean_cms/cards_section_component.rb +71 -0
- data/app/components/lean_cms/editable_content_component.html.erb +15 -0
- data/app/components/lean_cms/editable_content_component.rb +53 -0
- data/app/components/lean_cms/section_component.html.erb +18 -0
- data/app/components/lean_cms/section_component.rb +35 -0
- data/app/controllers/concerns/lean_cms/authentication.rb +60 -0
- data/app/controllers/concerns/lean_cms/authorization.rb +60 -0
- data/app/controllers/lean_cms/activity_controller.rb +16 -0
- data/app/controllers/lean_cms/application_controller.rb +48 -0
- data/app/controllers/lean_cms/dashboard_controller.rb +13 -0
- data/app/controllers/lean_cms/form_submissions_controller.rb +37 -0
- data/app/controllers/lean_cms/notification_settings_controller.rb +145 -0
- data/app/controllers/lean_cms/notifications_controller.rb +26 -0
- data/app/controllers/lean_cms/page_contents_controller.rb +403 -0
- data/app/controllers/lean_cms/password_setup_controller.rb +65 -0
- data/app/controllers/lean_cms/passwords_controller.rb +42 -0
- data/app/controllers/lean_cms/posts_controller.rb +78 -0
- data/app/controllers/lean_cms/sessions_controller.rb +50 -0
- data/app/controllers/lean_cms/settings_controller.rb +124 -0
- data/app/controllers/lean_cms/users_controller.rb +113 -0
- data/app/helpers/lean_cms/activity_helper.rb +190 -0
- data/app/helpers/lean_cms/application_helper.rb +43 -0
- data/app/helpers/lean_cms/content_helper.rb +34 -0
- data/app/helpers/lean_cms/page_content_helper.rb +359 -0
- data/app/javascript/controllers/cards_editor_controller.js +317 -0
- data/app/javascript/controllers/cms_sticky_overlay_controller.js +59 -0
- data/app/javascript/controllers/field_editor_form_controller.js +68 -0
- data/app/javascript/controllers/field_editor_modal_controller.js +79 -0
- data/app/javascript/controllers/inline_edit_controller.js +414 -0
- data/app/javascript/controllers/inline_edit_toggle_controller.js +81 -0
- data/app/javascript/controllers/notifications_controller.js +19 -0
- data/app/javascript/controllers/settings_inline_edit_sync_controller.js +38 -0
- data/app/javascript/controllers/settings_override_controller.js +45 -0
- data/app/mailers/lean_cms/application_mailer.rb +6 -0
- data/app/mailers/lean_cms/passwords_mailer.rb +8 -0
- data/app/mailers/lean_cms/users_mailer.rb +39 -0
- data/app/models/lean_cms/current.rb +6 -0
- data/app/models/lean_cms/form_submission.rb +45 -0
- data/app/models/lean_cms/magic_link.rb +76 -0
- data/app/models/lean_cms/meta_tag.rb +30 -0
- data/app/models/lean_cms/notification_setting.rb +69 -0
- data/app/models/lean_cms/page.rb +23 -0
- data/app/models/lean_cms/page_content.rb +245 -0
- data/app/models/lean_cms/post.rb +65 -0
- data/app/models/lean_cms/session.rb +7 -0
- data/app/models/lean_cms/setting.rb +156 -0
- data/app/policies/lean_cms/application_policy.rb +35 -0
- data/app/policies/lean_cms/page_content_policy.rb +31 -0
- data/app/policies/lean_cms/post_policy.rb +37 -0
- data/app/policies/lean_cms/setting_policy.rb +17 -0
- data/app/views/layouts/lean_cms/application.html.erb +114 -0
- data/app/views/layouts/lean_cms/auth.html.erb +200 -0
- data/app/views/lean_cms/activity/index.html.erb +79 -0
- data/app/views/lean_cms/dashboard/index.html.erb +180 -0
- data/app/views/lean_cms/form_submissions/index.html.erb +104 -0
- data/app/views/lean_cms/form_submissions/show.html.erb +157 -0
- data/app/views/lean_cms/notification_settings/edit.html.erb +192 -0
- data/app/views/lean_cms/notifications/index.html.erb +72 -0
- data/app/views/lean_cms/notifications/show.html.erb +39 -0
- data/app/views/lean_cms/page_contents/_field_editor.html.erb +174 -0
- data/app/views/lean_cms/page_contents/edit.html.erb +428 -0
- data/app/views/lean_cms/page_contents/index.html.erb +113 -0
- data/app/views/lean_cms/password_setup/show.html.erb +35 -0
- data/app/views/lean_cms/passwords/edit.html.erb +26 -0
- data/app/views/lean_cms/passwords/new.html.erb +21 -0
- data/app/views/lean_cms/passwords_mailer/reset.html.erb +6 -0
- data/app/views/lean_cms/passwords_mailer/reset.text.erb +4 -0
- data/app/views/lean_cms/posts/_form.html.erb +118 -0
- data/app/views/lean_cms/posts/edit.html.erb +31 -0
- data/app/views/lean_cms/posts/index.html.erb +100 -0
- data/app/views/lean_cms/posts/new.html.erb +16 -0
- data/app/views/lean_cms/sessions/new.html.erb +28 -0
- data/app/views/lean_cms/settings/edit.html.erb +384 -0
- data/app/views/lean_cms/shared/_admin_bar.html.erb +85 -0
- data/app/views/lean_cms/shared/_header.html.erb +86 -0
- data/app/views/lean_cms/shared/_notifications_bell.html.erb +84 -0
- data/app/views/lean_cms/shared/_sidebar.html.erb +102 -0
- data/app/views/lean_cms/users/_form.html.erb +105 -0
- data/app/views/lean_cms/users/edit.html.erb +8 -0
- data/app/views/lean_cms/users/index.html.erb +99 -0
- data/app/views/lean_cms/users/new.html.erb +8 -0
- data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.html.erb +13 -0
- data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.text.erb +11 -0
- data/app/views/lean_cms/users_mailer/invitation.html.erb +13 -0
- data/app/views/lean_cms/users_mailer/invitation.text.erb +11 -0
- data/app/views/lean_cms/users_mailer/reactivation.html.erb +13 -0
- data/app/views/lean_cms/users_mailer/reactivation.text.erb +11 -0
- data/config/importmap.rb +8 -0
- data/config/routes.rb +78 -0
- data/db/migrate/20251112034030_create_lean_cms_tables.rb +131 -0
- data/db/migrate/20260513000001_create_lean_cms_auth_tables.rb +31 -0
- data/db/migrate/20260514000001_create_paper_trail_versions.rb +16 -0
- data/db/migrate/20260514000002_create_action_text_tables.rb +18 -0
- data/db/migrate/20260514000003_create_active_storage_tables.rb +45 -0
- data/db/migrate/20260514000004_create_noticed_tables.rb +27 -0
- data/lib/generators/lean_cms/demo/demo_generator.rb +54 -0
- data/lib/generators/lean_cms/demo/templates/lean_cms_structure.yml +129 -0
- data/lib/generators/lean_cms/demo/templates/pages_controller.rb +30 -0
- data/lib/generators/lean_cms/demo/templates/views/pages/about.html.erb +40 -0
- data/lib/generators/lean_cms/demo/templates/views/pages/contact.html.erb +55 -0
- data/lib/generators/lean_cms/demo/templates/views/pages/home.html.erb +31 -0
- data/lib/generators/lean_cms/install/install_generator.rb +317 -0
- data/lib/generators/lean_cms/install/templates/add_lean_cms_columns_to_users.rb.tt +7 -0
- data/lib/generators/lean_cms/install/templates/lean_cms.rb +11 -0
- data/lib/generators/lean_cms/install/templates/lean_cms_structure.yml +29 -0
- data/lib/lean_cms/configuration.rb +32 -0
- data/lib/lean_cms/engine.rb +93 -0
- data/lib/lean_cms/loader.rb +217 -0
- data/lib/lean_cms/sync_helper.rb +182 -0
- data/lib/lean_cms/version.rb +3 -0
- data/lib/lean_cms.rb +26 -0
- data/lib/tasks/lean_cms.rake +390 -0
- metadata +313 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
namespace :lean_cms do
|
|
2
|
+
# Plain-text stdout logger for the Rake tasks — no timestamps or severity
|
|
3
|
+
# tags, since these are interactive CLI commands. Background-job callers
|
|
4
|
+
# use Rails.logger (or whatever they pass in) instead.
|
|
5
|
+
rake_logger = lambda do
|
|
6
|
+
logger = ActiveSupport::Logger.new($stdout)
|
|
7
|
+
logger.formatter = ->(_severity, _time, _progname, msg) { "#{msg}\n" }
|
|
8
|
+
logger
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc "Load page content structure from YAML file"
|
|
12
|
+
task load_structure: :environment do
|
|
13
|
+
LeanCms::Loader.new(logger: rake_logger.call).load!
|
|
14
|
+
rescue LeanCms::Loader::StructureFileMissing => e
|
|
15
|
+
warn "Error: #{e.message}"
|
|
16
|
+
exit 1
|
|
17
|
+
rescue LeanCms::Loader::NoUsersFound => e
|
|
18
|
+
warn "Error: #{e.message} Run `bin/rails generate authentication` (or your auth gem's install) first."
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "Clear all page content (WARNING: destructive)"
|
|
23
|
+
task clear_content: :environment do
|
|
24
|
+
print "Are you sure you want to delete ALL page content? (yes/no): "
|
|
25
|
+
confirmation = STDIN.gets.chomp
|
|
26
|
+
|
|
27
|
+
if confirmation.downcase == 'yes'
|
|
28
|
+
count = LeanCms::PageContent.count
|
|
29
|
+
LeanCms::PageContent.destroy_all
|
|
30
|
+
puts "Deleted #{count} page content records"
|
|
31
|
+
else
|
|
32
|
+
puts "Operation cancelled"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "Reload structure (clear and load)"
|
|
37
|
+
task reload_structure: :environment do
|
|
38
|
+
Rake::Task['lean_cms:clear_content'].invoke
|
|
39
|
+
Rake::Task['lean_cms:load_structure'].invoke
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc "Export current LeanCms::PageContent records to YAML (config/lean_cms_structure_export.yml)"
|
|
43
|
+
task export_structure: :environment do
|
|
44
|
+
require "yaml"
|
|
45
|
+
|
|
46
|
+
output_path = ENV["OUTPUT"] || Rails.root.join("config", "lean_cms_structure_export.yml").to_s
|
|
47
|
+
|
|
48
|
+
pages_yaml = {}
|
|
49
|
+
|
|
50
|
+
page_keys = LeanCms::PageContent.distinct.order(:page_order, :page).pluck(:page)
|
|
51
|
+
|
|
52
|
+
page_keys.each do |page_key|
|
|
53
|
+
page_scope = LeanCms::PageContent.where(page: page_key)
|
|
54
|
+
first = page_scope.order(:page_order).first
|
|
55
|
+
|
|
56
|
+
page_yaml = {
|
|
57
|
+
"display_title" => first.page_display_title.presence || page_key.titleize,
|
|
58
|
+
"page_order" => first.page_order || 0,
|
|
59
|
+
"sections" => {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
section_keys = page_scope.distinct.order(:section_order, :section).pluck(:section)
|
|
63
|
+
section_keys.each do |section_key|
|
|
64
|
+
section_scope = page_scope.where(section: section_key)
|
|
65
|
+
section_first = section_scope.order(:section_order).first
|
|
66
|
+
|
|
67
|
+
section_yaml = {
|
|
68
|
+
"display_title" => section_first.display_title.presence || section_key.titleize,
|
|
69
|
+
"section_order" => section_first.section_order || 0,
|
|
70
|
+
"fields" => {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
section_scope.order(:position, :key).each do |record|
|
|
74
|
+
case record.content_type
|
|
75
|
+
when "cards"
|
|
76
|
+
items = JSON.parse(record.content.to_s) rescue []
|
|
77
|
+
section_yaml["cards"] = {
|
|
78
|
+
"type" => "cards",
|
|
79
|
+
"max_cards" => record.options.is_a?(Hash) ? record.options["max_cards"] : nil,
|
|
80
|
+
"items" => items
|
|
81
|
+
}.compact
|
|
82
|
+
when "bullets"
|
|
83
|
+
items = JSON.parse(record.content.to_s) rescue []
|
|
84
|
+
section_yaml["bullets"] = {
|
|
85
|
+
"type" => "bullets",
|
|
86
|
+
"max_items" => record.options.is_a?(Hash) ? record.options["max_items"] : nil,
|
|
87
|
+
"items" => items
|
|
88
|
+
}.compact
|
|
89
|
+
else
|
|
90
|
+
field_yaml = { "type" => record.content_type }
|
|
91
|
+
field_yaml["label"] = record.label if record.label.present?
|
|
92
|
+
field_yaml["position"] = record.position if record.position.to_i != 0
|
|
93
|
+
|
|
94
|
+
# Current production value becomes the seeded default in fresh environments.
|
|
95
|
+
field_yaml["default"] =
|
|
96
|
+
if record.content_type == "rich_text"
|
|
97
|
+
record.rich_content&.to_s
|
|
98
|
+
elsif record.content_type == "boolean"
|
|
99
|
+
record.value == "true"
|
|
100
|
+
else
|
|
101
|
+
record.value
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
field_yaml.compact!
|
|
105
|
+
|
|
106
|
+
if record.options.is_a?(Hash)
|
|
107
|
+
field_yaml["max_length"] = record.options["max_length"] if record.options["max_length"]
|
|
108
|
+
field_yaml["options"] = record.options["options"] if record.options["options"]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
section_yaml["fields"][record.key] = field_yaml
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
page_yaml["sections"][section_key] = section_yaml
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
pages_yaml[page_key] = page_yaml
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
File.write(output_path, { "pages" => pages_yaml }.to_yaml)
|
|
122
|
+
puts "Exported #{LeanCms::PageContent.count} content records across #{page_keys.size} pages to:"
|
|
123
|
+
puts " #{output_path}"
|
|
124
|
+
puts ""
|
|
125
|
+
puts "Note: image attachments are not included in the YAML export. Re-attach them"
|
|
126
|
+
puts " via the CMS UI or copy ActiveStorage blobs separately."
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
desc "Show page content stats"
|
|
130
|
+
task stats: :environment do
|
|
131
|
+
puts "LeanCMS Page Content Statistics"
|
|
132
|
+
puts "=" * 60
|
|
133
|
+
|
|
134
|
+
pages = LeanCms::PageContent.distinct.pluck(:page)
|
|
135
|
+
|
|
136
|
+
pages.each do |page|
|
|
137
|
+
sections = LeanCms::PageContent.where(page: page).distinct.pluck(:section)
|
|
138
|
+
total_fields = LeanCms::PageContent.where(page: page).count
|
|
139
|
+
|
|
140
|
+
puts "\nPage: #{page.upcase}"
|
|
141
|
+
puts " Sections: #{sections.count}"
|
|
142
|
+
puts " Total fields: #{total_fields}"
|
|
143
|
+
|
|
144
|
+
sections.each do |section|
|
|
145
|
+
field_count = LeanCms::PageContent.where(page: page, section: section).count
|
|
146
|
+
puts " - #{section}: #{field_count} fields"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
puts "\n" + "=" * 60
|
|
151
|
+
puts "Total: #{LeanCms::PageContent.count} content fields across #{pages.count} pages"
|
|
152
|
+
puts "=" * 60
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# ============================================================================
|
|
156
|
+
# Content Sync Tasks
|
|
157
|
+
# These tasks enable a safe workflow for syncing SQLite database between
|
|
158
|
+
# local development and production environments.
|
|
159
|
+
# ============================================================================
|
|
160
|
+
|
|
161
|
+
# Guard for sync tasks that copy SQLite database files directly
|
|
162
|
+
# (pull / push / stage / start / finish). Lock / unlock / status are
|
|
163
|
+
# adapter-agnostic and don't need this — they only toggle a Setting.
|
|
164
|
+
sqlite_only = lambda do
|
|
165
|
+
adapter = ActiveRecord::Base.connection_db_config.adapter
|
|
166
|
+
return if adapter == "sqlite3"
|
|
167
|
+
|
|
168
|
+
warn <<~MSG
|
|
169
|
+
lean_cms:sync:* file-copy tasks (pull / push / stage / start / finish)
|
|
170
|
+
assume a SQLite database. The current connection uses #{adapter.inspect}.
|
|
171
|
+
|
|
172
|
+
Use your database's native dump/restore tooling instead:
|
|
173
|
+
Postgres: pg_dump / pg_restore
|
|
174
|
+
MySQL: mysqldump
|
|
175
|
+
|
|
176
|
+
The lock / unlock / status tasks work on any adapter and remain available.
|
|
177
|
+
MSG
|
|
178
|
+
exit 1
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
namespace :sync do
|
|
182
|
+
desc "Lock content editing on this instance"
|
|
183
|
+
task lock: :environment do
|
|
184
|
+
reason = ENV['REASON'] || 'Content sync in progress'
|
|
185
|
+
|
|
186
|
+
if LeanCms::Setting.content_locked?
|
|
187
|
+
puts "Content is already locked."
|
|
188
|
+
lock_info = LeanCms::Setting.content_lock_info
|
|
189
|
+
puts " Locked at: #{lock_info[:locked_at]}"
|
|
190
|
+
puts " Reason: #{lock_info[:reason]}"
|
|
191
|
+
else
|
|
192
|
+
LeanCms::Setting.lock_content!(reason)
|
|
193
|
+
puts "Content editing has been LOCKED."
|
|
194
|
+
puts " Reason: #{reason}"
|
|
195
|
+
puts " Locked at: #{Time.current}"
|
|
196
|
+
puts "\nEditors will not be able to make changes until unlocked."
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
desc "Unlock content editing on this instance"
|
|
201
|
+
task unlock: :environment do
|
|
202
|
+
if LeanCms::Setting.content_locked?
|
|
203
|
+
LeanCms::Setting.unlock_content!
|
|
204
|
+
puts "Content editing has been UNLOCKED."
|
|
205
|
+
puts "Editors can now make changes."
|
|
206
|
+
else
|
|
207
|
+
puts "Content is not locked."
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
desc "Show current lock status"
|
|
212
|
+
task status: :environment do
|
|
213
|
+
if LeanCms::Setting.content_locked?
|
|
214
|
+
lock_info = LeanCms::Setting.content_lock_info
|
|
215
|
+
puts "Content Status: LOCKED"
|
|
216
|
+
puts " Locked at: #{lock_info[:locked_at]}"
|
|
217
|
+
puts " Reason: #{lock_info[:reason]}"
|
|
218
|
+
else
|
|
219
|
+
puts "Content Status: UNLOCKED"
|
|
220
|
+
puts "Editors can make changes."
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
desc "Pull production database to local (run locally, SQLite only)"
|
|
225
|
+
task pull: :environment do
|
|
226
|
+
sqlite_only.call
|
|
227
|
+
require_relative '../lean_cms/sync_helper'
|
|
228
|
+
LeanCms::SyncHelper.pull_from_production
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
desc "Push local database to production (run locally, SQLite only)"
|
|
232
|
+
task push: :environment do
|
|
233
|
+
sqlite_only.call
|
|
234
|
+
require_relative '../lean_cms/sync_helper'
|
|
235
|
+
LeanCms::SyncHelper.push_to_production
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
desc "Stage development DB as production_local for local production testing (SQLite only)"
|
|
239
|
+
task stage: :environment do
|
|
240
|
+
sqlite_only.call
|
|
241
|
+
|
|
242
|
+
dev_db = Rails.root.join('storage', 'development.sqlite3').to_s
|
|
243
|
+
prod_local = Rails.root.join('storage', 'production_local.sqlite3').to_s
|
|
244
|
+
|
|
245
|
+
unless File.exist?(dev_db)
|
|
246
|
+
abort "Development database not found at #{dev_db}"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
puts "Staging development DB for local production testing..."
|
|
250
|
+
puts "=" * 60
|
|
251
|
+
|
|
252
|
+
# Checkpoint WAL on the dev DB first so the copy is consistent
|
|
253
|
+
puts " Checkpointing development database..."
|
|
254
|
+
system("sqlite3 #{dev_db} 'PRAGMA wal_checkpoint(TRUNCATE);'")
|
|
255
|
+
FileUtils.rm_f("#{dev_db}-shm")
|
|
256
|
+
FileUtils.rm_f("#{dev_db}-wal")
|
|
257
|
+
|
|
258
|
+
# Back up any existing production_local
|
|
259
|
+
if File.exist?(prod_local)
|
|
260
|
+
backup = "#{prod_local}.backup.#{Time.now.strftime('%Y%m%d_%H%M%S')}"
|
|
261
|
+
FileUtils.cp(prod_local, backup)
|
|
262
|
+
puts " Backed up existing production_local to: #{File.basename(backup)}"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
FileUtils.cp(dev_db, prod_local)
|
|
266
|
+
FileUtils.rm_f("#{prod_local}-shm")
|
|
267
|
+
FileUtils.rm_f("#{prod_local}-wal")
|
|
268
|
+
|
|
269
|
+
puts " Copied development.sqlite3 → production_local.sqlite3"
|
|
270
|
+
puts "\n" + "=" * 60
|
|
271
|
+
puts "Done! Start Rails in production mode against the local DB:"
|
|
272
|
+
puts ""
|
|
273
|
+
puts " RAILS_ENV=production DATABASE_URL=sqlite3:storage/production_local.sqlite3 bin/rails server"
|
|
274
|
+
puts ""
|
|
275
|
+
puts "When happy with the result, push to the real server:"
|
|
276
|
+
puts ""
|
|
277
|
+
puts " bin/rails lean_cms:sync:push"
|
|
278
|
+
puts "=" * 60
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
desc "Full sync workflow: lock -> pull -> (make changes) -> push -> unlock"
|
|
282
|
+
task :workflow do
|
|
283
|
+
puts "LeanCMS Content Sync Workflow"
|
|
284
|
+
puts "=" * 60
|
|
285
|
+
puts "\nNew project / first deploy:"
|
|
286
|
+
puts " 1. Develop in development DB (default)"
|
|
287
|
+
puts " 2. Stage for local production test: bin/rails lean_cms:sync:stage"
|
|
288
|
+
puts " 3. Start production server locally: RAILS_ENV=production DATABASE_URL=sqlite3:storage/production_local.sqlite3 bin/rails server"
|
|
289
|
+
puts " 4. Push to real server: bin/rails lean_cms:sync:push"
|
|
290
|
+
puts ""
|
|
291
|
+
puts "Ongoing sync workflow:"
|
|
292
|
+
puts " 1. Lock production: bin/kamal cms-lock"
|
|
293
|
+
puts " 2. Pull database: bin/rails lean_cms:sync:pull"
|
|
294
|
+
puts " 3. Make local changes"
|
|
295
|
+
puts " 4. Push database: bin/rails lean_cms:sync:push"
|
|
296
|
+
puts " 5. Unlock production: bin/kamal cms-unlock"
|
|
297
|
+
puts "\nOr use the combined commands:"
|
|
298
|
+
puts " bin/rails lean_cms:sync:start - Lock and pull"
|
|
299
|
+
puts " bin/rails lean_cms:sync:finish - Push and unlock"
|
|
300
|
+
puts "=" * 60
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
desc "Start sync: lock production and pull database (SQLite only)"
|
|
304
|
+
task start: :environment do
|
|
305
|
+
sqlite_only.call
|
|
306
|
+
puts "Starting content sync workflow..."
|
|
307
|
+
puts "=" * 60
|
|
308
|
+
|
|
309
|
+
# Lock production via Kamal
|
|
310
|
+
puts "\n1. Locking production..."
|
|
311
|
+
system("kamal app exec 'bin/rails lean_cms:sync:lock'") || abort("Failed to lock production")
|
|
312
|
+
|
|
313
|
+
# Pull database
|
|
314
|
+
puts "\n2. Pulling production database..."
|
|
315
|
+
Rake::Task['lean_cms:sync:pull'].invoke
|
|
316
|
+
|
|
317
|
+
puts "\n" + "=" * 60
|
|
318
|
+
puts "Sync started! Production is locked."
|
|
319
|
+
puts "Make your changes locally, then run: bin/rails lean_cms:sync:finish"
|
|
320
|
+
puts "=" * 60
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
desc "Finish sync: push database and unlock production (SQLite only)"
|
|
324
|
+
task finish: :environment do
|
|
325
|
+
sqlite_only.call
|
|
326
|
+
puts "Finishing content sync workflow..."
|
|
327
|
+
puts "=" * 60
|
|
328
|
+
|
|
329
|
+
# Push database
|
|
330
|
+
puts "\n1. Pushing local database to production..."
|
|
331
|
+
Rake::Task['lean_cms:sync:push'].invoke
|
|
332
|
+
|
|
333
|
+
# Unlock production via Kamal
|
|
334
|
+
puts "\n2. Unlocking production..."
|
|
335
|
+
system("kamal app exec 'bin/rails lean_cms:sync:unlock'") || abort("Failed to unlock production")
|
|
336
|
+
|
|
337
|
+
puts "\n" + "=" * 60
|
|
338
|
+
puts "Sync complete! Production is unlocked and updated."
|
|
339
|
+
puts "=" * 60
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
desc "Optimize images: generate WebP + fallback variants from app/assets/images/source/"
|
|
344
|
+
task optimize_images: :environment do
|
|
345
|
+
require "image_processing/vips"
|
|
346
|
+
|
|
347
|
+
source_dir = Rails.root.join("app/assets/images/source")
|
|
348
|
+
output_dir = Rails.root.join("app/assets/images")
|
|
349
|
+
|
|
350
|
+
unless source_dir.directory?
|
|
351
|
+
puts "No source directory at #{source_dir}."
|
|
352
|
+
puts "Create app/assets/images/source/, place originals there, and re-run."
|
|
353
|
+
exit 0
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
widths = (ENV["WIDTHS"] || "640,1280,1920").split(",").map(&:to_i)
|
|
357
|
+
webp_quality = (ENV["WEBP_QUALITY"] || "80").to_i
|
|
358
|
+
jpeg_quality = (ENV["JPEG_QUALITY"] || "85").to_i
|
|
359
|
+
written = skipped = 0
|
|
360
|
+
|
|
361
|
+
Dir.glob(source_dir.join("*.{jpg,jpeg,png}"), File::FNM_CASEFOLD).uniq.sort.each do |source|
|
|
362
|
+
base = File.basename(source, ".*")
|
|
363
|
+
source_ext = File.extname(source).delete(".").downcase
|
|
364
|
+
fallback = source_ext == "jpeg" ? "jpg" : source_ext
|
|
365
|
+
src_kb = File.size(source) / 1024
|
|
366
|
+
|
|
367
|
+
puts "#{base} (#{src_kb} KB #{source_ext})"
|
|
368
|
+
|
|
369
|
+
widths.each do |w|
|
|
370
|
+
[["webp", webp_quality], [fallback, fallback == "jpg" ? jpeg_quality : nil]].each do |fmt, q|
|
|
371
|
+
out = output_dir.join("#{base}-#{w}.#{fmt}")
|
|
372
|
+
if out.exist? && out.mtime >= File.mtime(source)
|
|
373
|
+
skipped += 1
|
|
374
|
+
next
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
pipeline = ImageProcessing::Vips.source(source).resize_to_limit(w, nil)
|
|
378
|
+
pipeline = pipeline.saver(quality: q) if q
|
|
379
|
+
pipeline.convert(fmt).call(destination: out.to_s)
|
|
380
|
+
|
|
381
|
+
puts " -> #{w}w #{fmt.upcase}: #{File.size(out) / 1024} KB"
|
|
382
|
+
written += 1
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
puts ""
|
|
388
|
+
puts "Wrote #{written} files, skipped #{skipped} up-to-date."
|
|
389
|
+
end
|
|
390
|
+
end
|