collavre 0.20.3 → 0.21.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/actiontext.css +92 -2
  3. data/app/assets/stylesheets/collavre/code_highlight.css +144 -26
  4. data/app/assets/stylesheets/collavre/comments_popup.css +83 -0
  5. data/app/assets/stylesheets/collavre/landing.css +507 -0
  6. data/app/channels/collavre/comments_presence_channel.rb +7 -0
  7. data/app/controllers/collavre/admin/integrations_controller.rb +82 -0
  8. data/app/controllers/collavre/admin/settings_controller.rb +22 -17
  9. data/app/controllers/collavre/application_controller.rb +27 -0
  10. data/app/controllers/collavre/channels_controller.rb +23 -0
  11. data/app/controllers/collavre/creatives_controller.rb +50 -6
  12. data/app/controllers/collavre/landing_controller.rb +8 -0
  13. data/app/controllers/collavre/public_assets_controller.rb +24 -0
  14. data/app/controllers/collavre/topics_controller.rb +21 -30
  15. data/app/helpers/collavre/comments_helper.rb +7 -0
  16. data/app/helpers/collavre/public_assets_helper.rb +14 -0
  17. data/app/javascript/controllers/comment_controller.js +9 -0
  18. data/app/javascript/controllers/comments/form_controller.js +4 -0
  19. data/app/javascript/controllers/comments/list_controller.js +10 -7
  20. data/app/javascript/controllers/comments/popup_controller.js +9 -0
  21. data/app/javascript/controllers/comments/presence_controller.js +83 -1
  22. data/app/javascript/controllers/comments/topics_controller.js +15 -0
  23. data/app/javascript/controllers/creatives/__tests__/sync_controller.test.js +89 -0
  24. data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +120 -0
  25. data/app/javascript/controllers/creatives/sync_controller.js +30 -9
  26. data/app/javascript/controllers/creatives/tree_controller.js +23 -0
  27. data/app/javascript/controllers/index.js +4 -1
  28. data/app/javascript/controllers/landing_video_controller.js +53 -0
  29. data/app/javascript/creatives/tree_renderer.js +6 -0
  30. data/app/javascript/lib/api/__tests__/queue_manager.test.js +27 -0
  31. data/app/javascript/lib/api/queue_manager.js +17 -5
  32. data/app/javascript/modules/__tests__/command_args_form.test.js +103 -0
  33. data/app/javascript/modules/__tests__/html_content_empty.test.js +41 -0
  34. data/app/javascript/modules/__tests__/markdown_source_reconcile.test.js +70 -0
  35. data/app/javascript/modules/command_args_form.js +22 -4
  36. data/app/javascript/modules/command_menu.js +27 -0
  37. data/app/javascript/modules/creative_row_editor.js +227 -17
  38. data/app/javascript/modules/html_content_empty.js +12 -0
  39. data/app/javascript/modules/markdown_source_reconcile.js +53 -0
  40. data/app/jobs/collavre/drop_trigger_job.rb +37 -8
  41. data/app/mailers/collavre/application_mailer.rb +1 -1
  42. data/app/models/collavre/channel/injected_message.rb +5 -0
  43. data/app/models/collavre/channel.rb +87 -0
  44. data/app/models/collavre/creative/describable.rb +65 -3
  45. data/app/models/collavre/creative.rb +2 -0
  46. data/app/models/collavre/integration_setting.rb +35 -0
  47. data/app/models/collavre/preview_channel.rb +93 -0
  48. data/app/models/collavre/system_setting.rb +13 -2
  49. data/app/models/collavre/topic.rb +3 -25
  50. data/app/models/concerns/collavre/ai_agent_resolvable.rb +12 -3
  51. data/app/services/collavre/ai_client.rb +3 -3
  52. data/app/services/collavre/channel_attacher.rb +58 -0
  53. data/app/services/collavre/comments/mcp_command.rb +31 -1
  54. data/app/services/collavre/creatives/tree_builder.rb +7 -3
  55. data/app/services/collavre/google_calendar_service.rb +4 -2
  56. data/app/services/collavre/markdown_converter.rb +130 -15
  57. data/app/services/collavre/markdown_importer.rb +7 -2
  58. data/app/services/collavre/orchestration/policy_resolver.rb +11 -1
  59. data/app/services/collavre/tools/creative_attach_files_service.rb +96 -0
  60. data/app/services/collavre/tools/creative_list_attachments_service.rb +42 -0
  61. data/app/services/collavre/tools/creative_remove_attachment_service.rb +35 -0
  62. data/app/services/collavre/tools/permission_denied_error.rb +9 -0
  63. data/app/services/collavre/tools/preview_attach_service.rb +128 -0
  64. data/app/services/collavre/tools/preview_detach_service.rb +61 -0
  65. data/app/services/collavre/tools/topic_authorizer.rb +24 -0
  66. data/app/services/collavre/topic_branch_service.rb +34 -26
  67. data/app/views/admin/shared/_tabs.html.erb +1 -0
  68. data/app/views/collavre/admin/integrations/_category.html.erb +22 -0
  69. data/app/views/collavre/admin/integrations/_setting_row.html.erb +54 -0
  70. data/app/views/collavre/admin/integrations/index.html.erb +42 -0
  71. data/app/views/collavre/admin/settings/_system_tab.html.erb +8 -0
  72. data/app/views/collavre/comments/_channel_chips.html.erb +33 -0
  73. data/app/views/collavre/comments/_comment.html.erb +6 -1
  74. data/app/views/collavre/comments/_comments_popup.html.erb +1 -0
  75. data/app/views/collavre/creatives/_inline_edit_form.html.erb +19 -0
  76. data/app/views/collavre/creatives/index.html.erb +10 -2
  77. data/app/views/collavre/landing/show.html.erb +130 -0
  78. data/app/views/layouts/collavre/landing.html.erb +33 -0
  79. data/config/locales/admin.en.yml +4 -2
  80. data/config/locales/admin.ko.yml +4 -2
  81. data/config/locales/channels.en.yml +11 -0
  82. data/config/locales/channels.ko.yml +11 -0
  83. data/config/locales/comments.en.yml +2 -0
  84. data/config/locales/comments.ko.yml +2 -0
  85. data/config/locales/creatives.en.yml +9 -0
  86. data/config/locales/creatives.ko.yml +8 -0
  87. data/config/locales/integrations.en.yml +44 -0
  88. data/config/locales/integrations.ko.yml +44 -0
  89. data/config/locales/landing.en.yml +51 -0
  90. data/config/locales/landing.ko.yml +51 -0
  91. data/config/routes.rb +18 -0
  92. data/db/migrate/20260526000000_create_channels.rb +42 -0
  93. data/db/migrate/20260527000000_add_dismissed_at_to_channels.rb +6 -0
  94. data/db/migrate/20260527000100_backfill_dismissed_at_for_legacy_detached_channels.rb +28 -0
  95. data/db/migrate/20260528000000_add_preview_channel_unique_index.rb +31 -0
  96. data/db/migrate/20260529000000_add_primary_agent_id_to_topics.rb +40 -0
  97. data/db/migrate/20260529100000_create_integration_settings.rb +15 -0
  98. data/db/seeds.rb +19 -0
  99. data/lib/collavre/aws_credentials.rb +75 -0
  100. data/lib/collavre/engine.rb +51 -0
  101. data/lib/collavre/integration_settings/key_definition.rb +29 -0
  102. data/lib/collavre/integration_settings/registry.rb +55 -0
  103. data/lib/collavre/integration_settings/resolver.rb +71 -0
  104. data/lib/collavre/integration_settings.rb +46 -0
  105. data/lib/collavre/ses_settings_interceptor.rb +72 -0
  106. data/lib/collavre/version.rb +1 -1
  107. data/lib/collavre.rb +3 -0
  108. metadata +52 -1
