collavre 0.3.2 → 0.5.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/actiontext.css +73 -71
  3. data/app/assets/stylesheets/collavre/activity_logs.css +18 -45
  4. data/app/assets/stylesheets/collavre/comments_popup.css +197 -35
  5. data/app/assets/stylesheets/collavre/creatives.css +101 -51
  6. data/app/assets/stylesheets/collavre/dark_mode.css +221 -88
  7. data/app/assets/stylesheets/collavre/design_tokens.css +334 -0
  8. data/app/assets/stylesheets/collavre/mention_menu.css +13 -9
  9. data/app/assets/stylesheets/collavre/popup.css +57 -27
  10. data/app/assets/stylesheets/collavre/slide_view.css +6 -6
  11. data/app/assets/stylesheets/collavre/user_menu.css +4 -5
  12. data/app/components/collavre/plans_timeline_component.html.erb +2 -2
  13. data/app/controllers/collavre/admin/orchestration_controller.rb +9 -2
  14. data/app/controllers/collavre/admin/settings_controller.rb +199 -0
  15. data/app/controllers/collavre/comments/reactions_controller.rb +1 -9
  16. data/app/controllers/collavre/comments_controller.rb +39 -162
  17. data/app/controllers/collavre/creatives_controller.rb +18 -58
  18. data/app/controllers/collavre/users_controller.rb +31 -3
  19. data/app/helpers/collavre/application_helper.rb +97 -0
  20. data/app/helpers/collavre/creatives_helper.rb +10 -202
  21. data/app/javascript/collavre.js +0 -1
  22. data/app/javascript/components/creative_tree_row.js +3 -2
  23. data/app/javascript/controllers/comment_controller.js +309 -4
  24. data/app/javascript/controllers/comments/form_controller.js +52 -0
  25. data/app/javascript/controllers/comments/presence_controller.js +13 -0
  26. data/app/javascript/controllers/creatives/tree_controller.js +2 -1
  27. data/app/javascript/controllers/link_creative_controller.js +29 -3
  28. data/app/javascript/lib/__tests__/html_code_block_wrapper.test.js +201 -0
  29. data/app/javascript/lib/html_code_block_wrapper.js +168 -0
  30. data/app/javascript/lib/utils/markdown.js +2 -1
  31. data/app/javascript/modules/creative_row_editor.js +5 -1
  32. data/app/javascript/utils/emoji_parser.js +21 -0
  33. data/app/jobs/collavre/ai_agent_job.rb +6 -2
  34. data/app/jobs/collavre/cron_action_job.rb +18 -6
  35. data/app/jobs/collavre/cron_scheduler_job.rb +112 -0
  36. data/app/models/collavre/comment/approvable.rb +50 -0
  37. data/app/models/collavre/comment/broadcastable.rb +119 -0
  38. data/app/models/collavre/comment/notifiable.rb +111 -0
  39. data/app/models/collavre/comment.rb +13 -258
  40. data/app/models/collavre/comment_reaction.rb +15 -0
  41. data/app/models/collavre/creative/describable.rb +86 -0
  42. data/app/models/collavre/creative/linkable.rb +77 -0
  43. data/app/models/collavre/creative/permissible.rb +103 -0
  44. data/app/models/collavre/creative.rb +3 -289
  45. data/app/models/collavre/orchestrator_policy.rb +1 -1
  46. data/app/models/collavre/system_setting.rb +27 -1
  47. data/app/models/collavre/user.rb +42 -0
  48. data/app/models/collavre/user_theme.rb +10 -0
  49. data/app/services/collavre/ai_agent/approval_handler.rb +110 -0
  50. data/app/services/collavre/ai_agent/message_builder.rb +129 -0
  51. data/app/services/collavre/ai_agent/review_handler.rb +70 -0
  52. data/app/services/collavre/ai_agent_service.rb +93 -150
  53. data/app/services/collavre/ai_client.rb +23 -4
  54. data/app/services/collavre/auto_theme_generator.rb +168 -50
  55. data/app/services/collavre/command_menu_service.rb +70 -0
  56. data/app/services/collavre/comment_move_service.rb +94 -0
  57. data/app/services/collavre/comments/action_executor.rb +10 -0
  58. data/app/services/collavre/comments/mcp_command.rb +1 -2
  59. data/app/services/collavre/creatives/create_service.rb +86 -0
  60. data/app/services/collavre/creatives/destroy_service.rb +41 -0
  61. data/app/services/collavre/creatives/index_query.rb +3 -0
  62. data/app/services/collavre/markdown_converter.rb +240 -0
  63. data/app/services/collavre/mention_parser.rb +63 -0
  64. data/app/services/collavre/orchestration/agent_context_builder.rb +24 -8
  65. data/app/services/collavre/orchestration/agent_orchestrator.rb +59 -10
  66. data/app/services/collavre/orchestration/loop_breaker.rb +12 -7
  67. data/app/services/collavre/orchestration/policy_resolver.rb +16 -2
  68. data/app/services/collavre/orchestration/scheduler.rb +4 -3
  69. data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
  70. data/app/services/collavre/system_events/context_builder.rb +1 -6
  71. data/app/services/collavre/tools/creative_batch_service.rb +107 -0
  72. data/app/services/collavre/tools/creative_update_service.rb +17 -12
  73. data/app/services/collavre/tools/cron_create_service.rb +17 -5
  74. data/app/views/admin/shared/_tabs.html.erb +2 -1
  75. data/app/views/collavre/admin/orchestration/show.html.erb +11 -0
  76. data/app/views/collavre/admin/settings/_system_tab.html.erb +138 -0
  77. data/app/views/collavre/admin/settings/_uiux_tab.html.erb +44 -0
  78. data/app/views/collavre/admin/settings/index.html.erb +11 -0
  79. data/app/views/collavre/admin/settings/uiux.html.erb +11 -0
  80. data/app/views/collavre/comments/_comment.html.erb +15 -5
  81. data/app/views/collavre/comments/_comments_popup.html.erb +9 -2
  82. data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +0 -3
  83. data/app/views/collavre/creatives/_share_button.html.erb +0 -52
  84. data/app/views/collavre/creatives/_share_modal.html.erb +52 -0
  85. data/app/views/collavre/creatives/index.html.erb +5 -8
  86. data/app/views/collavre/shared/navigation/_panels.html.erb +2 -2
  87. data/app/views/collavre/user_themes/index.html.erb +7 -9
  88. data/app/views/collavre/users/_contact_management.html.erb +2 -1
  89. data/app/views/collavre/users/edit_ai.html.erb +7 -0
  90. data/app/views/collavre/users/index.html.erb +16 -1
  91. data/app/views/collavre/users/new_ai.html.erb +18 -8
  92. data/app/views/collavre/users/passkeys.html.erb +1 -1
  93. data/app/views/collavre/users/show.html.erb +1 -1
  94. data/app/views/layouts/collavre/slide.html.erb +8 -1
  95. data/config/locales/admin.en.yml +88 -0
  96. data/config/locales/admin.ko.yml +88 -0
  97. data/config/locales/ai_agent.en.yml +5 -1
  98. data/config/locales/ai_agent.ko.yml +5 -1
  99. data/config/locales/comments.en.yml +5 -1
  100. data/config/locales/comments.ko.yml +5 -1
  101. data/config/locales/orchestration.en.yml +8 -0
  102. data/config/locales/orchestration.ko.yml +8 -0
  103. data/config/locales/users.en.yml +12 -0
  104. data/config/locales/users.ko.yml +12 -0
  105. data/config/routes.rb +7 -1
  106. data/db/migrate/20260212011655_add_quoted_comment_to_comments.rb +7 -0
  107. data/db/migrate/20260213044247_add_agent_conf_to_users.rb +5 -0
  108. data/lib/collavre/engine.rb +25 -0
  109. data/lib/collavre/version.rb +1 -1
  110. metadata +32 -1
