@cat-factory/app 0.26.7 → 0.28.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.
@@ -0,0 +1,236 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, reactive, ref } from 'vue'
3
+
4
+ // Deployment integration secrets for an account (admin only): the Slack app OAuth
5
+ // credentials and the container web-search upstream keys — both moved out of env into
6
+ // the per-account settings store, sealed at rest. Secrets are write-only: the panel only
7
+ // ever shows whether each integration is configured (the `summary`), never the values;
8
+ // blank inputs leave a configured secret unchanged. Hidden when the settings store isn't
9
+ // wired (no ENCRYPTION_KEY).
10
+ const props = defineProps<{ accountId: string }>()
11
+
12
+ const store = useAccountSettingsStore()
13
+ const toast = useToast()
14
+
15
+ const slack = reactive({ clientId: '', clientSecret: '', redirectUrl: '' })
16
+ const web = reactive({ braveApiKey: '', searxngUrl: '', searxngApiKey: '' })
17
+ const savingSlack = ref(false)
18
+ const savingWeb = ref(false)
19
+
20
+ const summary = computed(() => store.view?.summary ?? null)
21
+
22
+ onMounted(async () => {
23
+ try {
24
+ await store.load(props.accountId)
25
+ } catch (e) {
26
+ toast.add({
27
+ title: 'Could not load deployment settings',
28
+ description: e instanceof Error ? e.message : String(e),
29
+ icon: 'i-lucide-triangle-alert',
30
+ color: 'error',
31
+ })
32
+ }
33
+ })
34
+
35
+ async function saveSlack() {
36
+ if (!slack.clientId.trim() || !slack.clientSecret.trim() || !slack.redirectUrl.trim()) {
37
+ toast.add({ title: 'Enter the client id, secret and redirect URL', color: 'error' })
38
+ return
39
+ }
40
+ savingSlack.value = true
41
+ try {
42
+ await store.save(props.accountId, {
43
+ secrets: {
44
+ slackOAuth: {
45
+ clientId: slack.clientId.trim(),
46
+ clientSecret: slack.clientSecret.trim(),
47
+ redirectUrl: slack.redirectUrl.trim(),
48
+ },
49
+ },
50
+ })
51
+ slack.clientId = ''
52
+ slack.clientSecret = ''
53
+ slack.redirectUrl = ''
54
+ toast.add({ title: 'Slack OAuth saved', icon: 'i-lucide-check', color: 'success' })
55
+ } catch (e) {
56
+ toast.add({
57
+ title: 'Could not save Slack OAuth',
58
+ description: e instanceof Error ? e.message : String(e),
59
+ color: 'error',
60
+ })
61
+ } finally {
62
+ savingSlack.value = false
63
+ }
64
+ }
65
+
66
+ async function clearSlack() {
67
+ savingSlack.value = true
68
+ try {
69
+ await store.save(props.accountId, { secrets: { slackOAuth: null } })
70
+ toast.add({ title: 'Slack OAuth cleared', icon: 'i-lucide-check', color: 'success' })
71
+ } catch (e) {
72
+ toast.add({
73
+ title: 'Could not clear Slack OAuth',
74
+ description: e instanceof Error ? e.message : String(e),
75
+ color: 'error',
76
+ })
77
+ } finally {
78
+ savingSlack.value = false
79
+ }
80
+ }
81
+
82
+ async function saveWeb() {
83
+ const brave = web.braveApiKey.trim()
84
+ const searxng = web.searxngUrl.trim()
85
+ if (!brave && !searxng) {
86
+ toast.add({ title: 'Enter a Brave key or a SearXNG URL', color: 'error' })
87
+ return
88
+ }
89
+ savingWeb.value = true
90
+ try {
91
+ await store.save(props.accountId, {
92
+ secrets: {
93
+ webSearch: {
94
+ ...(brave ? { braveApiKey: brave } : {}),
95
+ ...(searxng ? { searxngUrl: searxng } : {}),
96
+ ...(web.searxngApiKey.trim() ? { searxngApiKey: web.searxngApiKey.trim() } : {}),
97
+ },
98
+ },
99
+ })
100
+ web.braveApiKey = ''
101
+ web.searxngUrl = ''
102
+ web.searxngApiKey = ''
103
+ toast.add({ title: 'Web search keys saved', icon: 'i-lucide-check', color: 'success' })
104
+ } catch (e) {
105
+ toast.add({
106
+ title: 'Could not save web search keys',
107
+ description: e instanceof Error ? e.message : String(e),
108
+ color: 'error',
109
+ })
110
+ } finally {
111
+ savingWeb.value = false
112
+ }
113
+ }
114
+
115
+ async function clearWeb() {
116
+ savingWeb.value = true
117
+ try {
118
+ await store.save(props.accountId, { secrets: { webSearch: null } })
119
+ toast.add({ title: 'Web search keys cleared', icon: 'i-lucide-check', color: 'success' })
120
+ } catch (e) {
121
+ toast.add({
122
+ title: 'Could not clear web search keys',
123
+ description: e instanceof Error ? e.message : String(e),
124
+ color: 'error',
125
+ })
126
+ } finally {
127
+ savingWeb.value = false
128
+ }
129
+ }
130
+ </script>
131
+
132
+ <template>
133
+ <div v-if="store.available !== false" class="space-y-6">
134
+ <div>
135
+ <h3 class="mb-1 font-semibold text-white">Deployment integrations</h3>
136
+ <p class="text-[11px] text-slate-400">
137
+ Credentials shared by every workspace in this account, sealed at rest. Values are never
138
+ shown after saving; leave a field blank to keep the stored secret.
139
+ </p>
140
+ </div>
141
+
142
+ <!-- Slack app OAuth -->
143
+ <section class="space-y-2">
144
+ <div class="flex items-center gap-2">
145
+ <h4 class="text-sm font-semibold text-slate-200">Slack app (OAuth)</h4>
146
+ <UBadge
147
+ :color="summary?.slackOAuthConfigured ? 'success' : 'neutral'"
148
+ variant="subtle"
149
+ size="xs"
150
+ >
151
+ {{ summary?.slackOAuthConfigured ? 'Configured' : 'Not set' }}
152
+ </UBadge>
153
+ </div>
154
+ <p class="text-[11px] text-slate-400">
155
+ Enables the "Add to Slack" OAuth flow. Without it, workspaces can still connect Slack by
156
+ pasting a bot token.
157
+ </p>
158
+ <div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
159
+ <UInput v-model="slack.clientId" placeholder="Client ID" size="sm" />
160
+ <UInput
161
+ v-model="slack.clientSecret"
162
+ type="password"
163
+ placeholder="Client secret"
164
+ size="sm"
165
+ />
166
+ <UInput v-model="slack.redirectUrl" placeholder="Redirect URL" size="sm" />
167
+ </div>
168
+ <div class="flex gap-2">
169
+ <UButton
170
+ color="primary"
171
+ size="xs"
172
+ icon="i-lucide-save"
173
+ :loading="savingSlack"
174
+ @click="saveSlack"
175
+ >
176
+ Save
177
+ </UButton>
178
+ <UButton
179
+ v-if="summary?.slackOAuthConfigured"
180
+ color="neutral"
181
+ variant="ghost"
182
+ size="xs"
183
+ :loading="savingSlack"
184
+ @click="clearSlack"
185
+ >
186
+ Clear
187
+ </UButton>
188
+ </div>
189
+ </section>
190
+
191
+ <!-- Web search keys -->
192
+ <section class="space-y-2 border-t border-slate-800 pt-6">
193
+ <div class="flex items-center gap-2">
194
+ <h4 class="text-sm font-semibold text-slate-200">Container web search</h4>
195
+ <UBadge :color="summary?.webSearch ? 'success' : 'neutral'" variant="subtle" size="xs">
196
+ {{ summary?.webSearch ? `Configured (${summary.webSearch})` : 'Not set' }}
197
+ </UBadge>
198
+ </div>
199
+ <p class="text-[11px] text-slate-400">
200
+ The search upstream container agents reach through the backend proxy. Set a Brave key
201
+ (recommended), or a self-hosted SearXNG URL (with an optional bearer key).
202
+ </p>
203
+ <div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
204
+ <UInput v-model="web.braveApiKey" type="password" placeholder="Brave API key" size="sm" />
205
+ <UInput v-model="web.searxngUrl" placeholder="SearXNG URL" size="sm" />
206
+ <UInput
207
+ v-model="web.searxngApiKey"
208
+ type="password"
209
+ placeholder="SearXNG key (optional)"
210
+ size="sm"
211
+ />
212
+ </div>
213
+ <div class="flex gap-2">
214
+ <UButton
215
+ color="primary"
216
+ size="xs"
217
+ icon="i-lucide-save"
218
+ :loading="savingWeb"
219
+ @click="saveWeb"
220
+ >
221
+ Save
222
+ </UButton>
223
+ <UButton
224
+ v-if="summary?.webSearch"
225
+ color="neutral"
226
+ variant="ghost"
227
+ size="xs"
228
+ :loading="savingWeb"
229
+ @click="clearWeb"
230
+ >
231
+ Clear
232
+ </UButton>
233
+ </div>
234
+ </section>
235
+ </div>
236
+ </template>
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, onMounted, ref } from 'vue'
3
3
  import type { AccountRole } from '~/types/domain'
