@cat-factory/app 0.258.2 → 0.259.1

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.
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { readEnvironmentAgainstClock } from './OutcomeSummaryWindow.logic'
3
+ import type { OutcomeEnvironment } from '~/utils/runOutcome'
4
+
5
+ /**
6
+ * The one thing the outcome card decides for itself: whether the TTL the payload carries has
7
+ * lapsed, and what that changes. The reduction behind the card is clock-free on purpose, so this
8
+ * is the only place a reader is told an environment is past its expiry, and the failure it
9
+ * guards is the section's worst one: a green "Live" badge and a working-looking button on a row
10
+ * whose own expiry date is in the past.
11
+ */
12
+ const NOW = 1_700_000_000_000
13
+
14
+ const env = (overrides: Partial<OutcomeEnvironment> = {}): OutcomeEnvironment => ({
15
+ url: 'https://preview.test',
16
+ state: 'live',
17
+ origin: 'deployer',
18
+ expiresAt: null,
19
+ retained: false,
20
+ frameId: 'frm_own',
21
+ environmentId: 'env_1',
22
+ detail: null,
23
+ ...overrides,
24
+ })
25
+
26
+ describe('readEnvironmentAgainstClock', () => {
27
+ it('offers a live environment whose TTL has not lapsed', () => {
28
+ const row = readEnvironmentAgainstClock(env({ expiresAt: NOW + 60_000 }), NOW)
29
+ expect(row).toMatchObject({ state: 'live', lapsed: false, openable: true })
30
+ })
31
+
32
+ it('withholds the link once the TTL has lapsed, and says the environment expired', () => {
33
+ const row = readEnvironmentAgainstClock(env({ expiresAt: NOW - 1 }), NOW)
34
+ expect(row).toMatchObject({ state: 'expired', lapsed: true, openable: false })
35
+ // The URL survives the lapse: it is what names the environment and what an operator greps.
36
+ expect(row.url).toBe('https://preview.test')
37
+ })
38
+
39
+ // A run with no disposer and no further polls keeps a `provisioning` row forever, and one
40
+ // whose TTL then lapsed never came up and never will.
41
+ it('applies the lapse to an environment still coming up', () => {
42
+ const row = readEnvironmentAgainstClock(env({ state: 'provisioning', expiresAt: NOW - 1 }), NOW)
43
+ expect(row).toMatchObject({ state: 'expired', lapsed: true, openable: false })
44
+ })
45
+
46
+ // The clock may only answer the question the payload left open. Where a producer already said
47
+ // WHERE the environment went, that word is the more specific one and it stands.
48
+ it('never overwrites a state that already names where the environment went', () => {
49
+ for (const state of ['failed', 'reclaimed', 'reclaiming'] as const) {
50
+ const row = readEnvironmentAgainstClock(env({ state, expiresAt: NOW - 1 }), NOW)
51
+ expect(row).toMatchObject({ state, lapsed: false, openable: false })
52
+ }
53
+ })
54
+
55
+ // `useNowTick` reads 0 until the card mounts, and every instant in history is "past" the epoch.
56
+ it('makes no clock-derived claim before the card has a clock', () => {
57
+ const row = readEnvironmentAgainstClock(env({ expiresAt: NOW - 1 }), 0)
58
+ expect(row).toMatchObject({ state: 'live', lapsed: false, openable: true })
59
+ })
60
+
61
+ it('offers nothing to click for a live environment that has no URL yet', () => {
62
+ const row = readEnvironmentAgainstClock(env({ url: null }), NOW)
63
+ expect(row).toMatchObject({ state: 'live', openable: false })
64
+ })
65
+
66
+ // A run that recorded no TTL is not an expired one: absent and lapsed are opposite facts.
67
+ it('leaves a row carrying no TTL exactly as the payload states it', () => {
68
+ expect(readEnvironmentAgainstClock(env(), NOW)).toMatchObject({
69
+ state: 'live',
70
+ lapsed: false,
71
+ openable: true,
72
+ })
73
+ })
74
+ })
@@ -0,0 +1,63 @@
1
+ // What the outcome card's environment rows say once a CLOCK is applied to them, extracted from
2
+ // `OutcomeSummaryWindow.vue` so the rule can be asserted without mounting the card (see
3
+ // `OutcomeSummaryWindow.logic.spec.ts`).
4
+ //
5
+ // The reduction that produces those rows (`composeRunOutcome`) is deliberately clock-free: the
6
+ // SPA composes it live off its own store and `GET /api/v1/runs/:runId/outcome` composes it
7
+ // server-side, and a rule that read a clock would let the two disagree about one run for as long
8
+ // as their clocks differ. What the payload carries instead is the TTL INSTANT.
9
+ //
10
+ // Somebody still has to say what that instant means now, and it has to be the surface with the
11
+ // clock. Left unapplied, a run whose environment the TTL sweep reclaimed hours ago renders a
12
+ // green "Live" badge and an enabled Open button beside an expiry date in the past: three claims
13
+ // on one row, of which the date is the only true one.
14
+
15
+ import type { OutcomeEnvironment, OutcomeEnvironmentState } from '~/utils/runOutcome'
16
+
17
+ /**
18
+ * The states that describe an environment still STANDING, and so the only ones a lapsed TTL
19
+ * changes what they say.
20
+ *
21
+ * `failed`, `reclaimed` and `reclaiming` already name where the environment went or what is
22
+ * happening to it. A clock may not overwrite those with a less specific word: an environment
23
+ * that never came up did not then expire, and saying so would send a reader looking for a TTL
24
+ * where a provisioning failure is the thing to fix.
25
+ */
26
+ const STANDING_ENVIRONMENT_STATES = new Set<OutcomeEnvironmentState>(['live', 'provisioning'])
27
+
28
+ /** One environment row as the card renders it: the payload's own fields, read against a clock. */
29
+ export interface OutcomeEnvironmentRow extends OutcomeEnvironment {
30
+ /** True when the row's TTL has lapsed against `nowMs` while it still claimed to be standing. */
31
+ lapsed: boolean
32
+ /** Whether the card offers the row as something to click. */
33
+ openable: boolean
34
+ }
35
+
36
+ /**
37
+ * Apply the reader's clock to one environment row.
38
+ *
39
+ * `nowMs` of 0 means the card has not ticked yet (`useNowTick` reads 0 until mounted). No clock
40
+ * means no clock-derived claim: the row reads exactly as the payload states it rather than
41
+ * having every TTL lapse against the epoch.
42
+ */
43
+ export function readEnvironmentAgainstClock(
44
+ entry: OutcomeEnvironment,
45
+ nowMs: number,
46
+ ): OutcomeEnvironmentRow {
47
+ const lapsed =
48
+ nowMs > 0 &&
49
+ entry.expiresAt != null &&
50
+ entry.expiresAt <= nowMs &&
51
+ STANDING_ENVIRONMENT_STATES.has(entry.state)
52
+ const state = lapsed ? 'expired' : entry.state
53
+ return {
54
+ ...entry,
55
+ state,
56
+ lapsed,
57
+ // A link is offered ONLY for a `live` row: an environment that has been reclaimed, has
58
+ // expired or never came up still shows its URL (an operator greps for it, and it says which
59
+ // environment the row is about) and must not be something a designer clicks expecting to see
60
+ // the change.
61
+ openable: state === 'live' && Boolean(entry.url),
62
+ }
63
+ }
@@ -16,9 +16,12 @@
16
16
  // judged against, which turns the tester's requirement IDS into the TITLES a reader came for.