@@ -1,10 +1,10 @@
1
1
 
2
2
  .common-popup {
3
3
  position: absolute;
4
- z-index: 1200;
5
- background: var(--color-section-bg);
6
- border: 1px solid var(--color-border);
7
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
4
+ z-index: var(--layer-modal);
5
+ background: var(--surface-section);
6
+ border: 1px solid var(--border-color);
7
+ box-shadow: var(--shadow-2);
8
8
  border-radius: 6px;
9
9
  padding: 0.35em;
10
10
  max-width: min(420px, 90vw);
@@ -18,6 +18,10 @@
18
18
  overflow-y: auto;
19
19
  }
20
20
 
21
+ .common-popup-list li {
22
+ list-style: none;
23
+ }
24
+
21
25
  .mention-popup ul,
22
26
  .common-popup ul {
23
27
  list-style: none;
@@ -34,7 +38,7 @@
34
38
  .mention-item.active,
35
39
  .common-popup-item:hover,
36
40
  .common-popup-item.active {
37
- background: var(--color-drag-over);
41
+ background: var(--border-drag-over);
38
42
  }
39
43
 
40
44
  .common-popup-item {
@@ -49,21 +53,21 @@
49
53
  }
50
54
 
51
55
  .command-label {
52
- font-weight: 600;
56
+ font-weight: var(--weight-6);
53
57
  }
54
58
 
55
59
  .command-args {
56
60
  font-size: 0.85em;
57
- color: var(--color-text-muted);
61
+ color: var(--text-muted);
58
62
  }
59
63
 
60
64
  .command-aliases {
61
65
  font-size: 0.85em;
62
- color: var(--color-text-muted);
66
+ color: var(--text-muted);
63
67
  }
64
68
 
65
69
  .command-description {
66
70
  margin-top: 0.2em;
67
71
  font-size: 0.85em;
68
- color: var(--color-text-muted);
72
+ color: var(--text-muted);
69
73
  }
@@ -1,9 +1,9 @@
1
1
  .popup-box {
2
- background: var(--color-bg);
3
- border: 1px solid var(--color-border);
2
+ background: var(--surface-bg);
3
+ border: 1px solid var(--border-color);
4
4
  padding: 1em;
5
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
6
- border-radius: 10px;
5
+ box-shadow: var(--shadow-2);
6
+ border-radius: var(--radius-3);
7
7
  position: relative;
8
8
  display: flex;
9
9
  flex-direction: column;
@@ -43,12 +43,12 @@
43
43
 
44
44
  .common-popup {
45
45
  position: absolute;
46
- z-index: 1000;
47
- background: var(--color-section-bg);
48
- border: 1px solid var(--color-border);
46
+ z-index: var(--layer-modal);
47
+ background: var(--surface-section);
48
+ border: 1px solid var(--border-color);
49
49
  padding: 1em;
50
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
51
- border-radius: 10px;
50
+ box-shadow: var(--shadow-3);
51
+ border-radius: var(--radius-3);
52
52
  display: flex;
53
53
  flex-direction: column;
54
54
  max-width: 90vw;
@@ -60,9 +60,9 @@
60
60
  margin-bottom: 0.75em;
61
61
  padding: 0.5em;
62
62
  border-radius: 6px;
63
- border: 1px solid var(--color-border);
64
- background: var(--color-bg);
65
- color: var(--color-text);
63
+ border: 1px solid var(--border-color);
64
+ background: var(--surface-bg);
65
+ color: var(--text-primary);
66
66
  }
67
67
 
68
68
  .common-popup h3 {
@@ -71,25 +71,55 @@
71
71
  font-size: 1.1em;
72
72
  }
73
73
 
74
+ .popup-box input:not([type="radio"]):not([type="checkbox"]),
75
+ .popup-box select {
76
+ width: 100%;
77
+ box-sizing: border-box;
78
+ }
79
+
74
80
  .popup-close-btn {
75
81
  position: absolute;
76
82
  top: 0.75em;
77
83
  right: 0.75em;
78
- background: none;
84
+ background: transparent;
79
85
  border: none;
86
+ padding: 0;
80
87
  font-size: 1.2em;
81
88
  line-height: 1;
82
89
  cursor: pointer;
83
- color: var(--color-text);
90
+ color: var(--text-primary);
91
+ opacity: 0.6;
92
+ appearance: none;
93
+ }
94
+
95
+ .popup-close-btn:hover {
96
+ opacity: 1;
97
+ }
98
+
99
+ .delete-share-btn {
100
+ background: transparent;
101
+ border: none;
102
+ padding: 0;
103
+ cursor: pointer;
104
+ color: var(--text-primary);
105
+ appearance: none;
106
+ font-size: 1.1em;
107
+ opacity: 0.6;
108
+ padding: 0 0.5em;
109
+ }
110
+
111
+ .delete-share-btn:hover {
112
+ opacity: 1;
113
+ color: var(--danger);
84
114
  }
85
115
 
86
116
  #github-integration-modal .github-modal-status {
87
- color: var(--color-muted);
117
+ color: var(--text-muted);
88
118
  margin-bottom: 1em;
89
119
  }
90
120
 
91
121
  #github-integration-modal .github-modal-subtext {
92
- color: var(--color-muted);
122
+ color: var(--text-muted);
93
123
  }
