@cat-factory/app 0.116.5 → 0.116.7
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/auth/AuthGate.vue +3 -1
- package/app/components/layout/SideBar.vue +8 -5
- package/app/components/settings/CustomManifestTypeEditor.vue +1 -1
- package/app/components/settings/InfraHandlersConfigurator.vue +2 -2
- package/app/composables/useSingleFlightProbe.spec.ts +114 -0
- package/app/composables/useSingleFlightProbe.ts +70 -0
- package/app/composables/useSourceIntegration.ts +17 -1
- package/app/pages/index.vue +7 -3
- package/app/stores/documents.ts +3 -1
- package/app/stores/fragmentLibrary.ts +7 -1
- package/app/stores/github.ts +8 -1
- package/app/stores/slack.ts +7 -1
- package/app/stores/tasks.ts +3 -1
- package/app/stores/workspace.spec.ts +67 -0
- package/app/stores/workspace.ts +30 -5
- package/app/utils/bootMarks.ts +49 -0
- package/i18n/locales/de.json +2 -2
- package/i18n/locales/en.json +2 -2
- package/i18n/locales/es.json +2 -2
- package/i18n/locales/fr.json +2 -2
- package/i18n/locales/he.json +2 -2
- package/i18n/locales/it.json +2 -2
- package/i18n/locales/ja.json +2 -2
- package/i18n/locales/pl.json +2 -2
- package/i18n/locales/tr.json +2 -2
- package/i18n/locales/uk.json +2 -2
- package/package.json +2 -2
|
@@ -14,7 +14,9 @@ const { t } = useI18n()
|
|
|
14
14
|
// out, so it must render even when auth is required and there's no user.
|
|
15
15
|
const isPublicRoute = computed(() => route.path === '/reset-password')
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// Stamp the first cold-open milestone once the auth handshake settles (app-startup initiative,
|
|
18
|
+
// item 1) — bootstrap resolves even on failure (it catches internally), so `finally` always fires.
|
|
19
|
+
onMounted(() => void auth.bootstrap().finally(() => markBoot('auth-ready')))
|
|
18
20
|
</script>
|
|
19
21
|
|
|
20
22
|
<template>
|
|
@@ -100,11 +100,14 @@ watch(
|
|
|
100
100
|
() => workspace.workspaceId,
|
|
101
101
|
(id) => {
|
|
102
102
|
if (!id) return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
void
|
|
107
|
-
void
|
|
103
|
+
// `ensureProbed` single-flights per board (app-startup initiative, item 12): on a cold open
|
|
104
|
+
// these coalesce with the board page's own github probe and don't refire on a re-mount, while a
|
|
105
|
+
// workspace switch (new id) still re-probes. `probe()` stays the explicit post-connect refresh.
|
|
106
|
+
void documents.ensureProbed()
|
|
107
|
+
void tasks.ensureProbed()
|
|
108
|
+
void github.ensureProbed()
|
|
109
|
+
void slack.ensureProbed()
|
|
110
|
+
void library.ensureProbed()
|
|
108
111
|
void providerConnections.ensureLoaded().catch(() => {})
|
|
109
112
|
},
|
|
110
113
|
{ immediate: true },
|
|
@@ -167,7 +167,7 @@ async function remove(type: CustomManifestType) {
|
|
|
167
167
|
:label="t('settings.infrastructure.customType.manifestId')"
|
|
168
168
|
:help="t('settings.infrastructure.customType.manifestIdHelp')"
|
|
169
169
|
>
|
|
170
|
-
<UInput v-model="draft.manifestId" class="font-mono" placeholder="my-
|
|
170
|
+
<UInput v-model="draft.manifestId" class="font-mono" placeholder="my-preview-template" />
|
|
171
171
|
</UFormField>
|
|
172
172
|
<UFormField :label="t('settings.infrastructure.customType.label')">
|
|
173
173
|
<UInput v-model="draft.label" />
|
|
@@ -276,7 +276,7 @@ const customSavedManifest = computed<Record<string, unknown> | undefined>(() =>
|
|
|
276
276
|
|
|
277
277
|
// The registry backend that builds the `remote-custom` handler's provider. The generic
|
|
278
278
|
// built-in `manifest` (BYO HTTP API) is the default; a deployment that registered a native
|
|
279
|
-
// custom env backend
|
|
279
|
+
// custom env backend can be picked here so the handler is pinned to it instead of
|
|
280
280
|
// silently resolving to the generic manifest provider. Only backends that serve the
|
|
281
281
|
// `remote-custom` engine are offered (the snapshot advertises each backend's engines).
|
|
282
282
|
const providerConnections = useProviderConnectionsStore()
|
|
@@ -342,7 +342,7 @@ async function saveCustom(payload: {
|
|
|
342
342
|
provisionType: 'custom',
|
|
343
343
|
manifestId: selectedCustomId.value,
|
|
344
344
|
config,
|
|
345
|
-
// Pin the chosen registry backend so a native custom backend
|
|
345
|
+
// Pin the chosen registry backend so a native custom backend builds the
|
|
346
346
|
// provider — absent, the engine would resolve to the generic manifest provider.
|
|
347
347
|
backendKind: selectedBackendKind.value,
|
|
348
348
|
secrets: payload.secrets,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { useSingleFlightProbe } from '~/composables/useSingleFlightProbe'
|
|
3
|
+
|
|
4
|
+
// Single-flight probe guard (app-startup initiative, item 12). Pure logic, no Pinia/Nuxt: a fake
|
|
5
|
+
// `run` (counting calls, with a manually-resolved promise) and a mutable `id` getter.
|
|
6
|
+
|
|
7
|
+
/** A `run` whose promise the test resolves by hand, plus a call counter. */
|
|
8
|
+
function deferredRun() {
|
|
9
|
+
let resolve!: () => void
|
|
10
|
+
const calls = { count: 0 }
|
|
11
|
+
const run = vi.fn(() => {
|
|
12
|
+
calls.count++
|
|
13
|
+
return new Promise<void>((r) => (resolve = r))
|
|
14
|
+
})
|
|
15
|
+
return { run, calls, resolve: () => resolve() }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('useSingleFlightProbe', () => {
|
|
19
|
+
it('coalesces concurrent probe() calls for the same board into one run', async () => {
|
|
20
|
+
const { run, calls, resolve } = deferredRun()
|
|
21
|
+
const { probe } = useSingleFlightProbe(run, () => 'ws1')
|
|
22
|
+
|
|
23
|
+
const a = probe()
|
|
24
|
+
const b = probe()
|
|
25
|
+
expect(calls.count).toBe(1) // one in-flight run shared by both callers
|
|
26
|
+
resolve()
|
|
27
|
+
await Promise.all([a, b])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('ensureProbed() is a no-op once the board is already probed', async () => {
|
|
31
|
+
const { run, calls, resolve } = deferredRun()
|
|
32
|
+
const { ensureProbed } = useSingleFlightProbe(run, () => 'ws1')
|
|
33
|
+
|
|
34
|
+
const first = ensureProbed()
|
|
35
|
+
resolve()
|
|
36
|
+
await first
|
|
37
|
+
expect(calls.count).toBe(1)
|
|
38
|
+
|
|
39
|
+
await ensureProbed() // already settled for ws1 → does not run again
|
|
40
|
+
expect(calls.count).toBe(1)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('ensureProbed() re-runs when the workspace id changes', async () => {
|
|
44
|
+
const { run, calls, resolve } = deferredRun()
|
|
45
|
+
let id = 'ws1'
|
|
46
|
+
const { ensureProbed } = useSingleFlightProbe(run, () => id)
|
|
47
|
+
|
|
48
|
+
const first = ensureProbed()
|
|
49
|
+
resolve()
|
|
50
|
+
await first
|
|
51
|
+
expect(calls.count).toBe(1)
|
|
52
|
+
|
|
53
|
+
id = 'ws2' // a switch — connections are per board, so it must re-probe
|
|
54
|
+
const second = ensureProbed()
|
|
55
|
+
resolve()
|
|
56
|
+
await second
|
|
57
|
+
expect(calls.count).toBe(2)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('probe() always re-runs (a deliberate refresh) even after a completed probe', async () => {
|
|
61
|
+
const { run, calls, resolve } = deferredRun()
|
|
62
|
+
const guard = useSingleFlightProbe(run, () => 'ws1')
|
|
63
|
+
|
|
64
|
+
const first = guard.probe()
|
|
65
|
+
resolve()
|
|
66
|
+
await first
|
|
67
|
+
expect(calls.count).toBe(1)
|
|
68
|
+
|
|
69
|
+
const refresh = guard.probe() // e.g. after a connect — re-reads
|
|
70
|
+
resolve()
|
|
71
|
+
await refresh
|
|
72
|
+
expect(calls.count).toBe(2)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('a probe() refresh in flight is shared by a concurrent ensureProbed()', async () => {
|
|
76
|
+
const { run, calls, resolve } = deferredRun()
|
|
77
|
+
const guard = useSingleFlightProbe(run, () => 'ws1')
|
|
78
|
+
|
|
79
|
+
const refresh = guard.probe()
|
|
80
|
+
const ensured = guard.ensureProbed() // rides the in-flight probe rather than firing a duplicate
|
|
81
|
+
expect(calls.count).toBe(1)
|
|
82
|
+
resolve()
|
|
83
|
+
await Promise.all([refresh, ensured])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('a superseded out-of-order completion does not stamp a stale probedId', async () => {
|
|
87
|
+
// ws1 probe starts, then a switch to ws2 starts a second — and ws2 (the newer, current board)
|
|
88
|
+
// resolves BEFORE the older ws1 probe. The late ws1 completion must NOT record ws1 as the
|
|
89
|
+
// settled board, else the next ensureProbed() for ws2 would redundantly re-run.
|
|
90
|
+
let id = 'ws1'
|
|
91
|
+
let n = 0
|
|
92
|
+
let resolve1!: () => void
|
|
93
|
+
let resolve2!: () => void
|
|
94
|
+
const run = vi.fn(() => {
|
|
95
|
+
n++
|
|
96
|
+
return new Promise<void>((r) => (n === 1 ? (resolve1 = r) : (resolve2 = r)))
|
|
97
|
+
})
|
|
98
|
+
const { ensureProbed } = useSingleFlightProbe(run, () => id)
|
|
99
|
+
|
|
100
|
+
const p1 = ensureProbed() // starts ws1
|
|
101
|
+
id = 'ws2'
|
|
102
|
+
const p2 = ensureProbed() // starts ws2 (a switch — different id)
|
|
103
|
+
expect(run).toHaveBeenCalledTimes(2)
|
|
104
|
+
|
|
105
|
+
resolve2() // the current board settles first
|
|
106
|
+
await p2
|
|
107
|
+
resolve1() // the superseded older probe settles late
|
|
108
|
+
await p1
|
|
109
|
+
|
|
110
|
+
// ws2 is the current, settled board → ensureProbed() is a no-op, not a third (redundant) run.
|
|
111
|
+
await ensureProbed()
|
|
112
|
+
expect(run).toHaveBeenCalledTimes(2)
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-flight guard for a per-workspace integration probe (app-startup initiative, item 12).
|
|
3
|
+
*
|
|
4
|
+
* On a board open the same probe is fired from several places at once — e.g. `github.probe()` runs
|
|
5
|
+
* from both the board page (to resolve the onboarding gate) and the SideBar, and the SideBar fans
|
|
6
|
+
* out five more (`documents` / `tasks` / `slack` / `library` / provider connections). Each was an
|
|
7
|
+
* independent network call, and a re-mount re-ran them. This wraps a store's probe with two
|
|
8
|
+
* behaviours keyed on the active workspace id:
|
|
9
|
+
*
|
|
10
|
+
* - {@link probe} — always re-runs the probe (deliberate refresh, e.g. after a connect), but a
|
|
11
|
+
* burst of concurrent callers on ONE board open shares the single in-flight request.
|
|
12
|
+
* - {@link ensureProbed} — runs the probe AT MOST ONCE per workspace: a no-op when this board is
|
|
13
|
+
* already probed, or the shared in-flight promise otherwise. This is what the on-board-open
|
|
14
|
+
* fan-out uses, so the duplicate/refire collapses to one call — while a workspace SWITCH (a new
|
|
15
|
+
* id) still re-probes, since connections are per board.
|
|
16
|
+
*
|
|
17
|
+
* The id-keying means no explicit reset on workspace change: a call for a different id than the last
|
|
18
|
+
* completed probe re-runs. `run` reads whatever workspace-scoped state it needs itself; `currentId`
|
|
19
|
+
* only supplies the key (and the "which board did this settle for" record).
|
|
20
|
+
*/
|
|
21
|
+
interface SingleFlightProbe {
|
|
22
|
+
probe: () => Promise<void>
|
|
23
|
+
ensureProbed: () => Promise<void>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useSingleFlightProbe(
|
|
27
|
+
run: () => Promise<void>,
|
|
28
|
+
currentId: () => string | null,
|
|
29
|
+
): SingleFlightProbe {
|
|
30
|
+
let inFlight: Promise<void> | null = null
|
|
31
|
+
let inFlightId: string | null = null
|
|
32
|
+
let probedId: string | null = null
|
|
33
|
+
|
|
34
|
+
function start(id: string | null): Promise<void> {
|
|
35
|
+
const p = Promise.resolve(run()).finally(() => {
|
|
36
|
+
// Only record "settled for this board" / clear the in-flight slot when a NEWER probe hasn't
|
|
37
|
+
// superseded us. Otherwise an out-of-order completion — an older probe for board A resolving
|
|
38
|
+
// after a newer probe for board B — would stamp probedId back to A and force a redundant
|
|
39
|
+
// re-probe of B on the next ensureProbed. The newer probe owns `inFlight`, so it records the
|
|
40
|
+
// board that's actually current.
|
|
41
|
+
if (inFlight === p) {
|
|
42
|
+
probedId = id
|
|
43
|
+
inFlight = null
|
|
44
|
+
inFlightId = null
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
inFlight = p
|
|
48
|
+
inFlightId = id
|
|
49
|
+
return p
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function probe(): Promise<void> {
|
|
53
|
+
const id = currentId()
|
|
54
|
+
// Share an already-running probe for the same board (the concurrent-burst case); otherwise
|
|
55
|
+
// start a fresh one — a deliberate refresh must always re-read.
|
|
56
|
+
if (inFlight && inFlightId === id) return inFlight
|
|
57
|
+
return start(id)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ensureProbed(): Promise<void> {
|
|
61
|
+
const id = currentId()
|
|
62
|
+
// Already settled for this board and nothing running → nothing to do.
|
|
63
|
+
if (probedId === id && !inFlight) return Promise.resolve()
|
|
64
|
+
// A probe for this board is in flight → ride it rather than firing a duplicate.
|
|
65
|
+
if (inFlight && inFlightId === id) return inFlight
|
|
66
|
+
return start(id)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { probe, ensureProbed }
|
|
70
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ComputedRef, type Ref, computed, ref } from 'vue'
|
|
2
2
|
import { apiErrorEnvelope, apiErrorStatus } from '~/composables/api/errors'
|
|
3
|
+
import { useSingleFlightProbe } from '~/composables/useSingleFlightProbe'
|
|
3
4
|
import { useUpsertList } from '~/composables/useUpsertList'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -25,6 +26,12 @@ export function useSourceIntegration<
|
|
|
25
26
|
fetch: () => Promise<{ sources: Desc[]; connections: Conn[] }>
|
|
26
27
|
/** Gate the probe (e.g. skip until a workspace is selected). */
|
|
27
28
|
enabled?: () => boolean
|
|
29
|
+
/**
|
|
30
|
+
* The active workspace id, keying the single-flight probe (app-startup initiative, item 12): a
|
|
31
|
+
* board-open fan-out via `ensureProbed` hits the source once per board, and a workspace switch
|
|
32
|
+
* (new id) re-probes. Omitted ⇒ a single bucket (probe-once for the composable's lifetime).
|
|
33
|
+
*/
|
|
34
|
+
workspaceId?: () => string | null
|
|
28
35
|
}): {
|
|
29
36
|
available: Ref<boolean | null>
|
|
30
37
|
probeError: Ref<{ status: number | null; message: string } | null>
|
|
@@ -38,6 +45,7 @@ export function useSourceIntegration<
|
|
|
38
45
|
upsertConnection: (conn: Conn) => void
|
|
39
46
|
removeConnection: (source: Source) => void
|
|
40
47
|
probe: () => Promise<void>
|
|
48
|
+
ensureProbed: () => Promise<void>
|
|
41
49
|
} {
|
|
42
50
|
/** null = unknown (not probed yet), true/false = integration on/off. */
|
|
43
51
|
const available = ref<boolean | null>(null)
|
|
@@ -67,7 +75,7 @@ export function useSourceIntegration<
|
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
/** Probe the integration: resolves `available`, the sources and connections. */
|
|
70
|
-
async function
|
|
78
|
+
async function runProbe() {
|
|
71
79
|
if (opts.enabled && !opts.enabled()) return
|
|
72
80
|
try {
|
|
73
81
|
const { sources: srcs, connections: conns } = await opts.fetch()
|
|
@@ -89,6 +97,13 @@ export function useSourceIntegration<
|
|
|
89
97
|
connections.value = []
|
|
90
98
|
}
|
|
91
99
|
}
|
|
100
|
+
// Single-flight the probe (app-startup initiative, item 12): `probe()` still re-reads on demand
|
|
101
|
+
// (a connect/disconnect refresh), but the board-open fan-out uses `ensureProbed` so it fetches
|
|
102
|
+
// once per board rather than on every SideBar/page (re)mount.
|
|
103
|
+
const { probe, ensureProbed } = useSingleFlightProbe(
|
|
104
|
+
runProbe,
|
|
105
|
+
opts.workspaceId ?? (() => 'source-integration'),
|
|
106
|
+
)
|
|
92
107
|
|
|
93
108
|
return {
|
|
94
109
|
available,
|
|
@@ -103,5 +118,6 @@ export function useSourceIntegration<
|
|
|
103
118
|
upsertConnection,
|
|
104
119
|
removeConnection,
|
|
105
120
|
probe,
|
|
121
|
+
ensureProbed,
|
|
106
122
|
}
|
|
107
123
|
}
|
package/app/pages/index.vue
CHANGED
|
@@ -245,12 +245,13 @@ watch(
|
|
|
245
245
|
|
|
246
246
|
// Probe the GitHub integration as soon as a board is active (re-probe per board —
|
|
247
247
|
// connections are per workspace). The result drives the onboarding gate below
|
|
248
|
-
// before the board mounts, so an unconnected user can't slip past it.
|
|
249
|
-
//
|
|
248
|
+
// before the board mounts, so an unconnected user can't slip past it. `ensureProbed`
|
|
249
|
+
// single-flights per board (app-startup initiative, item 12), so this and the SideBar's
|
|
250
|
+
// probe collapse to one request on a cold open instead of two.
|
|
250
251
|
watch(
|
|
251
252
|
() => workspace.workspaceId,
|
|
252
253
|
(id) => {
|
|
253
|
-
if (id) void github.
|
|
254
|
+
if (id) void github.ensureProbed()
|
|
254
255
|
},
|
|
255
256
|
{ immediate: true },
|
|
256
257
|
)
|
|
@@ -268,6 +269,9 @@ const stream = useWorkspaceStream()
|
|
|
268
269
|
// in the template would not unwrap, since `stream` is a plain object). Drives the headless
|
|
269
270
|
// `workspace-stream` readiness marker the e2e suite waits on.
|
|
270
271
|
const streamConnected = computed(() => stream.connected.value)
|
|
272
|
+
// Final cold-open milestone (app-startup initiative, item 1): a live, reconciled board. `markBoot`
|
|
273
|
+
// fires once, so only the first connect of the session times the waterfall's tail.
|
|
274
|
+
watch(streamConnected, (c) => c && markBoot('stream-connected'), { immediate: true })
|
|
271
275
|
const streamEverConnected = computed(() => stream.everConnected.value)
|
|
272
276
|
const streamConnectionFailed = computed(() => stream.connectionFailed.value)
|
|
273
277
|
watch(
|
package/app/stores/documents.ts
CHANGED
|
@@ -35,6 +35,7 @@ export const useDocumentsStore = defineStore('documents', () => {
|
|
|
35
35
|
DocumentSourceDescriptor
|
|
36
36
|
>({
|
|
37
37
|
enabled: () => !!workspace.workspaceId,
|
|
38
|
+
workspaceId: () => workspace.workspaceId,
|
|
38
39
|
fetch: async () => {
|
|
39
40
|
const [{ sources }, { connections }] = await Promise.all([
|
|
40
41
|
api.listDocumentSources(workspace.requireId()),
|
|
@@ -44,7 +45,7 @@ export const useDocumentsStore = defineStore('documents', () => {
|
|
|
44
45
|
},
|
|
45
46
|
})
|
|
46
47
|
const { available, sources, connections, connectedSources, anyConnected } = integration
|
|
47
|
-
const { descriptorFor, connectionFor, isConnected, probe } = integration
|
|
48
|
+
const { descriptorFor, connectionFor, isConnected, probe, ensureProbed } = integration
|
|
48
49
|
|
|
49
50
|
const { items: documents, upsert: upsertDoc } = useUpsertList<SourceDocument>({
|
|
50
51
|
key: (d) => `${d.source}:${d.externalId}`,
|
|
@@ -187,6 +188,7 @@ export const useDocumentsStore = defineStore('documents', () => {
|
|
|
187
188
|
isConnected,
|
|
188
189
|
docsForBlock,
|
|
189
190
|
probe,
|
|
191
|
+
ensureProbed,
|
|
190
192
|
connect,
|
|
191
193
|
disconnect,
|
|
192
194
|
loadDocuments,
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
ResolvedFragment,
|
|
11
11
|
UpdatePromptFragmentInput,
|
|
12
12
|
} from '~/types/domain'
|
|
13
|
+
import { useSingleFlightProbe } from '~/composables/useSingleFlightProbe'
|
|
13
14
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
14
15
|
import { useFragmentsStore } from '~/stores/fragments'
|
|
15
16
|
|
|
@@ -62,7 +63,7 @@ function fragmentLibrarySetup(kind: FragmentOwnerKind, resolveOwnerId: () => str
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
/** Probe the feature + load this owner's tier, sources and (ws) resolved catalog. */
|
|
65
|
-
async function
|
|
66
|
+
async function runProbe() {
|
|
66
67
|
const id = resolveOwnerId()
|
|
67
68
|
if (!id) return
|
|
68
69
|
try {
|
|
@@ -82,6 +83,10 @@ function fragmentLibrarySetup(kind: FragmentOwnerKind, resolveOwnerId: () => str
|
|
|
82
83
|
resolved.value = []
|
|
83
84
|
}
|
|
84
85
|
}
|
|
86
|
+
// Single-flight the probe keyed on the owner id (app-startup initiative, item 12): the SideBar's
|
|
87
|
+
// board-open fan-out uses `ensureProbed`, so it loads once per owner; `probe()` stays the
|
|
88
|
+
// on-demand refresh (a library mutation still re-reads via the explicit calls).
|
|
89
|
+
const { probe, ensureProbed } = useSingleFlightProbe(runProbe, () => resolveOwnerId())
|
|
85
90
|
|
|
86
91
|
async function refreshResolved() {
|
|
87
92
|
// Every library mutation lands here: drop the picker catalog's cache so the
|
|
@@ -192,6 +197,7 @@ function fragmentLibrarySetup(kind: FragmentOwnerKind, resolveOwnerId: () => str
|
|
|
192
197
|
viaWorkspaceId,
|
|
193
198
|
builtinCount,
|
|
194
199
|
probe,
|
|
200
|
+
ensureProbed,
|
|
195
201
|
refreshResolved,
|
|
196
202
|
create,
|
|
197
203
|
createDocumentFragment,
|
package/app/stores/github.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
OpenPullRequestInput,
|
|
14
14
|
ResyncRequest,
|
|
15
15
|
} from '~/types/domain'
|
|
16
|
+
import { useSingleFlightProbe } from '~/composables/useSingleFlightProbe'
|
|
16
17
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
17
18
|
import { useServicesStore } from '~/stores/services'
|
|
18
19
|
|
|
@@ -96,7 +97,7 @@ export const useGitHubStore = defineStore('github', () => {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
/** Probe the integration: resolves `available` and the current connection. */
|
|
99
|
-
async function
|
|
100
|
+
async function runProbe() {
|
|
100
101
|
if (!workspace.workspaceId) return
|
|
101
102
|
try {
|
|
102
103
|
const { connection: conn } = await api.getGitHubConnection(workspace.requireId())
|
|
@@ -108,6 +109,11 @@ export const useGitHubStore = defineStore('github', () => {
|
|
|
108
109
|
connection.value = null
|
|
109
110
|
}
|
|
110
111
|
}
|
|
112
|
+
// Single-flight the probe (app-startup initiative, item 12): `probe()` still re-reads on demand,
|
|
113
|
+
// but the on-board-open callers (the board page's onboarding gate + the SideBar) use
|
|
114
|
+
// `ensureProbed()` so their duplicate fire collapses to one request per board. A workspace switch
|
|
115
|
+
// (new id) re-probes.
|
|
116
|
+
const { probe, ensureProbed } = useSingleFlightProbe(runProbe, () => workspace.workspaceId)
|
|
111
117
|
|
|
112
118
|
/** Load the cached repos, pull requests and issues for the workspace. */
|
|
113
119
|
async function load() {
|
|
@@ -327,6 +333,7 @@ export const useGitHubStore = defineStore('github', () => {
|
|
|
327
333
|
pullUrl,
|
|
328
334
|
issueUrl,
|
|
329
335
|
probe,
|
|
336
|
+
ensureProbed,
|
|
330
337
|
load,
|
|
331
338
|
ensureLoaded,
|
|
332
339
|
loadAvailableRepos,
|
package/app/stores/slack.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
SlackMemberMappingEntry,
|
|
7
7
|
SlackNotificationSettings,
|
|
8
8
|
} from '~/types/domain'
|
|
9
|
+
import { useSingleFlightProbe } from '~/composables/useSingleFlightProbe'
|
|
9
10
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -43,7 +44,8 @@ export const useSlackStore = defineStore('slack', () => {
|
|
|
43
44
|
* is off — hide the UI. On success, capture the connection + whether OAuth is
|
|
44
45
|
* available. Called on workspace change, like the GitHub probe.
|
|
45
46
|
*/
|
|
46
|
-
async function
|
|
47
|
+
async function runProbe() {
|
|
48
|
+
if (!workspace.workspaceId) return
|
|
47
49
|
try {
|
|
48
50
|
const { connection: conn, oauthEnabled: oauth } = await api.getSlackConnection(
|
|
49
51
|
workspace.requireId(),
|
|
@@ -56,6 +58,9 @@ export const useSlackStore = defineStore('slack', () => {
|
|
|
56
58
|
connection.value = null
|
|
57
59
|
}
|
|
58
60
|
}
|
|
61
|
+
// Single-flight the probe (app-startup initiative, item 12) so the SideBar's board-open fan-out
|
|
62
|
+
// (via `ensureProbed`) hits Slack once per board; `probe()` stays the on-demand refresh.
|
|
63
|
+
const { probe, ensureProbed } = useSingleFlightProbe(runProbe, () => workspace.workspaceId)
|
|
59
64
|
|
|
60
65
|
/** Resolve the "Add to Slack" OAuth URL (only when oauthEnabled). */
|
|
61
66
|
function installUrl(): Promise<string> {
|
|
@@ -130,6 +135,7 @@ export const useSlackStore = defineStore('slack', () => {
|
|
|
130
135
|
saving,
|
|
131
136
|
connected,
|
|
132
137
|
probe,
|
|
138
|
+
ensureProbed,
|
|
133
139
|
installUrl,
|
|
134
140
|
connectWithToken,
|
|
135
141
|
disconnect,
|
package/app/stores/tasks.ts
CHANGED
|
@@ -34,6 +34,7 @@ export const useTasksStore = defineStore('tasks', () => {
|
|
|
34
34
|
// (integration disabled vs a server/backend error) instead of "install it first".
|
|
35
35
|
const integration = useSourceIntegration<TaskSourceKind, TaskConnection, TaskSourceState>({
|
|
36
36
|
enabled: () => !!workspace.workspaceId,
|
|
37
|
+
workspaceId: () => workspace.workspaceId,
|
|
37
38
|
fetch: async () => {
|
|
38
39
|
const [{ sources }, { connections }] = await Promise.all([
|
|
39
40
|
api.listTaskSources(workspace.requireId()),
|
|
@@ -44,7 +45,7 @@ export const useTasksStore = defineStore('tasks', () => {
|
|
|
44
45
|
})
|
|
45
46
|
const { available, probeError, sources, connections, connectedSources, anyConnected } =
|
|
46
47
|
integration
|
|
47
|
-
const { descriptorFor, connectionFor, isConnected, probe } = integration
|
|
48
|
+
const { descriptorFor, connectionFor, isConnected, probe, ensureProbed } = integration
|
|
48
49
|
|
|
49
50
|
const { items: tasks, upsert: upsertTask } = useUpsertList<SourceTask>({
|
|
50
51
|
key: (t) => `${t.source}:${t.externalId}`,
|
|
@@ -224,6 +225,7 @@ export const useTasksStore = defineStore('tasks', () => {
|
|
|
224
225
|
isConnected,
|
|
225
226
|
tasksForBlock,
|
|
226
227
|
probe,
|
|
228
|
+
ensureProbed,
|
|
227
229
|
checkSetup,
|
|
228
230
|
connect,
|
|
229
231
|
startLinearOAuth,
|
|
@@ -112,3 +112,70 @@ describe('workspace store refresh ordering', () => {
|
|
|
112
112
|
expect(board.getBlock('spawned')).toBeDefined()
|
|
113
113
|
})
|
|
114
114
|
})
|
|
115
|
+
|
|
116
|
+
// Cold-open waterfall flattening (app-startup initiative, item 8): `init()` fetches the persisted
|
|
117
|
+
// board's snapshot SPECULATIVELY, in parallel with the workspace list, instead of waiting for the
|
|
118
|
+
// list to resolve before the (heaviest) snapshot fetch. These pin that (a) a still-valid persisted
|
|
119
|
+
// board is opened with EXACTLY ONE snapshot fetch — the reused speculative one — and (b) a stale
|
|
120
|
+
// persisted id falls back cleanly, discarding the speculative result.
|
|
121
|
+
describe('workspace store cold-open speculative snapshot', () => {
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
// A working accounts store (the inert stub returns non-promises, which init's `.catch` chain +
|
|
124
|
+
// `accountWorkspaces` can't use). Auth off ⇒ all boards are in scope.
|
|
125
|
+
vi.stubGlobal('useAccountsStore', () => ({
|
|
126
|
+
load: async () => {},
|
|
127
|
+
enabled: false,
|
|
128
|
+
activeAccountId: null,
|
|
129
|
+
}))
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('reuses the speculative persisted snapshot — one getWorkspace on a cold open', async () => {
|
|
133
|
+
const getWorkspace = vi.fn().mockResolvedValue(snapshot('ws1', [block('f1')]))
|
|
134
|
+
const listWorkspaces = vi.fn().mockResolvedValue([{ id: 'ws1', name: 'ws1', accountId: null }])
|
|
135
|
+
vi.stubGlobal('useApi', () => ({ getWorkspace, listWorkspaces }))
|
|
136
|
+
|
|
137
|
+
const ws = useWorkspaceStore()
|
|
138
|
+
ws.workspaceId = 'ws1' // the persisted board (read from localStorage on a real cold open)
|
|
139
|
+
await ws.init()
|
|
140
|
+
|
|
141
|
+
// The persisted board's snapshot was fetched exactly once (speculatively) and REUSED —
|
|
142
|
+
// resolveActiveBoard did not fetch it a second time.
|
|
143
|
+
expect(listWorkspaces).toHaveBeenCalledTimes(1)
|
|
144
|
+
expect(getWorkspace).toHaveBeenCalledTimes(1)
|
|
145
|
+
expect(getWorkspace).toHaveBeenCalledWith('ws1')
|
|
146
|
+
expect(useBoardStore().getBlock('f1')).toBeDefined()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('falls back to the first board when the persisted id is gone', async () => {
|
|
150
|
+
const getWorkspace = vi.fn(async (id: string) => {
|
|
151
|
+
if (id === 'gone') throw new Error('404') // the speculative fetch for a removed board rejects
|
|
152
|
+
return snapshot(id, [block('f2')])
|
|
153
|
+
})
|
|
154
|
+
const listWorkspaces = vi.fn().mockResolvedValue([{ id: 'ws2', name: 'ws2', accountId: null }])
|
|
155
|
+
vi.stubGlobal('useApi', () => ({ getWorkspace, listWorkspaces }))
|
|
156
|
+
|
|
157
|
+
const ws = useWorkspaceStore()
|
|
158
|
+
ws.workspaceId = 'gone'
|
|
159
|
+
await ws.init()
|
|
160
|
+
|
|
161
|
+
// The rejected speculative fetch didn't wedge init; it fell back to the one board in scope.
|
|
162
|
+
expect(getWorkspace).toHaveBeenNthCalledWith(1, 'gone')
|
|
163
|
+
expect(getWorkspace).toHaveBeenCalledWith('ws2')
|
|
164
|
+
expect(ws.workspaceId).toBe('ws2')
|
|
165
|
+
expect(useBoardStore().getBlock('f2')).toBeDefined()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('no persisted board: no speculative fetch, opens the first board', async () => {
|
|
169
|
+
const getWorkspace = vi.fn().mockResolvedValue(snapshot('ws3', [block('f3')]))
|
|
170
|
+
const listWorkspaces = vi.fn().mockResolvedValue([{ id: 'ws3', name: 'ws3', accountId: null }])
|
|
171
|
+
vi.stubGlobal('useApi', () => ({ getWorkspace, listWorkspaces }))
|
|
172
|
+
|
|
173
|
+
const ws = useWorkspaceStore()
|
|
174
|
+
ws.workspaceId = null
|
|
175
|
+
await ws.init()
|
|
176
|
+
|
|
177
|
+
expect(getWorkspace).toHaveBeenCalledTimes(1)
|
|
178
|
+
expect(getWorkspace).toHaveBeenCalledWith('ws3')
|
|
179
|
+
expect(useBoardStore().getBlock('f3')).toBeDefined()
|
|
180
|
+
})
|
|
181
|
+
})
|
package/app/stores/workspace.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { useConsensusStore } from '~/stores/consensus'
|
|
|
32
32
|
import { useGitHubStore } from '~/stores/github'
|
|
33
33
|
import { useFragmentsStore } from '~/stores/fragments'
|
|
34
34
|
import { useProviderConnectionsStore } from '~/stores/providerConnections'
|
|
35
|
+
import { markBoot } from '~/utils/bootMarks'
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Owns the active workspace and bootstraps the app against the backend. On load
|
|
@@ -159,6 +160,17 @@ export const useWorkspaceStore = defineStore(
|
|
|
159
160
|
ready.value = false
|
|
160
161
|
error.value = null
|
|
161
162
|
try {
|
|
163
|
+
// Cold-open waterfall flattening (app-startup initiative, item 8): the persisted board is
|
|
164
|
+
// usually known from localStorage BEFORE any request fires, and its snapshot is the app's
|
|
165
|
+
// heaviest payload (the ~18-read aggregate). Fetch it SPECULATIVELY in parallel with the
|
|
166
|
+
// workspace list + accounts instead of waiting for the list to resolve first — one fewer
|
|
167
|
+
// sequential round trip on the critical path. Validated for membership in
|
|
168
|
+
// resolveActiveBoard; a stale/removed persisted id just discards the speculative result and
|
|
169
|
+
// falls back to today's path. `.catch` keeps a gone-board 404 from rejecting the whole init.
|
|
170
|
+
const persistedId = workspaceId.value
|
|
171
|
+
const speculativeSnapshot = persistedId
|
|
172
|
+
? api.getWorkspace(persistedId).catch(() => null)
|
|
173
|
+
: null
|
|
162
174
|
// Accounts (an auth concept — empty in dev, which leaves boards unscoped) and the
|
|
163
175
|
// workspace list are independent, so fetch them concurrently. resolveActiveBoard
|
|
164
176
|
// needs both, so it still runs after.
|
|
@@ -168,25 +180,38 @@ export const useWorkspaceStore = defineStore(
|
|
|
168
180
|
.catch(() => {}),
|
|
169
181
|
api.listWorkspaces(),
|
|
170
182
|
])
|
|
183
|
+
markBoot('workspaces-listed')
|
|
171
184
|
workspaces.value = workspaceList
|
|
172
|
-
await resolveActiveBoard()
|
|
185
|
+
await resolveActiveBoard(await speculativeSnapshot)
|
|
186
|
+
markBoot('snapshot-hydrated')
|
|
173
187
|
ready.value = true
|
|
174
188
|
} catch (e) {
|
|
175
189
|
error.value = e instanceof Error ? e.message : 'Failed to reach the backend.'
|
|
176
190
|
}
|
|
177
191
|
}
|
|
178
192
|
|
|
179
|
-
/**
|
|
180
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Open the persisted board (aligning the active account to it), else pick/create one.
|
|
195
|
+
*
|
|
196
|
+
* `prefetched` is the speculatively-fetched snapshot for the persisted board (see {@link init}):
|
|
197
|
+
* when it's for the SAME still-valid board we reuse it instead of re-fetching, so the cold open
|
|
198
|
+
* pays exactly one snapshot fetch — overlapped with the workspace list rather than after it.
|
|
199
|
+
*/
|
|
200
|
+
async function resolveActiveBoard(prefetched?: WorkspaceSnapshot | null) {
|
|
181
201
|
const accounts = useAccountsStore()
|
|
182
202
|
if (workspaceId.value) {
|
|
183
203
|
const existing = workspaces.value.find((w) => w.id === workspaceId.value)
|
|
184
204
|
if (existing) {
|
|
185
205
|
if (accounts.enabled && existing.accountId) accounts.activeAccountId = existing.accountId
|
|
186
|
-
hydrate(
|
|
206
|
+
hydrate(
|
|
207
|
+
prefetched && prefetched.workspace.id === existing.id
|
|
208
|
+
? prefetched
|
|
209
|
+
: await api.getWorkspace(existing.id),
|
|
210
|
+
)
|
|
187
211
|
return
|
|
188
212
|
}
|
|
189
|
-
// Persisted board is gone (deleted, or now another tenant's) — fall through
|
|
213
|
+
// Persisted board is gone (deleted, or now another tenant's) — fall through (and discard the
|
|
214
|
+
// now-irrelevant speculative snapshot).
|
|
190
215
|
workspaceId.value = null
|
|
191
216
|
}
|
|
192
217
|
const first = accountWorkspaces.value[0]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cold-open instrumentation (app-startup initiative, item 1). The SPA is `ssr: false`, so the whole
|
|
3
|
+
* "launch → usable board" path runs client-side after the bundle loads, and nothing timed the
|
|
4
|
+
* milestones along it. `markBoot` drops a `performance.mark` at each one plus `performance.measure`s
|
|
5
|
+
* so the cold-open waterfall is visible in a Playwright/browser trace:
|
|
6
|
+
* - one `cat-factory:open→<milestone>` measure = absolute ms from navigation start to the milestone,
|
|
7
|
+
* - one `cat-factory:<prev>→<milestone>` measure = the segment since the previous milestone.
|
|
8
|
+
*
|
|
9
|
+
* Milestones (fired once per cold open): `auth-ready` → `workspaces-listed` → `snapshot-hydrated`
|
|
10
|
+
* → `stream-connected`. It is pure instrumentation — no behaviour depends on it — and it is a no-op
|
|
11
|
+
* where the User Timing API is unavailable (SSR/prerender, ancient engines), so callers never guard.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const PREFIX = 'cat-factory'
|
|
15
|
+
|
|
16
|
+
/** Milestones already stamped this session — cold-open marks fire ONCE (later switches don't re-time). */
|
|
17
|
+
const seen = new Set<string>()
|
|
18
|
+
/** The previous milestone's mark name, so each call can measure the segment since it. */
|
|
19
|
+
let previousMark: string | null = null
|
|
20
|
+
|
|
21
|
+
export function markBoot(milestone: string): void {
|
|
22
|
+
if (seen.has(milestone)) return
|
|
23
|
+
if (typeof performance === 'undefined' || typeof performance.mark !== 'function') return
|
|
24
|
+
seen.add(milestone)
|
|
25
|
+
const mark = `${PREFIX}:${milestone}`
|
|
26
|
+
const prev = previousMark
|
|
27
|
+
try {
|
|
28
|
+
performance.mark(mark)
|
|
29
|
+
} catch {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
// Advance the chain as soon as the mark itself lands, INDEPENDENT of the derived measures below.
|
|
33
|
+
// Some engines reject the numeric-`start` measure form; if that throws we still want the NEXT
|
|
34
|
+
// milestone to measure its segment from THIS mark rather than a stale earlier one.
|
|
35
|
+
previousMark = mark
|
|
36
|
+
try {
|
|
37
|
+
// Absolute time from navigation start (DOMHighResTimeStamp 0 = timeOrigin) to this milestone.
|
|
38
|
+
performance.measure(`${PREFIX}:open→${milestone}`, { start: 0, end: mark })
|
|
39
|
+
// The segment since the previous milestone — the waterfall bar.
|
|
40
|
+
if (prev) {
|
|
41
|
+
performance.measure(`${PREFIX}:${prev.slice(PREFIX.length + 1)}→${milestone}`, {
|
|
42
|
+
start: prev,
|
|
43
|
+
end: mark,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Derived measures unsupported on this engine; the marks still land, so the trace is usable.
|
|
48
|
+
}
|
|
49
|
+
}
|
package/i18n/locales/de.json
CHANGED
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"editTitle": "{id} bearbeiten",
|
|
138
138
|
"addTitle": "Einen Typ hinzufügen",
|
|
139
139
|
"manifestId": "Manifest-ID",
|
|
140
|
-
"manifestIdHelp": "Ein Lower-Kebab-Slug, z. B. my-
|
|
140
|
+
"manifestIdHelp": "Ein Lower-Kebab-Slug, z. B. my-preview-template.",
|
|
141
141
|
"label": "Bezeichnung",
|
|
142
142
|
"acceptsInputHint": "Eingabehinweis (optional)",
|
|
143
143
|
"acceptsInputHintHelp": "Beschreibt die Eingabeform, die der Provider erwartet.",
|
|
@@ -1737,7 +1737,7 @@
|
|
|
1737
1737
|
},
|
|
1738
1738
|
"ephemeralEnvironments": {
|
|
1739
1739
|
"title": "Testumgebung nicht konfiguriert",
|
|
1740
|
-
"body": "Es ist kein Anbieter für ephemere Umgebungen registriert, sodass Test-Agenten, die eine
|
|
1740
|
+
"body": "Es ist kein Anbieter für ephemere Umgebungen registriert, sodass Test-Agenten, die eine aktive Vorschau-Umgebung benötigen, nicht laufen können. Öffnen Sie unten „Testumgebungen“ und verbinden Sie einen Kubernetes-Cluster oder einen benutzerdefinierten HTTP-Umgebungsanbieter, um sie zu aktivieren.",
|
|
1741
1741
|
"action": "Environment konfigurieren"
|
|
1742
1742
|
},
|
|
1743
1743
|
"binaryStorage": {
|
package/i18n/locales/en.json
CHANGED
|
@@ -1660,7 +1660,7 @@
|
|
|
1660
1660
|
},
|
|
1661
1661
|
"ephemeralEnvironments": {
|
|
1662
1662
|
"title": "Test environment not configured",
|
|
1663
|
-
"body": "No ephemeral environment provider is registered, so testing agents that need a live environment can't run.
|
|
1663
|
+
"body": "No ephemeral environment provider is registered, so testing agents that need a live preview environment can't run. Open Test environments below and connect a Kubernetes cluster or a custom HTTP environment provider to enable them.",
|
|
1664
1664
|
"action": "Configure environment"
|
|
1665
1665
|
},
|
|
1666
1666
|
"binaryStorage": {
|
|
@@ -1989,7 +1989,7 @@
|
|
|
1989
1989
|
"editTitle": "Edit {id}",
|
|
1990
1990
|
"addTitle": "Add a type",
|
|
1991
1991
|
"manifestId": "Manifest id",
|
|
1992
|
-
"manifestIdHelp": "A lower-kebab slug, e.g. my-
|
|
1992
|
+
"manifestIdHelp": "A lower-kebab slug, e.g. my-preview-template.",
|
|
1993
1993
|
"label": "Label",
|
|
1994
1994
|
"acceptsInputHint": "Input hint (optional)",
|
|
1995
1995
|
"acceptsInputHintHelp": "Describes the input shape the provider expects.",
|
package/i18n/locales/es.json
CHANGED
|
@@ -1600,7 +1600,7 @@
|
|
|
1600
1600
|
},
|
|
1601
1601
|
"ephemeralEnvironments": {
|
|
1602
1602
|
"title": "Entorno de pruebas no configurado",
|
|
1603
|
-
"body": "No hay ningún proveedor de entornos efímeros registrado, así que los agentes de pruebas que necesitan un entorno activo no pueden ejecutarse.
|
|
1603
|
+
"body": "No hay ningún proveedor de entornos efímeros registrado, así que los agentes de pruebas que necesitan un entorno de vista previa activo no pueden ejecutarse. Abre Entornos de prueba abajo y conecta un clúster de Kubernetes o un proveedor de entornos HTTP personalizado para habilitarlos.",
|
|
1604
1604
|
"action": "Configurar entorno"
|
|
1605
1605
|
},
|
|
1606
1606
|
"binaryStorage": {
|
|
@@ -2511,7 +2511,7 @@
|
|
|
2511
2511
|
"editTitle": "Editar {id}",
|
|
2512
2512
|
"addTitle": "Añadir un tipo",
|
|
2513
2513
|
"manifestId": "Id del manifiesto",
|
|
2514
|
-
"manifestIdHelp": "Un slug en minúsculas y guiones, p. ej. my-
|
|
2514
|
+
"manifestIdHelp": "Un slug en minúsculas y guiones, p. ej. my-preview-template.",
|
|
2515
2515
|
"label": "Etiqueta",
|
|
2516
2516
|
"acceptsInputHint": "Pista de entrada (opcional)",
|
|
2517
2517
|
"acceptsInputHintHelp": "Describe la forma de entrada que espera el proveedor.",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -1600,7 +1600,7 @@
|
|
|
1600
1600
|
},
|
|
1601
1601
|
"ephemeralEnvironments": {
|
|
1602
1602
|
"title": "Environnement de test non configuré",
|
|
1603
|
-
"body": "Aucun fournisseur d'environnements éphémères n'est enregistré, donc les agents de test qui nécessitent un environnement actif ne peuvent pas s'exécuter.
|
|
1603
|
+
"body": "Aucun fournisseur d'environnements éphémères n'est enregistré, donc les agents de test qui nécessitent un environnement de prévisualisation actif ne peuvent pas s'exécuter. Ouvrez Environnements de test ci-dessous et connectez un cluster Kubernetes ou un fournisseur d'environnements HTTP personnalisé pour les activer.",
|
|
1604
1604
|
"action": "Configurer l'environnement"
|
|
1605
1605
|
},
|
|
1606
1606
|
"binaryStorage": {
|
|
@@ -2511,7 +2511,7 @@
|
|
|
2511
2511
|
"editTitle": "Modifier {id}",
|
|
2512
2512
|
"addTitle": "Ajouter un type",
|
|
2513
2513
|
"manifestId": "Id du manifeste",
|
|
2514
|
-
"manifestIdHelp": "Un slug en minuscules avec des tirets, p. ex. my-
|
|
2514
|
+
"manifestIdHelp": "Un slug en minuscules avec des tirets, p. ex. my-preview-template.",
|
|
2515
2515
|
"label": "Libellé",
|
|
2516
2516
|
"acceptsInputHint": "Indice d'entrée (facultatif)",
|
|
2517
2517
|
"acceptsInputHintHelp": "Décrit la forme d'entrée attendue par le fournisseur.",
|
package/i18n/locales/he.json
CHANGED
|
@@ -1600,7 +1600,7 @@
|
|
|
1600
1600
|
},
|
|
1601
1601
|
"ephemeralEnvironments": {
|
|
1602
1602
|
"title": "סביבת בדיקות אינה מוגדרת",
|
|
1603
|
-
"body": "לא רשום שום ספק סביבות ארעיות, ולכן סוכני בדיקה הזקוקים
|
|
1603
|
+
"body": "לא רשום שום ספק סביבות ארעיות, ולכן סוכני בדיקה הזקוקים לסביבת תצוגה מקדימה פעילה אינם יכולים לפעול. פתחו את 'סביבות בדיקה' למטה וחברו אשכול Kubernetes או ספק סביבות HTTP מותאם אישית כדי להפעיל אותם.",
|
|
1604
1604
|
"action": "הגדרת סביבה"
|
|
1605
1605
|
},
|
|
1606
1606
|
"binaryStorage": {
|
|
@@ -1928,7 +1928,7 @@
|
|
|
1928
1928
|
"editTitle": "עריכת {id}",
|
|
1929
1929
|
"addTitle": "הוספת סוג",
|
|
1930
1930
|
"manifestId": "מזהה מניפסט",
|
|
1931
|
-
"manifestIdHelp": "slug באותיות קטנות ומקפים, לדוגמה my-
|
|
1931
|
+
"manifestIdHelp": "slug באותיות קטנות ומקפים, לדוגמה my-preview-template.",
|
|
1932
1932
|
"label": "תווית",
|
|
1933
1933
|
"acceptsInputHint": "רמז קלט (אופציונלי)",
|
|
1934
1934
|
"acceptsInputHintHelp": "מתאר את צורת הקלט שהספק מצפה לה.",
|
package/i18n/locales/it.json
CHANGED
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"editTitle": "Modifica {id}",
|
|
138
138
|
"addTitle": "Aggiungi un tipo",
|
|
139
139
|
"manifestId": "Id del manifest",
|
|
140
|
-
"manifestIdHelp": "Uno slug in lower-kebab, es. my-
|
|
140
|
+
"manifestIdHelp": "Uno slug in lower-kebab, es. my-preview-template.",
|
|
141
141
|
"label": "Etichetta",
|
|
142
142
|
"acceptsInputHint": "Suggerimento di input (facoltativo)",
|
|
143
143
|
"acceptsInputHintHelp": "Descrive la forma di input attesa dal provider.",
|
|
@@ -1737,7 +1737,7 @@
|
|
|
1737
1737
|
},
|
|
1738
1738
|
"ephemeralEnvironments": {
|
|
1739
1739
|
"title": "Ambiente di test non configurato",
|
|
1740
|
-
"body": "Nessun provider di ambiente effimero è registrato, quindi gli agenti di test che necessitano di un ambiente live non possono funzionare.
|
|
1740
|
+
"body": "Nessun provider di ambiente effimero è registrato, quindi gli agenti di test che necessitano di un ambiente di anteprima live non possono funzionare. Apri Ambienti di test qui sotto e connetti un cluster Kubernetes o un provider di ambienti HTTP personalizzato per abilitarli.",
|
|
1741
1741
|
"action": "Configura l'ambiente"
|
|
1742
1742
|
},
|
|
1743
1743
|
"binaryStorage": {
|
package/i18n/locales/ja.json
CHANGED
|
@@ -1600,7 +1600,7 @@
|
|
|
1600
1600
|
},
|
|
1601
1601
|
"ephemeralEnvironments": {
|
|
1602
1602
|
"title": "テスト環境が未設定です",
|
|
1603
|
-
"body": "
|
|
1603
|
+
"body": "エフェメラル環境プロバイダーが登録されていないため、稼働中のプレビュー環境を必要とするテストエージェントを実行できません。下の「テスト環境」を開き、Kubernetes クラスターまたはカスタム HTTP 環境プロバイダーを接続して有効にしてください。",
|
|
1604
1604
|
"action": "環境を設定"
|
|
1605
1605
|
},
|
|
1606
1606
|
"binaryStorage": {
|
|
@@ -1929,7 +1929,7 @@
|
|
|
1929
1929
|
"editTitle": "{id} を編集",
|
|
1930
1930
|
"addTitle": "タイプを追加",
|
|
1931
1931
|
"manifestId": "マニフェスト ID",
|
|
1932
|
-
"manifestIdHelp": "小文字とハイフンの slug。例: my-
|
|
1932
|
+
"manifestIdHelp": "小文字とハイフンの slug。例: my-preview-template。",
|
|
1933
1933
|
"label": "ラベル",
|
|
1934
1934
|
"acceptsInputHint": "入力ヒント(任意)",
|
|
1935
1935
|
"acceptsInputHintHelp": "プロバイダーが期待する入力形式を説明します。",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -1600,7 +1600,7 @@
|
|
|
1600
1600
|
},
|
|
1601
1601
|
"ephemeralEnvironments": {
|
|
1602
1602
|
"title": "Środowisko testowe nie jest skonfigurowane",
|
|
1603
|
-
"body": "Nie zarejestrowano żadnego dostawcy środowisk efemerycznych, więc agenci testowi wymagający działającego środowiska nie mogą działać.
|
|
1603
|
+
"body": "Nie zarejestrowano żadnego dostawcy środowisk efemerycznych, więc agenci testowi wymagający działającego środowiska podglądu nie mogą działać. Otwórz poniżej „Środowiska testowe” i podłącz klaster Kubernetes lub niestandardowego dostawcę środowisk HTTP, aby je włączyć.",
|
|
1604
1604
|
"action": "Skonfiguruj środowisko"
|
|
1605
1605
|
},
|
|
1606
1606
|
"binaryStorage": {
|
|
@@ -2511,7 +2511,7 @@
|
|
|
2511
2511
|
"editTitle": "Edytuj {id}",
|
|
2512
2512
|
"addTitle": "Dodaj typ",
|
|
2513
2513
|
"manifestId": "Id manifestu",
|
|
2514
|
-
"manifestIdHelp": "Slug małymi literami z myślnikami, np. my-
|
|
2514
|
+
"manifestIdHelp": "Slug małymi literami z myślnikami, np. my-preview-template.",
|
|
2515
2515
|
"label": "Etykieta",
|
|
2516
2516
|
"acceptsInputHint": "Wskazówka wejścia (opcjonalnie)",
|
|
2517
2517
|
"acceptsInputHintHelp": "Opisuje format wejścia oczekiwany przez dostawcę.",
|
package/i18n/locales/tr.json
CHANGED
|
@@ -1600,7 +1600,7 @@
|
|
|
1600
1600
|
},
|
|
1601
1601
|
"ephemeralEnvironments": {
|
|
1602
1602
|
"title": "Test ortamı yapılandırılmadı",
|
|
1603
|
-
"body": "Kayıtlı bir geçici ortam sağlayıcısı yok; bu nedenle çalışan bir
|
|
1603
|
+
"body": "Kayıtlı bir geçici ortam sağlayıcısı yok; bu nedenle çalışan bir önizleme ortamına ihtiyaç duyan test aracıları çalışamaz. Aşağıdaki Test ortamları sekmesini açın ve etkinleştirmek için bir Kubernetes kümesi ya da özel bir HTTP ortam sağlayıcısı bağlayın.",
|
|
1604
1604
|
"action": "Ortamı yapılandır"
|
|
1605
1605
|
},
|
|
1606
1606
|
"binaryStorage": {
|
|
@@ -1929,7 +1929,7 @@
|
|
|
1929
1929
|
"editTitle": "{id} düzenle",
|
|
1930
1930
|
"addTitle": "Tür ekle",
|
|
1931
1931
|
"manifestId": "Manifest kimliği",
|
|
1932
|
-
"manifestIdHelp": "Küçük harf ve tireli bir slug, ör. my-
|
|
1932
|
+
"manifestIdHelp": "Küçük harf ve tireli bir slug, ör. my-preview-template.",
|
|
1933
1933
|
"label": "Etiket",
|
|
1934
1934
|
"acceptsInputHint": "Girdi ipucu (isteğe bağlı)",
|
|
1935
1935
|
"acceptsInputHintHelp": "Sağlayıcının beklediği girdi biçimini açıklar.",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -1600,7 +1600,7 @@
|
|
|
1600
1600
|
},
|
|
1601
1601
|
"ephemeralEnvironments": {
|
|
1602
1602
|
"title": "Тестове середовище не налаштоване",
|
|
1603
|
-
"body": "Не зареєстровано жодного постачальника ефемерних середовищ, тож тестові агенти, яким потрібне робоче
|
|
1603
|
+
"body": "Не зареєстровано жодного постачальника ефемерних середовищ, тож тестові агенти, яким потрібне робоче середовище попереднього перегляду, не можуть працювати. Відкрийте нижче «Тестові середовища» та підключіть кластер Kubernetes або власного постачальника середовищ HTTP, щоб увімкнути їх.",
|
|
1604
1604
|
"action": "Налаштувати середовище"
|
|
1605
1605
|
},
|
|
1606
1606
|
"binaryStorage": {
|
|
@@ -2511,7 +2511,7 @@
|
|
|
2511
2511
|
"editTitle": "Редагувати {id}",
|
|
2512
2512
|
"addTitle": "Додати тип",
|
|
2513
2513
|
"manifestId": "Id маніфесту",
|
|
2514
|
-
"manifestIdHelp": "Slug у нижньому регістрі з дефісами, напр. my-
|
|
2514
|
+
"manifestIdHelp": "Slug у нижньому регістрі з дефісами, напр. my-preview-template.",
|
|
2515
2515
|
"label": "Мітка",
|
|
2516
2516
|
"acceptsInputHint": "Підказка вводу (необов'язково)",
|
|
2517
2517
|
"acceptsInputHintHelp": "Описує форму вводу, яку очікує провайдер.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.116.
|
|
3
|
+
"version": "0.116.7",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"valibot": "^1.4.2",
|
|
35
35
|
"vue": "3.5.39",
|
|
36
36
|
"wretch": "^3.0.9",
|
|
37
|
-
"@cat-factory/contracts": "0.128.
|
|
37
|
+
"@cat-factory/contracts": "0.128.2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@toad-contracts/testing": "0.3.2",
|