@cat-factory/app 0.289.0 → 0.290.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.
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { readStatusNote, showsProviderFailure } from './EnvironmentStatusPanel.logic'
3
+ import type { RunEnvironment } from '~/types/execution'
4
+
5
+ /**
6
+ * The panel is where a person watches an environment come up, so it is also where the two prose
7
+ * channels can be shown to contradict each other. The failure this pins is a fault going unshown:
8
+ * a note is only ever context, and rendering it while `lastError` is suppressed reports a healthy
9
+ * spin-up on a row that recorded a real problem.
10
+ */
11
+ const env = (over: Partial<RunEnvironment>): RunEnvironment =>
12
+ ({ id: 'env-1', url: null, status: 'provisioning', ...over }) as RunEnvironment
13
+
14
+ describe('readStatusNote', () => {
15
+ it('shows what a still-provisioning environment is waiting on', () => {
16
+ expect(readStatusNote(env({ statusNote: ' the deploy job is queued ' }))).toBe(
17
+ 'the deploy job is queued',
18
+ )
19
+ })
20
+
21
+ it('withholds the note whenever a fault is recorded, whatever the status', () => {
22
+ // A teardown carries the row's `lastError` forward, so a failed-then-torn-down environment is
23
+ // a real shape with both fields set and a status the error block does not cover. Keyed off
24
+ // that block's own render condition, the panel showed the note and NO fault at all.
25
+ expect(
26
+ readStatusNote(
27
+ env({ status: 'torn_down', lastError: 'quota exceeded', statusNote: 'still deploying' }),
28
+ ),
29
+ ).toBeNull()
30
+ expect(
31
+ readStatusNote(
32
+ env({ status: 'failed', lastError: 'quota exceeded', statusNote: 'still deploying' }),
33
+ ),
34
+ ).toBeNull()
35
+ })
36
+
37
+ it('says nothing beside an environment that reached the state the note explains', () => {
38
+ // "Provider note: the workload is not routed yet" beside a green READY badge is two claims,
39
+ // and the badge is the true one.
40
+ expect(readStatusNote(env({ status: 'ready', statusNote: 'not routed yet' }))).toBeNull()
41
+ expect(readStatusNote(env({ status: 'torn_down', statusNote: 'still deploying' }))).toBeNull()
42
+ expect(
43
+ readStatusNote(env({ status: 'tearing_down', statusNote: 'still deploying' })),
44
+ ).toBeNull()
45
+ })
46
+
47
+ it('keeps the note on a terminal status that recorded no fault', () => {
48
+ // The same disposition kernel's readiness verdict takes: with no error to show, the last
49
+ // thing the provider said is all there is.
50
+ expect(
51
+ readStatusNote(env({ status: 'failed', statusNote: 'the deploy job never started' })),
52
+ ).toBe('the deploy job never started')
53
+ })
54
+
55
+ it('reads a blank note, an absent one and no environment alike', () => {
56
+ expect(readStatusNote(env({ statusNote: ' ' }))).toBeNull()
57
+ expect(readStatusNote(env({}))).toBeNull()
58
+ expect(readStatusNote(null)).toBeNull()
59
+ })
60
+ })
61
+
62
+ describe('showsProviderFailure', () => {
63
+ it('is the fault block, on the statuses that stopped at one', () => {
64
+ expect(showsProviderFailure(env({ status: 'failed', lastError: 'quota' }))).toBe(true)
65
+ expect(showsProviderFailure(env({ status: 'expired', lastError: 'quota' }))).toBe(true)
66
+ expect(showsProviderFailure(env({ status: 'provisioning', lastError: 'quota' }))).toBe(false)
67
+ expect(showsProviderFailure(env({ status: 'failed' }))).toBe(false)
68
+ expect(showsProviderFailure(null)).toBe(false)
69
+ })
70
+ })
@@ -0,0 +1,48 @@
1
+ // Which of an environment's two prose channels the panel shows, extracted from
2
+ // `EnvironmentStatusPanel.vue` so the precedence can be asserted without mounting the panel (see
3
+ // `EnvironmentStatusPanel.logic.spec.ts`).
4
+ //
5
+ // The environment record carries two accounts of itself and they answer different questions.
6
+ // `lastError` is a recorded FAULT: the provider's verbatim cause, written on a status the
7
+ // environment will not leave. `statusNote` is the provider's account of a state it has NOT left
8
+ // yet: why this environment is not ready. A row can carry both, and which one a reader is shown
9
+ // decides which layer they go looking at.
10
+
11
+ import type { RunEnvironment } from '~/types/execution'
12
+ import type { EnvironmentStatus } from '@cat-factory/contracts'
13
+
14
+ /**
15
+ * The statuses whose failure block the panel renders: a fault is shown as the headline account
16
+ * only where the environment actually stopped at one.
17
+ */
18
+ const FAILURE_STATUSES = new Set<EnvironmentStatus>(['failed', 'expired'])
19
+
20
+ /**
21
+ * The statuses a note still says something about. `ready` has REACHED the state the note explains
22
+ * not being in, and the two teardown statuses describe a spin-up nobody is waiting on any more (a
23
+ * teardown carries the row's note forward, so this is a live shape rather than a hypothetical
24
+ * one). On `failed` / `expired` the note is what a provider that recorded no fault last said,
25
+ * which is the disposition kernel's readiness verdict takes for the same pair.
26
+ */
27
+ const NOTE_STATUSES = new Set<EnvironmentStatus>(['provisioning', 'failed', 'expired'])
28
+
29
+ /** Whether the verbatim provider error is the panel's account of this environment. */
30
+ export function showsProviderFailure(env: RunEnvironment | null | undefined): boolean {
31
+ return !!env?.lastError && FAILURE_STATUSES.has(env.status)
32
+ }
33
+
34
+ /**
35
+ * The note to render, or null.
36
+ *
37
+ * Two rules, and it is the FAULT's presence that decides the first rather than whether the error
38
+ * block happens to be on screen. A recorded `lastError` outranks a note wherever they collide,
39
+ * whatever the status: they are two claims about one environment and the fault is the more
40
+ * specific one, so keying this off the error block's own render condition hid a real fault on
41
+ * every status that block does not cover. And a status that has left the state the note describes
42
+ * has nothing left to add with it.
43
+ */
44
+ export function readStatusNote(env: RunEnvironment | null | undefined): string | null {
45
+ if (!env || env.lastError) return null
46
+ if (!NOTE_STATUSES.has(env.status)) return null
47
+ return env.statusNote?.trim() || null
48
+ }
@@ -5,6 +5,10 @@
5
5
  // shows whether the env is spinning up / running / shut down / errored, with the error.
