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.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +235 -0
  3. data/LICENSE +21 -0
  4. data/README.md +107 -0
  5. data/app/assets/images/lean_cms/sloth-404.png +0 -0
  6. data/app/assets/images/lean_cms/sloth-500.png +0 -0
  7. data/app/assets/images/lean_cms/sloth-favicon-16.png +0 -0
  8. data/app/assets/images/lean_cms/sloth-favicon-32.png +0 -0
  9. data/app/assets/images/lean_cms/sloth-favicon-64.png +0 -0
  10. data/app/assets/images/lean_cms/sloth-logo.png +0 -0
  11. data/app/assets/lean_cms/actiontext.css +440 -0
  12. data/app/assets/lean_cms/cms_edit_controls.css +548 -0
  13. data/app/assets/tailwind/lean_cms/engine.css +14 -0
  14. data/app/components/lean_cms/base_component.rb +61 -0
  15. data/app/components/lean_cms/bullets_section_component.html.erb +23 -0
  16. data/app/components/lean_cms/bullets_section_component.rb +54 -0
  17. data/app/components/lean_cms/cards_section_component.html.erb +237 -0
  18. data/app/components/lean_cms/cards_section_component.rb +71 -0
  19. data/app/components/lean_cms/editable_content_component.html.erb +15 -0
  20. data/app/components/lean_cms/editable_content_component.rb +53 -0
  21. data/app/components/lean_cms/section_component.html.erb +18 -0
  22. data/app/components/lean_cms/section_component.rb +35 -0
  23. data/app/controllers/concerns/lean_cms/authentication.rb +60 -0
  24. data/app/controllers/concerns/lean_cms/authorization.rb +60 -0
  25. data/app/controllers/lean_cms/activity_controller.rb +16 -0
  26. data/app/controllers/lean_cms/application_controller.rb +48 -0
  27. data/app/controllers/lean_cms/dashboard_controller.rb +13 -0
  28. data/app/controllers/lean_cms/form_submissions_controller.rb +37 -0
  29. data/app/controllers/lean_cms/notification_settings_controller.rb +145 -0
  30. data/app/controllers/lean_cms/notifications_controller.rb +26 -0
  31. data/app/controllers/lean_cms/page_contents_controller.rb +403 -0
  32. data/app/controllers/lean_cms/password_setup_controller.rb +65 -0
  33. data/app/controllers/lean_cms/passwords_controller.rb +42 -0
  34. data/app/controllers/lean_cms/posts_controller.rb +78 -0
  35. data/app/controllers/lean_cms/sessions_controller.rb +50 -0
  36. data/app/controllers/lean_cms/settings_controller.rb +124 -0
  37. data/app/controllers/lean_cms/users_controller.rb +113 -0
  38. data/app/helpers/lean_cms/activity_helper.rb +190 -0
  39. data/app/helpers/lean_cms/application_helper.rb +43 -0
  40. data/app/helpers/lean_cms/content_helper.rb +34 -0
  41. data/app/helpers/lean_cms/page_content_helper.rb +359 -0
  42. data/app/javascript/controllers/cards_editor_controller.js +317 -0
  43. data/app/javascript/controllers/cms_sticky_overlay_controller.js +59 -0
  44. data/app/javascript/controllers/field_editor_form_controller.js +68 -0
  45. data/app/javascript/controllers/field_editor_modal_controller.js +79 -0
  46. data/app/javascript/controllers/inline_edit_controller.js +414 -0
  47. data/app/javascript/controllers/inline_edit_toggle_controller.js +81 -0
  48. data/app/javascript/controllers/notifications_controller.js +19 -0
  49. data/app/javascript/controllers/settings_inline_edit_sync_controller.js +38 -0
  50. data/app/javascript/controllers/settings_override_controller.js +45 -0
  51. data/app/mailers/lean_cms/application_mailer.rb +6 -0
  52. data/app/mailers/lean_cms/passwords_mailer.rb +8 -0
  53. data/app/mailers/lean_cms/users_mailer.rb +39 -0
  54. data/app/models/lean_cms/current.rb +6 -0
  55. data/app/models/lean_cms/form_submission.rb +45 -0
  56. data/app/models/lean_cms/magic_link.rb +76 -0
  57. data/app/models/lean_cms/meta_tag.rb +30 -0
  58. data/app/models/lean_cms/notification_setting.rb +69 -0
  59. data/app/models/lean_cms/page.rb +23 -0
  60. data/app/models/lean_cms/page_content.rb +245 -0
  61. data/app/models/lean_cms/post.rb +65 -0
  62. data/app/models/lean_cms/session.rb +7 -0
  63. data/app/models/lean_cms/setting.rb +156 -0
  64. data/app/policies/lean_cms/application_policy.rb +35 -0
  65. data/app/policies/lean_cms/page_content_policy.rb +31 -0
  66. data/app/policies/lean_cms/post_policy.rb +37 -0
  67. data/app/policies/lean_cms/setting_policy.rb +17 -0
  68. data/app/views/layouts/lean_cms/application.html.erb +114 -0
  69. data/app/views/layouts/lean_cms/auth.html.erb +200 -0
  70. data/app/views/lean_cms/activity/index.html.erb +79 -0
  71. data/app/views/lean_cms/dashboard/index.html.erb +180 -0
  72. data/app/views/lean_cms/form_submissions/index.html.erb +104 -0
  73. data/app/views/lean_cms/form_submissions/show.html.erb +157 -0
  74. data/app/views/lean_cms/notification_settings/edit.html.erb +192 -0
  75. data/app/views/lean_cms/notifications/index.html.erb +72 -0
  76. data/app/views/lean_cms/notifications/show.html.erb +39 -0
  77. data/app/views/lean_cms/page_contents/_field_editor.html.erb +174 -0
  78. data/app/views/lean_cms/page_contents/edit.html.erb +428 -0
  79. data/app/views/lean_cms/page_contents/index.html.erb +113 -0
  80. data/app/views/lean_cms/password_setup/show.html.erb +35 -0
  81. data/app/views/lean_cms/passwords/edit.html.erb +26 -0
  82. data/app/views/lean_cms/passwords/new.html.erb +21 -0
  83. data/app/views/lean_cms/passwords_mailer/reset.html.erb +6 -0
  84. data/app/views/lean_cms/passwords_mailer/reset.text.erb +4 -0
  85. data/app/views/lean_cms/posts/_form.html.erb +118 -0
  86. data/app/views/lean_cms/posts/edit.html.erb +31 -0
  87. data/app/views/lean_cms/posts/index.html.erb +100 -0
  88. data/app/views/lean_cms/posts/new.html.erb +16 -0
  89. data/app/views/lean_cms/sessions/new.html.erb +28 -0
  90. data/app/views/lean_cms/settings/edit.html.erb +384 -0
  91. data/app/views/lean_cms/shared/_admin_bar.html.erb +85 -0
  92. data/app/views/lean_cms/shared/_header.html.erb +86 -0
  93. data/app/views/lean_cms/shared/_notifications_bell.html.erb +84 -0
  94. data/app/views/lean_cms/shared/_sidebar.html.erb +102 -0
  95. data/app/views/lean_cms/users/_form.html.erb +105 -0
  96. data/app/views/lean_cms/users/edit.html.erb +8 -0
  97. data/app/views/lean_cms/users/index.html.erb +99 -0
  98. data/app/views/lean_cms/users/new.html.erb +8 -0
  99. data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.html.erb +13 -0
  100. data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.text.erb +11 -0
  101. data/app/views/lean_cms/users_mailer/invitation.html.erb +13 -0
  102. data/app/views/lean_cms/users_mailer/invitation.text.erb +11 -0
  103. data/app/views/lean_cms/users_mailer/reactivation.html.erb +13 -0
  104. data/app/views/lean_cms/users_mailer/reactivation.text.erb +11 -0
  105. data/config/importmap.rb +8 -0
  106. data/config/routes.rb +78 -0
  107. data/db/migrate/20251112034030_create_lean_cms_tables.rb +131 -0
  108. data/db/migrate/20260513000001_create_lean_cms_auth_tables.rb +31 -0
  109. data/db/migrate/20260514000001_create_paper_trail_versions.rb +16 -0
  110. data/db/migrate/20260514000002_create_action_text_tables.rb +18 -0
  111. data/db/migrate/20260514000003_create_active_storage_tables.rb +45 -0
  112. data/db/migrate/20260514000004_create_noticed_tables.rb +27 -0
  113. data/lib/generators/lean_cms/demo/demo_generator.rb +54 -0
  114. data/lib/generators/lean_cms/demo/templates/lean_cms_structure.yml +129 -0
  115. data/lib/generators/lean_cms/demo/templates/pages_controller.rb +30 -0
  116. data/lib/generators/lean_cms/demo/templates/views/pages/about.html.erb +40 -0
  117. data/lib/generators/lean_cms/demo/templates/views/pages/contact.html.erb +55 -0
  118. data/lib/generators/lean_cms/demo/templates/views/pages/home.html.erb +31 -0
  119. data/lib/generators/lean_cms/install/install_generator.rb +317 -0
  120. data/lib/generators/lean_cms/install/templates/add_lean_cms_columns_to_users.rb.tt +7 -0
  121. data/lib/generators/lean_cms/install/templates/lean_cms.rb +11 -0
  122. data/lib/generators/lean_cms/install/templates/lean_cms_structure.yml +29 -0
  123. data/lib/lean_cms/configuration.rb +32 -0
  124. data/lib/lean_cms/engine.rb +93 -0
  125. data/lib/lean_cms/loader.rb +217 -0
  126. data/lib/lean_cms/sync_helper.rb +182 -0
  127. data/lib/lean_cms/version.rb +3 -0
  128. data/lib/lean_cms.rb +26 -0
  129. data/lib/tasks/lean_cms.rake +390 -0
  130. metadata +313 -0