94
124
 
95
125
  #github-integration-modal .github-existing-connections {
@@ -99,19 +129,19 @@
99
129
  #github-integration-modal .github-modal-list {
100
130
  padding-left: 1.2em;
101
131
  margin-bottom: 0.75em;
102
- color: var(--color-text);
132
+ color: var(--text-primary);
103
133
  }
104
134
 
105
135
  #github-integration-modal .github-modal-list-box {
106
- border: 1px solid var(--color-border);
136
+ border: 1px solid var(--border-color);
107
137
  padding: 0.5em;
108
- border-radius: 4px;
109
- background: var(--color-section-bg);
110
- color: var(--color-text);
138
+ border-radius: var(--radius-1);
139
+ background: var(--surface-section);
140
+ color: var(--text-primary);
111
141
  }
112
142
 
113
143
  #github-integration-modal .github-modal-empty {
114
- color: var(--color-muted);
144
+ color: var(--text-muted);
115
145
  }
116
146
 
117
147
  .popup-menu-wrapper {
@@ -122,11 +152,11 @@
122
152
  .popup-menu {
123
153
  display: none;
124
154
  position: absolute;
125
- z-index: 1000;
126
- background: var(--color-section-bg);
127
- border: 1px solid var(--color-border);
155
+ z-index: var(--layer-modal);
156
+ background: var(--surface-section);
157
+ border: 1px solid var(--border-color);
128
158
  padding: 0.5em;
129
- box-shadow: 0 2px 8px var(--color-border);
159
+ box-shadow: 0 2px 8px var(--border-color);
130
160
  min-width: 220px;
131
161
  top: calc(100% + 4px);
132
162
  left: 0;
@@ -164,4 +194,4 @@
164
194
  display: block;
165
195
  min-width: 100%;
166
196
  }
167
- }
197
+ }
@@ -2,7 +2,7 @@
2
2
  height: 100vh;