6
6
  import type { InfraEngine, ProvisionType } from '@cat-factory/contracts'
7
7
  import type { RunEnvironment, HumanTestEnvironmentStatus } from '~/types/execution'
8
+ import {
9
+ readStatusNote,
10
+ showsProviderFailure,
11
+ } from '~/components/environments/EnvironmentStatusPanel.logic'
8
12
 
9
13
  const props = defineProps<{
10
14
  environment: RunEnvironment | null
@@ -86,6 +90,12 @@ const ENV_STATUS_META = computed<
86
90
  },
87
91
  }))
88
92
 
93
+ // Which of the environment's two prose channels this panel shows. Both predicates live in
94
+ // `EnvironmentStatusPanel.logic.ts`, where the precedence between a recorded fault and a
95
+ // still-coming-up note is stated once and asserted without mounting the panel.
96
+ const failureShown = computed(() => showsProviderFailure(props.environment))
97
+ const statusNote = computed(() => readStatusNote(props.environment))
98
+
89
99
  // The two statuses that describe a transition IN FLIGHT. Only these ever animate, and only
90
100
  // while the run driving the transition is still being driven itself.
91
101
  const envInTransition = computed(
@@ -140,12 +150,19 @@ const envInTransition = computed(
140
150
  </dl>
141
151
  <!-- The verbatim provider error when the environment failed/expired. -->
142
152
  <pre
143
- v-if="
144
- environment.lastError &&
145
- (environment.status === 'failed' || environment.status === 'expired')
146
- "
153
+ v-if="failureShown"
147
154
  class="mt-1 max-h-32 overflow-auto whitespace-pre-wrap rounded border border-rose-900/60 bg-rose-950/40 p-1.5 text-[11px] text-rose-200/90"
148
155
  >{{ environment.lastError }}</pre>
156
+ <!-- What the provider says it is still waiting on. Muted rather than alarming: an
157
+ environment mid-rollout is healthy, and styling this like the error above would report
158
+ a fault every deploy. Bounded like the error block, because the text is provider
159
+ prose. -->
160
+ <p
161
+ v-if="statusNote"
162
+ class="mt-1 max-h-32 overflow-auto whitespace-pre-wrap break-words text-[11px] text-slate-400"
163
+ >
164
+ {{ t('environments.statusNote', { note: statusNote }) }}
165
+ </p>
149
166
  </div>
150
167
  <p v-else class="text-[12px] text-slate-500">
151
168
  {{ degradedReason ?? t('environments.empty') }}
@@ -20,6 +20,7 @@ const env = (overrides: Partial<OutcomeEnvironment> = {}): OutcomeEnvironment =>
20
20
  frameId: 'frm_own',
21
21
  environmentId: 'env_1',
22
22
  detail: null,
23
+ detailKind: null,
23
24
  ...overrides,
24
25
  })