17
17
  import { computed, onUnmounted, ref, watch } from 'vue'
18
18
  import type {
19
+ EnvironmentsGap,
19
20
  OutcomeCheckKind,
20
21
  OutcomeCheckState,
21
22
  OutcomeDisposition,
23
+ OutcomeEnvironmentOrigin,
24
+ OutcomeEnvironmentState,
22
25
  OutcomeSource,
23
26
  OutcomeSpecJoin,
24
27
  OutcomeVisual,
@@ -35,6 +38,8 @@ import { REPRODUCTION_STATUS_KEYS } from '~/utils/reproduction'
35
38
  import type { RequirementVerdictStatus, TestConcernSeverity } from '~/types/domain'
36
39
  import type { TestEnvironment } from '@cat-factory/contracts'
37
40
  import { useArtifactBlobs } from '~/composables/useArtifactBlobs'
41
+ import { useNowTick } from '~/composables/useStepTimer'
42
+ import { readEnvironmentAgainstClock } from '~/components/outcome/OutcomeSummaryWindow.logic'
38
43
  import ArtifactLightbox from '~/components/media/ArtifactLightbox.vue'
39
44
  import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
40
45
  import MarkdownProse from '~/components/common/MarkdownProse.vue'
@@ -46,7 +51,12 @@ const documents = useDocumentsStore()
46
51
  const execution = useExecutionStore()
47
52
  const serviceSpec = useServiceSpecStore()
48
53
  const ui = useUiStore()
49
- const { t } = useI18n()
54
+ const { t, d } = useI18n()
55
+
56
+ // The wall clock this card reads a TTL against. Coarse on purpose: an environment's expiry is
57
+ // the only thing here that moves with time, and a per-second tick would re-render the whole card
58
+ // for a boundary that matters at minute granularity.
59
+ const nowTick = useNowTick(30_000)
50
60
 
51
61
  // Per-window blob cache for the captured views; revoked on unmount so the (large) image bytes
52
62
  // don't outlive the card.
@@ -136,6 +146,38 @@ const SOURCES_GAP_KEYS: Record<SourcesGap, string> = {
136
146
  run_unavailable: RUN_UNAVAILABLE_KEY,
137
147
  none_linked: 'outcome.sources.gap.none_linked',
138
148
  }
149
+ const ENVIRONMENTS_GAP_KEYS: Record<EnvironmentsGap, string> = {
150
+ run_unavailable: RUN_UNAVAILABLE_KEY,
151
+ no_environment_step: 'outcome.environments.gap.no_environment_step',
152
+ not_provisioned: 'outcome.environments.gap.not_provisioned',
153
+ infraless: 'outcome.environments.gap.infraless',
154
+ }
155
+ const ENVIRONMENT_STATE_KEYS: Record<OutcomeEnvironmentState, string> = {
156
+ live: 'outcome.environments.state.live',
157
+ provisioning: 'outcome.environments.state.provisioning',
158
+ failed: 'outcome.environments.state.failed',
159
+ reclaiming: 'outcome.environments.state.reclaiming',
160
+ reclaimed: 'outcome.environments.state.reclaimed',
161
+ expired: 'outcome.environments.state.expired',
162
+ }
163
+ const ENVIRONMENT_STATE_COLOR: Record<OutcomeEnvironmentState, BadgeColor> = {
164
+ live: 'success',
165
+ provisioning: 'info',
166
+ failed: 'error',
167
+ reclaiming: 'neutral',
168
+ reclaimed: 'neutral',
169
+ expired: 'neutral',
170
+ }
171
+ /**
172
+ * Where the row came from, said out loud. `projected` is the one that changes what a reader
173
+ * should conclude (nothing has settled yet, so this row can still move), and the three are
174
+ * mapped exhaustively so a new producer cannot ship as a blank line.
175
+ */
176
+ const ENVIRONMENT_ORIGIN_KEYS: Record<OutcomeEnvironmentOrigin, string> = {
177
+ deployer: 'outcome.environments.origin.deployer',
178
+ human_test: 'outcome.environments.origin.human_test',
179
+ projected: 'outcome.environments.origin.projected',
180
+ }
139
181
  const VISUALS_GAP_KEYS: Record<VisualsGap, string> = {
140
182
  run_unavailable: RUN_UNAVAILABLE_KEY,
141
183
  no_visual_step: 'outcome.visuals.gap.no_visual_step',
@@ -337,6 +379,28 @@ const sourceRows = computed(() => {
337
379
  }))
338
380
  })
