@cat-factory/app 0.46.9 → 0.46.11

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.
Files changed (42) hide show
  1. package/app/components/board/AddTaskModal.vue +106 -88
  2. package/app/components/board/AgentFailureCard.vue +8 -5
  3. package/app/components/board/AgentStopButton.vue +8 -5
  4. package/app/components/board/BoardCanvas.vue +13 -9
  5. package/app/components/board/RecurringPipelineModal.vue +33 -23
  6. package/app/components/board/nodes/BlockNode.vue +50 -34
  7. package/app/components/board/nodes/DecisionBadge.vue +6 -3
  8. package/app/components/board/nodes/DraggableTask.vue +2 -1
  9. package/app/components/board/nodes/EpicNode.vue +10 -3
  10. package/app/components/board/nodes/ModuleFrame.vue +6 -5
  11. package/app/components/board/nodes/TaskCard.vue +40 -22
  12. package/app/components/board/nodes/TaskPipelineMini.vue +4 -3
  13. package/app/components/layout/BoardToolbar.vue +26 -19
  14. package/app/components/observability/StepMetricsBar.vue +31 -11
  15. package/app/components/observability/StepModelActivity.vue +3 -2
  16. package/app/components/panels/AgentStepDetail.vue +52 -39
  17. package/app/components/panels/DecisionModal.vue +12 -5
  18. package/app/components/panels/GenericStructuredResultView.vue +16 -6
  19. package/app/components/panels/InspectorPanel.vue +35 -28
  20. package/app/components/panels/ObservabilityPanel.vue +92 -49
  21. package/app/components/panels/StepMetadataCard.vue +87 -30
  22. package/app/components/panels/StepRestartControl.vue +4 -3
  23. package/app/components/panels/StepRunMeta.vue +23 -10
  24. package/app/components/panels/StepTestReport.vue +14 -11
  25. package/app/components/panels/inspector/ContainerSummary.vue +25 -14
  26. package/app/components/panels/inspector/EpicChildren.vue +7 -4
  27. package/app/components/panels/inspector/RecurringScheduleSettings.vue +60 -19
  28. package/app/components/panels/inspector/ServiceFragments.vue +3 -2
  29. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +18 -13
  30. package/app/components/panels/inspector/ServiceTestConfig.vue +50 -39
  31. package/app/components/panels/inspector/TaskAgentConfig.vue +6 -5
  32. package/app/components/panels/inspector/TaskDependencies.vue +9 -4
  33. package/app/components/panels/inspector/TaskEstimateBadge.vue +14 -13
  34. package/app/components/panels/inspector/TaskExecution.vue +65 -31
  35. package/app/components/panels/inspector/TaskRunSettings.vue +93 -56
  36. package/app/components/panels/inspector/TaskStructure.vue +5 -4
  37. package/i18n/locales/en.json +606 -0
  38. package/i18n/locales/es.json +596 -2
  39. package/i18n/locales/fr.json +596 -2
  40. package/i18n/locales/pl.json +596 -2
  41. package/i18n/locales/uk.json +596 -2
  42. package/package.json +1 -1
@@ -25,6 +25,7 @@ const props = defineProps<{
25
25
  }>()
26
26
 
27
27
  const models = useModelsStore()
28
+ const { t, d } = useI18n()
28
29
 
