@cat-factory/app 0.46.12 → 0.47.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.
- package/app/components/auth/AuthGate.vue +1 -1
- package/app/components/auth/LoginScreen.vue +131 -1
- package/app/components/layout/AccountDeploymentSettings.vue +232 -5
- package/app/components/providers/ApiKeysSection.vue +27 -3
- package/app/components/providers/PersonalSubscriptionSection.vue +17 -2
- package/app/components/providers/SignInRequiredNotice.vue +19 -0
- package/app/components/settings/AccountSettingsPanel.vue +9 -6
- package/app/components/settings/IssueTrackerPanel.vue +114 -53
- package/app/components/settings/LocalModeSettingsPanel.vue +60 -28
- package/app/components/settings/LocalModelEndpointsPanel.vue +72 -27
- package/app/components/settings/MergeThresholdsPanel.vue +93 -45
- package/app/components/settings/ModelConfigurationPanel.vue +62 -36
- package/app/components/settings/ObservabilityConnectionPanel.vue +65 -35
- package/app/components/settings/OpenRouterCatalogPanel.vue +70 -40
- package/app/components/settings/ProviderConnectionPanel.vue +115 -61
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +9 -12
- package/app/components/settings/UserSecretsSection.vue +37 -18
- package/app/components/settings/WorkspaceSettingsPanel.vue +114 -62
- package/app/composables/api/auth.ts +7 -0
- package/app/stores/auth.ts +25 -1
- package/app/types/accountSettings.ts +4 -0
- package/i18n/locales/en.json +496 -0
- package/i18n/locales/es.json +490 -0
- package/i18n/locales/fr.json +490 -0
- package/i18n/locales/pl.json +490 -0
- package/i18n/locales/uk.json +490 -0
- package/package.json +2 -2
|
@@ -8,6 +8,7 @@ import { computed, ref, watch } from 'vue'
|
|
|
8
8
|
import type { ProviderConfigField, UserSecretKind } from '~/types/userSecrets'
|
|
9
9
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
10
10
|
|
|
11
|
+
const { t } = useI18n()
|
|
11
12
|
const ui = useUiStore()
|
|
12
13
|
const store = useUserSecretsStore()
|
|
13
14
|
const toast = useToast()
|
|
@@ -102,12 +103,14 @@ async function save() {
|
|
|
102
103
|
values.value[secretField.value!.key] = ''
|
|
103
104
|
testResult.value = null
|
|
104
105
|
toast.add({
|
|
105
|
-
title:
|
|
106
|
+
title: t('settings.userSecrets.toast.saved', {
|
|
107
|
+
label: descriptor.value?.label ?? t('settings.userSecrets.secretFallback'),
|
|
108
|
+
}),
|
|
106
109
|
icon: 'i-lucide-check',
|
|
107
110
|
color: 'success',
|
|
108
111
|
})
|
|
109
112
|
} catch (e) {
|
|
110
|
-
notifyError('
|
|
113
|
+
notifyError(t('settings.userSecrets.toast.saveFailed'), e)
|
|
111
114
|
} finally {
|
|
112
115
|
busy.value = false
|
|
113
116
|
}
|
|
@@ -118,9 +121,9 @@ async function remove() {
|
|
|
118
121
|
try {
|
|
119
122
|
await store.remove(kind.value)
|
|
120
123
|
resetDraft()
|
|
121
|
-
toast.add({ title: '
|
|
124
|
+
toast.add({ title: t('settings.userSecrets.toast.removed'), icon: 'i-lucide-check' })
|
|
122
125
|
} catch (e) {
|
|
123
|
-
notifyError('
|
|
126
|
+
notifyError(t('settings.userSecrets.toast.removeFailed'), e)
|
|
124
127
|
} finally {
|
|
125
128
|
busy.value = false
|
|
126
129
|
}
|
|
@@ -128,17 +131,25 @@ async function remove() {
|
|
|
128
131
|
</script>
|
|
129
132
|
|
|
130
133
|
<template>
|
|
131
|
-
<UModal
|
|
134
|
+
<UModal
|
|
135
|
+
v-model:open="open"
|
|
136
|
+
:title="t('settings.userSecrets.title')"
|
|
137
|
+
:ui="{ content: 'max-w-xl' }"
|
|
138
|
+
>
|
|
132
139
|
<template #title>
|
|
133
|
-
<IntegrationBackTitle title="
|
|
140
|
+
<IntegrationBackTitle :title="t('settings.userSecrets.title')" @back="back" />
|
|
134
141
|
</template>
|
|
135
142
|
<template #body>
|
|
136
143
|
<div class="space-y-4">
|
|
137
144
|
<p class="text-xs text-slate-400">
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
<i18n-t keypath="settings.userSecrets.intro" tag="span" scope="global">
|
|
146
|
+
<template #you>
|
|
147
|
+
<strong>{{ t('settings.userSecrets.introYou') }}</strong>
|
|
148
|
+
</template>
|
|
149
|
+
<template #scope>
|
|
150
|
+
<span class="text-slate-300">{{ t('settings.userSecrets.introScope') }}</span>
|
|
151
|
+
</template>
|
|
152
|
+
</i18n-t>
|
|
142
153
|
</p>
|
|
143
154
|
|
|
144
155
|
<div
|
|
@@ -147,7 +158,9 @@ async function remove() {
|
|
|
147
158
|
>
|
|
148
159
|
<div>
|
|
149
160
|
<span class="font-medium text-slate-200">{{ status.label }}</span>
|
|
150
|
-
<div class="text-[11px] text-emerald-400">
|
|
161
|
+
<div class="text-[11px] text-emerald-400">
|
|
162
|
+
{{ t('settings.userSecrets.connectedStored') }}
|
|
163
|
+
</div>
|
|
151
164
|
</div>
|
|
152
165
|
<UButton
|
|
153
166
|
icon="i-lucide-trash-2"
|
|
@@ -164,17 +177,23 @@ async function remove() {
|
|
|
164
177
|
class="rounded-lg border border-dashed border-slate-700 p-3 space-y-3"
|
|
165
178
|
>
|
|
166
179
|
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
167
|
-
{{
|
|
180
|
+
{{
|
|
181
|
+
status ? t('settings.userSecrets.replaceToken') : t('settings.userSecrets.addToken')
|
|
182
|
+
}}
|
|
168
183
|
</p>
|
|
169
184
|
|
|
170
|
-
<UFormField label="
|
|
185
|
+
<UFormField :label="t('settings.userSecrets.labelField')">
|
|
171
186
|
<UInput v-model="labelDraft" :placeholder="descriptor.label" />
|
|
172
187
|
</UFormField>
|
|
173
188
|
|
|
174
189
|
<UFormField
|
|
175
190
|
v-for="field in descriptor.configFields"
|
|
176
191
|
:key="field.key"
|
|
177
|
-
:label="
|
|
192
|
+
:label="
|
|
193
|
+
field.required
|
|
194
|
+
? field.label
|
|
195
|
+
: t('settings.userSecrets.optionalField', { label: field.label })
|
|
196
|
+
"
|
|
178
197
|
:help="field.help"
|
|
179
198
|
>
|
|
180
199
|
<UInput
|
|
@@ -195,13 +214,13 @@ async function remove() {
|
|
|
195
214
|
:disabled="!buildPayload()"
|
|
196
215
|
@click="test()"
|
|
197
216
|
>
|
|
198
|
-
|
|
217
|
+
{{ t('settings.userSecrets.testConnection') }}
|
|
199
218
|
</UButton>
|
|
200
219
|
<span v-if="testResult && testResult.ok" class="text-xs text-emerald-400">
|
|
201
|
-
{{ testResult.message ?? '
|
|
220
|
+
{{ testResult.message ?? t('settings.userSecrets.tokenValid') }}
|
|
202
221
|
</span>
|
|
203
222
|
<span v-else-if="testResult" class="text-xs text-rose-400">
|
|
204
|
-
{{ testResult.message ?? '
|
|
223
|
+
{{ testResult.message ?? t('settings.userSecrets.tokenRejected') }}
|
|
205
224
|
</span>
|
|
206
225
|
</div>
|
|
207
226
|
|
|
@@ -213,7 +232,7 @@ async function remove() {
|
|
|
213
232
|
:disabled="!buildPayload()"
|
|
214
233
|
@click="save()"
|
|
215
234
|
>
|
|
216
|
-
{{ status ? '
|
|
235
|
+
{{ status ? t('common.save') : t('settings.userSecrets.addToken') }}
|
|
217
236
|
</UButton>
|
|
218
237
|
</div>
|
|
219
238
|
</div>
|
|
@@ -14,6 +14,7 @@ import IssueTrackerPanel from '~/components/settings/IssueTrackerPanel.vue'
|
|
|
14
14
|
import ServiceFragmentDefaultsPanel from '~/components/settings/ServiceFragmentDefaultsPanel.vue'
|
|
15
15
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
16
16
|
|
|
17
|
+
const { t, te } = useI18n()
|
|
17
18
|
const ui = useUiStore()
|
|
18
19
|
const store = useWorkspaceSettingsStore()
|
|
19
20
|
const toast = useToast()
|
|
@@ -31,35 +32,63 @@ const activeTab = computed({
|
|
|
31
32
|
set: (v: string) => ui.setWorkspaceSettingsTab(v),
|
|
32
33
|
})
|
|
33
34
|
|
|
34
|
-
const tabs = [
|
|
35
|
+
const tabs = computed(() => [
|
|
35
36
|
{
|
|
36
37
|
value: 'workspace',
|
|
37
|
-
label: '
|
|
38
|
+
label: t('settings.workspaceSettings.tabs.workspace'),
|
|
38
39
|
icon: 'i-lucide-sliders-horizontal',
|
|
39
40
|
slot: 'workspace',
|
|
40
41
|
},
|
|
41
|
-
{
|
|
42
|
-
|
|
42
|
+
{
|
|
43
|
+
value: 'budget',
|
|
44
|
+
label: t('settings.workspaceSettings.tabs.budget'),
|
|
45
|
+
icon: 'i-lucide-wallet',
|
|
46
|
+
slot: 'budget',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
value: 'merge',
|
|
50
|
+
label: t('settings.workspaceSettings.tabs.merge'),
|
|
51
|
+
icon: 'i-lucide-git-merge',
|
|
52
|
+
slot: 'merge',
|
|
53
|
+
},
|
|
43
54
|
{
|
|
44
55
|
value: 'tracker',
|
|
45
|
-
label: '
|
|
56
|
+
label: t('settings.workspaceSettings.tabs.tracker'),
|
|
46
57
|
icon: 'i-lucide-list-checks',
|
|
47
58
|
slot: 'tracker',
|
|
48
59
|
},
|
|
49
60
|
{
|
|
50
61
|
value: 'fragments',
|
|
51
|
-
label: '
|
|
62
|
+
label: t('settings.workspaceSettings.tabs.fragments'),
|
|
52
63
|
icon: 'i-lucide-book-open-check',
|
|
53
64
|
slot: 'fragments',
|
|
54
65
|
},
|
|
55
|
-
]
|
|
66
|
+
])
|
|
56
67
|
|
|
57
68
|
const TASK_TYPES: CreateTaskType[] = ['feature', 'bug', 'document', 'spike']
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
|
|
70
|
+
// Per-task-type label for the "Max {type} tasks" inputs. An exhaustive Record keyed off
|
|
71
|
+
// the CreateTaskType union (a missing member fails the typecheck); each value is a LITERAL
|
|
72
|
+
// catalog key so the typed-message-keys check sees it. Leaf keys mirror the enum verbatim.
|
|
73
|
+
const TASK_TYPE_KEYS: Record<CreateTaskType, string> = {
|
|
74
|
+
feature: 'settings.workspaceSettings.taskTypes.feature',
|
|
75
|
+
bug: 'settings.workspaceSettings.taskTypes.bug',
|
|
76
|
+
document: 'settings.workspaceSettings.taskTypes.document',
|
|
77
|
+
spike: 'settings.workspaceSettings.taskTypes.spike',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const MODES = computed<{ value: TaskLimitMode; label: string }[]>(() => [
|
|
81
|
+
{ value: 'off', label: t('settings.workspaceSettings.taskLimit.modes.off') },
|
|
82
|
+
{ value: 'shared', label: t('settings.workspaceSettings.taskLimit.modes.shared') },
|
|
83
|
+
{ value: 'per_type', label: t('settings.workspaceSettings.taskLimit.modes.per_type') },
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
/** The localized "Max {type} tasks" label for a per-type running-task limit input. */
|
|
87
|
+
function maxTaskTypeLabel(type: CreateTaskType): string {
|
|
88
|
+
const key = TASK_TYPE_KEYS[type]
|
|
89
|
+
const typeLabel = te(key) ? t(key) : type
|
|
90
|
+
return t('settings.workspaceSettings.taskLimit.maxPerType', { type: typeLabel })
|
|
91
|
+
}
|
|
63
92
|
|
|
64
93
|
// Local editable copy, kept in sync with the store's settings.
|
|
65
94
|
const draft = reactive({
|
|
@@ -116,10 +145,14 @@ async function save() {
|
|
|
116
145
|
artifactRetentionDays: draft.artifactRetentionDays,
|
|
117
146
|
kaizenEnabled: draft.kaizenEnabled,
|
|
118
147
|
})
|
|
119
|
-
toast.add({
|
|
148
|
+
toast.add({
|
|
149
|
+
title: t('settings.workspaceSettings.toast.saved'),
|
|
150
|
+
icon: 'i-lucide-check',
|
|
151
|
+
color: 'success',
|
|
152
|
+
})
|
|
120
153
|
} catch (e) {
|
|
121
154
|
toast.add({
|
|
122
|
-
title: '
|
|
155
|
+
title: t('settings.workspaceSettings.toast.saveFailed'),
|
|
123
156
|
description: e instanceof Error ? e.message : String(e),
|
|
124
157
|
icon: 'i-lucide-triangle-alert',
|
|
125
158
|
color: 'error',
|
|
@@ -142,10 +175,14 @@ async function saveBudget() {
|
|
|
142
175
|
spendCurrency: draft.spendCurrency.trim() ? draft.spendCurrency.trim().toUpperCase() : null,
|
|
143
176
|
spendMonthlyLimit: monthlyLimit,
|
|
144
177
|
})
|
|
145
|
-
toast.add({
|
|
178
|
+
toast.add({
|
|
179
|
+
title: t('settings.workspaceSettings.toast.budgetSaved'),
|
|
180
|
+
icon: 'i-lucide-check',
|
|
181
|
+
color: 'success',
|
|
182
|
+
})
|
|
146
183
|
} catch (e) {
|
|
147
184
|
toast.add({
|
|
148
|
-
title: '
|
|
185
|
+
title: t('settings.workspaceSettings.toast.budgetSaveFailed'),
|
|
149
186
|
description: e instanceof Error ? e.message : String(e),
|
|
150
187
|
icon: 'i-lucide-triangle-alert',
|
|
151
188
|
color: 'error',
|
|
@@ -157,9 +194,13 @@ async function saveBudget() {
|
|
|
157
194
|
</script>
|
|
158
195
|
|
|
159
196
|
<template>
|
|
160
|
-
<UModal
|
|
197
|
+
<UModal
|
|
198
|
+
v-model:open="open"
|
|
199
|
+
:title="t('settings.workspaceSettings.title')"
|
|
200
|
+
:ui="{ content: 'max-w-3xl' }"
|
|
201
|
+
>
|
|
161
202
|
<template #title>
|
|
162
|
-
<IntegrationBackTitle title="
|
|
203
|
+
<IntegrationBackTitle :title="t('settings.workspaceSettings.title')" @back="back" />
|
|
163
204
|
</template>
|
|
164
205
|
<template #body>
|
|
165
206
|
<UTabs
|
|
@@ -173,15 +214,21 @@ async function saveBudget() {
|
|
|
173
214
|
<div class="space-y-6">
|
|
174
215
|
<!-- Run-timing escalation -->
|
|
175
216
|
<section class="space-y-2">
|
|
176
|
-
<h3 class="text-sm font-semibold text-slate-200">
|
|
217
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
218
|
+
{{ t('settings.workspaceSettings.waiting.heading') }}
|
|
219
|
+
</h3>
|
|
177
220
|
<p class="text-[11px] text-slate-400">
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
221
|
+
<i18n-t keypath="settings.workspaceSettings.waiting.body" tag="span" scope="global">
|
|
222
|
+
<template #overdue>
|
|
223
|
+
<span class="text-error-400">{{
|
|
224
|
+
t('settings.workspaceSettings.waiting.overdue')
|
|
225
|
+
}}</span>
|
|
226
|
+
</template>
|
|
227
|
+
</i18n-t>
|
|
181
228
|
</p>
|
|
182
229
|
<label class="block w-48">
|
|
183
230
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
184
|
-
|
|
231
|
+
{{ t('settings.workspaceSettings.waiting.escalateAfter') }}
|
|
185
232
|
</span>
|
|
186
233
|
<UInput
|
|
187
234
|
v-model.number="draft.waitingEscalationMinutes"
|
|
@@ -194,15 +241,16 @@ async function saveBudget() {
|
|
|
194
241
|
|
|
195
242
|
<!-- Per-service running-task limit -->
|
|
196
243
|
<section class="space-y-2">
|
|
197
|
-
<h3 class="text-sm font-semibold text-slate-200">
|
|
244
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
245
|
+
{{ t('settings.workspaceSettings.taskLimit.heading') }}
|
|
246
|
+
</h3>
|
|
198
247
|
<p class="text-[11px] text-slate-400">
|
|
199
|
-
|
|
200
|
-
is refused with a clear message until a running task finishes.
|
|
248
|
+
{{ t('settings.workspaceSettings.taskLimit.body') }}
|
|
201
249
|
</p>
|
|
202
250
|
<label class="block w-64">
|
|
203
|
-
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
|
|
204
|
-
|
|
205
|
-
>
|
|
251
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">{{
|
|
252
|
+
t('settings.workspaceSettings.taskLimit.mode')
|
|
253
|
+
}}</span>
|
|
206
254
|
<USelect
|
|
207
255
|
v-model="draft.taskLimitMode"
|
|
208
256
|
:items="MODES"
|
|
@@ -214,48 +262,53 @@ async function saveBudget() {
|
|
|
214
262
|
|
|
215
263
|
<label v-if="draft.taskLimitMode === 'shared'" class="block w-48">
|
|
216
264
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
217
|
-
|
|
265
|
+
{{ t('settings.workspaceSettings.taskLimit.maxRunning') }}
|
|
218
266
|
</span>
|
|
219
267
|
<UInput v-model.number="draft.taskLimitShared" type="number" :min="1" size="sm" />
|
|
220
268
|
</label>
|
|
221
269
|
|
|
222
270
|
<div v-else-if="draft.taskLimitMode === 'per_type'" class="grid grid-cols-2 gap-3">
|
|
223
|
-
<label v-for="
|
|
271
|
+
<label v-for="taskType in TASK_TYPES" :key="taskType" class="block">
|
|
224
272
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
225
|
-
|
|
273
|
+
{{ maxTaskTypeLabel(taskType) }}
|
|
226
274
|
</span>
|
|
227
|
-
<UInput
|
|
275
|
+
<UInput
|
|
276
|
+
v-model.number="draft.perType[taskType]"
|
|
277
|
+
type="number"
|
|
278
|
+
:min="1"
|
|
279
|
+
size="sm"
|
|
280
|
+
/>
|
|
228
281
|
</label>
|
|
229
282
|
</div>
|
|
230
283
|
</section>
|
|
231
284
|
|
|
232
285
|
<!-- Agent observability -->
|
|
233
286
|
<section class="space-y-2">
|
|
234
|
-
<h3 class="text-sm font-semibold text-slate-200">
|
|
287
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
288
|
+
{{ t('settings.workspaceSettings.observability.heading') }}
|
|
289
|
+
</h3>
|
|
235
290
|
<p class="text-[11px] text-slate-400">
|
|
236
|
-
|
|
237
|
-
best-practice fragments folded in, and the full content of the files injected into
|
|
238
|
-
its container — so it can be inspected later in the observability view. The bodies
|
|
239
|
-
are kept for the same window as the per-call LLM telemetry. Turn off to stop storing
|
|
240
|
-
it (existing snapshots are pruned by retention).
|
|
291
|
+
{{ t('settings.workspaceSettings.observability.body') }}
|
|
241
292
|
</p>
|
|
242
293
|
<label class="flex items-center gap-2">
|
|
243
294
|
<USwitch v-model="draft.storeAgentContext" size="sm" />
|
|
244
|
-
<span class="text-sm text-slate-200">
|
|
295
|
+
<span class="text-sm text-slate-200">{{
|
|
296
|
+
t('settings.workspaceSettings.observability.toggle')
|
|
297
|
+
}}</span>
|
|
245
298
|
</label>
|
|
246
299
|
</section>
|
|
247
300
|
|
|
248
301
|
<!-- Visual-confirmation artifact retention -->
|
|
249
302
|
<section class="space-y-2">
|
|
250
|
-
<h3 class="text-sm font-semibold text-slate-200">
|
|
303
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
304
|
+
{{ t('settings.workspaceSettings.retention.heading') }}
|
|
305
|
+
</h3>
|
|
251
306
|
<p class="text-[11px] text-slate-400">
|
|
252
|
-
|
|
253
|
-
images they’re reviewed against (the visual-confirmation gate). A daily cleanup job
|
|
254
|
-
deletes both the image bytes and their metadata once they age past this window.
|
|
307
|
+
{{ t('settings.workspaceSettings.retention.body') }}
|
|
255
308
|
</p>
|
|
256
309
|
<label class="block w-48">
|
|
257
310
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
258
|
-
|
|
311
|
+
{{ t('settings.workspaceSettings.retention.days') }}
|
|
259
312
|
</span>
|
|
260
313
|
<UInput
|
|
261
314
|
v-model.number="draft.artifactRetentionDays"
|
|
@@ -269,18 +322,17 @@ async function saveBudget() {
|
|
|
269
322
|
|
|
270
323
|
<!-- Kaizen agent -->
|
|
271
324
|
<section class="space-y-2">
|
|
272
|
-
<h3 class="text-sm font-semibold text-slate-200">
|
|
325
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
326
|
+
{{ t('settings.workspaceSettings.kaizen.heading') }}
|
|
327
|
+
</h3>
|
|
273
328
|
<p class="text-[11px] text-slate-400">
|
|
274
|
-
|
|
275
|
-
and efficient vs confused and chaotic — and recommends prompt/model improvements. A
|
|
276
|
-
prompt + agent + model combination that grades highly with no recommendations five
|
|
277
|
-
times in a row is marked verified and is no longer graded. Grading runs in the
|
|
278
|
-
background and is shown inside run details and the Kaizen screen. Set the grader's
|
|
279
|
-
model in Model Configuration (the “Kaizen” agent).
|
|
329
|
+
{{ t('settings.workspaceSettings.kaizen.body') }}
|
|
280
330
|
</p>
|
|
281
331
|
<label class="flex items-center gap-2">
|
|
282
332
|
<USwitch v-model="draft.kaizenEnabled" size="sm" />
|
|
283
|
-
<span class="text-sm text-slate-200">
|
|
333
|
+
<span class="text-sm text-slate-200">{{
|
|
334
|
+
t('settings.workspaceSettings.kaizen.toggle')
|
|
335
|
+
}}</span>
|
|
284
336
|
</label>
|
|
285
337
|
</section>
|
|
286
338
|
|
|
@@ -292,7 +344,7 @@ async function saveBudget() {
|
|
|
292
344
|
:loading="saving"
|
|
293
345
|
@click="save"
|
|
294
346
|
>
|
|
295
|
-
|
|
347
|
+
{{ t('common.save') }}
|
|
296
348
|
</UButton>
|
|
297
349
|
</div>
|
|
298
350
|
</div>
|
|
@@ -302,28 +354,28 @@ async function saveBudget() {
|
|
|
302
354
|
<template #budget>
|
|
303
355
|
<div class="space-y-6">
|
|
304
356
|
<section class="space-y-2">
|
|
305
|
-
<h3 class="text-sm font-semibold text-slate-200">
|
|
357
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
358
|
+
{{ t('settings.workspaceSettings.budget.heading') }}
|
|
359
|
+
</h3>
|
|
306
360
|
<p class="text-[11px] text-slate-400">
|
|
307
|
-
|
|
308
|
-
reached, runs in this workspace pause and the board shows a warning. Leave blank to
|
|
309
|
-
inherit the built-in default (~100 EUR/month).
|
|
361
|
+
{{ t('settings.workspaceSettings.budget.body') }}
|
|
310
362
|
</p>
|
|
311
363
|
<div class="grid grid-cols-2 gap-3">
|
|
312
364
|
<label class="block">
|
|
313
365
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
314
|
-
|
|
366
|
+
{{ t('settings.workspaceSettings.budget.monthlyLimit') }}
|
|
315
367
|
</span>
|
|
316
368
|
<UInput
|
|
317
369
|
v-model="draft.spendMonthlyLimit"
|
|
318
370
|
type="number"
|
|
319
371
|
:min="0"
|
|
320
|
-
placeholder="
|
|
372
|
+
:placeholder="t('settings.workspaceSettings.budget.defaultPlaceholder')"
|
|
321
373
|
size="sm"
|
|
322
374
|
/>
|
|
323
375
|
</label>
|
|
324
376
|
<label class="block">
|
|
325
377
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
326
|
-
|
|
378
|
+
{{ t('settings.workspaceSettings.budget.currency') }}
|
|
327
379
|
</span>
|
|
328
380
|
<UInput
|
|
329
381
|
v-model="draft.spendCurrency"
|
|
@@ -344,7 +396,7 @@ async function saveBudget() {
|
|
|
344
396
|
:loading="savingBudget"
|
|
345
397
|
@click="saveBudget"
|
|
346
398
|
>
|
|
347
|
-
|
|
399
|
+
{{ t('settings.workspaceSettings.budget.save') }}
|
|
348
400
|
</UButton>
|
|
349
401
|
</div>
|
|
350
402
|
</div>
|
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
logoutContract,
|
|
6
6
|
meContract,
|
|
7
7
|
passwordLoginContract,
|
|
8
|
+
patLoginContract,
|
|
8
9
|
peekInvitationContract,
|
|
9
10
|
resetPasswordContract,
|
|
10
11
|
signupContract,
|
|
11
12
|
} from '@cat-factory/contracts'
|
|
13
|
+
import type { VcsProviderWire } from '@cat-factory/contracts'
|
|
12
14
|
import type { ApiContext } from './context'
|
|
13
15
|
|
|
14
16
|
/** Auth/session endpoints + the events-WebSocket ticket mint. */
|
|
@@ -27,6 +29,11 @@ export function authApi({ http, send, ws }: ApiContext) {
|
|
|
27
29
|
passwordLogin: (body: { email: string; password: string }) =>
|
|
28
30
|
send(passwordLoginContract, { pathPrefix: '/auth', body }),
|
|
29
31
|
|
|
32
|
+
// Local mode only: log in as the account a source-control PAT belongs to. `token`
|
|
33
|
+
// omitted ⇒ use the server-configured PAT (one-click); present ⇒ a pasted token.
|
|
34
|
+
patLogin: (body: { provider: VcsProviderWire; token?: string }) =>
|
|
35
|
+
send(patLoginContract, { pathPrefix: '/auth', body }),
|
|
36
|
+
|
|
30
37
|
// Request a reset link. Always succeeds (204) regardless of whether the email is
|
|
31
38
|
// registered, so the response can't be used to enumerate accounts.
|
|
32
39
|
forgotPassword: (body: { email: string }) =>
|
package/app/stores/auth.ts
CHANGED
|
@@ -37,6 +37,15 @@ export const useAuthStore = defineStore(
|
|
|
37
37
|
/** May the app render? True when auth is off, or on with a known user. */
|
|
38
38
|
const isAuthenticated = computed(() => !required.value || user.value !== null)
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Whether the SPA must show the login screen before the board. Auth-enabled deployments
|
|
42
|
+
* gate on a user as before; local mode ALSO gates (even though its API stays dev-open),
|
|
43
|
+
* because anonymous local use can't store per-user credentials — see the login flow.
|
|
44
|
+
*/
|
|
45
|
+
const needsLogin = computed(
|
|
46
|
+
() => (required.value || localMode.value?.enabled === true) && user.value === null,
|
|
47
|
+
)
|
|
48
|
+
|
|
40
49
|
/** Pull a token handed back in the post-login URL fragment (#token=…). */
|
|
41
50
|
function consumeRedirectToken() {
|
|
42
51
|
if (typeof window === 'undefined') return
|
|
@@ -62,7 +71,11 @@ export const useAuthStore = defineStore(
|
|
|
62
71
|
return
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
|
|
74
|
+
// Resolve a stored session into a user whenever sign-in applies — auth-enabled
|
|
75
|
+
// deployments, OR local mode (which mints real sessions via PAT/password even though
|
|
76
|
+
// its API otherwise runs dev-open). Without this, a local session wouldn't survive a
|
|
77
|
+
// reload.
|
|
78
|
+
if ((required.value || localMode.value?.enabled === true) && token.value) {
|
|
66
79
|
try {
|
|
67
80
|
user.value = (await api.getMe()).user
|
|
68
81
|
} catch {
|
|
@@ -133,6 +146,15 @@ export const useAuthStore = defineStore(
|
|
|
133
146
|
applySession(await api.passwordLogin(body))
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Local mode: sign in as the account a source-control PAT belongs to. `token` omitted
|
|
151
|
+
* uses the server-configured PAT (one-click); otherwise a pasted token. Resolves to the
|
|
152
|
+
* SAME canonical user as GitHub OAuth would (keyed on the provider's numeric id).
|
|
153
|
+
*/
|
|
154
|
+
async function patLogin(body: { provider: 'github' | 'gitlab'; token?: string }) {
|
|
155
|
+
applySession(await api.patLogin(body))
|
|
156
|
+
}
|
|
157
|
+
|
|
136
158
|
/** Request a password-reset link by email (always resolves; never reveals existence). */
|
|
137
159
|
async function forgotPassword(email: string) {
|
|
138
160
|
await api.forgotPassword({ email })
|
|
@@ -164,11 +186,13 @@ export const useAuthStore = defineStore(
|
|
|
164
186
|
localMode,
|
|
165
187
|
ready,
|
|
166
188
|
isAuthenticated,
|
|
189
|
+
needsLogin,
|
|
167
190
|
bootstrap,
|
|
168
191
|
login,
|
|
169
192
|
loginWithGoogle,
|
|
170
193
|
signup,
|
|
171
194
|
passwordLogin,
|
|
195
|
+
patLogin,
|
|
172
196
|
forgotPassword,
|
|
173
197
|
resetPassword,
|
|
174
198
|
logout,
|