3
3
  display: flex;
4
4
  overflow: auto;
5
- padding: 2rem;
5
+ padding: var(--space-7);
6
6
  box-sizing: border-box;
7
7
  }
8
8
 
@@ -17,12 +17,12 @@
17
17
 
18
18
  #slide-controls {
19
19
  position: fixed;
20
- bottom: 0.5rem;
20
+ bottom: var(--space-2);
21
21
  left: 50%;
22
22
  transform: translateX(-50%);
23
23
  z-index: 10;
24
24
  display: flex;
25
- gap: 0.5rem;
25
+ gap: var(--space-2);
26
26
  align-items: center;
27
27
  }
28
28
 
@@ -58,7 +58,7 @@
58
58
  left: 0;
59
59
  right: 0;
60
60
  height: 20vh;
61
- background: var(--color-border);
61
+ background: var(--border-color);
62
62
  align-items: center;
63
63
  justify-content: center;
64
64
  }
@@ -66,14 +66,14 @@
66
66
  #slide-caption {
67
67
  display: block;
68
68
  pointer-events: none;
69
- padding: 0.5rem;
69
+ padding: var(--space-2);
70
70
  text-align: center;
71
71
  white-space: pre-wrap;
72
72
  }
73
73
 
74
74
  #slide-timer {
75
75
  display: block;
