collavre 0.20.2 → 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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/collavre/actiontext.css +92 -2
- data/app/assets/stylesheets/collavre/code_highlight.css +144 -26
- data/app/assets/stylesheets/collavre/comments_popup.css +83 -0
- data/app/assets/stylesheets/collavre/landing.css +507 -0
- data/app/channels/collavre/comments_presence_channel.rb +7 -0
- data/app/controllers/collavre/admin/integrations_controller.rb +82 -0
- data/app/controllers/collavre/admin/settings_controller.rb +22 -17
- data/app/controllers/collavre/application_controller.rb +27 -0
- data/app/controllers/collavre/channels_controller.rb +23 -0
- data/app/controllers/collavre/creatives_controller.rb +50 -6
- data/app/controllers/collavre/landing_controller.rb +8 -0
- data/app/controllers/collavre/passwords_controller.rb +1 -0
- data/app/controllers/collavre/public_assets_controller.rb +24 -0
- data/app/controllers/collavre/topics_controller.rb +21 -30
- data/app/helpers/collavre/comments_helper.rb +7 -0
- data/app/helpers/collavre/public_assets_helper.rb +14 -0
- data/app/javascript/controllers/comment_controller.js +9 -0
- data/app/javascript/controllers/comments/form_controller.js +4 -0
- data/app/javascript/controllers/comments/list_controller.js +10 -7
- data/app/javascript/controllers/comments/popup_controller.js +9 -0
- data/app/javascript/controllers/comments/presence_controller.js +83 -1
- data/app/javascript/controllers/comments/topics_controller.js +15 -0
- data/app/javascript/controllers/creatives/__tests__/sync_controller.test.js +89 -0
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +120 -0
- data/app/javascript/controllers/creatives/sync_controller.js +30 -9
- data/app/javascript/controllers/creatives/tree_controller.js +23 -0
- data/app/javascript/controllers/index.js +4 -1
- data/app/javascript/controllers/landing_video_controller.js +53 -0
- data/app/javascript/creatives/tree_renderer.js +6 -0
- data/app/javascript/lib/api/__tests__/queue_manager.test.js +27 -0
- data/app/javascript/lib/api/queue_manager.js +17 -5
- data/app/javascript/modules/__tests__/command_args_form.test.js +103 -0
- data/app/javascript/modules/__tests__/html_content_empty.test.js +41 -0
- data/app/javascript/modules/__tests__/markdown_source_reconcile.test.js +70 -0
- data/app/javascript/modules/command_args_form.js +22 -4
- data/app/javascript/modules/command_menu.js +27 -0
- data/app/javascript/modules/creative_row_editor.js +227 -17
- data/app/javascript/modules/html_content_empty.js +12 -0
- data/app/javascript/modules/markdown_source_reconcile.js +53 -0
- data/app/jobs/collavre/drop_trigger_job.rb +37 -8
- data/app/mailers/collavre/application_mailer.rb +1 -1
- data/app/models/collavre/channel/injected_message.rb +5 -0
- data/app/models/collavre/channel.rb +87 -0
- data/app/models/collavre/creative/describable.rb +65 -3
- data/app/models/collavre/creative.rb +2 -0
- data/app/models/collavre/integration_setting.rb +35 -0
- data/app/models/collavre/preview_channel.rb +93 -0
- data/app/models/collavre/system_setting.rb +13 -2
- data/app/models/collavre/topic.rb +3 -25
- data/app/models/concerns/collavre/ai_agent_resolvable.rb +12 -3
- data/app/services/collavre/ai_client.rb +3 -3
- data/app/services/collavre/channel_attacher.rb +58 -0
- data/app/services/collavre/comments/mcp_command.rb +31 -1
- data/app/services/collavre/creatives/tree_builder.rb +7 -3
- data/app/services/collavre/google_calendar_service.rb +4 -2
- data/app/services/collavre/markdown_converter.rb +130 -15
- data/app/services/collavre/markdown_importer.rb +7 -2
- data/app/services/collavre/orchestration/policy_resolver.rb +11 -1
- data/app/services/collavre/tools/creative_attach_files_service.rb +96 -0
- data/app/services/collavre/tools/creative_list_attachments_service.rb +42 -0
- data/app/services/collavre/tools/creative_remove_attachment_service.rb +35 -0
- data/app/services/collavre/tools/permission_denied_error.rb +9 -0
- data/app/services/collavre/tools/preview_attach_service.rb +128 -0
- data/app/services/collavre/tools/preview_detach_service.rb +61 -0
- data/app/services/collavre/tools/topic_authorizer.rb +24 -0
- data/app/services/collavre/topic_branch_service.rb +34 -26
- data/app/views/admin/shared/_tabs.html.erb +1 -0
- data/app/views/collavre/admin/integrations/_category.html.erb +22 -0
- data/app/views/collavre/admin/integrations/_setting_row.html.erb +54 -0
- data/app/views/collavre/admin/integrations/index.html.erb +42 -0
- data/app/views/collavre/admin/settings/_system_tab.html.erb +8 -0
- data/app/views/collavre/comments/_channel_chips.html.erb +33 -0
- data/app/views/collavre/comments/_comment.html.erb +6 -1
- data/app/views/collavre/comments/_comments_popup.html.erb +1 -0
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +19 -0
- data/app/views/collavre/creatives/index.html.erb +10 -2
- data/app/views/collavre/landing/show.html.erb +130 -0
- data/app/views/layouts/collavre/landing.html.erb +33 -0
- data/config/locales/admin.en.yml +4 -2
- data/config/locales/admin.ko.yml +4 -2
- data/config/locales/channels.en.yml +11 -0
- data/config/locales/channels.ko.yml +11 -0
- data/config/locales/comments.en.yml +2 -0
- data/config/locales/comments.ko.yml +2 -0
- data/config/locales/creatives.en.yml +9 -0
- data/config/locales/creatives.ko.yml +8 -0
- data/config/locales/integrations.en.yml +44 -0
- data/config/locales/integrations.ko.yml +44 -0
- data/config/locales/landing.en.yml +51 -0
- data/config/locales/landing.ko.yml +51 -0
- data/config/routes.rb +18 -0
- data/db/migrate/20260526000000_create_channels.rb +42 -0
- data/db/migrate/20260527000000_add_dismissed_at_to_channels.rb +6 -0
- data/db/migrate/20260527000100_backfill_dismissed_at_for_legacy_detached_channels.rb +28 -0
- data/db/migrate/20260528000000_add_preview_channel_unique_index.rb +31 -0
- data/db/migrate/20260529000000_add_primary_agent_id_to_topics.rb +40 -0
- data/db/migrate/20260529100000_create_integration_settings.rb +15 -0
- data/db/seeds.rb +19 -0
- data/lib/collavre/aws_credentials.rb +75 -0
- data/lib/collavre/engine.rb +51 -0
- data/lib/collavre/integration_settings/key_definition.rb +29 -0
- data/lib/collavre/integration_settings/registry.rb +55 -0
- data/lib/collavre/integration_settings/resolver.rb +71 -0
- data/lib/collavre/integration_settings.rb +46 -0
- data/lib/collavre/ses_settings_interceptor.rb +72 -0
- data/lib/collavre/version.rb +1 -1
- data/lib/collavre.rb +3 -0
- metadata +52 -1
|
@@ -132,6 +132,12 @@ en:
|
|
|
132
132
|
metadata_tooltip: Edit metadata
|
|
133
133
|
metadata_title: Metadata
|
|
134
134
|
metadata_save: Save
|
|
135
|
+
toggle_markdown: Markdown
|
|
136
|
+
toggle_richtext: Rich Text
|
|
137
|
+
markdown_to_richtext_confirm: Switching to Rich Text will discard the Markdown
|
|
138
|
+
source. Continue?
|
|
139
|
+
richtext_to_markdown_confirm: Switching to Markdown will discard the current
|
|
140
|
+
rich text content. Continue?
|
|
135
141
|
share:
|
|
136
142
|
shared: Creative shared successfully.
|
|
137
143
|
permission_updated: Permission updated successfully.
|
|
@@ -147,8 +153,11 @@ en:
|
|
|
147
153
|
progress_recalculated: All parent progress recalculated.
|
|
148
154
|
errors:
|
|
149
155
|
no_permission: You do not have permission to perform this action.
|
|
156
|
+
metadata_must_be_object: Metadata must be an object.
|
|
150
157
|
inline_editor:
|
|
151
158
|
placeholder: Describe the creative…
|
|
159
|
+
markdown_placeholder: Write in Markdown…
|
|
160
|
+
markdown_preview_placeholder: Preview
|
|
152
161
|
edit_title: Edit creative
|
|
153
162
|
edit:
|
|
154
163
|
inline_editor_only_html: Editing creatives now happens directly in the inline
|
|
@@ -120,6 +120,11 @@ ko:
|
|
|
120
120
|
metadata_tooltip: 메타데이터 편집
|
|
121
121
|
metadata_title: 메타데이터
|
|
122
122
|
metadata_save: 저장
|
|
123
|
+
toggle_markdown: 마크다운
|
|
124
|
+
toggle_richtext: 서식 편집
|
|
125
|
+
markdown_to_richtext_confirm: 서식 편집으로 전환하면 마크다운 원본이 삭제됩니다.
|
|
126
|
+
계속하시겠습니까?
|
|
127
|
+
richtext_to_markdown_confirm: 마크다운으로 전환하면 현재 서식 편집 내용이 삭제됩니다. 계속하시겠습니까?
|
|
123
128
|
share:
|
|
124
129
|
shared: 크리에이티브가 성공적으로 공유되었습니다.
|
|
125
130
|
permission_updated: 권한이 성공적으로 변경되었습니다.
|
|
@@ -134,8 +139,11 @@ ko:
|
|
|
134
139
|
progress_recalculated: 진행률을 다시 계산 했습니다.
|
|
135
140
|
errors:
|
|
136
141
|
no_permission: 이 작업을 수행할 권한이 없습니다.
|
|
142
|
+
metadata_must_be_object: 메타데이터는 객체여야 합니다.
|
|
137
143
|
inline_editor:
|
|
138
144
|
placeholder: 크리에이티브를 설명해주세요…
|
|
145
|
+
markdown_placeholder: 마크다운으로 작성…
|
|
146
|
+
markdown_preview_placeholder: 미리보기
|
|
139
147
|
edit_title: 크리에이티브 수정
|
|
140
148
|
edit:
|
|
141
149
|
inline_editor_only_html: 크리에이티브 수정은 인라인 편집기에서 바로 진행하세요.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
en:
|
|
2
|
+
collavre:
|
|
3
|
+
admin:
|
|
4
|
+
integrations:
|
|
5
|
+
title: "Integrations"
|
|
6
|
+
subtitle: "Configure external integration secrets. Database values take precedence over ENV; ENV is the seed/fallback."
|
|
7
|
+
tab_label: "Integrations"
|
|
8
|
+
empty_state: "No integrations registered yet."
|
|
9
|
+
sensitive: "(sensitive)"
|
|
10
|
+
restart_required: "restart required"
|
|
11
|
+
placeholder_leave_blank: "Leave blank to keep current"
|
|
12
|
+
env_var_label: "ENV"
|
|
13
|
+
confirm_reset: "Delete the DB value and fall back to ENV?"
|
|
14
|
+
headers:
|
|
15
|
+
key: "Key"
|
|
16
|
+
value: "Value"
|
|
17
|
+
actions: "Actions"
|
|
18
|
+
source:
|
|
19
|
+
db: "DB"
|
|
20
|
+
env: "ENV"
|
|
21
|
+
default: "default"
|
|
22
|
+
unknown: "unknown"
|
|
23
|
+
category:
|
|
24
|
+
slack: "Slack"
|
|
25
|
+
google_oauth: "Google OAuth"
|
|
26
|
+
github_oauth: "GitHub OAuth"
|
|
27
|
+
notion_oauth: "Notion OAuth"
|
|
28
|
+
github: "GitHub Integration"
|
|
29
|
+
aws_s3: "AWS S3"
|
|
30
|
+
aws_ses: "AWS SES"
|
|
31
|
+
firebase: "Firebase"
|
|
32
|
+
fcm: "FCM"
|
|
33
|
+
gemini: "Gemini"
|
|
34
|
+
llm: "LLM API Keys"
|
|
35
|
+
openclaw: "OpenClaw Gateway"
|
|
36
|
+
mail: "Mail / Host"
|
|
37
|
+
misc: "Miscellaneous"
|
|
38
|
+
actions:
|
|
39
|
+
save: "Save changes"
|
|
40
|
+
reset: "Reset to ENV"
|
|
41
|
+
flash:
|
|
42
|
+
updated: "Integration settings saved."
|
|
43
|
+
restart_required: "Some changed keys require a server restart to take effect."
|
|
44
|
+
reset_done: "Reset %{key} to ENV/default."
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
ko:
|
|
2
|
+
collavre:
|
|
3
|
+
admin:
|
|
4
|
+
integrations:
|
|
5
|
+
title: "통합"
|
|
6
|
+
subtitle: "외부 통합 비밀값을 설정합니다. DB 값이 ENV보다 우선하며, ENV는 시드/폴백입니다."
|
|
7
|
+
tab_label: "통합"
|
|
8
|
+
empty_state: "등록된 통합이 없습니다."
|
|
9
|
+
sensitive: "(민감)"
|
|
10
|
+
restart_required: "재시작 필요"
|
|
11
|
+
placeholder_leave_blank: "비워두면 현재 값 유지"
|
|
12
|
+
env_var_label: "환경변수"
|
|
13
|
+
confirm_reset: "DB 값을 삭제하고 ENV로 폴백할까요?"
|
|
14
|
+
headers:
|
|
15
|
+
key: "키"
|
|
16
|
+
value: "값"
|
|
17
|
+
actions: "작업"
|
|
18
|
+
source:
|
|
19
|
+
db: "DB"
|
|
20
|
+
env: "ENV"
|
|
21
|
+
default: "기본값"
|
|
22
|
+
unknown: "알 수 없음"
|
|
23
|
+
category:
|
|
24
|
+
slack: "Slack"
|
|
25
|
+
google_oauth: "Google OAuth"
|
|
26
|
+
github_oauth: "GitHub OAuth"
|
|
27
|
+
notion_oauth: "Notion OAuth"
|
|
28
|
+
github: "GitHub 연동"
|
|
29
|
+
aws_s3: "AWS S3"
|
|
30
|
+
aws_ses: "AWS SES"
|
|
31
|
+
firebase: "Firebase"
|
|
32
|
+
fcm: "FCM"
|
|
33
|
+
gemini: "Gemini"
|
|
34
|
+
llm: "LLM API 키"
|
|
35
|
+
openclaw: "OpenClaw 게이트웨이"
|
|
36
|
+
mail: "메일 / 호스트"
|
|
37
|
+
misc: "기타"
|
|
38
|
+
actions:
|
|
39
|
+
save: "변경사항 저장"
|
|
40
|
+
reset: "ENV로 초기화"
|
|
41
|
+
flash:
|
|
42
|
+
updated: "통합 설정이 저장되었습니다."
|
|
43
|
+
restart_required: "변경된 일부 키는 서버 재시작 후에 적용됩니다."
|
|
44
|
+
reset_done: "%{key}을(를) ENV/기본값으로 초기화했습니다."
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
en:
|
|
2
|
+
collavre:
|
|
3
|
+
landing:
|
|
4
|
+
meta:
|
|
5
|
+
title: "Where Teams Think Together"
|
|
6
|
+
description: "The collaborative workspace where hierarchical creatives, AI agents, and real-time chat come together. Organize, collaborate, and ship — with AI as your teammate."
|
|
7
|
+
hero:
|
|
8
|
+
headline_html: "A platform for both AI agents and people."
|
|
9
|
+
subline: "Achieve sustainable productivity gains."
|
|
10
|
+
learn_more: "Learn More"
|
|
11
|
+
demo:
|
|
12
|
+
title: "See it in action"
|
|
13
|
+
placeholder: "Product demo video goes here"
|
|
14
|
+
problem:
|
|
15
|
+
title: "Your tools are failing you"
|
|
16
|
+
notion:
|
|
17
|
+
title: "Notion"
|
|
18
|
+
desc: "Docs become graveyards. Nobody edits someone else's page. Search replaces structure."
|
|
19
|
+
jira:
|
|
20
|
+
title: "Jira"
|
|
21
|
+
desc: "Great for tasks, terrible for knowledge. Results never sync back to the wiki. Information overload."
|
|
22
|
+
slack:
|
|
23
|
+
title: "Slack"
|
|
24
|
+
desc: "Conversations vanish in the stream. Flat channel lists explode. Context is lost daily."
|
|
25
|
+
features:
|
|
26
|
+
title: "Everything in one living tree"
|
|
27
|
+
subtitle: "Creatives are hierarchical blocks that organize your work, track progress, and carry context — all at once."
|
|
28
|
+
tree:
|
|
29
|
+
title: "Hierarchical Creatives"
|
|
30
|
+
desc: "Organize work as a tree. Drag, link, and reuse blocks across projects. Progress rolls up automatically."
|
|
31
|
+
ai:
|
|
32
|
+
title: "AI Agents as Teammates"
|
|
33
|
+
desc: "Add AI agents to any conversation. They review, collaborate, and respond to @mentions — just like a colleague."
|
|
34
|
+
context:
|
|
35
|
+
title: "Context System"
|
|
36
|
+
desc: "Reduce cognitive load for people, and inject the knowledge and skills AI needs."
|
|
37
|
+
chat:
|
|
38
|
+
title: "Real-time Chat & Topics"
|
|
39
|
+
desc: "Every creative has a chat. Organize conversations with topics, move messages between creatives, and keep discussions focused."
|
|
40
|
+
integrations:
|
|
41
|
+
title: "Integrations"
|
|
42
|
+
desc: "GitHub PRs auto-analyzed, Slack messages synced, Google Calendar events linked, MCP tools built-in."
|
|
43
|
+
progress:
|
|
44
|
+
title: "Live Progress"
|
|
45
|
+
desc: "Track completion from leaf to root. Filter by tags, plans, or status. See your project's real health at a glance."
|
|
46
|
+
cta:
|
|
47
|
+
title: "Ready to collaborate smarter?"
|
|
48
|
+
subtitle: "Start organizing your work with AI-powered context."
|
|
49
|
+
start: "Get Started"
|
|
50
|
+
footer:
|
|
51
|
+
rights: "All rights reserved."
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
ko:
|
|
2
|
+
collavre:
|
|
3
|
+
landing:
|
|
4
|
+
meta:
|
|
5
|
+
title: "팀이 함께 생각하는 곳"
|
|
6
|
+
description: "계층형 크리에이티브, AI 에이전트, 실시간 채팅이 하나로. 체계적으로 정리하고, 협업하고, AI와 함께 완성하세요."
|
|
7
|
+
hero:
|
|
8
|
+
headline_html: "AI 에이전트와 사람 모두의 플랫폼."
|
|
9
|
+
subline: "지속 가능한 생산성 향상을 이루세요."
|
|
10
|
+
learn_more: "알아보기"
|
|
11
|
+
demo:
|
|
12
|
+
title: "직접 확인하세요"
|
|
13
|
+
placeholder: "실제 작동 화면이 들어갈 자리입니다"
|
|
14
|
+
problem:
|
|
15
|
+
title: "기존 도구의 한계"
|
|
16
|
+
notion:
|
|
17
|
+
title: "Notion"
|
|
18
|
+
desc: "문서는 무덤이 됩니다. 남의 페이지를 고치기 꺼려하고, 검색이 구조를 대체합니다."
|
|
19
|
+
jira:
|
|
20
|
+
title: "Jira"
|
|
21
|
+
desc: "작업 관리는 좋지만 지식 관리는 안 됩니다. 결과물이 위키로 돌아오지 않습니다."
|
|
22
|
+
slack:
|
|
23
|
+
title: "Slack"
|
|
24
|
+
desc: "대화가 흘러가 버립니다. 채널은 폭발적으로 늘어나고, 맥락은 매일 사라집니다."
|
|
25
|
+
features:
|
|
26
|
+
title: "하나의 살아있는 트리로"
|
|
27
|
+
subtitle: "크리에이티브는 작업을 정리하고, 진행률을 추적하며, 맥락을 전달하는 계층형 블록입니다."
|
|
28
|
+
tree:
|
|
29
|
+
title: "계층형 크리에이티브"
|
|
30
|
+
desc: "작업을 트리로 구성하세요. 드래그, 링크, 재사용이 자유롭고 진행률은 자동 계산됩니다."
|
|
31
|
+
ai:
|
|
32
|
+
title: "AI 에이전트 팀원"
|
|
33
|
+
desc: "AI 에이전트를 대화에 추가하세요. 리뷰, 협업, @멘션 응답 — 동료처럼 일합니다."
|
|
34
|
+
context:
|
|
35
|
+
title: "컨텍스트 시스템"
|
|
36
|
+
desc: "사람의 인지 부하를 줄이고, AI에게 필요한 지식과 Skill을 주입합니다."
|
|
37
|
+
chat:
|
|
38
|
+
title: "실시간 채팅 & 토픽"
|
|
39
|
+
desc: "모든 크리에이티브에 채팅이 있습니다. 토픽으로 대화를 정리하고, 메시지를 다른 크리에이티브로 옮기세요."
|
|
40
|
+
integrations:
|
|
41
|
+
title: "외부 연동"
|
|
42
|
+
desc: "GitHub PR 자동 분석, Slack 동기화, Google Calendar 연동, MCP 도구 내장."
|
|
43
|
+
progress:
|
|
44
|
+
title: "실시간 진행률"
|
|
45
|
+
desc: "리프부터 루트까지 완료율 추적. 태그, 계획, 상태별 필터. 프로젝트 건강도를 한눈에."
|
|
46
|
+
cta:
|
|
47
|
+
title: "더 똑똑하게 협업할 준비 되셨나요?"
|
|
48
|
+
subtitle: "AI 기반 컨텍스트로 작업을 정리하세요."
|
|
49
|
+
start: "시작하기"
|
|
50
|
+
footer:
|
|
51
|
+
rights: "모든 권리 보유."
|
data/config/routes.rb
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
Collavre::Engine.routes.draw do
|
|
2
|
+
# Landing page
|
|
3
|
+
get "landing", to: "landing#show"
|
|
4
|
+
|
|
2
5
|
# Authentication routes
|
|
3
6
|
resource :session, only: [ :new, :create, :destroy ]
|
|
4
7
|
resources :passwords, param: :token, only: [ :new, :create, :edit, :update ]
|
|
@@ -28,8 +31,13 @@ Collavre::Engine.routes.draw do
|
|
|
28
31
|
match "/auth/google_oauth2/callback", to: "google_auth#callback", via: [ :get, :post ]
|
|
29
32
|
|
|
30
33
|
delete "/attachments/:signed_id", to: "attachments#destroy", as: :attachment
|
|
34
|
+
get "/public-assets/blobs/:signed_id/*filename",
|
|
35
|
+
to: "public_assets#show",
|
|
36
|
+
as: :public_asset,
|
|
37
|
+
format: false
|
|
31
38
|
|
|
32
39
|
resources :calendar_events, only: [ :destroy ]
|
|
40
|
+
resources :channels, only: [ :destroy ]
|
|
33
41
|
resources :contacts, only: [ :destroy ]
|
|
34
42
|
resources :devices, only: [ :create ]
|
|
35
43
|
|
|
@@ -58,6 +66,7 @@ Collavre::Engine.routes.draw do
|
|
|
58
66
|
get :next_name
|
|
59
67
|
end
|
|
60
68
|
member do
|
|
69
|
+
get :channel_chips
|
|
61
70
|
patch :move
|
|
62
71
|
patch :archive
|
|
63
72
|
patch :unarchive
|
|
@@ -134,5 +143,14 @@ Collavre::Engine.routes.draw do
|
|
|
134
143
|
get "/uiux", to: "admin/settings#uiux", as: :uiux
|
|
135
144
|
patch "/uiux", to: "admin/settings#update_uiux"
|
|
136
145
|
resource :orchestration, only: [ :show, :update ], controller: "admin/orchestration"
|
|
146
|
+
|
|
147
|
+
resources :integrations, only: [ :index ], param: :key, controller: "admin/integrations" do
|
|
148
|
+
collection do
|
|
149
|
+
patch :bulk_update
|
|
150
|
+
end
|
|
151
|
+
member do
|
|
152
|
+
delete :reset
|
|
153
|
+
end
|
|
154
|
+
end
|
|
137
155
|
end
|
|
138
156
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
class CreateChannels < ActiveRecord::Migration[8.1]
|
|
2
|
+
def up
|
|
3
|
+
postgres = connection.adapter_name == "PostgreSQL"
|
|
4
|
+
|
|
5
|
+
create_table :channels do |t|
|
|
6
|
+
t.string :type, null: false
|
|
7
|
+
t.references :topic, null: false, foreign_key: { to_table: :topics }, index: true
|
|
8
|
+
if postgres
|
|
9
|
+
t.jsonb :config, null: false, default: {}
|
|
10
|
+
else
|
|
11
|
+
t.json :config, null: false, default: {}
|
|
12
|
+
end
|
|
13
|
+
t.integer :state, null: false, default: 0
|
|
14
|
+
t.string :latest_label
|
|
15
|
+
t.string :latest_link
|
|
16
|
+
t.datetime :last_event_at
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if postgres
|
|
21
|
+
add_index :channels, :config, using: :gin
|
|
22
|
+
add_index :channels, :type
|
|
23
|
+
# Concurrent webhook safety: a single PR may emit pull_request.opened twice
|
|
24
|
+
# back-to-back (or auto-attach can race with pr_monitor). Without this
|
|
25
|
+
# constraint, the dispatch loop would inject every event N times.
|
|
26
|
+
execute <<~SQL
|
|
27
|
+
CREATE UNIQUE INDEX index_channels_on_type_topic_repo_pr
|
|
28
|
+
ON channels (type, topic_id, (config->>'repo_full_name'), (config->>'pr_number'))
|
|
29
|
+
SQL
|
|
30
|
+
else
|
|
31
|
+
add_index :channels, :type
|
|
32
|
+
execute <<~SQL
|
|
33
|
+
CREATE UNIQUE INDEX index_channels_on_type_topic_repo_pr
|
|
34
|
+
ON channels (type, topic_id, json_extract(config, '$.repo_full_name'), json_extract(config, '$.pr_number'))
|
|
35
|
+
SQL
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def down
|
|
40
|
+
drop_table :channels
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class BackfillDismissedAtForLegacyDetachedChannels < ActiveRecord::Migration[8.1]
|
|
2
|
+
# Chips are now rendered via `not_dismissed` (dismissed_at IS NULL) instead
|
|
3
|
+
# of `state = active`. Without a backfill, any channel that was already
|
|
4
|
+
# detached pre-upgrade — user clicked X under the old flow, or the PR auto-
|
|
5
|
+
# detached on close — would reappear as a chip with the default-open badge
|
|
6
|
+
# the first time the new view runs. Mark legacy detached rows as dismissed
|
|
7
|
+
# using their last-touched timestamp as a best-effort dismissed_at.
|
|
8
|
+
#
|
|
9
|
+
# Idempotent: the WHERE clause skips rows that already have dismissed_at.
|
|
10
|
+
# Scoped to GithubPrChannel because the dismiss-on-detach UI ships only for
|
|
11
|
+
# PR chips. Future Channel STI subtypes opt in by adding their own backfill
|
|
12
|
+
# or by being created post-upgrade.
|
|
13
|
+
def up
|
|
14
|
+
execute <<~SQL.squish
|
|
15
|
+
UPDATE channels
|
|
16
|
+
SET dismissed_at = COALESCE(updated_at, CURRENT_TIMESTAMP)
|
|
17
|
+
WHERE state = 1
|
|
18
|
+
AND dismissed_at IS NULL
|
|
19
|
+
AND type = 'CollavreGithub::GithubPrChannel'
|
|
20
|
+
SQL
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def down
|
|
24
|
+
# No-op: we can't distinguish backfilled rows from rows the user actually
|
|
25
|
+
# dismissed at the exact same timestamp, and leaving dismissed_at populated
|
|
26
|
+
# on rollback is harmless.
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class AddPreviewChannelUniqueIndex < ActiveRecord::Migration[8.1]
|
|
2
|
+
def up
|
|
3
|
+
postgres = connection.adapter_name == "PostgreSQL"
|
|
4
|
+
|
|
5
|
+
# Mirror the PR-channel uniqueness guarantee for preview channels: a topic
|
|
6
|
+
# has at most one PreviewChannel per worktree_id. Without this, a racing
|
|
7
|
+
# preview_attach (e.g. concurrent CI + skill calls) could land two rows
|
|
8
|
+
# for the same worktree and the chip would render twice. `type` is
|
|
9
|
+
# included so the partial index does not collide with other channel
|
|
10
|
+
# subtypes that happen to store a `worktree_id` in their config.
|
|
11
|
+
if postgres
|
|
12
|
+
execute <<~SQL
|
|
13
|
+
CREATE UNIQUE INDEX index_channels_on_topic_preview_worktree
|
|
14
|
+
ON channels (topic_id, (config->>'worktree_id'))
|
|
15
|
+
WHERE type = 'Collavre::PreviewChannel'
|
|
16
|
+
SQL
|
|
17
|
+
else
|
|
18
|
+
execute <<~SQL
|
|
19
|
+
CREATE UNIQUE INDEX index_channels_on_topic_preview_worktree
|
|
20
|
+
ON channels (topic_id, json_extract(config, '$.worktree_id'))
|
|
21
|
+
WHERE type = 'Collavre::PreviewChannel'
|
|
22
|
+
SQL
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def down
|
|
27
|
+
if connection.index_exists?(:channels, nil, name: "index_channels_on_topic_preview_worktree")
|
|
28
|
+
execute "DROP INDEX index_channels_on_topic_preview_worktree"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddPrimaryAgentIdToTopics < ActiveRecord::Migration[8.0]
|
|
4
|
+
def up
|
|
5
|
+
add_column :topics, :primary_agent_id, :integer
|
|
6
|
+
add_index :topics, :primary_agent_id
|
|
7
|
+
|
|
8
|
+
# Migrate existing data from orchestrator_policies using ActiveRecord (adapter-agnostic)
|
|
9
|
+
Collavre::OrchestratorPolicy
|
|
10
|
+
.where(policy_type: "arbitration", scope_type: "Topic", enabled: true)
|
|
11
|
+
.find_each do |policy|
|
|
12
|
+
agent_id = policy.config&.dig("primary_agent_id")
|
|
13
|
+
next unless agent_id.present?
|
|
14
|
+
|
|
15
|
+
Collavre::Topic.where(id: policy.scope_id).update_all(primary_agent_id: agent_id)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Remove migrated topic-scoped arbitration policies
|
|
19
|
+
Collavre::OrchestratorPolicy
|
|
20
|
+
.where(scope_type: "Topic", policy_type: "arbitration")
|
|
21
|
+
.delete_all
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def down
|
|
25
|
+
# Re-create orchestrator_policies from topics with primary_agent_id
|
|
26
|
+
Collavre::Topic.where.not(primary_agent_id: nil).find_each do |topic|
|
|
27
|
+
Collavre::OrchestratorPolicy.create!(
|
|
28
|
+
policy_type: "arbitration",
|
|
29
|
+
scope_type: "Topic",
|
|
30
|
+
scope_id: topic.id,
|
|
31
|
+
config: { "strategy" => "primary_first", "primary_agent_id" => topic.primary_agent_id },
|
|
32
|
+
priority: 10,
|
|
33
|
+
enabled: true
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
remove_index :topics, :primary_agent_id
|
|
38
|
+
remove_column :topics, :primary_agent_id
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreateIntegrationSettings < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :integration_settings do |t|
|
|
4
|
+
t.string :key, null: false
|
|
5
|
+
t.text :value
|
|
6
|
+
t.string :category, null: false
|
|
7
|
+
t.boolean :sensitive, null: false, default: true
|
|
8
|
+
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
add_index :integration_settings, :key, unique: true
|
|
13
|
+
add_index :integration_settings, :category
|
|
14
|
+
end
|
|
15
|
+
end
|
data/db/seeds.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
module ChannelBotSeed
|
|
3
|
+
def self.call
|
|
4
|
+
email = Channel::BOT_EMAIL
|
|
5
|
+
name = Channel::BOT_NAME
|
|
6
|
+
user = User.find_or_initialize_by(email: email)
|
|
7
|
+
user.name = name
|
|
8
|
+
user.password = SecureRandom.hex(32) if user.new_record?
|
|
9
|
+
user.email_verified_at ||= Time.current
|
|
10
|
+
user.searchable = false if user.respond_to?(:searchable=)
|
|
11
|
+
user.llm_vendor = nil
|
|
12
|
+
user.save!
|
|
13
|
+
Rails.logger.info "[Collavre] Channel bot user ensured: #{email}"
|
|
14
|
+
user
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Collavre::ChannelBotSeed.call
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
# Returns source-coherent AWS credential pairs (S3 access key id + secret,
|
|
5
|
+
# SES SMTP username + password). Both halves of a pair come from the same
|
|
6
|
+
# source — DB > ENV > Rails credentials — so a partial admin save cannot
|
|
7
|
+
# combine a DB-saved value with an ENV-only sibling and produce a
|
|
8
|
+
# mismatched pair that breaks every upload or every SMTP delivery.
|
|
9
|
+
#
|
|
10
|
+
# Each entry is `[registry_key, label, env_var, credentials_path]`.
|
|
11
|
+
# `credentials_path` may be nil when the pair has no Rails.credentials
|
|
12
|
+
# fallback (S3 keys aren't carried in credentials by convention).
|
|
13
|
+
module AwsCredentials
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# @param boot_safe [Boolean] when true, swallow encryption errors so
|
|
17
|
+
# `storage.yml` / env configs can boot before
|
|
18
|
+
# `active_record_encryption.rb` runs. Runtime callers (e.g.
|
|
19
|
+
# `SesSettingsInterceptor`) MUST leave this false so a decryption
|
|
20
|
+
# failure surfaces instead of silently falling back to ENV.
|
|
21
|
+
# @return [Hash{Symbol => String}] coherent S3 credential pair or `{}`
|
|
22
|
+
def s3(boot_safe: false)
|
|
23
|
+
coherent_pair(
|
|
24
|
+
boot_safe,
|
|
25
|
+
[ :aws_s3_access_key_id, :access_key_id, "AWS_S3_ACCESS_KEY_ID", nil ],
|
|
26
|
+
[ :aws_s3_secret_access_key, :secret_access_key, "AWS_S3_SECRET_ACCESS_KEY", nil ]
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Hash{Symbol => String}] coherent SES SMTP credential pair or `{}`
|
|
31
|
+
def ses_smtp(boot_safe: false)
|
|
32
|
+
coherent_pair(
|
|
33
|
+
boot_safe,
|
|
34
|
+
[ :aws_ses_smtp_username, :user_name, "AWS_SES_SMTP_USERNAME", %i[aws smtp_username] ],
|
|
35
|
+
[ :aws_ses_smtp_password, :password, "AWS_SES_SMTP_PASSWORD", %i[aws smtp_password] ]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def coherent_pair(boot_safe, *entries)
|
|
40
|
+
[ db_pair(entries, boot_safe), env_pair(entries), credentials_pair(entries) ]
|
|
41
|
+
.find { |pair| pair.values.all?(&:present?) } || {}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def db_pair(entries, boot_safe)
|
|
45
|
+
entries.to_h { |entry| [ entry[1], read_db(entry[0], boot_safe: boot_safe) ] }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def env_pair(entries)
|
|
49
|
+
entries.to_h { |entry| [ entry[1], ENV[entry[2]].presence ] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def credentials_pair(entries)
|
|
53
|
+
entries.to_h do |entry|
|
|
54
|
+
path = entry[3]
|
|
55
|
+
value = path ? Rails.application.credentials.dig(*path).presence : nil
|
|
56
|
+
[ entry[1], value ]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def read_db(key, boot_safe: false)
|
|
61
|
+
return nil unless defined?(Collavre::IntegrationSetting)
|
|
62
|
+
Collavre::IntegrationSetting.find_by(key: key.to_s)&.value.presence
|
|
63
|
+
rescue ActiveRecord::StatementInvalid,
|
|
64
|
+
ActiveRecord::NoDatabaseError,
|
|
65
|
+
ActiveRecord::ConnectionNotEstablished,
|
|
66
|
+
NameError
|
|
67
|
+
nil
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
raise unless boot_safe &&
|
|
70
|
+
defined?(ActiveRecord::Encryption::Errors::Base) &&
|
|
71
|
+
e.is_a?(ActiveRecord::Encryption::Errors::Base)
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/collavre/engine.rb
CHANGED
|
@@ -16,6 +16,57 @@ module Collavre
|
|
|
16
16
|
root.join("app/assets/stylesheets")
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Register engine-internal integration settings keys with the central
|
|
20
|
+
# registry. These keys are consumed by engine code (mailer `from`, public
|
|
21
|
+
# assets helper, MCP upload service), so the engine itself must own their
|
|
22
|
+
# registration — host apps may not include the app-level
|
|
23
|
+
# `integration_settings_app.rb` initializer when mounting the engine as a gem.
|
|
24
|
+
# `register` is idempotent, so host app re-registration remains safe.
|
|
25
|
+
initializer "collavre.integration_settings_registry", before: :load_config_initializers do
|
|
26
|
+
if defined?(Collavre::IntegrationSettings::Registry)
|
|
27
|
+
registry = Collavre::IntegrationSettings::Registry.instance
|
|
28
|
+
registry.register(:default_mailer_from, category: "mail", sensitive: false, requires_restart: true)
|
|
29
|
+
registry.register(:public_assets_host, category: "mail", sensitive: false, requires_restart: false)
|
|
30
|
+
registry.register(:mcp_upload_root, category: "misc", sensitive: false, requires_restart: false)
|
|
31
|
+
# LLM keys consumed by Collavre::AiClient (engine service). Owned by the
|
|
32
|
+
# engine so gem-mounted host apps don't depend on the app-level
|
|
33
|
+
# `ruby_llm.rb` initializer for ENV fallback.
|
|
34
|
+
registry.register(:gemini_api_key, category: "llm", sensitive: true, requires_restart: true)
|
|
35
|
+
registry.register(:openai_api_key, category: "llm", sensitive: true, requires_restart: false)
|
|
36
|
+
registry.register(:anthropic_api_key, category: "llm", sensitive: true, requires_restart: false)
|
|
37
|
+
registry.register(:gemini_api_base, category: "llm", sensitive: false, requires_restart: true)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# AWS keys are consumed by `config/environments/*.rb` (active_storage.service
|
|
42
|
+
# decision, SMTP scaffold) and `config/storage.yml` ERB, both of which evaluate
|
|
43
|
+
# during the `:load_environment_config` initializer — earlier than
|
|
44
|
+
# `:load_config_initializers`. Register them in their own block so
|
|
45
|
+
# `IntegrationSettings.fetch` can resolve the keys at that earlier point.
|
|
46
|
+
# S3 keys are `requires_restart: true` because Rails resolves the storage
|
|
47
|
+
# service once at boot; SES keys are runtime-injected by SesSettingsInterceptor.
|
|
48
|
+
initializer "collavre.integration_settings_registry.aws", before: :load_environment_config do
|
|
49
|
+
if defined?(Collavre::IntegrationSettings::Registry)
|
|
50
|
+
registry = Collavre::IntegrationSettings::Registry.instance
|
|
51
|
+
registry.register(:aws_s3_access_key_id, category: "aws_s3", sensitive: true, requires_restart: true)
|
|
52
|
+
registry.register(:aws_s3_secret_access_key, category: "aws_s3", sensitive: true, requires_restart: true)
|
|
53
|
+
registry.register(:aws_s3_bucket, category: "aws_s3", sensitive: false, requires_restart: true)
|
|
54
|
+
registry.register(:aws_region, category: "aws_s3", sensitive: false, requires_restart: true)
|
|
55
|
+
registry.register(:aws_ses_smtp_username, category: "aws_ses", sensitive: true, requires_restart: false)
|
|
56
|
+
registry.register(:aws_ses_smtp_password, category: "aws_ses", sensitive: true, requires_restart: false)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Register the SES SMTP settings interceptor so each outgoing mail picks up
|
|
61
|
+
# the current DB > ENV > credentials value for SES creds at send time.
|
|
62
|
+
# Hooked after ActionMailer loads to ensure Mail::SMTP is defined.
|
|
63
|
+
initializer "collavre.ses_settings_interceptor" do
|
|
64
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
65
|
+
require "mail"
|
|
66
|
+
::Mail.register_interceptor(Collavre::SesSettingsInterceptor)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
19
70
|
# Add engine migrations to main app's migration path
|
|
20
71
|
# This allows migrations to live in the engine but be run from the host app
|
|
21
72
|
initializer "collavre.migrations" do |app|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module IntegrationSettings
|
|
5
|
+
# Value object describing a registered integration setting key.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [r] key
|
|
8
|
+
# @return [Symbol] canonical key identifier (e.g. :slack_client_id)
|
|
9
|
+
# @!attribute [r] category
|
|
10
|
+
# @return [String] grouping for the admin UI (e.g. "slack", "google_oauth")
|
|
11
|
+
# @!attribute [r] sensitive
|
|
12
|
+
# @return [Boolean] when true the value is masked in the admin UI
|
|
13
|
+
# @!attribute [r] requires_restart
|
|
14
|
+
# @return [Boolean] when true the admin UI surfaces a "restart required" badge
|
|
15
|
+
# @!attribute [r] env_var
|
|
16
|
+
# @return [String] name of the ENV variable used as fallback / seed source
|
|
17
|
+
# @!attribute [r] default
|
|
18
|
+
# @return [String, nil] static default returned when neither DB nor ENV has a value
|
|
19
|
+
KeyDefinition = Struct.new(
|
|
20
|
+
:key,
|
|
21
|
+
:category,
|
|
22
|
+
:sensitive,
|
|
23
|
+
:requires_restart,
|
|
24
|
+
:env_var,
|
|
25
|
+
:default,
|
|
26
|
+
keyword_init: true
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|