ruby_cms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. checksums.yaml +7 -0
  2. data/.cursor/dhh.mdc +698 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +10 -0
  6. data/README.md +235 -0
  7. data/Rakefile +30 -0
  8. data/app/components/ruby_cms/admin/admin_page/admin_table_content.rb +32 -0
  9. data/app/components/ruby_cms/admin/admin_page.rb +345 -0
  10. data/app/components/ruby_cms/admin/base_component.rb +78 -0
  11. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +149 -0
  12. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +127 -0
  13. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +15 -0
  14. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +41 -0
  15. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +33 -0
  16. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +174 -0
  17. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +59 -0
  18. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +159 -0
  19. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +192 -0
  20. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +97 -0
  21. data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +137 -0
  22. data/app/controllers/concerns/ruby_cms/admin_pagination.rb +120 -0
  23. data/app/controllers/concerns/ruby_cms/admin_turbo_table.rb +68 -0
  24. data/app/controllers/concerns/ruby_cms/page_tracking.rb +52 -0
  25. data/app/controllers/concerns/ruby_cms/visitor_error_capture.rb +39 -0
  26. data/app/controllers/ruby_cms/admin/analytics_controller.rb +191 -0
  27. data/app/controllers/ruby_cms/admin/base_controller.rb +105 -0
  28. data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +390 -0
  29. data/app/controllers/ruby_cms/admin/dashboard_controller.rb +50 -0
  30. data/app/controllers/ruby_cms/admin/locale_controller.rb +20 -0
  31. data/app/controllers/ruby_cms/admin/permissions_controller.rb +66 -0
  32. data/app/controllers/ruby_cms/admin/settings_controller.rb +223 -0
  33. data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +59 -0
  34. data/app/controllers/ruby_cms/admin/users_controller.rb +107 -0
  35. data/app/controllers/ruby_cms/admin/visitor_errors_controller.rb +89 -0
  36. data/app/controllers/ruby_cms/admin/visual_editor_controller.rb +322 -0
  37. data/app/controllers/ruby_cms/errors_controller.rb +35 -0
  38. data/app/helpers/ruby_cms/admin/admin_page_helper.rb +21 -0
  39. data/app/helpers/ruby_cms/admin/bulk_action_table_helper.rb +159 -0
  40. data/app/helpers/ruby_cms/application_helper.rb +41 -0
  41. data/app/helpers/ruby_cms/bulk_action_table_helper.rb +151 -0
  42. data/app/helpers/ruby_cms/content_blocks_helper.rb +375 -0
  43. data/app/helpers/ruby_cms/settings_helper.rb +160 -0
  44. data/app/javascript/controllers/ruby_cms/auto_save_preference_controller.js +73 -0
  45. data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +553 -0
  46. data/app/javascript/controllers/ruby_cms/clickable_row_controller.js +28 -0
  47. data/app/javascript/controllers/ruby_cms/flash_messages_controller.js +29 -0
  48. data/app/javascript/controllers/ruby_cms/index.js +104 -0
  49. data/app/javascript/controllers/ruby_cms/locale_tabs_controller.js +34 -0
  50. data/app/javascript/controllers/ruby_cms/mobile_menu_controller.js +55 -0
  51. data/app/javascript/controllers/ruby_cms/nav_order_sortable_controller.js +192 -0
  52. data/app/javascript/controllers/ruby_cms/page_preview_controller.js +135 -0
  53. data/app/javascript/controllers/ruby_cms/toggle_controller.js +39 -0
  54. data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +321 -0
  55. data/app/models/concerns/content_block/publishable.rb +54 -0
  56. data/app/models/concerns/content_block/searchable.rb +22 -0
  57. data/app/models/content_block.rb +155 -0
  58. data/app/models/ruby_cms/content_block.rb +8 -0
  59. data/app/models/ruby_cms/permission.rb +28 -0
  60. data/app/models/ruby_cms/permittable.rb +39 -0
  61. data/app/models/ruby_cms/preference.rb +111 -0
  62. data/app/models/ruby_cms/user_permission.rb +12 -0
  63. data/app/models/ruby_cms/visitor_error.rb +109 -0
  64. data/app/services/ruby_cms/analytics/report.rb +362 -0
  65. data/app/services/ruby_cms/security_tracker.rb +92 -0
  66. data/app/views/layouts/ruby_cms/_admin_flash_messages.html.erb +37 -0
  67. data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +121 -0
  68. data/app/views/layouts/ruby_cms/admin.html.erb +81 -0
  69. data/app/views/layouts/ruby_cms/minimal.html.erb +181 -0
  70. data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -0
  71. data/app/views/ruby_cms/admin/analytics/page_details.html.erb +84 -0
  72. data/app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb +3 -0
  73. data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +40 -0
  74. data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +58 -0
  75. data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +51 -0
  76. data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +31 -0
  77. data/app/views/ruby_cms/admin/analytics/partials/_security_alert.html.erb +4 -0
  78. data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +21 -0
  79. data/app/views/ruby_cms/admin/analytics/visitor_details.html.erb +125 -0
  80. data/app/views/ruby_cms/admin/content_blocks/_form.html.erb +161 -0
  81. data/app/views/ruby_cms/admin/content_blocks/_row.html.erb +25 -0
  82. data/app/views/ruby_cms/admin/content_blocks/edit.html.erb +17 -0
  83. data/app/views/ruby_cms/admin/content_blocks/index.html.erb +66 -0
  84. data/app/views/ruby_cms/admin/content_blocks/new.html.erb +5 -0
  85. data/app/views/ruby_cms/admin/content_blocks/show.html.erb +110 -0
  86. data/app/views/ruby_cms/admin/dashboard/index.html.erb +198 -0
  87. data/app/views/ruby_cms/admin/permissions/_row.html.erb +11 -0
  88. data/app/views/ruby_cms/admin/permissions/index.html.erb +62 -0
  89. data/app/views/ruby_cms/admin/settings/index.html.erb +220 -0
  90. data/app/views/ruby_cms/admin/shared/_bulk_action_table_index.html.erb +56 -0
  91. data/app/views/ruby_cms/admin/user_permissions/index.html.erb +55 -0
  92. data/app/views/ruby_cms/admin/users/_row.html.erb +14 -0
  93. data/app/views/ruby_cms/admin/users/index.html.erb +70 -0
  94. data/app/views/ruby_cms/admin/visitor_errors/_row.html.erb +35 -0
  95. data/app/views/ruby_cms/admin/visitor_errors/index.html.erb +57 -0
  96. data/app/views/ruby_cms/admin/visitor_errors/show.html.erb +147 -0
  97. data/app/views/ruby_cms/admin/visual_editor/index.html.erb +144 -0
  98. data/app/views/ruby_cms/errors/not_found.html.erb +92 -0
  99. data/config/database.yml +6 -0
  100. data/config/importmap.rb +36 -0
  101. data/config/locales/en.yml +101 -0
  102. data/config/routes.rb +65 -0
  103. data/db/migrate/20260125000001_create_ruby_cms_permissions.rb +14 -0
  104. data/db/migrate/20260125000002_create_ruby_cms_user_permissions.rb +14 -0
  105. data/db/migrate/20260125000003_create_ruby_cms_content_blocks.rb +19 -0
  106. data/db/migrate/20260125000010_add_indexes_to_ruby_cms_tables.rb +9 -0
  107. data/db/migrate/20260127000001_add_locale_to_ruby_cms_content_blocks.rb +34 -0
  108. data/db/migrate/20260129000001_create_ruby_cms_visitor_errors.rb +24 -0
  109. data/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +8 -0
  110. data/db/migrate/20260130000002_create_ruby_cms_preferences.rb +16 -0
  111. data/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +8 -0
  112. data/db/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb +19 -0
  113. data/db/migrate/20260212000001_use_unprefixed_cms_tables.rb +146 -0
  114. data/exe/ruby_cms +25 -0
  115. data/lib/generators/ruby_cms/install_generator.rb +1062 -0
  116. data/lib/generators/ruby_cms/templates/admin.html.erb +82 -0
  117. data/lib/generators/ruby_cms/templates/ruby_cms.rb +86 -0
  118. data/lib/ruby_cms/app_integration.rb +82 -0
  119. data/lib/ruby_cms/cli.rb +169 -0
  120. data/lib/ruby_cms/content_blocks_grouping.rb +41 -0
  121. data/lib/ruby_cms/content_blocks_sync.rb +329 -0
  122. data/lib/ruby_cms/css_compiler.rb +35 -0
  123. data/lib/ruby_cms/engine.rb +498 -0
  124. data/lib/ruby_cms/settings.rb +145 -0
  125. data/lib/ruby_cms/settings_registry.rb +289 -0
  126. data/lib/ruby_cms/version.rb +5 -0
  127. data/lib/ruby_cms.rb +195 -0
  128. data/lib/tasks/ruby_cms.rake +27 -0
  129. data/log/test.log +17875 -0
  130. data/sig/ruby_cms.rbs +4 -0
  131. metadata +223 -0
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddLocaleToRubyCmsContentBlocks < ActiveRecord::Migration[7.1]
4
+ def change
5
+ add_column :ruby_cms_content_blocks, :locale, :string, default: "en", null: false
6
+
7
+ # Remove old unique index on key
8
+ remove_index :ruby_cms_content_blocks, :key if index_exists?(:ruby_cms_content_blocks, :key)
9
+
10
+ # Add new composite unique index on key + locale
11
+ add_index :ruby_cms_content_blocks, %i[key locale], unique: true
12
+
13
+ # Add index for locale queries
14
+ add_index :ruby_cms_content_blocks, :locale
15
+
16
+ # Migrate existing records to default locale
17
+ # This ensures existing content blocks get the default locale
18
+ # Use execute to avoid model loading issues during migration
19
+ reversible do |dir|
20
+ dir.up do
21
+ default_locale = begin
22
+ I18n.default_locale.to_s
23
+ rescue StandardError
24
+ "en"
25
+ end
26
+ execute <<~SQL.squish
27
+ UPDATE ruby_cms_content_blocks
28
+ SET locale = '#{default_locale}'
29
+ WHERE locale IS NULL OR locale = ''
30
+ SQL
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRubyCmsVisitorErrors < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :ruby_cms_visitor_errors do |t|
6
+ t.string :error_class, null: false
7
+ t.text :error_message, null: false
8
+ t.string :request_path, null: false
9
+ t.string :request_method
10
+ t.string :ip_address
11
+ t.text :user_agent
12
+ t.text :backtrace
13
+ t.text :request_params
14
+ t.string :session_id
15
+ t.boolean :resolved, default: false, null: false
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :ruby_cms_visitor_errors, :created_at
21
+ add_index :ruby_cms_visitor_errors, :request_path
22
+ add_index :ruby_cms_visitor_errors, :resolved
23
+ end
24
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddRefererAndQueryToRubyCmsVisitorErrors < ActiveRecord::Migration[7.1]
4
+ def change
5
+ add_column :ruby_cms_visitor_errors, :referer, :string
6
+ add_column :ruby_cms_visitor_errors, :query_string, :string
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRubyCmsPreferences < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :ruby_cms_preferences do |t|
6
+ t.string :key, null: false
7
+ t.text :value
8
+ t.string :value_type, default: "string", null: false
9
+ t.text :description
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :ruby_cms_preferences, :key, unique: true
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddCategoryToRubyCmsPreferences < ActiveRecord::Migration[7.1]
4
+ def change
5
+ add_column :ruby_cms_preferences, :category, :string, default: "general"
6
+ add_index :ruby_cms_preferences, :category
7
+ end
8
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddRubyCmsAnalyticsFieldsToAhoyEvents < ActiveRecord::Migration[7.1]
4
+ def change
5
+ return unless table_exists?(:ahoy_events)
6
+
7
+ add_column :ahoy_events, :page_name, :string unless column_exists?(:ahoy_events, :page_name)
8
+ add_column :ahoy_events, :request_path, :string unless column_exists?(:ahoy_events, :request_path)
9
+ add_column :ahoy_events, :ip_address, :string unless column_exists?(:ahoy_events, :ip_address)
10
+ add_column :ahoy_events, :user_agent, :text unless column_exists?(:ahoy_events, :user_agent)
11
+ add_column :ahoy_events, :description, :text unless column_exists?(:ahoy_events, :description)
12
+
13
+ add_index :ahoy_events, :page_name, if_not_exists: true
14
+ add_index :ahoy_events, :request_path, if_not_exists: true
15
+ add_index :ahoy_events, :ip_address, if_not_exists: true
16
+ add_index :ahoy_events, %i[name page_name], if_not_exists: true
17
+ add_index :ahoy_events, %i[name request_path], if_not_exists: true
18
+ end
19
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ensures CMS uses unprefixed table names (preferences, content_blocks, permissions,
4
+ # user_permissions, visitor_errors). Creates tables only if they don't exist so host
5
+ # apps with existing tables are not overwritten. Adds missing columns when tables exist.
6
+ class UseUnprefixedCmsTables < ActiveRecord::Migration[7.1]
7
+ def up
8
+ create_preferences_if_missing
9
+ ensure_content_blocks_table
10
+ ensure_content_blocks_columns
11
+ create_permissions_if_missing
12
+ create_user_permissions_if_missing
13
+ ensure_visitor_errors_table
14
+ ensure_visitor_errors_columns
15
+ end
16
+
17
+ def down
18
+ # No-op: leave unprefixed tables in place; host app may rely on them.
19
+ end
20
+
21
+ private
22
+
23
+ def create_preferences_if_missing
24
+ return if table_exists?(:preferences)
25
+
26
+ create_table :preferences do |t|
27
+ t.string :key, null: false
28
+ t.text :value
29
+ t.string :value_type, default: "string", null: false
30
+ t.text :description
31
+ t.string :category, default: "general"
32
+
33
+ t.timestamps
34
+ end
35
+ add_index :preferences, :key, unique: true
36
+ add_index :preferences, :category
37
+ end
38
+
39
+ def ensure_content_blocks_table
40
+ return if table_exists?(:content_blocks)
41
+
42
+ create_table :content_blocks do |t|
43
+ t.string :key, null: false
44
+ t.string :title
45
+ t.text :content
46
+ t.string :content_type, default: "text", null: false
47
+ t.boolean :published, default: false, null: false
48
+ t.string :locale, default: "en", null: false
49
+ t.references :updated_by, null: true, foreign_key: false
50
+
51
+ t.timestamps
52
+ end
53
+ add_index :content_blocks, %i[key locale], unique: true
54
+ add_index :content_blocks, :locale
55
+ add_index :content_blocks, %i[published content_type]
56
+ end
57
+
58
+ def ensure_content_blocks_columns
59
+ return unless table_exists?(:content_blocks)
60
+
61
+ unless column_exists?(:content_blocks, :locale)
62
+ add_column :content_blocks, :locale, :string, default: "en", null: false
63
+ add_index :content_blocks, :locale
64
+ default_locale = begin
65
+ I18n.default_locale
66
+ rescue StandardError
67
+ "en"
68
+ end.to_s
69
+ execute <<~SQL.squish
70
+ UPDATE content_blocks SET locale = '#{default_locale}' WHERE locale IS NULL OR locale = ''
71
+ SQL
72
+ end
73
+
74
+ unless column_exists?(:content_blocks, :updated_by_id)
75
+ add_reference :content_blocks, :updated_by, null: true, foreign_key: false
76
+ add_index :content_blocks, :updated_by_id unless index_exists?(:content_blocks, :updated_by_id)
77
+ end
78
+
79
+ # Replace unique index on key with key+locale if we have old index
80
+ if index_exists?(:content_blocks, :key, unique: true) && !index_exists?(:content_blocks, %i[key locale], unique: true)
81
+ remove_index :content_blocks, :key
82
+ add_index :content_blocks, %i[key locale], unique: true
83
+ end
84
+
85
+ return if index_exists?(:content_blocks, %i[published content_type])
86
+
87
+ add_index :content_blocks, %i[published content_type]
88
+ end
89
+
90
+ def create_permissions_if_missing
91
+ return if table_exists?(:permissions)
92
+
93
+ create_table :permissions do |t|
94
+ t.string :key, null: false
95
+ t.string :name
96
+
97
+ t.timestamps
98
+ end
99
+ add_index :permissions, :key, unique: true
100
+ end
101
+
102
+ def create_user_permissions_if_missing
103
+ return if table_exists?(:user_permissions)
104
+
105
+ create_table :user_permissions do |t|
106
+ t.references :user, null: false, foreign_key: false
107
+ t.references :permission, null: false, foreign_key: false
108
+
109
+ t.timestamps
110
+ end
111
+ add_index :user_permissions, %i[user_id permission_id], unique: true
112
+ end
113
+
114
+ def ensure_visitor_errors_table
115
+ return if table_exists?(:visitor_errors)
116
+
117
+ create_table :visitor_errors do |t|
118
+ t.string :error_class, null: false
119
+ t.text :error_message, null: false
120
+ t.string :request_path, null: false
121
+ t.string :request_method
122
+ t.string :ip_address
123
+ t.text :user_agent
124
+ t.text :backtrace
125
+ t.text :request_params
126
+ t.string :session_id
127
+ t.string :referer
128
+ t.string :query_string
129
+ t.boolean :resolved, default: false, null: false
130
+
131
+ t.timestamps
132
+ end
133
+ add_index :visitor_errors, :created_at
134
+ add_index :visitor_errors, :request_path
135
+ add_index :visitor_errors, :resolved
136
+ end
137
+
138
+ def ensure_visitor_errors_columns
139
+ return unless table_exists?(:visitor_errors)
140
+
141
+ add_column :visitor_errors, :referer, :string unless column_exists?(:visitor_errors, :referer)
142
+ return if column_exists?(:visitor_errors, :query_string)
143
+
144
+ add_column :visitor_errors, :query_string, :string
145
+ end
146
+ end
data/exe/ruby_cms ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Find Rails app root (current dir or nearest parent with config/application.rb)
5
+ app_root = File.expand_path(Dir.pwd)
6
+ loop do
7
+ break if File.file?(File.join(app_root, "config/application.rb"))
8
+
9
+ parent = File.dirname(app_root)
10
+ break if parent == app_root
11
+
12
+ app_root = parent
13
+ end
14
+
15
+ unless File.file?(File.join(app_root, "config/application.rb"))
16
+ warn "Not in a Rails application (config/application.rb not found)."
17
+ warn "Run from your app root, or use: rails ruby_cms:setup_admin"
18
+ exit 1
19
+ end
20
+
21
+ Dir.chdir(app_root)
22
+ require File.join(app_root, "config/environment")
23
+
24
+ require "ruby_cms/cli"
25
+ RubyCms::CLI.start(ARGV)