339
381
 
382
+ /**
383
+ * The environments the run stood up, with everything the row needs resolved once.
384
+ *
385
+ * The TTL is applied HERE rather than in the reduction, and that division is deliberate: the
386
+ * payload is clock-free so the endpoint's answer and this card's live composition cannot
387
+ * disagree about one run, and this surface is the one with a clock to say what the instant it
388
+ * carries means now. The rule itself lives in `OutcomeSummaryWindow.logic.ts`, where it is
389
+ * asserted without mounting the card.
390
+ *
391
+ * The frame is named by its BLOCK title where the board has it. A frame id says nothing to the
392
+ * person this card is for, so an unresolvable one renders as no label rather than as an id.
393
+ */
394
+ const environmentRows = computed(() => {
395
+ const environments = outcome.value?.environments
396
+ if (!environments || environments.status !== 'reported') return []
397
+ return environments.entries.map((entry, index) => ({
398
+ ...readEnvironmentAgainstClock(entry, nowTick.value),
399
+ key: `${index}:${entry.environmentId ?? entry.url ?? entry.frameId ?? 'env'}`,
400
+ service: entry.frameId ? (board.getBlock(entry.frameId)?.title ?? null) : null,
401
+ }))
402
+ })
403
+
340
404
  /** Drill into the full test report (this card is the summary, never a replacement for it). */