29
30
  const { isRunning, durationLabel } = useStepTimer({
30
31
  step: () => props.step,
@@ -36,7 +37,7 @@ const modelLabel = computed(() => (props.step.model ? models.labelForRef(props.s
36
37
  const runId = computed(() => props.step.runId ?? props.instanceId ?? null)
37
38
 
38
39
  function formatClock(ms?: number | null): string | null {
39
- return ms ? new Date(ms).toLocaleString() : null
40
+ return ms ? d(new Date(ms), 'long') : null
40
41
  }
41
42
 
42
43
  async function copyRunId() {
@@ -47,13 +48,17 @@ async function copyRunId() {
47
48
  <template>
48
49
  <div class="flex flex-col gap-4">
49
50
  <div v-if="stepNumber && totalSteps">
50
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">Step</h4>
51
- <p class="text-[12px] text-slate-300">{{ stepNumber }} of {{ totalSteps }}</p>
51
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
52
+ {{ t('panels.stepMeta.step') }}
53
+ </h4>
54
+ <p class="text-[12px] text-slate-300">
55
+ {{ t('panels.stepMeta.stepOf', { number: stepNumber, total: totalSteps }) }}
56
+ </p>
52
57
  </div>
53
58
 
54
59
  <div v-if="durationLabel">
55
60
  <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
56
- Duration
61
+ {{ t('panels.stepMeta.duration') }}
57
62
  </h4>
58
63
  <p class="flex items-center gap-1.5 text-[12px] tabular-nums text-slate-300">
59
64
  <UIcon
@@ -62,34 +67,42 @@ async function copyRunId() {
62
67
  class="h-3 w-3 animate-spin text-indigo-400"
63
68
  />
64
69
  {{ durationLabel }}
65
- <span v-if="isRunning" class="text-[11px] text-slate-500">elapsed</span>
70
+ <span v-if="isRunning" class="text-[11px] text-slate-500">{{
71
+ t('panels.stepMeta.elapsed')
72
+ }}</span>
66
73
  </p>
67
74
  </div>
68
75
 
69
76
  <div v-if="formatClock(step.startedAt)">
70
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">Started</h4>
77
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
78
+ {{ t('panels.stepMeta.started') }}
79
+ </h4>
71
80
  <p class="text-[12px] text-slate-300">{{ formatClock(step.startedAt) }}</p>
72
81
  </div>
73
82
 
74
83
  <div v-if="formatClock(step.finishedAt)">
75
84
  <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
76
- Finished
85
+ {{ t('panels.stepMeta.finished') }}
77
86
  </h4>
78
87
  <p class="text-[12px] text-slate-300">{{ formatClock(step.finishedAt) }}</p>
79
88
  </div>
80
89
 
81
90
  <div v-if="step.model">
82
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">Model</h4>
91
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
92
+ {{ t('panels.stepMeta.model') }}
93
+ </h4>
83
94
  <p class="break-all text-[12px] text-slate-300" :title="step.model">
84
95
  {{ modelLabel ?? step.model }}
85
96
  </p>
86
97
  </div>
87
98
 
88
99
  <div v-if="runId">
89
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">Run</h4>
100
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
101
+ {{ t('panels.stepMeta.run') }}
102
+ </h4>
90
103
  <p
91
104
  class="cursor-pointer break-all font-mono text-[12px] text-slate-400 hover:text-slate-200"
92
- :title="`${runId} click to copy`"
105
+ :title="t('panels.stepMeta.clickToCopy', { id: runId })"
93
106
  @click="copyRunId"
94
107
  >
95
108
  {{ runId }}
@@ -9,6 +9,8 @@ defineProps<{
9
9
  phase: TesterStepState | null
10
10
  }>()
11
11
 
12
+ const { t } = useI18n()
13
+
12
14
  const SEVERITY_COLOR: Record<string, string> = {
13
15
  low: '#64748b',
14
16
  medium: '#f59e0b',
@@ -26,16 +28,17 @@ const OUTCOME_COLOR: Record<string, string> = {
26
28
  <section class="mt-4 scroll-mt-4">
27
29
  <div class="mb-2 flex items-center gap-1.5 text-[11px]">
28
30
  <UIcon name="i-lucide-flask-conical" class="h-3.5 w-3.5 text-slate-400" />
29
- <span class="font-semibold uppercase tracking-wide text-slate-400"> Test report </span>
31
+ <span class="font-semibold uppercase tracking-wide text-slate-400">
32
+ {{ t('panels.testReport.title') }}
33
+ </span>
30
34
  <UBadge :color="report.greenlight ? 'success' : 'warning'" variant="subtle" size="sm">
31
- {{ report.greenlight ? 'Greenlit' : 'Needs fixes' }}
35
+ {{
36
+ report.greenlight ? t('panels.testReport.greenlit') : t('panels.testReport.needsFixes')
37
+ }}
32
38
  </UBadge>
33
39
  <span v-if="phase && phase.attempts > 0" class="text-[11px] text-slate-500">
34
- {{ phase.attempts }}/{{ phase.maxAttempts }} fix attempt(s)<span
35
- v-if="phase.phase === 'fixing'"
36
- >
37
- · fixing…</span
38
- >
40
+ {{ t('panels.testReport.fixAttempts', { count: phase.attempts, max: phase.maxAttempts })
41
+ }}<span v-if="phase.phase === 'fixing'"> {{ t('panels.testReport.fixing') }}</span>
39
42
  </span>
40
43
  </div>
41
44
  <p v-if="report.summary" class="mb-3 text-[13px] leading-relaxed text-slate-300">
@@ -43,14 +46,14 @@ const OUTCOME_COLOR: Record<string, string> = {
43
46
  </p>
44
47
 
45
48
  <div v-if="report.tested.length" class="mb-3">
46
- <div class="mb-1 text-[11px] text-slate-500">Tested</div>
49
+ <div class="mb-1 text-[11px] text-slate-500">{{ t('panels.testReport.tested') }}</div>
47
50
  <ul class="space-y-0.5 text-[12px] text-slate-300">
48
- <li v-for="(t, i) in report.tested" :key="i">• {{ t }}</li>
51
+ <li v-for="(item, i) in report.tested" :key="i">• {{ item }}</li>
49
52
  </ul>
50
53
  </div>
51
54
 
52
55
  <div v-if="report.outcomes.length" class="mb-3 space-y-1">
53
- <div class="text-[11px] text-slate-500">Outcomes</div>
56
+ <div class="text-[11px] text-slate-500">{{ t('panels.testReport.outcomes') }}</div>
54
57
  <div v-for="(o, i) in report.outcomes" :key="i" class="flex items-start gap-2 text-[12px]">
55
58
  <span
56
59
  class="mt-1 h-2 w-2 shrink-0 rounded-full"
@@ -63,7 +66,7 @@ const OUTCOME_COLOR: Record<string, string> = {
63
66
  </div>
64
67
 
65
68
  <div v-if="report.concerns.length" class="space-y-1">
66
- <div class="text-[11px] text-slate-500">Concerns</div>
69
+ <div class="text-[11px] text-slate-500">{{ t('panels.testReport.concerns') }}</div>
67
70
  <div
68
71
  v-for="(c, i) in report.concerns"
69
72
  :key="i"
@@ -6,6 +6,7 @@ const props = defineProps<{ block: Block }>()
6
6
 
7
7
  const board = useBoardStore()
8
8
  const ui = useUiStore()
9
+ const { t } = useI18n()
9
10
 
10
11
  const isFrame = computed(() => (props.block.level ?? 'frame') === 'frame')
11
12
  const modules = computed(() => (isFrame.value ? board.modulesOf(props.block.id) : []))
@@ -23,7 +24,7 @@ function addTask() {
23
24
  <!-- modules (services only) -->
24
25
  <div v-if="modules.length">
25
26
  <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
26
- Modules ({{ modules.length }})
27
+ {{ t('inspector.container.modules', { count: modules.length }) }}
27
28
  </div>
28
29
  <ul class="space-y-1">
29
30
  <li
@@ -34,9 +35,13 @@ function addTask() {
34
35
  >
35
36
  <UIcon name="i-lucide-package" class="h-3.5 w-3.5 text-violet-400" />
36
37
  <span class="truncate text-xs text-slate-200">{{ m.title }}</span>
37
- <span class="ml-auto text-[10px] text-slate-500"
38
- >{{ board.tasksOf(m.id).length }} task(s)</span
39
- >
38
+ <span class="ml-auto text-[10px] text-slate-500">{{
39
+ t(
40
+ 'inspector.container.taskCount',
41
+ { count: board.tasksOf(m.id).length },
42
+ board.tasksOf(m.id).length,
43
+ )
44
+ }}</span>
40
45
  </li>
41
46
  </ul>
42
47
  </div>
@@ -44,31 +49,37 @@ function addTask() {
44
49
  <div>
45
50
  <div class="mb-1 flex items-center justify-between">
46
51
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
47
- {{ isFrame ? 'All tasks' : 'Tasks' }} ({{ tasks.length }})
52
+ {{
53
+ isFrame
54
+ ? t('inspector.container.allTasks', { count: tasks.length })
55
+ : t('inspector.container.tasks', { count: tasks.length })
56
+ }}
48
57
  </span>
49
58
  <UButton size="xs" variant="soft" color="primary" icon="i-lucide-plus" @click="addTask">
50
- Add task
59
+ {{ t('inspector.container.addTask') }}
51
60
  </UButton>
52
61
  </div>
53
62
  <ul v-if="tasks.length" class="space-y-1">
54
63
  <li
55
- v-for="t in tasks"
56
- :key="t.id"
64
+ v-for="task in tasks"
65
+ :key="task.id"
57
66
  class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 hover:bg-slate-800/60"
58
- @click="ui.select(t.id)"
67
+ @click="ui.select(task.id)"
59
68
  >
60
69
  <span
61
70
  class="h-2 w-2 shrink-0 rounded-full"
62
- :style="{ backgroundColor: STATUS_META[t.status].color }"
71
+ :style="{ backgroundColor: STATUS_META[task.status].color }"
63
72
  />
64
- <span class="truncate text-xs text-slate-200">{{ t.title }}</span>
65
- <span class="ml-auto text-[10px] text-slate-500">{{ STATUS_META[t.status].label }}</span>
73
+ <span class="truncate text-xs text-slate-200">{{ task.title }}</span>
74
+ <span class="ml-auto text-[10px] text-slate-500">{{
75
+ STATUS_META[task.status].label
76
+ }}</span>
66
77
  </li>
67
78
  </ul>
68
- <div v-else class="text-[11px] text-slate-500">No tasks yet — add one to start work.</div>
79
+ <div v-else class="text-[11px] text-slate-500">{{ t('inspector.container.noTasks') }}</div>
69
80
  </div>
70
81
  <p v-if="isFrame" class="text-[11px] text-slate-500">
71
- Services are long-lived — they don't "complete". Work happens in their tasks &amp; modules.
82
+ {{ t('inspector.container.servicesHint') }}
72
83
  </p>
73
84
  </div>
74
85
  </template>
@@ -10,6 +10,7 @@ const props = defineProps<{ block: Block }>()
10
10
 
11
11
  const board = useBoardStore()
12
12
  const ui = useUiStore()
13
+ const { t } = useI18n()
13
14
 
14
15
  const members = computed(() => board.epicMembers(props.block.id))
15
16
  const done = computed(() => members.value.filter((m) => m.status === 'done').length)
@@ -47,13 +48,15 @@ const groups = computed(() => {
47
48
  <div>
48
49
  <div class="mb-1 flex items-center justify-between">
49
50
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
50
- Epic tasks
51
+ {{ t('inspector.epicChildren.title') }}
51
52
  </span>
52
- <span class="text-[11px] text-slate-500">{{ done }}/{{ members.length }} done</span>
53
+ <span class="text-[11px] text-slate-500">{{
54
+ t('inspector.epicChildren.doneCount', { done, total: members.length })
55
+ }}</span>
53
56
  </div>
54
57
 
55
58
  <div v-if="members.length === 0" class="text-[11px] text-slate-500">
56
- No tasks belong to this epic yet. Import an epic's children, or set a task's epic.
59
+ {{ t('inspector.epicChildren.empty') }}
57
60
  </div>
58
61
 
59
62
  <div v-else class="space-y-2">
@@ -64,7 +67,7 @@ const groups = computed(() => {
64
67
  >
65
68
  <div class="mb-1 flex items-center gap-1 text-[11px] font-medium text-slate-300">
66
69
  <UIcon name="i-lucide-box" class="h-3 w-3 text-slate-500" />
67
- {{ group.service?.title ?? 'Unassigned' }}
70
+ {{ group.service?.title ?? t('inspector.epicChildren.unassigned') }}
68
71
  </div>
69
72
  <div v-for="(mod, mi) in [...group.modules.values()]" :key="mi" class="pl-1">
70
73
  <div v-if="mod.module" class="text-[10px] uppercase tracking-wide text-slate-500">
@@ -9,6 +9,7 @@ const props = defineProps<{ block: Block }>()
9
9
  const recurring = useRecurringPipelinesStore()
10
10
  const pipelines = usePipelinesStore()
11
11
  const toast = useToast()
12
+ const { t, d } = useI18n()
12
13
 
13
14
  const schedule = computed(() => recurring.byBlock(props.block.id))
14
15
  const runs = computed(() =>
@@ -32,17 +33,30 @@ const pipelineName = computed(
32
33
  () => pipelines.getPipeline(schedule.value?.pipelineId ?? '')?.name ?? schedule.value?.pipelineId,
33
34
  )
34
35
 
36
+ const WEEKDAY_KEYS = [
37
+ 'inspector.recurring.weekdays.sun',
38
+ 'inspector.recurring.weekdays.mon',
39
+ 'inspector.recurring.weekdays.tue',
40
+ 'inspector.recurring.weekdays.wed',
41
+ 'inspector.recurring.weekdays.thu',
42
+ 'inspector.recurring.weekdays.fri',
43
+ 'inspector.recurring.weekdays.sat',
44
+ ] as const
45
+
35
46
  function describeCadence(r: Recurrence): string {
36
- const every = r.intervalHours % 24 === 0 ? `${r.intervalHours / 24}d` : `${r.intervalHours}h`
47
+ const every =
48
+ r.intervalHours % 24 === 0
49
+ ? t('inspector.recurring.intervalDays', { count: r.intervalHours / 24 })
50
+ : t('inspector.recurring.intervalHours', { count: r.intervalHours })
37
51
  const days =
38
52
  r.weekdays.length === 0
39
- ? 'any day'
40
- : r.weekdays.map((d) => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d]).join(' ')
53
+ ? t('inspector.recurring.anyDay')
54
+ : r.weekdays.map((wd) => t(WEEKDAY_KEYS[wd]!)).join(' ')
41
55
  const window =
42
56
  r.windowStartHour === null && r.windowEndHour === null
43
57
  ? ''
44
58
  : ` · ${String(r.windowStartHour ?? 0).padStart(2, '0')}:00–${String(r.windowEndHour ?? 24).padStart(2, '0')}:00`
45
- return `Every ${every} · ${days}${window} · ${r.timezone}`
59
+ return t('inspector.recurring.cadence', { every, days, window, timezone: r.timezone })
46
60
  }
47
61
 
48
62
  function startEdit() {
@@ -58,7 +72,11 @@ async function saveEdit() {
58
72
  await recurring.update(schedule.value.id, { recurrence: draft.value })
59
73
  editing.value = false
60
74
  } catch (e) {
61
- toast.add({ title: 'Could not update schedule', description: errMsg(e), color: 'error' })
75
+ toast.add({
76
+ title: t('inspector.recurring.updateFailed'),
77
+ description: errMsg(e),
78
+ color: 'error',
79
+ })
62
80
  } finally {
63
81
  busy.value = false
64
82
  }
@@ -70,7 +88,11 @@ async function toggleEnabled() {
70
88
  try {
71
89
  await recurring.update(schedule.value.id, { enabled: !schedule.value.enabled })
72
90
  } catch (e) {
73
- toast.add({ title: 'Could not update schedule', description: errMsg(e), color: 'error' })
91
+ toast.add({
92
+ title: t('inspector.recurring.updateFailed'),
93
+ description: errMsg(e),
94
+ color: 'error',
95
+ })
74
96
  } finally {
75
97
  busy.value = false
76
98
  }
@@ -82,7 +104,11 @@ async function runNow() {
82
104
  try {
83
105
  await recurring.runNow(schedule.value.id)
84
106
  } catch (e) {
85
- toast.add({ title: 'Could not run now', description: errMsg(e), color: 'error' })
107
+ toast.add({
108
+ title: t('inspector.recurring.runNowFailed'),
109
+ description: errMsg(e),
110
+ color: 'error',
111
+ })
86
112
  } finally {
87
113
  busy.value = false
88
114
  }
@@ -98,8 +124,17 @@ const RUN_COLOR: Record<string, string> = {
98
124
  failed: 'text-rose-400',
99
125
  skipped: 'text-slate-500',
100
126
  }
127
+ const RUN_STATUS_KEYS: Record<string, string> = {
128
+ running: 'inspector.recurring.runStatus.running',
129
+ done: 'inspector.recurring.runStatus.done',
130
+ failed: 'inspector.recurring.runStatus.failed',
131
+ skipped: 'inspector.recurring.runStatus.skipped',
132
+ }
133
+ function runStatusLabel(status: string): string {
134
+ return RUN_STATUS_KEYS[status] ? t(RUN_STATUS_KEYS[status]!) : status
135
+ }
101
136
  function fmtTime(ms: number) {
102
- return new Date(ms).toLocaleString()
137
+ return d(new Date(ms), 'long')
103
138
  }
104
139
  </script>
105
140
 
@@ -113,10 +148,10 @@ function fmtTime(ms: number) {
113
148
  class="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-indigo-300"
114
149
  >
115
150
  <UIcon name="i-lucide-repeat" class="h-3.5 w-3.5" />
116
- Recurring pipeline
151
+ {{ t('inspector.recurring.title') }}
117
152
  </span>
118
153
  <UBadge :color="schedule.enabled ? 'primary' : 'neutral'" variant="subtle" size="xs">
119
- {{ schedule.enabled ? 'Active' : 'Paused' }}
154
+ {{ schedule.enabled ? t('inspector.recurring.active') : t('inspector.recurring.paused') }}
120
155
  </UBadge>
121
156
  </div>
122
157
 
@@ -126,10 +161,12 @@ function fmtTime(ms: number) {
126
161
 
127
162
  <template v-if="!editing">
128
163
  <p class="text-[11px] text-slate-400">{{ describeCadence(schedule.recurrence) }}</p>
129
- <p class="text-[11px] text-slate-500">Next run: {{ fmtTime(schedule.nextRunAt) }}</p>
164
+ <p class="text-[11px] text-slate-500">
165
+ {{ t('inspector.recurring.nextRun', { time: fmtTime(schedule.nextRunAt) }) }}
166
+ </p>
130
167
  <div class="flex flex-wrap gap-1.5 pt-1">
131
168
  <UButton size="xs" variant="soft" icon="i-lucide-play" :loading="busy" @click="runNow">
132
- Run now
169
+ {{ t('inspector.recurring.runNow') }}
133
170
  </UButton>
134
171
  <UButton
135
172
  size="xs"
@@ -139,7 +176,7 @@ function fmtTime(ms: number) {
139
176
  :loading="busy"
140
177
  @click="toggleEnabled"
141
178
  >
142
- {{ schedule.enabled ? 'Pause' : 'Resume' }}
179
+ {{ schedule.enabled ? t('inspector.recurring.pause') : t('inspector.recurring.resume') }}
143
180
  </UButton>
144
181
  <UButton
145
182
  size="xs"
@@ -148,7 +185,7 @@ function fmtTime(ms: number) {
148
185
  icon="i-lucide-pencil"
149
186
  @click="startEdit"
150
187
  >
151
- Edit cadence
188
+ {{ t('inspector.recurring.editCadence') }}
152
189
  </UButton>
153
190
  </div>
154
191
  </template>
@@ -156,19 +193,23 @@ function fmtTime(ms: number) {
156
193
  <template v-else-if="draft">
157
194
  <RecurringRecurrenceEditor v-model="draft" />
158
195
  <div class="flex justify-end gap-1.5 pt-1">
159
- <UButton size="xs" variant="ghost" color="neutral" @click="editing = false">Cancel</UButton>
160
- <UButton size="xs" color="primary" :loading="busy" @click="saveEdit">Save</UButton>
196
+ <UButton size="xs" variant="ghost" color="neutral" @click="editing = false">{{
197
+ t('common.cancel')
198
+ }}</UButton>
199
+ <UButton size="xs" color="primary" :loading="busy" @click="saveEdit">{{
200
+ t('common.save')
201
+ }}</UButton>
161
202
  </div>
162
203
  </template>
163
204
 
164
205
  <!-- run history -->
165
206
  <div v-if="runs.length" class="space-y-1 border-t border-slate-800 pt-2">
166
207
  <span class="text-[10px] font-semibold uppercase tracking-wide text-slate-500">
167
- Recent runs
208
+ {{ t('inspector.recurring.recentRuns') }}
168
209
  </span>
169
210
  <div v-for="run in runs" :key="run.id" class="flex items-center gap-2 text-[11px]">
170
- <span :class="RUN_COLOR[run.status] ?? 'text-slate-400'" class="w-14 shrink-0 capitalize">
171
- {{ run.status }}
211
+ <span :class="RUN_COLOR[run.status] ?? 'text-slate-400'" class="w-14 shrink-0">
212
+ {{ runStatusLabel(run.status) }}
172
213
  </span>
173
214
  <span class="truncate text-slate-500">{{ fmtTime(run.startedAt) }}</span>
174
215
  <span v-if="run.outcome" class="ml-auto truncate text-slate-500">{{ run.outcome }}</span>
@@ -9,6 +9,7 @@ const props = defineProps<{ block: Block }>()
9
9
 
10
10
  const board = useBoardStore()
11
11
  const fragments = useFragmentsStore()
12
+ const { t } = useI18n()
12
13
 
13
14
  onMounted(() => fragments.ensureLoaded())
14
15
 
@@ -49,7 +50,7 @@ function removeFragment(id: string) {
49
50
  <div>
50
51
  <div class="mb-1 flex items-center justify-between">
51
52
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
52
- Service best practices
53
+ {{ t('inspector.fragments.serviceTitle') }}
53
54
  </span>
54
55
  <UDropdownMenu v-if="fragmentMenu.length" :items="fragmentMenu">
55
56
  <UButton
@@ -76,7 +77,7 @@ function removeFragment(id: string) {
76
77
  </UBadge>
77
78
  </div>
78
79
  <div v-else class="text-[11px] text-slate-500">
79
- None code-aware agents on this service follow their default guidance.
80
+ {{ t('inspector.fragments.serviceEmpty') }}
80
81
  </div>
81
82
  </div>
82
83
  </template>
@@ -11,6 +11,7 @@ const props = defineProps<{ block: Block }>()
11
11
  const store = useReleaseHealthStore()
12
12
  const ui = useUiStore()
13
13
  const toast = useToast()
14
+ const { t } = useI18n()
14
15
 
15
16
  const busy = ref(false)
16
17
  const draft = reactive({ monitorIds: '', sloIds: '', envTag: '' })
@@ -56,9 +57,13 @@ async function save() {
56
57
  sloIds: parseIds(draft.sloIds),
57
58
  envTag: draft.envTag.trim() || null,
58
59
  })
59
- toast.add({ title: 'Monitoring saved', icon: 'i-lucide-check', color: 'success' })
60
+ toast.add({
61
+ title: t('inspector.releaseHealth.savedToast'),
62
+ icon: 'i-lucide-check',
63
+ color: 'success',
64
+ })
60
65
  } catch (e) {
61
- notifyError('Could not save the mapping', e)
66
+ notifyError(t('inspector.releaseHealth.saveFailed'), e)
62
67
  } finally {
63
68
  busy.value = false
64
69
  }
@@ -72,7 +77,7 @@ async function clear() {
72
77
  draft.sloIds = ''
73
78
  draft.envTag = ''
74
79
  } catch (e) {
75
- notifyError('Could not clear the mapping', e)
80
+ notifyError(t('inspector.releaseHealth.clearFailed'), e)
76
81
  } finally {
77
82
  busy.value = false
78
83
  }
@@ -83,7 +88,7 @@ async function clear() {
83
88
  <div class="space-y-2">
84
89
  <div class="flex items-center justify-between">
85
90
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
86
- Post-release health
91
+ {{ t('inspector.releaseHealth.title') }}
87
92
  </span>
88
93
  <UButton
89
94
  v-if="saved"
@@ -94,7 +99,7 @@ async function clear() {
94
99
  :loading="busy"
95
100
  @click="clear"
96
101
  >
97
- Clear
102
+ {{ t('inspector.releaseHealth.clear') }}
98
103
  </UButton>
99
104
  </div>
100
105
 
@@ -103,32 +108,32 @@ async function clear() {
103
108
  v-if="!connected"
104
109
  class="flex items-center justify-between gap-2 rounded-md border border-slate-800 bg-slate-900/60 px-2 py-1.5 text-[11px] text-slate-400"
105
110
  >
106
- <span>Connect an observability integration to watch this service after releases.</span>
111
+ <span>{{ t('inspector.releaseHealth.connectPrompt') }}</span>
107
112
  <UButton
108
113
  color="neutral"
109
114
  variant="soft"
110
115
  size="xs"
111
116
  icon="i-lucide-plug"
112
- title="Connect an observability integration first"
117
+ :title="t('inspector.releaseHealth.connectFirst')"
113
118
  @click="ui.openObservabilityConnection()"
114
119
  >
115
- Connect
120
+ {{ t('inspector.releaseHealth.connect') }}
116
121
  </UButton>
117
122
  </div>
118
123
 
119
124
  <div v-else class="space-y-2">
120
125
  <p class="text-[11px] text-slate-500">
121
- The monitors and SLOs the gate watches after this service ships. Comma-separate ids.
126
+ {{ t('inspector.releaseHealth.hint') }}
122
127
  </p>
123
128
  <div class="grid grid-cols-2 gap-2">
124
- <UFormField label="Monitor ids">
129
+ <UFormField :label="t('inspector.releaseHealth.monitorIds')">
125
130
  <UInput v-model="draft.monitorIds" placeholder="123, 456" size="sm" class="w-full" />
126
131
  </UFormField>
127
- <UFormField label="SLO ids">
132
+ <UFormField :label="t('inspector.releaseHealth.sloIds')">
128
133
  <UInput v-model="draft.sloIds" placeholder="abc, def" size="sm" class="w-full" />
129
134
  </UFormField>
130
135
  </div>
131
- <UFormField label="Env tag (optional)">
136
+ <UFormField :label="t('inspector.releaseHealth.envTag')">
132
137
  <UInput v-model="draft.envTag" placeholder="prod" size="sm" class="w-full" />
133
138
  </UFormField>
134
139
  <div class="flex justify-end">
@@ -140,7 +145,7 @@ async function clear() {
140
145
  :loading="busy"
141
146
  @click="save"
142
147
  >
143
- Save monitoring
148
+ {{ t('inspector.releaseHealth.saveMonitoring') }}
144
149
  </UButton>
145
150
  </div>
146
151
  </div>