@@ -0,0 +1,507 @@
1
+ /* ============================================================================
2
+ * Collavre Landing Page
3
+ * Uses design tokens exclusively — no hardcoded colors.
4
+ * Primary accent: --color-active (the blue used throughout the app UI)
5
+ *
6
+ * Typography scale (4 levels only):
7
+ * L1 --text-7 Hero title, CTA title
8
+ * L2 --text-5 Section titles, tagline
9
+ * L3 --text-3 Card titles, subtitles, button-lg
10
+ * L4 --text-2 Body copy, descriptions, buttons, footer
11
+ * ============================================================================ */
12
+
13
+ /* ─── Reset & Base ──────────────────────────────────────────────────────────── */
14
+ .landing-page {
15
+ overflow-x: hidden;
16
+ scroll-behavior: smooth;
17
+ }
18
+
19
+ .landing-page nav {
20
+ display: none; /* hide app navigation */
21
+ }
22
+
23
+ .landing {
24
+ font-family: var(--font-sans);
25
+ color: var(--text-primary);
26
+ line-height: var(--leading-2);
27
+ }
28
+
29
+ /* Container: sets the content boundary */
30
+ .landing-container {
31
+ max-width: 1080px;
32
+ margin: 0 auto;
33
+ padding: 0 var(--space-5);
34
+ }
35
+
36
+ /* ─── Buttons ───────────────────────────────────────────────────────────────── */
37
+ .landing-btn {
38
+ display: inline-flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ padding: var(--space-2) var(--space-5);
42
+ border-radius: var(--radius-3);
43
+ font-weight: var(--weight-6);
44
+ font-size: var(--text-2); /* L4 */
45
+ text-decoration: none;
46
+ transition: all 0.2s var(--ease-2);
47
+ background: var(--color-active);
48
+ color: white;
49
+ border: none;
50
+ cursor: pointer;
51
+ }
52
+
53
+ .landing-btn:hover {
54
+ filter: brightness(1.1);
55
+ transform: translateY(-1px);
56
+ box-shadow: var(--shadow-2);
57
+ }
58
+
59
+ .landing-btn-lg {
60
+ padding: var(--space-3) var(--space-7);
61
+ font-size: var(--text-3); /* L3 */
62
+ border-radius: var(--radius-4);
63
+ }
64
+
65
+ .landing-btn-ghost {
66
+ background: transparent;
67
+ color: var(--text-muted);
68
+ border: var(--border-1) solid var(--border-color);
69
+ }
70
+
71
+ .landing-btn-ghost:hover {
72
+ color: var(--text-primary);
73
+ border-color: var(--text-muted);
74
+ background: var(--surface-hover);
75
+ filter: none;
76
+ }
77
+
78
+ /* ─── Hero ──────────────────────────────────────────────────────────────────── */
79
+ .landing-hero {
80
+ position: relative;
81
+ padding: var(--space-10) 0 var(--space-8);
82
+ text-align: center;
83
+ }
84
+
85
+ .landing-hero-glow {
86
+ position: absolute;
87
+ top: -20%;
88
+ left: 50%;
89
+ transform: translateX(-50%);
90
+ width: 600px;
91
+ height: 600px;
92
+ background: radial-gradient(circle, color-mix(in srgb, var(--color-active) 12%, transparent) 0%, transparent 70%);
93
+ pointer-events: none;
94
+ z-index: 0;
95
+ }
96
+
97
+ .landing-hero-title { /* L1 — main headline */
98
+ font-size: var(--text-7);
99
+ font-weight: var(--weight-8);
100
+ letter-spacing: 0.02em;
101
+ line-height: var(--leading-1);
102
+ color: var(--text-primary);
103
+ margin: 0 auto var(--space-5);
104
+ max-width: 900px;
105
+ position: relative;
106
+ z-index: 1;
107
+ }
108
+
109
+ .landing-hero-sub { /* L3 — subline */
110
+ font-size: var(--text-3);
111
+ font-weight: var(--weight-6);
112
+ color: var(--text-muted);
113
+ margin: 0 auto var(--space-5);
114
+ max-width: 900px;
115
+ text-align: center;
116
+ position: relative;
117
+ z-index: 1;
118
+ }
119
+
120
+ .landing-hero-brand { /* L1 — "콜라브" in brand color */
121
+ font-size: var(--text-7);
122
+ font-weight: var(--weight-8);
123
+ letter-spacing: 0.02em;
124
+ color: var(--color-active);
125
+ margin: 0 auto var(--space-7);
126
+ text-align: center;
127
+ position: relative;
128
+ z-index: 1;
129
+ }
130
+
131
+ .landing-hero-actions {
132
+ display: flex;
133
+ gap: var(--space-3);
134
+ justify-content: center;
135
+ flex-wrap: wrap;
136
+ position: relative;
137
+ z-index: 1;
138
+ }
139
+
140
+ /* ─── Demo Section ──────────────────────────────────────────────────────────── */
141
+
142
+ .landing-demo-frame {
143
+ position: relative;
144
+ max-width: 900px;
145
+ margin: 0 auto;
146
+ aspect-ratio: 16 / 9;
147
+ border-radius: var(--radius-4);
148
+ border: var(--border-1) solid var(--border-color);
149
+ background: var(--surface-section);
150
+ overflow: hidden;
151
+ box-shadow: var(--shadow-3);
152
+ }
153
+
154
+ .landing-demo-video {
155
+ width: 100%;
156
+ height: 100%;
157
+ object-fit: cover;
158
+ display: block;
159
+ }
160
+
161
+ /* ─── Demo: Progress bar ──────────────────────────────────────────────────── */
162
+ .landing-demo-progress {
163
+ position: absolute;
164
+ bottom: 0;
165
+ left: 0;
166
+ right: 0;
167
+ height: 3px;
168
+ background: color-mix(in srgb, var(--text-primary) 15%, transparent);
169
+ z-index: 2;
170
+ }
171
+
172
+ .landing-demo-progress-fill {
173
+ height: 100%;
174
+ width: 0;
175
+ background: var(--color-active);
176
+ transition: width 0.1s linear;
177
+ }
178
+
179
+ /* ─── Demo: Play/Pause toggle ─────────────────────────────────────────────── */
180
+ .landing-demo-toggle {
181
+ position: absolute;
182
+ inset: 0;
183
+ width: 100%;
184
+ height: 100%;
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ background: transparent;
189
+ border: none;
190
+ cursor: pointer;
191
+ opacity: 0;
192
+ transition: opacity 0.25s ease;
193
+ z-index: 1;
194
+ }
195
+
196
+ .landing-demo-frame:hover .landing-demo-toggle {
197
+ opacity: 1;
198
+ background: color-mix(in srgb, var(--bg-primary) 40%, transparent);
199
+ }
200
+
201
+ .landing-demo-toggle-icon {
202
+ font-size: var(--text-5); /* L2 */
203
+ color: var(--text-primary);
204
+ background: color-mix(in srgb, var(--bg-primary) 80%, transparent);
205
+ width: 56px;
206
+ height: 56px;
207
+ border-radius: 50%;
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ backdrop-filter: blur(4px);
212
+ box-shadow: var(--shadow-2);
213
+ }
214
+
215
+ /* ─── Sections (shared) ─────────────────────────────────────────────────────── */
216
+ .landing-section {
217
+ padding: var(--space-10) 0;
218
+ }
219
+
220
+ .landing-section-title { /* L2 */
221
+ font-size: var(--text-5);
222
+ font-weight: var(--weight-7);
223
+ text-align: center;
224
+ letter-spacing: -0.02em;
225
+ margin-bottom: var(--space-3);
226
+ }
227
+
228
+ .landing-section-sub { /* L4 */
229
+ font-size: var(--text-2);
230
+ color: var(--text-muted);
231
+ text-align: center;
232
+ max-width: 560px;
233
+ margin: 0 auto var(--space-8);
234
+ }
235
+
236
+ /* ─── Problem Section ───────────────────────────────────────────────────────── */
237
+ .landing-problem {
238
+ background: var(--surface-section);
239
+ }
240
+
241
+ .landing-problem-grid {
242
+ display: grid;
243
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
244
+ gap: var(--space-5);
245
+ max-width: 900px;
246
+ margin: var(--space-7) auto 0;
247
+ }
248
+
249
+ .landing-problem-card {
250
+ padding: var(--space-5);
251
+ border-radius: var(--radius-3);
252
+ border: var(--border-1) solid var(--border-color);
253
+ background: var(--surface-bg);
254
+ transition: border-color 0.2s var(--ease-2);
255
+ }
256
+
257
+ .landing-problem-card:hover {
258
+ border-color: var(--text-muted);
259
+ }
260
+
261
+ .landing-problem-icon {
262
+ font-size: var(--text-5); /* L2 — emoji decorative */
263
+ display: block;
264
+ margin-bottom: var(--space-3);
265
+ }
266
+
267
+ .landing-problem-card h3 { /* L3 */
268
+ font-size: var(--text-3);
269
+ font-weight: var(--weight-6);
270
+ margin-bottom: var(--space-2);
271
+ }
272
+
273
+ .landing-problem-card p { /* L4 */
274
+ font-size: var(--text-2);
275
+ color: var(--text-muted);
276
+ line-height: var(--leading-2);
277
+ }
278
+
279
+ /* ─── Features Grid ─────────────────────────────────────────────────────────── */
280
+ .landing-features {
281
+ background: var(--surface-bg);
282
+ }
283
+
284
+ .landing-features-grid {
285
+ display: grid;
286
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
287
+ gap: var(--space-5);
288
+ }
289
+
290
+ .landing-feature-card {
291
+ padding: var(--space-5);
292
+ border-radius: var(--radius-3);
293
+ border: var(--border-1) solid var(--border-color);
294
+ background: var(--surface-section);
295
+ transition: all 0.25s var(--ease-2);
296
+ }
297
+
298
+ .landing-feature-card:hover {
299
+ transform: translateY(-2px);
300
+ box-shadow: var(--shadow-2);
301
+ border-color: var(--color-active);
302
+ }
303
+
304
+ .landing-feature-icon {
305
+ font-size: var(--text-5); /* L2 — emoji decorative */
306
+ margin-bottom: var(--space-3);
307
+ }
308
+
309
+ .landing-feature-card h3 { /* L3 */
310
+ font-size: var(--text-3);
311
+ font-weight: var(--weight-6);
312
+ margin-bottom: var(--space-2);
313
+ }
314
+
315
+ .landing-feature-card p { /* L4 */
316
+ font-size: var(--text-2);
317
+ color: var(--text-muted);
318
+ line-height: var(--leading-2);
319
+ }
320
+
321
+ /* ─── How It Works ──────────────────────────────────────────────────────────── */
322
+ .landing-how {
323
+ background: var(--surface-section);
324
+ }
325
+
326
+ .landing-steps {
327
+ display: flex;
328
+ align-items: flex-start;
329
+ justify-content: center;
330
+ gap: var(--space-3);
331
+ flex-wrap: wrap;
332
+ max-width: 900px;
333
+ margin: var(--space-7) auto 0;
334
+ }
335
+
336
+ .landing-step {
337
+ flex: 1;
338
+ min-width: 200px;
339
+ max-width: 260px;
340
+ text-align: center;
341
+ }
342
+
343
+ .landing-step-num {
344
+ width: 48px;
345
+ height: 48px;
346
+ border-radius: var(--radius-round);
347
+ background: var(--color-active);
348
+ color: white;
349
+ font-size: var(--text-3); /* L3 */
350
+ font-weight: var(--weight-7);
351
+ display: inline-flex;
352
+ align-items: center;
353
+ justify-content: center;
354
+ margin-bottom: var(--space-3);
355
+ }
356
+
357
+ .landing-step h3 { /* L3 */
358
+ font-size: var(--text-3);
359
+ font-weight: var(--weight-6);
360
+ margin-bottom: var(--space-2);
361
+ }
362
+
363
+ .landing-step p { /* L4 */
364
+ font-size: var(--text-2);
365
+ color: var(--text-muted);
366
+ line-height: var(--leading-2);
367
+ }
368
+
369
+ .landing-step-arrow {
370
+ font-size: var(--text-5); /* L2 */
371
+ color: var(--text-muted);
372
+ padding-top: var(--space-3);
373
+ user-select: none;
374
+ }
375
+
376
+ /* ─── CTA Section ───────────────────────────────────────────────────────────── */
377
+ .landing-cta {
378
+ text-align: center;
379
+ padding: var(--space-11) 0;
380
+ background: var(--surface-bg);
381
+ }
382
+
383
+ .landing-cta-title { /* L1 */
384
+ font-size: var(--text-7);
385
+ font-weight: var(--weight-8);
386
+ letter-spacing: 0.02em;
387
+ margin-bottom: var(--space-3);
388
+ }
389
+
390
+ .landing-cta-sub { /* L4 */
391
+ font-size: var(--text-2);
392
+ color: var(--text-muted);
393
+ margin-bottom: var(--space-7);
394
+ max-width: 480px;
395
+ margin-left: auto;
396
+ margin-right: auto;
397
+ }
398
+
399
+ /* ─── Footer (center-aligned) ───────────────────────────────────────────────── */
400
+ .landing-footer {
401
+ border-top: var(--border-1) solid var(--border-color);
402
+ padding: var(--space-5) 0;
403
+ }
404
+
405
+ .landing-footer-inner {
406
+ display: flex;
407
+ flex-direction: column;
408
+ align-items: center;
409
+ gap: var(--space-2);
410
+ text-align: center;
411
+ }
412
+
413
+ .landing-footer-brand { /* L4 */
414
+ font-weight: var(--weight-6);
415
+ font-size: var(--text-2);
416
+ color: var(--text-primary);
417
+ }
418
+
419
+ .landing-footer-copy { /* L4 */
420
+ font-size: var(--text-2);
421
+ color: var(--text-muted);
422
+ }
423
+
424
+ /* ─── Demo: Theme-aware video switching ────────────────────────────────────── */
425
+ .landing-demo-dark { display: none; }
426
+
427
+ body.dark-mode .landing-demo-light { display: none; }
428
+ body.dark-mode .landing-demo-dark { display: block; }
429
+
430
+ @media (prefers-color-scheme: dark) {
431
+ body:not(.light-mode) .landing-demo-light { display: none; }
432
+ body:not(.light-mode) .landing-demo-dark { display: block; }
433
+ }
434
+
435
+ /* ─── Responsive ────────────────────────────────────────────────────────────── */
436
+ @media (max-width: 640px) {
437
+ .landing-container {
438
+ padding: 0 var(--space-3);
439
+ }
440
+
441
+ .landing-hero {
442
+ padding: var(--space-9) 0 var(--space-7);
443
+ }
444
+
445
+ .landing-hero-title { /* L1 scaled down on mobile */
446
+ font-size: var(--text-5);
447
+ }
448
+
449
+ .landing-hero-brand { /* L1 scaled down on mobile */
450
+ font-size: var(--text-5);
451
+ }
452
+
453
+ .landing-hero-glow {
454
+ width: 350px;
455
+ height: 350px;
456
+ }
457
+
458
+ .landing-section {
459
+ padding: var(--space-8) 0;
460
+ }
461
+
462
+ .landing-steps {
463
+ flex-direction: column;
464
+ align-items: center;
465
+ }
466
+
467
+ .landing-step-arrow {
468
+ transform: rotate(90deg);
469
+ padding: 0;
470
+ }
471
+
472
+ .landing-step {
473
+ max-width: 100%;
474
+ }
475
+
476
+ .landing-features-grid {
477
+ grid-template-columns: 1fr;
478
+ }
479
+
480
+ .landing-cta-title { /* L1 scaled down on mobile */
481
+ font-size: var(--text-5);
482
+ }
483
+ }
484
+
485
+ /* ─── Entrance animations ───────────────────────────────────────────────────── */
486
+ @keyframes landing-fade-up {
487
+ from {
488
+ opacity: 0;
489
+ transform: translateY(20px);
490
+ }
491
+ to {
492
+ opacity: 1;
493
+ transform: translateY(0);
494
+ }
495
+ }
496
+
497
+ .landing-hero-title,
498
+ .landing-hero-sub,
499
+ .landing-hero-brand,
500
+ .landing-hero-actions {
501
+ animation: landing-fade-up 0.6s var(--ease-out-3) both;
502
+ }
503
+
504
+ .landing-hero-title { animation-delay: 0s; }
505
+ .landing-hero-sub { animation-delay: 0.1s; }
506
+ .landing-hero-brand { animation-delay: 0.2s; }
507
+ .landing-hero-actions { animation-delay: 0.3s; }
@@ -1,5 +1,12 @@
1
1
  module Collavre