341
405
  function openTestReport() {
342
406
  if (instance.value) ui.openTestEvidence(instance.value.id)
@@ -681,6 +745,78 @@ function openTestReport() {
681
745
  </template>
682
746
  </section>
683
747
 
748
+ <!-- Where to go and look: the running preview, which is the verification a person who does
749
+ not read diffs starts from. Beside the captured views on purpose: the shots are what
750
+ this run saw, this is the thing itself. -->
751
+ <section class="mb-5" data-testid="outcome-environments">
752
+ <h3 class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
753
+ {{ t('outcome.environments.title') }}
754
+ </h3>
755
+ <template v-if="outcome.environments.status === 'reported'">
756
+ <div
757
+ v-for="row in environmentRows"
758
+ :key="row.key"
759
+ class="mb-2 rounded-md border border-slate-800 bg-slate-950/40 px-2.5 py-2 last:mb-0"
760
+ data-testid="outcome-environment"
761
+ :data-state="row.state"
762
+ >
763
+ <div class="flex flex-wrap items-center gap-2">
764
+ <UBadge :color="ENVIRONMENT_STATE_COLOR[row.state]" variant="subtle" size="sm">
765
+ {{ t(ENVIRONMENT_STATE_KEYS[row.state]) }}
766
+ </UBadge>
767
+ <span v-if="row.service" class="truncate text-[12px] text-slate-300">
768
+ {{ row.service }}
769
+ </span>
770
+ <span class="text-[11px] text-slate-500">
771
+ {{ t(ENVIRONMENT_ORIGIN_KEYS[row.origin]) }}
772
+ </span>
773
+ </div>
774
+ <UButton
775
+ v-if="row.openable"
776
+ :to="row.url ?? undefined"
777
+ target="_blank"
778
+ rel="noopener"
779
+ external
780
+ color="primary"
781
+ variant="soft"
782
+ size="xs"
783
+ class="mt-1.5"
784
+ icon="i-lucide-external-link"
785
+ data-testid="outcome-environment-open"
786
+ >
787
+ {{ t('outcome.environments.open') }}
788
+ </UButton>
789
+ <p
790
+ v-else-if="row.url"
791
+ class="mt-1.5 break-all text-[12px] text-slate-500"
792
+ data-testid="outcome-environment-url"
793
+ >
794
+ {{ row.url }}
795
+ </p>
796
+ <p v-if="row.retained" class="mt-1 text-[11px] text-slate-400">
797
+ {{ t('outcome.environments.retained') }}
798
+ </p>
799
+ <p v-if="row.expiresAt" class="mt-1 text-[11px] text-slate-500">
800
+ {{
801
+ row.lapsed
802
+ ? t('outcome.environments.expired', { date: d(new Date(row.expiresAt), 'long') })
803
+ : t('outcome.environments.expires', { date: d(new Date(row.expiresAt), 'long') })
804
+ }}
805
+ </p>
806
+ <p
807
+ v-if="row.detail"
808
+ class="mt-1 break-words text-[12px] leading-relaxed text-slate-500"
809
+ data-testid="outcome-environment-detail"
810
+ >
811
+ {{ row.detail }}
812
+ </p>
813
+ </div>
814
+ </template>
815
+ <p v-else class="text-[13px] italic leading-relaxed text-slate-500">
816
+ {{ t(ENVIRONMENTS_GAP_KEYS[outcome.environments.gap]) }}
817
+ </p>
818
+ </section>
819
+
684
820
  <!-- The machine checks, listed only where one actually recorded a verdict. -->
685
821
  <section v-if="checkRows.length" data-testid="outcome-checks">
686
822
  <h3 class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
@@ -24,6 +24,10 @@ export type {
24
24
  OutcomeCheckState,
25
25
  OutcomeConcern,
26
26
  OutcomeDisposition,
27
+ OutcomeEnvironment,
28
+ OutcomeEnvironments,
29
+ OutcomeEnvironmentOrigin,
30
+ OutcomeEnvironmentState,
27
31
  OutcomePullRequest,
28
32
  OutcomeRequirement,
29
33
  OutcomeRequirements,
@@ -33,6 +37,7 @@ export type {
33
37
  OutcomeTests,
34
38
  OutcomeVisual,
35
39
  OutcomeVisuals,
40
+ EnvironmentsGap,
36
41
  RequirementsGap,
37
42
  RunOutcome,
38
43
  RunUnavailableGap,
@@ -6414,6 +6414,31 @@
6414
6414
  "none_captured": "Die Oberfläche sollte aufgenommen werden, es wurde aber keine Ansicht erfasst."
6415
6415
  }
6416
6416
  },
6417
+ "environments": {
6418
+ "title": "Live-Umgebung",
6419
+ "open": "Live-Umgebung öffnen",
6420
+ "retained": "Diese Umgebung soll den Lauf überdauern.",
6421
+ "expires": "Läuft am {date} ab",
6422
+ "expired": "Abgelaufen am {date}",
6423
+ "state": {
6424
+ "live": "Live",
6425
+ "provisioning": "Wird bereitgestellt",
6426
+ "failed": "Kam nie hoch",
6427
+ "reclaiming": "Wird abgebaut",
6428
+ "reclaimed": "Abgebaut",
6429
+ "expired": "Abgelaufen"
6430
+ },
6431
+ "origin": {
6432
+ "deployer": "Von diesem Lauf bereitgestellt",
6433
+ "human_test": "Für den manuellen Test bereitgestellt",
6434
+ "projected": "Der Lauf arbeitet noch, das kann sich also noch ändern"
6435
+ },
6436
+ "gap": {
6437
+ "no_environment_step": "In dieser Pipeline stellt nichts eine Umgebung bereit, es gibt also nichts zu öffnen.",
6438
+ "not_provisioned": "Eine Umgebung sollte bereitgestellt werden, bisher wurde aber keine erfasst.",
6439
+ "infraless": "Dieser Service deklariert keine eigene Umgebung, es wurde also nichts bereitgestellt."
6440
+ }
6441
+ },
6417
6442
  "checks": {
6418
6443
  "title": "Prüfungen",
6419
6444
  "row": "{kind}: {state}",
@@ -6129,6 +6129,31 @@
6129
6129
  "none_captured": "The interface was meant to be captured, but no view was."
6130
6130
  }
6131
6131
  },
6132
+ "environments": {
6133
+ "title": "Live environment",
6134
+ "open": "Open the live environment",
6135
+ "retained": "This environment is meant to outlive the run.",
6136
+ "expires": "Expires {date}",
6137
+ "expired": "Expired {date}",
6138
+ "state": {
6139
+ "live": "Live",
6140
+ "provisioning": "Coming up",
6141
+ "failed": "Never came up",
6142
+ "reclaiming": "Being torn down",
6143
+ "reclaimed": "Torn down",
6144
+ "expired": "Expired"
6145
+ },
6146
+ "origin": {
6147
+ "deployer": "Stood up by this run",
6148
+ "human_test": "Stood up for the hands-on test",
6149
+ "projected": "The run is still working, so this can still change"
6150
+ },
6151
+ "gap": {
6152
+ "no_environment_step": "Nothing in this pipeline stands an environment up, so there is nothing to open.",
6153
+ "not_provisioned": "An environment was meant to be stood up, and none has been recorded yet.",
6154
+ "infraless": "This service declares no environment of its own, so nothing was stood up."
6155
+ }
6156
+ },
6132
6157
  "checks": {
6133
6158
  "title": "Checks",
6134
6159
  "row": "{kind}: {state}",
@@ -5851,6 +5851,31 @@
5851
5851
  "none_captured": "La interfaz debía capturarse, pero no se capturó ninguna vista."
5852
5852
  }
5853
5853
  },
