@cat-factory/app 0.121.0 → 0.121.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/composables/useUpsertList.spec.ts +13 -0
- package/app/stores/accounts.ts +9 -9
- package/app/stores/agentRuns.spec.ts +54 -1
- package/app/stores/agentRuns.ts +10 -19
- package/app/stores/bootstrap.ts +9 -5
- package/app/stores/github.ts +15 -8
- package/app/stores/pipelines.ts +12 -10
- package/app/stores/releaseHealth.ts +8 -5
- package/app/stores/sharedStacks.ts +3 -8
- package/app/stores/ui/modals.ts +810 -0
- package/app/stores/ui/navigation.ts +63 -0
- package/app/stores/ui/resultViews.ts +246 -0
- package/app/stores/ui.spec.ts +30 -0
- package/app/stores/ui.ts +19 -1072
- package/package.json +1 -1
|
@@ -25,6 +25,19 @@ describe('useUpsertList', () => {
|
|
|
25
25
|
expect(items.value.map((x) => x.id)).toEqual(['b', 'a'])
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
+
it('replaces an existing item in place under prepend (no reorder)', () => {
|
|
29
|
+
// The github `pulls` list is prepend (newest-first); re-opening / optimistically
|
|
30
|
+
// merging an existing PR must replace it where it sits, not bump it to the front.
|
|
31
|
+
const { items, upsert } = useUpsertList<Item>({ key: (x) => x.id, prepend: true })
|
|
32
|
+
upsert({ id: 'a', v: 1 })
|
|
33
|
+
upsert({ id: 'b', v: 2 })
|
|
34
|
+
upsert({ id: 'a', v: 9 }) // existing key → replace in place
|
|
35
|
+
expect(items.value).toEqual([
|
|
36
|
+
{ id: 'b', v: 2 },
|
|
37
|
+
{ id: 'a', v: 9 },
|
|
38
|
+
])
|
|
39
|
+
})
|
|
40
|
+
|
|
28
41
|
it('removes by key and looks up by key', () => {
|
|
29
42
|
const { items, upsert, remove, get } = useUpsertList<Item>({ key: (x) => x.id })
|
|
30
43
|
upsert({ id: 'a', v: 1 })
|
package/app/stores/accounts.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
|
+
import { useUpsertList } from '~/composables/useUpsertList'
|
|
3
4
|
import type {
|
|
4
5
|
Account,
|
|
5
6
|
AccountInvitation,
|
|
@@ -23,7 +24,7 @@ export const useAccountsStore = defineStore(
|
|
|
23
24
|
() => {
|
|
24
25
|
const api = useApi()
|
|
25
26
|
|
|
26
|
-
const accounts =
|
|
27
|
+
const { items: accounts, upsert: upsertAccount } = useUpsertList<Account>({ key: (a) => a.id })
|
|
27
28
|
/** Active account id (persisted so a reload keeps the same context). */
|
|
28
29
|
const activeAccountId = ref<string | null>(null)
|
|
29
30
|
const ready = ref(false)
|
|
@@ -46,7 +47,7 @@ export const useAccountsStore = defineStore(
|
|
|
46
47
|
/** Create a shared org account and make it active. */
|
|
47
48
|
async function createOrg(name: string) {
|
|
48
49
|
const account = await api.createAccount({ name })
|
|
49
|
-
|
|
50
|
+
upsertAccount(account)
|
|
50
51
|
activeAccountId.value = account.id
|
|
51
52
|
return account
|
|
52
53
|
}
|
|
@@ -62,8 +63,7 @@ export const useAccountsStore = defineStore(
|
|
|
62
63
|
*/
|
|
63
64
|
async function setDefaultCloudProvider(id: string, provider: CloudProvider) {
|
|
64
65
|
const updated = await api.updateAccount(id, { defaultCloudProvider: provider })
|
|
65
|
-
|
|
66
|
-
if (i >= 0) accounts.value[i] = updated
|
|
66
|
+
upsertAccount(updated)
|
|
67
67
|
return updated
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -73,14 +73,15 @@ export const useAccountsStore = defineStore(
|
|
|
73
73
|
*/
|
|
74
74
|
async function setSpendMonthlyLimit(id: string, limit: number | null) {
|
|
75
75
|
const updated = await api.updateAccount(id, { spendMonthlyLimit: limit })
|
|
76
|
-
|
|
77
|
-
if (i >= 0) accounts.value[i] = updated
|
|
76
|
+
upsertAccount(updated)
|
|
78
77
|
return updated
|
|
79
78
|
}
|
|
80
79
|
|
|
81
80
|
// ---- members + invitations -------------------------------------------
|
|
82
81
|
|
|
83
|
-
const members =
|
|
82
|
+
const { items: members, upsert: upsertMember } = useUpsertList<AccountMember>({
|
|
83
|
+
key: (m) => m.userId,
|
|
84
|
+
})
|
|
84
85
|
const invitations = ref<AccountInvitation[]>([])
|
|
85
86
|
|
|
86
87
|
/** Load the active account's member roster + pending invitations. */
|
|
@@ -108,8 +109,7 @@ export const useAccountsStore = defineStore(
|
|
|
108
109
|
/** Set a member's role set (admin-only); patches the loaded roster in place. */
|
|
109
110
|
async function setMemberRoles(accountId: string, userId: string, roles: AccountRole[]) {
|
|
110
111
|
const updated = await api.setMemberRoles(accountId, userId, roles)
|
|
111
|
-
|
|
112
|
-
if (i >= 0) members.value[i] = updated
|
|
112
|
+
upsertMember(updated)
|
|
113
113
|
return updated
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
-
import type { BootstrapJob } from '~/types/domain'
|
|
2
|
+
import type { BootstrapJob, EnvConfigRepairJob } from '~/types/domain'
|
|
3
3
|
import { useAgentRunsStore } from '~/stores/agentRuns'
|
|
4
4
|
|
|
5
5
|
/** Minimal BootstrapJob factory — only the fields the store's reconcile logic touches. */
|
|
@@ -72,3 +72,56 @@ describe('agentRuns store — monotonic bootstrap reconcile', () => {
|
|
|
72
72
|
expect(store.bootstrapJobs[0]!.status).toBe('succeeded')
|
|
73
73
|
})
|
|
74
74
|
})
|
|
75
|
+
|
|
76
|
+
/** Minimal EnvConfigRepairJob factory — only the fields the store touches. */
|
|
77
|
+
function repairJob(id: string, over: Partial<EnvConfigRepairJob> = {}): EnvConfigRepairJob {
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
workspaceId: 'ws_test',
|
|
81
|
+
owner: 'acme',
|
|
82
|
+
repo: 'svc',
|
|
83
|
+
branch: 'main',
|
|
84
|
+
status: 'running',
|
|
85
|
+
ok: null,
|
|
86
|
+
issues: [],
|
|
87
|
+
subtasks: null,
|
|
88
|
+
error: null,
|
|
89
|
+
failure: null,
|
|
90
|
+
createdAt: 1,
|
|
91
|
+
updatedAt: 1,
|
|
92
|
+
...over,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('agentRuns store — env-config-repair (plain useUpsertList adoption, candidate #3)', () => {
|
|
97
|
+
let store: ReturnType<typeof useAgentRunsStore>
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
store = useAgentRunsStore()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('upsertEnvConfigRepair prepends a new run (newest-first) and replaces in place by id', () => {
|
|
103
|
+
store.upsertEnvConfigRepair(repairJob('r1'))
|
|
104
|
+
store.upsertEnvConfigRepair(repairJob('r2'))
|
|
105
|
+
// Newest prepended, so r2 leads r1.
|
|
106
|
+
expect(store.envConfigRepairJobs.map((j) => j.id)).toEqual(['r2', 'r1'])
|
|
107
|
+
// A later event for r1 replaces it IN PLACE (no reorder), unlike a monotonic-guarded list.
|
|
108
|
+
store.upsertEnvConfigRepair(repairJob('r1', { status: 'succeeded' }))
|
|
109
|
+
expect(store.envConfigRepairJobs.map((j) => j.id)).toEqual(['r2', 'r1'])
|
|
110
|
+
expect(store.envConfigRepairById('r1')?.status).toBe('succeeded')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('envConfigRepairById looks a run up by id (absent → undefined)', () => {
|
|
114
|
+
store.upsertEnvConfigRepair(repairJob('r1'))
|
|
115
|
+
expect(store.envConfigRepairById('r1')?.id).toBe('r1')
|
|
116
|
+
expect(store.envConfigRepairById('nope')).toBeUndefined()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('hydrateEnvConfigRepair replaces the cache newest-first', () => {
|
|
120
|
+
store.upsertEnvConfigRepair(repairJob('stale'))
|
|
121
|
+
store.hydrateEnvConfigRepair([
|
|
122
|
+
repairJob('old', { createdAt: 1 }),
|
|
123
|
+
repairJob('new', { createdAt: 9 }),
|
|
124
|
+
])
|
|
125
|
+
expect(store.envConfigRepairJobs.map((j) => j.id)).toEqual(['new', 'old'])
|
|
126
|
+
})
|
|
127
|
+
})
|
package/app/stores/agentRuns.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
} from '~/types/domain'
|
|
10
10
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
11
11
|
import { useExecutionStore } from '~/stores/execution'
|
|
12
|
+
import { useUpsertList } from '~/composables/useUpsertList'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* A coarse, per-block view of the current "agent run" against a block, regardless
|
|
@@ -62,9 +63,15 @@ export const useAgentRunsStore = defineStore('agentRuns', () => {
|
|
|
62
63
|
* Env-config-repair runs for this workspace, newest-first. These have NO board block —
|
|
63
64
|
* they're surfaced only on the infrastructure-providers window (looked up by the
|
|
64
65
|
* `repairJobId` the `bootstrapRepo` response returned), so they're held separately and
|
|
65
|
-
* NOT merged into {@link byBlock}.
|
|
66
|
+
* NOT merged into {@link byBlock}. Unlike the bootstrap list this is a PLAIN find-by-id
|
|
67
|
+
* upsert (no `updatedAt` monotonic guard), so it routes through the shared
|
|
68
|
+
* {@link useUpsertList} helper (the last plain-upsert holdout, refactoring candidate #3).
|
|
66
69
|
*/
|
|
67
|
-
const
|
|
70
|
+
const {
|
|
71
|
+
items: envConfigRepairJobs,
|
|
72
|
+
upsert: upsertEnvConfigRepair,
|
|
73
|
+
get: envConfigRepairById,
|
|
74
|
+
} = useUpsertList<EnvConfigRepairJob>({ key: (j) => j.id, prepend: true })
|
|
68
75
|
|
|
69
76
|
/**
|
|
70
77
|
* Reconcile the cached bootstrap runs with a server snapshot for `workspaceId`. A snapshot is
|
|
@@ -96,27 +103,11 @@ export const useAgentRunsStore = defineStore('agentRuns', () => {
|
|
|
96
103
|
bootstrapJobs.value = [...reconciled, ...preserved].sort((a, b) => b.createdAt - a.createdAt)
|
|
97
104
|
}
|
|
98
105
|
|
|
99
|
-
/** Replace the cached env-config-repair runs with a server snapshot. */
|
|
106
|
+
/** Replace the cached env-config-repair runs with a server snapshot (newest-first). */
|
|
100
107
|
function hydrateEnvConfigRepair(jobs: EnvConfigRepairJob[]) {
|
|
101
108
|
envConfigRepairJobs.value = [...jobs].sort((a, b) => b.createdAt - a.createdAt)
|
|
102
109
|
}
|
|
103
110
|
|
|
104
|
-
/**
|
|
105
|
-
* Patch an env-config-repair run from a real-time `env-config-repair` event (or after
|
|
106
|
-
* launching one): replace it in place by id, else prepend it. Keeps the infra window's
|
|
107
|
-
* "repairing…" indicator reactive to live progress / outcome without a refetch.
|
|
108
|
-
*/
|
|
109
|
-
function upsertEnvConfigRepair(job: EnvConfigRepairJob) {
|
|
110
|
-
const i = envConfigRepairJobs.value.findIndex((j) => j.id === job.id)
|
|
111
|
-
if (i >= 0) envConfigRepairJobs.value[i] = job
|
|
112
|
-
else envConfigRepairJobs.value.unshift(job)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Look up a single env-config-repair run by id (the infra window tracks one by `repairJobId`). */
|
|
116
|
-
function envConfigRepairById(id: string): EnvConfigRepairJob | undefined {
|
|
117
|
-
return envConfigRepairJobs.value.find((j) => j.id === id)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
111
|
/**
|
|
121
112
|
* Patch a bootstrap run from a real-time `bootstrap` event (or after launching
|
|
122
113
|
* one): replace it in place by id, else prepend it. Keeps the service card
|
package/app/stores/bootstrap.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ReferenceArchitecture,
|
|
7
7
|
UpdateReferenceArchitectureInput,
|
|
8
8
|
} from '~/types/domain'
|
|
9
|
+
import { useUpsertList } from '~/composables/useUpsertList'
|
|
9
10
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
10
11
|
import { useAgentRunsStore } from '~/stores/agentRuns'
|
|
11
12
|
|
|
@@ -27,7 +28,11 @@ export const useBootstrapStore = defineStore('bootstrap', () => {
|
|
|
27
28
|
|
|
28
29
|
/** null = unknown (not probed yet), true/false = module reachable or not. */
|
|
29
30
|
const available = ref<boolean | null>(null)
|
|
30
|
-
const
|
|
31
|
+
const {
|
|
32
|
+
items: architectures,
|
|
33
|
+
upsert: upsertArchitecture,
|
|
34
|
+
remove: dropArchitecture,
|
|
35
|
+
} = useUpsertList<ReferenceArchitecture>({ key: (a) => a.id, prepend: true })
|
|
31
36
|
const loading = ref(false)
|
|
32
37
|
|
|
33
38
|
const hasArchitectures = computed(() => architectures.value.length > 0)
|
|
@@ -50,22 +55,21 @@ export const useBootstrapStore = defineStore('bootstrap', () => {
|
|
|
50
55
|
/** Register a new reference architecture. */
|
|
51
56
|
async function createArchitecture(input: CreateReferenceArchitectureInput) {
|
|
52
57
|
const created = await api.createReferenceArchitecture(workspace.requireId(), input)
|
|
53
|
-
|
|
58
|
+
upsertArchitecture(created)
|
|
54
59
|
return created
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
/** Patch a reference architecture. */
|
|
58
63
|
async function updateArchitecture(id: string, input: UpdateReferenceArchitectureInput) {
|
|
59
64
|
const updated = await api.updateReferenceArchitecture(workspace.requireId(), id, input)
|
|
60
|
-
|
|
61
|
-
if (i >= 0) architectures.value[i] = updated
|
|
65
|
+
upsertArchitecture(updated)
|
|
62
66
|
return updated
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
/** Remove a reference architecture. */
|
|
66
70
|
async function deleteArchitecture(id: string) {
|
|
67
71
|
await api.deleteReferenceArchitecture(workspace.requireId(), id)
|
|
68
|
-
|
|
72
|
+
dropArchitecture(id)
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
/**
|
package/app/stores/github.ts
CHANGED
|
@@ -14,9 +14,13 @@ import type {
|
|
|
14
14
|
ResyncRequest,
|
|
15
15
|
} from '~/types/domain'
|
|
16
16
|
import { useSingleFlightProbe } from '~/composables/useSingleFlightProbe'
|
|
17
|
+
import { useUpsertList } from '~/composables/useUpsertList'
|
|
17
18
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
18
19
|
import { useServicesStore } from '~/stores/services'
|
|
19
20
|
|
|
21
|
+
/** Stable identity for a pull request in the `pulls` list: repo + PR number. */
|
|
22
|
+
const pullKey = (repoGithubId: number, number: number) => `${repoGithubId}:${number}`
|
|
23
|
+
|
|
20
24
|
/**
|
|
21
25
|
* GitHub integration state: the workspace's App installation, the projected
|
|
22
26
|
* repos/branches/pull-requests/issues the backend caches in D1, and the actions
|
|
@@ -42,7 +46,14 @@ export const useGitHubStore = defineStore('github', () => {
|
|
|
42
46
|
const availableRepos = ref<GitHubAvailableRepo[]>([])
|
|
43
47
|
const loadingAvailable = ref(false)
|
|
44
48
|
const savingRepos = ref(false)
|
|
45
|
-
const
|
|
49
|
+
const {
|
|
50
|
+
items: pulls,
|
|
51
|
+
upsert: upsertPull,
|
|
52
|
+
get: getPull,
|
|
53
|
+
} = useUpsertList<GitHubPullRequest>({
|
|
54
|
+
key: (p) => pullKey(p.repoGithubId, p.number),
|
|
55
|
+
prepend: true,
|
|
56
|
+
})
|
|
46
57
|
const issues = ref<GitHubIssue[]>([])
|
|
47
58
|
/** Branches loaded lazily per repo (by GitHub numeric id). */
|
|
48
59
|
const branches = ref<Record<number, GitHubBranch[]>>({})
|
|
@@ -269,11 +280,7 @@ export const useGitHubStore = defineStore('github', () => {
|
|
|
269
280
|
|
|
270
281
|
async function openPullRequest(repoGithubId: number, input: OpenPullRequestInput) {
|
|
271
282
|
const pr = await api.openGitHubPullRequest(workspace.requireId(), repoGithubId, input)
|
|
272
|
-
|
|
273
|
-
(p) => p.repoGithubId === pr.repoGithubId && p.number === pr.number,
|
|
274
|
-
)
|
|
275
|
-
if (i >= 0) pulls.value[i] = pr
|
|
276
|
-
else pulls.value.unshift(pr)
|
|
283
|
+
upsertPull(pr)
|
|
277
284
|
return pr
|
|
278
285
|
}
|
|
279
286
|
|
|
@@ -284,8 +291,8 @@ export const useGitHubStore = defineStore('github', () => {
|
|
|
284
291
|
) {
|
|
285
292
|
await api.mergeGitHubPullRequest(workspace.requireId(), repoGithubId, number, input)
|
|
286
293
|
// Optimistically reflect the merge until the next sync confirms it.
|
|
287
|
-
const
|
|
288
|
-
if (
|
|
294
|
+
const existing = getPull(pullKey(repoGithubId, number))
|
|
295
|
+
if (existing) upsertPull({ ...existing, state: 'closed', merged: true })
|
|
289
296
|
}
|
|
290
297
|
|
|
291
298
|
function comment(repoGithubId: number, number: number, body: string) {
|
package/app/stores/pipelines.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { AgentKind, Pipeline } from '~/types/domain'
|
|
|
4
4
|
import type { ConsensusStepConfig, StepGating } from '~/types/consensus'
|
|
5
5
|
import type { StepOptions, TesterQualityConfig } from '@cat-factory/contracts'
|
|
6
6
|
import { companionForProducer, uid } from '~/utils/catalog'
|
|
7
|
+
import { useUpsertList } from '~/composables/useUpsertList'
|
|
7
8
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
8
9
|
|
|
9
10
|
/** A sensible default config when a step is first flipped to consensus in the builder. */
|
|
@@ -31,7 +32,11 @@ function defaultConsensusConfig(): ConsensusStepConfig {
|
|
|
31
32
|
*/
|
|
32
33
|
export const usePipelinesStore = defineStore('pipelines', () => {
|
|
33
34
|
const api = useApi()
|
|
34
|
-
const
|
|
35
|
+
const {
|
|
36
|
+
items: pipelines,
|
|
37
|
+
upsert: upsertPipeline,
|
|
38
|
+
remove: dropPipeline,
|
|
39
|
+
} = useUpsertList<Pipeline>({ key: (p) => p.id })
|
|
35
40
|
/**
|
|
36
41
|
* Current built-in catalog versions (`seedPipelines()`), keyed by pipeline id, from the
|
|
37
42
|
* workspace snapshot. A built-in whose stored `version` is below its catalog value here has
|
|
@@ -369,13 +374,12 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
369
374
|
const payload = draftPayload()
|
|
370
375
|
if (editingId.value) {
|
|
371
376
|
const updated = await api.updatePipeline(wsId, editingId.value, payload)
|
|
372
|
-
|
|
373
|
-
if (i >= 0) pipelines.value[i] = updated
|
|
377
|
+
upsertPipeline(updated)
|
|
374
378
|
clearDraft()
|
|
375
379
|
return updated
|
|
376
380
|
}
|
|
377
381
|
const pipeline = await api.createPipeline(wsId, payload)
|
|
378
|
-
|
|
382
|
+
upsertPipeline(pipeline)
|
|
379
383
|
clearDraft()
|
|
380
384
|
return pipeline
|
|
381
385
|
}
|
|
@@ -383,14 +387,14 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
383
387
|
/** Clone any pipeline (built-in or custom) into an editable copy, ready to edit. */
|
|
384
388
|
async function clonePipeline(id: string): Promise<Pipeline> {
|
|
385
389
|
const clone = await api.clonePipeline(useWorkspaceStore().requireId(), id)
|
|
386
|
-
|
|
390
|
+
upsertPipeline(clone)
|
|
387
391
|
loadForEdit(clone)
|
|
388
392
|
return clone
|
|
389
393
|
}
|
|
390
394
|
|
|
391
395
|
async function removePipeline(id: string) {
|
|
392
396
|
await api.removePipeline(useWorkspaceStore().requireId(), id)
|
|
393
|
-
|
|
397
|
+
dropPipeline(id)
|
|
394
398
|
if (editingId.value === id) clearDraft()
|
|
395
399
|
}
|
|
396
400
|
|
|
@@ -401,8 +405,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
401
405
|
*/
|
|
402
406
|
async function reseed(id: string): Promise<Pipeline> {
|
|
403
407
|
const updated = await api.reseedPipeline(useWorkspaceStore().requireId(), id)
|
|
404
|
-
|
|
405
|
-
if (i >= 0) pipelines.value[i] = updated
|
|
408
|
+
upsertPipeline(updated)
|
|
406
409
|
if (editingId.value === id) clearDraft()
|
|
407
410
|
return updated
|
|
408
411
|
}
|
|
@@ -410,8 +413,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
410
413
|
/** Set a pipeline's organizational metadata (labels / archive). Works on built-ins too. */
|
|
411
414
|
async function organize(id: string, body: { labels?: string[]; archived?: boolean }) {
|
|
412
415
|
const updated = await api.organizePipeline(useWorkspaceStore().requireId(), id, body)
|
|
413
|
-
|
|
414
|
-
if (i >= 0) pipelines.value[i] = updated
|
|
416
|
+
upsertPipeline(updated)
|
|
415
417
|
return updated
|
|
416
418
|
}
|
|
417
419
|
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
IncidentEnrichmentView,
|
|
11
11
|
UpsertIncidentEnrichmentInput,
|
|
12
12
|
} from '~/types/incidentEnrichment'
|
|
13
|
+
import { useUpsertList } from '~/composables/useUpsertList'
|
|
13
14
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -26,7 +27,11 @@ export const useReleaseHealthStore = defineStore('releaseHealth', () => {
|
|
|
26
27
|
provider: null,
|
|
27
28
|
summary: null,
|
|
28
29
|
})
|
|
29
|
-
const
|
|
30
|
+
const {
|
|
31
|
+
items: configs,
|
|
32
|
+
upsert: upsertConfig,
|
|
33
|
+
remove: dropConfig,
|
|
34
|
+
} = useUpsertList<ReleaseHealthConfig>({ key: (c) => c.blockId })
|
|
30
35
|
// Incident-enrichment (PagerDuty + incident.io) connection — write-only secrets, the
|
|
31
36
|
// store only ever holds the presence summary. Wired alongside observability.
|
|
32
37
|
const incident = ref<IncidentEnrichmentView>({ connected: false, summary: null })
|
|
@@ -91,16 +96,14 @@ export const useReleaseHealthStore = defineStore('releaseHealth', () => {
|
|
|
91
96
|
async function saveConfig(blockId: string, input: UpsertReleaseHealthConfigInput) {
|
|
92
97
|
const ws = useWorkspaceStore()
|
|
93
98
|
const saved = await api.upsertReleaseHealthConfig(ws.requireId(), blockId, input)
|
|
94
|
-
|
|
95
|
-
if (idx >= 0) configs.value[idx] = saved
|
|
96
|
-
else configs.value.push(saved)
|
|
99
|
+
upsertConfig(saved)
|
|
97
100
|
return saved
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
async function removeConfig(blockId: string) {
|
|
101
104
|
const ws = useWorkspaceStore()
|
|
102
105
|
await api.deleteReleaseHealthConfig(ws.requireId(), blockId)
|
|
103
|
-
|
|
106
|
+
dropConfig(blockId)
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
/** Load the incident-enrichment connection (separate opt-in gate from observability). */
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
|
-
import { ref } from 'vue'
|
|
3
2
|
import type {
|
|
4
3
|
DetectSharedStackInput,
|
|
5
4
|
SharedStack,
|
|
6
5
|
UpdateSharedStackInput,
|
|
7
6
|
} from '~/types/sharedStacks'
|
|
7
|
+
import { useUpsertList } from '~/composables/useUpsertList'
|
|
8
8
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -19,18 +19,13 @@ import { useWorkspaceStore } from '~/stores/workspace'
|
|
|
19
19
|
*/
|
|
20
20
|
export const useSharedStacksStore = defineStore('sharedStacks', () => {
|
|
21
21
|
const api = useApi()
|
|
22
|
-
const stacks =
|
|
22
|
+
const { items: stacks, upsert: patch } = useUpsertList<SharedStack>({ key: (s) => s.id })
|
|
23
23
|
|
|
24
24
|
function hydrate(list: SharedStack[]) {
|
|
25
|
+
// Keep the snapshot sorted oldest-first; the helper's plain hydrate wouldn't sort.
|
|
25
26
|
stacks.value = [...list].sort((a, b) => a.createdAt - b.createdAt)
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function patch(stack: SharedStack) {
|
|
29
|
-
const idx = stacks.value.findIndex((s) => s.id === stack.id)
|
|
30
|
-
if (idx >= 0) stacks.value[idx] = stack
|
|
31
|
-
else stacks.value.push(stack)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
29
|
async function create(input: Parameters<typeof api.createSharedStack>[1]) {
|
|
35
30
|
const ws = useWorkspaceStore()
|
|
36
31
|
const created = await api.createSharedStack(ws.requireId(), input)
|