2
2
  class CommentsPresenceChannel < ApplicationCable::Channel
3
+ def self.broadcast_channel_chips_changed(creative_id, topic_id:)
4
+ ActionCable.server.broadcast(
5
+ "comments_presence:#{creative_id}",
6
+ { channel_chips: { topic_id: topic_id } }
7
+ )
8
+ end
9
+
3
10
  def self.broadcast_shares_changed(creative_id, shared_user_id:, permission: nil, action: "updated", has_access: nil, can_comment: nil, has_access_changed: nil, can_comment_changed: nil)
4
11
  ActionCable.server.broadcast(
5
12
  "comments_presence:#{creative_id}",
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ module Admin
5
+ class IntegrationsController < ApplicationController
6
+ before_action :require_system_admin!
7
+ before_action :load_definition!, only: [ :reset ]
8
+
9
+ Registry = Collavre::IntegrationSettings::Registry
10
+ Resolver = Collavre::IntegrationSettings::Resolver
11
+
12
+ def index
13
+ @grouped_settings = build_grouped_settings
14
+ end
15
+
16
+ def bulk_update
17
+ submitted = (params[:integration_setting] || {}).to_unsafe_h
18
+ restart_required_changed = false
19
+
20
+ Collavre::IntegrationSetting.transaction do
21
+ submitted.each do |key, raw_value|
22
+ value = raw_value.to_s
23
+ next if value.blank?
24
+
25
+ definition = Registry.instance.find(key) or next
26
+
27
+ row = Collavre::IntegrationSetting.find_or_initialize_by(key: definition.key.to_s)
28
+ row.category = definition.category
29
+ row.value = value
30
+ row.save!
31
+
32
+ restart_required_changed ||= definition.requires_restart
33
+ end
34
+ end
35
+
36
+ flash[:notice] = t("collavre.admin.integrations.flash.updated")
37
+ flash[:warning] = t("collavre.admin.integrations.flash.restart_required") if restart_required_changed
38
+ redirect_to collavre.admin_integrations_path
39
+ end
40
+
41
+ def reset
42
+ Collavre::IntegrationSetting.where(key: @definition.key.to_s).destroy_all
43
+ redirect_to collavre.admin_integrations_path,
44
+ notice: t("collavre.admin.integrations.flash.reset_done", key: @definition.key)
45
+ end
46
+
47
+ private
48
+
49
+ def load_definition!
50
+ @definition = Registry.instance.find(params[:key]) or
51
+ (render file: Rails.root.join("public/404.html"), status: :not_found, layout: false and return)
52
+ end
53
+
54
+ def build_grouped_settings
55
+ Registry.instance.by_category.sort_by { |category, _defs| category.to_s }.map do |category, definitions|
56
+ rows = definitions.map { |definition| build_row(definition) }
57
+ [ category, rows ]
58
+ end
59
+ end
60
+
61
+ def build_row(definition)
62
+ value = Resolver.get(definition.key)
63
+ source = Resolver.source_for(definition.key)
64
+ display = definition.sensitive ? mask_value(value) : value
65
+
66
+ {
67
+ definition: definition,
68
+ source: source,
69
+ display_value: display,
70
+ present: value.present?
71
+ }
72
+ end
73
+
74
+ def mask_value(value)
75
+ return nil if value.blank?
76
+ return "••••" if value.to_s.length <= 4
77
+
78
+ "••••#{value.to_s[-4..]}"
79
+ end
80
+ end
81
+ end
82
+ end
@@ -10,6 +10,7 @@ module Collavre
10
10
  @mcp_tool_approval = SystemSetting.find_by(key: "mcp_tool_approval_required")&.value == "true"
11
11
  @creatives_login_required = SystemSetting.creatives_login_required?
12
12
  @home_page_path = SystemSetting.home_page_path
13
+ @home_page_path_authenticated = SystemSetting.home_page_path_authenticated
13
14
 
14
15
  # Account lockout settings
15
16
  @max_login_attempts = SystemSetting.max_login_attempts
@@ -93,23 +94,9 @@ module Collavre
93
94
  creatives_login_setting.value = params[:creatives_login_required] == "1" ? "true" : "false"
94
95
  creatives_login_setting.save!
95
96
 
96
- # Home Page Path
97
- home_page_path_input = params[:home_page_path].to_s.strip
98
- if home_page_path_input.present?
99
- normalized_path, error = validate_and_normalize_home_page_path(home_page_path_input)
100
- if error
101
- home_page_setting = SystemSetting.new(key: "home_page_path")
102
- home_page_setting.errors.add(:base, error)
103
- raise ActiveRecord::RecordInvalid, home_page_setting
104
- end
105
- home_page_setting = SystemSetting.find_or_initialize_by(key: "home_page_path")
106
- home_page_setting.value = normalized_path
107
- home_page_setting.save!
108
- else
109
- home_page_setting = SystemSetting.find_or_initialize_by(key: "home_page_path")
110
- home_page_setting.value = nil
111
- home_page_setting.save!
112
- end
97
+ # Home Page Paths (unauthenticated default + authenticated override)
98
+ save_home_page_path("home_page_path", params[:home_page_path])
99
+ save_home_page_path("home_page_path_authenticated", params[:home_page_path_authenticated])
113
100
 
114
101
  # Account Lockout Settings
115
102
  max_attempts = params[:max_login_attempts].to_i
@@ -171,6 +158,7 @@ module Collavre
171
158
  @mcp_tool_approval = params[:mcp_tool_approval] == "1"
172
159
  @creatives_login_required = params[:creatives_login_required] == "1"
173
160
  @home_page_path = params[:home_page_path]
161
+ @home_page_path_authenticated = params[:home_page_path_authenticated]
174
162
  @max_login_attempts = params[:max_login_attempts].to_i.positive? ? params[:max_login_attempts].to_i : SystemSetting::DEFAULT_MAX_LOGIN_ATTEMPTS
175
163
  @lockout_duration_minutes = params[:lockout_duration_minutes].to_i.positive? ? params[:lockout_duration_minutes].to_i : SystemSetting::DEFAULT_LOCKOUT_DURATION_MINUTES
176
164
  @password_min_length = [ [ params[:password_min_length].to_i, SystemSetting::DEFAULT_PASSWORD_MIN_LENGTH ].max, 72 ].min
@@ -186,6 +174,23 @@ module Collavre
186
174
 
187
175
  private
188
176
 
177
+ def save_home_page_path(key, raw_value)
178
+ input = raw_value.to_s.strip
179
+ setting = SystemSetting.find_or_initialize_by(key: key)
180
+ if input.present?
181
+ normalized_path, error = validate_and_normalize_home_page_path(input)
182
+ if error
183
+ stub = SystemSetting.new(key: key)
184
+ stub.errors.add(:base, error)
185
+ raise ActiveRecord::RecordInvalid, stub
186
+ end
187
+ setting.value = normalized_path
188
+ else
189
+ setting.value = nil
190
+ end
191
+ setting.save!
192
+ end
193
+
189
194
  def validate_and_normalize_home_page_path(value)
190
195
  path = value.to_s.strip
191
196
 
@@ -9,8 +9,35 @@ module Collavre
9
9
  # of sync with the session cookie and POSTs fail with 422.
10
10
  after_action :set_csrf_token_header
11
11
 
12
+ before_action :redirect_authenticated_root_to_home
13
+
12
14
  private
13
15
 
16
+ # If the original request was for "/" and the user is signed in,
17
+ # honor SystemSetting.home_page_path_authenticated by redirecting.
18
+ # The middleware preserves "/" as a stable URL for unauthenticated
19
+ # visitors; authenticated users get a real URL change so the address
20
+ # bar reflects state.
21
+ #
22
+ # Loop safety: the env flag is only set when PATH_INFO == "/" hits
23
+ # the middleware. Direct visits to the redirect target never trip it,
24
+ # so comparing request.path to target would be wrong here — when both
25
+ # home_page_path and home_page_path_authenticated resolve to the same
26
+ # path, the middleware has already rewritten request.path to that
27
+ # target while the browser URL is still "/", and we must still
28
+ # redirect so the address bar reflects state.
29
+ def redirect_authenticated_root_to_home
30
+ return unless request.get? || request.head?
31
+ return unless request.env["collavre.root_request"]
32
+ return unless authenticated?
33
+
34
+ target = SystemSetting.home_page_path_authenticated
35
+ return if target.blank?
36
+ return if target == "/"
37
+
38
+ redirect_to target
39
+ end
40
+
14
41
  def set_csrf_token_header
15
42
  return unless protect_against_forgery?
16
43