@cat-factory/app 0.109.4 → 0.110.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/board/AddTaskModal.vue +15 -15
- package/app/components/panels/inspector/TaskRunSettings.vue +12 -12
- package/app/components/pipeline/PipelineBuilder.vue +22 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +300 -203
- package/app/components/settings/{MergePresetHealthModal.vue → RiskPolicyHealthModal.vue} +19 -19
- package/app/components/settings/{MergeThresholdsPanel.vue → RiskPolicyPanel.vue} +46 -46
- package/app/components/settings/UsageSettings.vue +142 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +14 -2
- package/app/composables/api/board.ts +1 -1
- package/app/composables/api/execution.ts +5 -0
- package/app/composables/api/presets.ts +18 -18
- package/app/composables/api/reviews.ts +7 -6
- package/app/composables/{useMergePresetHealth.ts → useRiskPolicyHealth.ts} +10 -10
- package/app/pages/index.vue +9 -9
- package/app/stores/board.ts +2 -2
- package/app/stores/pipelines.ts +41 -1
- package/app/stores/requirements.ts +9 -13
- package/app/stores/{mergePresets.ts → riskPolicies.ts} +13 -13
- package/app/stores/ui.ts +17 -17
- package/app/stores/usage.ts +60 -0
- package/app/stores/workspace.spec.ts +1 -1
- package/app/stores/workspace.ts +4 -4
- package/app/types/merge.ts +3 -3
- package/app/types/requirements.ts +1 -0
- package/app/utils/{mergePreset.ts → riskPolicy.ts} +4 -4
- package/i18n/locales/de.json +52 -27
- package/i18n/locales/en.json +52 -27
- package/i18n/locales/es.json +52 -27
- package/i18n/locales/fr.json +53 -28
- package/i18n/locales/he.json +53 -28
- package/i18n/locales/it.json +53 -28
- package/i18n/locales/ja.json +52 -27
- package/i18n/locales/pl.json +53 -28
- package/i18n/locales/tr.json +52 -27
- package/i18n/locales/uk.json +53 -28
- package/package.json +2 -2
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// Startup advisory for built-in merge presets that drifted from the catalog. Opened once per
|
|
3
|
-
// session from the board page when `
|
|
3
|
+
// session from the board page when `useRiskPolicyHealth` reports any issue. Lists:
|
|
4
4
|
// • new built-in presets the workspace doesn't have yet (ADD them);
|
|
5
5
|
// • built-ins with a newer catalog version available (RESEED to adopt it).
|
|
6
6
|
// Both fixes are the same reseed call (it creates or updates by catalog id). Detection is
|
|
7
|
-
// client-side (see
|
|
7
|
+
// client-side (see useRiskPolicyHealth); the actions hit the riskPolicies store.
|
|
8
8
|
const { t } = useI18n()
|
|
9
9
|
const ui = useUiStore()
|
|
10
|
-
const presets =
|
|
11
|
-
const { newPresets, outdated, hasIssues } =
|
|
10
|
+
const presets = useRiskPoliciesStore()
|
|
11
|
+
const { newPresets, outdated, hasIssues } = useRiskPolicyHealth()
|
|
12
12
|
const toast = useToast()
|
|
13
13
|
|
|
14
14
|
const open = computed({
|
|
15
|
-
get: () => ui.
|
|
15
|
+
get: () => ui.riskPolicyHealthOpen,
|
|
16
16
|
set: (v: boolean) => {
|
|
17
|
-
if (!v) ui.
|
|
17
|
+
if (!v) ui.closeRiskPolicyHealth()
|
|
18
18
|
},
|
|
19
19
|
})
|
|
20
20
|
|
|
@@ -29,7 +29,7 @@ async function reseed(id: string) {
|
|
|
29
29
|
await presets.reseed(id)
|
|
30
30
|
} catch (e) {
|
|
31
31
|
toast.add({
|
|
32
|
-
title: t('
|
|
32
|
+
title: t('riskPolicy.health.toast.reseedFailed'),
|
|
33
33
|
description: e instanceof Error ? e.message : String(e),
|
|
34
34
|
icon: 'i-lucide-triangle-alert',
|
|
35
35
|
color: 'error',
|
|
@@ -53,11 +53,11 @@ const reseedableCount = computed(
|
|
|
53
53
|
</script>
|
|
54
54
|
|
|
55
55
|
<template>
|
|
56
|
-
<UModal v-model:open="open" :title="t('
|
|
56
|
+
<UModal v-model:open="open" :title="t('riskPolicy.health.title')" :ui="{ content: 'max-w-2xl' }">
|
|
57
57
|
<template #body>
|
|
58
58
|
<div v-if="!hasIssues" class="py-6 text-center text-sm text-slate-400">
|
|
59
59
|
<UIcon name="i-lucide-check-circle-2" class="mx-auto mb-2 h-8 w-8 text-emerald-400" />
|
|
60
|
-
{{ t('
|
|
60
|
+
{{ t('riskPolicy.health.allValid') }}
|
|
61
61
|
</div>
|
|
62
62
|
|
|
63
63
|
<div v-else class="space-y-5">
|
|
@@ -66,10 +66,10 @@ const reseedableCount = computed(
|
|
|
66
66
|
<div class="flex items-center gap-2">
|
|
67
67
|
<UIcon name="i-lucide-sparkles" class="h-4 w-4 text-emerald-400" />
|
|
68
68
|
<h3 class="text-sm font-semibold text-slate-200">
|
|
69
|
-
{{ t('
|
|
69
|
+
{{ t('riskPolicy.health.newHeading') }}
|
|
70
70
|
</h3>
|
|
71
71
|
</div>
|
|
72
|
-
<p class="text-[11px] text-slate-500">{{ t('
|
|
72
|
+
<p class="text-[11px] text-slate-500">{{ t('riskPolicy.health.newDescription') }}</p>
|
|
73
73
|
<ul class="space-y-2">
|
|
74
74
|
<li
|
|
75
75
|
v-for="i in newPresets"
|
|
@@ -90,7 +90,7 @@ const reseedableCount = computed(
|
|
|
90
90
|
:disabled="anyBusy"
|
|
91
91
|
@click="reseed(i.id)"
|
|
92
92
|
>
|
|
93
|
-
{{ t('
|
|
93
|
+
{{ t('riskPolicy.health.add') }}
|
|
94
94
|
</UButton>
|
|
95
95
|
</li>
|
|
96
96
|
</ul>
|
|
@@ -101,10 +101,10 @@ const reseedableCount = computed(
|
|
|
101
101
|
<div class="flex items-center gap-2">
|
|
102
102
|
<UIcon name="i-lucide-arrow-up-circle" class="h-4 w-4 text-amber-400" />
|
|
103
103
|
<h3 class="text-sm font-semibold text-slate-200">
|
|
104
|
-
{{ t('
|
|
104
|
+
{{ t('riskPolicy.health.updatesHeading') }}
|
|
105
105
|
</h3>
|
|
106
106
|
</div>
|
|
107
|
-
<p class="text-[11px] text-slate-500">{{ t('
|
|
107
|
+
<p class="text-[11px] text-slate-500">{{ t('riskPolicy.health.updatesDescription') }}</p>
|
|
108
108
|
<ul class="space-y-2">
|
|
109
109
|
<li
|
|
110
110
|
v-for="i in outdated"
|
|
@@ -115,7 +115,7 @@ const reseedableCount = computed(
|
|
|
115
115
|
<span class="truncate text-sm font-medium text-slate-100">{{ i.name }}</span>
|
|
116
116
|
<p class="text-[11px] text-amber-400/80">
|
|
117
117
|
{{
|
|
118
|
-
t('
|
|
118
|
+
t('riskPolicy.health.versionAvailable', {
|
|
119
119
|
from: i.fromVersion ?? 0,
|
|
120
120
|
to: i.toVersion ?? 0,
|
|
121
121
|
})
|
|
@@ -131,7 +131,7 @@ const reseedableCount = computed(
|
|
|
131
131
|
:disabled="anyBusy"
|
|
132
132
|
@click="reseed(i.id)"
|
|
133
133
|
>
|
|
134
|
-
{{ t('
|
|
134
|
+
{{ t('riskPolicy.health.reseed') }}
|
|
135
135
|
</UButton>
|
|
136
136
|
</li>
|
|
137
137
|
</ul>
|
|
@@ -149,16 +149,16 @@ const reseedableCount = computed(
|
|
|
149
149
|
:loading="anyBusy"
|
|
150
150
|
@click="reseedAll"
|
|
151
151
|
>
|
|
152
|
-
{{ t('
|
|
152
|
+
{{ t('riskPolicy.health.reseedAll', { count: reseedableCount }) }}
|
|
153
153
|
</UButton>
|
|
154
154
|
<span v-else />
|
|
155
155
|
<UButton
|
|
156
156
|
color="neutral"
|
|
157
157
|
variant="ghost"
|
|
158
158
|
:disabled="anyBusy"
|
|
159
|
-
@click="ui.
|
|
159
|
+
@click="ui.closeRiskPolicyHealth()"
|
|
160
160
|
>
|
|
161
|
-
{{ hasIssues ? t('
|
|
161
|
+
{{ hasIssues ? t('riskPolicy.health.dismiss') : t('riskPolicy.health.done') }}
|
|
162
162
|
</UButton>
|
|
163
163
|
</div>
|
|
164
164
|
</template>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// Workspace settings: the merge-threshold preset library a task picks its
|
|
3
3
|
// auto-merge policy from (the `merger` step compares a PR's assessment against the
|
|
4
|
-
// resolved preset). Full CRUD over the
|
|
4
|
+
// resolved preset). Full CRUD over the riskPolicies store — the same library the
|
|
5
5
|
// task inspector's "Merge policy" dropdown selects from. Exactly one preset is the
|
|
6
6
|
// default; it cannot be deleted or un-defaulted (the backend enforces this too).
|
|
7
7
|
import { computed, reactive, ref, watch } from 'vue'
|
|
8
|
-
import type {
|
|
8
|
+
import type { RiskPolicy, RequirementConcernLevel } from '~/types/merge'
|
|
9
9
|
|
|
10
10
|
const { t } = useI18n()
|
|
11
11
|
|
|
@@ -13,10 +13,10 @@ const { t } = useI18n()
|
|
|
13
13
|
// the typecheck); each value is a LITERAL catalog key so the typed-message-keys check sees
|
|
14
14
|
// it. Leaf keys mirror the enum value verbatim.
|
|
15
15
|
const CONCERN_LABEL_KEYS: Record<RequirementConcernLevel, string> = {
|
|
16
|
-
none: 'settings.
|
|
17
|
-
low: 'settings.
|
|
18
|
-
medium: 'settings.
|
|
19
|
-
high: 'settings.
|
|
16
|
+
none: 'settings.riskPolicy.concern.none',
|
|
17
|
+
low: 'settings.riskPolicy.concern.low',
|
|
18
|
+
medium: 'settings.riskPolicy.concern.medium',
|
|
19
|
+
high: 'settings.riskPolicy.concern.high',
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// Concern-level options for the requirements auto-pass threshold (none < low < medium < high).
|
|
@@ -27,7 +27,7 @@ const CONCERN_LEVELS = computed<{ value: RequirementConcernLevel; label: string
|
|
|
27
27
|
{ value: 'high', label: t(CONCERN_LABEL_KEYS.high) },
|
|
28
28
|
])
|
|
29
29
|
|
|
30
|
-
const store =
|
|
30
|
+
const store = useRiskPoliciesStore()
|
|
31
31
|
const toast = useToast()
|
|
32
32
|
const { confirm } = useConfirm()
|
|
33
33
|
|
|
@@ -45,7 +45,7 @@ interface Draft {
|
|
|
45
45
|
}
|
|
46
46
|
const drafts = reactive<Record<string, Draft>>({})
|
|
47
47
|
|
|
48
|
-
function toDraft(p:
|
|
48
|
+
function toDraft(p: RiskPolicy): Draft {
|
|
49
49
|
return {
|
|
50
50
|
name: p.name,
|
|
51
51
|
maxComplexity: Math.round(p.maxComplexity * 100),
|
|
@@ -78,7 +78,7 @@ function notifyError(title: string, e: unknown) {
|
|
|
78
78
|
})
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
async function save(p:
|
|
81
|
+
async function save(p: RiskPolicy) {
|
|
82
82
|
const d = drafts[p.id]
|
|
83
83
|
if (!d) return
|
|
84
84
|
busy.value = p.id
|
|
@@ -94,32 +94,32 @@ async function save(p: MergeThresholdPreset) {
|
|
|
94
94
|
autoMergeEnabled: d.autoMergeEnabled,
|
|
95
95
|
})
|
|
96
96
|
toast.add({
|
|
97
|
-
title: t('settings.
|
|
97
|
+
title: t('settings.riskPolicy.toast.saved'),
|
|
98
98
|
icon: 'i-lucide-check',
|
|
99
99
|
color: 'success',
|
|
100
100
|
})
|
|
101
101
|
} catch (e) {
|
|
102
|
-
notifyError(t('settings.
|
|
102
|
+
notifyError(t('settings.riskPolicy.toast.saveFailed'), e)
|
|
103
103
|
} finally {
|
|
104
104
|
busy.value = null
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
async function makeDefault(p:
|
|
108
|
+
async function makeDefault(p: RiskPolicy) {
|
|
109
109
|
busy.value = p.id
|
|
110
110
|
try {
|
|
111
111
|
await store.update(p.id, { isDefault: true })
|
|
112
112
|
} catch (e) {
|
|
113
|
-
notifyError(t('settings.
|
|
113
|
+
notifyError(t('settings.riskPolicy.toast.defaultFailed'), e)
|
|
114
114
|
} finally {
|
|
115
115
|
busy.value = null
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
async function remove(p:
|
|
119
|
+
async function remove(p: RiskPolicy) {
|
|
120
120
|
const ok = await confirm({
|
|
121
|
-
title: t('settings.
|
|
122
|
-
description: t('settings.
|
|
121
|
+
title: t('settings.riskPolicy.confirmDelete.title'),
|
|
122
|
+
description: t('settings.riskPolicy.confirmDelete.body', { name: p.name }),
|
|
123
123
|
variant: 'destructive',
|
|
124
124
|
confirmLabel: t('common.delete'),
|
|
125
125
|
icon: 'i-lucide-trash-2',
|
|
@@ -129,7 +129,7 @@ async function remove(p: MergeThresholdPreset) {
|
|
|
129
129
|
try {
|
|
130
130
|
await store.remove(p.id)
|
|
131
131
|
} catch (e) {
|
|
132
|
-
notifyError(t('settings.
|
|
132
|
+
notifyError(t('settings.riskPolicy.toast.deleteFailed'), e)
|
|
133
133
|
} finally {
|
|
134
134
|
busy.value = null
|
|
135
135
|
}
|
|
@@ -165,12 +165,12 @@ async function create() {
|
|
|
165
165
|
draft.name = ''
|
|
166
166
|
draft.autoMergeEnabled = true
|
|
167
167
|
toast.add({
|
|
168
|
-
title: t('settings.
|
|
168
|
+
title: t('settings.riskPolicy.toast.created'),
|
|
169
169
|
icon: 'i-lucide-check',
|
|
170
170
|
color: 'success',
|
|
171
171
|
})
|
|
172
172
|
} catch (e) {
|
|
173
|
-
notifyError(t('settings.
|
|
173
|
+
notifyError(t('settings.riskPolicy.toast.createFailed'), e)
|
|
174
174
|
} finally {
|
|
175
175
|
creating.value = false
|
|
176
176
|
}
|
|
@@ -180,13 +180,13 @@ async function create() {
|
|
|
180
180
|
<template>
|
|
181
181
|
<div class="space-y-4">
|
|
182
182
|
<i18n-t
|
|
183
|
-
keypath="settings.
|
|
183
|
+
keypath="settings.riskPolicy.intro"
|
|
184
184
|
tag="p"
|
|
185
185
|
class="text-xs text-slate-400"
|
|
186
186
|
scope="global"
|
|
187
187
|
>
|
|
188
188
|
<template #merger>
|
|
189
|
-
<span class="text-slate-300">{{ t('settings.
|
|
189
|
+
<span class="text-slate-300">{{ t('settings.riskPolicy.mergerAgent') }}</span>
|
|
190
190
|
</template>
|
|
191
191
|
</i18n-t>
|
|
192
192
|
|
|
@@ -200,10 +200,10 @@ async function create() {
|
|
|
200
200
|
v-model="drafts[p.id]!.name"
|
|
201
201
|
size="sm"
|
|
202
202
|
class="flex-1"
|
|
203
|
-
:placeholder="t('settings.
|
|
203
|
+
:placeholder="t('settings.riskPolicy.presetNamePlaceholder')"
|
|
204
204
|
/>
|
|
205
205
|
<UBadge v-if="p.isDefault" color="primary" variant="subtle" size="sm">
|
|
206
|
-
{{ t('settings.
|
|
206
|
+
{{ t('settings.riskPolicy.default') }}
|
|
207
207
|
</UBadge>
|
|
208
208
|
<UButton
|
|
209
209
|
v-else
|
|
@@ -214,7 +214,7 @@ async function create() {
|
|
|
214
214
|
:loading="busy === p.id"
|
|
215
215
|
@click="makeDefault(p)"
|
|
216
216
|
>
|
|
217
|
-
{{ t('settings.
|
|
217
|
+
{{ t('settings.riskPolicy.makeDefault') }}
|
|
218
218
|
</UButton>
|
|
219
219
|
<UButton
|
|
220
220
|
color="error"
|
|
@@ -224,8 +224,8 @@ async function create() {
|
|
|
224
224
|
:disabled="p.isDefault || busy === p.id"
|
|
225
225
|
:title="
|
|
226
226
|
p.isDefault
|
|
227
|
-
? t('settings.
|
|
228
|
-
: t('settings.
|
|
227
|
+
? t('settings.riskPolicy.deleteDefaultBlocked')
|
|
228
|
+
: t('settings.riskPolicy.deletePreset')
|
|
229
229
|
"
|
|
230
230
|
@click="remove(p)"
|
|
231
231
|
/>
|
|
@@ -234,7 +234,7 @@ async function create() {
|
|
|
234
234
|
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
235
235
|
<label class="block">
|
|
236
236
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
237
|
-
{{ t('settings.
|
|
237
|
+
{{ t('settings.riskPolicy.field.maxComplexity') }}
|
|
238
238
|
</span>
|
|
239
239
|
<UInput
|
|
240
240
|
v-model.number="drafts[p.id]!.maxComplexity"
|
|
@@ -246,7 +246,7 @@ async function create() {
|
|
|
246
246
|
</label>
|
|
247
247
|
<label class="block">
|
|
248
248
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
249
|
-
{{ t('settings.
|
|
249
|
+
{{ t('settings.riskPolicy.field.maxRisk') }}
|
|
250
250
|
</span>
|
|
251
251
|
<UInput
|
|
252
252
|
v-model.number="drafts[p.id]!.maxRisk"
|
|
@@ -258,7 +258,7 @@ async function create() {
|
|
|
258
258
|
</label>
|
|
259
259
|
<label class="block">
|
|
260
260
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
261
|
-
{{ t('settings.
|
|
261
|
+
{{ t('settings.riskPolicy.field.maxImpact') }}
|
|
262
262
|
</span>
|
|
263
263
|
<UInput
|
|
264
264
|
v-model.number="drafts[p.id]!.maxImpact"
|
|
@@ -270,7 +270,7 @@ async function create() {
|
|
|
270
270
|
</label>
|
|
271
271
|
<label class="block">
|
|
272
272
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
273
|
-
{{ t('settings.
|
|
273
|
+
{{ t('settings.riskPolicy.field.ciMaxAttempts') }}
|
|
274
274
|
</span>
|
|
275
275
|
<UInput
|
|
276
276
|
v-model.number="drafts[p.id]!.ciMaxAttempts"
|
|
@@ -282,7 +282,7 @@ async function create() {
|
|
|
282
282
|
</label>
|
|
283
283
|
<label class="block">
|
|
284
284
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
285
|
-
{{ t('settings.
|
|
285
|
+
{{ t('settings.riskPolicy.field.maxRequirementIterations') }}
|
|
286
286
|
</span>
|
|
287
287
|
<UInput
|
|
288
288
|
v-model.number="drafts[p.id]!.maxRequirementIterations"
|
|
@@ -294,7 +294,7 @@ async function create() {
|
|
|
294
294
|
</label>
|
|
295
295
|
<label class="block">
|
|
296
296
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
297
|
-
{{ t('settings.
|
|
297
|
+
{{ t('settings.riskPolicy.field.maxRequirementConcernAllowed') }}
|
|
298
298
|
</span>
|
|
299
299
|
<USelect
|
|
300
300
|
v-model="drafts[p.id]!.maxRequirementConcernAllowed"
|
|
@@ -309,11 +309,11 @@ async function create() {
|
|
|
309
309
|
<USwitch
|
|
310
310
|
v-model="drafts[p.id]!.autoMergeEnabled"
|
|
311
311
|
size="sm"
|
|
312
|
-
:label="t('settings.
|
|
312
|
+
:label="t('settings.riskPolicy.field.autoMerge')"
|
|
313
313
|
:description="
|
|
314
314
|
drafts[p.id]!.autoMergeEnabled
|
|
315
|
-
? t('settings.
|
|
316
|
-
: t('settings.
|
|
315
|
+
? t('settings.riskPolicy.autoMergeOnHint')
|
|
316
|
+
: t('settings.riskPolicy.autoMergeOffHint')
|
|
317
317
|
"
|
|
318
318
|
/>
|
|
319
319
|
<UButton
|
|
@@ -332,22 +332,22 @@ async function create() {
|
|
|
332
332
|
<!-- create -->
|
|
333
333
|
<div class="rounded-lg border border-dashed border-slate-700 p-3">
|
|
334
334
|
<p class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
335
|
-
{{ t('settings.
|
|
335
|
+
{{ t('settings.riskPolicy.newPreset') }}
|
|
336
336
|
</p>
|
|
337
337
|
<div class="flex flex-wrap items-end gap-3">
|
|
338
338
|
<label class="block min-w-40 flex-1">
|
|
339
339
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
340
|
-
{{ t('settings.
|
|
340
|
+
{{ t('settings.riskPolicy.create.name') }}
|
|
341
341
|
</span>
|
|
342
342
|
<UInput
|
|
343
343
|
v-model="draft.name"
|
|
344
344
|
size="sm"
|
|
345
|
-
:placeholder="t('settings.
|
|
345
|
+
:placeholder="t('settings.riskPolicy.create.namePlaceholder')"
|
|
346
346
|
/>
|
|
347
347
|
</label>
|
|
348
348
|
<label class="block w-20">
|
|
349
349
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
350
|
-
{{ t('settings.
|
|
350
|
+
{{ t('settings.riskPolicy.create.complexity') }}
|
|
351
351
|
</span>
|
|
352
352
|
<UInput
|
|
353
353
|
v-model.number="draft.maxComplexity"
|
|
@@ -359,25 +359,25 @@ async function create() {
|
|
|
359
359
|
</label>
|
|
360
360
|
<label class="block w-20">
|
|
361
361
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
362
|
-
{{ t('settings.
|
|
362
|
+
{{ t('settings.riskPolicy.create.risk') }}
|
|
363
363
|
</span>
|
|
364
364
|
<UInput v-model.number="draft.maxRisk" type="number" :min="0" :max="100" size="sm" />
|
|
365
365
|
</label>
|
|
366
366
|
<label class="block w-20">
|
|
367
367
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
368
|
-
{{ t('settings.
|
|
368
|
+
{{ t('settings.riskPolicy.create.impact') }}
|
|
369
369
|
</span>
|
|
370
370
|
<UInput v-model.number="draft.maxImpact" type="number" :min="0" :max="100" size="sm" />
|
|
371
371
|
</label>
|
|
372
372
|
<label class="block w-20">
|
|
373
373
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
374
|
-
{{ t('settings.
|
|
374
|
+
{{ t('settings.riskPolicy.create.ciFix') }}
|
|
375
375
|
</span>
|
|
376
376
|
<UInput v-model.number="draft.ciMaxAttempts" type="number" :min="0" :max="50" size="sm" />
|
|
377
377
|
</label>
|
|
378
378
|
<label class="block w-20">
|
|
379
379
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
380
|
-
{{ t('settings.
|
|
380
|
+
{{ t('settings.riskPolicy.create.reqIter') }}
|
|
381
381
|
</span>
|
|
382
382
|
<UInput
|
|
383
383
|
v-model.number="draft.maxRequirementIterations"
|
|
@@ -389,7 +389,7 @@ async function create() {
|
|
|
389
389
|
</label>
|
|
390
390
|
<label class="block w-32">
|
|
391
391
|
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
392
|
-
{{ t('settings.
|
|
392
|
+
{{ t('settings.riskPolicy.create.autoPass') }}
|
|
393
393
|
</span>
|
|
394
394
|
<USelect
|
|
395
395
|
v-model="draft.maxRequirementConcernAllowed"
|
|
@@ -401,7 +401,7 @@ async function create() {
|
|
|
401
401
|
<USwitch
|
|
402
402
|
v-model="draft.autoMergeEnabled"
|
|
403
403
|
size="sm"
|
|
404
|
-
:label="t('settings.
|
|
404
|
+
:label="t('settings.riskPolicy.field.autoMerge')"
|
|
405
405
|
/>
|
|
406
406
|
<UButton
|
|
407
407
|
color="primary"
|
|
@@ -411,7 +411,7 @@ async function create() {
|
|
|
411
411
|
:disabled="!draft.name.trim()"
|
|
412
412
|
@click="create"
|
|
413
413
|
>
|
|
414
|
-
{{ t('settings.
|
|
414
|
+
{{ t('settings.riskPolicy.add') }}
|
|
415
415
|
</UButton>
|
|
416
416
|
</div>
|
|
417
417
|
</div>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The Usage report: token usage this billing period, broken down into flat-rate
|
|
3
|
+
// subscription harness usage (Claude Code / Codex / GLM / pooled Kimi & DeepSeek) and
|
|
4
|
+
// metered API/proxy calls. Reporting only — the spend budget (Budget tab) still counts
|
|
5
|
+
// only the metered rows. See docs/initiatives/usage-and-quota-tracking.md.
|
|
6
|
+
import { computed, watch } from 'vue'
|
|
7
|
+
import type { UsageBreakdownRow } from '@cat-factory/contracts'
|
|
8
|
+
|
|
9
|
+
const { t, n, d } = useI18n()
|
|
10
|
+
const usage = useUsageStore()
|
|
11
|
+
const workspace = useWorkspaceStore()
|
|
12
|
+
|
|
13
|
+
// Load (and reload) whenever the active workspace resolves/changes.
|
|
14
|
+
watch(
|
|
15
|
+
() => workspace.workspaceId,
|
|
16
|
+
(ws) => {
|
|
17
|
+
if (ws) usage.load(ws)
|
|
18
|
+
},
|
|
19
|
+
{ immediate: true },
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const currency = computed(() => usage.report?.currency ?? 'EUR')
|
|
23
|
+
const money = (value: number) => n(value, { key: 'currency', currency: currency.value })
|
|
24
|
+
|
|
25
|
+
// The largest single-row token total in a section, so the per-row bars are relative to the
|
|
26
|
+
// section's heaviest model (a full bar = the biggest consumer, not an absolute quota).
|
|
27
|
+
function maxTokens(rows: UsageBreakdownRow[]): number {
|
|
28
|
+
return rows.reduce((m, r) => Math.max(m, r.inputTokens + r.outputTokens), 0)
|
|
29
|
+
}
|
|
30
|
+
function pctOf(row: UsageBreakdownRow, rows: UsageBreakdownRow[]): number {
|
|
31
|
+
const max = maxTokens(rows)
|
|
32
|
+
if (max <= 0) return 0
|
|
33
|
+
return Math.round(((row.inputTokens + row.outputTokens) / max) * 100)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const hasAny = computed(() => usage.rows.length > 0)
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="space-y-6">
|
|
41
|
+
<p class="text-[11px] text-slate-400">
|
|
42
|
+
{{ t('settings.usage.body') }}
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<p v-if="usage.report" class="text-[11px] text-slate-500">
|
|
46
|
+
{{ t('settings.usage.period', { date: d(new Date(usage.report.periodStart), 'short') }) }}
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
<p v-if="usage.loading" class="text-[11px] text-slate-500">{{ t('common.loading') }}</p>
|
|
50
|
+
<p v-else-if="usage.error" class="text-[11px] text-rose-400">{{ usage.error }}</p>
|
|
51
|
+
<p v-else-if="!hasAny" class="text-[11px] text-slate-500">{{ t('settings.usage.empty') }}</p>
|
|
52
|
+
|
|
53
|
+
<template v-else>
|
|
54
|
+
<!-- Subscriptions (flat-rate quota harnesses) -->
|
|
55
|
+
<section v-if="usage.subscription.length" class="space-y-3">
|
|
56
|
+
<div class="flex items-baseline justify-between">
|
|
57
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
58
|
+
{{ t('settings.usage.subscription') }}
|
|
59
|
+
</h3>
|
|
60
|
+
<span class="text-[11px] text-slate-400">
|
|
61
|
+
{{
|
|
62
|
+
t('settings.usage.tokens', {
|
|
63
|
+
input: formatTokens(usage.subscriptionTotal.inputTokens),
|
|
64
|
+
output: formatTokens(usage.subscriptionTotal.outputTokens),
|
|
65
|
+
})
|
|
66
|
+
}}
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
<p class="text-[10px] text-slate-500">{{ t('settings.usage.illustrative') }}</p>
|
|
70
|
+
<div
|
|
71
|
+
v-for="row in usage.subscription"
|
|
72
|
+
:key="`sub-${row.vendor}-${row.provider}-${row.model}`"
|
|
73
|
+
class="space-y-1"
|
|
74
|
+
:data-testid="'usage-row-subscription'"
|
|
75
|
+
>
|
|
76
|
+
<div class="flex items-center justify-between gap-2 text-[11px]">
|
|
77
|
+
<span class="min-w-0 truncate font-medium text-slate-300">{{ row.model }}</span>
|
|
78
|
+
<span class="shrink-0 rounded bg-slate-700/60 px-1.5 py-0.5 text-[10px] text-slate-300">
|
|
79
|
+
{{ row.vendor ?? row.provider }}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
<UProgress :model-value="pctOf(row, usage.subscription)" size="xs" color="primary" />
|
|
83
|
+
<div class="flex justify-between text-[10px] text-slate-500">
|
|
84
|
+
<span>
|
|
85
|
+
{{
|
|
86
|
+
t('settings.usage.tokens', {
|
|
87
|
+
input: formatTokens(row.inputTokens),
|
|
88
|
+
output: formatTokens(row.outputTokens),
|
|
89
|
+
})
|
|
90
|
+
}}
|
|
91
|
+
· {{ t('settings.usage.calls', { count: row.calls }) }}
|
|
92
|
+
</span>
|
|
93
|
+
<span>~{{ money(row.costEstimate) }}</span>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
<!-- Metered API / proxy calls (real spend) -->
|
|
99
|
+
<section v-if="usage.metered.length" class="space-y-3">
|
|
100
|
+
<div class="flex items-baseline justify-between">
|
|
101
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
102
|
+
{{ t('settings.usage.metered') }}
|
|
103
|
+
</h3>
|
|
104
|
+
<span class="text-[11px] text-slate-400">
|
|
105
|
+
{{
|
|
106
|
+
t('settings.usage.tokens', {
|
|
107
|
+
input: formatTokens(usage.meteredTotal.inputTokens),
|
|
108
|
+
output: formatTokens(usage.meteredTotal.outputTokens),
|
|
109
|
+
})
|
|
110
|
+
}}
|
|
111
|
+
· {{ money(usage.meteredTotal.costEstimate) }}
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div
|
|
115
|
+
v-for="row in usage.metered"
|
|
116
|
+
:key="`met-${row.provider}-${row.model}`"
|
|
117
|
+
class="space-y-1"
|
|
118
|
+
:data-testid="'usage-row-metered'"
|
|
119
|
+
>
|
|
120
|
+
<div class="flex items-center justify-between gap-2 text-[11px]">
|
|
121
|
+
<span class="min-w-0 truncate font-medium text-slate-300">
|
|
122
|
+
{{ row.provider }}:{{ row.model }}
|
|
123
|
+
</span>
|
|
124
|
+
</div>
|
|
125
|
+
<UProgress :model-value="pctOf(row, usage.metered)" size="xs" color="neutral" />
|
|
126
|
+
<div class="flex justify-between text-[10px] text-slate-500">
|
|
127
|
+
<span>
|
|
128
|
+
{{
|
|
129
|
+
t('settings.usage.tokens', {
|
|
130
|
+
input: formatTokens(row.inputTokens),
|
|
131
|
+
output: formatTokens(row.outputTokens),
|
|
132
|
+
})
|
|
133
|
+
}}
|
|
134
|
+
· {{ t('settings.usage.calls', { count: row.calls }) }}
|
|
135
|
+
</span>
|
|
136
|
+
<span>{{ money(row.costEstimate) }}</span>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</section>
|
|
140
|
+
</template>
|
|
141
|
+
</div>
|
|
142
|
+
</template>
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
// standalone modals).
|
|
10
10
|
import { reactive, ref, watch } from 'vue'
|
|
11
11
|
import type { CreateTaskType, TaskLimitMode } from '~/types/domain'
|
|
12
|
-
import
|
|
12
|
+
import RiskPolicyPanel from '~/components/settings/RiskPolicyPanel.vue'
|
|
13
13
|
import IssueTrackerPanel from '~/components/settings/IssueTrackerPanel.vue'
|
|
14
14
|
import ServiceFragmentDefaultsPanel from '~/components/settings/ServiceFragmentDefaultsPanel.vue'
|
|
15
15
|
import BudgetSettings from '~/components/settings/BudgetSettings.vue'
|
|
16
|
+
import UsageSettings from '~/components/settings/UsageSettings.vue'
|
|
16
17
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
17
18
|
|
|
18
19
|
const { t, te } = useI18n()
|
|
@@ -46,6 +47,12 @@ const tabs = computed(() => [
|
|
|
46
47
|
icon: 'i-lucide-wallet',
|
|
47
48
|
slot: 'budget',
|
|
48
49
|
},
|
|
50
|
+
{
|
|
51
|
+
value: 'usage',
|
|
52
|
+
label: t('settings.workspaceSettings.tabs.usage'),
|
|
53
|
+
icon: 'i-lucide-bar-chart-3',
|
|
54
|
+
slot: 'usage',
|
|
55
|
+
},
|
|
49
56
|
{
|
|
50
57
|
value: 'merge',
|
|
51
58
|
label: t('settings.workspaceSettings.tabs.merge'),
|
|
@@ -331,9 +338,14 @@ async function save() {
|
|
|
331
338
|
<BudgetSettings />
|
|
332
339
|
</template>
|
|
333
340
|
|
|
341
|
+
<!-- Usage report (metered + subscription token usage this period) -->
|
|
342
|
+
<template #usage>
|
|
343
|
+
<UsageSettings />
|
|
344
|
+
</template>
|
|
345
|
+
|
|
334
346
|
<!-- Merge thresholds -->
|
|
335
347
|
<template #merge>
|
|
336
|
-
<
|
|
348
|
+
<RiskPolicyPanel />
|
|
337
349
|
</template>
|
|
338
350
|
|
|
339
351
|
<!-- Issue tracker -->
|
|
@@ -47,7 +47,7 @@ export function boardApi({ send, ws }: ApiContext) {
|
|
|
47
47
|
description?: string
|
|
48
48
|
taskType?: CreateTaskType
|
|
49
49
|
taskTypeFields?: TaskTypeFields
|
|
50
|
-
|
|
50
|
+
riskPolicyId?: string
|
|
51
51
|
modelPresetId?: string
|
|
52
52
|
pipelineId?: string
|
|
53
53
|
agentConfig?: Record<string, string>
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getExecutionAgentContextContract,
|
|
6
6
|
getExecutionLlmMetricsContract,
|
|
7
7
|
getExecutionSearchQueriesContract,
|
|
8
|
+
getWorkspaceUsageContract,
|
|
8
9
|
mergeBlockContract,
|
|
9
10
|
rejectStepContract,
|
|
10
11
|
requestStepChangesContract,
|
|
@@ -157,5 +158,9 @@ export function executionApi({ send, sendWith, ws, pwHeaders }: ApiContext) {
|
|
|
157
158
|
// ---- spend safeguard --------------------------------------------------
|
|
158
159
|
resumeSpend: (workspaceId: string) =>
|
|
159
160
|
send(resumeSpendContract, { pathPrefix: ws(workspaceId) }),
|
|
161
|
+
|
|
162
|
+
// ---- usage report -----------------------------------------------------
|
|
163
|
+
getUsage: (workspaceId: string) =>
|
|
164
|
+
send(getWorkspaceUsageContract, { pathPrefix: ws(workspaceId) }),
|
|
160
165
|
}
|
|
161
166
|
}
|