5854
+ "environments": {
5855
+ "title": "Entorno en vivo",
5856
+ "open": "Abrir el entorno en vivo",
5857
+ "retained": "Este entorno está pensado para seguir existiendo después de la ejecución.",
5858
+ "expires": "Caduca el {date}",
5859
+ "expired": "Caducó el {date}",
5860
+ "state": {
5861
+ "live": "En vivo",
5862
+ "provisioning": "Levantándose",
5863
+ "failed": "Nunca llegó a levantarse",
5864
+ "reclaiming": "Desmontándose",
5865
+ "reclaimed": "Desmontado",
5866
+ "expired": "Caducado"
5867
+ },
5868
+ "origin": {
5869
+ "deployer": "Levantado por esta ejecución",
5870
+ "human_test": "Levantado para la prueba manual",
5871
+ "projected": "La ejecución sigue en curso, así que esto aún puede cambiar"
5872
+ },
5873
+ "gap": {
5874
+ "no_environment_step": "Nada en esta canalización levanta un entorno, así que no hay nada que abrir.",
5875
+ "not_provisioned": "Debía levantarse un entorno y todavía no se ha registrado ninguno.",
5876
+ "infraless": "Este servicio no declara ningún entorno propio, así que no se levantó nada."
5877
+ }
5878
+ },
5854
5879
  "checks": {
5855
5880
  "title": "Comprobaciones",
5856
5881
  "row": "{kind}: {state}",
@@ -5851,6 +5851,31 @@
5851
5851
  "none_captured": "L'interface devait être capturée, mais aucune vue ne l'a été."
5852
5852
  }
