@cat-factory/app 0.199.0 → 0.200.2
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/foundational/FoundationalContractSummary.vue +36 -0
- package/app/components/foundational/FoundationalServiceCatalogList.vue +170 -0
- package/app/components/foundational/FoundationalServiceManager.vue +111 -0
- package/app/components/foundational/FoundationalServicePanel.vue +37 -0
- package/app/components/foundational/FoundationalServiceRegistry.vue +339 -0
- package/app/components/foundational/FoundationalServiceSources.vue +398 -0
- package/app/components/foundational/FoundationalSuppressions.vue +75 -0
- package/app/components/layout/AccountFoundationalSettings.vue +25 -0
- package/app/components/layout/AccountSkillSettings.vue +1 -1
- package/app/components/settings/AccountSettingsPanel.vue +17 -1
- package/app/components/skills/SkillLibraryManager.vue +1 -1
- package/app/composables/api/foundationalServices.ts +131 -0
- package/app/composables/api/skills.ts +1 -1
- package/app/composables/useApi.ts +2 -0
- package/app/composables/useNavContributions.ts +1 -0
- package/app/composables/usePipelineErrorToast.ts +8 -0
- package/app/modular/nav-contributions.spec.ts +6 -0
- package/app/modular/nav-contributions.ts +26 -0
- package/app/pages/index.vue +4 -0
- package/app/stores/foundationalServices.spec.ts +121 -0
- package/app/stores/foundationalServices.ts +276 -0
- package/app/stores/skillLibrary.ts +1 -1
- package/app/stores/skills.spec.ts +1 -1
- package/app/stores/skills.ts +1 -1
- package/app/stores/ui/modals.ts +12 -0
- package/app/types/domain.ts +1 -0
- package/app/types/foundationalServices.ts +32 -0
- package/app/types/skills.ts +1 -1
- package/i18n/locales/de.json +146 -6
- package/i18n/locales/en.json +146 -6
- package/i18n/locales/es.json +146 -6
- package/i18n/locales/fr.json +146 -6
- package/i18n/locales/he.json +146 -6
- package/i18n/locales/it.json +146 -6
- package/i18n/locales/ja.json +146 -6
- package/i18n/locales/pl.json +146 -6
- package/i18n/locales/tr.json +146 -6
- package/i18n/locales/uk.json +146 -6
- package/package.json +2 -2
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Repo sources of foundational-service definitions (backend/docs/adr/0031-foundational-services.md).
|
|
3
|
+
// Two link shapes, and the choice is the whole reason this form has a mode switch:
|
|
4
|
+
// - `directory` — every immediate subdirectory of the linked path is a service, identified by
|
|
5
|
+
// its `service.md`, with its contract files beside it. The "we keep our specs in a repo" case.
|
|
6
|
+
// - `files` — an explicit list of contract files, all describing the ONE service the link
|
|
7
|
+
// names. The "just point at my openapi.yaml" case, where there is no directory convention to
|
|
8
|
+
// adopt — which is why the link itself must supply the identity, and the form demands it.
|
|
9
|
+
// Mirrors the skill library's sources UI: with the GitHub App connected the user searches a repo
|
|
10
|
+
// and browses to a path; otherwise the manual owner/name fields are the fallback.
|
|
11
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
12
|
+
import type {
|
|
13
|
+
FoundationalServiceOwnerKind,
|
|
14
|
+
FoundationalServiceSourceMode,
|
|
15
|
+
GitHubAvailableRepo,
|
|
16
|
+
} from '~/types/domain'
|
|
17
|
+
import {
|
|
18
|
+
useFoundationalServices,
|
|
19
|
+
useFoundationalServicesStore,
|
|
20
|
+
} from '~/stores/foundationalServices'
|
|
21
|
+
import GitHubRepoSearchSelect from '~/components/github/GitHubRepoSearchSelect.vue'
|
|
22
|
+
import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
|
|
23
|
+
|
|
24
|
+
const props = defineProps<{ kind: FoundationalServiceOwnerKind; ownerId: string }>()
|
|
25
|
+
|
|
26
|
+
const catalog =
|
|
27
|
+
props.kind === 'workspace'
|
|
28
|
+
? useFoundationalServicesStore()
|
|
29
|
+
: useFoundationalServices(props.kind, props.ownerId)
|
|
30
|
+
const github = useGitHubStore()
|
|
31
|
+
const toast = useToast()
|
|
32
|
+
const { t, d } = useI18n()
|
|
33
|
+
const { confirm } = useConfirm()
|
|
34
|
+
|
|
35
|
+
// The rich GitHub pickers reuse the active board's App installation; without it the form falls
|
|
36
|
+
// back to manual text entry.
|
|
37
|
+
const githubReady = computed(() => github.available === true && github.connected)
|
|
38
|
+
|
|
39
|
+
function notifyError(title: string, e: unknown) {
|
|
40
|
+
toast.add({
|
|
41
|
+
title,
|
|
42
|
+
description: e instanceof Error ? e.message : String(e),
|
|
43
|
+
icon: 'i-lucide-triangle-alert',
|
|
44
|
+
color: 'error',
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Per-row in-flight tracking so only the control that triggered an action spins.
|
|
49
|
+
const busyRows = reactive(new Set<string>())
|
|
50
|
+
const rowBusy = (key: string) => busyRows.has(key)
|
|
51
|
+
async function withRow(key: string, fn: () => Promise<void>) {
|
|
52
|
+
if (busyRows.has(key)) return
|
|
53
|
+
busyRows.add(key)
|
|
54
|
+
try {
|
|
55
|
+
await fn()
|
|
56
|
+
} finally {
|
|
57
|
+
busyRows.delete(key)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---- link a repo source ----------------------------------------------------
|
|
62
|
+
const mode = ref<FoundationalServiceSourceMode>('directory')
|
|
63
|
+
const repoId = ref<number | undefined>(undefined)
|
|
64
|
+
const repo = ref<GitHubAvailableRepo | undefined>(undefined)
|
|
65
|
+
const dirPath = ref<string | undefined>(undefined)
|
|
66
|
+
const filePaths = ref<string[]>([])
|
|
67
|
+
const gitRef = ref('')
|
|
68
|
+
const manual = reactive({ repoOwner: '', repoName: '', dirPath: '', filePaths: '' })
|
|
69
|
+
const named = reactive({ serviceId: '', serviceName: '', serviceSummary: '' })
|
|
70
|
+
const linking = ref(false)
|
|
71
|
+
|
|
72
|
+
// A new repo selection clears whatever was browsed against the previous one.
|
|
73
|
+
watch(repoId, () => {
|
|
74
|
+
dirPath.value = undefined
|
|
75
|
+
filePaths.value = []
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const modeItems = computed(() => [
|
|
79
|
+
{ value: 'directory' as const, label: t('foundational.sources.mode.directory') },
|
|
80
|
+
{ value: 'files' as const, label: t('foundational.sources.mode.files') },
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
const ownerName = computed<{ owner: string; name: string } | null>(() => {
|
|
84
|
+
if (githubReady.value) {
|
|
85
|
+
return repo.value ? { owner: repo.value.owner, name: repo.value.name } : null
|
|
86
|
+
}
|
|
87
|
+
const owner = manual.repoOwner.trim()
|
|
88
|
+
const name = manual.repoName.trim()
|
|
89
|
+
return owner && name ? { owner, name } : null
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
/** The linked files, from the browser cart or the manual newline/comma list. */
|
|
93
|
+
const linkedFiles = computed(() =>
|
|
94
|
+
githubReady.value
|
|
95
|
+
? filePaths.value
|
|
96
|
+
: manual.filePaths
|
|
97
|
+
.split(/[\n,]/)
|
|
98
|
+
.map((p) => p.trim())
|
|
99
|
+
.filter(Boolean),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
// A `files` source names the service its files describe — the backend refuses the link
|
|
103
|
+
// otherwise, so the button is disabled rather than letting the user discover it as a 422.
|
|
104
|
+
const valid = computed(() => {
|
|
105
|
+
if (!ownerName.value) return false
|
|
106
|
+
if (mode.value !== 'files') return true
|
|
107
|
+
return Boolean(named.serviceId.trim() && named.serviceName.trim() && linkedFiles.value.length)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
function resetDraft() {
|
|
111
|
+
repoId.value = undefined
|
|
112
|
+
repo.value = undefined
|
|
113
|
+
dirPath.value = undefined
|
|
114
|
+
filePaths.value = []
|
|
115
|
+
gitRef.value = ''
|
|
116
|
+
Object.assign(manual, { repoOwner: '', repoName: '', dirPath: '', filePaths: '' })
|
|
117
|
+
Object.assign(named, { serviceId: '', serviceName: '', serviceSummary: '' })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function toggleFile(path: string) {
|
|
121
|
+
filePaths.value = filePaths.value.includes(path)
|
|
122
|
+
? filePaths.value.filter((p) => p !== path)
|
|
123
|
+
: [...filePaths.value, path]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function link() {
|
|
127
|
+
const target = ownerName.value
|
|
128
|
+
if (!target || !valid.value) return
|
|
129
|
+
linking.value = true
|
|
130
|
+
try {
|
|
131
|
+
const result = await catalog.linkSource({
|
|
132
|
+
repoOwner: target.owner,
|
|
133
|
+
repoName: target.name,
|
|
134
|
+
gitRef: gitRef.value.trim() || undefined,
|
|
135
|
+
mode: mode.value,
|
|
136
|
+
dirPath: (githubReady.value ? dirPath.value : manual.dirPath.trim()) || undefined,
|
|
137
|
+
...(mode.value === 'files'
|
|
138
|
+
? {
|
|
139
|
+
filePaths: linkedFiles.value,
|
|
140
|
+
serviceId: named.serviceId.trim(),
|
|
141
|
+
serviceName: named.serviceName.trim(),
|
|
142
|
+
serviceSummary: named.serviceSummary.trim() || undefined,
|
|
143
|
+
}
|
|
144
|
+
: {}),
|
|
145
|
+
})
|
|
146
|
+
resetDraft()
|
|
147
|
+
toast.add({ title: t('foundational.toast.sourceLinked'), icon: 'i-lucide-git-branch' })
|
|
148
|
+
return result
|
|
149
|
+
} catch (e) {
|
|
150
|
+
notifyError(t('foundational.toast.linkSourceFailed'), e)
|
|
151
|
+
} finally {
|
|
152
|
+
linking.value = false
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function sync(id: string) {
|
|
157
|
+
await withRow(`sync:${id}`, async () => {
|
|
158
|
+
try {
|
|
159
|
+
const result = await catalog.syncSource(id)
|
|
160
|
+
toast.add({
|
|
161
|
+
title: t('foundational.toast.synced', {
|
|
162
|
+
updated: result.upserted,
|
|
163
|
+
removed: result.tombstoned,
|
|
164
|
+
}),
|
|
165
|
+
icon: 'i-lucide-refresh-cw',
|
|
166
|
+
color: 'info',
|
|
167
|
+
})
|
|
168
|
+
} catch (e) {
|
|
169
|
+
notifyError(t('foundational.toast.syncFailed'), e)
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function check(id: string) {
|
|
175
|
+
await withRow(`check:${id}`, async () => {
|
|
176
|
+
try {
|
|
177
|
+
const status = await catalog.checkSource(id)
|
|
178
|
+
toast.add({
|
|
179
|
+
title: status.changed
|
|
180
|
+
? t('foundational.toast.changesAvailable')
|
|
181
|
+
: t('foundational.toast.upToDate'),
|
|
182
|
+
icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
|
|
183
|
+
})
|
|
184
|
+
} catch (e) {
|
|
185
|
+
notifyError(t('foundational.toast.checkSourceFailed'), e)
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function unlink(id: string) {
|
|
191
|
+
const source = catalog.sources.find((s) => s.id === id)
|
|
192
|
+
const ok = await confirm({
|
|
193
|
+
title: t('foundational.confirmUnlinkSource.title'),
|
|
194
|
+
description: t('foundational.confirmUnlinkSource.body', {
|
|
195
|
+
repo: source ? `${source.repoOwner}/${source.repoName}` : '',
|
|
196
|
+
}),
|
|
197
|
+
variant: 'destructive',
|
|
198
|
+
confirmLabel: t('foundational.confirmUnlinkSource.confirm'),
|
|
199
|
+
icon: 'i-lucide-unplug',
|
|
200
|
+
})
|
|
201
|
+
if (!ok) return
|
|
202
|
+
await withRow(`unlink:${id}`, async () => {
|
|
203
|
+
try {
|
|
204
|
+
await catalog.unlinkSource(id)
|
|
205
|
+
toast.add({ title: t('foundational.toast.sourceUnlinked'), icon: 'i-lucide-unplug' })
|
|
206
|
+
} catch (e) {
|
|
207
|
+
notifyError(t('foundational.toast.unlinkSourceFailed'), e)
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
</script>
|
|
212
|
+
|
|
213
|
+
<template>
|
|
214
|
+
<div class="flex flex-col gap-3" data-testid="foundational-sources">
|
|
215
|
+
<div
|
|
216
|
+
v-for="s in catalog.sources"
|
|
217
|
+
:key="s.id"
|
|
218
|
+
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/60 p-3"
|
|
219
|
+
>
|
|
220
|
+
<UIcon name="i-lucide-git-branch" class="h-4 w-4 shrink-0 text-slate-400" />
|
|
221
|
+
<div class="min-w-0">
|
|
222
|
+
<span class="font-mono text-sm text-slate-100">
|
|
223
|
+
{{ s.repoOwner }}/{{ s.repoName }}<span class="text-slate-500">/{{ s.dirPath }}</span>
|
|
224
|
+
</span>
|
|
225
|
+
<p class="text-xs text-slate-500">
|
|
226
|
+
{{
|
|
227
|
+
s.mode === 'files'
|
|
228
|
+
? t('foundational.sources.metaFiles', {
|
|
229
|
+
service: s.serviceName ?? s.serviceId ?? '',
|
|
230
|
+
count: s.filePaths.length,
|
|
231
|
+
})
|
|
232
|
+
: t('foundational.sources.metaDirectory')
|
|
233
|
+
}}
|
|
234
|
+
</p>
|
|
235
|
+
<p class="text-xs text-slate-500">
|
|
236
|
+
{{
|
|
237
|
+
s.lastSyncedAt
|
|
238
|
+
? t('foundational.sources.metaSynced', {
|
|
239
|
+
ref: s.gitRef,
|
|
240
|
+
date: d(new Date(s.lastSyncedAt), 'short'),
|
|
241
|
+
})
|
|
242
|
+
: t('foundational.sources.metaNever', { ref: s.gitRef })
|
|
243
|
+
}}
|
|
244
|
+
</p>
|
|
245
|
+
</div>
|
|
246
|
+
<UBadge
|
|
247
|
+
v-if="catalog.sourceChanges[s.id]"
|
|
248
|
+
size="xs"
|
|
249
|
+
color="warning"
|
|
250
|
+
variant="subtle"
|
|
251
|
+
class="ms-auto"
|
|
252
|
+
>
|
|
253
|
+
{{ t('foundational.sources.changes') }}
|
|
254
|
+
</UBadge>
|
|
255
|
+
<div class="ms-auto flex gap-1">
|
|
256
|
+
<UButton
|
|
257
|
+
icon="i-lucide-search-check"
|
|
258
|
+
size="xs"
|
|
259
|
+
variant="ghost"
|
|
260
|
+
:loading="rowBusy(`check:${s.id}`)"
|
|
261
|
+
:title="t('foundational.sources.check')"
|
|
262
|
+
@click="check(s.id)"
|
|
263
|
+
/>
|
|
264
|
+
<UButton
|
|
265
|
+
icon="i-lucide-refresh-cw"
|
|
266
|
+
size="xs"
|
|
267
|
+
variant="ghost"
|
|
268
|
+
:loading="rowBusy(`sync:${s.id}`)"
|
|
269
|
+
:title="t('foundational.sources.sync')"
|
|
270
|
+
@click="sync(s.id)"
|
|
271
|
+
/>
|
|
272
|
+
<UButton
|
|
273
|
+
icon="i-lucide-unplug"
|
|
274
|
+
size="xs"
|
|
275
|
+
color="error"
|
|
276
|
+
variant="ghost"
|
|
277
|
+
:loading="rowBusy(`unlink:${s.id}`)"
|
|
278
|
+
:title="t('foundational.sources.unlink')"
|
|
279
|
+
@click="unlink(s.id)"
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
<p v-if="!catalog.sources.length" class="text-sm text-slate-500">
|
|
284
|
+
{{ t('foundational.sources.empty') }}
|
|
285
|
+
</p>
|
|
286
|
+
|
|
287
|
+
<!-- Linking needs the GitHub integration; say so rather than offering a form that 503s. -->
|
|
288
|
+
<div
|
|
289
|
+
v-if="!catalog.sourcesAvailable"
|
|
290
|
+
class="rounded-md border border-slate-800 bg-slate-900/40 p-3 text-sm text-slate-500"
|
|
291
|
+
>
|
|
292
|
+
{{ t('foundational.sources.githubRequired') }}
|
|
293
|
+
</div>
|
|
294
|
+
<div v-else class="rounded-md border border-slate-800 p-3">
|
|
295
|
+
<p class="mb-2 text-sm font-medium">{{ t('foundational.sources.linkTitle') }}</p>
|
|
296
|
+
<div class="flex flex-col gap-2">
|
|
297
|
+
<URadioGroup v-model="mode" :items="modeItems" orientation="horizontal" size="sm" />
|
|
298
|
+
<p class="text-xs text-slate-500">
|
|
299
|
+
{{
|
|
300
|
+
mode === 'files'
|
|
301
|
+
? t('foundational.sources.mode.filesHint')
|
|
302
|
+
: t('foundational.sources.mode.directoryHint')
|
|
303
|
+
}}
|
|
304
|
+
</p>
|
|
305
|
+
|
|
306
|
+
<!-- Connected: search a repo, then browse to the folder / pick the contract files -->
|
|
307
|
+
<template v-if="githubReady">
|
|
308
|
+
<GitHubRepoSearchSelect v-model="repoId" @update:repo="repo = $event" />
|
|
309
|
+
<div
|
|
310
|
+
v-if="repoId !== undefined"
|
|
311
|
+
class="rounded-md border border-slate-800 bg-slate-900/40 p-2"
|
|
312
|
+
>
|
|
313
|
+
<RepoTreeBrowser
|
|
314
|
+
v-if="mode === 'files'"
|
|
315
|
+
:repo-github-id="repoId"
|
|
316
|
+
mode="file"
|
|
317
|
+
multiple
|
|
318
|
+
:selected-paths="filePaths"
|
|
319
|
+
@toggle="toggleFile"
|
|
320
|
+
/>
|
|
321
|
+
<RepoTreeBrowser v-else v-model="dirPath" :repo-github-id="repoId" mode="dir" />
|
|
322
|
+
<p class="mt-2 truncate text-xs text-slate-400">
|
|
323
|
+
<template v-if="mode === 'files'">
|
|
324
|
+
{{ t('foundational.sources.selectedFiles', { count: filePaths.length }) }}
|
|
325
|
+
</template>
|
|
326
|
+
<template v-else-if="dirPath">
|
|
327
|
+
{{ t('foundational.sources.selectedDir') }}
|
|
328
|
+
<code class="text-slate-200">{{ dirPath }}</code>
|
|
329
|
+
</template>
|
|
330
|
+
<template v-else>{{ t('foundational.sources.wholeRepo') }}</template>
|
|
331
|
+
</p>
|
|
332
|
+
</div>
|
|
333
|
+
</template>
|
|
334
|
+
|
|
335
|
+
<!-- Not connected to the App: manual owner/name + path fallback -->
|
|
336
|
+
<template v-else>
|
|
337
|
+
<div class="flex gap-2">
|
|
338
|
+
<UInput
|
|
339
|
+
v-model="manual.repoOwner"
|
|
340
|
+
:placeholder="t('foundational.sources.ownerPlaceholder')"
|
|
341
|
+
class="flex-1"
|
|
342
|
+
/>
|
|
343
|
+
<UInput
|
|
344
|
+
v-model="manual.repoName"
|
|
345
|
+
:placeholder="t('foundational.sources.repoPlaceholder')"
|
|
346
|
+
class="flex-1"
|
|
347
|
+
/>
|
|
348
|
+
</div>
|
|
349
|
+
<UTextarea
|
|
350
|
+
v-if="mode === 'files'"
|
|
351
|
+
v-model="manual.filePaths"
|
|
352
|
+
:rows="3"
|
|
353
|
+
:placeholder="t('foundational.sources.filePathsPlaceholder')"
|
|
354
|
+
/>
|
|
355
|
+
<UInput
|
|
356
|
+
v-else
|
|
357
|
+
v-model="manual.dirPath"
|
|
358
|
+
:placeholder="t('foundational.sources.dirPlaceholder')"
|
|
359
|
+
/>
|
|
360
|
+
</template>
|
|
361
|
+
|
|
362
|
+
<!-- `files` mode carries no directory convention to read identity from, so the link
|
|
363
|
+
supplies it. -->
|
|
364
|
+
<template v-if="mode === 'files'">
|
|
365
|
+
<div class="flex gap-2">
|
|
366
|
+
<UInput
|
|
367
|
+
v-model="named.serviceId"
|
|
368
|
+
:placeholder="t('foundational.sources.serviceIdPlaceholder')"
|
|
369
|
+
class="flex-1"
|
|
370
|
+
/>
|
|
371
|
+
<UInput
|
|
372
|
+
v-model="named.serviceName"
|
|
373
|
+
:placeholder="t('foundational.sources.serviceNamePlaceholder')"
|
|
374
|
+
class="flex-1"
|
|
375
|
+
/>
|
|
376
|
+
</div>
|
|
377
|
+
<UInput
|
|
378
|
+
v-model="named.serviceSummary"
|
|
379
|
+
:placeholder="t('foundational.sources.serviceSummaryPlaceholder')"
|
|
380
|
+
/>
|
|
381
|
+
</template>
|
|
382
|
+
|
|
383
|
+
<UInput v-model="gitRef" :placeholder="t('foundational.sources.refPlaceholder')" />
|
|
384
|
+
<UButton
|
|
385
|
+
icon="i-lucide-link"
|
|
386
|
+
size="sm"
|
|
387
|
+
:disabled="!valid"
|
|
388
|
+
:loading="linking"
|
|
389
|
+
class="self-start"
|
|
390
|
+
data-testid="foundational-link-source"
|
|
391
|
+
@click="link"
|
|
392
|
+
>
|
|
393
|
+
{{ t('foundational.sources.link') }}
|
|
394
|
+
</UButton>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</template>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// What this board is opted OUT of (backend/docs/adr/0031-foundational-services.md). A suppressed id is
|
|
3
|
+
// by construction absent from the merged catalog, so without this list the suppress action would
|
|
4
|
+
// be a one-way door — which is why it is a section of its own rather than a badge on the catalog.
|
|
5
|
+
//
|
|
6
|
+
// `inherited: false` is rendered distinctly rather than hidden: the tombstone shadows nothing
|
|
7
|
+
// today (the account withdrew the service, or this row is what remains of the board deleting its
|
|
8
|
+
// own registration), and an operator reading it as "a capability is being withheld" would go
|
|
9
|
+
// looking for something that is not there.
|
|
10
|
+
import { reactive } from 'vue'
|
|
11
|
+
import { useFoundationalServicesStore } from '~/stores/foundationalServices'
|
|
12
|
+
|
|
13
|
+
const catalog = useFoundationalServicesStore()
|
|
14
|
+
const toast = useToast()
|
|
15
|
+
const { t } = useI18n()
|
|
16
|
+
|
|
17
|
+
const busyRows = reactive(new Set<string>())
|
|
18
|
+
const rowBusy = (id: string) => busyRows.has(id)
|
|
19
|
+
|
|
20
|
+
async function restore(serviceId: string) {
|
|
21
|
+
if (busyRows.has(serviceId)) return
|
|
22
|
+
busyRows.add(serviceId)
|
|
23
|
+
try {
|
|
24
|
+
await catalog.restore(serviceId)
|
|
25
|
+
toast.add({ title: t('foundational.toast.restored'), icon: 'i-lucide-eye' })
|
|
26
|
+
} catch (e) {
|
|
27
|
+
toast.add({
|
|
28
|
+
title: t('foundational.toast.restoreFailed'),
|
|
29
|
+
description: e instanceof Error ? e.message : String(e),
|
|
30
|
+
icon: 'i-lucide-triangle-alert',
|
|
31
|
+
color: 'error',
|
|
32
|
+
})
|
|
33
|
+
} finally {
|
|
34
|
+
busyRows.delete(serviceId)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div
|
|
41
|
+
v-if="catalog.suppressions.length"
|
|
42
|
+
class="flex flex-col gap-2"
|
|
43
|
+
data-testid="foundational-suppressions"
|
|
44
|
+
>
|
|
45
|
+
<p class="text-sm font-medium">{{ t('foundational.suppressions.title') }}</p>
|
|
46
|
+
<p class="text-xs text-slate-500">{{ t('foundational.suppressions.intro') }}</p>
|
|
47
|
+
<div
|
|
48
|
+
v-for="s in catalog.suppressions"
|
|
49
|
+
:key="s.id"
|
|
50
|
+
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/40 p-3"
|
|
51
|
+
>
|
|
52
|
+
<UIcon name="i-lucide-eye-off" class="h-4 w-4 shrink-0 text-slate-500" />
|
|
53
|
+
<div class="min-w-0 flex-1">
|
|
54
|
+
<p class="truncate text-sm text-slate-300">
|
|
55
|
+
{{ s.name || s.id }}
|
|
56
|
+
<code v-if="s.name" class="ms-1 text-[11px] text-slate-500">{{ s.id }}</code>
|
|
57
|
+
</p>
|
|
58
|
+
<p v-if="s.summary" class="text-xs text-slate-500">{{ s.summary }}</p>
|
|
59
|
+
<p v-if="!s.inherited" class="text-[11px] text-slate-500">
|
|
60
|
+
{{ t('foundational.suppressions.shadowsNothing') }}
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
<UButton
|
|
64
|
+
icon="i-lucide-eye"
|
|
65
|
+
size="xs"
|
|
66
|
+
variant="ghost"
|
|
67
|
+
:loading="rowBusy(s.id)"
|
|
68
|
+
:data-testid="`foundational-restore-${s.id}`"
|
|
69
|
+
@click="restore(s.id)"
|
|
70
|
+
>
|
|
71
|
+
{{ t('foundational.suppressions.restore') }}
|
|
72
|
+
</UButton>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Account-tier foundational services (backend/docs/adr/0031-foundational-services.md): the shared
|
|
3
|
+
// capabilities the whole organisation runs — file storage, notifications, audit — which every
|
|
4
|
+
// board in the account inherits and can override or opt out of. A body-only section rendered in
|
|
5
|
+
// the "Foundational services" tab of AccountSettingsPanel; available for ALL account types.
|
|
6
|
+
import FoundationalServiceManager from '~/components/foundational/FoundationalServiceManager.vue'
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{ accountId: string }>()
|
|
9
|
+
const { t } = useI18n()
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<div class="space-y-6 text-sm">
|
|
14
|
+
<section>
|
|
15
|
+
<p class="mb-3 text-[11px] text-slate-400">
|
|
16
|
+
{{ t('layout.accountFoundational.intro') }}
|
|
17
|
+
</p>
|
|
18
|
+
<FoundationalServiceManager
|
|
19
|
+
kind="account"
|
|
20
|
+
:owner-id="props.accountId"
|
|
21
|
+
:show-catalog="false"
|
|
22
|
+
/>
|
|
23
|
+
</section>
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// Account-tier repo-sourced Claude Skills library (
|
|
2
|
+
// Account-tier repo-sourced Claude Skills library (ADR 0024): a team
|
|
3
3
|
// authors skills in a repo (`<skill>/SKILL.md` + resources), the account syncs them into a
|
|
4
4
|
// catalog shared across its workspaces, and a pipeline `skill` step runs one. A body-only
|
|
5
5
|
// section rendered in the "Skills" tab of AccountSettingsPanel; available for ALL account types.
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
// Account settings — a single tabbed modal for the per-account configuration, distinct
|
|
3
3
|
// from Workspace settings. Hosts the team panel (members + roles, invitations, email
|
|
4
4
|
// sender, account-wide API keys; org-scoped, with a create-org CTA on a personal account)
|
|
5
|
-
//
|
|
5
|
+
// the account-tier prompt-fragment library, the repo-sourced skills, and the foundational-service
|
|
6
|
+
// catalog (all available for every account type).
|
|
6
7
|
// Opened from the SideBar Configuration section, the account switcher and the command
|
|
7
8
|
// bar; bound to the `ui` store so any surface can open it and deep-link to a tab.
|
|
8
9
|
import AccountTeamSettings from '~/components/layout/AccountTeamSettings.vue'
|
|
9
10
|
import AccountFragmentSettings from '~/components/layout/AccountFragmentSettings.vue'
|
|
10
11
|
import AccountSkillSettings from '~/components/layout/AccountSkillSettings.vue'
|
|
12
|
+
import AccountFoundationalSettings from '~/components/layout/AccountFoundationalSettings.vue'
|
|
11
13
|
|
|
12
14
|
const { t } = useI18n()
|
|
13
15
|
const ui = useUiStore()
|
|
@@ -39,6 +41,12 @@ const tabs = computed(() => [
|
|
|
39
41
|
icon: 'i-lucide-book-open-check',
|
|
40
42
|
slot: 'skills',
|
|
41
43
|
},
|
|
44
|
+
{
|
|
45
|
+
value: 'foundational',
|
|
46
|
+
label: t('settings.account.tabs.foundational'),
|
|
47
|
+
icon: 'i-lucide-boxes',
|
|
48
|
+
slot: 'foundational',
|
|
49
|
+
},
|
|
42
50
|
])
|
|
43
51
|
</script>
|
|
44
52
|
|
|
@@ -61,6 +69,14 @@ const tabs = computed(() => [
|
|
|
61
69
|
<template #fragments>
|
|
62
70
|
<AccountFragmentSettings :account-id="accounts.activeAccountId" />
|
|
63
71
|
</template>
|
|
72
|
+
<template #foundational>
|
|
73
|
+
<!-- Key on the account for the same reason the skills tab is: a mid-modal account
|
|
74
|
+
switch must remount against a fresh owner-keyed store, not the stale initial one. -->
|
|
75
|
+
<AccountFoundationalSettings
|
|
76
|
+
:key="accounts.activeAccountId ?? undefined"
|
|
77
|
+
:account-id="accounts.activeAccountId"
|
|
78
|
+
/>
|
|
79
|
+
</template>
|
|
64
80
|
<template #skills>
|
|
65
81
|
<!-- Key on the account so a mid-modal account switch remounts against a fresh
|
|
66
82
|
account-keyed skill-library store rather than the stale initial one. -->
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// Repo-sourced Claude Skills library manager (
|
|
2
|
+
// Repo-sourced Claude Skills library manager (ADR 0024), for the account
|
|
3
3
|
// tier (skills are a single tier — shared across the account's workspaces). Link repo directories
|
|
4
4
|
// of `<skill>/SKILL.md` folders, resync them (with a "changes available" badge), and review the
|
|
5
5
|
// synced skill catalog a pipeline `skill` step picks from. Mirrors the fragment library's
|