collavre 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 (410) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +221 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/stylesheets/collavre/actiontext.css +577 -0
  5. data/app/assets/stylesheets/collavre/activity_logs.css +99 -0
  6. data/app/assets/stylesheets/collavre/comments_popup.css +692 -0
  7. data/app/assets/stylesheets/collavre/creatives.css +559 -0
  8. data/app/assets/stylesheets/collavre/dark_mode.css +118 -0
  9. data/app/assets/stylesheets/collavre/mention_menu.css +43 -0
  10. data/app/assets/stylesheets/collavre/popup.css +160 -0
  11. data/app/assets/stylesheets/collavre/print.css +37 -0
  12. data/app/assets/stylesheets/collavre/slide_view.css +79 -0
  13. data/app/assets/stylesheets/collavre/user_menu.css +34 -0
  14. data/app/channels/collavre/comments_presence_channel.rb +54 -0
  15. data/app/channels/collavre/slide_view_channel.rb +11 -0
  16. data/app/channels/collavre/topics_channel.rb +12 -0
  17. data/app/components/collavre/avatar_component.html.erb +15 -0
  18. data/app/components/collavre/avatar_component.rb +59 -0
  19. data/app/components/collavre/inbox/badge_component.html.erb +6 -0
  20. data/app/components/collavre/inbox/badge_component.rb +18 -0
  21. data/app/components/collavre/plans_timeline_component.html.erb +14 -0
  22. data/app/components/collavre/plans_timeline_component.rb +56 -0
  23. data/app/components/collavre/popup_menu_component.html.erb +6 -0
  24. data/app/components/collavre/popup_menu_component.rb +30 -0
  25. data/app/components/collavre/progress_filter_component.html.erb +5 -0
  26. data/app/components/collavre/progress_filter_component.rb +10 -0
  27. data/app/components/collavre/user_mention_menu_component.html.erb +3 -0
  28. data/app/components/collavre/user_mention_menu_component.rb +8 -0
  29. data/app/controllers/collavre/application_controller.rb +15 -0
  30. data/app/controllers/collavre/attachments_controller.rb +44 -0
  31. data/app/controllers/collavre/calendar_events_controller.rb +15 -0
  32. data/app/controllers/collavre/comment_read_pointers_controller.rb +80 -0
  33. data/app/controllers/collavre/comments/activity_logs_controller.rb +26 -0
  34. data/app/controllers/collavre/comments/reactions_controller.rb +82 -0
  35. data/app/controllers/collavre/comments_controller.rb +464 -0
  36. data/app/controllers/collavre/contacts_controller.rb +10 -0
  37. data/app/controllers/collavre/creative_expanded_states_controller.rb +27 -0
  38. data/app/controllers/collavre/creative_imports_controller.rb +24 -0
  39. data/app/controllers/collavre/creative_plans_controller.rb +69 -0
  40. data/app/controllers/collavre/creative_shares_controller.rb +79 -0
  41. data/app/controllers/collavre/creatives_controller.rb +535 -0
  42. data/app/controllers/collavre/devices_controller.rb +19 -0
  43. data/app/controllers/collavre/email_verifications_controller.rb +16 -0
  44. data/app/controllers/collavre/emails_controller.rb +11 -0
  45. data/app/controllers/collavre/github_auth_controller.rb +25 -0
  46. data/app/controllers/collavre/google_auth_controller.rb +43 -0
  47. data/app/controllers/collavre/inbox_items_controller.rb +64 -0
  48. data/app/controllers/collavre/invites_controller.rb +27 -0
  49. data/app/controllers/collavre/notion_auth_controller.rb +25 -0
  50. data/app/controllers/collavre/passwords_controller.rb +37 -0
  51. data/app/controllers/collavre/plans_controller.rb +110 -0
  52. data/app/controllers/collavre/sessions_controller.rb +57 -0
  53. data/app/controllers/collavre/topics_controller.rb +58 -0
  54. data/app/controllers/collavre/user_themes_controller.rb +58 -0
  55. data/app/controllers/collavre/users_controller.rb +390 -0
  56. data/app/helpers/collavre/application_helper.rb +4 -0
  57. data/app/helpers/collavre/comments_helper.rb +9 -0
  58. data/app/helpers/collavre/creatives_helper.rb +343 -0
  59. data/app/helpers/collavre/navigation_helper.rb +163 -0
  60. data/app/helpers/collavre/user_themes_helper.rb +4 -0
  61. data/app/javascript/collavre.js +26 -0
  62. data/app/javascript/components/InlineLexicalEditor.jsx +889 -0
  63. data/app/javascript/components/LinkPopup.jsx +112 -0
  64. data/app/javascript/components/creative_tree_row.js +503 -0
  65. data/app/javascript/components/plugins/attachment_cleanup_plugin.jsx +95 -0
  66. data/app/javascript/components/plugins/image_upload_plugin.jsx +162 -0
  67. data/app/javascript/controllers/click_target_controller.js +13 -0
  68. data/app/javascript/controllers/comment_controller.js +162 -0
  69. data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +68 -0
  70. data/app/javascript/controllers/comments/form_controller.js +530 -0
  71. data/app/javascript/controllers/comments/list_controller.js +715 -0
  72. data/app/javascript/controllers/comments/mention_menu_controller.js +41 -0
  73. data/app/javascript/controllers/comments/popup_controller.js +385 -0
  74. data/app/javascript/controllers/comments/presence_controller.js +311 -0
  75. data/app/javascript/controllers/comments/topics_controller.js +338 -0
  76. data/app/javascript/controllers/common_popup_controller.js +55 -0
  77. data/app/javascript/controllers/creatives/drag_drop_controller.js +45 -0
  78. data/app/javascript/controllers/creatives/expansion_controller.js +222 -0
  79. data/app/javascript/controllers/creatives/import_controller.js +116 -0
  80. data/app/javascript/controllers/creatives/row_editor_controller.js +8 -0
  81. data/app/javascript/controllers/creatives/select_mode_controller.js +231 -0
  82. data/app/javascript/controllers/creatives/set_plan_modal_controller.js +107 -0
  83. data/app/javascript/controllers/creatives/tree_controller.js +218 -0
  84. data/app/javascript/controllers/index.js +79 -0
  85. data/app/javascript/controllers/link_creative_controller.js +91 -0
  86. data/app/javascript/controllers/popup_menu_controller.js +82 -0
  87. data/app/javascript/controllers/progress_filter_controller.js +35 -0
  88. data/app/javascript/controllers/reaction_picker_controller.js +107 -0
  89. data/app/javascript/controllers/share_invite_controller.js +15 -0
  90. data/app/javascript/controllers/share_user_search_controller.js +121 -0
  91. data/app/javascript/controllers/tabs_controller.js +43 -0
  92. data/app/javascript/creatives/drag_drop/dom.js +170 -0
  93. data/app/javascript/creatives/drag_drop/event_handlers.js +846 -0
  94. data/app/javascript/creatives/drag_drop/indicator.js +35 -0
  95. data/app/javascript/creatives/drag_drop/operations.js +116 -0
  96. data/app/javascript/creatives/drag_drop/state.js +31 -0
  97. data/app/javascript/creatives/tree_renderer.js +248 -0
  98. data/app/javascript/lib/api/__tests__/queue_manager.test.js +153 -0
  99. data/app/javascript/lib/api/creatives.js +79 -0
  100. data/app/javascript/lib/api/csrf_fetch.js +22 -0
  101. data/app/javascript/lib/api/drag_drop.js +31 -0
  102. data/app/javascript/lib/api/queue_manager.js +423 -0
  103. data/app/javascript/lib/apply_lexical_styles.js +15 -0
  104. data/app/javascript/lib/common_popup.js +195 -0
  105. data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +91 -0
  106. data/app/javascript/lib/lexical/__tests__/attachment_payload.test.js +194 -0
  107. data/app/javascript/lib/lexical/action_text_attachment_node.js +459 -0
  108. data/app/javascript/lib/lexical/attachment_node.jsx +170 -0
  109. data/app/javascript/lib/lexical/attachment_payload.js +293 -0
  110. data/app/javascript/lib/lexical/dom_attachment_utils.js +66 -0
  111. data/app/javascript/lib/lexical/image_node.jsx +159 -0
  112. data/app/javascript/lib/lexical/style_attributes.js +40 -0
  113. data/app/javascript/lib/responsive_images.js +54 -0
  114. data/app/javascript/lib/turbo_stream_actions.js +33 -0
  115. data/app/javascript/lib/utils/markdown.js +23 -0
  116. data/app/javascript/modules/creative_guide.js +53 -0
  117. data/app/javascript/modules/creative_row_editor.js +1841 -0
  118. data/app/javascript/modules/creative_row_swipe.js +43 -0
  119. data/app/javascript/modules/creatives.js +15 -0
  120. data/app/javascript/modules/export_to_markdown.js +34 -0
  121. data/app/javascript/modules/inbox_panel.js +226 -0
  122. data/app/javascript/modules/lexical_inline_editor.jsx +133 -0
  123. data/app/javascript/modules/mention_menu.js +77 -0
  124. data/app/javascript/modules/plans_menu.js +39 -0
  125. data/app/javascript/modules/plans_timeline.js +397 -0
  126. data/app/javascript/modules/share_modal.js +73 -0
  127. data/app/javascript/modules/share_user_popup.js +77 -0
  128. data/app/javascript/modules/slide_view.js +163 -0
  129. data/app/javascript/services/cable.js +32 -0
  130. data/app/javascript/slide_view.js +2 -0
  131. data/app/javascript/utils/caret_position.js +42 -0
  132. data/app/javascript/utils/clipboard.js +40 -0
  133. data/app/jobs/collavre/ai_agent_job.rb +27 -0
  134. data/app/jobs/collavre/inbox_summary_job.rb +24 -0
  135. data/app/jobs/collavre/notion_export_job.rb +30 -0
  136. data/app/jobs/collavre/notion_sync_job.rb +48 -0
  137. data/app/jobs/collavre/permission_cache_cleanup_job.rb +36 -0
  138. data/app/jobs/collavre/permission_cache_job.rb +71 -0
  139. data/app/jobs/collavre/push_notification_job.rb +86 -0
  140. data/app/mailers/collavre/application_mailer.rb +17 -0
  141. data/app/mailers/collavre/creative_mailer.rb +9 -0
  142. data/app/mailers/collavre/email_verification_mailer.rb +20 -0
  143. data/app/mailers/collavre/inbox_mailer.rb +19 -0
  144. data/app/mailers/collavre/invitation_mailer.rb +16 -0
  145. data/app/mailers/collavre/passwords_mailer.rb +10 -0
  146. data/app/models/collavre/activity_log.rb +13 -0
  147. data/app/models/collavre/application_record.rb +5 -0
  148. data/app/models/collavre/calendar_event.rb +20 -0
  149. data/app/models/collavre/comment.rb +307 -0
  150. data/app/models/collavre/comment_presence_store.rb +30 -0
  151. data/app/models/collavre/comment_reaction.rb +11 -0
  152. data/app/models/collavre/comment_read_pointer.rb +26 -0
  153. data/app/models/collavre/contact.rb +23 -0
  154. data/app/models/collavre/creative.rb +413 -0
  155. data/app/models/collavre/creative_expanded_state.rb +11 -0
  156. data/app/models/collavre/creative_share.rb +122 -0
  157. data/app/models/collavre/creative_shares_cache.rb +18 -0
  158. data/app/models/collavre/current.rb +14 -0
  159. data/app/models/collavre/device.rb +13 -0
  160. data/app/models/collavre/email.rb +14 -0
  161. data/app/models/collavre/github_account.rb +10 -0
  162. data/app/models/collavre/github_repository_link.rb +19 -0
  163. data/app/models/collavre/inbox_item.rb +95 -0
  164. data/app/models/collavre/invitation.rb +22 -0
  165. data/app/models/collavre/label.rb +47 -0
  166. data/app/models/collavre/mcp_tool.rb +30 -0
  167. data/app/models/collavre/notion_account.rb +17 -0
  168. data/app/models/collavre/notion_block_link.rb +10 -0
  169. data/app/models/collavre/notion_page_link.rb +19 -0
  170. data/app/models/collavre/plan.rb +20 -0
  171. data/app/models/collavre/session.rb +24 -0
  172. data/app/models/collavre/system_setting.rb +144 -0
  173. data/app/models/collavre/tag.rb +10 -0
  174. data/app/models/collavre/task.rb +10 -0
  175. data/app/models/collavre/task_action.rb +10 -0
  176. data/app/models/collavre/topic.rb +12 -0
  177. data/app/models/collavre/user.rb +174 -0
  178. data/app/models/collavre/user_theme.rb +10 -0
  179. data/app/models/collavre/variation.rb +5 -0
  180. data/app/models/collavre/webauthn_credential.rb +11 -0
  181. data/app/services/collavre/ai_agent_service.rb +193 -0
  182. data/app/services/collavre/ai_client.rb +183 -0
  183. data/app/services/collavre/ai_system_prompt_renderer.rb +38 -0
  184. data/app/services/collavre/auto_theme_generator.rb +198 -0
  185. data/app/services/collavre/comment_link_formatter.rb +60 -0
  186. data/app/services/collavre/comments/action_executor.rb +262 -0
  187. data/app/services/collavre/comments/action_validator.rb +58 -0
  188. data/app/services/collavre/comments/calendar_command.rb +97 -0
  189. data/app/services/collavre/comments/command_processor.rb +37 -0
  190. data/app/services/collavre/comments/mcp_command.rb +109 -0
  191. data/app/services/collavre/comments/mcp_command_builder.rb +32 -0
  192. data/app/services/collavre/creatives/filter_pipeline.rb +196 -0
  193. data/app/services/collavre/creatives/filters/assignee_filter.rb +30 -0
  194. data/app/services/collavre/creatives/filters/base_filter.rb +24 -0
  195. data/app/services/collavre/creatives/filters/comment_filter.rb +21 -0
  196. data/app/services/collavre/creatives/filters/date_filter.rb +58 -0
  197. data/app/services/collavre/creatives/filters/progress_filter.rb +25 -0
  198. data/app/services/collavre/creatives/filters/search_filter.rb +28 -0
  199. data/app/services/collavre/creatives/filters/tag_filter.rb +16 -0
  200. data/app/services/collavre/creatives/importer.rb +47 -0
  201. data/app/services/collavre/creatives/index_query.rb +191 -0
  202. data/app/services/collavre/creatives/path_exporter.rb +131 -0
  203. data/app/services/collavre/creatives/permission_cache_builder.rb +194 -0
  204. data/app/services/collavre/creatives/permission_checker.rb +42 -0
  205. data/app/services/collavre/creatives/plan_tagger.rb +53 -0
  206. data/app/services/collavre/creatives/progress_service.rb +89 -0
  207. data/app/services/collavre/creatives/reorderer.rb +187 -0
  208. data/app/services/collavre/creatives/tree_builder.rb +231 -0
  209. data/app/services/collavre/creatives/tree_formatter.rb +36 -0
  210. data/app/services/collavre/gemini_parent_recommender.rb +77 -0
  211. data/app/services/collavre/github/client.rb +112 -0
  212. data/app/services/collavre/github/pull_request_analyzer.rb +280 -0
  213. data/app/services/collavre/github/pull_request_processor.rb +181 -0
  214. data/app/services/collavre/github/webhook_provisioner.rb +130 -0
  215. data/app/services/collavre/google_calendar_service.rb +149 -0
  216. data/app/services/collavre/link_preview_fetcher.rb +230 -0
  217. data/app/services/collavre/markdown_importer.rb +202 -0
  218. data/app/services/collavre/mcp_service.rb +217 -0
  219. data/app/services/collavre/notion_client.rb +231 -0
  220. data/app/services/collavre/notion_creative_exporter.rb +296 -0
  221. data/app/services/collavre/notion_service.rb +249 -0
  222. data/app/services/collavre/ppt_importer.rb +76 -0
  223. data/app/services/collavre/ruby_llm_interaction_logger.rb +34 -0
  224. data/app/services/collavre/system_events/context_builder.rb +41 -0
  225. data/app/services/collavre/system_events/dispatcher.rb +19 -0
  226. data/app/services/collavre/system_events/router.rb +72 -0
  227. data/app/services/collavre/tools/creative_retrieval_service.rb +138 -0
  228. data/app/views/admin/shared/_tabs.html.erb +4 -0
  229. data/app/views/collavre/comments/_activity_log_details.html.erb +23 -0
  230. data/app/views/collavre/comments/_comment.html.erb +147 -0
  231. data/app/views/collavre/comments/_comments_popup.html.erb +46 -0
  232. data/app/views/collavre/comments/_list.html.erb +10 -0
  233. data/app/views/collavre/comments/_presence_avatars.html.erb +8 -0
  234. data/app/views/collavre/comments/_reaction_picker.html.erb +15 -0
  235. data/app/views/collavre/comments/_read_receipts.html.erb +19 -0
  236. data/app/views/collavre/creatives/_add_button.html.erb +20 -0
  237. data/app/views/collavre/creatives/_delete_button.html.erb +12 -0
  238. data/app/views/collavre/creatives/_github_integration_modal.html.erb +77 -0
  239. data/app/views/collavre/creatives/_import_upload_zone.html.erb +10 -0
  240. data/app/views/collavre/creatives/_inline_edit_form.html.erb +89 -0
  241. data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +24 -0
  242. data/app/views/collavre/creatives/_notion_integration_modal.html.erb +90 -0
  243. data/app/views/collavre/creatives/_set_plan_modal.html.erb +25 -0
  244. data/app/views/collavre/creatives/_share_button.html.erb +55 -0
  245. data/app/views/collavre/creatives/edit.html.erb +4 -0
  246. data/app/views/collavre/creatives/index.html.erb +192 -0
  247. data/app/views/collavre/creatives/new.html.erb +4 -0
  248. data/app/views/collavre/creatives/show.html.erb +29 -0
  249. data/app/views/collavre/creatives/slide_view.html.erb +20 -0
  250. data/app/views/collavre/email_verification_mailer/verify.html.erb +4 -0
  251. data/app/views/collavre/email_verification_mailer/verify.text.erb +2 -0
  252. data/app/views/collavre/emails/index.html.erb +19 -0
  253. data/app/views/collavre/emails/show.html.erb +5 -0
  254. data/app/views/collavre/inbox_items/_item.html.erb +18 -0
  255. data/app/views/collavre/inbox_items/_items.html.erb +3 -0
  256. data/app/views/collavre/inbox_items/_list.html.erb +7 -0
  257. data/app/views/collavre/inbox_items/index.html.erb +1 -0
  258. data/app/views/collavre/inbox_mailer/daily_summary.html.erb +11 -0
  259. data/app/views/collavre/inbox_mailer/daily_summary.text.erb +8 -0
  260. data/app/views/collavre/invitation_mailer/invite.html.erb +5 -0
  261. data/app/views/collavre/invitation_mailer/invite.text.erb +3 -0
  262. data/app/views/collavre/invites/show.html.erb +8 -0
  263. data/app/views/collavre/passwords/edit.html.erb +12 -0
  264. data/app/views/collavre/passwords/new.html.erb +17 -0
  265. data/app/views/collavre/passwords_mailer/reset.html.erb +4 -0
  266. data/app/views/collavre/passwords_mailer/reset.text.erb +2 -0
  267. data/app/views/collavre/sessions/new.html.erb +27 -0
  268. data/app/views/collavre/sessions/providers/_google.html.erb +7 -0
  269. data/app/views/collavre/sessions/providers/_passkey.html.erb +3 -0
  270. data/app/views/collavre/shared/_link_creative_modal.html.erb +6 -0
  271. data/app/views/collavre/shared/_navigation.html.erb +37 -0
  272. data/app/views/collavre/shared/navigation/_help_button.html.erb +1 -0
  273. data/app/views/collavre/shared/navigation/_inbox_button.html.erb +5 -0
  274. data/app/views/collavre/shared/navigation/_mobile_inbox_button.html.erb +3 -0
  275. data/app/views/collavre/shared/navigation/_mobile_plans_button.html.erb +1 -0
  276. data/app/views/collavre/shared/navigation/_panels.html.erb +18 -0
  277. data/app/views/collavre/shared/navigation/_plans_button.html.erb +3 -0
  278. data/app/views/collavre/shared/navigation/_search_form.html.erb +6 -0
  279. data/app/views/collavre/user_themes/index.html.erb +75 -0
  280. data/app/views/collavre/users/_app_header.html.erb +5 -0
  281. data/app/views/collavre/users/_contact_management.html.erb +77 -0
  282. data/app/views/collavre/users/_id_pwd_fields.html.erb +27 -0
  283. data/app/views/collavre/users/edit_ai.html.erb +79 -0
  284. data/app/views/collavre/users/edit_password.html.erb +27 -0
  285. data/app/views/collavre/users/index.html.erb +80 -0
  286. data/app/views/collavre/users/new.html.erb +33 -0
  287. data/app/views/collavre/users/new_ai.html.erb +87 -0
  288. data/app/views/collavre/users/passkeys.html.erb +43 -0
  289. data/app/views/collavre/users/show.html.erb +143 -0
  290. data/app/views/inbox/badge_component/_count.html.erb +2 -0
  291. data/app/views/layouts/collavre/slide.html.erb +29 -0
  292. data/config/locales/comments.en.yml +114 -0
  293. data/config/locales/comments.ko.yml +110 -0
  294. data/config/locales/contacts.en.yml +16 -0
  295. data/config/locales/contacts.ko.yml +16 -0
  296. data/config/locales/creatives.en.yml +183 -0
  297. data/config/locales/creatives.ko.yml +164 -0
  298. data/config/locales/invites.en.yml +19 -0
  299. data/config/locales/invites.ko.yml +17 -0
  300. data/config/locales/notifications.en.yml +8 -0
  301. data/config/locales/notifications.ko.yml +8 -0
  302. data/config/locales/plans.en.yml +12 -0
  303. data/config/locales/plans.ko.yml +19 -0
  304. data/config/locales/themes.en.yml +29 -0
  305. data/config/locales/themes.ko.yml +27 -0
  306. data/config/locales/users.en.yml +151 -0
  307. data/config/locales/users.ko.yml +146 -0
  308. data/config/routes.rb +92 -0
  309. data/db/migrate/20241201000000_create_notion_integrations.rb +29 -0
  310. data/db/migrate/20250128110017_create_creatives.rb +11 -0
  311. data/db/migrate/20250128120122_create_users.rb +11 -0
  312. data/db/migrate/20250128120123_create_sessions.rb +11 -0
  313. data/db/migrate/20250128123633_create_subscribers.rb +10 -0
  314. data/db/migrate/20250312000000_create_notion_block_links.rb +16 -0
  315. data/db/migrate/20250312010000_allow_multiple_notion_blocks_per_creative.rb +5 -0
  316. data/db/migrate/20250522115048_remove_name_from_creatives.rb +5 -0
  317. data/db/migrate/20250522190651_add_parent_id_to_creatives.rb +5 -0
  318. data/db/migrate/20250523133100_rename_inventory_count_to_progress.rb +13 -0
  319. data/db/migrate/20250523133101_add_sequence_to_creatives.rb +5 -0
  320. data/db/migrate/20250525205100_add_user_id_to_creatives.rb +10 -0
  321. data/db/migrate/20250527014217_create_creative_shares.rb +12 -0
  322. data/db/migrate/20250528142349_add_origin_id_to_creatives.rb +5 -0
  323. data/db/migrate/20250530060200_create_tags.rb +9 -0
  324. data/db/migrate/20250531105150_add_value_to_tags.rb +5 -0
  325. data/db/migrate/20250531140142_create_labels.rb +12 -0
  326. data/db/migrate/20250531140145_change_tag_to_label_reference.rb +6 -0
  327. data/db/migrate/20250601000000_create_comments.rb +10 -0
  328. data/db/migrate/20250601061830_drop_plans_and_variations.rb +6 -0
  329. data/db/migrate/20250604122600_add_owner_to_labels.rb +5 -0
  330. data/db/migrate/20250606000000_rename_email_address_to_email_in_users.rb +7 -0
  331. data/db/migrate/20250606150329_create_creative_hierarchies.rb +16 -0
  332. data/db/migrate/20250610142000_create_creative_expanded_states.rb +11 -0
  333. data/db/migrate/20250611030138_create_invitations.rb +15 -0
  334. data/db/migrate/20250611105524_create_inbox_items.rb +14 -0
  335. data/db/migrate/20250612150000_update_permissions.rb +43 -0
  336. data/db/migrate/20250612232913_add_email_verified_at_to_users.rb +5 -0
  337. data/db/migrate/20250616065905_add_avatar_to_users.rb +5 -0
  338. data/db/migrate/20250617092111_add_display_level_to_users.rb +5 -0
  339. data/db/migrate/20250620004558_create_emails.rb +13 -0
  340. data/db/migrate/20250621000000_create_comment_read_pointers.rb +11 -0
  341. data/db/migrate/20250622000000_add_completion_mark_to_users.rb +5 -0
  342. data/db/migrate/20250623000000_add_theme_to_users.rb +5 -0
  343. data/db/migrate/20250624000000_add_name_to_users.rb +18 -0
  344. data/db/migrate/20250714190000_create_devices.rb +17 -0
  345. data/db/migrate/20250715120000_add_notifications_enabled_to_users.rb +5 -0
  346. data/db/migrate/20250823000000_add_no_access_permission.rb +25 -0
  347. data/db/migrate/20250826000000_add_calendar_id_to_users.rb +5 -0
  348. data/db/migrate/20250827000000_add_google_oauth_tokens_to_users.rb +8 -0
  349. data/db/migrate/20250827061238_add_timezone_to_users.rb +5 -0
  350. data/db/migrate/20250828000000_create_calendar_events.rb +16 -0
  351. data/db/migrate/20250828060000_add_creative_to_calendar_events.rb +5 -0
  352. data/db/migrate/20250830141052_add_locale_to_users.rb +5 -0
  353. data/db/migrate/20250830141101_add_message_key_to_inbox_items.rb +6 -0
  354. data/db/migrate/20250902025423_remove_description_and_featured_image_from_creatives.rb +6 -0
  355. data/db/migrate/20250910000000_add_private_to_comments.rb +5 -0
  356. data/db/migrate/20250910105640_migrate_write_permissions_to_admin.rb +17 -0
  357. data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +27 -0
  358. data/db/migrate/20250923002959_deduplicate_device_fcm_tokens.rb +25 -0
  359. data/db/migrate/20250924000000_add_system_admin_to_users.rb +6 -0
  360. data/db/migrate/20250925000000_create_github_integrations.rb +26 -0
  361. data/db/migrate/20250927000000_add_webhook_secret_to_github_repository_links.rb +29 -0
  362. data/db/migrate/20250928000000_add_action_and_approver_to_comments.rb +6 -0
  363. data/db/migrate/20250928010000_add_action_execution_tracking_to_comments.rb +6 -0
  364. data/db/migrate/20250928105957_add_github_gemini_prompt_to_creatives.rb +5 -0
  365. data/db/migrate/20250929000000_add_comment_and_creative_refs_to_inbox_items.rb +6 -0
  366. data/db/migrate/20251001000001_create_contacts.rb +71 -0
  367. data/db/migrate/20251002000000_add_shared_by_to_creative_shares.rb +14 -0
  368. data/db/migrate/20251124120902_add_ai_fields_to_users.rb +7 -0
  369. data/db/migrate/20251124122218_add_created_by_id_to_users.rb +5 -0
  370. data/db/migrate/20251124124521_add_llm_api_key_to_users.rb +5 -0
  371. data/db/migrate/20251124130000_add_searchable_to_users.rb +6 -0
  372. data/db/migrate/20251125072705_migrate_linked_creative_children_to_origin.rb +28 -0
  373. data/db/migrate/20251126040752_add_description_to_creatives.rb +75 -0
  374. data/db/migrate/20251127000000_move_comments_from_linked_creatives_to_origins.rb +18 -0
  375. data/db/migrate/20251201073823_add_tools_to_users.rb +5 -0
  376. data/db/migrate/20251202062715_create_mcp_tools.rb +16 -0
  377. data/db/migrate/20251204125754_create_tasks_and_task_actions.rb +23 -0
  378. data/db/migrate/20251204125756_add_routing_expression_to_users.rb +5 -0
  379. data/db/migrate/20251204161133_set_default_routing_expression_for_ai_agents.rb +13 -0
  380. data/db/migrate/20251211033025_nullify_self_referencing_origins.rb +9 -0
  381. data/db/migrate/20251211080040_create_topics_and_add_topic_to_comments.rb +15 -0
  382. data/db/migrate/20251215143500_create_activity_logs.rb +16 -0
  383. data/db/migrate/20251222125727_create_webauthn_credentials.rb +14 -0
  384. data/db/migrate/20251222125839_add_webauthn_id_to_users.rb +5 -0
  385. data/db/migrate/20251223022315_create_comment_reactions.rb +13 -0
  386. data/db/migrate/20251223072625_create_user_themes.rb +11 -0
  387. data/db/migrate/20251230074456_add_creative_id_to_labels.rb +5 -0
  388. data/db/migrate/20251230113607_refactor_labels.rb +38 -0
  389. data/db/migrate/20251231010012_backfill_tags_for_labels.rb +15 -0
  390. data/db/migrate/20251231013234_drop_subscribers.rb +9 -0
  391. data/db/migrate/20260106090544_allow_null_user_id_in_creative_shares.rb +6 -0
  392. data/db/migrate/20260106160643_create_system_settings.rb +11 -0
  393. data/db/migrate/20260116000000_create_creative_shares_cache.rb +15 -0
  394. data/db/migrate/20260116000001_populate_creative_shares_cache.rb +23 -0
  395. data/db/migrate/20260119022933_make_source_share_id_nullable_in_creative_shares_caches.rb +7 -0
  396. data/db/migrate/20260119023446_populate_owner_cache_entries.rb +25 -0
  397. data/db/migrate/20260119100000_add_account_lockout_to_users.rb +6 -0
  398. data/db/migrate/20260119110000_add_last_active_at_to_sessions.rb +6 -0
  399. data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +60 -0
  400. data/db/migrate/20260120162259_remove_fk_from_creative_shares_caches.rb +7 -0
  401. data/db/migrate/20260120163856_remove_timestamps_from_creative_shares_caches.rb +6 -0
  402. data/lib/collavre/configuration.rb +14 -0
  403. data/lib/collavre/engine.rb +77 -0
  404. data/lib/collavre/user_extensions.rb +29 -0
  405. data/lib/collavre/version.rb +3 -0
  406. data/lib/collavre.rb +26 -0
  407. data/lib/generators/collavre/install/install_generator.rb +105 -0
  408. data/lib/generators/collavre/install/templates/build.cjs.tt +100 -0
  409. data/lib/tasks/collavre_assets.rake +15 -0
  410. metadata +591 -0