5853
5853
  },
5854
+ "environments": {
5855
+ "title": "Environnement en ligne",
5856
+ "open": "Ouvrir l'environnement en ligne",
5857
+ "retained": "Cet environnement est censé survivre à l’exécution.",
5858
+ "expires": "Expire le {date}",
5859
+ "expired": "Expiré le {date}",
5860
+ "state": {
5861
+ "live": "En ligne",
5862
+ "provisioning": "En cours de création",
5863
+ "failed": "Jamais démarré",
5864
+ "reclaiming": "En cours de suppression",
5865
+ "reclaimed": "Supprimé",
5866
+ "expired": "Expiré"
5867
+ },
5868
+ "origin": {
5869
+ "deployer": "Créé par cette exécution",
5870
+ "human_test": "Créé pour le test manuel",
5871
+ "projected": "L'exécution est toujours en cours, cela peut encore changer"
5872
+ },
5873
+ "gap": {
5874
+ "no_environment_step": "Rien dans ce pipeline ne crée d'environnement, il n'y a donc rien à ouvrir.",
5875
+ "not_provisioned": "Un environnement devait être créé et aucun n'a encore été enregistré.",
5876
+ "infraless": "Ce service ne déclare aucun environnement propre, rien n'a donc été créé."
5877
+ }
5878
+ },
5854
5879
  "checks": {
5855
5880
  "title": "Contrôles",
5856
5881
  "row": "{kind} : {state}",
@@ -5851,6 +5851,31 @@
5851
5851
  "none_captured": "הממשק היה אמור להיות מצולם, אך שום תצוגה לא נלכדה."
5852
5852
  }
5853
5853
  },
5854
+ "environments": {
5855
+ "title": "סביבה פעילה",
5856
+ "open": "פתיחת הסביבה הפעילה",
5857
+ "retained": "סביבה זו נועדה להישאר קיימת גם לאחר סיום ההרצה.",
5858
+ "expires": "פג תוקף ב־{date}",
5859
+ "expired": "פג תוקף ב-{date}",
5860
+ "state": {
5861
+ "live": "פעילה",
5862
+ "provisioning": "עולה",
5863
+ "failed": "מעולם לא עלתה",
5864
+ "reclaiming": "בפירוק",
5865
+ "reclaimed": "פורקה",
5866
+ "expired": "פג תוקף"
5867
+ },
5868
+ "origin": {
5869
+ "deployer": "הוקמה על ידי ההרצה הזו",
5870
+ "human_test": "הוקמה לצורך בדיקה ידנית",
5871
+ "projected": "ההרצה עדיין נמשכת, ולכן זה עשוי להשתנות"
5872
+ },
5873
+ "gap": {
5874
+ "no_environment_step": "שום שלב בצינור הזה אינו מקים סביבה, ולכן אין מה לפתוח.",
5875
+ "not_provisioned": "הייתה אמורה לקום סביבה, ועדיין לא נרשמה אף אחת.",
5876
+ "infraless": "השירות הזה אינו מגדיר סביבה משלו, ולכן לא הוקם דבר."
5877
+ }
5878
+ },
5854
5879
  "checks": {
5855
5880
  "title": "בדיקות",
5856
5881
  "row": "{kind}: {state}",
@@ -6414,6 +6414,31 @@
6414
6414
  "none_captured": "L'interfaccia doveva essere catturata, ma non è stata catturata alcuna vista."
6415
6415
  }