25
26
 
@@ -803,12 +803,21 @@ function openTestReport() {
803
803
  : t('outcome.environments.expires', { date: d(new Date(row.expiresAt), 'long') })
804
804
  }}
805
805
  </p>
806
+ <!-- One slot, two kinds of claim. A provider's note about a spin-up in progress is
807
+ labelled as one; a recorded fault is the row's own prose. Unlabelled they read
808
+ identically, and "the deploy job is queued behind 3 others" in the slot that
809
+ otherwise holds "quota exceeded" reports a fault the environment does not have. -->
806
810
  <p
807
811
  v-if="row.detail"
808
812
  class="mt-1 break-words text-[12px] leading-relaxed text-slate-500"
809
813
  data-testid="outcome-environment-detail"
814
+ :data-detail-kind="row.detailKind"
810
815
  >
811
- {{ row.detail }}
816
+ {{
817
+ row.detailKind === 'note'
818
+ ? t('environments.statusNote', { note: row.detail })
819
+ : row.detail
820
+ }}
812
821
  </p>
813
822
  </div>
814
823
  </template>
@@ -96,6 +96,7 @@ const OPERATION_LABEL = computed<Record<ProvisioningOperation, string>>(() => ({
96
96
  teardown: t('provisioning.operation.teardown'),
97
97
  'teardown-verify': t('provisioning.operation.teardown-verify'),
98
98
  status: t('provisioning.operation.status'),
99
+ remediate: t('provisioning.operation.remediate'),
99
100
  dispatch: t('provisioning.operation.dispatch'),
100
101
  release: t('provisioning.operation.release'),
101
102
  'poll-failure': t('provisioning.operation.poll-failure'),
@@ -26,6 +26,7 @@ export type {
26
26
  OutcomeDisposition,
27
27
  OutcomeEnvironment,
28
28
  OutcomeEnvironments,
29
+ OutcomeEnvironmentDetailKind,
29
30
  OutcomeEnvironmentOrigin,
30
31
  OutcomeEnvironmentState,
31
32
  OutcomePullRequest,
@@ -6874,6 +6874,7 @@
6874
6874
  "empty": "Keine ephemere Umgebung für diesen Lauf.",
6875
6875
  "provisionTypeLabel": "Bereitstellungstyp:",
6876
6876
  "engineLabel": "Engine:",
6877
+ "statusNote": "Hinweis des Anbieters: {note}",
6877
6878
  "provisionType": {
6878
6879
  "kubernetes": "Kubernetes",
6879
6880
  "docker-compose": "Docker Compose",
@@ -6899,6 +6900,7 @@
6899
6900
  "teardown": "Abbauen",
6900
6901
  "teardown-verify": "Abbau-Prüfung",
6901
6902
  "status": "Statusprüfung",
6903
+ "remediate": "Reparatur vor Ort",
6902
6904
  "dispatch": "Hochfahren",
6903
6905
  "release": "Abbauen",
6904
6906
  "poll-failure": "Health-Check"
@@ -6597,6 +6597,7 @@
6597
6597
  "empty": "No ephemeral environment for this run.",
6598
6598
  "provisionTypeLabel": "Provision type:",
6599
6599
  "engineLabel": "Engine:",
6600
+ "statusNote": "Provider note: {note}",
6600
6601
  "provisionType": {
6601
6602
  "kubernetes": "Kubernetes",
6602
6603
  "docker-compose": "Docker Compose",
@@ -6622,6 +6623,7 @@
6622
6623
  "teardown": "Tear down",
6623
6624
  "teardown-verify": "Teardown check",
6624
6625
  "status": "Status check",
6626
+ "remediate": "Repair in place",
6625
6627
  "dispatch": "Spin up",
6626
6628
  "release": "Tear down",
6627
6629
  "poll-failure": "Health check"
@@ -6293,6 +6293,7 @@
6293
6293
  "empty": "No hay entorno efímero para esta ejecución.",
6294
6294
  "provisionTypeLabel": "Tipo de aprovisionamiento:",
6295
6295
  "engineLabel": "Motor:",
6296
+ "statusNote": "Nota del proveedor: {note}",
6296
6297
  "provisionType": {
6297
6298
  "kubernetes": "Kubernetes",
6298
6299
  "docker-compose": "Docker Compose",
@@ -6318,6 +6319,7 @@
6318
6319
  "teardown": "Desmontar",
6319
6320
  "teardown-verify": "Comprobación de desmontaje",
6320
6321
  "status": "Comprobación de estado",
6322
+ "remediate": "Reparación en sitio",
6321
6323
  "dispatch": "Arrancar",
6322
6324
  "release": "Desmontar",
6323
6325
  "poll-failure": "Comprobación de salud"
@@ -6293,6 +6293,7 @@
6293
6293
  "empty": "Aucun environnement éphémère pour cette exécution.",
6294
6294
  "provisionTypeLabel": "Type de provisionnement :",
6295
6295
  "engineLabel": "Moteur :",
6296
+ "statusNote": "Note du fournisseur : {note}",
6296
6297
  "provisionType": {
6297
6298
  "kubernetes": "Kubernetes",
6298
6299
  "docker-compose": "Docker Compose",
@@ -6318,6 +6319,7 @@
6318
6319
  "teardown": "Démanteler",
6319
6320
  "teardown-verify": "Vérification du démantèlement",
6320
6321
  "status": "Vérification de l'état",
6322
+ "remediate": "Réparation sur place",
6321
6323
  "dispatch": "Démarrer",
6322
6324
  "release": "Démanteler",
6323
6325
  "poll-failure": "Contrôle de santé"
@@ -6293,6 +6293,7 @@
6293
6293
  "empty": "אין סביבה זמנית להרצה הזו.",
6294
6294
  "provisionTypeLabel": "סוג אספקה:",
6295
6295
  "engineLabel": "מנוע:",
6296
+ "statusNote": "הערת הספק: {note}",
6296
6297
  "provisionType": {
6297
6298
  "kubernetes": "Kubernetes",
6298
6299
  "docker-compose": "Docker Compose",
@@ -6318,6 +6319,7 @@
6318
6319
  "teardown": "פירוק",
6319
6320
  "teardown-verify": "בדיקת פירוק",
6320
6321
  "status": "בדיקת מצב",
6322
+ "remediate": "תיקון במקום",
6321
6323
  "dispatch": "הקמה",
6322
6324
  "release": "פירוק",
6323
6325
  "poll-failure": "בדיקת תקינות"
@@ -6874,6 +6874,7 @@
6874
6874
  "empty": "Nessun ambiente effimero per questa esecuzione.",
6875
6875
  "provisionTypeLabel": "Tipo di provisioning:",
6876
6876
  "engineLabel": "Motore:",
6877
+ "statusNote": "Nota del provider: {note}",
6877
6878
  "provisionType": {
6878
6879
  "kubernetes": "Kubernetes",
6879
6880
  "docker-compose": "Docker Compose",
@@ -6899,6 +6900,7 @@
6899
6900
  "teardown": "Smantellamento",
6900
6901
  "teardown-verify": "Verifica dello smantellamento",
6901
6902
  "status": "Controllo dello stato",
6903
+ "remediate": "Riparazione sul posto",
6902
6904
  "dispatch": "Avvio",
6903
6905
  "release": "Smantellamento",
6904
6906
  "poll-failure": "Controllo dello stato di salute"
@@ -6293,6 +6293,7 @@
6293
6293
  "empty": "この実行に一時環境はありません。",
6294
6294
  "provisionTypeLabel": "プロビジョニングタイプ:",
6295
6295
  "engineLabel": "エンジン:",
6296
+ "statusNote": "プロバイダーからの注記: {note}",
6296
6297
  "provisionType": {
6297
6298
  "kubernetes": "Kubernetes",
6298
6299
  "docker-compose": "Docker Compose",
@@ -6318,6 +6319,7 @@
6318
6319
  "teardown": "破棄",
6319
6320
  "teardown-verify": "破棄の確認",
6320
6321
  "status": "ステータスチェック",
6322
+ "remediate": "その場で修復",
6321
6323
  "dispatch": "起動",
6322
6324
  "release": "破棄",
6323
6325
  "poll-failure": "ヘルスチェック"
@@ -6293,6 +6293,7 @@
6293
6293
  "empty": "Brak środowiska efemerycznego dla tego uruchomienia.",
6294
6294
  "provisionTypeLabel": "Typ provisioningu:",
6295
6295
  "engineLabel": "Silnik:",
6296
+ "statusNote": "Uwaga dostawcy: {note}",
6296
6297
  "provisionType": {
6297
6298
  "kubernetes": "Kubernetes",
6298
6299
  "docker-compose": "Docker Compose",
@@ -6318,6 +6319,7 @@
6318
6319
  "teardown": "Zatrzymaj",
6319
6320
  "teardown-verify": "Sprawdzenie zatrzymania",
6320
6321
  "status": "Sprawdzenie stanu",
6322
+ "remediate": "Naprawa na miejscu",
6321
6323
  "dispatch": "Uruchom",
6322
6324
  "release": "Zatrzymaj",
6323
6325
  "poll-failure": "Kontrola kondycji"
@@ -6293,6 +6293,7 @@
6293
6293
  "empty": "Bu çalışma için geçici ortam yok.",
6294
6294
  "provisionTypeLabel": "Sağlama türü:",
6295
6295
  "engineLabel": "Motor:",
6296
+ "statusNote": "Sağlayıcı notu: {note}",
6296
6297
  "provisionType": {
6297
6298
  "kubernetes": "Kubernetes",
6298
6299
  "docker-compose": "Docker Compose",
@@ -6318,6 +6319,7 @@
6318
6319
  "teardown": "Kapat",
6319
6320
  "teardown-verify": "Kapatma kontrolü",
6320
6321
  "status": "Durum kontrolü",
6322
+ "remediate": "Yerinde onarım",
6321
6323
  "dispatch": "Başlat",
6322
6324
  "release": "Kapat",
6323
6325
  "poll-failure": "Sağlık kontrolü"
@@ -6293,6 +6293,7 @@
6293
6293
  "empty": "Для цього запуску немає ефемерного середовища.",
6294
6294
  "provisionTypeLabel": "Тип провіженінгу:",
6295
6295
  "engineLabel": "Рушій:",
6296
+ "statusNote": "Примітка провайдера: {note}",
6296
6297
  "provisionType": {
6297
6298
  "kubernetes": "Kubernetes",
6298
6299
  "docker-compose": "Docker Compose",
@@ -6318,6 +6319,7 @@
6318
6319
  "teardown": "Згорнути",
6319
6320
  "teardown-verify": "Перевірка згортання",
6320
6321
  "status": "Перевірка стану",
6322
+ "remediate": "Ремонт на місці",
6321
6323
  "dispatch": "Запустити",
6322
6324
  "release": "Згорнути",
6323
6325
  "poll-failure": "Перевірка справності"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.289.0",
3
+ "version": "0.290.0",
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",
@@ -18,7 +18,7 @@
18
18
  "access": "public"
19
19
  },
20
20
  "dependencies": {
21
- "@cat-factory/contracts": "0.335.0",
21
+ "@cat-factory/contracts": "0.336.0",
22
22
  "@modular-frontend/core": "0.6.0",
23
23
  "@modular-vue/core": "^1.5.0",
24
24
  "@modular-vue/journeys": "^1.4.0",