76
- margin-top: 0.5rem;
76
+ margin-top: var(--space-2);
77
77
  font-variant-numeric: tabular-nums;
78
78
  }
79
79
  }
@@ -1,6 +1,6 @@
1
1
  .nav-avatar {
2
- width: 32px;
3
- height: 32px;
2
+ width: var(--space-7);
3
+ height: var(--space-7);
4
4
  border-radius: 50%;
5
5
  }
6
6
 
@@ -27,8 +27,7 @@
27
27
  display: flex;
28
28
  align-items: center;
29
29
  justify-content: center;
30
- font-weight: bold;
31
- color: var(--color-text);
30
+ font-weight: var(--weight-7);
31
+ color: var(--text-primary);
32
32
  pointer-events: none;
33
33
  }
34
-
@@ -1,5 +1,5 @@
1
1
  <div class="plans-timeline-wrapper">
2
- <button id="timeline-today-btn" type="button"><%= t('collavre.plans.today', default: '오늘') %></button>
2
+ <button id="timeline-today-btn" class="btn btn-xs" type="button"><%= t('collavre.plans.today', default: '오늘') %></button>
3
3
  <div id="plans-timeline" class="horizontal-timeline" data-plans='<%= raw plan_data.to_json %>' data-start-date="<%= @start_date %>" data-end-date="<%= @end_date %>" data-delete-confirm="<%= t('collavre.plans.delete_confirm', default: 'Are you sure?') %>"></div>
4
4
  </div>
5
5
  <hr>
@@ -9,6 +9,6 @@
9
9
  <input type="text" id="plan-select-creative-input" placeholder="<%= t('collavre.plans.select_creative', default: 'Select Creative') %>" autocomplete="off">
10
10
  <%= form.date_field :start_date, placeholder: t('collavre.plans.start_date'), id: 'plan-start-date' %>
11
11
  <%= form.date_field :target_date, placeholder: t('collavre.plans.target_date'), id: 'plan-target-date' %>
12
- <%= form.submit t('collavre.plans.add_plan'), id: 'add-plan-btn', disabled: true %>
12
+ <%= form.submit t('collavre.plans.add_plan'), id: 'add-plan-btn', class: 'btn btn-sm btn-primary', disabled: true %>
13
13
  <% end %>
14
14
  </div>
@@ -39,7 +39,8 @@ module Collavre
39
39
  # Group by type for readable YAML structure
40
40
  structure = {
41
41
  "arbitration" => { "global" => nil, "overrides" => [] },
42
- "scheduling" => { "global" => nil, "overrides" => [] }
42
+ "scheduling" => { "global" => nil, "overrides" => [] },
43
+ "collaboration" => { "global" => nil, "overrides" => [] }
43
44
  }
44
45
 
45
46
  policies.each do |policy|
@@ -89,6 +90,12 @@ module Collavre
89
90
  "backoff_strategy" => "exponential",
90
91
  "topic_max_concurrent_jobs" => 1
91
92
  }
93
+ },
94
+ "collaboration" => {
95
+ "global" => {
96
+ "a2a_completion_instruction" => nil,
97
+ "mention_rule" => nil
98
+ }
92
99
  }
93
100
  }
94
101
  end
@@ -97,7 +104,7 @@ module Collavre
97
104
  raise PolicyValidationError, t("admin.orchestration.invalid_format") unless parsed.is_a?(Hash)
98
105
 
99
106
  parsed.each do |type, data|
100
- unless %w[arbitration scheduling].include?(type)
107
+ unless %w[arbitration scheduling collaboration].include?(type)
101
108
  raise PolicyValidationError, t("admin.orchestration.unknown_policy_type", type: type)
102
109
  end