6416
6416
  },
6417
+ "environments": {
6418
+ "title": "Ambiente attivo",
6419
+ "open": "Apri l'ambiente attivo",
6420
+ "retained": "Questo ambiente è pensato per sopravvivere all’esecuzione.",
6421
+ "expires": "Scade il {date}",
6422
+ "expired": "Scaduto il {date}",
6423
+ "state": {
6424
+ "live": "Attivo",
6425
+ "provisioning": "In avvio",
6426
+ "failed": "Mai avviato",
6427
+ "reclaiming": "In dismissione",
6428
+ "reclaimed": "Dismesso",
6429
+ "expired": "Scaduto"
6430
+ },
6431
+ "origin": {
6432
+ "deployer": "Avviato da questa esecuzione",
6433
+ "human_test": "Avviato per il test manuale",
6434
+ "projected": "L'esecuzione è ancora in corso, quindi può ancora cambiare"
6435
+ },
6436
+ "gap": {
6437
+ "no_environment_step": "In questa pipeline nulla avvia un ambiente, quindi non c'è niente da aprire.",
6438
+ "not_provisioned": "Doveva essere avviato un ambiente e finora non ne è stato registrato nessuno.",
6439
+ "infraless": "Questo servizio non dichiara un ambiente proprio, quindi non è stato avviato nulla."
6440
+ }
6441
+ },
6417
6442
  "checks": {
6418
6443
  "title": "Controlli",
6419
6444
  "row": "{kind}: {state}",
@@ -5851,6 +5851,31 @@
5851
5851
  "none_captured": "画面を取得する予定でしたが、ビューは取得されませんでした。"
5852
5852
  }
5853
5853
  },
5854
+ "environments": {
5855
+ "title": "稼働中の環境",
5856
+ "open": "稼働中の環境を開く",
5857
+ "retained": "この環境は実行の終了後も残るように設定されています。",
5858
+ "expires": "{date} に期限切れ",
5859
+ "expired": "{date} に期限切れ",
5860
+ "state": {
5861
+ "live": "稼働中",
5862
+ "provisioning": "起動中",
5863
+ "failed": "起動できませんでした",
5864
+ "reclaiming": "破棄中",
5865
+ "reclaimed": "破棄済み",
5866
+ "expired": "期限切れ"
5867
+ },
5868
+ "origin": {
5869
+ "deployer": "この実行が用意した環境",
5870
+ "human_test": "手動テスト用に用意した環境",
5871
+ "projected": "実行がまだ続いているため、この内容は変わる可能性があります"
5872
+ },
5873
+ "gap": {
5874
+ "no_environment_step": "このパイプラインには環境を用意する処理がないため、開けるものはありません。",
5875
+ "not_provisioned": "環境が用意されるはずですが、まだ何も記録されていません。",
5876
+ "infraless": "このサービスは独自の環境を宣言していないため、何も用意されていません。"
5877
+ }
5878
+ },
5854
5879
  "checks": {
5855
5880
  "title": "チェック",
5856
5881
  "row": "{kind}: {state}",
@@ -5851,6 +5851,31 @@
5851
5851
  "none_captured": "Interfejs miał zostać zarejestrowany, ale nie zapisano żadnego widoku."
5852
5852
  }
5853
5853
  },
5854
+ "environments": {
5855
+ "title": "Działające środowisko",
5856
+ "open": "Otwórz działające środowisko",
5857
+ "retained": "To środowisko ma istnieć dłużej niż samo uruchomienie.",
5858
+ "expires": "Wygasa {date}",
5859
+ "expired": "Wygasło {date}",
5860
+ "state": {
5861
+ "live": "Działa",
5862
+ "provisioning": "Uruchamiane",
5863
+ "failed": "Nigdy nie wystartowało",
5864
+ "reclaiming": "Usuwane",
5865
+ "reclaimed": "Usunięte",
5866
+ "expired": "Wygasło"
5867
+ },
5868
+ "origin": {
5869
+ "deployer": "Uruchomione przez ten przebieg",
5870
+ "human_test": "Uruchomione na potrzeby testu ręcznego",
5871
+ "projected": "Przebieg wciąż trwa, więc to może się jeszcze zmienić"
5872
+ },
5873
+ "gap": {
5874
+ "no_environment_step": "Nic w tym potoku nie uruchamia środowiska, więc nie ma czego otworzyć.",
5875
+ "not_provisioned": "Środowisko miało zostać uruchomione, ale żadnego jeszcze nie zapisano.",
5876
+ "infraless": "Ta usługa nie deklaruje własnego środowiska, więc nic nie zostało uruchomione."
5877
+ }
5878
+ },
5854
5879
  "checks": {
5855
5880
  "title": "Sprawdzenia",
5856
5881
  "row": "{kind}: {state}",
@@ -5851,6 +5851,31 @@
5851
5851
  "none_captured": "Arayüzün yakalanması gerekiyordu ama hiçbir görünüm yakalanmadı."
5852
5852
  }