@@ -0,0 +1,217 @@
1
+ require "yaml"
2
+
3
+ module LeanCms
4
+ # Loads page content structure from a YAML file into LeanCms::PageContent
5
+ # records. Idempotent — re-running against an unchanged YAML is a no-op;
6
+ # changes to field metadata (label, position, etc.) get applied without
7
+ # clobbering editor-supplied values.
8
+ #
9
+ # Usage from a background job, runner, or one-off script:
10
+ #
11
+ # result = LeanCms::Loader.new.load!
12
+ # result.created # => 28
13
+ # result.skipped # => 0
14
+ #
15
+ # Usage from a rake task (with progress on stdout):
16
+ #
17
+ # LeanCms::Loader.new(logger: Logger.new($stdout)).load!
18
+ #
19
+ # See `lib/tasks/lean_cms.rake` for the thin Rake wrappers.
20
+ class Loader
21
+ Result = Struct.new(:total_fields, :created, :updated, :skipped, keyword_init: true)
22
+
23
+ DEFAULT_YAML_PATH = "config/lean_cms_structure.yml".freeze
24
+
25
+ class StructureFileMissing < StandardError; end
26
+ class NoUsersFound < StandardError; end
27
+
28
+ def initialize(yaml_path: nil, system_user: nil, logger: nil)
29
+ @yaml_path = yaml_path || Rails.root.join(DEFAULT_YAML_PATH)
30
+ @system_user = system_user
31
+ @logger = logger || ActiveSupport::Logger.new(IO::NULL)
32
+ end
33
+
34
+ def load!
35
+ raise StructureFileMissing, "Structure file not found at #{@yaml_path}" unless File.exist?(@yaml_path)
36
+
37
+ structure = YAML.load_file(@yaml_path)
38
+ pages = structure["pages"] || {}
39
+ return empty_result if pages.empty?
40
+
41
+ user = resolve_system_user
42
+ raise NoUsersFound, "No users in database. Create at least one before loading structure." unless user
43
+
44
+ @system_user = user
45
+ @logger.info "Loading LeanCMS page content structure..."
46
+ @logger.info "Using system user: #{user.email_address}"
47
+ @logger.info "=" * 60
48
+
49
+ @total = @created = @updated = @skipped = 0
50
+
51
+ pages.each do |page_key, page_data|
52
+ load_page(page_key, page_data)
53
+ end
54
+
55
+ @logger.info ""
56
+ @logger.info "=" * 60
57
+ @logger.info "Summary:"
58
+ @logger.info " Total fields: #{@total}"
59
+ @logger.info " Created: #{@created}"
60
+ @logger.info " Updated: #{@updated}"
61
+ @logger.info " Skipped: #{@skipped}"
62
+ @logger.info "=" * 60
63
+
64
+ Result.new(total_fields: @total, created: @created, updated: @updated, skipped: @skipped)
65
+ end
66
+
67
+ private
68
+
69
+ def empty_result
70
+ @logger.warn "No pages defined in structure file at #{@yaml_path}"
71
+ Result.new(total_fields: 0, created: 0, updated: 0, skipped: 0)
72
+ end
73
+
74
+ def resolve_system_user
75
+ return @system_user if @system_user
76
+
77
+ user_class = LeanCms.user_class.constantize
78
+ user_class.where(is_super_admin: true).first || user_class.first
79
+ end
80
+
81
+ def load_page(page_key, page_data)
82
+ sections = page_data["sections"] || {}
83
+ page_display_title = page_data["display_title"] || page_key.titleize
84
+ page_order = page_data["page_order"] || 0
85
+
86
+ @logger.info ""
87
+ @logger.info "Page: #{page_key.upcase} (#{page_display_title}) [order: #{page_order}]"
88
+
89
+ sections.each do |section_key, section_data|
90
+ load_section(page_key, page_display_title, page_order, section_key, section_data)
91
+ end
92
+ end
93
+
94
+ def load_section(page_key, page_display_title, page_order, section_key, section_data)
95
+ section_display_title = section_data["display_title"] || section_key.titleize
96
+ section_order = section_data["section_order"] || 0
97
+
98
+ @logger.info " Section: #{section_key} (#{section_display_title}) [order: #{section_order}]"
99
+
100
+ section_meta = {
101
+ page_key: page_key,
102
+ page_display_title: page_display_title,
103
+ page_order: page_order,
104
+ section_key: section_key,
105
+ section_display_title: section_display_title,
106
+ section_order: section_order
107
+ }
108
+
109
+ (section_data["fields"] || {}).each do |field_key, field_data|
110
+ next unless field_data.is_a?(Hash) && field_data["type"]
111
+ load_field(section_meta, field_key, field_data)
112
+ end
113
+
114
+ cards_data = section_data["cards"]
115
+ load_cards(section_meta, cards_data) if cards_data && cards_data["items"]
116
+
117
+ bullets_data = section_data["bullets"]
118
+ load_bullets(section_meta, bullets_data) if bullets_data && bullets_data["items"]
119
+ end
120
+
121
+ def load_field(meta, field_key, field_data)
122
+ @total += 1
123
+ record = LeanCms::PageContent.find_or_initialize_content(
124
+ page: meta[:page_key], section: meta[:section_key], key: field_key
125
+ )
126
+
127
+ apply_common_attributes(record, field_data["label"], field_data["type"], meta)
128
+ apply_default_value(record, field_data["default"]) if record.new_record?
129
+ apply_field_options(record, field_data)
130
+ record.position = field_data["position"] || 0
131
+ record.last_edited_by = @system_user
132
+
133
+ persist(record, label: field_key, type_summary: field_data["type"])
134
+ end
135
+
136
+ def load_cards(meta, cards_data)
137
+ @total += 1
138
+ record = LeanCms::PageContent.find_or_initialize_content(
139
+ page: meta[:page_key], section: meta[:section_key], key: "cards"
140
+ )
141
+
142
+ apply_common_attributes(record, "Cards", "cards", meta)
143
+ record.position = 999
144
+ record.options = { "max_cards" => cards_data["max_cards"], "type" => cards_data["type"] }.compact
145
+ record.content = cards_data["items"].to_json if record.new_record?
146
+ record.last_edited_by = @system_user
147
+
148
+ persist(record, label: "cards", type_summary: "#{cards_data['items'].size} items")
149
+ end
150
+
151
+ def load_bullets(meta, bullets_data)
152
+ @total += 1
153
+ record = LeanCms::PageContent.find_or_initialize_content(
154
+ page: meta[:page_key], section: meta[:section_key], key: "bullets"
155
+ )
156
+
157
+ apply_common_attributes(record, "Bullet Points", "bullets", meta)
158
+ record.position = 999
159
+ record.options = { "max_items" => bullets_data["max_items"] || 10, "type" => "bullets" }
160
+ record.content = bullets_data["items"].to_json if record.new_record?
161
+ record.last_edited_by = @system_user
162
+
163
+ persist(record, label: "bullets", type_summary: "#{bullets_data['items'].size} items")
164
+ end
165
+
166
+ def apply_common_attributes(record, label, content_type, meta)
167
+ record.label = label
168
+ record.content_type = content_type
169
+ record.display_title = meta[:section_display_title]
170
+ record.page_display_title = meta[:page_display_title]
171
+ record.page_order = meta[:page_order]
172
+ record.section_order = meta[:section_order]
173
+ end
174
+
175
+ def apply_default_value(record, default_value)
176
+ if record.rich_text?
177
+ record.rich_content = default_value
178
+ elsif record.boolean?
179
+ record.value = (default_value == true || default_value == "true").to_s
180
+ elsif default_value
181
+ record.value = default_value.to_s
182
+ end
183
+ end
184
+
185
+ def apply_field_options(record, field_data)
186
+ if record.dropdown? && field_data["options"]
187
+ record.options = { "options" => field_data["options"] }
188
+ end
189
+
190
+ if field_data["max_length"]
191
+ record.options ||= {}
192
+ record.options = record.options.merge("max_length" => field_data["max_length"].to_i)
193
+ end
194
+ end
195
+
196
+ def persist(record, label:, type_summary:)
197
+ if record.new_record?
198
+ if record.save
199
+ @created += 1
200
+ @logger.info " ✓ Created: #{label} (#{type_summary})"
201
+ else
202
+ @logger.error " ✗ Error: #{label} - #{record.errors.full_messages.join(', ')}"
203
+ end
204
+ elsif record.changed?
205
+ if record.save
206
+ @updated += 1
207
+ @logger.info " ↻ Updated: #{label} (#{type_summary})"
208
+ else
209
+ @logger.error " ✗ Error: #{label} - #{record.errors.full_messages.join(', ')}"
210
+ end
211
+ else
212
+ @skipped += 1
213
+ @logger.info " - Skipped: #{label} (already exists)"
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,182 @@
1
+ module LeanCms
2
+ class SyncHelper
3
+ class << self
4
+ def config
5
+ @config ||= load_config
6
+ end
7
+
8
+ def pull_from_production
9
+ puts "Pulling production database..."
10
+
11
+ validate_config!
12
+ ensure_local_dir_exists
13
+
14
+ # Stop the container to ensure clean database state
15
+ puts " Stopping production container..."
16
+ run_ssh("docker stop $(docker ps -q --filter name=#{config[:service]}-web) 2>/dev/null || true")
17
+
18
+ # Pull the database file
19
+ puts " Downloading production.sqlite3..."
20
+ run_local("scp #{config[:ssh_user]}@#{config[:server]}:#{config[:remote_storage_path]}/production.sqlite3 #{local_db_path}")
21
+
22
+ # Also pull Active Storage files if they exist
23
+ puts " Downloading Active Storage files..."
24
+ run_local("scp -r #{config[:ssh_user]}@#{config[:server]}:#{config[:remote_storage_path]}/ #{local_storage_path}/ 2>/dev/null || true")
25
+
26
+ # Restart the container
27
+ puts " Restarting production container..."
28
+ run_ssh("docker start $(docker ps -aq --filter name=#{config[:service]}-web) 2>/dev/null || true")
29
+
30
+ # Clean up WAL files locally (they're from production)
31
+ cleanup_wal_files(local_db_path)
32
+
33
+ puts "\nDatabase pulled successfully!"
34
+ puts " Local path: #{local_db_path}"
35
+ puts "\nYou can now make changes locally using:"
36
+ puts " RAILS_ENV=production_local bin/rails console"
37
+ end
38
+
39
+ def push_to_production
40
+ puts "Pushing local database to production..."
41
+
42
+ validate_config!
43
+ validate_local_db_exists!
44
+
45
+ # Checkpoint the local database (flush WAL to main file)
46
+ checkpoint_database(local_db_path)
47
+
48
+ # Stop the container
49
+ puts " Stopping production container..."
50
+ run_ssh("docker stop $(docker ps -q --filter name=#{config[:service]}-web) 2>/dev/null || true")
51
+
52
+ # Backup production database first
53
+ timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
54
+ puts " Backing up production database..."
55
+ run_ssh("cp #{config[:remote_storage_path]}/production.sqlite3 #{config[:remote_storage_path]}/production.sqlite3.backup.#{timestamp} 2>/dev/null || true")
56
+
57
+ # Push the database file
58
+ puts " Uploading production.sqlite3..."
59
+ run_local("scp #{local_db_path} #{config[:ssh_user]}@#{config[:server]}:#{config[:remote_storage_path]}/production.sqlite3")
60
+
61
+ # Push Active Storage files
62
+ puts " Uploading Active Storage files..."
63
+ run_local("scp -r #{local_storage_path}/* #{config[:ssh_user]}@#{config[:server]}:#{config[:remote_storage_path]}/ 2>/dev/null || true")
64
+
65
+ # Clean up WAL files on server
66
+ puts " Cleaning up WAL files..."
67
+ run_ssh("rm -f #{config[:remote_storage_path]}/production.sqlite3-shm #{config[:remote_storage_path]}/production.sqlite3-wal")
68
+
69
+ # Restart the container
70
+ puts " Restarting production container..."
71
+ run_ssh("docker start $(docker ps -aq --filter name=#{config[:service]}-web) 2>/dev/null || true")
72
+
73
+ puts "\nDatabase pushed successfully!"
74
+ puts " Backup saved as: production.sqlite3.backup.#{timestamp}"
75
+
76
+ clear_production_cache
77
+ warm_cache
78
+ end
79
+
80
+ private
81
+
82
+ def load_config
83
+ deploy_config = YAML.load_file(Rails.root.join('config', 'deploy.yml'))
84
+
85
+ server = deploy_config.dig('servers', 'web')&.first
86
+ service = deploy_config['service']
87
+ ssh_user = deploy_config.dig('ssh', 'user') || 'root'
88
+ host = deploy_config.dig('proxy', 'host')
89
+ ssl = deploy_config.dig('proxy', 'ssl') != false
90
+
91
+ {
92
+ server: server,
93
+ service: service,
94
+ ssh_user: ssh_user,
95
+ host: host,
96
+ base_url: "#{ssl ? 'https' : 'http'}://#{host}",
97
+ remote_storage_path: "/var/lib/docker/volumes/#{service}_storage/_data",
98
+ local_storage_path: Rails.root.join('storage').to_s
99
+ }
100
+ end
101
+
102
+ def validate_config!
103
+ raise "No server configured in config/deploy.yml" unless config[:server]
104
+ raise "No service name configured in config/deploy.yml" unless config[:service]
105
+ end
106
+
107
+ def validate_local_db_exists!
108
+ raise "Local database not found at #{local_db_path}" unless File.exist?(local_db_path)
109
+ end
110
+
111
+ def ensure_local_dir_exists
112
+ FileUtils.mkdir_p(local_storage_path)
113
+ end
114
+
115
+ def local_db_path
116
+ Rails.root.join('storage', 'production_local.sqlite3').to_s
117
+ end
118
+
119
+ def local_storage_path
120
+ Rails.root.join('storage').to_s
121
+ end
122
+
123
+ def run_ssh(command)
124
+ full_command = "ssh #{config[:ssh_user]}@#{config[:server]} \"#{command}\""
125
+ result = system(full_command)
126
+ raise "SSH command failed: #{command}" unless result
127
+ result
128
+ end
129
+
130
+ def run_local(command)
131
+ result = system(command)
132
+ raise "Local command failed: #{command}" unless result
133
+ result
134
+ end
135
+
136
+ def cleanup_wal_files(db_path)
137
+ FileUtils.rm_f("#{db_path}-shm")
138
+ FileUtils.rm_f("#{db_path}-wal")
139
+ end
140
+
141
+ def clear_production_cache
142
+ puts "\nClearing production cache..."
143
+ puts " Waiting for container to become healthy..."
144
+ sleep 5
145
+
146
+ run_ssh(
147
+ "docker exec $(docker ps -q --filter name=#{config[:service]}-web) " \
148
+ "bin/rails runner 'Rails.cache.clear' 2>/dev/null || true"
149
+ )
150
+ puts " Cache cleared."
151
+ end
152
+
153
+ def warm_cache
154
+ base_url = config[:base_url]
155
+ return unless base_url.present?
156
+
157
+ pages = %w[/ /services /about /contact /blog /portfolio]
158
+
159
+ puts "\nWarming cache..."
160
+ puts " Waiting for container to become healthy..."
161
+ sleep 5
162
+
163
+ pages.each do |path|
164
+ url = "#{base_url}#{path}"
165
+ code = `curl -s -o /dev/null -w "%{http_code}" --max-time 15 "#{url}"`.strip
166
+ status = code == "200" ? "✓" : "✗"
167
+ puts " #{status} #{code} #{url}"
168
+ end
169
+
170
+ puts "Cache warmed."
171
+ end
172
+
173
+ def checkpoint_database(db_path)
174
+ return unless File.exist?(db_path)
175
+
176
+ puts " Checkpointing local database..."
177
+ system("sqlite3 #{db_path} 'PRAGMA wal_checkpoint(TRUNCATE);'")
178
+ cleanup_wal_files(db_path)
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,3 @@
1
+ module LeanCms
2
+ VERSION = "0.2.12"
3
+ end
data/lib/lean_cms.rb ADDED
@@ -0,0 +1,26 @@
1
+ require "lean_cms/version"
2
+ require "lean_cms/configuration"
3
+
4
+ # Runtime dependencies — explicitly required so consumers can use the
5
+ # constants (e.g. Pundit::Authorization in ApplicationController) without
6
+ # adding them to their host Gemfile. These are also declared in the
7
+ # gemspec.
8
+ require "paper_trail"
9
+ require "view_component"
10
+ require "kaminari"
11
+ require "pundit"
12
+ require "noticed"
13
+ require "image_processing/vips"
14
+ require "meta_tags"
15
+ require "rack/attack"
16
+
17
+ require "lean_cms/engine"
18
+ require "lean_cms/sync_helper"
19
+ require "lean_cms/loader"
20
+
21
+ module LeanCms
22
+ # Table name prefix for all Lean CMS models
23
+ def self.table_name_prefix
24
+ "lean_cms_"
25
+ end
26
+ end