@cat-factory/app 0.97.1 → 0.99.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/environments/EnvironmentSetupWizard.vue +654 -0
- package/app/components/layout/SideBar.vue +14 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +29 -0
- package/app/components/settings/BudgetSettings.vue +294 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +3 -94
- package/app/composables/api/environments.ts +7 -1
- package/app/composables/api/preflights.ts +16 -0
- package/app/composables/api/userSettings.ts +12 -0
- package/app/composables/useApi.ts +4 -0
- package/app/pages/index.vue +4 -0
- package/app/stores/accounts.ts +12 -0
- package/app/stores/environmentWizard.ts +489 -0
- package/app/stores/preflights.ts +48 -0
- package/app/stores/ui.ts +21 -0
- package/app/stores/userSettings.ts +28 -0
- package/app/stores/workspace.ts +21 -2
- package/app/types/domain.ts +3 -0
- package/i18n/locales/en.json +98 -4
- package/i18n/locales/es.json +98 -4
- package/i18n/locales/fr.json +98 -4
- package/i18n/locales/he.json +98 -4
- package/i18n/locales/ja.json +98 -4
- package/i18n/locales/pl.json +98 -4
- package/i18n/locales/tr.json +98 -4
- package/i18n/locales/uk.json +98 -4
- package/package.json +2 -2
|
@@ -43,6 +43,7 @@ const github = useGitHubStore()
|
|
|
43
43
|
const services = useServicesStore()
|
|
44
44
|
const infra = useInfraConfigStore()
|
|
45
45
|
const agentRuns = useAgentRunsStore()
|
|
46
|
+
const ui = useUiStore()
|
|
46
47
|
const { t } = useI18n()
|
|
47
48
|
|
|
48
49
|
// The custom-manifest-type catalog feeds the `custom` picker. Cheap + shared (coalesced).
|
|
@@ -443,6 +444,34 @@ function setSize(value: InstanceSize) {
|
|
|
443
444
|
</p>
|
|
444
445
|
</div>
|
|
445
446
|
|
|
447
|
+
<!-- Nudge into the guided environment setup wizard for a docker-compose service: the wizard
|
|
448
|
+
drives detect → review (recipe + analyst draft) → preflight → save so the single Deployer
|
|
449
|
+
provisions the compose stack, rather than editing the raw path inline. -->
|
|
450
|
+
<div
|
|
451
|
+
v-if="provisionType === 'docker-compose'"
|
|
452
|
+
class="flex items-center justify-between gap-2 rounded border border-primary-800/40 bg-primary-950/20 p-2"
|
|
453
|
+
data-testid="env-setup-nudge"
|
|
454
|
+
>
|
|
455
|
+
<div class="min-w-0">
|
|
456
|
+
<p class="text-[11px] font-medium text-primary-200/90">
|
|
457
|
+
{{ t('inspector.testConfig.envWizard.title') }}
|
|
458
|
+
</p>
|
|
459
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
460
|
+
{{ t('inspector.testConfig.envWizard.hint') }}
|
|
461
|
+
</p>
|
|
462
|
+
</div>
|
|
463
|
+
<UButton
|
|
464
|
+
size="xs"
|
|
465
|
+
variant="soft"
|
|
466
|
+
color="primary"
|
|
467
|
+
icon="i-lucide-flask-conical"
|
|
468
|
+
data-testid="env-setup-nudge-open"
|
|
469
|
+
@click="ui.openEnvironmentSetup(props.block.id)"
|
|
470
|
+
>
|
|
471
|
+
{{ t('inspector.testConfig.envWizard.open') }}
|
|
472
|
+
</UButton>
|
|
473
|
+
</div>
|
|
474
|
+
|
|
446
475
|
<!-- Auto-detect a recommended provisioning config from the repo (slice 11). Non-binding:
|
|
447
476
|
it prefills the form below + the kube edit refs; the user confirms/edits everything. -->
|
|
448
477
|
<div v-if="repoContext" class="space-y-2 rounded border border-slate-800 bg-slate-900/40 p-2">
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The Budget configuration screen: the three spend-budget tiers (workspace, account,
|
|
3
|
+
// user). The workspace tier is a per-workspace monthly limit + currency; the account and
|
|
4
|
+
// user tiers are monthly ceilings that gate a run when EITHER is exhausted. When the
|
|
5
|
+
// operator sets a hard cap env var, the account/user input cannot exceed it and the cap is
|
|
6
|
+
// shown here. See docs/initiatives/tiered-budgets.md.
|
|
7
|
+
import { computed, reactive, ref, watch, type Ref } from 'vue'
|
|
8
|
+
|
|
9
|
+
const { t, n } = useI18n()
|
|
10
|
+
const toast = useToast()
|
|
11
|
+
|
|
12
|
+
const settingsStore = useWorkspaceSettingsStore()
|
|
13
|
+
const userSettingsStore = useUserSettingsStore()
|
|
14
|
+
const accounts = useAccountsStore()
|
|
15
|
+
const workspace = useWorkspaceStore()
|
|
16
|
+
|
|
17
|
+
const caps = computed(() => workspace.budgetCaps)
|
|
18
|
+
const capCurrency = computed(() => caps.value?.currency ?? 'EUR')
|
|
19
|
+
// Format an amount in a given currency. The account/user tiers are in the base pricing
|
|
20
|
+
// currency (`capCurrency`); the workspace tier is in its OWN overridden `spend.currency`, so
|
|
21
|
+
// its callers pass that in — otherwise a USD workspace on a EUR deployment renders `€` on USD.
|
|
22
|
+
const money = (value: number, currency: string = capCurrency.value) =>
|
|
23
|
+
n(value, { key: 'currency', currency })
|
|
24
|
+
|
|
25
|
+
// Persist one tier's budget: save, toast success, then best-effort refresh the snapshot AFTER
|
|
26
|
+
// the save has succeeded. A transient snapshot-refresh failure must NOT report a persisted
|
|
27
|
+
// budget as failed (the spend meter also catches up on the next pushed snapshot); a genuine
|
|
28
|
+
// save rejection surfaces its message so the user sees why (e.g. the operator hard-cap reject).
|
|
29
|
+
async function runSave(saving: Ref<boolean>, save: () => Promise<unknown>) {
|
|
30
|
+
saving.value = true
|
|
31
|
+
try {
|
|
32
|
+
await save()
|
|
33
|
+
toast.add({ title: t('settings.workspaceSettings.toast.budgetSaved'), color: 'success' })
|
|
34
|
+
try {
|
|
35
|
+
await useWorkspaceStore().refresh()
|
|
36
|
+
} catch {
|
|
37
|
+
// ignore — the budget is persisted; the meter will catch up on the next snapshot.
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
toast.add({
|
|
41
|
+
title: t('settings.workspaceSettings.toast.budgetSaveFailed'),
|
|
42
|
+
description: e instanceof Error ? e.message : String(e),
|
|
43
|
+
color: 'error',
|
|
44
|
+
})
|
|
45
|
+
} finally {
|
|
46
|
+
saving.value = false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---- Workspace tier -------------------------------------------------------
|
|
51
|
+
const wsDraft = reactive({ spendCurrency: '', spendMonthlyLimit: '' })
|
|
52
|
+
function hydrateWorkspace() {
|
|
53
|
+
const s = settingsStore.settings
|
|
54
|
+
wsDraft.spendCurrency = s.spendCurrency ?? ''
|
|
55
|
+
wsDraft.spendMonthlyLimit = s.spendMonthlyLimit == null ? '' : String(s.spendMonthlyLimit)
|
|
56
|
+
}
|
|
57
|
+
watch(() => settingsStore.settings, hydrateWorkspace, { immediate: true })
|
|
58
|
+
|
|
59
|
+
const savingWorkspace = ref(false)
|
|
60
|
+
function saveWorkspace() {
|
|
61
|
+
const raw = String(wsDraft.spendMonthlyLimit ?? '').trim()
|
|
62
|
+
return runSave(savingWorkspace, () =>
|
|
63
|
+
settingsStore.update({
|
|
64
|
+
spendCurrency: wsDraft.spendCurrency.trim()
|
|
65
|
+
? wsDraft.spendCurrency.trim().toUpperCase()
|
|
66
|
+
: null,
|
|
67
|
+
spendMonthlyLimit: raw === '' ? null : Number(raw),
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- Account tier ---------------------------------------------------------
|
|
73
|
+
const account = computed(() => accounts.activeAccount)
|
|
74
|
+
const canEditAccount = computed(() => account.value?.roles?.includes('admin') ?? false)
|
|
75
|
+
const accountCap = computed(() => caps.value?.accountMonthlyLimitMax ?? null)
|
|
76
|
+
const accountDraft = ref('')
|
|
77
|
+
watch(
|
|
78
|
+
account,
|
|
79
|
+
(a) => {
|
|
80
|
+
accountDraft.value = a?.spendMonthlyLimit == null ? '' : String(a.spendMonthlyLimit)
|
|
81
|
+
},
|
|
82
|
+
{ immediate: true },
|
|
83
|
+
)
|
|
84
|
+
const accountOverCap = computed(
|
|
85
|
+
() =>
|
|
86
|
+
accountCap.value != null &&
|
|
87
|
+
accountDraft.value.trim() !== '' &&
|
|
88
|
+
Number(accountDraft.value) > accountCap.value,
|
|
89
|
+
)
|
|
90
|
+
const savingAccount = ref(false)
|
|
91
|
+
function saveAccount() {
|
|
92
|
+
const acc = account.value
|
|
93
|
+
if (!acc || accountOverCap.value) return
|
|
94
|
+
const raw = accountDraft.value.trim()
|
|
95
|
+
return runSave(savingAccount, () =>
|
|
96
|
+
accounts.setSpendMonthlyLimit(acc.id, raw === '' ? null : Number(raw)),
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---- User tier ------------------------------------------------------------
|
|
101
|
+
const userCap = computed(() => caps.value?.userMonthlyLimitMax ?? null)
|
|
102
|
+
const userDraft = ref('')
|
|
103
|
+
watch(
|
|
104
|
+
() => userSettingsStore.settings,
|
|
105
|
+
(s) => {
|
|
106
|
+
userDraft.value = s.spendMonthlyLimit == null ? '' : String(s.spendMonthlyLimit)
|
|
107
|
+
},
|
|
108
|
+
{ immediate: true },
|
|
109
|
+
)
|
|
110
|
+
const userOverCap = computed(
|
|
111
|
+
() =>
|
|
112
|
+
userCap.value != null &&
|
|
113
|
+
userDraft.value.trim() !== '' &&
|
|
114
|
+
Number(userDraft.value) > userCap.value,
|
|
115
|
+
)
|
|
116
|
+
const savingUser = ref(false)
|
|
117
|
+
function saveUser() {
|
|
118
|
+
if (userOverCap.value) return
|
|
119
|
+
const raw = userDraft.value.trim()
|
|
120
|
+
return runSave(savingUser, () =>
|
|
121
|
+
userSettingsStore.update({ spendMonthlyLimit: raw === '' ? null : Number(raw) }),
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<template>
|
|
127
|
+
<div class="space-y-8">
|
|
128
|
+
<p class="text-[11px] text-slate-400">
|
|
129
|
+
{{ t('settings.workspaceSettings.budget.body') }}
|
|
130
|
+
</p>
|
|
131
|
+
|
|
132
|
+
<!-- Workspace tier -->
|
|
133
|
+
<section class="space-y-2">
|
|
134
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
135
|
+
{{ t('settings.workspaceSettings.budget.workspace') }}
|
|
136
|
+
</h3>
|
|
137
|
+
<div class="grid grid-cols-2 gap-3">
|
|
138
|
+
<label class="block">
|
|
139
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
140
|
+
{{ t('settings.workspaceSettings.budget.monthlyLimit') }}
|
|
141
|
+
</span>
|
|
142
|
+
<UInput
|
|
143
|
+
v-model="wsDraft.spendMonthlyLimit"
|
|
144
|
+
type="number"
|
|
145
|
+
:min="0"
|
|
146
|
+
:placeholder="t('settings.workspaceSettings.budget.defaultPlaceholder')"
|
|
147
|
+
size="sm"
|
|
148
|
+
/>
|
|
149
|
+
</label>
|
|
150
|
+
<label class="block">
|
|
151
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
152
|
+
{{ t('settings.workspaceSettings.budget.currency') }}
|
|
153
|
+
</span>
|
|
154
|
+
<UInput
|
|
155
|
+
v-model="wsDraft.spendCurrency"
|
|
156
|
+
placeholder="EUR"
|
|
157
|
+
maxlength="3"
|
|
158
|
+
size="sm"
|
|
159
|
+
class="uppercase"
|
|
160
|
+
/>
|
|
161
|
+
</label>
|
|
162
|
+
</div>
|
|
163
|
+
<div v-if="workspace.spend" class="text-[11px] text-slate-400">
|
|
164
|
+
{{
|
|
165
|
+
t('settings.workspaceSettings.budget.spent', {
|
|
166
|
+
spent: money(workspace.spend.costSpent, workspace.spend.currency),
|
|
167
|
+
limit: money(workspace.spend.costLimit, workspace.spend.currency),
|
|
168
|
+
})
|
|
169
|
+
}}
|
|
170
|
+
</div>
|
|
171
|
+
<div class="flex justify-end">
|
|
172
|
+
<UButton
|
|
173
|
+
color="primary"
|
|
174
|
+
icon="i-lucide-save"
|
|
175
|
+
size="sm"
|
|
176
|
+
:loading="savingWorkspace"
|
|
177
|
+
@click="saveWorkspace"
|
|
178
|
+
>
|
|
179
|
+
{{ t('settings.workspaceSettings.budget.saveTier') }}
|
|
180
|
+
</UButton>
|
|
181
|
+
</div>
|
|
182
|
+
</section>
|
|
183
|
+
|
|
184
|
+
<!-- Account tier -->
|
|
185
|
+
<section v-if="account" class="space-y-2">
|
|
186
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
187
|
+
{{ t('settings.workspaceSettings.budget.account') }}
|
|
188
|
+
</h3>
|
|
189
|
+
<p class="text-[11px] text-slate-400">
|
|
190
|
+
{{ t('settings.workspaceSettings.budget.accountBody') }}
|
|
191
|
+
</p>
|
|
192
|
+
<label class="block">
|
|
193
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
194
|
+
{{ t('settings.workspaceSettings.budget.monthlyLimit') }}
|
|
195
|
+
</span>
|
|
196
|
+
<UInput
|
|
197
|
+
v-model="accountDraft"
|
|
198
|
+
type="number"
|
|
199
|
+
:min="0"
|
|
200
|
+
:max="accountCap ?? undefined"
|
|
201
|
+
:disabled="!canEditAccount"
|
|
202
|
+
:placeholder="t('settings.workspaceSettings.budget.noLimitPlaceholder')"
|
|
203
|
+
size="sm"
|
|
204
|
+
/>
|
|
205
|
+
</label>
|
|
206
|
+
<p
|
|
207
|
+
v-if="accountCap != null"
|
|
208
|
+
class="text-[11px]"
|
|
209
|
+
:class="accountOverCap ? 'text-rose-400' : 'text-amber-400'"
|
|
210
|
+
>
|
|
211
|
+
{{ t('settings.workspaceSettings.budget.hardCap', { amount: money(accountCap) }) }}
|
|
212
|
+
<span class="text-slate-500"
|
|
213
|
+
>({{ t('settings.workspaceSettings.budget.hardCapHint') }})</span
|
|
214
|
+
>
|
|
215
|
+
</p>
|
|
216
|
+
<div v-if="workspace.accountSpend" class="text-[11px] text-slate-400">
|
|
217
|
+
{{
|
|
218
|
+
t('settings.workspaceSettings.budget.spent', {
|
|
219
|
+
spent: money(workspace.accountSpend.costSpent),
|
|
220
|
+
limit: money(workspace.accountSpend.costLimit),
|
|
221
|
+
})
|
|
222
|
+
}}
|
|
223
|
+
</div>
|
|
224
|
+
<p v-if="!canEditAccount" class="text-[11px] text-slate-500">
|
|
225
|
+
{{ t('settings.workspaceSettings.budget.adminOnly') }}
|
|
226
|
+
</p>
|
|
227
|
+
<div v-if="canEditAccount" class="flex justify-end">
|
|
228
|
+
<UButton
|
|
229
|
+
color="primary"
|
|
230
|
+
icon="i-lucide-save"
|
|
231
|
+
size="sm"
|
|
232
|
+
:loading="savingAccount"
|
|
233
|
+
:disabled="accountOverCap"
|
|
234
|
+
@click="saveAccount"
|
|
235
|
+
>
|
|
236
|
+
{{ t('settings.workspaceSettings.budget.saveTier') }}
|
|
237
|
+
</UButton>
|
|
238
|
+
</div>
|
|
239
|
+
</section>
|
|
240
|
+
|
|
241
|
+
<!-- User tier -->
|
|
242
|
+
<section class="space-y-2">
|
|
243
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
244
|
+
{{ t('settings.workspaceSettings.budget.user') }}
|
|
245
|
+
</h3>
|
|
246
|
+
<p class="text-[11px] text-slate-400">
|
|
247
|
+
{{ t('settings.workspaceSettings.budget.userBody') }}
|
|
248
|
+
</p>
|
|
249
|
+
<label class="block">
|
|
250
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
251
|
+
{{ t('settings.workspaceSettings.budget.monthlyLimit') }}
|
|
252
|
+
</span>
|
|
253
|
+
<UInput
|
|
254
|
+
v-model="userDraft"
|
|
255
|
+
type="number"
|
|
256
|
+
:min="0"
|
|
257
|
+
:max="userCap ?? undefined"
|
|
258
|
+
:placeholder="t('settings.workspaceSettings.budget.noLimitPlaceholder')"
|
|
259
|
+
size="sm"
|
|
260
|
+
/>
|
|
261
|
+
</label>
|
|
262
|
+
<p
|
|
263
|
+
v-if="userCap != null"
|
|
264
|
+
class="text-[11px]"
|
|
265
|
+
:class="userOverCap ? 'text-rose-400' : 'text-amber-400'"
|
|
266
|
+
>
|
|
267
|
+
{{ t('settings.workspaceSettings.budget.hardCap', { amount: money(userCap) }) }}
|
|
268
|
+
<span class="text-slate-500"
|
|
269
|
+
>({{ t('settings.workspaceSettings.budget.hardCapHint') }})</span
|
|
270
|
+
>
|
|
271
|
+
</p>
|
|
272
|
+
<div v-if="workspace.userSpend" class="text-[11px] text-slate-400">
|
|
273
|
+
{{
|
|
274
|
+
t('settings.workspaceSettings.budget.spent', {
|
|
275
|
+
spent: money(workspace.userSpend.costSpent),
|
|
276
|
+
limit: money(workspace.userSpend.costLimit),
|
|
277
|
+
})
|
|
278
|
+
}}
|
|
279
|
+
</div>
|
|
280
|
+
<div class="flex justify-end">
|
|
281
|
+
<UButton
|
|
282
|
+
color="primary"
|
|
283
|
+
icon="i-lucide-save"
|
|
284
|
+
size="sm"
|
|
285
|
+
:loading="savingUser"
|
|
286
|
+
:disabled="userOverCap"
|
|
287
|
+
@click="saveUser"
|
|
288
|
+
>
|
|
289
|
+
{{ t('settings.workspaceSettings.budget.saveTier') }}
|
|
290
|
+
</UButton>
|
|
291
|
+
</div>
|
|
292
|
+
</section>
|
|
293
|
+
</div>
|
|
294
|
+
</template>
|
|
@@ -12,6 +12,7 @@ import type { CreateTaskType, TaskLimitMode } from '~/types/domain'
|
|
|
12
12
|
import MergeThresholdsPanel from '~/components/settings/MergeThresholdsPanel.vue'
|
|
13
13
|
import IssueTrackerPanel from '~/components/settings/IssueTrackerPanel.vue'
|
|
14
14
|
import ServiceFragmentDefaultsPanel from '~/components/settings/ServiceFragmentDefaultsPanel.vue'
|
|
15
|
+
import BudgetSettings from '~/components/settings/BudgetSettings.vue'
|
|
15
16
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
16
17
|
|
|
17
18
|
const { t, te } = useI18n()
|
|
@@ -114,9 +115,6 @@ const draft = reactive({
|
|
|
114
115
|
storeAgentContext: true,
|
|
115
116
|
artifactRetentionDays: 14,
|
|
116
117
|
kaizenEnabled: true,
|
|
117
|
-
// Budget: empty string ⇒ "use the built-in default" (null on the wire).
|
|
118
|
-
spendCurrency: '',
|
|
119
|
-
spendMonthlyLimit: '',
|
|
120
118
|
})
|
|
121
119
|
|
|
122
120
|
function hydrate() {
|
|
@@ -129,8 +127,6 @@ function hydrate() {
|
|
|
129
127
|
draft.storeAgentContext = s.storeAgentContext
|
|
130
128
|
draft.artifactRetentionDays = s.artifactRetentionDays
|
|
131
129
|
draft.kaizenEnabled = s.kaizenEnabled
|
|
132
|
-
draft.spendCurrency = s.spendCurrency ?? ''
|
|
133
|
-
draft.spendMonthlyLimit = s.spendMonthlyLimit == null ? '' : String(s.spendMonthlyLimit)
|
|
134
130
|
}
|
|
135
131
|
|
|
136
132
|
// `store.settings` is always replaced wholesale (store hydrate/update reassign the ref),
|
|
@@ -176,46 +172,6 @@ async function save() {
|
|
|
176
172
|
saving.value = false
|
|
177
173
|
}
|
|
178
174
|
}
|
|
179
|
-
|
|
180
|
-
const savingBudget = ref(false)
|
|
181
|
-
|
|
182
|
-
async function saveBudget() {
|
|
183
|
-
savingBudget.value = true
|
|
184
|
-
// The number input emits a raw number once edited but starts as a string from hydrate, so
|
|
185
|
-
// coerce through String() before trimming. Blank ⇒ "use the built-in default" (null on the wire).
|
|
186
|
-
const raw = String(draft.spendMonthlyLimit ?? '').trim()
|
|
187
|
-
const monthlyLimit = raw === '' ? null : Number(raw)
|
|
188
|
-
try {
|
|
189
|
-
await store.update({
|
|
190
|
-
spendCurrency: draft.spendCurrency.trim() ? draft.spendCurrency.trim().toUpperCase() : null,
|
|
191
|
-
spendMonthlyLimit: monthlyLimit,
|
|
192
|
-
})
|
|
193
|
-
toast.add({
|
|
194
|
-
title: t('settings.workspaceSettings.toast.budgetSaved'),
|
|
195
|
-
icon: 'i-lucide-check',
|
|
196
|
-
color: 'success',
|
|
197
|
-
})
|
|
198
|
-
// The settings PUT only returns the settings; re-fetch the snapshot so the toolbar's
|
|
199
|
-
// spend meter reflects the newly-set limit/currency (spendService.status) right away.
|
|
200
|
-
// Best-effort and AFTER the save succeeded: a transient snapshot-refresh failure must
|
|
201
|
-
// not report a successfully-saved budget as failed (the meter also catches up on the
|
|
202
|
-
// next snapshot pushed over the stream).
|
|
203
|
-
try {
|
|
204
|
-
await useWorkspaceStore().refresh()
|
|
205
|
-
} catch {
|
|
206
|
-
// ignore — the budget is persisted; the meter will catch up on the next snapshot.
|
|
207
|
-
}
|
|
208
|
-
} catch (e) {
|
|
209
|
-
toast.add({
|
|
210
|
-
title: t('settings.workspaceSettings.toast.budgetSaveFailed'),
|
|
211
|
-
description: e instanceof Error ? e.message : String(e),
|
|
212
|
-
icon: 'i-lucide-triangle-alert',
|
|
213
|
-
color: 'error',
|
|
214
|
-
})
|
|
215
|
-
} finally {
|
|
216
|
-
savingBudget.value = false
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
175
|
</script>
|
|
220
176
|
|
|
221
177
|
<template>
|
|
@@ -370,56 +326,9 @@ async function saveBudget() {
|
|
|
370
326
|
</div>
|
|
371
327
|
</template>
|
|
372
328
|
|
|
373
|
-
<!-- Budget -->
|
|
329
|
+
<!-- Budget (workspace / account / user tiers) -->
|
|
374
330
|
<template #budget>
|
|
375
|
-
<
|
|
376
|
-
<section class="space-y-2">
|
|
377
|
-
<h3 class="text-sm font-semibold text-slate-200">
|
|
378
|
-
{{ t('settings.workspaceSettings.budget.heading') }}
|
|
379
|
-
</h3>
|
|
380
|
-
<p class="text-[11px] text-slate-400">
|
|
381
|
-
{{ t('settings.workspaceSettings.budget.body') }}
|
|
382
|
-
</p>
|
|
383
|
-
<div class="grid grid-cols-2 gap-3">
|
|
384
|
-
<label class="block">
|
|
385
|
-
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
386
|
-
{{ t('settings.workspaceSettings.budget.monthlyLimit') }}
|
|
387
|
-
</span>
|
|
388
|
-
<UInput
|
|
389
|
-
v-model="draft.spendMonthlyLimit"
|
|
390
|
-
type="number"
|
|
391
|
-
:min="0"
|
|
392
|
-
:placeholder="t('settings.workspaceSettings.budget.defaultPlaceholder')"
|
|
393
|
-
size="sm"
|
|
394
|
-
/>
|
|
395
|
-
</label>
|
|
396
|
-
<label class="block">
|
|
397
|
-
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
398
|
-
{{ t('settings.workspaceSettings.budget.currency') }}
|
|
399
|
-
</span>
|
|
400
|
-
<UInput
|
|
401
|
-
v-model="draft.spendCurrency"
|
|
402
|
-
placeholder="EUR"
|
|
403
|
-
maxlength="3"
|
|
404
|
-
size="sm"
|
|
405
|
-
class="uppercase"
|
|
406
|
-
/>
|
|
407
|
-
</label>
|
|
408
|
-
</div>
|
|
409
|
-
</section>
|
|
410
|
-
|
|
411
|
-
<div class="flex justify-end">
|
|
412
|
-
<UButton
|
|
413
|
-
color="primary"
|
|
414
|
-
icon="i-lucide-save"
|
|
415
|
-
size="sm"
|
|
416
|
-
:loading="savingBudget"
|
|
417
|
-
@click="saveBudget"
|
|
418
|
-
>
|
|
419
|
-
{{ t('settings.workspaceSettings.budget.save') }}
|
|
420
|
-
</UButton>
|
|
421
|
-
</div>
|
|
422
|
-
</div>
|
|
331
|
+
<BudgetSettings />
|
|
423
332
|
</template>
|
|
424
333
|
|
|
425
334
|
<!-- Merge thresholds -->
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { listEnvironmentsContract } from '@cat-factory/contracts'
|
|
1
|
+
import { listEnvironmentsContract, provisionEnvironmentContract } from '@cat-factory/contracts'
|
|
2
|
+
import type { ProvisionEnvironmentInput } from '@cat-factory/contracts'
|
|
2
3
|
import type { ApiContext } from './context'
|
|
3
4
|
|
|
4
5
|
/** Ephemeral environments: the workspace's live env handles (used to resolve frontend bindings). */
|
|
@@ -6,5 +7,10 @@ export function environmentsApi({ send, ws }: ApiContext) {
|
|
|
6
7
|
return {
|
|
7
8
|
listEnvironments: (workspaceId: string) =>
|
|
8
9
|
send(listEnvironmentsContract, { pathPrefix: ws(workspaceId) }),
|
|
10
|
+
|
|
11
|
+
// Manually provision an environment for a service frame (outside a pipeline run) — the setup
|
|
12
|
+
// wizard's "trial provision" against the just-saved config. Returns the resulting handle.
|
|
13
|
+
provisionEnvironment: (workspaceId: string, body: ProvisionEnvironmentInput) =>
|
|
14
|
+
send(provisionEnvironmentContract, { pathPrefix: ws(workspaceId), body }),
|
|
9
15
|
}
|
|
10
16
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { runPreflightsContract } from '@cat-factory/contracts'
|
|
2
|
+
import type { PreflightRef } from '@cat-factory/contracts'
|
|
3
|
+
import type { ApiContext } from './context'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Preflight checks (machine-prerequisite probes for a stack recipe): run a set of refs and get one
|
|
7
|
+
* verdict each (pass / fail / warn + detail + remediation). Used by the environment setup wizard's
|
|
8
|
+
* checklist + live re-check. The probes run only on the local (host) facade; the endpoint 503s
|
|
9
|
+
* elsewhere. See PreflightController in @cat-factory/server.
|
|
10
|
+
*/
|
|
11
|
+
export function preflightsApi({ send, ws }: ApiContext) {
|
|
12
|
+
return {
|
|
13
|
+
runPreflights: (workspaceId: string, prerequisites: PreflightRef[]) =>
|
|
14
|
+
send(runPreflightsContract, { pathPrefix: ws(workspaceId), body: { prerequisites } }),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getUserSettingsContract, updateUserSettingsContract } from '@cat-factory/contracts'
|
|
2
|
+
import type { UpdateUserSettingsInput } from '~/types/domain'
|
|
3
|
+
import type { ApiContext } from './context'
|
|
4
|
+
|
|
5
|
+
/** Per-user settings (the user-tier spend budget), scoped to the signed-in user. */
|
|
6
|
+
export function userSettingsApi({ send }: ApiContext) {
|
|
7
|
+
return {
|
|
8
|
+
getUserSettings: () => send(getUserSettingsContract, {}),
|
|
9
|
+
updateUserSettings: (body: UpdateUserSettingsInput) =>
|
|
10
|
+
send(updateUserSettingsContract, { body }),
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -21,6 +21,7 @@ import { localSettingsApi } from './api/localSettings'
|
|
|
21
21
|
import { modelsApi } from './api/models'
|
|
22
22
|
import { notificationsApi } from './api/notifications'
|
|
23
23
|
import { packageRegistriesApi } from './api/packageRegistries'
|
|
24
|
+
import { preflightsApi } from './api/preflights'
|
|
24
25
|
import { presetsApi } from './api/presets'
|
|
25
26
|
import { sharedStacksApi } from './api/sharedStacks'
|
|
26
27
|
import { providerConnectionsApi } from './api/providerConnections'
|
|
@@ -35,6 +36,7 @@ import { slackApi } from './api/slack'
|
|
|
35
36
|
import { specApi } from './api/spec'
|
|
36
37
|
import { tasksApi } from './api/tasks'
|
|
37
38
|
import { userSecretsApi } from './api/userSecrets'
|
|
39
|
+
import { userSettingsApi } from './api/userSettings'
|
|
38
40
|
import { workspacesApi } from './api/workspaces'
|
|
39
41
|
|
|
40
42
|
/**
|
|
@@ -113,6 +115,7 @@ export function useApi() {
|
|
|
113
115
|
...specApi(ctx),
|
|
114
116
|
...notificationsApi(ctx),
|
|
115
117
|
...presetsApi(ctx),
|
|
118
|
+
...preflightsApi(ctx),
|
|
116
119
|
...sharedStacksApi(ctx),
|
|
117
120
|
...providerConnectionsApi(ctx),
|
|
118
121
|
...infraHandlersApi(ctx),
|
|
@@ -129,5 +132,6 @@ export function useApi() {
|
|
|
129
132
|
...slackApi(ctx),
|
|
130
133
|
...bootstrapApi(ctx),
|
|
131
134
|
...userSecretsApi(ctx),
|
|
135
|
+
...userSettingsApi(ctx),
|
|
132
136
|
}
|
|
133
137
|
}
|
package/app/pages/index.vue
CHANGED
|
@@ -95,6 +95,9 @@ const PackageRegistriesPanel = defineAsyncComponent(
|
|
|
95
95
|
const InfrastructureWindow = defineAsyncComponent(
|
|
96
96
|
() => import('~/components/settings/InfrastructureWindow.vue'),
|
|
97
97
|
)
|
|
98
|
+
const EnvironmentSetupWizard = defineAsyncComponent(
|
|
99
|
+
() => import('~/components/environments/EnvironmentSetupWizard.vue'),
|
|
100
|
+
)
|
|
98
101
|
const ModelConfigurationPanel = defineAsyncComponent(
|
|
99
102
|
() => import('~/components/settings/ModelConfigurationPanel.vue'),
|
|
100
103
|
)
|
|
@@ -373,6 +376,7 @@ watch(
|
|
|
373
376
|
<ObservabilityConnectionPanel v-if="ui.observabilityConnectionOpen" />
|
|
374
377
|
<PackageRegistriesPanel v-if="ui.packageRegistriesOpen" />
|
|
375
378
|
<InfrastructureWindow v-if="ui.infrastructureOpen" />
|
|
379
|
+
<EnvironmentSetupWizard v-if="ui.environmentWizardOpen" />
|
|
376
380
|
<ModelConfigurationPanel v-if="ui.modelConfigOpen" />
|
|
377
381
|
<LocalModelEndpointsPanel v-if="ui.localModelsOpen" />
|
|
378
382
|
<SandboxPanel v-if="ui.sandboxOpen" />
|
package/app/stores/accounts.ts
CHANGED
|
@@ -67,6 +67,17 @@ export const useAccountsStore = defineStore(
|
|
|
67
67
|
return updated
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Set an account's monthly spend budget (the account tier). Admin-only on the
|
|
72
|
+
* backend; `null` clears the limit. Patches the loaded account in place on success.
|
|
73
|
+
*/
|
|
74
|
+
async function setSpendMonthlyLimit(id: string, limit: number | null) {
|
|
75
|
+
const updated = await api.updateAccount(id, { spendMonthlyLimit: limit })
|
|
76
|
+
const i = accounts.value.findIndex((a) => a.id === id)
|
|
77
|
+
if (i >= 0) accounts.value[i] = updated
|
|
78
|
+
return updated
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
// ---- members + invitations -------------------------------------------
|
|
71
82
|
|
|
72
83
|
const members = ref<AccountMember[]>([])
|
|
@@ -139,6 +150,7 @@ export const useAccountsStore = defineStore(
|
|
|
139
150
|
createOrg,
|
|
140
151
|
switchTo,
|
|
141
152
|
setDefaultCloudProvider,
|
|
153
|
+
setSpendMonthlyLimit,
|
|
142
154
|
loadRoster,
|
|
143
155
|
invite,
|
|
144
156
|
revokeInvite,
|