@cat-factory/app 0.9.1 → 0.11.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 +209 -21
- package/app/components/board/nodes/BlockNode.vue +2 -2
- package/app/components/focus/BlockFocusView.vue +2 -2
- package/app/components/layout/AccountTeamSettings.vue +6 -3
- package/app/components/layout/CommandBar.vue +7 -33
- package/app/components/layout/IntegrationsHub.vue +230 -0
- package/app/components/layout/SideBar.vue +8 -170
- package/app/components/panels/GenericStructuredResultView.vue +131 -0
- package/app/components/panels/InspectorPanel.vue +6 -2
- package/app/components/panels/StepResultViewHost.vue +4 -0
- package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +148 -0
- package/app/components/providers/ApiKeysSection.vue +67 -26
- package/app/components/providers/VendorCredentialsModal.vue +115 -84
- package/app/components/settings/IssueTrackerWritebackPanel.vue +45 -57
- package/app/components/settings/MergeThresholdsPanel.vue +189 -226
- package/app/components/settings/ObservabilityConnectionPanel.vue +151 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +46 -61
- package/app/components/settings/WorkspaceSettingsPanel.vue +136 -63
- package/app/composables/api/releaseHealth.ts +11 -10
- package/app/pages/index.vue +4 -8
- package/app/stores/agents.ts +27 -2
- package/app/stores/releaseHealth.ts +48 -12
- package/app/stores/ui.ts +34 -42
- package/app/stores/workspace.ts +4 -0
- package/app/types/domain.ts +33 -1
- package/app/types/execution.ts +6 -0
- package/app/types/releaseHealth.ts +19 -11
- package/app/utils/catalog.spec.ts +10 -0
- package/app/utils/catalog.ts +20 -6
- package/package.json +1 -1
- package/app/components/board/ContextPicker.vue +0 -367
- package/app/components/settings/DatadogPanel.vue +0 -213
|
@@ -4,23 +4,16 @@
|
|
|
4
4
|
// served by GET /prompt-fragments. Changing it does not retroactively change existing
|
|
5
5
|
// services — each owns its selection from creation. Persisted via the
|
|
6
6
|
// serviceFragmentDefaults store (the backend replaces the whole list on each change).
|
|
7
|
-
import { ref } from 'vue'
|
|
7
|
+
import { onMounted, ref } from 'vue'
|
|
8
8
|
|
|
9
|
-
const ui = useUiStore()
|
|
10
9
|
const fragments = useFragmentsStore()
|
|
11
10
|
const defaults = useServiceFragmentDefaultsStore()
|
|
12
11
|
const toast = useToast()
|
|
13
12
|
|
|
14
|
-
const open = computed({
|
|
15
|
-
get: () => ui.serviceFragmentDefaultsOpen,
|
|
16
|
-
set: (v: boolean) => (v ? ui.openServiceFragmentDefaults() : ui.closeServiceFragmentDefaults()),
|
|
17
|
-
})
|
|
18
|
-
|
|
19
13
|
const busy = ref(false)
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
})
|
|
15
|
+
// The tab renders when Workspace settings opens; load the fragment pool then.
|
|
16
|
+
onMounted(() => void fragments.ensureLoaded())
|
|
24
17
|
|
|
25
18
|
const selected = computed(() =>
|
|
26
19
|
defaults.fragmentIds
|
|
@@ -68,57 +61,49 @@ function remove(id: string) {
|
|
|
68
61
|
</script>
|
|
69
62
|
|
|
70
63
|
<template>
|
|
71
|
-
<
|
|
72
|
-
<
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
default does not affect services that already exist.
|
|
80
|
-
</p>
|
|
64
|
+
<div class="space-y-4">
|
|
65
|
+
<p class="text-xs text-slate-400">
|
|
66
|
+
Pick the best-practice fragments every <span class="text-slate-300">new</span> service starts
|
|
67
|
+
with. Their guidance is folded into the prompt of every
|
|
68
|
+
<span class="text-slate-300">code-aware</span> agent (coder, reviewer, architect, fixers) on
|
|
69
|
+
the service's tasks. You can refine the set per service in its inspector; changing this
|
|
70
|
+
default does not affect services that already exist.
|
|
71
|
+
</p>
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Add fragment
|
|
100
|
-
</UButton>
|
|
101
|
-
</UDropdownMenu>
|
|
102
|
-
</div>
|
|
73
|
+
<div class="flex items-center justify-between">
|
|
74
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
75
|
+
Default fragments
|
|
76
|
+
</span>
|
|
77
|
+
<UDropdownMenu v-if="menu.length" :items="menu" :ui="{ content: 'max-h-72 overflow-y-auto' }">
|
|
78
|
+
<UButton
|
|
79
|
+
size="xs"
|
|
80
|
+
variant="ghost"
|
|
81
|
+
color="neutral"
|
|
82
|
+
icon="i-lucide-plus"
|
|
83
|
+
trailing-icon="i-lucide-chevron-down"
|
|
84
|
+
:loading="busy"
|
|
85
|
+
>
|
|
86
|
+
Add fragment
|
|
87
|
+
</UButton>
|
|
88
|
+
</UDropdownMenu>
|
|
89
|
+
</div>
|
|
103
90
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
</template>
|
|
123
|
-
</UModal>
|
|
91
|
+
<div v-if="selected.length" class="flex flex-wrap gap-1">
|
|
92
|
+
<UBadge
|
|
93
|
+
v-for="f in selected"
|
|
94
|
+
:key="f.id"
|
|
95
|
+
color="primary"
|
|
96
|
+
variant="subtle"
|
|
97
|
+
size="sm"
|
|
98
|
+
class="cursor-pointer"
|
|
99
|
+
:title="f.summary"
|
|
100
|
+
@click="remove(f.id)"
|
|
101
|
+
>
|
|
102
|
+
{{ f.title }}<UIcon name="i-lucide-x" class="ml-0.5 h-3 w-3" />
|
|
103
|
+
</UBadge>
|
|
104
|
+
</div>
|
|
105
|
+
<p v-else class="text-[11px] text-slate-500">
|
|
106
|
+
None — new services start with no service-level fragments.
|
|
107
|
+
</p>
|
|
108
|
+
</div>
|
|
124
109
|
</template>
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// Workspace settings
|
|
3
|
-
//
|
|
4
|
-
// -
|
|
5
|
-
//
|
|
6
|
-
// -
|
|
7
|
-
//
|
|
2
|
+
// Workspace settings — a single tabbed modal that gathers the workspace-wide
|
|
3
|
+
// configuration that used to live in separate windows:
|
|
4
|
+
// - Workspace: the run-timing escalation threshold + per-service running-task limit.
|
|
5
|
+
// - Merge thresholds: the auto-merge preset library.
|
|
6
|
+
// - Issue writeback: the tracker writeback toggles.
|
|
7
|
+
// - Service best practices: the default fragments new services inherit.
|
|
8
|
+
// The latter three are body-only section components rendered in tabs here (no longer
|
|
9
|
+
// standalone modals).
|
|
8
10
|
import { reactive, ref, watch } from 'vue'
|
|
9
11
|
import type { CreateTaskType, TaskLimitMode } from '~/types/domain'
|
|
12
|
+
import MergeThresholdsPanel from '~/components/settings/MergeThresholdsPanel.vue'
|
|
13
|
+
import IssueTrackerWritebackPanel from '~/components/settings/IssueTrackerWritebackPanel.vue'
|
|
14
|
+
import ServiceFragmentDefaultsPanel from '~/components/settings/ServiceFragmentDefaultsPanel.vue'
|
|
10
15
|
|
|
11
16
|
const ui = useUiStore()
|
|
12
17
|
const store = useWorkspaceSettingsStore()
|
|
@@ -17,6 +22,35 @@ const open = computed({
|
|
|
17
22
|
set: (v: boolean) => (v ? ui.openWorkspaceSettings() : ui.closeWorkspaceSettings()),
|
|
18
23
|
})
|
|
19
24
|
|
|
25
|
+
// Which tab is shown — driven by the ui store so other surfaces (command bar,
|
|
26
|
+
// integrations) can deep-link straight to a tab.
|
|
27
|
+
const activeTab = computed({
|
|
28
|
+
get: () => ui.workspaceSettingsTab,
|
|
29
|
+
set: (v: string) => ui.setWorkspaceSettingsTab(v),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const tabs = [
|
|
33
|
+
{
|
|
34
|
+
value: 'workspace',
|
|
35
|
+
label: 'Workspace',
|
|
36
|
+
icon: 'i-lucide-sliders-horizontal',
|
|
37
|
+
slot: 'workspace',
|
|
38
|
+
},
|
|
39
|
+
{ value: 'merge', label: 'Merge thresholds', icon: 'i-lucide-git-merge', slot: 'merge' },
|
|
40
|
+
{
|
|
41
|
+
value: 'writeback',
|
|
42
|
+
label: 'Issue writeback',
|
|
43
|
+
icon: 'i-lucide-message-square-reply',
|
|
44
|
+
slot: 'writeback',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
value: 'fragments',
|
|
48
|
+
label: 'Service best practices',
|
|
49
|
+
icon: 'i-lucide-book-open-check',
|
|
50
|
+
slot: 'fragments',
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
|
|
20
54
|
const TASK_TYPES: CreateTaskType[] = ['feature', 'bug', 'document', 'spike']
|
|
21
55
|
const MODES: { value: TaskLimitMode; label: string }[] = [
|
|
22
56
|
{ value: 'off', label: 'No limit' },
|
|
@@ -78,65 +112,104 @@ async function save() {
|
|
|
78
112
|
</script>
|
|
79
113
|
|
|
80
114
|
<template>
|
|
81
|
-
<UModal v-model:open="open" title="Workspace settings" :ui="{ content: 'max-w-
|
|
115
|
+
<UModal v-model:open="open" title="Workspace settings" :ui="{ content: 'max-w-3xl' }">
|
|
82
116
|
<template #body>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
117
|
+
<UTabs
|
|
118
|
+
v-model="activeTab"
|
|
119
|
+
:items="tabs"
|
|
120
|
+
variant="link"
|
|
121
|
+
:ui="{ root: 'gap-4', list: 'overflow-x-auto' }"
|
|
122
|
+
>
|
|
123
|
+
<!-- Workspace -->
|
|
124
|
+
<template #workspace>
|
|
125
|
+
<div class="space-y-6">
|
|
126
|
+
<!-- Run-timing escalation -->
|
|
127
|
+
<section class="space-y-2">
|
|
128
|
+
<h3 class="text-sm font-semibold text-slate-200">Waiting for a human</h3>
|
|
129
|
+
<p class="text-[11px] text-slate-400">
|
|
130
|
+
A run parked on a human decision (a review, an approval, a merge) waits as long as
|
|
131
|
+
it needs — it is never cancelled. After this many minutes its notification turns red
|
|
132
|
+
and is flagged <span class="text-error-400">Overdue</span> in the inbox.
|
|
133
|
+
</p>
|
|
134
|
+
<label class="block w-48">
|
|
135
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
136
|
+
Escalate after (minutes)
|
|
137
|
+
</span>
|
|
138
|
+
<UInput
|
|
139
|
+
v-model.number="draft.waitingEscalationMinutes"
|
|
140
|
+
type="number"
|
|
141
|
+
:min="1"
|
|
142
|
+
size="sm"
|
|
143
|
+
/>
|
|
144
|
+
</label>
|
|
145
|
+
</section>
|
|
146
|
+
|
|
147
|
+
<!-- Per-service running-task limit -->
|
|
148
|
+
<section class="space-y-2">
|
|
149
|
+
<h3 class="text-sm font-semibold text-slate-200">Running tasks per service</h3>
|
|
150
|
+
<p class="text-[11px] text-slate-400">
|
|
151
|
+
Cap how many tasks may run at once under one service. Starting a task over the limit
|
|
152
|
+
is refused with a clear message until a running task finishes.
|
|
153
|
+
</p>
|
|
154
|
+
<label class="block w-64">
|
|
155
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
|
|
156
|
+
>Mode</span
|
|
157
|
+
>
|
|
158
|
+
<USelect
|
|
159
|
+
v-model="draft.taskLimitMode"
|
|
160
|
+
:items="MODES"
|
|
161
|
+
value-key="value"
|
|
162
|
+
size="sm"
|
|
163
|
+
class="w-full"
|
|
164
|
+
/>
|
|
165
|
+
</label>
|
|
166
|
+
|
|
167
|
+
<label v-if="draft.taskLimitMode === 'shared'" class="block w-48">
|
|
168
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
169
|
+
Max running tasks
|
|
170
|
+
</span>
|
|
171
|
+
<UInput v-model.number="draft.taskLimitShared" type="number" :min="1" size="sm" />
|
|
172
|
+
</label>
|
|
173
|
+
|
|
174
|
+
<div v-else-if="draft.taskLimitMode === 'per_type'" class="grid grid-cols-2 gap-3">
|
|
175
|
+
<label v-for="t in TASK_TYPES" :key="t" class="block">
|
|
176
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
177
|
+
Max {{ t }} tasks
|
|
178
|
+
</span>
|
|
179
|
+
<UInput v-model.number="draft.perType[t]" type="number" :min="1" size="sm" />
|
|
180
|
+
</label>
|
|
181
|
+
</div>
|
|
182
|
+
</section>
|
|
183
|
+
|
|
184
|
+
<div class="flex justify-end">
|
|
185
|
+
<UButton
|
|
186
|
+
color="primary"
|
|
187
|
+
icon="i-lucide-save"
|
|
188
|
+
size="sm"
|
|
189
|
+
:loading="saving"
|
|
190
|
+
@click="save"
|
|
191
|
+
>
|
|
192
|
+
Save
|
|
193
|
+
</UButton>
|
|
194
|
+
</div>
|
|
131
195
|
</div>
|
|
132
|
-
</
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
196
|
+
</template>
|
|
197
|
+
|
|
198
|
+
<!-- Merge thresholds -->
|
|
199
|
+
<template #merge>
|
|
200
|
+
<MergeThresholdsPanel />
|
|
201
|
+
</template>
|
|
202
|
+
|
|
203
|
+
<!-- Issue writeback -->
|
|
204
|
+
<template #writeback>
|
|
205
|
+
<IssueTrackerWritebackPanel />
|
|
206
|
+
</template>
|
|
207
|
+
|
|
208
|
+
<!-- Service best practices -->
|
|
209
|
+
<template #fragments>
|
|
210
|
+
<ServiceFragmentDefaultsPanel />
|
|
211
|
+
</template>
|
|
212
|
+
</UTabs>
|
|
140
213
|
</template>
|
|
141
214
|
</UModal>
|
|
142
215
|
</template>
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
ObservabilityConnectionView,
|
|
3
3
|
ReleaseHealthConfig,
|
|
4
|
-
|
|
4
|
+
UpsertObservabilityConnectionInput,
|
|
5
5
|
UpsertReleaseHealthConfigInput,
|
|
6
6
|
} from '~/types/releaseHealth'
|
|
7
7
|
import type { ApiContext } from './context'
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/** Post-release-health: the observability connection + per-block monitor/SLO mapping. */
|
|
10
10
|
export function releaseHealthApi({ http, ws }: ApiContext) {
|
|
11
11
|
return {
|
|
12
|
-
// ----
|
|
13
|
-
|
|
14
|
-
http<
|
|
12
|
+
// ---- Observability connection ------------------------------------------
|
|
13
|
+
getObservabilityConnection: (workspaceId: string) =>
|
|
14
|
+
http<ObservabilityConnectionView>(`${ws(workspaceId)}/observability/connection`),
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
http<
|
|
16
|
+
setObservabilityConnection: (workspaceId: string, body: UpsertObservabilityConnectionInput) =>
|
|
17
|
+
http<ObservabilityConnectionView>(`${ws(workspaceId)}/observability/connection`, {
|
|
18
18
|
method: 'PUT',
|
|
19
19
|
body,
|
|
20
20
|
}),
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
http(`${ws(workspaceId)}/
|
|
22
|
+
deleteObservabilityConnection: (workspaceId: string) =>
|
|
23
|
+
http(`${ws(workspaceId)}/observability/connection`, { method: 'DELETE' }),
|
|
24
24
|
|
|
25
|
+
// ---- Per-block monitor/SLO config --------------------------------------
|
|
25
26
|
listReleaseHealthConfigs: (workspaceId: string) =>
|
|
26
27
|
http<ReleaseHealthConfig[]>(`${ws(workspaceId)}/release-health-configs`),
|
|
27
28
|
|
package/app/pages/index.vue
CHANGED
|
@@ -25,12 +25,10 @@ import SlackPanel from '~/components/slack/SlackPanel.vue'
|
|
|
25
25
|
import GitHubOnboarding from '~/components/github/GitHubOnboarding.vue'
|
|
26
26
|
import FragmentLibraryPanel from '~/components/fragments/FragmentLibraryPanel.vue'
|
|
27
27
|
import CommandBar from '~/components/layout/CommandBar.vue'
|
|
28
|
-
import
|
|
29
|
-
import IssueTrackerWritebackPanel from '~/components/settings/IssueTrackerWritebackPanel.vue'
|
|
28
|
+
import IntegrationsHub from '~/components/layout/IntegrationsHub.vue'
|
|
30
29
|
import WorkspaceSettingsPanel from '~/components/settings/WorkspaceSettingsPanel.vue'
|
|
31
|
-
import
|
|
30
|
+
import ObservabilityConnectionPanel from '~/components/settings/ObservabilityConnectionPanel.vue'
|
|
32
31
|
import ModelConfigurationPanel from '~/components/settings/ModelConfigurationPanel.vue'
|
|
33
|
-
import ServiceFragmentDefaultsPanel from '~/components/settings/ServiceFragmentDefaultsPanel.vue'
|
|
34
32
|
import LocalModelEndpointsPanel from '~/components/settings/LocalModelEndpointsPanel.vue'
|
|
35
33
|
import OpenRouterCatalogPanel from '~/components/settings/OpenRouterCatalogPanel.vue'
|
|
36
34
|
import VendorCredentialsModal from '~/components/providers/VendorCredentialsModal.vue'
|
|
@@ -118,12 +116,10 @@ watch(
|
|
|
118
116
|
<SlackPanel />
|
|
119
117
|
<FragmentLibraryPanel />
|
|
120
118
|
<CommandBar />
|
|
121
|
-
<
|
|
122
|
-
<IssueTrackerWritebackPanel />
|
|
119
|
+
<IntegrationsHub />
|
|
123
120
|
<WorkspaceSettingsPanel />
|
|
124
|
-
<
|
|
121
|
+
<ObservabilityConnectionPanel />
|
|
125
122
|
<ModelConfigurationPanel />
|
|
126
|
-
<ServiceFragmentDefaultsPanel />
|
|
127
123
|
<LocalModelEndpointsPanel />
|
|
128
124
|
<OpenRouterCatalogPanel />
|
|
129
125
|
<VendorCredentialsModal />
|
package/app/stores/agents.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import { AGENT_ARCHETYPES, AGENT_BY_KIND, uid } from '~/utils/catalog'
|
|
4
|
-
import type { AgentArchetype, AgentKind } from '~/types/domain'
|
|
4
|
+
import type { AgentArchetype, AgentKind, CustomAgentKind } from '~/types/domain'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The agent palette. Seeded from the static catalog, but custom agents can be
|
|
@@ -36,5 +36,30 @@ export const useAgentsStore = defineStore('agents', () => {
|
|
|
36
36
|
return archetype
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Merge the deployment's registered CUSTOM agent kinds (from the workspace snapshot)
|
|
41
|
+
* into the palette catalog: each becomes a first-class palette block + a kind-based
|
|
42
|
+
* lookup (so timelines / inspectors render it instead of the generic fallback), and its
|
|
43
|
+
* declared `resultView` opens through the same registry the built-ins use. Idempotent
|
|
44
|
+
* and built-in-safe — a kind already known (a built-in, or a prior load) is left
|
|
45
|
+
* untouched, so a snapshot can't shadow a built-in or duplicate on reload.
|
|
46
|
+
*/
|
|
47
|
+
function registerCustomKinds(kinds: CustomAgentKind[]) {
|
|
48
|
+
for (const { kind, presentation } of kinds) {
|
|
49
|
+
if (AGENT_BY_KIND[kind]) continue
|
|
50
|
+
const archetype: AgentArchetype = {
|
|
51
|
+
kind,
|
|
52
|
+
label: presentation.label,
|
|
53
|
+
icon: presentation.icon,
|
|
54
|
+
color: presentation.color,
|
|
55
|
+
description: presentation.description,
|
|
56
|
+
...(presentation.category ? { category: presentation.category } : {}),
|
|
57
|
+
...(presentation.resultView ? { resultView: presentation.resultView } : {}),
|
|
58
|
+
}
|
|
59
|
+
AGENT_BY_KIND[kind] = archetype
|
|
60
|
+
archetypes.value.push(archetype)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { archetypes, get, addAgent, registerCustomKinds }
|
|
40
65
|
})
|
|
@@ -1,50 +1,83 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import type {
|
|
4
|
-
|
|
4
|
+
ObservabilityConnectionView,
|
|
5
5
|
ReleaseHealthConfig,
|
|
6
|
-
|
|
6
|
+
UpsertObservabilityConnectionInput,
|
|
7
7
|
UpsertReleaseHealthConfigInput,
|
|
8
8
|
} from '~/types/releaseHealth'
|
|
9
9
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* The workspace's
|
|
13
|
-
*
|
|
14
|
-
* `post-release-health` gate reads. Loaded on demand (the
|
|
15
|
-
* snapshot, since the secrets never leave the server.
|
|
12
|
+
* The workspace's post-release-health settings: the (single) observability connection —
|
|
13
|
+
* provider + credentials, write-only, never read back — and the per-block monitor/SLO
|
|
14
|
+
* mappings the `post-release-health` gate reads. Loaded on demand (the observability panel
|
|
15
|
+
* + the service inspector), not from the snapshot, since the secrets never leave the server.
|
|
16
16
|
*/
|
|
17
17
|
export const useReleaseHealthStore = defineStore('releaseHealth', () => {
|
|
18
18
|
const api = useApi()
|
|
19
19
|
|
|
20
|
-
const connection = ref<
|
|
20
|
+
const connection = ref<ObservabilityConnectionView>({
|
|
21
|
+
connected: false,
|
|
22
|
+
provider: null,
|
|
23
|
+
summary: null,
|
|
24
|
+
})
|
|
21
25
|
const configs = ref<ReleaseHealthConfig[]>([])
|
|
22
26
|
const loading = ref(false)
|
|
27
|
+
// Mirrors the backend's opt-in gate (`OBSERVABILITY_ENABLED`): `null` until first
|
|
28
|
+
// probed, then `true`/`false`. The hub + inspector hide their observability entry
|
|
29
|
+
// points when this is false, so a disabled backend doesn't surface a dead control.
|
|
30
|
+
const available = ref<boolean | null>(null)
|
|
31
|
+
let inFlight: Promise<void> | null = null
|
|
23
32
|
|
|
33
|
+
/** Force a refresh of the connection + per-block configs (used after a save/remove). */
|
|
24
34
|
async function load() {
|
|
25
35
|
const ws = useWorkspaceStore()
|
|
26
36
|
loading.value = true
|
|
27
37
|
try {
|
|
28
38
|
const [conn, list] = await Promise.all([
|
|
29
|
-
api.
|
|
39
|
+
api.getObservabilityConnection(ws.requireId()),
|
|
30
40
|
api.listReleaseHealthConfigs(ws.requireId()),
|
|
31
41
|
])
|
|
32
42
|
connection.value = conn
|
|
33
43
|
configs.value = list
|
|
44
|
+
available.value = true
|
|
45
|
+
} catch {
|
|
46
|
+
// 503 (observability disabled) or any error → hide the UI entry points.
|
|
47
|
+
available.value = false
|
|
48
|
+
connection.value = { connected: false, provider: null, summary: null }
|
|
49
|
+
configs.value = []
|
|
34
50
|
} finally {
|
|
35
51
|
loading.value = false
|
|
36
52
|
}
|
|
37
53
|
}
|
|
38
54
|
|
|
39
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Load once and share the result: repeated hub opens / frame-inspector mounts reuse
|
|
57
|
+
* the resolved state (and coalesce a concurrent in-flight request) instead of each
|
|
58
|
+
* re-fetching the connection + the whole configs list. Use `load()` to force a refresh.
|
|
59
|
+
*/
|
|
60
|
+
async function ensureLoaded() {
|
|
61
|
+
if (available.value !== null) return
|
|
62
|
+
if (!inFlight) inFlight = load().finally(() => (inFlight = null))
|
|
63
|
+
return inFlight
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function saveConnection(input: UpsertObservabilityConnectionInput) {
|
|
40
67
|
const ws = useWorkspaceStore()
|
|
41
|
-
connection.value = await api.
|
|
68
|
+
connection.value = await api.setObservabilityConnection(ws.requireId(), input)
|
|
69
|
+
available.value = true
|
|
42
70
|
}
|
|
43
71
|
|
|
44
72
|
async function removeConnection() {
|
|
45
73
|
const ws = useWorkspaceStore()
|
|
46
|
-
await api.
|
|
47
|
-
connection.value = { connected: false,
|
|
74
|
+
await api.deleteObservabilityConnection(ws.requireId())
|
|
75
|
+
connection.value = { connected: false, provider: null, summary: null }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The saved config for a specific block (the service inspector reads this). */
|
|
79
|
+
function configForBlock(blockId: string): ReleaseHealthConfig | undefined {
|
|
80
|
+
return configs.value.find((c) => c.blockId === blockId)
|
|
48
81
|
}
|
|
49
82
|
|
|
50
83
|
async function saveConfig(blockId: string, input: UpsertReleaseHealthConfigInput) {
|
|
@@ -66,9 +99,12 @@ export const useReleaseHealthStore = defineStore('releaseHealth', () => {
|
|
|
66
99
|
connection,
|
|
67
100
|
configs,
|
|
68
101
|
loading,
|
|
102
|
+
available,
|
|
69
103
|
load,
|
|
104
|
+
ensureLoaded,
|
|
70
105
|
saveConnection,
|
|
71
106
|
removeConnection,
|
|
107
|
+
configForBlock,
|
|
72
108
|
saveConfig,
|
|
73
109
|
removeConfig,
|
|
74
110
|
}
|