@cat-factory/app 0.134.0 → 0.135.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,25 @@
1
1
  <script setup lang="ts">
2
- // The environment setup wizard (shared-stacks slice 7): a guided detect → review → preflight →
3
- // save flow that configures a service frame's `docker-compose` provisioning so the SINGLE Deployer
4
- // step provisions it (recipe execution + shared stacks + preflights), and the Tester targets the
5
- // resulting env. All cross-step state + actions live in `useEnvironmentWizardStore`; this component
6
- // is the presentation shell. On save it registers the workspace's `docker-compose` handler AND
7
- // writes the recipe onto the frame, then optionally trial-provisions with live logs.
8
- import { computed, ref, watch } from 'vue'
9
- import type {
10
- MergeableRecipeField,
11
- ProvisioningComposeFileCandidate,
12
- ProvisioningProfileCandidate,
13
- ProvisioningSeedDumpCandidate,
14
- } from '@cat-factory/contracts'
15
- import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
2
+ // The environment setup wizard shell (shared-stacks slice 7; converted to a
3
+ // modular-vue journey in slice 3 of the modular-vue adoption
4
+ // docs/initiatives/modular-vue-adoption.md).
5
+ //
6
+ // This component is now purely the MODAL + STEPPER CHROME. The step sequence,
7
+ // forward/back navigation, and resume are owned by the `environment-setup`
8
+ // journey (`~/modular/journeys/environmentSetup`), hosted here by `<JourneyHost>`:
9
+ // it starts the journey on open (RESUMING the in-flight instance for the same
10
+ // frame when one was left mid-flow, via the Pinia persistence adapter), renders
11
+ // the current step through `<JourneyOutlet>`, and abandons it on close. Each step
12
+ // component drives the `environmentWizard` store for its data/actions and fires
13
+ // the journey's exits to advance. On `complete` (the save step's Done) the journey
14
+ // clears its persisted blob and we close the modal.
15
+ import { computed } from 'vue'
16
+ import { JourneyHost, JourneyOutlet } from '@modular-vue/journeys'
17
+ import { environmentSetupHandle } from '~/modular/journeys/environmentSetup'
18
+ import { ENV_STEP_ORDER, type EnvStep } from '~/modular/journeys/environmentSetup.logic'
16
19
 
17
20
  const ui = useUiStore()
18
- const store = useEnvironmentWizardStore()
19
- const preflights = usePreflightsStore()
20
21
  const { t } = useI18n()
21
22
 
