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,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
|
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
|