4
+ import AccountDeploymentSettings from '~/components/layout/AccountDeploymentSettings.vue'
4
5
 
5
6
  // Team settings for an org account: the member roster (with combinable admin /
6
7
  // developer / product roles), pending email invitations, and the per-account
@@ -236,5 +237,10 @@ async function disconnectEmail() {
236
237
  <ProvidersApiKeysSection :account-id="accountId" category="proxy" />
237
238
  </div>
238
239
  </section>
240
+
241
+ <!-- deployment integration secrets (admin-only): Slack OAuth app + web-search keys -->
242
+ <section v-if="isAdmin">
243
+ <AccountDeploymentSettings :account-id="accountId" />
244
+ </section>
239
245
  </div>
240
246
  </template>
@@ -181,6 +181,14 @@ const commands = computed<Command[]>(() => {
181
181
  keywords: 'local model runner ollama lm studio llamacpp vllm endpoint',
182
182
  run: () => ui.openLocalModels(),
183
183
  })
184
+ list.push({
185
+ id: 'sandbox',
186
+ label: 'Open Sandbox',
187
+ group: 'Workspace',
188
+ icon: 'i-lucide-flask-conical',
189
+ keywords: 'sandbox prompt model test experiment judge fixture benchmark evaluate',
190
+ run: () => ui.openSandbox(),
191
+ })
184
192
 
185
193
  return list
186
194
  })
@@ -125,6 +125,19 @@ watch(
125
125
  >
126
126
  Integrations
127
127
  </UButton>
128
+ <!-- The Sandbox: try prompt versions/models against graded fixtures, off to the
129
+ side of the board. Opens the on-demand testing window. -->
130
+ <UButton
131
+ block
132
+ color="primary"
133
+ variant="soft"
134
+ size="sm"
135
+ icon="i-lucide-flask-conical"
136
+ class="justify-start"
137
+ @click="ui.openSandbox()"
138
+ >
139
+ Sandbox
140
+ </UButton>
128
141
  </div>
129
142
  </section>
130
143