22
- // The host-probe runtime isn't wired (a non-local facade 503'd) — the checklist degrades to a note.
23
- const preflightsUnavailable = computed(() => preflights.available === false)
24
-
25
23
  const open = computed({
26
24
  get: () => ui.environmentWizardOpen,
27
25
  set: (v: boolean) => {
@@ -29,118 +27,25 @@ const open = computed({
29
27
  },
30
28
  })
31
29
 
32
- // Reset + (re)seed the flow whenever the modal opens, honouring the frame the launcher preselected.
33
- watch(
34
- open,
35
- (isOpen) => {
36
- if (isOpen) store.open(ui.environmentWizardFrameId)
37
- },
38
- { immediate: true },
39
- )
30
+ // Read once at journey start; keyed for resume. Frozen for the host's lifetime,
31
+ // so the modal remounts the host per open (it renders under `v-if` upstream).
32
+ const input = computed(() => ({ frameId: ui.environmentWizardFrameId }))
40
33
 
41
- // ---- Stepper header ---------------------------------------------------------
42
- const STEP_ORDER = ['pick', 'review', 'preflight', 'save'] as const
43
- type Step = (typeof STEP_ORDER)[number]
44
- const STEP_LABEL = computed<Record<Step, string>>(() => ({
34
+ const STEP_LABEL = computed<Record<EnvStep, string>>(() => ({
45
35
  pick: t('environmentWizard.steps.pick'),
46
36
  review: t('environmentWizard.steps.review'),
47
37
  preflight: t('environmentWizard.steps.preflight'),
48
38
  save: t('environmentWizard.steps.save'),
49
39
  }))
50
- const currentIndex = computed(() => STEP_ORDER.indexOf(store.step as Step))
51
-
52
- // ---- Provenance -------------------------------------------------------------
53
- const FIELD_LABEL = computed<Record<MergeableRecipeField, string>>(() => ({
54
- composeFiles: t('environmentWizard.field.composeFiles'),
55
- composeProfiles: t('environmentWizard.field.composeProfiles'),
56
- envFiles: t('environmentWizard.field.envFiles'),
57
- externalNetworks: t('environmentWizard.field.externalNetworks'),
58
- sharedStackRefs: t('environmentWizard.field.sharedStackRefs'),
59
- prerequisites: t('environmentWizard.field.prerequisites'),
60
- setupSteps: t('environmentWizard.field.setupSteps'),
61
- healthGate: t('environmentWizard.field.healthGate'),
62
- teardownSteps: t('environmentWizard.field.teardownSteps'),
63
- }))
64
- const ORIGIN_COLOR: Record<'detector' | 'analyst' | 'both', 'success' | 'info' | 'warning'> = {
65
- detector: 'success',
66
- analyst: 'info',
67
- both: 'warning',
68
- }
69
- const ORIGIN_LABEL = computed<Record<'detector' | 'analyst' | 'both', string>>(() => ({
70
- detector: t('environmentWizard.origin.detector'),
71
- analyst: t('environmentWizard.origin.analyst'),
72
- both: t('environmentWizard.origin.both'),
73
- }))
74
-
75
- // ---- Candidate helpers ------------------------------------------------------
76
- const composeFileCandidates = computed<ProvisioningComposeFileCandidate[]>(
77
- () => store.recommendation?.composeFileCandidates ?? [],
78
- )
79
- const profileCandidates = computed<ProvisioningProfileCandidate[]>(
80
- () => store.recommendation?.profileCandidates ?? [],
81
- )
82
- const seedDumpCandidates = computed<ProvisioningSeedDumpCandidate[]>(
83
- () => store.recommendation?.seedDumpCandidates ?? [],
84
- )
85
- const composeServiceOptions = computed(() =>
86
- (store.recommendation?.composeServiceCandidates ?? []).map((c) => ({
87
- label: c.recommended ? `${c.service} · ${t('environmentWizard.recommended')}` : c.service,
88
- value: c.service,
89
- })),
90
- )
91
- // The repo ships its own imperative bring-up (a `bin/*console*` / Makefile / justfile the
92
- // deterministic scan can't read) — the strongest signal to run the analyst, so when present we
93
- // elevate the deep-analysis affordance from a quiet opt-in to a prominent nudge naming the CLI.
94
- const repoCliHint = computed(() => store.recommendation?.repoCliHint)
95
-
96
- function fileEnabled(path: string): boolean {
97
- return store.recipe.composeFiles?.includes(path) ?? false
98
- }
99
- function profileEnabled(profile: string): boolean {
100
- return store.recipe.composeProfiles?.includes(profile) ?? false
101
- }
102
- function seedAdded(path: string): boolean {
103
- return (store.recipe.setupSteps ?? []).some(
104
- (s) => s.kind === 'compose-exec' && s.stdinFile === path,
105
- )
106
- }
107
-
108
- // ---- Raw recipe editor ------------------------------------------------------
109
- const rawOpen = ref(false)
110
- const rawText = ref('')
111
- const rawError = ref<string | null>(null)
112
- function openRaw() {
113
- rawText.value = JSON.stringify(store.recipe, null, 2)
114
- rawError.value = null
115
- rawOpen.value = true
116
- }
117
- function toggleRaw() {
118
- if (rawOpen.value) rawOpen.value = false
119
- else openRaw()
120
- }
121
- function applyRaw() {
122
- rawError.value = store.setRecipeFromJson(rawText.value)
123
- if (!rawError.value) rawOpen.value = false
124
- }
125
-
126
- // ---- Preflight verdict presentation -----------------------------------------
127
- const PREFLIGHT_COLOR: Record<'pass' | 'warn' | 'fail', 'success' | 'warning' | 'error'> = {
128
- pass: 'success',
129
- warn: 'warning',
130
- fail: 'error',
131
- }
132
40
 
133
- // ---- Step navigation --------------------------------------------------------
134
- const canLeaveReview = computed(
135
- () => !!store.recommendation && store.composeService.trim().length > 0,
136
- )
137
- function back() {
138
- const idx = currentIndex.value
139
- if (idx > 0) store.goToStep(STEP_ORDER[idx - 1]!)
140
- }
141
- function next() {
142
- const idx = currentIndex.value
143
- if (idx < STEP_ORDER.length - 1) store.goToStep(STEP_ORDER[idx + 1]!)
41
+ /** The crumb index of a step, relative to the current journey step. */
42
+ function crumbState(
43
+ entry: EnvStep,
44
+ currentEntry: EnvStep | undefined,
45
+ ): 'current' | 'done' | 'todo' {
46
+ if (!currentEntry) return 'todo'
47
+ if (entry === currentEntry) return 'current'
48
+ return ENV_STEP_ORDER.indexOf(entry) < ENV_STEP_ORDER.indexOf(currentEntry) ? 'done' : 'todo'
144
49
  }
145
50
  </script>
146
51
 
@@ -153,522 +58,55 @@ function next() {
153
58
  >
154
59
  <template #body>
155
60
  <div class="space-y-5" data-testid="env-setup-wizard">
156
- <!-- stepper header -->
157
- <ol class="flex items-center gap-2 text-[11px]">
158
- <li
159
- v-for="(s, i) in STEP_ORDER"
160
- :key="s"
161
- class="flex items-center gap-2"
162
- :data-testid="`env-setup-crumb-${s}`"
163
- >
164
- <span
165
- class="flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-semibold"
166
- :class="
167
- i === currentIndex
168
- ? 'bg-primary-500 text-white'
169
- : i < currentIndex
170
- ? 'bg-emerald-600/70 text-white'
171
- : 'bg-slate-700 text-slate-300'
172
- "
173
- >{{ i + 1 }}</span
174
- >
175
- <span :class="i === currentIndex ? 'text-slate-100' : 'text-slate-500'">
176
- {{ STEP_LABEL[s] }}
177
- </span>
178
- <UIcon
179
- v-if="i < STEP_ORDER.length - 1"
180
- name="i-lucide-chevron-right"
181
- class="h-3 w-3 text-slate-600"
182
- />
183
- </li>
184
- </ol>
185
-
186
- <!-- ============================ PICK ============================ -->
187
- <section v-if="store.step === 'pick'" class="space-y-3" data-testid="env-setup-step-pick">
188
- <p class="text-sm text-slate-400">{{ t('environmentWizard.pick.intro') }}</p>
189
- <p v-if="!store.serviceFrames.length" class="text-sm text-slate-500">
190
- {{ t('environmentWizard.pick.empty') }}
191
- </p>
192
- <div v-else class="space-y-1.5">
193
- <UButton
194
- v-for="frame in store.serviceFrames"
195
- :key="frame.id"
196
- block
197
- color="neutral"
198
- variant="soft"
199
- class="justify-start"
200
- icon="i-lucide-box"
201
- :data-testid="`env-setup-frame-${frame.id}`"
202
- @click="store.selectFrame(frame.id)"
203
- >
204
- {{ frame.title }}
205
- </UButton>
206
- </div>
207
- </section>
208
-
209
- <!-- =========================== REVIEW =========================== -->
210
- <section
211
- v-else-if="store.step === 'review'"
212
- class="space-y-4"
213
- data-testid="env-setup-step-review"
61
+ <JourneyHost
62
+ :handle="environmentSetupHandle"
63
+ :input="input"
64
+ @finished="ui.closeEnvironmentSetup()"
214
65
  >
215
- <!-- detection status -->
216
- <div class="flex items-center justify-between gap-2">
217
- <div class="min-w-0">
218
- <p class="text-sm font-medium text-slate-200">{{ store.targetFrame?.title }}</p>
219
- <p class="text-[11px] text-slate-500">
220
- {{ t('environmentWizard.review.detectHint') }}
221
- </p>
222
- </div>
223
- <UButton
224
- size="xs"
225
- variant="soft"
226
- color="primary"
227
- icon="i-lucide-wand-sparkles"
228
- :loading="store.detecting"
229
- :disabled="!store.hasRepo"
230
- data-testid="env-setup-detect"
231
- @click="store.detect()"
232
- >
233
- {{ t('environmentWizard.review.detect') }}
234
- </UButton>
235
- </div>
236
-
237
- <p v-if="!store.hasRepo" class="text-[12px] text-amber-300/80">
238
- {{ t('environmentWizard.review.noRepo') }}
239
- </p>
240
- <p
241
- v-else-if="store.detectError"
242
- class="text-[12px] text-rose-300/80"
243
- data-testid="env-setup-detect-error"
244
- >
245
- {{ t('environmentWizard.review.detectError') }}
246
- </p>
247
-
248
- <template v-if="store.recommendation && !store.detecting">
249
- <!-- deep analysis (opt-in; elevated to a prominent nudge when the repo ships its own CLI) -->
250
- <div
251
- class="rounded-md border p-3"
252
- :class="
253
- repoCliHint
254
- ? 'border-primary-700/60 bg-primary-950/30'
255
- : 'border-slate-800 bg-slate-900/40'
256
- "
257
- >
258
- <p
259
- v-if="repoCliHint"
260
- class="mb-2 flex items-start gap-1.5 text-[11px] text-primary-300"
261
- data-testid="env-setup-cli-nudge"
262
- >
263
- <UIcon name="i-lucide-lightbulb" class="mt-0.5 h-3.5 w-3.5 shrink-0" />
264
- <span>{{
265
- t('environmentWizard.analysis.cliNudge', { path: repoCliHint.path })
266
- }}</span>
267
- </p>
268
- <div class="flex items-center justify-between gap-2">
269
- <div class="min-w-0">
270
- <p class="text-[12px] font-medium text-slate-300">
271
- {{ t('environmentWizard.analysis.title') }}
272
- </p>
273
- <p class="text-[11px] text-slate-500">
274
- {{ t('environmentWizard.analysis.hint') }}
275
- </p>
276
- </div>
277
- <UButton
278
- size="xs"
279
- variant="soft"
280
- :color="repoCliHint ? 'primary' : 'neutral'"
281
- icon="i-lucide-sparkles"
282
- :loading="store.analysisStatus === 'running'"
283
- :disabled="!store.canAnalyze || store.analysisStatus === 'running'"
284
- data-testid="env-setup-run-analysis"
285
- @click="store.startAnalysis()"
286
- >
287
- {{ t('environmentWizard.analysis.run') }}
288
- </UButton>
289
- </div>
290
- <p
291
- v-if="!store.canAnalyze"
292
- class="mt-2 text-[11px] text-slate-500"
293
- data-testid="env-setup-analysis-unavailable"
294
- >
295
- {{ t('environmentWizard.analysis.unavailable') }}
296
- </p>
297
- <p
298
- v-else-if="store.analysisStatus === 'failed'"
299
- class="mt-2 text-[11px] text-rose-300/80"
66
+ <template #default="{ instanceId, instance }">
67
+ <!-- stepper header, driven by the journey's current step -->
68
+ <ol class="flex items-center gap-2 text-[11px]">
69
+ <li
70
+ v-for="(s, i) in ENV_STEP_ORDER"
71
+ :key="s"
72
+ class="flex items-center gap-2"
73
+ :data-testid="`env-setup-crumb-${s}`"
300
74
  >
301
- {{ t('environmentWizard.analysis.failed') }}
302
- </p>
303
- <div
304
- v-else-if="store.analysisStatus === 'ready'"
305
- class="mt-2 space-y-2"
306
- data-testid="env-setup-analysis-ready"
307
- >
308
- <p v-if="store.merged?.summary" class="text-[11px] leading-snug text-slate-400">
309
- {{ store.merged.summary }}
310
- </p>
311
- <UButton
312
- size="xs"
313
- variant="soft"
314
- color="primary"
315
- icon="i-lucide-git-merge"
316
- data-testid="env-setup-apply-analysis"
317
- @click="store.applyAnalystDraft()"
318
- >
319
- {{ t('environmentWizard.analysis.apply') }}
320
- </UButton>
321
- </div>
322
- </div>
323
-
324
- <!-- per-field provenance -->
325
- <div v-if="store.merged?.fields.length" class="space-y-1.5">
326
- <p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
327
- {{ t('environmentWizard.review.provenanceTitle') }}
328
- </p>
329
- <div class="flex flex-wrap gap-1.5">
330
- <UBadge
331
- v-for="f in store.merged.fields"
332
- :key="f.field"
333
- :color="ORIGIN_COLOR[f.origin]"
334
- variant="subtle"
335
- size="sm"
336
- :title="f.detectorMessage ?? f.analystRationale ?? ''"
337
- :data-testid="`env-setup-field-${f.field}`"
338
- >
339
- {{ FIELD_LABEL[f.field] }} · {{ ORIGIN_LABEL[f.origin] }}
340
- </UBadge>
341
- </div>
342
- </div>
343
-
344
- <!-- exposed compose service + port -->
345
- <div class="grid grid-cols-2 gap-3">
346
- <UFormField :label="t('environmentWizard.review.service')" required>
347
- <USelect
348
- v-if="composeServiceOptions.length"
349
- v-model="store.composeService"
350
- :items="composeServiceOptions"
351
- value-key="value"
352
- :placeholder="t('environmentWizard.review.servicePlaceholder')"
353
- class="w-full"
354
- data-testid="env-setup-service-select"
355
- />
356
- <UInput
357
- v-else
358
- v-model="store.composeService"
359
- :placeholder="t('environmentWizard.review.servicePlaceholder')"
360
- class="w-full"
361
- data-testid="env-setup-service-input"
362
- />
363
- </UFormField>
364
- <UFormField :label="t('environmentWizard.review.port')">
365
- <UInput
366
- v-model.number="store.exposedPort"
367
- type="number"
368
- class="w-full"
369
- data-testid="env-setup-port"
370
- />
371
- </UFormField>
372
- </div>
373
-
374
- <!-- compose file layering -->
375
- <div v-if="composeFileCandidates.length" class="space-y-1.5">
376
- <p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
377
- {{ t('environmentWizard.review.composeFiles') }}
378
- </p>
379
- <div class="flex flex-wrap gap-1.5">
380
- <UButton
381
- v-for="c in composeFileCandidates"
382
- :key="c.path"
383
- size="xs"
384
- :color="fileEnabled(c.path) ? 'primary' : 'neutral'"
385
- :variant="fileEnabled(c.path) ? 'soft' : 'ghost'"
386
- :icon="fileEnabled(c.path) ? 'i-lucide-check' : 'i-lucide-plus'"
387
- :data-testid="`env-setup-file-${c.name}`"
388
- @click="store.toggleComposeFile(c.path)"
389
- >
390
- {{ c.name }}<span v-if="c.os" class="ms-1 text-[9px] opacity-70">{{ c.os }}</span>
391
- </UButton>
392
- </div>
393
- </div>
394
-
395
- <!-- compose profiles -->
396
- <div v-if="profileCandidates.length" class="space-y-1.5">
397
- <p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
398
- {{ t('environmentWizard.review.profiles') }}
399
- </p>
400
- <div class="flex flex-wrap gap-1.5">
401
- <UButton
402
- v-for="c in profileCandidates"
403
- :key="c.profile"
404
- size="xs"
405
- :color="profileEnabled(c.profile) ? 'primary' : 'neutral'"
406
- :variant="profileEnabled(c.profile) ? 'soft' : 'ghost'"
407
- :icon="profileEnabled(c.profile) ? 'i-lucide-check' : 'i-lucide-plus'"
408
- :data-testid="`env-setup-profile-${c.profile}`"
409
- @click="store.toggleProfile(c.profile)"
410
- >
411
- {{ c.profile }}
412
- </UButton>
413
- </div>
414
- </div>
415
-
416
- <!-- seed dumps -->
417
- <div v-if="seedDumpCandidates.length" class="space-y-1.5">
418
- <p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
419
- {{ t('environmentWizard.review.seedDumps') }}
420
- </p>
421
- <div class="space-y-1">
422
- <div
423
- v-for="c in seedDumpCandidates"
424
- :key="c.path"
425
- class="flex items-center justify-between gap-2 rounded border border-slate-800 bg-slate-900/40 px-2 py-1"
75
+ <span
76
+ class="flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-semibold"
77
+ :class="{
78
+ 'bg-primary-500 text-white':
79
+ crumbState(s, (instance?.step?.entry as EnvStep) ?? undefined) === 'current',
80
+ 'bg-emerald-600/70 text-white':
81
+ crumbState(s, (instance?.step?.entry as EnvStep) ?? undefined) === 'done',
82
+ 'bg-slate-700 text-slate-300':
83
+ crumbState(s, (instance?.step?.entry as EnvStep) ?? undefined) === 'todo',
84
+ }"
85
+ >{{ i + 1 }}</span
426
86
  >
427
- <span class="truncate text-[11px] text-slate-300">{{ c.path }}</span>
428
- <UButton
429
- size="xs"
430
- :color="seedAdded(c.path) ? 'success' : 'neutral'"
431
- :variant="seedAdded(c.path) ? 'soft' : 'ghost'"
432
- :icon="seedAdded(c.path) ? 'i-lucide-check' : 'i-lucide-plus'"
433
- :disabled="seedAdded(c.path)"
434
- :data-testid="`env-setup-seed-${c.name}`"
435
- @click="store.addSeedStep(c)"
436
- >
437
- {{
438
- seedAdded(c.path)
439
- ? t('environmentWizard.review.seedAdded')
440
- : t('environmentWizard.review.seedAdd')
441
- }}
442
- </UButton>
443
- </div>
444
- </div>
445
- </div>
446
-
447
- <!-- raw recipe editor -->
448
- <div>
449
- <UButton
450
- size="xs"
451
- variant="ghost"
452
- color="neutral"
453
- :icon="rawOpen ? 'i-lucide-chevron-down' : 'i-lucide-code'"
454
- data-testid="env-setup-raw-toggle"
455
- @click="toggleRaw"
456
- >
457
- {{ t('environmentWizard.review.rawEditor') }}
458
- </UButton>
459
- <div v-if="rawOpen" class="mt-2 space-y-2">
460
- <UTextarea
461
- v-model="rawText"
462
- :rows="10"
463
- class="w-full font-mono text-[11px]"
464
- data-testid="env-setup-raw-text"
465
- />
466
- <p
467
- v-if="rawError"
468
- class="text-[11px] text-rose-300/80"
469
- data-testid="env-setup-raw-error"
87
+ <span
88
+ :class="
89
+ crumbState(s, (instance?.step?.entry as EnvStep) ?? undefined) === 'current'
90
+ ? 'text-slate-100'
91
+ : 'text-slate-500'
92
+ "
470
93
  >
471
- {{ rawError }}
472
- </p>
473
- <div class="flex justify-end">
474
- <UButton
475
- size="xs"
476
- color="primary"
477
- data-testid="env-setup-raw-apply"
478
- @click="applyRaw"
479
- >
480
- {{ t('environmentWizard.review.rawApply') }}
481
- </UButton>
482
- </div>
483
- </div>
484
- </div>
485
- </template>
486
- </section>
487
-
488
- <!-- ========================== PREFLIGHT ========================= -->
489
- <section
490
- v-else-if="store.step === 'preflight'"
491
- class="space-y-3"
492
- data-testid="env-setup-step-preflight"
493
- >
494
- <div class="flex items-center justify-between gap-2">
495
- <p class="text-sm text-slate-400">{{ t('environmentWizard.preflight.intro') }}</p>
496
- <UButton
497
- size="xs"
498
- variant="soft"
499
- color="primary"
500
- icon="i-lucide-list-checks"
501
- :loading="store.preflightRunning"
502
- data-testid="env-setup-preflight-run"
503
- @click="store.runPreflight()"
504
- >
505
- {{ t('environmentWizard.preflight.run') }}
506
- </UButton>
507
- </div>
508
-
509
- <p
510
- v-if="!store.recipe.prerequisites?.length"
511
- class="text-[12px] text-slate-500"
512
- data-testid="env-setup-preflight-none"
513
- >
514
- {{ t('environmentWizard.preflight.none') }}
515
- </p>
516
- <p
517
- v-else-if="preflightsUnavailable"
518
- class="text-[12px] text-amber-300/80"
519
- data-testid="env-setup-preflight-unavailable"
520
- >
521
- {{ t('environmentWizard.preflight.unavailable') }}
522
- </p>
523
- <p
524
- v-if="store.preflightError"
525
- class="text-[12px] text-rose-300/80"
526
- data-testid="env-setup-preflight-error"
527
- >
528
- {{ store.preflightError }}
529
- </p>
530
-
531
- <ul
532
- v-if="store.preflightResults?.length"
533
- class="space-y-2"
534
- data-testid="env-setup-preflight-results"
535
- >
536
- <li
537
- v-for="r in store.preflightResults"
538
- :key="r.title"
539
- class="rounded border border-slate-800 bg-slate-900/40 p-2"
540
- >
541
- <div class="flex items-center gap-2">
542
- <UBadge :color="PREFLIGHT_COLOR[r.status]" variant="subtle" size="sm">
543
- {{ r.status }}
544
- </UBadge>
545
- <span class="text-[12px] text-slate-200">{{ r.title }}</span>
546
- <span v-if="!r.required" class="ms-auto text-[10px] text-slate-500">
547
- {{ t('environmentWizard.preflight.optional') }}
94
+ {{ STEP_LABEL[s] }}
548
95
  </span>
549
- </div>
550
- <p v-if="r.detail" class="mt-1 text-[11px] text-slate-400">{{ r.detail }}</p>
551
- <pre
552
- v-if="r.status !== 'pass' && r.remediation"
553
- class="mt-1 max-h-40 overflow-auto whitespace-pre-wrap rounded border border-amber-900/40 bg-amber-950/20 p-1.5 text-[11px] text-amber-200/90"
554
- >{{ r.remediation }}</pre
555
- >
556
- </li>
557
- </ul>
558
- </section>
559
-
560
- <!-- ============================ SAVE ============================ -->
561
- <section v-else class="space-y-3" data-testid="env-setup-step-save">
562
- <UFormField
563
- :label="t('environmentWizard.save.handlerLabel')"
564
- :description="t('environmentWizard.save.handlerHint')"
565
- >
566
- <UInput
567
- v-model="store.handlerLabel"
568
- class="w-full"
569
- data-testid="env-setup-handler-label"
570
- />
571
- </UFormField>
572
-
573
- <div
574
- class="rounded-md border border-slate-800 bg-slate-900/40 p-3 text-[12px] text-slate-300"
575
- >
576
- <p>
577
- {{
578
- t('environmentWizard.save.summary', {
579
- frame: store.targetFrame?.title ?? '',
580
- service: store.composeService,
581
- })
582
- }}
583
- </p>
584
- </div>
585
-
586
- <p
587
- v-if="store.saveError"
588
- class="text-[12px] text-rose-300/80"
589
- data-testid="env-setup-save-error"
590
- >
591
- {{ store.saveError }}
592
- </p>
593
-
594
- <div v-if="!store.saved" class="flex justify-end">
595
- <UButton
596
- color="primary"
597
- icon="i-lucide-save"
598
- :loading="store.saving"
599
- :disabled="!store.composeService.trim()"
600
- data-testid="env-setup-save"
601
- @click="store.save()"
602
- >
603
- {{ t('environmentWizard.save.save') }}
604
- </UButton>
605
- </div>
606
-
607
- <!-- saved: confirmation + optional trial provision -->
608
- <template v-else>
609
- <div
610
- class="flex items-center gap-2 rounded-md border border-emerald-800/50 bg-emerald-950/30 p-2 text-[12px] text-emerald-200"
611
- data-testid="env-setup-saved"
612
- >
613
- <UIcon name="i-lucide-check-circle" class="h-4 w-4" />
614
- {{ t('environmentWizard.save.saved') }}
615
- </div>
96
+ <UIcon
97
+ v-if="i < ENV_STEP_ORDER.length - 1"
98
+ name="i-lucide-chevron-right"
99
+ class="h-3 w-3 text-slate-600"
100
+ />
101
+ </li>
102
+ </ol>
616
103
 
617
- <div class="flex items-center justify-between gap-2">
618
- <p class="text-[11px] text-slate-500">{{ t('environmentWizard.trial.hint') }}</p>
619
- <UButton
620
- size="xs"
621
- variant="soft"
622
- color="neutral"
623
- icon="i-lucide-play"
624
- :loading="store.trialing"
625
- :disabled="store.trialStarted"
626
- data-testid="env-setup-trial"
627
- @click="store.trialProvision()"
628
- >
629
- {{ t('environmentWizard.trial.run') }}
630
- </UButton>
104
+ <!-- the current step (pick / review / preflight / save) -->
105
+ <div class="mt-4">
106
+ <JourneyOutlet :instance-id="instanceId" />
631
107
  </div>
632
- <p v-if="store.trialError" class="text-[11px] text-rose-300/80">
633
- {{ store.trialError }}
634
- </p>
635
- <ProvisioningLogsDrawer v-if="store.trialStarted" subsystem="environment" />
636
108
  </template>
637
- </section>
638
-
639
- <!-- footer nav -->
640
- <div class="flex items-center justify-between border-t border-slate-800 pt-3">
641
- <UButton
642
- color="neutral"
643
- variant="ghost"
644
- :disabled="currentIndex === 0"
645
- icon="i-lucide-arrow-left"
646
- data-testid="env-setup-back"
647
- @click="back"
648
- >
649
- {{ t('common.back') }}
650
- </UButton>
651
- <UButton
652
- v-if="store.step !== 'save'"
653
- color="primary"
654
- trailing-icon="i-lucide-arrow-right"
655
- :disabled="(store.step === 'review' && !canLeaveReview) || store.step === 'pick'"
656
- data-testid="env-setup-next"
657
- @click="next"
658
- >
659
- {{ t('common.next') }}
660
- </UButton>
661
- <UButton
662
- v-else
663
- color="neutral"
664
- variant="soft"
665
- icon="i-lucide-check"
666
- data-testid="env-setup-done"
667
- @click="ui.closeEnvironmentSetup()"
668
- >
669
- {{ t('common.done') }}
670
- </UButton>
671
- </div>
109
+ </JourneyHost>
672
110
  </div>
673
111
  </template>
674
112
  </UModal>