@@ -0,0 +1,95 @@
1
+ module Collavre
2
+ class InboxItem < ApplicationRecord
3
+ self.table_name = "inbox_items"
4
+
5
+ # Use non-namespaced partial path for backward compatibility
6
+ def to_partial_path
7
+ "inbox_items/inbox_item"
8
+ end
9
+
10
+ INTERPOLATION_PATTERN = /%%|%\{([\w|]+)\}|%<(\w+)>[^\d]*?\d*\.?\d*[bBdiouxXeEfgGcps]/.freeze
11
+
12
+ belongs_to :owner, class_name: Collavre.configuration.user_class_name
13
+ belongs_to :comment, class_name: "Collavre::Comment", optional: true
14
+ belongs_to :creative, class_name: "Collavre::Creative", optional: true
15
+
16
+ after_commit :broadcast_badge_update, on: %i[create update destroy]
17
+ after_create_commit :enqueue_push_notification
18
+
19
+ attribute :state, :string, default: "new"
20
+ validates :state, inclusion: { in: %w[new read archived] }
21
+ validates :message_key, presence: true
22
+
23
+ scope :new_items, -> { where(state: "new") }
24
+ scope :read_items, -> { where(state: "read") }
25
+
26
+
27
+ def read?
28
+ state == "read"
29
+ end
30
+
31
+ def localized_message(locale: I18n.locale)
32
+ msg =
33
+ if message_key.present?
34
+ params = message_params || {}
35
+ translate_message(message_key, params.symbolize_keys, locale: locale)
36
+ else
37
+ message
38
+ end
39
+
40
+ msg&.gsub("&nbsp;", " ")&.gsub("\u00A0", " ")
41
+ end
42
+
43
+ private
44
+
45
+ def translate_message(message_key, params, locale:)
46
+ I18n.t(message_key, **params, locale: locale)
47
+ rescue I18n::MissingInterpolationArgument => e
48
+ missing_keys = extract_missing_keys(e.string, params)
49
+ fallback_params =
50
+ missing_keys.index_with do |missing_key|
51
+ default_interpolation_value(missing_key, locale: locale)
52
+ end
53
+
54
+ I18n.t(message_key, **params.merge(fallback_params), locale: locale)
55
+ end
56
+
57
+ def extract_missing_keys(translation_string, params)
58
+ interpolations =
59
+ translation_string.to_s.scan(INTERPOLATION_PATTERN).map do |match|
60
+ match.compact.first&.to_sym
61
+ end
62
+
63
+ interpolations.compact.uniq - params.keys
64
+ end
65
+
66
+ def default_interpolation_value(key, locale:)
67
+ case key.to_sym
68
+ when :comment_content
69
+ I18n.t("collavre.inbox.comment_content_unavailable", locale: locale, default: "")
70
+ else
71
+ ""
72
+ end
73
+ end
74
+
75
+ def broadcast_badge_update
76
+ # Recompute the new count for this owner:
77
+ new_count = InboxItem.where(owner: owner, state: "new").count
78
+
79
+ # Use Turbo::StreamsChannel to broadcast replace to that user's inbox stream:
80
+ %w[desktop-inbox-badge mobile-inbox-badge].each do |target_id|
81
+ Turbo::StreamsChannel.broadcast_replace_to(
82
+ [ "inbox", owner ],
83
+ target: target_id,
84
+ partial: "inbox/badge_component/count",
85
+ locals: { count: new_count, badge_id: target_id, show_zero: false }
86
+ )
87
+ end
88
+ end
89
+
90
+ def enqueue_push_notification
91
+ msg = localized_message(locale: owner.locale || "en")
92
+ PushNotificationJob.perform_later(owner_id, message: msg, link: link)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,22 @@
1
+ module Collavre
2
+ class Invitation < ApplicationRecord
3
+ self.table_name = "invitations"
4
+
5
+ belongs_to :inviter, class_name: Collavre.configuration.user_class_name
6
+ belongs_to :creative, class_name: "Collavre::Creative"
7
+
8
+ enum :permission, CreativeShare.permissions
9
+
10
+ generates_token_for :invite, expires_in: 15.days
11
+
12
+ validates :expires_at, presence: true
13
+
14
+ before_validation :set_default_expires_at, on: :create
15
+
16
+ private
17
+
18
+ def set_default_expires_at
19
+ self.expires_at ||= 15.days.from_now
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ module Collavre
2
+ class Label < ApplicationRecord
3
+ self.table_name = "labels"
4
+ # Use short class names for STI to avoid namespace issues during hot reload
5
+ self.store_full_sti_class = false
6
+
7
+ has_many :tags, class_name: "Collavre::Tag", dependent: :destroy
8
+ belongs_to :owner, class_name: Collavre.configuration.user_class_name, optional: true
9
+ belongs_to :creative, class_name: "Collavre::Creative", optional: false
10
+
11
+ delegate :description, to: :creative, allow_nil: true
12
+ alias_method :name, :description
13
+
14
+ # STI: Plan, Version, etc subclasses use type column
15
+ # creative_id, value, target_date etc attributes included
16
+
17
+ # Resolve short STI class names to namespaced versions
18
+ # Use constantize to always get fresh class reference after hot reload
19
+ def self.find_sti_class(type_name)
20
+ type_name = "Collavre::#{type_name}" unless type_name.start_with?("Collavre::")
21
+ type_name.constantize
22
+ end
23
+
24
+ after_create :create_auto_tag
25
+
26
+ # Check if user has permission to read this label
27
+ # If linked to a Creative, delegates to Creative's permission system
28
+ # Otherwise falls back to owner-based or public (nil owner) visibility
29
+ def readable_by?(user)
30
+ return true if owner_id.present? && owner_id == user&.id
31
+
32
+ if creative_id.present? && creative
33
+ creative.has_permission?(user, :read)
34
+ else
35
+ owner_id.nil?
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def create_auto_tag
42
+ return unless creative_id.present?
43
+
44
+ tags.create!(creative_id: creative_id)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,30 @@
1
+ module Collavre
2
+ class McpTool < ApplicationRecord
3
+ self.table_name = "mcp_tools"
4
+
5
+ belongs_to :creative, class_name: "Collavre::Creative"
6
+
7
+ validates :name, presence: true, uniqueness: true
8
+ validates :source_code, presence: true
9
+
10
+ after_destroy :unregister_tool
11
+
12
+ scope :active, -> { where.not(approved_at: nil) }
13
+
14
+ def active?
15
+ approved_at.present?
16
+ end
17
+
18
+ def approve!
19
+ # Register the tool immediately upon approval
20
+ ::McpService.register_tool_from_source(source_code)
21
+ update!(approved_at: Time.current)
22
+ end
23
+
24
+ private
25
+
26
+ def unregister_tool
27
+ ::McpService.delete_tool(name)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ module Collavre
2
+ class NotionAccount < ApplicationRecord
3
+ self.table_name = "notion_accounts"
4
+
5
+ belongs_to :user, class_name: Collavre.configuration.user_class_name
6
+ has_many :notion_page_links, class_name: "Collavre::NotionPageLink", dependent: :destroy
7
+
8
+ encrypts :token, deterministic: false
9
+
10
+ validates :notion_uid, :token, presence: true
11
+ validates :notion_uid, uniqueness: true
12
+
13
+ def expired?
14
+ token_expires_at.present? && token_expires_at < Time.current
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+ module Collavre
2
+ class NotionBlockLink < ApplicationRecord
3
+ self.table_name = "notion_block_links"
4
+
5
+ belongs_to :notion_page_link, class_name: "Collavre::NotionPageLink"
6
+ belongs_to :creative, class_name: "Collavre::Creative"
7
+
8
+ validates :block_id, presence: true, uniqueness: { scope: :notion_page_link_id }
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ module Collavre
2
+ class NotionPageLink < ApplicationRecord
3
+ self.table_name = "notion_page_links"
4
+
5
+ belongs_to :creative, class_name: "Collavre::Creative"
6
+ belongs_to :notion_account, class_name: "Collavre::NotionAccount"
7
+ has_many :notion_block_links, class_name: "Collavre::NotionBlockLink", dependent: :destroy
8
+
9
+ validates :page_id, :page_title, presence: true
10
+ validates :page_id, uniqueness: true
11
+
12
+ scope :recent, -> { order(last_synced_at: :desc) }
13
+ scope :synced, -> { where.not(last_synced_at: nil) }
14
+
15
+ def mark_synced!
16
+ touch(:last_synced_at)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ require "set"
2
+
3
+ module Collavre
4
+ class Plan < Label
5
+ validates :target_date, presence: true
6
+
7
+ def progress(_user = nil)
8
+ tagged_ids = Tag.where(label_id: id).pluck(:creative_id)
9
+ return 0 if tagged_ids.empty?
10
+
11
+ root_ids = Creative.where(id: tagged_ids).map { |c| c.root.id }.uniq
12
+ roots = Creative.where(id: root_ids)
13
+ tagged_set = tagged_ids.to_set
14
+ values = roots.map { |c| c.progress_for_plan(tagged_set) }.compact
15
+ return 0 if values.empty?
16
+
17
+ values.sum.to_f / values.size
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ module Collavre
2
+ class Session < ApplicationRecord
3
+ self.table_name = "sessions"
4
+
5
+ belongs_to :user, class_name: "Collavre::User"
6
+
7
+ def expired?
8
+ return false unless Collavre::SystemSetting.session_timeout_enabled?
9
+ return true if last_active_at.nil?
10
+
11
+ last_active_at < Collavre::SystemSetting.session_timeout.ago
12
+ end
13
+
14
+ def touch_activity!
15
+ update_column(:last_active_at, Time.current) if should_touch_activity?
16
+ end
17
+
18
+ private
19
+
20
+ def should_touch_activity?
21
+ last_active_at.nil? || last_active_at < 1.minute.ago
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,144 @@
1
+ module Collavre
2
+ class SystemSetting < ApplicationRecord
3
+ self.table_name = "system_settings"
4
+
5
+ # Cache expiry time for settings
6
+ CACHE_EXPIRY = 5.minutes
7
+
8
+ # Default values for account lockout
9
+ DEFAULT_MAX_LOGIN_ATTEMPTS = 5
10
+ DEFAULT_LOCKOUT_DURATION_MINUTES = 30
11
+
12
+ # Default values for session timeout (in minutes, 0 = no timeout)
13
+ DEFAULT_SESSION_TIMEOUT_MINUTES = 0
14
+
15
+ # Default values for password policy
16
+ DEFAULT_PASSWORD_MIN_LENGTH = 8
17
+
18
+ # Default values for rate limiting
19
+ DEFAULT_PASSWORD_RESET_RATE_LIMIT = 5
20
+ DEFAULT_PASSWORD_RESET_RATE_PERIOD_MINUTES = 60
21
+ DEFAULT_API_RATE_LIMIT = 100
22
+ DEFAULT_API_RATE_PERIOD_MINUTES = 1
23
+
24
+ # Default values for creatives access
25
+ # By default, public access is allowed (false)
26
+ DEFAULT_CREATIVES_LOGIN_REQUIRED = false
27
+
28
+ # Default home page path (nil means use root_path "/")
29
+ DEFAULT_HOME_PAGE_PATH = nil
30
+
31
+ validates :key, presence: true, uniqueness: true
32
+
33
+ # Clear cache after save
34
+ after_commit :clear_cache
35
+
36
+ # Cached setting retrieval
37
+ def self.cached_value(key, default = nil)
38
+ Rails.cache.fetch("system_setting:#{key}", expires_in: CACHE_EXPIRY) do
39
+ find_by(key: key)&.value
40
+ end || default
41
+ end
42
+
43
+ def self.clear_all_cache
44
+ # Clear known setting keys
45
+ %w[
46
+ help_menu_link mcp_tool_approval_required max_login_attempts
47
+ lockout_duration_minutes session_timeout_minutes password_min_length
48
+ password_reset_rate_limit password_reset_rate_period_minutes
49
+ api_rate_limit api_rate_period_minutes auth_providers_disabled
50
+ creatives_login_required home_page_path
51
+ ].each { |k| Rails.cache.delete("system_setting:#{k}") }
52
+ end
53
+
54
+ private
55
+
56
+ def clear_cache
57
+ Rails.cache.delete("system_setting:#{key}")
58
+ # If key was changed, well also clear the old key's cache entry
59
+ if saved_change_to_key?
60
+ old_key = saved_change_to_key.first
61
+ Rails.cache.delete("system_setting:#{old_key}") if old_key.present?
62
+ end
63
+ end
64
+
65
+ def self.help_menu_link
66
+ cached_value("help_menu_link")
67
+ end
68
+
69
+ def self.creatives_login_required?
70
+ cached_value("creatives_login_required", DEFAULT_CREATIVES_LOGIN_REQUIRED.to_s) == "true"
71
+ end
72
+
73
+ def self.home_page_path
74
+ value = cached_value("home_page_path")
75
+ value.presence
76
+ end
77
+
78
+ def self.mcp_tool_approval_required?
79
+ if Current.mcp_tool_approval_required.nil?
80
+ Current.mcp_tool_approval_required = cached_value("mcp_tool_approval_required") == "true"
81
+ end
82
+ Current.mcp_tool_approval_required
83
+ end
84
+
85
+ # Account lockout settings
86
+ def self.max_login_attempts
87
+ cached_value("max_login_attempts")&.to_i || DEFAULT_MAX_LOGIN_ATTEMPTS
88
+ end
89
+
90
+ def self.lockout_duration_minutes
91
+ cached_value("lockout_duration_minutes")&.to_i || DEFAULT_LOCKOUT_DURATION_MINUTES
92
+ end
93
+
94
+ def self.lockout_duration
95
+ lockout_duration_minutes.minutes
96
+ end
97
+
98
+ # Password policy settings (capped at 72 due to bcrypt limit)
99
+ def self.password_min_length
100
+ value = cached_value("password_min_length")&.to_i
101
+ value = DEFAULT_PASSWORD_MIN_LENGTH if value.nil? || value < 1
102
+ [ value, 72 ].min
103
+ end
104
+
105
+ # Session timeout settings
106
+ def self.session_timeout_minutes
107
+ cached_value("session_timeout_minutes")&.to_i || DEFAULT_SESSION_TIMEOUT_MINUTES
108
+ end
109
+
110
+ def self.session_timeout_enabled?
111
+ session_timeout_minutes > 0
112
+ end
113
+
114
+ def self.session_timeout
115
+ session_timeout_minutes.minutes
116
+ end
117
+
118
+ # Rate limiting settings - Password Reset
119
+ def self.password_reset_rate_limit
120
+ cached_value("password_reset_rate_limit")&.to_i || DEFAULT_PASSWORD_RESET_RATE_LIMIT
121
+ end
122
+
123
+ def self.password_reset_rate_period_minutes
124
+ cached_value("password_reset_rate_period_minutes")&.to_i || DEFAULT_PASSWORD_RESET_RATE_PERIOD_MINUTES
125
+ end
126
+
127
+ def self.password_reset_rate_period
128
+ password_reset_rate_period_minutes.minutes
129
+ end
130
+
131
+ # Rate limiting settings - API
132
+ def self.api_rate_limit
133
+ cached_value("api_rate_limit")&.to_i || DEFAULT_API_RATE_LIMIT
134
+ end
135
+
136
+ def self.api_rate_period_minutes
137
+ cached_value("api_rate_period_minutes")&.to_i || DEFAULT_API_RATE_PERIOD_MINUTES
138
+ end
139
+
140
+ def self.api_rate_period
141
+ api_rate_period_minutes.minutes
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,10 @@
1
+ module Collavre
2
+ class Tag < ApplicationRecord
3
+ self.table_name = "tags"
4
+
5
+ belongs_to :label, class_name: "Collavre::Label"
6
+
7
+ validates :creative_id, presence: true
8
+ # value 컬럼이 추가되었습니다.
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Collavre
2
+ class Task < ApplicationRecord
3
+ self.table_name = "tasks"
4
+
5
+ belongs_to :agent, class_name: "Collavre::User"
6
+ has_many :task_actions, class_name: "Collavre::TaskAction", dependent: :destroy
7
+
8
+ validates :name, presence: true
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Collavre
2
+ class TaskAction < ApplicationRecord
3
+ self.table_name = "task_actions"
4
+
5
+ belongs_to :task, class_name: "Collavre::Task"
6
+
7
+ validates :action_type, presence: true
8
+ validates :status, presence: true
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ module Collavre
2
+ class Topic < ApplicationRecord
3
+ self.table_name = "topics"
4
+
5
+ belongs_to :creative, class_name: "Collavre::Creative"
6
+ belongs_to :user, class_name: Collavre.configuration.user_class_name
7
+
8
+ has_many :comments, class_name: "Collavre::Comment", dependent: :destroy
9
+
10
+ validates :name, presence: true, uniqueness: { scope: :creative_id }
11
+ end
12
+ end
@@ -0,0 +1,174 @@
1
+ module Collavre
2
+ class User < ApplicationRecord
3
+ self.table_name = "users"
4
+
5
+ has_many :user_themes, class_name: "Collavre::UserTheme", dependent: :destroy
6
+ DEFAULT_DISPLAY_LEVEL = 6
7
+
8
+ has_secure_password
9
+ has_many :sessions, class_name: "Collavre::Session", dependent: :destroy
10
+ has_many :oauth_applications, class_name: "Doorkeeper::Application", as: :owner, dependent: :destroy
11
+ has_many :access_grants, class_name: "Doorkeeper::AccessGrant", foreign_key: :resource_owner_id, dependent: :delete_all
12
+ has_many :access_tokens, class_name: "Doorkeeper::AccessToken", foreign_key: :resource_owner_id, dependent: :delete_all
13
+ has_many :webauthn_credentials, class_name: "Collavre::WebauthnCredential", dependent: :destroy
14
+ has_many :devices, class_name: "Collavre::Device", dependent: :destroy
15
+ has_many :emails, class_name: "Collavre::Email", dependent: :destroy
16
+ has_many :contacts, class_name: "Collavre::Contact", dependent: :destroy
17
+ has_many :contact_users, through: :contacts
18
+ has_many :contact_memberships, class_name: "Collavre::Contact", foreign_key: :contact_user_id, dependent: :destroy, inverse_of: :contact_user
19
+ has_one :github_account, class_name: "Collavre::GithubAccount", dependent: :destroy
20
+ has_one :notion_account, class_name: "Collavre::NotionAccount", dependent: :destroy
21
+ has_many :tasks, class_name: "Collavre::Task", foreign_key: :agent_id, dependent: :destroy
22
+
23
+ # Associations that reference creatives - must be destroyed BEFORE creatives
24
+ has_many :topics, class_name: "Collavre::Topic", dependent: :destroy
25
+ has_many :calendar_events, class_name: "Collavre::CalendarEvent", dependent: :destroy
26
+ has_many :comment_read_pointers, class_name: "Collavre::CommentReadPointer", dependent: :destroy
27
+ has_many :creative_expanded_states, class_name: "Collavre::CreativeExpandedState", dependent: :destroy
28
+ has_many :creative_shares, class_name: "Collavre::CreativeShare", dependent: :destroy
29
+ has_many :creative_shares_caches, class_name: "Collavre::CreativeSharesCache", dependent: :delete_all
30
+ has_many :shared_creative_shares, class_name: "Collavre::CreativeShare", foreign_key: :shared_by_id,
31
+ dependent: :nullify, inverse_of: :shared_by
32
+ has_many :inbox_items, class_name: "Collavre::InboxItem", foreign_key: :owner_id, dependent: :destroy, inverse_of: :owner
33
+ has_many :invitations, class_name: "Collavre::Invitation", foreign_key: :inviter_id, dependent: :destroy, inverse_of: :inviter
34
+ has_many :activity_logs, class_name: "Collavre::ActivityLog", dependent: :destroy
35
+ has_many :labels, class_name: "Collavre::Label", foreign_key: :owner_id, dependent: :destroy
36
+ has_many :comments, class_name: "Collavre::Comment", dependent: :destroy
37
+ has_many :action_executed_comments, class_name: "Collavre::Comment", foreign_key: :action_executed_by_id,
38
+ dependent: :nullify, inverse_of: :action_executed_by
39
+ has_many :approved_comments, class_name: "Collavre::Comment", foreign_key: :approver_id,
40
+ dependent: :nullify, inverse_of: :approver
41
+ has_many :comment_reactions, class_name: "Collavre::CommentReaction", dependent: :destroy
42
+
43
+ # Creatives must be destroyed AFTER all associations that reference them
44
+ has_many :creatives, class_name: "Collavre::Creative", dependent: :destroy
45
+
46
+ belongs_to :creator, class_name: "Collavre::User", foreign_key: "created_by_id", optional: true
47
+ has_many :created_ai_users, class_name: "Collavre::User", foreign_key: "created_by_id", dependent: :destroy
48
+
49
+ has_one_attached :avatar
50
+
51
+ attribute :display_level, :integer, default: DEFAULT_DISPLAY_LEVEL
52
+ attribute :completion_mark, :string, default: ""
53
+ attribute :theme, :string
54
+ attribute :calendar_id, :string
55
+ attribute :name, :string
56
+ attribute :notifications_enabled, :boolean
57
+ attribute :timezone, :string
58
+ attribute :locale, :string
59
+ attribute :system_admin, :boolean, default: false
60
+ attribute :searchable, :boolean, default: false
61
+
62
+ attribute :google_uid, :string
63
+ attribute :google_access_token, :string
64
+ attribute :google_refresh_token, :string
65
+ attribute :google_token_expires_at, :datetime
66
+
67
+ attribute :system_prompt, :string
68
+ attribute :llm_vendor, :string
69
+ attribute :llm_model, :string
70
+ attribute :llm_api_key, :string
71
+ attribute :tools, :json, default: -> { [] }
72
+
73
+ encrypts :llm_api_key, deterministic: false
74
+ encrypts :google_access_token, deterministic: false
75
+ encrypts :google_refresh_token, deterministic: false
76
+
77
+ SUPPORTED_LLM_MODELS = [
78
+ "gemini-2.5-flash",
79
+ "gemini-1.5-flash",
80
+ "gemini-1.5-pro"
81
+ ].freeze
82
+
83
+ def ai_user?
84
+ llm_vendor.present?
85
+ end
86
+
87
+ def self.mentionable_for(creative)
88
+ scope = where(searchable: true)
89
+ return scope unless creative
90
+
91
+ origin = creative.effective_origin
92
+ permitted_users = [ origin.user ].compact + origin.all_shared_users(:feedback).map(&:user)
93
+ permitted_ids = permitted_users.compact.map(&:id)
94
+ permitted_ids.any? ? scope.or(where(id: permitted_ids)) : scope
95
+ end
96
+
97
+ normalizes :email, with: ->(e) { e.strip.downcase }
98
+ normalizes :timezone, with: ->(tz) do
99
+ tz = tz.to_s.strip
100
+ next if tz.blank?
101
+ ActiveSupport::TimeZone[tz]&.tzinfo&.identifier || tz
102
+ end
103
+
104
+ validates :email, presence: true, uniqueness: true,
105
+ format: { with: URI::MailTo::EMAIL_REGEXP }
106
+ validates :name, presence: true
107
+ validates :display_level, numericality: { only_integer: true, greater_than: 0 }
108
+ validate :theme_accessibility
109
+ validate :password_meets_minimum_length
110
+ validates :timezone,
111
+ inclusion: { in: ActiveSupport::TimeZone.all.map { |z| z.tzinfo.identifier } },
112
+ allow_nil: true
113
+
114
+ generates_token_for :email_verification, expires_in: 1.day do
115
+ email
116
+ end
117
+
118
+ def email_verified?
119
+ email_verified_at.present?
120
+ end
121
+
122
+ def display_name
123
+ name.presence || email
124
+ end
125
+
126
+ # Account lockout methods
127
+ def locked?
128
+ locked_at.present? && locked_at > Collavre::SystemSetting.lockout_duration.ago
129
+ end
130
+
131
+ def lock_account!
132
+ update_columns(locked_at: Time.current)
133
+ end
134
+
135
+ def unlock_account!
136
+ update_columns(locked_at: nil, failed_login_attempts: 0)
137
+ end
138
+
139
+ def record_failed_login!
140
+ new_count = (failed_login_attempts || 0) + 1
141
+ if new_count >= Collavre::SystemSetting.max_login_attempts
142
+ update_columns(failed_login_attempts: new_count, locked_at: Time.current)
143
+ else
144
+ update_column(:failed_login_attempts, new_count)
145
+ end
146
+ end
147
+
148
+ def reset_failed_login_attempts!
149
+ update_column(:failed_login_attempts, 0) if failed_login_attempts.to_i > 0
150
+ end
151
+
152
+ def remaining_lockout_time
153
+ return 0 unless locked?
154
+ ((locked_at + Collavre::SystemSetting.lockout_duration) - Time.current).to_i
155
+ end
156
+
157
+ def password_meets_minimum_length
158
+ return if password.blank?
159
+
160
+ min_length = Collavre::SystemSetting.password_min_length
161
+ if password.length < min_length
162
+ errors.add(:password, :too_short, count: min_length)
163
+ end
164
+ end
165
+
166
+ def theme_accessibility
167
+ return if theme.blank? || %w[light dark].include?(theme)
168
+
169
+ unless user_themes.exists?(id: theme)
170
+ errors.add(:theme, "is invalid")
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,10 @@
1
+ module Collavre
2
+ class UserTheme < ApplicationRecord
3
+ self.table_name = "user_themes"
4
+
5
+ belongs_to :user, class_name: Collavre.configuration.user_class_name
6
+
7
+ validates :name, presence: true
8
+ validates :variables, presence: true
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ module Collavre
2
+ class Variation < Label
3
+ # Variation-specific logic here
4
+ end
5
+ end