103
110
 
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module Admin
5
+ class SettingsController < ApplicationController
6
+ before_action :require_system_admin!
7
+
8
+ def index
9
+ @help_link = SystemSetting.find_by(key: "help_menu_link")&.value
10
+ @mcp_tool_approval = SystemSetting.find_by(key: "mcp_tool_approval_required")&.value == "true"
11
+ @creatives_login_required = SystemSetting.creatives_login_required?
12
+ @home_page_path = SystemSetting.home_page_path
13
+
14
+ # Account lockout settings
15
+ @max_login_attempts = SystemSetting.max_login_attempts
16
+ @lockout_duration_minutes = SystemSetting.lockout_duration_minutes
17
+
18
+ # Password policy settings
19
+ @password_min_length = SystemSetting.password_min_length
20
+
21
+ # Session timeout settings
22
+ @session_timeout_minutes = SystemSetting.session_timeout_minutes
23
+
24
+ # Rate limiting settings
25
+ @password_reset_rate_limit = SystemSetting.password_reset_rate_limit
26
+ @password_reset_rate_period_minutes = SystemSetting.password_reset_rate_period_minutes
27
+ @api_rate_limit = SystemSetting.api_rate_limit
28
+ @api_rate_period_minutes = SystemSetting.api_rate_period_minutes
29
+
30
+ # Storage is "disabled" list. View expects "enabled" list.
31
+ all_provider_keys = Rails.application.config.auth_providers.map { |p| p[:key].to_s }
32
+ disabled_providers = SystemSetting.find_by(key: "auth_providers_disabled")&.value&.split(",") || []
33
+ @enabled_auth_providers = all_provider_keys - disabled_providers
34
+ end
35
+
36
+ def uiux
37
+ @default_light_theme_id = SystemSetting.default_light_theme_id
38
+ @default_dark_theme_id = SystemSetting.default_dark_theme_id
39
+ @available_themes = Collavre::UserTheme.all.order(:name)
40
+ end
41
+
42
+ def update_uiux
43
+ SystemSetting.transaction do
44
+ light_theme_id = params[:default_light_theme_id].to_s.strip
45
+ light_theme_setting = SystemSetting.find_or_initialize_by(key: "default_light_theme_id")
46
+ light_theme_setting.value = light_theme_id.present? ? light_theme_id : nil
47
+ light_theme_setting.save!
48
+
49
+ dark_theme_id = params[:default_dark_theme_id].to_s.strip
50
+ dark_theme_setting = SystemSetting.find_or_initialize_by(key: "default_dark_theme_id")
51
+ dark_theme_setting.value = dark_theme_id.present? ? dark_theme_id : nil
52
+ dark_theme_setting.save!
53
+ end
54
+
55
+ redirect_to collavre.admin_uiux_path, notice: t("admin.settings.updated")
56
+ rescue ActiveRecord::RecordInvalid => e
57
+ flash.now[:alert] = e.record.errors.full_messages.join(", ")
58
+ @default_light_theme_id = params[:default_light_theme_id]
59
+ @default_dark_theme_id = params[:default_dark_theme_id]
60
+ @available_themes = Collavre::UserTheme.all.order(:name)
61
+ render :uiux, status: :unprocessable_entity
62
+ end
63
+
64
+ def update
65
+ SystemSetting.transaction do
66
+ # Help Link
67
+ help_link_setting = SystemSetting.find_or_initialize_by(key: "help_menu_link")
68
+ help_link_setting.value = params[:help_link].to_s.strip
69
+ help_link_setting.save!
70
+
71
+ # MCP Tool Approval
72
+ mcp_setting = SystemSetting.find_or_initialize_by(key: "mcp_tool_approval_required")
73
+ mcp_setting.value = params[:mcp_tool_approval] == "1" ? "true" : "false"
74
+ mcp_setting.save!
75
+
76
+ # Creatives Login Required
77
+ creatives_login_setting = SystemSetting.find_or_initialize_by(key: "creatives_login_required")
78
+ creatives_login_setting.value = params[:creatives_login_required] == "1" ? "true" : "false"
79
+ creatives_login_setting.save!
80
+
81
+ # Home Page Path
82
+ home_page_path_input = params[:home_page_path].to_s.strip
83
+ if home_page_path_input.present?
84
+ normalized_path, error = validate_and_normalize_home_page_path(home_page_path_input)
85
+ if error
86
+ home_page_setting = SystemSetting.new(key: "home_page_path")
87
+ home_page_setting.errors.add(:base, error)
88
+ raise ActiveRecord::RecordInvalid, home_page_setting
89
+ end
90
+ home_page_setting = SystemSetting.find_or_initialize_by(key: "home_page_path")
91
+ home_page_setting.value = normalized_path
92
+ home_page_setting.save!
93
+ else
94
+ home_page_setting = SystemSetting.find_or_initialize_by(key: "home_page_path")
95
+ home_page_setting.value = nil
96
+ home_page_setting.save!
97
+ end
98
+
99
+ # Account Lockout Settings
100
+ max_attempts = params[:max_login_attempts].to_i
101
+ max_attempts = SystemSetting::DEFAULT_MAX_LOGIN_ATTEMPTS if max_attempts < 1
102
+ SystemSetting.find_or_initialize_by(key: "max_login_attempts").tap { |s| s.value = max_attempts.to_s; s.save! }
103
+
104
+ lockout_duration = params[:lockout_duration_minutes].to_i
105
+ lockout_duration = SystemSetting::DEFAULT_LOCKOUT_DURATION_MINUTES if lockout_duration < 1
106
+ SystemSetting.find_or_initialize_by(key: "lockout_duration_minutes").tap { |s| s.value = lockout_duration.to_s; s.save! }
107
+
108
+ # Password Policy Settings
109
+ password_min_length = [ [ params[:password_min_length].to_i, SystemSetting::DEFAULT_PASSWORD_MIN_LENGTH ].max, 72 ].min
110
+ SystemSetting.find_or_initialize_by(key: "password_min_length").tap { |s| s.value = password_min_length.to_s; s.save! }
111
+
112
+ # Session Timeout Settings
113
+ session_timeout = [ params[:session_timeout_minutes].to_i, 0 ].max
114
+ SystemSetting.find_or_initialize_by(key: "session_timeout_minutes").tap { |s| s.value = session_timeout.to_s; s.save! }
115
+
116
+ # Rate Limiting - Password Reset
117
+ pw_reset_limit = params[:password_reset_rate_limit].to_i
118
+ pw_reset_limit = SystemSetting::DEFAULT_PASSWORD_RESET_RATE_LIMIT if pw_reset_limit < 1
119
+ SystemSetting.find_or_initialize_by(key: "password_reset_rate_limit").tap { |s| s.value = pw_reset_limit.to_s; s.save! }
120
+
121
+ pw_reset_period = params[:password_reset_rate_period_minutes].to_i
122
+ pw_reset_period = SystemSetting::DEFAULT_PASSWORD_RESET_RATE_PERIOD_MINUTES if pw_reset_period < 1
123
+ SystemSetting.find_or_initialize_by(key: "password_reset_rate_period_minutes").tap { |s| s.value = pw_reset_period.to_s; s.save! }
124
+
125
+ # Rate Limiting - API
126
+ api_limit = params[:api_rate_limit].to_i
127
+ api_limit = SystemSetting::DEFAULT_API_RATE_LIMIT if api_limit < 1
128
+ SystemSetting.find_or_initialize_by(key: "api_rate_limit").tap { |s| s.value = api_limit.to_s; s.save! }
129
+
130
+ api_period = params[:api_rate_period_minutes].to_i
131
+ api_period = SystemSetting::DEFAULT_API_RATE_PERIOD_MINUTES if api_period < 1
132
+ SystemSetting.find_or_initialize_by(key: "api_rate_period_minutes").tap { |s| s.value = api_period.to_s; s.save! }
133
+
134
+ # Auth Providers
135
+ auth_providers = Array(params[:auth_providers]).reject(&:blank?)
136
+ if auth_providers.empty?
137
+ auth_setting = SystemSetting.new(key: "auth_providers_enabled")
138
+ auth_setting.errors.add(:base, t("admin.settings.auth_provider_required"))
139
+ raise ActiveRecord::RecordInvalid, auth_setting
140
+ end
141
+
142
+ all_provider_keys = Rails.application.config.auth_providers.map { |p| p[:key].to_s }
143
+ disabled_providers = all_provider_keys - auth_providers
144
+ SystemSetting.find_or_initialize_by(key: "auth_providers_disabled").tap { |s| s.value = disabled_providers.join(","); s.save! }
145
+ end
146
+
147
+ redirect_to collavre.admin_settings_path, notice: t("admin.settings.updated")
148
+ rescue ActiveRecord::RecordInvalid => e
149
+ flash.now[:alert] = e.record.errors.full_messages.join(", ")
150
+ @help_link = params[:help_link]
151
+ @mcp_tool_approval = params[:mcp_tool_approval] == "1"
152
+ @creatives_login_required = params[:creatives_login_required] == "1"
153
+ @home_page_path = params[:home_page_path]
154
+ @max_login_attempts = params[:max_login_attempts].to_i.positive? ? params[:max_login_attempts].to_i : SystemSetting::DEFAULT_MAX_LOGIN_ATTEMPTS
155
+ @lockout_duration_minutes = params[:lockout_duration_minutes].to_i.positive? ? params[:lockout_duration_minutes].to_i : SystemSetting::DEFAULT_LOCKOUT_DURATION_MINUTES
156
+ @password_min_length = [ [ params[:password_min_length].to_i, SystemSetting::DEFAULT_PASSWORD_MIN_LENGTH ].max, 72 ].min
157
+ @session_timeout_minutes = [ params[:session_timeout_minutes].to_i, 0 ].max
158
+ @password_reset_rate_limit = params[:password_reset_rate_limit].to_i.positive? ? params[:password_reset_rate_limit].to_i : SystemSetting::DEFAULT_PASSWORD_RESET_RATE_LIMIT
159
+ @password_reset_rate_period_minutes = params[:password_reset_rate_period_minutes].to_i.positive? ? params[:password_reset_rate_period_minutes].to_i : SystemSetting::DEFAULT_PASSWORD_RESET_RATE_PERIOD_MINUTES
160
+ @api_rate_limit = params[:api_rate_limit].to_i.positive? ? params[:api_rate_limit].to_i : SystemSetting::DEFAULT_API_RATE_LIMIT
161
+ @api_rate_period_minutes = params[:api_rate_period_minutes].to_i.positive? ? params[:api_rate_period_minutes].to_i : SystemSetting::DEFAULT_API_RATE_PERIOD_MINUTES
162
+ @enabled_auth_providers = params[:auth_providers] || []
163
+ render :index, status: :unprocessable_entity
164
+ end
165
+
166
+ private
167
+
168
+ def validate_and_normalize_home_page_path(value)
169
+ path = value.to_s.strip
170
+
171
+ if path.match?(%r{\A[a-z][a-z0-9+.-]*://}i)
172
+ return [ nil, t("admin.settings.home_page_path_invalid_url") ]
173
+ end
174
+
175
+ path = path.split(/[?#]/).first
176
+ path = "/#{path}" unless path.start_with?("/")
177
+ path = path.gsub(%r{/+}, "/")
178
+ return [ nil, nil ] if path == "/"
179
+
180
+ begin
181
+ route_info = Rails.application.routes.recognize_path(path, method: :get)
182
+
183
+ if route_info[:format].present? && route_info[:format] != "html"
184
+ return [ nil, t("admin.settings.home_page_path_not_html", path: path) ]
185
+ end
186
+
187
+ non_html_paths = %w[/service-worker /manifest /up]
188
+ if non_html_paths.any? { |p| path.start_with?(p) }
189
+ return [ nil, t("admin.settings.home_page_path_not_html", path: path) ]
190
+ end
191
+ rescue ActionController::RoutingError
192
+ return [ nil, t("admin.settings.home_page_path_not_routable", path: path) ]
193
+ end
194
+
195
+ [ path, nil ]
196
+ end
197
+ end
198
+ end
199
+ end
@@ -45,15 +45,7 @@ module Collavre
45
45
  end
46
46
 
47
47
  def broadcast_reaction_update
48
- payload = build_reaction_payload
49
- Turbo::StreamsChannel.broadcast_action_to(
50
- [ @creative, :comments ],
51
- action: "update_reactions",
52
- target: view_context.dom_id(@comment),
53
- attributes: {
54
- data: payload.to_json
55
- }
56
- )
48
+ CommentReaction.broadcast_reaction_update(@comment)
57
49
  end
58
50
 
59
51
  def set_creative