5853
5853
  },
5854
+ "environments": {
5855
+ "title": "Canlı ortam",
5856
+ "open": "Canlı ortamı aç",
5857
+ "retained": "Bu ortamın çalıştırma bittikten sonra da kalması amaçlanıyor.",
5858
+ "expires": "{date} tarihinde sona eriyor",
5859
+ "expired": "{date} tarihinde süresi doldu",
5860
+ "state": {
5861
+ "live": "Canlı",
5862
+ "provisioning": "Ayağa kalkıyor",
5863
+ "failed": "Hiç ayağa kalkmadı",
5864
+ "reclaiming": "Kaldırılıyor",
5865
+ "reclaimed": "Kaldırıldı",
5866
+ "expired": "Süresi doldu"
5867
+ },
5868
+ "origin": {
5869
+ "deployer": "Bu çalışma tarafından ayağa kaldırıldı",
5870
+ "human_test": "Elle yapılan test için ayağa kaldırıldı",
5871
+ "projected": "Çalışma sürüyor, bu yüzden bu durum değişebilir"
5872
+ },
5873
+ "gap": {
5874
+ "no_environment_step": "Bu hatta ortam ayağa kaldıran bir adım yok, açılacak bir şey de yok.",
5875
+ "not_provisioned": "Bir ortam ayağa kaldırılacaktı, ancak henüz hiçbiri kaydedilmedi.",
5876
+ "infraless": "Bu servis kendine ait bir ortam tanımlamıyor, bu yüzden hiçbir şey ayağa kaldırılmadı."
5877
+ }
5878
+ },
5854
5879
  "checks": {
5855
5880
  "title": "Denetimler",
5856
5881
  "row": "{kind}: {state}",
@@ -5851,6 +5851,31 @@
5851
5851
  "none_captured": "Інтерфейс мали зняти, але жодного екрана не збережено."
5852
5852
  }
5853
5853
  },
5854
+ "environments": {
5855
+ "title": "Робоче середовище",
5856
+ "open": "Відкрити робоче середовище",
5857
+ "retained": "Це середовище має існувати й після завершення запуску.",
5858
+ "expires": "Діє до {date}",
5859
+ "expired": "Термін дії минув {date}",
5860
+ "state": {
5861
+ "live": "Працює",
5862
+ "provisioning": "Розгортається",
5863
+ "failed": "Так і не запустилося",
5864
+ "reclaiming": "Згортається",
5865
+ "reclaimed": "Згорнуто",
5866
+ "expired": "Термін вичерпано"
5867
+ },
5868
+ "origin": {
5869
+ "deployer": "Розгорнуто цим запуском",
5870
+ "human_test": "Розгорнуто для ручного тестування",
5871
+ "projected": "Запуск ще триває, тож це може змінитися"
5872
+ },
5873
+ "gap": {
5874
+ "no_environment_step": "У цьому конвеєрі ніщо не розгортає середовище, тож відкривати нічого.",
5875
+ "not_provisioned": "Середовище мало розгорнутися, але поки що не зафіксовано жодного.",
5876
+ "infraless": "Ця служба не оголошує власного середовища, тож нічого не розгорталося."
5877
+ }
5878
+ },
5854
5879
  "checks": {
5855
5880
  "title": "Перевірки",
5856
5881
  "row": "{kind}: {state}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.258.2",
3
+ "version": "0.259.1",
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",
@@ -40,7 +40,7 @@
40
40
  "valibot": "^1.4.2",
41
41
  "vue": "3.5.41",
42
42
  "wretch": "^3.0.9",
43
- "@cat-factory/contracts": "0.287.1"
43
+ "@cat-factory/contracts": "0.289.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",