@cat-factory/app 0.218.0 → 0.219.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/layout/AccountFailureKindRules.vue +285 -0
- package/app/components/layout/AccountPlatformAlertSettings.vue +43 -1
- package/app/components/panels/OperatorDashboardPanel.vue +7 -19
- package/app/types/execution.ts +1 -0
- package/app/utils/failureKinds.spec.ts +82 -0
- package/app/utils/failureKinds.ts +91 -0
- package/i18n/locales/de.json +22 -1
- package/i18n/locales/en.json +22 -1
- package/i18n/locales/es.json +22 -1
- package/i18n/locales/fr.json +22 -1
- package/i18n/locales/he.json +22 -1
- package/i18n/locales/it.json +22 -1
- package/i18n/locales/ja.json +22 -1
- package/i18n/locales/pl.json +22 -1
- package/i18n/locales/tr.json +22 -1
- package/i18n/locales/uk.json +22 -1
- package/package.json +2 -2
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { AgentFailureKind, PlatformFailureKindRule } from '~/types/execution'
|
|
4
|
+
import {
|
|
5
|
+
AGENT_FAILURE_KINDS,
|
|
6
|
+
FAILURE_KIND_KEYS,
|
|
7
|
+
failureKindRuleFaults,
|
|
8
|
+
isAgentFailureKind,
|
|
9
|
+
MAX_FAILURE_KIND_RULES,
|
|
10
|
+
} from '~/utils/failureKinds'
|
|
11
|
+
|
|
12
|
+
// The per-failure-kind alert rules of the platform-health sheet: "page when `evicted` reaches
|
|
13
|
+
// 5% of the window's failures". Its own component because it edits a LIST where
|
|
14
|
+
// its parent edits scalars, and the two have different notions of what an override is.
|
|
15
|
+
//
|
|
16
|
+
// That difference is the whole of this file. A scalar override is per field: leave the box
|
|
17
|
+
// empty and that one ceiling inherits. A list cannot work that way — merging rule by rule would
|
|
18
|
+
// let an account retune a deployment rule but never DROP one, and silently reinstating a rule an
|
|
19
|
+
// operator deleted is the worse failure for something wired to a pager. So the account's list
|
|
20
|
+
// REPLACES the deployment's, which makes "no rules at all" a setting somebody can choose, and
|
|
21
|
+
// makes it indistinguishable from "inherit" unless the two are asked separately. Hence the
|
|
22
|
+
// explicit override switch: off omits the field, on sends the list, empty included.
|
|
23
|
+
|
|
24
|
+
const props = defineProps<{
|
|
25
|
+
/** The account's rules, or undefined when it inherits the deployment's. */
|
|
26
|
+
modelValue: PlatformFailureKindRule[] | undefined
|
|
27
|
+
}>()
|
|
28
|
+
const emit = defineEmits<{ 'update:modelValue': [PlatformFailureKindRule[] | undefined] }>()
|
|
29
|
+
|
|
30
|
+
const { t } = useI18n()
|
|
31
|
+
|
|
32
|
+
const overriding = computed(() => props.modelValue !== undefined)
|
|
33
|
+
const rules = computed(() => props.modelValue ?? [])
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The kinds offered for a row: the known vocabulary, plus whatever this row already names.
|
|
37
|
+
*
|
|
38
|
+
* The second half matters on exactly the row that would otherwise lose data. A rule stored
|
|
39
|
+
* against a kind a later release RETIRED still parses (the contract keeps the field a string so
|
|
40
|
+
* one stale rule cannot take the account's whole settings row down with it), so it reaches this
|
|
41
|
+
* editor, and a select that only offered current members would silently re-point it at the first
|
|
42
|
+
* one the moment anything else on the sheet was saved. It is offered back, marked unrecognised,
|
|
43
|
+
* so the human decides whether to re-pick it or drop it.
|
|
44
|
+
*/
|
|
45
|
+
function kindItems(current: string) {
|
|
46
|
+
const known = AGENT_FAILURE_KINDS.map((kind) => ({
|
|
47
|
+
label: t(FAILURE_KIND_KEYS[kind]),
|
|
48
|
+
value: kind as string,
|
|
49
|
+
}))
|
|
50
|
+
if (current === '' || isAgentFailureKind(current)) return known
|
|
51
|
+
return [
|
|
52
|
+
...known,
|
|
53
|
+
{
|
|
54
|
+
label: t('settings.platformAlerts.failureKinds.unknownKind', { kind: current }),
|
|
55
|
+
value: current,
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The kinds already spoken for, so the UI can say which row is the duplicate. */
|
|
61
|
+
const duplicateKinds = computed(() => {
|
|
62
|
+
const seen = new Set<string>()
|
|
63
|
+
const dupes = new Set<string>()
|
|
64
|
+
for (const rule of rules.value) {
|
|
65
|
+
if (seen.has(rule.kind)) dupes.add(rule.kind)
|
|
66
|
+
seen.add(rule.kind)
|
|
67
|
+
}
|
|
68
|
+
return dupes
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
/** A share is edited in PERCENT, which is how the rule is spoken ("5% or more"), stored 0..1. */
|
|
72
|
+
function sharePercent(rule: PlatformFailureKindRule): number {
|
|
73
|
+
return Math.round(rule.maxShare * 1000) / 10
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function setOverriding(on: boolean) {
|
|
77
|
+
// Turning the override ON starts from the empty list rather than a seeded row: an editor that
|
|
78
|
+
// invented a rule would have the human deleting a page they never asked for.
|
|
79
|
+
emit('update:modelValue', on ? [] : undefined)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function patch(index: number, next: Partial<PlatformFailureKindRule>) {
|
|
83
|
+
emit(
|
|
84
|
+
'update:modelValue',
|
|
85
|
+
rules.value.map((rule, i) => (i === index ? { ...rule, ...next } : rule)),
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setShare(index: number, raw: string) {
|
|
90
|
+
const percent = Number(raw.trim())
|
|
91
|
+
// A blank or unreadable box is left at zero rather than dropped: `maxShare` has no "unset"
|
|
92
|
+
// (a rule without a ceiling is not a rule), and 0 is refused by the contract, so an incomplete
|
|
93
|
+
// row is REPORTED by `faults` below instead of being quietly discarded on save.
|
|
94
|
+
patch(index, { maxShare: Number.isFinite(percent) ? percent / 100 : 0 })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function setMinCount(index: number, raw: string) {
|
|
98
|
+
const trimmed = raw.trim()
|
|
99
|
+
if (trimmed === '') {
|
|
100
|
+
const { minCount: _dropped, ...rest } = rules.value[index]!
|
|
101
|
+
emit(
|
|
102
|
+
'update:modelValue',
|
|
103
|
+
rules.value.map((rule, i) => (i === index ? rest : rule)),
|
|
104
|
+
)
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
const parsed = Number(trimmed)
|
|
108
|
+
patch(index, { minCount: Number.isFinite(parsed) ? Math.round(parsed) : 1 })
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The first kind nothing has claimed, or undefined once every one is spoken for.
|
|
113
|
+
*
|
|
114
|
+
* Undefined is what DISABLES the add button, rather than seeding a duplicate of the first kind
|
|
115
|
+
* and letting the duplicate warning explain it afterwards: at most one rule per kind is the
|
|
116
|
+
* contract, so a row the editor can only add in a state the contract refuses is a row it should
|
|
117
|
+
* not offer. It also puts the list's cap out of reach by construction — the vocabulary is far
|
|
118
|
+
* shorter than `MAX_FAILURE_KIND_RULES`, so unique-by-kind is the binding limit.
|
|
119
|
+
*/
|
|
120
|
+
const nextFreeKind = computed<AgentFailureKind | undefined>(() => {
|
|
121
|
+
const taken = new Set(rules.value.map((rule) => rule.kind))
|
|
122
|
+
return AGENT_FAILURE_KINDS.find((kind) => !taken.has(kind))
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
function addRule() {
|
|
126
|
+
const kind = nextFreeKind.value
|
|
127
|
+
if (kind === undefined) return
|
|
128
|
+
emit('update:modelValue', [...rules.value, { kind, maxShare: 0.05 }])
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function removeRule(index: number) {
|
|
132
|
+
emit(
|
|
133
|
+
'update:modelValue',
|
|
134
|
+
rules.value.filter((_, i) => i !== index),
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* What the backend would refuse about this list. The same helper the parent's save path calls,
|
|
140
|
+
* so what the sheet flags and what the save stops on cannot disagree.
|
|
141
|
+
*/
|
|
142
|
+
const faults = computed(() => failureKindRuleFaults(rules.value))
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<template>
|
|
146
|
+
<div class="space-y-2" data-testid="platform-alert-failure-kinds">
|
|
147
|
+
<label class="text-[11px] font-medium text-slate-300">
|
|
148
|
+
{{ t('settings.platformAlerts.failureKinds.label') }}
|
|
149
|
+
</label>
|
|
150
|
+
<p class="text-[11px] text-slate-400">
|
|
151
|
+
{{ t('settings.platformAlerts.failureKinds.description') }}
|
|
152
|
+
</p>
|
|
153
|
+
|
|
154
|
+
<div class="space-y-1">
|
|
155
|
+
<UCheckbox
|
|
156
|
+
:model-value="overriding"
|
|
157
|
+
size="sm"
|
|
158
|
+
:label="t('settings.platformAlerts.failureKinds.overrideLabel')"
|
|
159
|
+
data-testid="platform-alert-failure-kinds-override"
|
|
160
|
+
@update:model-value="setOverriding(!!$event)"
|
|
161
|
+
/>
|
|
162
|
+
<p class="ps-6 text-[11px] text-slate-400">
|
|
163
|
+
{{ t('settings.platformAlerts.failureKinds.overrideHint') }}
|
|
164
|
+
</p>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<template v-if="overriding">
|
|
168
|
+
<p
|
|
169
|
+
v-if="rules.length === 0"
|
|
170
|
+
class="text-[11px] text-slate-500"
|
|
171
|
+
data-testid="platform-alert-failure-kinds-empty"
|
|
172
|
+
>
|
|
173
|
+
{{ t('settings.platformAlerts.failureKinds.empty') }}
|
|
174
|
+
</p>
|
|
175
|
+
|
|
176
|
+
<div
|
|
177
|
+
v-for="(rule, index) in rules"
|
|
178
|
+
:key="index"
|
|
179
|
+
class="grid grid-cols-1 items-end gap-2 sm:grid-cols-[2fr_1fr_1fr_auto]"
|
|
180
|
+
:data-testid="`platform-alert-failure-kind-row-${index}`"
|
|
181
|
+
>
|
|
182
|
+
<div class="space-y-1">
|
|
183
|
+
<label class="block text-[11px] text-slate-300">
|
|
184
|
+
{{ t('settings.platformAlerts.failureKinds.kindLabel') }}
|
|
185
|
+
</label>
|
|
186
|
+
<USelect
|
|
187
|
+
:model-value="rule.kind"
|
|
188
|
+
:items="kindItems(rule.kind)"
|
|
189
|
+
value-key="value"
|
|
190
|
+
size="sm"
|
|
191
|
+
:data-testid="`platform-alert-failure-kind-${index}`"
|
|
192
|
+
@update:model-value="patch(index, { kind: String($event) })"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="space-y-1">
|
|
196
|
+
<label class="block text-[11px] text-slate-300">
|
|
197
|
+
{{ t('settings.platformAlerts.failureKinds.shareLabel') }}
|
|
198
|
+
</label>
|
|
199
|
+
<UInput
|
|
200
|
+
:model-value="String(sharePercent(rule))"
|
|
201
|
+
type="number"
|
|
202
|
+
step="1"
|
|
203
|
+
size="sm"
|
|
204
|
+
:data-testid="`platform-alert-failure-share-${index}`"
|
|
205
|
+
@update:model-value="setShare(index, String($event))"
|
|
206
|
+
/>
|
|
207
|
+
<p v-if="index === 0" class="text-[11px] text-slate-500">
|
|
208
|
+
{{ t('settings.platformAlerts.failureKinds.shareHint') }}
|
|
209
|
+
</p>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="space-y-1">
|
|
212
|
+
<label class="block text-[11px] text-slate-300">
|
|
213
|
+
{{ t('settings.platformAlerts.failureKinds.minCountLabel') }}
|
|
214
|
+
</label>
|
|
215
|
+
<UInput
|
|
216
|
+
:model-value="rule.minCount === undefined ? '' : String(rule.minCount)"
|
|
217
|
+
type="number"
|
|
218
|
+
step="1"
|
|
219
|
+
size="sm"
|
|
220
|
+
:placeholder="t('settings.platformAlerts.failureKinds.minCountPlaceholder')"
|
|
221
|
+
:data-testid="`platform-alert-failure-min-count-${index}`"
|
|
222
|
+
@update:model-value="setMinCount(index, String($event))"
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
<UButton
|
|
226
|
+
color="neutral"
|
|
227
|
+
variant="subtle"
|
|
228
|
+
size="xs"
|
|
229
|
+
icon="i-lucide-trash-2"
|
|
230
|
+
:aria-label="t('settings.platformAlerts.failureKinds.removeRule')"
|
|
231
|
+
:data-testid="`platform-alert-failure-remove-${index}`"
|
|
232
|
+
@click="removeRule(index)"
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<p
|
|
237
|
+
v-if="faults.tooMany"
|
|
238
|
+
class="text-[11px] text-amber-300"
|
|
239
|
+
data-testid="platform-alert-failure-kinds-too-many"
|
|
240
|
+
>
|
|
241
|
+
{{
|
|
242
|
+
t('settings.platformAlerts.failureKinds.tooManyRules', { max: MAX_FAILURE_KIND_RULES })
|
|
243
|
+
}}
|
|
244
|
+
</p>
|
|
245
|
+
<p
|
|
246
|
+
v-else-if="duplicateKinds.size > 0"
|
|
247
|
+
class="text-[11px] text-amber-300"
|
|
248
|
+
data-testid="platform-alert-failure-kinds-duplicate"
|
|
249
|
+
>
|
|
250
|
+
{{ t('settings.platformAlerts.failureKinds.duplicateKind') }}
|
|
251
|
+
</p>
|
|
252
|
+
<p
|
|
253
|
+
v-else-if="faults.rows.length > 0"
|
|
254
|
+
class="text-[11px] text-amber-300"
|
|
255
|
+
data-testid="platform-alert-failure-kinds-invalid"
|
|
256
|
+
>
|
|
257
|
+
{{
|
|
258
|
+
t('settings.platformAlerts.failureKinds.invalidRows', { rows: faults.rows.join(', ') })
|
|
259
|
+
}}
|
|
260
|
+
</p>
|
|
261
|
+
|
|
262
|
+
<UButton
|
|
263
|
+
color="neutral"
|
|
264
|
+
variant="subtle"
|
|
265
|
+
size="xs"
|
|
266
|
+
icon="i-lucide-plus"
|
|
267
|
+
:disabled="nextFreeKind === undefined"
|
|
268
|
+
data-testid="platform-alert-failure-kinds-add"
|
|
269
|
+
@click="addRule"
|
|
270
|
+
>
|
|
271
|
+
{{ t('settings.platformAlerts.failureKinds.addRule') }}
|
|
272
|
+
</UButton>
|
|
273
|
+
<p
|
|
274
|
+
v-if="nextFreeKind === undefined"
|
|
275
|
+
class="text-[11px] text-slate-500"
|
|
276
|
+
data-testid="platform-alert-failure-kinds-all-covered"
|
|
277
|
+
>
|
|
278
|
+
{{ t('settings.platformAlerts.failureKinds.allKindsCovered') }}
|
|
279
|
+
</p>
|
|
280
|
+
<p class="text-[11px] text-slate-500">
|
|
281
|
+
{{ t('settings.platformAlerts.failureKinds.minCountHint') }}
|
|
282
|
+
</p>
|
|
283
|
+
</template>
|
|
284
|
+
</div>
|
|
285
|
+
</template>
|
|
@@ -4,7 +4,14 @@ import type {
|
|
|
4
4
|
PlatformAlertSettings,
|
|
5
5
|
PlatformAlertThresholdOverrides,
|
|
6
6
|
PlatformAlertWindow,
|
|
7
|
+
PlatformFailureKindRule,
|
|
7
8
|
} from '~/types/execution'
|
|
9
|
+
import AccountFailureKindRules from '~/components/layout/AccountFailureKindRules.vue'
|
|
10
|
+
import {
|
|
11
|
+
failureKindRuleFaults,
|
|
12
|
+
hasFailureKindRuleFaults,
|
|
13
|
+
MAX_FAILURE_KIND_RULES,
|
|
14
|
+
} from '~/utils/failureKinds'
|
|
8
15
|
|
|
9
16
|
// Per-account tuning for the platform-health alert sweep (admin only): the ceilings the
|
|
10
17
|
// deployment's aggregate run health is checked against, and the window they are evaluated over.
|
|
@@ -38,9 +45,12 @@ const windowItems = computed(() =>
|
|
|
38
45
|
)
|
|
39
46
|
|
|
40
47
|
/**
|
|
41
|
-
* The
|
|
48
|
+
* The NUMERIC ceilings, each rendered as one row: the contract key plus the input's step.
|
|
42
49
|
* ONE table drives the form, the hydrate and the save, so adding a threshold to the contract
|
|
43
50
|
* is a single entry rather than a form field plus a save branch free to disagree with it.
|
|
51
|
+
*
|
|
52
|
+
* `failureKindRules` is deliberately absent from it: it is a LIST, so it has neither a step nor
|
|
53
|
+
* a blank-means-inherit box, and it is edited by its own component below.
|
|
44
54
|
*/
|
|
45
55
|
const THRESHOLDS = [
|
|
46
56
|
{ field: 'minRuns', step: 1 },
|
|
@@ -83,6 +93,10 @@ const thresholdHints = computed<Record<ThresholdField, string>>(() => ({
|
|
|
83
93
|
const muted = ref(false)
|
|
84
94
|
const alertWindow = ref<PlatformAlertWindow | ''>('')
|
|
85
95
|
const values = ref<Record<ThresholdField, string>>(blankValues())
|
|
96
|
+
// Undefined is "inherit the deployment's rules"; an EMPTY array is "this account has none".
|
|
97
|
+
// Kept apart for the same reason blank and 0 are above, and here the two are further apart
|
|
98
|
+
// still: one follows the deployment's pager wiring, the other switches it off.
|
|
99
|
+
const failureKindRules = ref<PlatformFailureKindRule[] | undefined>(undefined)
|
|
86
100
|
const saving = ref(false)
|
|
87
101
|
|
|
88
102
|
function blankValues(): Record<ThresholdField, string> {
|
|
@@ -112,6 +126,9 @@ function hydrate() {
|
|
|
112
126
|
const next = blankValues()
|
|
113
127
|
for (const th of THRESHOLDS) next[th.field] = toDisplay(th.field, stored?.thresholds?.[th.field])
|
|
114
128
|
values.value = next
|
|
129
|
+
// Copied, not aliased: the editor mutates by replacement, and hydrating from the store's own
|
|
130
|
+
// objects would let an unsaved edit read back as the account's stored state.
|
|
131
|
+
failureKindRules.value = stored?.thresholds?.failureKindRules?.map((rule) => ({ ...rule }))
|
|
115
132
|
}
|
|
116
133
|
|
|
117
134
|
onMounted(async () => {
|
|
@@ -159,9 +176,14 @@ function collectThresholds(): PlatformAlertThresholdOverrides {
|
|
|
159
176
|
const parsed = fromDisplay(th.field, values.value[th.field])
|
|
160
177
|
if (parsed !== undefined) out[th.field] = parsed
|
|
161
178
|
}
|
|
179
|
+
// Sent whole or omitted whole. An empty list is a real setting and travels as one.
|
|
180
|
+
if (failureKindRules.value !== undefined) out.failureKindRules = failureKindRules.value
|
|
162
181
|
return out
|
|
163
182
|
}
|
|
164
183
|
|
|
184
|
+
/** What the backend would refuse about the per-kind rules, from the helper the editor shows. */
|
|
185
|
+
const ruleFaults = computed(() => failureKindRuleFaults(failureKindRules.value ?? []))
|
|
186
|
+
|
|
165
187
|
async function save() {
|
|
166
188
|
if (!loaded.value) {
|
|
167
189
|
toast.add({ title: t('settings.platformAlerts.notLoaded'), color: 'error' })
|
|
@@ -175,6 +197,22 @@ async function save() {
|
|
|
175
197
|
})
|
|
176
198
|
return
|
|
177
199
|
}
|
|
200
|
+
// Refused here rather than left to the write boundary: the API rejects the WHOLE config blob,
|
|
201
|
+
// so a bad rule would read to the admin as the model policy beside it failing to save. The
|
|
202
|
+
// two faults describe themselves differently because they are fixed differently — a row
|
|
203
|
+
// number is useless advice for a list that is simply too long.
|
|
204
|
+
if (hasFailureKindRuleFaults(ruleFaults.value)) {
|
|
205
|
+
toast.add({
|
|
206
|
+
title: t('settings.platformAlerts.failureKinds.invalidTitle'),
|
|
207
|
+
description: ruleFaults.value.tooMany
|
|
208
|
+
? t('settings.platformAlerts.failureKinds.tooManyRules', { max: MAX_FAILURE_KIND_RULES })
|
|
209
|
+
: t('settings.platformAlerts.failureKinds.invalidRows', {
|
|
210
|
+
rows: ruleFaults.value.rows.join(', '),
|
|
211
|
+
}),
|
|
212
|
+
color: 'error',
|
|
213
|
+
})
|
|
214
|
+
return
|
|
215
|
+
}
|
|
178
216
|
const thresholds = collectThresholds()
|
|
179
217
|
// Each key is omitted rather than nulled when it carries no override: an absent key is what
|
|
180
218
|
// the backend reads as "inherit the deployment default", and a stored null would be a value.
|
|
@@ -210,12 +248,14 @@ function resetAll() {
|
|
|
210
248
|
muted.value = false
|
|
211
249
|
alertWindow.value = ''
|
|
212
250
|
values.value = blankValues()
|
|
251
|
+
failureKindRules.value = undefined
|
|
213
252
|
}
|
|
214
253
|
|
|
215
254
|
const hasOverrides = computed(
|
|
216
255
|
() =>
|
|
217
256
|
muted.value ||
|
|
218
257
|
alertWindow.value !== '' ||
|
|
258
|
+
failureKindRules.value !== undefined ||
|
|
219
259
|
THRESHOLDS.some((th) => values.value[th.field].trim() !== ''),
|
|
220
260
|
)
|
|
221
261
|
</script>
|
|
@@ -288,6 +328,8 @@ const hasOverrides = computed(
|
|
|
288
328
|
</div>
|
|
289
329
|
</div>
|
|
290
330
|
|
|
331
|
+
<AccountFailureKindRules v-model="failureKindRules" />
|
|
332
|
+
|
|
291
333
|
<!--
|
|
292
334
|
A save REPLACES the whole account config and this sheet edits one key of it, so with the
|
|
293
335
|
current config not in hand there is nothing to carry forward. Say so and disable the
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, watch } from 'vue'
|
|
3
3
|
import { onKeyStroke } from '@vueuse/core'
|
|
4
|
-
import type {
|
|
4
|
+
import type { PlatformObservabilityWindow } from '~/types/execution'
|
|
5
5
|
import { formatMs } from '~/utils/observability'
|
|
6
|
+
import { FAILURE_KIND_KEYS, isAgentFailureKind } from '~/utils/failureKinds'
|
|
6
7
|
|
|
7
8
|
// Deployment-level (platform-operator) observability dashboard: the aggregate health of the
|
|
8
9
|
// active account's runs — outcome totals + success rate, a time-bucketed outcome trend, the
|
|
@@ -29,25 +30,12 @@ const WINDOWS: { value: PlatformObservabilityWindow; label: string }[] = [
|
|
|
29
30
|
{ value: '90d', label: t('platformObservability.window.ninetyDays') },
|
|
30
31
|
]
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
dispatch: 'platformObservability.failureKind.dispatch',
|
|
37
|
-
environment: 'platformObservability.failureKind.environment',
|
|
38
|
-
evicted: 'platformObservability.failureKind.evicted',
|
|
39
|
-
timeout: 'platformObservability.failureKind.timeout',
|
|
40
|
-
agent: 'platformObservability.failureKind.agent',
|
|
41
|
-
job_failed: 'platformObservability.failureKind.job_failed',
|
|
42
|
-
rejected: 'platformObservability.failureKind.rejected',
|
|
43
|
-
companion_rejected: 'platformObservability.failureKind.companion_rejected',
|
|
44
|
-
stalled: 'platformObservability.failureKind.stalled',
|
|
45
|
-
cancelled: 'platformObservability.failureKind.cancelled',
|
|
46
|
-
unknown: 'platformObservability.failureKind.unknown',
|
|
47
|
-
}
|
|
33
|
+
// The enum→label map is SHARED with the alert-settings panel (`~/utils/failureKinds`), which
|
|
34
|
+
// offers the same vocabulary as the subject of a per-kind alert rule: one map, so the kind an
|
|
35
|
+
// operator points a page at and the kind this breakdown shows can never be labelled differently.
|
|
36
|
+
// An out-of-enum kind (a retired one on an old row) falls back to its raw code.
|
|
48
37
|
function failureLabel(kind: string): string {
|
|
49
|
-
|
|
50
|
-
return key ? t(key) : kind
|
|
38
|
+
return isAgentFailureKind(kind) ? t(FAILURE_KIND_KEYS[kind]) : kind
|
|
51
39
|
}
|
|
52
40
|
|
|
53
41
|
const DAY_MS = 24 * 60 * 60 * 1000
|
package/app/types/execution.ts
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { PlatformFailureKindRule } from '~/types/execution'
|
|
3
|
+
import {
|
|
4
|
+
AGENT_FAILURE_KINDS,
|
|
5
|
+
FAILURE_KIND_KEYS,
|
|
6
|
+
failureKindRuleFaults,
|
|
7
|
+
hasFailureKindRuleFaults,
|
|
8
|
+
isAgentFailureKind,
|
|
9
|
+
MAX_FAILURE_KIND_RULES,
|
|
10
|
+
} from '~/utils/failureKinds'
|
|
11
|
+
|
|
12
|
+
describe('the failure-kind vocabulary', () => {
|
|
13
|
+
it('offers exactly the kinds the contract declares, and labels every one', () => {
|
|
14
|
+
// The map is what both the dashboard breakdown and the alert-rule picker read, so a kind
|
|
15
|
+
// added to the contract without a label here would render a raw code in both.
|
|
16
|
+
expect(AGENT_FAILURE_KINDS.length).toBeGreaterThan(0)
|
|
17
|
+
for (const kind of AGENT_FAILURE_KINDS) expect(FAILURE_KIND_KEYS[kind]).toBeTruthy()
|
|
18
|
+
expect(Object.keys(FAILURE_KIND_KEYS).sort()).toEqual([...AGENT_FAILURE_KINDS].sort())
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('recognises a current kind and refuses a retired or mistyped one', () => {
|
|
22
|
+
// Re-exported from `@cat-factory/contracts` rather than reimplemented, because the backend
|
|
23
|
+
// asks the identical question of an operator-typed kind in the env var. Pinned through the
|
|
24
|
+
// SPA's own import path so the re-export cannot quietly disappear.
|
|
25
|
+
expect(isAgentFailureKind('evicted')).toBe(true)
|
|
26
|
+
// The case the predicate exists for: a kind that a release retired still arrives on stored
|
|
27
|
+
// rules and old run rows, and must render as itself rather than as `undefined` or as
|
|
28
|
+
// whichever current member a guess landed on.
|
|
29
|
+
expect(isAgentFailureKind('evicetd')).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('never offers more kinds than a rule list may hold, so the cap cannot bind first', () => {
|
|
33
|
+
// What makes "one rule per kind" the binding limit in the editor, and therefore what lets
|
|
34
|
+
// the add button simply stop rather than seeding a row the contract would refuse.
|
|
35
|
+
expect(AGENT_FAILURE_KINDS.length).toBeLessThanOrEqual(MAX_FAILURE_KIND_RULES)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('failureKindRuleFaults', () => {
|
|
40
|
+
const rule = (over: Partial<PlatformFailureKindRule> = {}): PlatformFailureKindRule => ({
|
|
41
|
+
kind: 'evicted',
|
|
42
|
+
maxShare: 0.05,
|
|
43
|
+
...over,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('accepts a well-formed set of rules', () => {
|
|
47
|
+
expect(
|
|
48
|
+
failureKindRuleFaults([rule(), rule({ kind: 'timeout', maxShare: 1, minCount: 3 })]),
|
|
49
|
+
).toEqual({ rows: [], tooMany: false })
|
|
50
|
+
expect(failureKindRuleFaults([])).toEqual({ rows: [], tooMany: false })
|
|
51
|
+
expect(hasFailureKindRuleFaults(failureKindRuleFaults([rule()]))).toBe(false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('names the 1-based rows the backend would refuse', () => {
|
|
55
|
+
// The share bounds mirror the contract: 0 is satisfied by any distribution (including a kind
|
|
56
|
+
// that never occurred), and there is nothing above "all of them".
|
|
57
|
+
expect(failureKindRuleFaults([rule({ maxShare: 0 })]).rows).toEqual([1])
|
|
58
|
+
expect(failureKindRuleFaults([rule({ maxShare: 1.5 })]).rows).toEqual([1])
|
|
59
|
+
expect(failureKindRuleFaults([rule(), rule({ kind: 'timeout', minCount: 0 })]).rows).toEqual([
|
|
60
|
+
2,
|
|
61
|
+
])
|
|
62
|
+
expect(failureKindRuleFaults([rule({ kind: 'timeout', minCount: 2.5 })]).rows).toEqual([1])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('names BOTH rows of a duplicated kind, since either could be the one to fix', () => {
|
|
66
|
+
expect(failureKindRuleFaults([rule(), rule({ maxShare: 0.5 })]).rows).toEqual([1, 2])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('reports an over-long list as its own fault, not as a bad row', () => {
|
|
70
|
+
// "There are too many rules" is fixed by deleting any of them, so pointing at a row number
|
|
71
|
+
// would name a row that is perfectly well-formed. The two faults travel separately for that
|
|
72
|
+
// reason, and either one alone must still stop the save.
|
|
73
|
+
const many = Array.from({ length: MAX_FAILURE_KIND_RULES + 1 }, (_, i) =>
|
|
74
|
+
rule({ kind: `kind${i}` }),
|
|
75
|
+
)
|
|
76
|
+
const faults = failureKindRuleFaults(many)
|
|
77
|
+
expect(faults).toEqual({ rows: [], tooMany: true })
|
|
78
|
+
expect(hasFailureKindRuleFaults(faults)).toBe(true)
|
|
79
|
+
// Exactly at the cap is fine — the bound is inclusive, as `v.maxLength` is.
|
|
80
|
+
expect(failureKindRuleFaults(many.slice(0, MAX_FAILURE_KIND_RULES)).tooMany).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Shared presentation for the run FAILURE TAXONOMY — the vocabulary the operator dashboard
|
|
3
|
+
// renders as a breakdown and the alert settings panel offers as the subject of a per-kind
|
|
4
|
+
// rule. Two surfaces answering about the same set, so the enum→key map lives here once: a
|
|
5
|
+
// second copy is a map that drifts, and the surface that drifts is the one where an operator
|
|
6
|
+
// picks the kind a page is wired to.
|
|
7
|
+
//
|
|
8
|
+
// The map is an exhaustive `Record<AgentFailureKind, …>`, so adding a kind to the contract
|
|
9
|
+
// fails the typecheck here rather than rendering a raw code in either place.
|
|
10
|
+
//
|
|
11
|
+
// What is PRESENTATION lives here; what is a RULE comes from `@cat-factory/contracts` and is
|
|
12
|
+
// re-exported so a component has one import. `isAgentFailureKind` in particular is not the
|
|
13
|
+
// SPA's to own: the backend asks the identical question of an operator-typed kind in
|
|
14
|
+
// `PLATFORM_ALERTS_FAILURE_KIND_RATES`, and two copies of "which kinds exist" is the pair that
|
|
15
|
+
// drifts the moment one is retired.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
import { agentFailureKindSchema, MAX_FAILURE_KIND_RULES } from '@cat-factory/contracts'
|
|
19
|
+
import type { AgentFailureKind, PlatformFailureKindRule } from '~/types/execution'
|
|
20
|
+
|
|
21
|
+
export { isAgentFailureKind, MAX_FAILURE_KIND_RULES } from '@cat-factory/contracts'
|
|
22
|
+
|
|
23
|
+
/** i18n key per failure kind. */
|
|
24
|
+
export const FAILURE_KIND_KEYS: Record<AgentFailureKind, string> = {
|
|
25
|
+
preflight: 'platformObservability.failureKind.preflight',
|
|
26
|
+
dispatch: 'platformObservability.failureKind.dispatch',
|
|
27
|
+
environment: 'platformObservability.failureKind.environment',
|
|
28
|
+
evicted: 'platformObservability.failureKind.evicted',
|
|
29
|
+
timeout: 'platformObservability.failureKind.timeout',
|
|
30
|
+
agent: 'platformObservability.failureKind.agent',
|
|
31
|
+
job_failed: 'platformObservability.failureKind.job_failed',
|
|
32
|
+
rejected: 'platformObservability.failureKind.rejected',
|
|
33
|
+
companion_rejected: 'platformObservability.failureKind.companion_rejected',
|
|
34
|
+
stalled: 'platformObservability.failureKind.stalled',
|
|
35
|
+
cancelled: 'platformObservability.failureKind.cancelled',
|
|
36
|
+
unknown: 'platformObservability.failureKind.unknown',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The kinds a human may be OFFERED, in the contract's own declared order.
|
|
41
|
+
*
|
|
42
|
+
* Read off the picklist rather than restated, so the choices a rule can be written against are
|
|
43
|
+
* the choices the backend recognises. The DISPLAYED set is deliberately not the same thing as
|
|
44
|
+
* the set of values that may ARRIVE: a stored rule or a persisted run row can still name a kind
|
|
45
|
+
* a later release retired, which `isAgentFailureKind` is for.
|
|
46
|
+
*/
|
|
47
|
+
export const AGENT_FAILURE_KINDS = agentFailureKindSchema.options as readonly AgentFailureKind[]
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether a stored per-kind rule list would be REFUSED by the contract, and why.
|
|
51
|
+
*
|
|
52
|
+
* One implementation shared by the editor and the save path, because they are the same question
|
|
53
|
+
* asked twice: a fault the editor flags but the save path does not is a save that fails on the
|
|
54
|
+
* WHOLE settings blob, for a reason the sheet never showed and about a sibling setting the admin
|
|
55
|
+
* never touched.
|
|
56
|
+
*
|
|
57
|
+
* The two faults are kept APART rather than folded into one list of bad rows, because they need
|
|
58
|
+
* different fixes and one of them belongs to no row in particular: "row 3 is malformed" is fixed
|
|
59
|
+
* in row 3, while "there are more rules than the contract allows" is fixed by deleting any of
|
|
60
|
+
* them. Reporting the second as a row number would point at a row that is perfectly fine.
|
|
61
|
+
*
|
|
62
|
+
* Deliberately mirrors the contract rather than re-deciding anything: a share in (0, 1], a whole
|
|
63
|
+
* minimum count of 1 or more when one is set, at most one rule per kind, and at most
|
|
64
|
+
* {@link MAX_FAILURE_KIND_RULES} rules.
|
|
65
|
+
*/
|
|
66
|
+
export interface FailureKindRuleFaults {
|
|
67
|
+
/** 1-based positions of individual rules the contract would refuse. */
|
|
68
|
+
rows: number[]
|
|
69
|
+
/** Whether the LIST is over the contract's cap, which is no single row's fault. */
|
|
70
|
+
tooMany: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function failureKindRuleFaults(
|
|
74
|
+
rules: readonly PlatformFailureKindRule[],
|
|
75
|
+
): FailureKindRuleFaults {
|
|
76
|
+
const counts = new Map<string, number>()
|
|
77
|
+
for (const rule of rules) counts.set(rule.kind, (counts.get(rule.kind) ?? 0) + 1)
|
|
78
|
+
const rows = rules.flatMap((rule, index) => {
|
|
79
|
+
const shareOk = rule.maxShare > 0 && rule.maxShare <= 1
|
|
80
|
+
const countOk =
|
|
81
|
+
rule.minCount === undefined || (Number.isInteger(rule.minCount) && rule.minCount >= 1)
|
|
82
|
+
const unique = (counts.get(rule.kind) ?? 0) === 1
|
|
83
|
+
return shareOk && countOk && unique ? [] : [index + 1]
|
|
84
|
+
})
|
|
85
|
+
return { rows, tooMany: rules.length > MAX_FAILURE_KIND_RULES }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Whether a list is savable at all — the one question both call sites actually branch on. */
|
|
89
|
+
export function hasFailureKindRuleFaults(faults: FailureKindRuleFaults): boolean {
|
|
90
|
+
return faults.rows.length > 0 || faults.tooMany
|
|
91
|
+
}
|
package/i18n/locales/de.json
CHANGED
|
@@ -1174,7 +1174,28 @@
|
|
|
1174
1174
|
"maxSweepFailures": "Aufeinanderfolgende fehlgeschlagene Durchläufe einer Hintergrundprüfung."
|
|
1175
1175
|
},
|
|
1176
1176
|
"notLoaded": "Die aktuellen Einstellungen dieses Kontos konnten nicht geladen werden; ein Speichern würde die übrigen überschreiben. Bitte vor dem Bearbeiten neu laden.",
|
|
1177
|
-
"invalidNumbers": "Diese Obergrenzen sind keine Zahlen"
|
|
1177
|
+
"invalidNumbers": "Diese Obergrenzen sind keine Zahlen",
|
|
1178
|
+
"failureKinds": {
|
|
1179
|
+
"label": "Regeln pro Fehlerart",
|
|
1180
|
+
"description": "Alarm auslösen, wenn eine Fehlerart den Anteil an Fehlschlägen erreicht, den Sie ihr zugestehen. Die Obergrenze für die dominante Ursache oben fragt, ob eine Art alle anderen überlagert; diese Regeln fragen, ob eine benannte Art das erreicht, was diese Installation von ihr toleriert.",
|
|
1181
|
+
"overrideLabel": "Eigene Regeln pro Fehlerart für dieses Konto festlegen",
|
|
1182
|
+
"overrideHint": "Aus: Das Konto verwendet die Regeln der Installation. Ein: Die Liste unten ersetzt sie vollständig, und eine leere Liste bedeutet gar keine Regeln pro Fehlerart.",
|
|
1183
|
+
"empty": "Keine Regeln. Dieses Konto löst den Alarm pro Fehlerart nicht aus.",
|
|
1184
|
+
"kindLabel": "Fehlerart",
|
|
1185
|
+
"shareLabel": "Anteil an Fehlschlägen (%)",
|
|
1186
|
+
"shareHint": "Die Regel löst ab diesem Anteil aus.",
|
|
1187
|
+
"minCountLabel": "Mindestanzahl Fehlschläge",
|
|
1188
|
+
"minCountPlaceholder": "1",
|
|
1189
|
+
"minCountHint": "Wie viele Fehlschläge dieser Art im Zeitfenster nötig sind, bevor die Regel auslösen kann. Ohne diesen Wert ist ein niedriger Anteil überempfindlich: Fünf Läufe mit einer Verdrängung sind bereits 20 %.",
|
|
1190
|
+
"addRule": "Regel hinzufügen",
|
|
1191
|
+
"allKindsCovered": "Jede Fehlerart hat bereits eine Regel.",
|
|
1192
|
+
"removeRule": "Regel entfernen",
|
|
1193
|
+
"duplicateKind": "Jede Fehlerart darf höchstens eine Regel haben.",
|
|
1194
|
+
"tooManyRules": "Höchstens {max} Regeln pro Fehlerart. Entfernen Sie einige vor dem Speichern.",
|
|
1195
|
+
"unknownKind": "{kind} (unbekannt)",
|
|
1196
|
+
"invalidTitle": "Diese Regeln pro Fehlerart können nicht gespeichert werden",
|
|
1197
|
+
"invalidRows": "Prüfen Sie die Regeln {rows}: Der Anteil muss über 0 und höchstens 100 liegen, eine Mindestanzahl muss eine ganze Zahl ab 1 sein, und jede Art darf nur einmal vorkommen."
|
|
1198
|
+
}
|
|
1178
1199
|
}
|
|
1179
1200
|
},
|
|
1180
1201
|
"inspector": {
|
package/i18n/locales/en.json
CHANGED
|
@@ -3648,7 +3648,28 @@
|
|
|
3648
3648
|
"maxSweepFailures": "Consecutive failed passes of one background sweep."
|
|
3649
3649
|
},
|
|
3650
3650
|
"notLoaded": "This account's current settings could not be loaded, so saving would overwrite the rest of them. Reload before editing.",
|
|
3651
|
-
"invalidNumbers": "These ceilings are not numbers"
|
|
3651
|
+
"invalidNumbers": "These ceilings are not numbers",
|
|
3652
|
+
"failureKinds": {
|
|
3653
|
+
"label": "Per-failure-kind rules",
|
|
3654
|
+
"description": "Page when one failure kind reaches the share of failures you allow it. The dominant-cause ceiling above asks whether one kind is swamping the rest; these ask whether a named kind has reached what this deployment tolerates from it.",
|
|
3655
|
+
"overrideLabel": "Set this account's own per-kind rules",
|
|
3656
|
+
"overrideHint": "Off, the account uses the rules the deployment configured. On, the list below replaces them entirely, and an empty list means no per-kind rules at all.",
|
|
3657
|
+
"empty": "No rules. This account will not raise the per-kind alert.",
|
|
3658
|
+
"kindLabel": "Failure kind",
|
|
3659
|
+
"shareLabel": "Share of failures (%)",
|
|
3660
|
+
"shareHint": "The rule fires at this share or above.",
|
|
3661
|
+
"minCountLabel": "Minimum failures",
|
|
3662
|
+
"minCountPlaceholder": "1",
|
|
3663
|
+
"minCountHint": "How many failures of that kind the window needs before the rule can fire. A low share is a hair trigger without it: five runs with one eviction is already 20%.",
|
|
3664
|
+
"addRule": "Add rule",
|
|
3665
|
+
"allKindsCovered": "Every failure kind already has a rule.",
|
|
3666
|
+
"removeRule": "Remove rule",
|
|
3667
|
+
"duplicateKind": "Each failure kind may carry at most one rule.",
|
|
3668
|
+
"tooManyRules": "At most {max} per-kind rules. Remove some before saving.",
|
|
3669
|
+
"unknownKind": "{kind} (unrecognised)",
|
|
3670
|
+
"invalidTitle": "These per-kind rules cannot be saved",
|
|
3671
|
+
"invalidRows": "Check rules {rows}: the share must be above 0 and at most 100, a minimum count must be a whole number of 1 or more, and each kind may appear once."
|
|
3672
|
+
}
|
|
3652
3673
|
}
|
|
3653
3674
|
},
|
|
3654
3675
|
"providers": {
|
package/i18n/locales/es.json
CHANGED
|
@@ -3541,7 +3541,28 @@
|
|
|
3541
3541
|
"maxSweepFailures": "Pasadas fallidas consecutivas de un barrido en segundo plano."
|
|
3542
3542
|
},
|
|
3543
3543
|
"notLoaded": "No se pudieron cargar los ajustes actuales de esta cuenta, así que guardar sobrescribiría el resto. Vuelve a cargar antes de editar.",
|
|
3544
|
-
"invalidNumbers": "Estos límites no son números"
|
|
3544
|
+
"invalidNumbers": "Estos límites no son números",
|
|
3545
|
+
"failureKinds": {
|
|
3546
|
+
"label": "Reglas por tipo de fallo",
|
|
3547
|
+
"description": "Avisa cuando un tipo de fallo alcanza la proporción de fallos que le permites. El límite de causa dominante de arriba pregunta si un tipo está desbordando al resto; estas reglas preguntan si un tipo concreto ha alcanzado lo que este despliegue tolera de él.",
|
|
3548
|
+
"overrideLabel": "Definir reglas por tipo propias de esta cuenta",
|
|
3549
|
+
"overrideHint": "Desactivado, la cuenta usa las reglas configuradas en el despliegue. Activado, la lista de abajo las sustituye por completo, y una lista vacía significa ninguna regla por tipo.",
|
|
3550
|
+
"empty": "Sin reglas. Esta cuenta no generará la alerta por tipo de fallo.",
|
|
3551
|
+
"kindLabel": "Tipo de fallo",
|
|
3552
|
+
"shareLabel": "Proporción de fallos (%)",
|
|
3553
|
+
"shareHint": "La regla se activa a partir de esta proporción.",
|
|
3554
|
+
"minCountLabel": "Fallos mínimos",
|
|
3555
|
+
"minCountPlaceholder": "1",
|
|
3556
|
+
"minCountHint": "Cuántos fallos de ese tipo necesita la ventana antes de que la regla pueda dispararse. Sin esto, una proporción baja salta a la mínima: cinco ejecuciones con un desalojo ya son el 20 %.",
|
|
3557
|
+
"addRule": "Añadir regla",
|
|
3558
|
+
"allKindsCovered": "Cada tipo de fallo ya tiene una regla.",
|
|
3559
|
+
"removeRule": "Eliminar regla",
|
|
3560
|
+
"duplicateKind": "Cada tipo de fallo admite como máximo una regla.",
|
|
3561
|
+
"tooManyRules": "Como máximo {max} reglas por tipo. Elimina algunas antes de guardar.",
|
|
3562
|
+
"unknownKind": "{kind} (no reconocido)",
|
|
3563
|
+
"invalidTitle": "Estas reglas por tipo no se pueden guardar",
|
|
3564
|
+
"invalidRows": "Revisa las reglas {rows}: la proporción debe ser mayor que 0 y como máximo 100, el mínimo debe ser un número entero de 1 o más, y cada tipo puede aparecer una sola vez."
|
|
3565
|
+
}
|
|
3545
3566
|
}
|
|
3546
3567
|
},
|
|
3547
3568
|
"providers": {
|
package/i18n/locales/fr.json
CHANGED
|
@@ -3541,7 +3541,28 @@
|
|
|
3541
3541
|
"maxSweepFailures": "Passages consécutifs en échec d'un balayage en arrière-plan."
|
|
3542
3542
|
},
|
|
3543
3543
|
"notLoaded": "Les paramètres actuels de ce compte n'ont pas pu être chargés ; enregistrer écraserait les autres. Rechargez avant de modifier.",
|
|
3544
|
-
"invalidNumbers": "Ces plafonds ne sont pas des nombres"
|
|
3544
|
+
"invalidNumbers": "Ces plafonds ne sont pas des nombres",
|
|
3545
|
+
"failureKinds": {
|
|
3546
|
+
"label": "Règles par type d'échec",
|
|
3547
|
+
"description": "Alerter quand un type d'échec atteint la part d'échecs que vous lui accordez. Le plafond de cause dominante ci-dessus demande si un type submerge les autres ; ces règles demandent si un type nommé a atteint ce que ce déploiement tolère de lui.",
|
|
3548
|
+
"overrideLabel": "Définir les règles par type propres à ce compte",
|
|
3549
|
+
"overrideHint": "Désactivé, le compte utilise les règles configurées par le déploiement. Activé, la liste ci-dessous les remplace entièrement, et une liste vide signifie aucune règle par type.",
|
|
3550
|
+
"empty": "Aucune règle. Ce compte ne déclenchera pas l'alerte par type d'échec.",
|
|
3551
|
+
"kindLabel": "Type d'échec",
|
|
3552
|
+
"shareLabel": "Part des échecs (%)",
|
|
3553
|
+
"shareHint": "La règle se déclenche à partir de cette part.",
|
|
3554
|
+
"minCountLabel": "Échecs minimum",
|
|
3555
|
+
"minCountPlaceholder": "1",
|
|
3556
|
+
"minCountHint": "Combien d'échecs de ce type la fenêtre doit compter avant que la règle puisse se déclencher. Sans cela, une part faible se déclenche pour un rien : cinq exécutions avec une éviction, c'est déjà 20 %.",
|
|
3557
|
+
"addRule": "Ajouter une règle",
|
|
3558
|
+
"allKindsCovered": "Chaque type d'échec a déjà une règle.",
|
|
3559
|
+
"removeRule": "Supprimer la règle",
|
|
3560
|
+
"duplicateKind": "Chaque type d'échec ne peut porter qu'une seule règle.",
|
|
3561
|
+
"tooManyRules": "Au plus {max} règles par type. Supprimez-en avant d’enregistrer.",
|
|
3562
|
+
"unknownKind": "{kind} (non reconnu)",
|
|
3563
|
+
"invalidTitle": "Ces règles par type ne peuvent pas être enregistrées",
|
|
3564
|
+
"invalidRows": "Vérifiez les règles {rows} : la part doit être supérieure à 0 et au plus 100, un minimum doit être un entier supérieur ou égal à 1, et chaque type ne peut apparaître qu'une fois."
|
|
3565
|
+
}
|
|
3545
3566
|
}
|
|
3546
3567
|
},
|
|
3547
3568
|
"providers": {
|
package/i18n/locales/he.json
CHANGED
|
@@ -3541,7 +3541,28 @@
|
|
|
3541
3541
|
"maxSweepFailures": "מעברים כושלים רצופים של סריקת רקע אחת."
|
|
3542
3542
|
},
|
|
3543
3543
|
"notLoaded": "לא ניתן היה לטעון את ההגדרות הנוכחיות של חשבון זה, ולכן שמירה תדרוס את שאר ההגדרות. יש לטעון מחדש לפני עריכה.",
|
|
3544
|
-
"invalidNumbers": "תקרות אלה אינן מספרים"
|
|
3544
|
+
"invalidNumbers": "תקרות אלה אינן מספרים",
|
|
3545
|
+
"failureKinds": {
|
|
3546
|
+
"label": "כללים לפי סוג כשל",
|
|
3547
|
+
"description": "התרעה כאשר סוג כשל מגיע לחלק בכשלים שהוגדר לו. תקרת הסיבה הדומיננטית שלמעלה שואלת אם סוג אחד מציף את כל השאר; הכללים כאן שואלים אם סוג מסוים הגיע למה שהפריסה הזאת מוכנה לספוג ממנו.",
|
|
3548
|
+
"overrideLabel": "הגדרת כללים משלו לחשבון הזה",
|
|
3549
|
+
"overrideHint": "כבוי, החשבון משתמש בכללים שהוגדרו בפריסה. דלוק, הרשימה שלמטה מחליפה אותם לחלוטין, ורשימה ריקה משמעה שאין כללים לפי סוג כלל.",
|
|
3550
|
+
"empty": "אין כללים. החשבון הזה לא יפיק התרעה לפי סוג כשל.",
|
|
3551
|
+
"kindLabel": "סוג כשל",
|
|
3552
|
+
"shareLabel": "חלק מהכשלים (%)",
|
|
3553
|
+
"shareHint": "הכלל פועל מהחלק הזה ומעלה.",
|
|
3554
|
+
"minCountLabel": "מינימום כשלים",
|
|
3555
|
+
"minCountPlaceholder": "1",
|
|
3556
|
+
"minCountHint": "כמה כשלים מהסוג הזה נדרשים בחלון לפני שהכלל יכול לפעול. בלי זה, אחוז נמוך רגיש מדי: חמש ריצות עם פינוי אחד הן כבר 20%.",
|
|
3557
|
+
"addRule": "הוספת כלל",
|
|
3558
|
+
"allKindsCovered": "לכל סוג כשל כבר יש כלל.",
|
|
3559
|
+
"removeRule": "הסרת כלל",
|
|
3560
|
+
"duplicateKind": "לכל סוג כשל אפשר להגדיר כלל אחד לכל היותר.",
|
|
3561
|
+
"tooManyRules": "לכל היותר {max} כללים לפי סוג. הסירו חלק לפני השמירה.",
|
|
3562
|
+
"unknownKind": "{kind} (לא מזוהה)",
|
|
3563
|
+
"invalidTitle": "לא ניתן לשמור את הכללים האלה",
|
|
3564
|
+
"invalidRows": "בדקו את הכללים {rows}: החלק חייב להיות גדול מ-0 ולכל היותר 100, מינימום חייב להיות מספר שלם מ-1 ומעלה, וכל סוג יכול להופיע פעם אחת בלבד."
|
|
3565
|
+
}
|
|
3545
3566
|
}
|
|
3546
3567
|
},
|
|
3547
3568
|
"providers": {
|
package/i18n/locales/it.json
CHANGED
|
@@ -1174,7 +1174,28 @@
|
|
|
1174
1174
|
"maxSweepFailures": "Passaggi falliti consecutivi di una scansione in background."
|
|
1175
1175
|
},
|
|
1176
1176
|
"notLoaded": "Non è stato possibile caricare le impostazioni correnti di questo account, quindi il salvataggio sovrascriverebbe le altre. Ricarica prima di modificare.",
|
|
1177
|
-
"invalidNumbers": "Questi limiti non sono numeri"
|
|
1177
|
+
"invalidNumbers": "Questi limiti non sono numeri",
|
|
1178
|
+
"failureKinds": {
|
|
1179
|
+
"label": "Regole per tipo di errore",
|
|
1180
|
+
"description": "Avvisa quando un tipo di errore raggiunge la quota di errori che gli concedi. Il tetto sulla causa dominante qui sopra chiede se un tipo sta sommergendo gli altri; queste regole chiedono se un tipo specifico ha raggiunto ciò che questo deployment tollera da esso.",
|
|
1181
|
+
"overrideLabel": "Imposta regole per tipo proprie di questo account",
|
|
1182
|
+
"overrideHint": "Disattivato, l'account usa le regole configurate dal deployment. Attivato, l'elenco qui sotto le sostituisce del tutto, e un elenco vuoto significa nessuna regola per tipo.",
|
|
1183
|
+
"empty": "Nessuna regola. Questo account non genererà l'avviso per tipo di errore.",
|
|
1184
|
+
"kindLabel": "Tipo di errore",
|
|
1185
|
+
"shareLabel": "Quota di errori (%)",
|
|
1186
|
+
"shareHint": "La regola scatta da questa quota in su.",
|
|
1187
|
+
"minCountLabel": "Errori minimi",
|
|
1188
|
+
"minCountPlaceholder": "1",
|
|
1189
|
+
"minCountHint": "Quanti errori di quel tipo servono nella finestra prima che la regola possa scattare. Senza, una quota bassa scatta per un nulla: cinque esecuzioni con uno sfratto sono già il 20%.",
|
|
1190
|
+
"addRule": "Aggiungi regola",
|
|
1191
|
+
"allKindsCovered": "Ogni tipo di errore ha già una regola.",
|
|
1192
|
+
"removeRule": "Rimuovi regola",
|
|
1193
|
+
"duplicateKind": "Ogni tipo di errore può avere al massimo una regola.",
|
|
1194
|
+
"tooManyRules": "Al massimo {max} regole per tipo. Rimuovine alcune prima di salvare.",
|
|
1195
|
+
"unknownKind": "{kind} (non riconosciuto)",
|
|
1196
|
+
"invalidTitle": "Queste regole per tipo non possono essere salvate",
|
|
1197
|
+
"invalidRows": "Controlla le regole {rows}: la quota deve essere maggiore di 0 e al massimo 100, il minimo deve essere un numero intero da 1 in su, e ogni tipo può comparire una volta sola."
|
|
1198
|
+
}
|
|
1178
1199
|
}
|
|
1179
1200
|
},
|
|
1180
1201
|
"inspector": {
|
package/i18n/locales/ja.json
CHANGED
|
@@ -3541,7 +3541,28 @@
|
|
|
3541
3541
|
"maxSweepFailures": "ひとつのバックグラウンド監視が連続して失敗した回数。"
|
|
3542
3542
|
},
|
|
3543
3543
|
"notLoaded": "このアカウントの現在の設定を読み込めなかったため、保存すると他の設定が上書きされます。編集する前に再読み込みしてください。",
|
|
3544
|
-
"invalidNumbers": "これらの上限値が数値ではありません"
|
|
3544
|
+
"invalidNumbers": "これらの上限値が数値ではありません",
|
|
3545
|
+
"failureKinds": {
|
|
3546
|
+
"label": "失敗種別ごとのルール",
|
|
3547
|
+
"description": "ある失敗種別が、許容する失敗の割合に達したときに通知します。上のドミナント原因のしきい値は「1 つの種別が他をのみ込んでいるか」を問うものですが、これらのルールは「名指しした種別がこのデプロイで許容する範囲に達したか」を問います。",
|
|
3548
|
+
"overrideLabel": "このアカウント独自の種別ルールを設定する",
|
|
3549
|
+
"overrideHint": "オフの場合、アカウントはデプロイ側で設定されたルールを使います。オンの場合、下の一覧がそれを完全に置き換え、空の一覧は「種別ルールなし」を意味します。",
|
|
3550
|
+
"empty": "ルールがありません。このアカウントでは種別ごとのアラートは発報しません。",
|
|
3551
|
+
"kindLabel": "失敗種別",
|
|
3552
|
+
"shareLabel": "失敗に占める割合 (%)",
|
|
3553
|
+
"shareHint": "この割合以上でルールが発報します。",
|
|
3554
|
+
"minCountLabel": "最小失敗件数",
|
|
3555
|
+
"minCountPlaceholder": "1",
|
|
3556
|
+
"minCountHint": "ルールが発報できるようになるまでに、その種別の失敗が対象期間に何件必要かを指定します。これがないと低い割合は過敏になります。5 件の実行のうち 1 件が退避なら、それだけで 20% です。",
|
|
3557
|
+
"addRule": "ルールを追加",
|
|
3558
|
+
"allKindsCovered": "すべての失敗種別にルールが設定済みです。",
|
|
3559
|
+
"removeRule": "ルールを削除",
|
|
3560
|
+
"duplicateKind": "1 つの失敗種別に設定できるルールは 1 つまでです。",
|
|
3561
|
+
"tooManyRules": "種別ごとのルールは最大 {max} 件です。保存する前にいくつか削除してください。",
|
|
3562
|
+
"unknownKind": "{kind}(未知の種別)",
|
|
3563
|
+
"invalidTitle": "これらの種別ルールは保存できません",
|
|
3564
|
+
"invalidRows": "ルール {rows} を確認してください。割合は 0 より大きく 100 以下、最小件数は 1 以上の整数、各種別は 1 回だけ指定できます。"
|
|
3565
|
+
}
|
|
3545
3566
|
}
|
|
3546
3567
|
},
|
|
3547
3568
|
"providers": {
|
package/i18n/locales/pl.json
CHANGED
|
@@ -3541,7 +3541,28 @@
|
|
|
3541
3541
|
"maxSweepFailures": "Kolejne nieudane przebiegi jednego zadania w tle."
|
|
3542
3542
|
},
|
|
3543
3543
|
"notLoaded": "Nie udało się wczytać bieżących ustawień tego konta, więc zapis nadpisałby pozostałe. Odśwież przed edycją.",
|
|
3544
|
-
"invalidNumbers": "Te limity nie są liczbami"
|
|
3544
|
+
"invalidNumbers": "Te limity nie są liczbami",
|
|
3545
|
+
"failureKinds": {
|
|
3546
|
+
"label": "Reguły dla poszczególnych rodzajów awarii",
|
|
3547
|
+
"description": "Alarm, gdy jeden rodzaj awarii osiągnie dopuszczony dla niego udział w awariach. Powyższy limit dominującej przyczyny pyta, czy jeden rodzaj przytłacza pozostałe; te reguły pytają, czy wskazany rodzaj osiągnął to, co to wdrożenie z jego strony toleruje.",
|
|
3548
|
+
"overrideLabel": "Ustaw własne reguły tego konta",
|
|
3549
|
+
"overrideHint": "Wyłączone: konto korzysta z reguł skonfigurowanych we wdrożeniu. Włączone: poniższa lista zastępuje je w całości, a pusta lista oznacza brak reguł dla rodzajów awarii.",
|
|
3550
|
+
"empty": "Brak reguł. To konto nie zgłosi alertu dla rodzaju awarii.",
|
|
3551
|
+
"kindLabel": "Rodzaj awarii",
|
|
3552
|
+
"shareLabel": "Udział w awariach (%)",
|
|
3553
|
+
"shareHint": "Reguła uruchamia się od tego udziału wzwyż.",
|
|
3554
|
+
"minCountLabel": "Minimalna liczba awarii",
|
|
3555
|
+
"minCountPlaceholder": "1",
|
|
3556
|
+
"minCountHint": "Ile awarii tego rodzaju musi wystąpić w oknie, zanim reguła zadziała. Bez tego niski udział jest nadwrażliwy: pięć uruchomień z jednym wywłaszczeniem to już 20%.",
|
|
3557
|
+
"addRule": "Dodaj regułę",
|
|
3558
|
+
"allKindsCovered": "Każdy rodzaj awarii ma już regułę.",
|
|
3559
|
+
"removeRule": "Usuń regułę",
|
|
3560
|
+
"duplicateKind": "Każdy rodzaj awarii może mieć najwyżej jedną regułę.",
|
|
3561
|
+
"tooManyRules": "Najwyżej {max} reguł na rodzaj. Usuń część przed zapisaniem.",
|
|
3562
|
+
"unknownKind": "{kind} (nierozpoznany)",
|
|
3563
|
+
"invalidTitle": "Tych reguł nie można zapisać",
|
|
3564
|
+
"invalidRows": "Sprawdź reguły {rows}: udział musi być większy niż 0 i najwyżej 100, minimum musi być liczbą całkowitą od 1 w górę, a każdy rodzaj może wystąpić tylko raz."
|
|
3565
|
+
}
|
|
3545
3566
|
}
|
|
3546
3567
|
},
|
|
3547
3568
|
"providers": {
|
package/i18n/locales/tr.json
CHANGED
|
@@ -3541,7 +3541,28 @@
|
|
|
3541
3541
|
"maxSweepFailures": "Bir arka plan taramasının üst üste başarısız geçişi."
|
|
3542
3542
|
},
|
|
3543
3543
|
"notLoaded": "Bu hesabın mevcut ayarları yüklenemedi; kaydetmek diğerlerinin üzerine yazar. Düzenlemeden önce yeniden yükleyin.",
|
|
3544
|
-
"invalidNumbers": "Bu üst sınırlar sayı değil"
|
|
3544
|
+
"invalidNumbers": "Bu üst sınırlar sayı değil",
|
|
3545
|
+
"failureKinds": {
|
|
3546
|
+
"label": "Hata türü başına kurallar",
|
|
3547
|
+
"description": "Bir hata türü, ona tanıdığınız hata payına ulaştığında uyarır. Yukarıdaki baskın neden tavanı bir türün diğerlerini bastırıp bastırmadığını sorar; buradaki kurallar ise adı geçen bir türün bu kurulumun ondan kabul ettiği sınıra ulaşıp ulaşmadığını sorar.",
|
|
3548
|
+
"overrideLabel": "Bu hesaba özel tür kuralları tanımla",
|
|
3549
|
+
"overrideHint": "Kapalıyken hesap, kurulumda yapılandırılan kuralları kullanır. Açıkken aşağıdaki liste onların yerini tamamen alır ve boş liste hiç tür kuralı olmaması demektir.",
|
|
3550
|
+
"empty": "Kural yok. Bu hesap tür bazlı uyarı üretmeyecek.",
|
|
3551
|
+
"kindLabel": "Hata türü",
|
|
3552
|
+
"shareLabel": "Hatalar içindeki pay (%)",
|
|
3553
|
+
"shareHint": "Kural bu paydan itibaren tetiklenir.",
|
|
3554
|
+
"minCountLabel": "En az hata sayısı",
|
|
3555
|
+
"minCountPlaceholder": "1",
|
|
3556
|
+
"minCountHint": "Kuralın tetiklenebilmesi için pencerede o türden kaç hata gerektiğini belirler. Bu olmadan düşük bir pay çok hassas kalır: beş çalıştırmada bir tahliye zaten %20 eder.",
|
|
3557
|
+
"addRule": "Kural ekle",
|
|
3558
|
+
"allKindsCovered": "Her hata türünün zaten bir kuralı var.",
|
|
3559
|
+
"removeRule": "Kuralı kaldır",
|
|
3560
|
+
"duplicateKind": "Her hata türü en fazla bir kural taşıyabilir.",
|
|
3561
|
+
"tooManyRules": "En fazla {max} tür kuralı. Kaydetmeden önce bir kısmını kaldırın.",
|
|
3562
|
+
"unknownKind": "{kind} (tanınmıyor)",
|
|
3563
|
+
"invalidTitle": "Bu tür kuralları kaydedilemiyor",
|
|
3564
|
+
"invalidRows": "{rows} numaralı kuralları gözden geçirin: pay 0'dan büyük ve en fazla 100 olmalı, en az sayı 1 veya üzeri bir tam sayı olmalı ve her tür yalnızca bir kez yer alabilir."
|
|
3565
|
+
}
|
|
3545
3566
|
}
|
|
3546
3567
|
},
|
|
3547
3568
|
"providers": {
|
package/i18n/locales/uk.json
CHANGED
|
@@ -3541,7 +3541,28 @@
|
|
|
3541
3541
|
"maxSweepFailures": "Послідовні невдалі проходи одного фонового прибирання."
|
|
3542
3542
|
},
|
|
3543
3543
|
"notLoaded": "Не вдалося завантажити поточні налаштування цього облікового запису, тож збереження перезапише решту. Перезавантажте перед редагуванням.",
|
|
3544
|
-
"invalidNumbers": "Ці обмеження не є числами"
|
|
3544
|
+
"invalidNumbers": "Ці обмеження не є числами",
|
|
3545
|
+
"failureKinds": {
|
|
3546
|
+
"label": "Правила за видом збою",
|
|
3547
|
+
"description": "Сповіщати, коли один вид збою досягає дозволеної для нього частки збоїв. Стеля домінантної причини вище запитує, чи один вид перекриває решту; ці правила запитують, чи названий вид досяг межі, яку це розгортання від нього терпить.",
|
|
3548
|
+
"overrideLabel": "Задати власні правила для цього облікового запису",
|
|
3549
|
+
"overrideHint": "Вимкнено: обліковий запис використовує правила, налаштовані в розгортанні. Увімкнено: список нижче замінює їх повністю, а порожній список означає відсутність правил за видом збою.",
|
|
3550
|
+
"empty": "Правил немає. Цей обліковий запис не подаватиме сповіщення за видом збою.",
|
|
3551
|
+
"kindLabel": "Вид збою",
|
|
3552
|
+
"shareLabel": "Частка збоїв (%)",
|
|
3553
|
+
"shareHint": "Правило спрацьовує від цієї частки й вище.",
|
|
3554
|
+
"minCountLabel": "Мінімум збоїв",
|
|
3555
|
+
"minCountPlaceholder": "1",
|
|
3556
|
+
"minCountHint": "Скільки збоїв цього виду потрібно у вікні, перш ніж правило зможе спрацювати. Без цього низька частка надто чутлива: п'ять запусків з одним витісненням це вже 20%.",
|
|
3557
|
+
"addRule": "Додати правило",
|
|
3558
|
+
"allKindsCovered": "Кожен вид збою вже має правило.",
|
|
3559
|
+
"removeRule": "Вилучити правило",
|
|
3560
|
+
"duplicateKind": "Кожен вид збою може мати щонайбільше одне правило.",
|
|
3561
|
+
"tooManyRules": "Щонайбільше {max} правил за видом. Вилучіть частину перед збереженням.",
|
|
3562
|
+
"unknownKind": "{kind} (нерозпізнаний)",
|
|
3563
|
+
"invalidTitle": "Ці правила не можна зберегти",
|
|
3564
|
+
"invalidRows": "Перевірте правила {rows}: частка має бути більшою за 0 і не більшою за 100, мінімум має бути цілим числом від 1, і кожен вид може траплятися лише раз."
|
|
3565
|
+
}
|
|
3545
3566
|
}
|
|
3546
3567
|
},
|
|
3547
3568
|
"providers": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.219.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"valibot": "^1.4.2",
|
|
41
41
|
"vue": "3.5.40",
|
|
42
42
|
"wretch": "^3.0.9",
|
|
43
|
-
"@cat-factory/contracts": "0.
|
|
43
|
+
"@cat-factory/contracts": "0.232.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@toad-contracts/testing": "0.3.2",
|