@cat-factory/app 0.310.3 → 0.311.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.
package/README.md CHANGED
@@ -611,6 +611,14 @@ The seams, and what a new feature should use rather than reading the store ad ho
611
611
  - **A surface that narrows inline** reads `useUiRoleStore().fullSurface` (the frame header's bug-hunt
612
612
  button, the palette's per-connection integration commands). Same rule as the tier: what remains
613
613
  must be exactly what the full surface would have shown, only less of it.
614
+ - **A setup prompt** (a "configure this" card over the board) goes through `canActOnSetup`
615
+ (`app/utils/setupAudience.ts`) with the caller from `useSetupActor()`: it combines the role's
616
+ surface with the board and account grants and the owner the server reports for the gap
617
+ (`infraSetupOwners`). A caller who cannot act gets no card. If the gap stops runs they could start,
618
+ it reaches them as one line saying how it gets fixed (`blockingSetupNotice`): the owner to ask, or
619
+ the role switch when they hold the grant and only the designer surface hides the card. A viewer
620
+ gets nothing, and the line honours the card's dismissals. Never a button that opens a screen the
621
+ caller's save is refused on.
614
622
  - **A tutorial tour whose step clicks a non-`intake` nav entry declares
615
623
  `TUTORIAL_REQUIREMENTS.fullSurface`.** Which tours those are is not a judgement call:
616
624
  `tutorial-tours.spec.ts` derives the pairing from `navItemVisible`, so a tour that gains such a
@@ -3,8 +3,11 @@
3
3
  // has no connection yet. cat-factory's whole flow runs on a connected repository host (agents
4
4
  // open pull/merge requests on the user's repos), so the board is withheld until the workspace
5
5
  // connects one. <VcsConnectSurfaces> renders whichever connect methods the deployment serves; a
6
- // "Sign out" escape hatch avoids trapping a user who needs to switch accounts.
6
+ // "Sign out" escape hatch avoids trapping a user who needs to switch accounts. The board switcher is
7
+ // the other escape: the gate replaces the whole board, sidebar included, so without it a member
8
+ // who landed on a board they cannot connect had no way to reach a board they can.
7
9
  import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
10
+ import BoardSwitcher from '~/components/layout/BoardSwitcher.vue'
8
11
  import { VCS_PROVIDER_ICONS, VCS_PROVIDER_LABELS } from '~/utils/vcs'
9
12
 
10
13
  const { t } = useI18n()
@@ -39,6 +42,11 @@ const title = computed(() =>
39
42
 
40
43
  <VcsConnectSurfaces :app-intro="t('github.onboarding.appIntro')" />
41
44
 
45
+ <div class="mt-6 border-t border-default pt-4" data-testid="vcs-onboarding-switch-board">
46
+ <p class="mb-2 text-xs text-muted">{{ t('vcs.onboarding.switchBoard') }}</p>
47
+ <BoardSwitcher standalone />
48
+ </div>
49
+
42
50
  <p
43
51
  v-if="auth.required && auth.user"
44
52
  class="mt-6 border-t border-default pt-4 text-center text-xs text-dimmed"
@@ -53,7 +53,7 @@ function invitationStatusLabel(status: InvitationStatus): string {
53
53
  }
54
54
 
55
55
  /** Whether the signed-in caller is an admin of this account (drives edit affordances). */
56
- const isAdmin = computed(() => accounts.activeAccount?.roles?.includes('admin') ?? false)
56
+ const isAdmin = computed(() => accounts.isActiveAccountAdmin)
57
57
  /**
58
58
  * Members / roles / invitations are org-scoped — the backend rejects membership on a
59
59
  * personal account. For a personal account we show a "create an organization" CTA in
@@ -12,7 +12,16 @@ import SectionLabel from '~/components/common/SectionLabel.vue'
12
12
  // account row folds away — it is a label with a menu duplicated inside the board menu's
13
13
  // reach — and the board button keeps only its glyph. Both dropdowns are unchanged, so
14
14
  // switching boards never requires expanding the sidebar first.
15
- withDefaults(defineProps<{ collapsed?: boolean }>(), { collapsed: false })
15
+ //
16
+ // `standalone` is for a host OUTSIDE the board page (the repository-host gate), where the
17
+ // page-level dialogs are not mounted. The items that open one are left out there: "Account
18
+ // settings" opens `AccountSettingsPanel` and "Delete board" awaits `ConfirmDialog`, so on the gate
19
+ // both would do nothing and then fire once a board page mounted, the delete against the board that
20
+ // was active when it was asked.
21
+ const props = withDefaults(defineProps<{ collapsed?: boolean; standalone?: boolean }>(), {
22
+ collapsed: false,
23
+ standalone: false,
24
+ })
16
25
 
17
26
  const { t } = useI18n()
18
27
 
@@ -65,14 +74,19 @@ const accountItems = computed<DropdownMenuItem[][]>(() => [
65
74
  },
66
75
  // Account settings: the unified per-account panel (team + roles + invitations + email
67
76
  // sender + account-tier fragment library). The panel itself handles personal accounts
68
- // (prompting to create an org for the team tab), so this is not gated.
69
- {
70
- label: t('layout.boardSwitcher.account.settings'),
71
- icon: 'i-lucide-settings',
72
- onSelect: () => ui.openAccountSettings(),
73
- },
77
+ // (prompting to create an org for the team tab), so this is not gated by role; it is left out
78
+ // only where the panel is not mounted (`standalone`).
79
+ ...(props.standalone
80
+ ? []
81
+ : [
82
+ {
83
+ label: t('layout.boardSwitcher.account.settings'),
84
+ icon: 'i-lucide-settings',
85
+ onSelect: () => ui.openAccountSettings(),
86
+ },
87
+ ]),
74
88
  // Admins can set the account-wide default provider new services inherit.
75
- ...(accounts.activeAccount?.roles?.includes('admin')
89
+ ...(accounts.isActiveAccountAdmin
76
90
  ? [
77
91
  {
78
92
  label: t('layout.boardSwitcher.account.defaultProvider'),
@@ -122,12 +136,17 @@ const boardItems = computed<DropdownMenuItem[][]>(() => [
122
136
  icon: 'i-lucide-pencil',
123
137
  onSelect: () => openPrompt('rename'),
124
138
  },
125
- {
126
- label: t('layout.boardSwitcher.board.delete'),
127
- icon: 'i-lucide-trash-2',
128
- color: 'error' as const,
129
- onSelect: () => void removeBoard(),
130
- },
139
+ // Delete confirms through the page's `ConfirmDialog`, absent in `standalone`.
140
+ ...(props.standalone
141
+ ? []
142
+ : [
143
+ {
144
+ label: t('layout.boardSwitcher.board.delete'),
145
+ icon: 'i-lucide-trash-2',
146
+ color: 'error' as const,
147
+ onSelect: () => void removeBoard(),
148
+ },
149
+ ]),
131
150
  ]
132
151
  : []),
133
152
  ],
@@ -22,14 +22,17 @@ const ui = useUiStore()
22
22
  const workspace = useWorkspaceStore()
23
23
  const settingsStore = useWorkspaceSettingsStore()
24
24
  const { canManageSettings } = useWorkspaceAccess()
25
+ const uiRole = useUiRoleStore()
25
26
 
26
27
  // Only prompt someone who can actually answer. A member/viewer cannot write workspace settings
27
28
  // (the endpoint is `settings.manage`-gated), so for them this would be an un-actionable nag
28
- // pointing at a screen that would refuse their save.
29
+ // pointing at a screen that would refuse their save. The designer surface never configures the
30
+ // platform, so it is kept off there too, whatever the grant.
29
31
  const show = computed(
30
32
  () =>
31
33
  workspace.ready &&
32
34
  canManageSettings.value &&
35
+ uiRole.fullSurface &&
33
36
  needsDefaultProvisioningChoice(settingsStore.settings) &&
34
37
  !ui.defaultProvisionDismissed,
35
38
  )
@@ -6,6 +6,11 @@
6
6
  // runtime whose account picked no backend — incl. Cloudflare without an ARTIFACT_BUCKET binding;
7
7
  // ephemeral test environments on any runtime that wires the integration).
8
8
  //
9
+ // Audience: a card renders only for a caller who can act on its area, per the server-reported
10
+ // owner (`infraSetupOwners`) and `canActOnSetup`. A caller who cannot (a member, a viewer, the
11
+ // designer surface, or anyone when the operator owns the gap) sees one compact line instead, and
12
+ // only for a gap that stops every run (`InfraSetupNotice`).
13
+ //
9
14
  // Positioning/stacking against the sibling advisory banners (AI-readiness, provider-config) is
10
15
  // owned by the shared, click-through banner column in `pages/index.vue` — so concurrent prompts
11
16
  // stack vertically instead of drawing on top of each other. This component only stacks its OWN
@@ -52,11 +57,14 @@ import {
52
57
  import type { DropdownMenuItem } from '@nuxt/ui'
53
58
  import type { InfraSetupArea, InfraSetupStatus } from '~/types/domain'
54
59
  import { infraSetupDismissalKey, type InfraSetupCardKind } from '~/utils/infraSetup'
60
+ import { blockingSetupNotice, canActOnSetup } from '~/utils/setupAudience'
61
+ import InfraSetupNotice from '~/components/layout/InfraSetupNotice.vue'
55
62
 
56
63
  const { t } = useI18n()
57
64
  const ui = useUiStore()
58
65
  const auth = useAuthStore()
59
66
  const workspace = useWorkspaceStore()
67
+ const { actor } = useSetupActor()
60
68
 
61
69
  // Severity order: no executor blocks EVERY agent, so it leads; a missing test environment blocks
62
70
  // only testing agents; missing storage only degrades the UI-tester's screenshots.
@@ -140,6 +148,9 @@ const visible = computed<AreaCard[]>(() => {
140
148
  return AREAS.filter((area) => {
141
149
  const kind = cardKind(status[area])
142
150
  if (!kind) return false
151
+ // A card is for the people who can act on it. Everyone else hears about a gap only when it
152
+ // stops their own work, through the one line below (`blockingSetupNotice`).
153
+ if (!canActOnSetup(workspace.infraSetupOwners?.[area], actor.value)) return false
143
154
  // Both dismissals are keyed by the CLAIM, not by the area: silencing "you haven't configured
144
155
  // this" must not also silence "you configured it and it is now down". The permanent dismissal
145
156
  // only ever covers a setup gap (`dismissPermanently` is offered nowhere else).
@@ -152,6 +163,18 @@ const visible = computed<AreaCard[]>(() => {
152
163
  }))
153
164
  })
154
165
 
166
+ // The one line for a caller who cannot act on a gap that stops every run. It honours BOTH
167
+ // dismissals of the card it stands in for, keyed by the same claim: someone who said "don't
168
+ // notify me again" on the card (before their role changed, say) is not nagged again by the line.
169
+ const notice = computed(() => {
170
+ const found = blockingSetupNotice(workspace.infraSetup, workspace.infraSetupOwners, actor.value)
171
+ if (!found) return null
172
+ if (ui.infraSetupSessionDismissed.includes(infraSetupDismissalKey(found.area, found.kind))) {
173
+ return null
174
+ }
175
+ return found.kind === 'setup' && dismissedForUser.value.includes(found.area) ? null : found
176
+ })
177
+
155
178
  /** Which card an area's status raises, or null when it raises none (`configured`/`not_applicable`). */
156
179
  function cardKind(status: InfraSetupStatus): InfraSetupCardKind | null {
157
180
  if (isInfraSetupHealthStatus(status)) return 'outage'
@@ -169,7 +192,7 @@ function titleKey(card: AreaCard): string {
169
192
  * The dismiss dropdown: the product wants the user asked WHICH kind of dismissal on close. An
170
193
  * outage offers the session option ONLY — see the fork note at the top of this file.
171
194
  */
172
- function dismissMenu(card: AreaCard): DropdownMenuItem[][] {
195
+ function dismissMenu(card: Pick<AreaCard, 'area' | 'kind'>): DropdownMenuItem[][] {
173
196
  const session = {
174
197
  label: t('layout.infraSetupBanner.dismiss.session'),
175
198
  icon: 'i-lucide-clock',
@@ -195,11 +218,12 @@ function dismissMenu(card: AreaCard): DropdownMenuItem[][] {
195
218
  than an assertive `role="alert"` per card — an advisory setup nag shouldn't interrupt a
196
219
  screen reader, and up to three stacked alerts would spam it. -->
197
220
  <div
198
- v-if="visible.length > 0"
221
+ v-if="visible.length > 0 || notice"
199
222
  class="flex w-full flex-col items-center gap-2"
200
223
  role="status"
201
224
  aria-live="polite"
202
225
  >
226
+ <InfraSetupNotice v-if="notice" :notice="notice" :dismiss-menu="dismissMenu(notice)" />
203
227
  <!-- An OUTAGE reads red, a setup gap amber: one is something breaking now, the other is
204
228
  something never switched on, and a reader has to be able to tell at a glance. -->
205
229
  <div
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ // The one line a caller sees for a setup gap they cannot fix from where they are, when it stops
3
+ // their runs. It replaces the full card (and its "Configure" button that would open a screen their
4
+ // save is refused on) with two facts: what does not work, and how it gets fixed. Compact on
5
+ // purpose: the caller has nothing to do here, so it must not cover the board the way an actionable
6
+ // card may.
7
+ import type { DropdownMenuItem } from '@nuxt/ui'
8
+ import type { BlockingInfraArea, SetupNotice, SetupRemedy } from '~/utils/setupAudience'
9
+
10
+ defineProps<{
11
+ notice: SetupNotice
12
+ /** The card's own session / permanent choice, built by the banner so both close the same claim. */
13
+ dismissMenu: DropdownMenuItem[][]
14
+ }>()
15
+
16
+ const { t } = useI18n()
17
+
18
+ // Exhaustive over the blocking areas and the remedies, so a new member of either fails typecheck
19
+ // here rather than rendering a raw key.
20
+ const STATE_KEYS: Record<BlockingInfraArea, Record<SetupNotice['kind'], string>> = {
21
+ agentExecutor: {
22
+ setup: 'layout.infraSetupNotice.agentExecutor.setup',
23
+ outage: 'layout.infraSetupNotice.agentExecutor.outage',
24
+ },
25
+ }
26
+ const REMEDY_KEYS: Record<SetupRemedy, string> = {
27
+ workspace_admin: 'layout.infraSetupNotice.owner.workspace_admin',
28
+ account_admin: 'layout.infraSetupNotice.owner.account_admin',
29
+ operator: 'layout.infraSetupNotice.owner.operator',
30
+ full_surface: 'layout.infraSetupNotice.fullSurface',
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <div
36
+ class="pointer-events-auto flex w-full max-w-3xl items-center gap-3 rounded-xl border px-4 py-2 shadow-lg backdrop-blur"
37
+ :class="
38
+ notice.kind === 'outage'
39
+ ? 'border-app-error-500/50 bg-app-error-950/90'
40
+ : 'border-app-warning-500/50 bg-app-warning-950/90'
41
+ "
42
+ :data-testid="`infra-setup-notice-${notice.area}`"
43
+ :data-infra-remedy="notice.remedy"
44
+ >
45
+ <UIcon
46
+ :name="notice.kind === 'outage' ? 'i-lucide-plug-zap' : 'i-lucide-server-off'"
47
+ class="h-5 w-5 shrink-0"
48
+ :class="notice.kind === 'outage' ? 'text-app-error-400' : 'text-app-warning-400'"
49
+ />
50
+ <p
51
+ class="min-w-0 flex-1 text-sm"
52
+ :class="notice.kind === 'outage' ? 'text-app-error-100' : 'text-app-warning-100'"
53
+ >
54
+ {{ t(STATE_KEYS[notice.area][notice.kind]) }}
55
+ <span class="opacity-80">{{ t(REMEDY_KEYS[notice.remedy]) }}</span>
56
+ </p>
57
+ <UDropdownMenu :items="dismissMenu" :content="{ align: 'end' }">
58
+ <UButton
59
+ color="neutral"
60
+ variant="ghost"
61
+ size="xs"
62
+ icon="i-lucide-x"
63
+ class="shrink-0"
64
+ :aria-label="t('common.close')"
65
+ :data-testid="`infra-setup-notice-dismiss-${notice.area}`"
66
+ />
67
+ </UDropdownMenu>
68
+ </div>
69
+ </template>
@@ -6,11 +6,13 @@
6
6
  // link into its connect panel. Dismissible per session.
7
7
  import { computed, ref, watch } from 'vue'
8
8
  import type { ProviderConnectionKind } from '~/types/providerConnections'
9
+ import { canActOnSetup } from '~/utils/setupAudience'
9
10
 
10
11
  const { t } = useI18n()
11
12
  const ui = useUiStore()
12
13
  const workspace = useWorkspaceStore()
13
14
  const store = useProviderConnectionsStore()
15
+ const { actor } = useSetupActor()
14
16
 
15
17
  // Exhaustive per-kind title/action keys (leaf names mirror the kind verbatim).
16
18
  const TITLE_KEYS: Record<ProviderConnectionKind, string> = {
@@ -33,7 +35,12 @@ watch(
33
35
 
34
36
  const dismissed = ref(false)
35
37
  const pending = computed(() => store.needingConfig as ProviderConnectionKind[])
36
- const show = computed(() => pending.value.length > 0 && !dismissed.value)
38
+ // Only for the people whose save would land: the connection fields are a workspace-admin write,
39
+ // and a member or a designer could only open the panel and be refused.
40
+ const show = computed(
41
+ () =>
42
+ pending.value.length > 0 && !dismissed.value && canActOnSetup('workspace_admin', actor.value),
43
+ )
37
44
  </script>
38
45
 
39
46
  <template>
@@ -9,6 +9,7 @@ import { computed } from 'vue'
9
9
 
10
10
  const { t } = useI18n()
11
11
  const ui = useUiStore()
12
+ const { canManageSecrets } = useWorkspaceAccess()
12
13
 
13
14
  const open = computed({
14
15
  get: () => ui.aiProviderSetupOpen,
@@ -49,7 +50,12 @@ const routes = computed<Route[]>(() => [
49
50
  title: t('providers.onboarding.routes.keys.title'),
50
51
  body: t('providers.onboarding.routes.keys.body'),
51
52
  cta: t('providers.onboarding.routes.keys.cta'),
52
- onSelect: () => go(ui.openVendorCredentials),
53
+ // The default tab is the workspace's pooled subscriptions, a `secrets.manage` write. Anyone else
54
+ // lands on the direct keys, which open on their own "My keys" scope (see ApiKeysSection).
55
+ onSelect: () =>
56
+ go(() =>
57
+ canManageSecrets.value ? ui.openVendorCredentials() : ui.openVendorCredentials('direct'),
58
+ ),
53
59
  },
54
60
  {
55
61
  icon: 'i-lucide-route',
@@ -37,8 +37,27 @@ const { confirmAction, toastDone } = useConfirmAction()
37
37
  /** Account-wide mode (single account scope) vs the default workspace/user toggle. */
38
38
  const isAccount = computed(() => !!props.accountId)
39
39
 
40
- /** Which store the form writes to: the shared workspace keys or the user's own. */
41
- const scope = ref<'workspace' | 'user'>('workspace')
40
+ /**
41
+ * Which store the form writes to: the shared workspace keys or the user's own. The DEFAULT follows
42
+ * the caller's grant on the active board: the workspace for whoever may write it (`secrets.manage`)
43
+ * and "My keys" for everyone else, so a first save lands instead of being refused. It is derived,
44
+ * not captured once, because this section stays mounted while the board (and so the grant)
45
+ * changes. A scope the user picks by hand wins until they move to another board.
46
+ */
47
+ const { canManageSecrets } = useWorkspaceAccess()
48
+ const pickedScope = ref<'workspace' | 'user' | null>(null)
49
+ const scope = computed<'workspace' | 'user'>({
50
+ get: () => pickedScope.value ?? (canManageSecrets.value ? 'workspace' : 'user'),
51
+ set: (value) => {
52
+ pickedScope.value = value
53
+ },
54
+ })
55
+ watch(
56
+ () => workspace.workspaceId,
57
+ () => {
58
+ pickedScope.value = null
59
+ },
60
+ )
42
61
 
43
62
  // "My keys" (user scope) are stored per-user, so they need a signed-in user. Block just
44
63
  // that scope when there's none (a deployment without sign-in); workspace/account keys are
@@ -27,6 +27,12 @@ const props = defineProps<{
27
27
 
28
28
  const { t } = useI18n()
29
29
  const github = useGitHubStore()
30
+ const { canManageIntegrations } = useWorkspaceAccess()
31
+
32
+ // Connecting is an `integrations.manage` write, and the connect-options read is gated the same way,
33
+ // so for anyone else the options come back empty. That empty list says nothing about the
34
+ // deployment: name the board admin instead of telling a member the operator configured nothing.
35
+ const adminRequired = computed(() => !canManageIntegrations.value)
30
36
 
31
37
  // Whether the deployment serves neither surface. Rendered as a statement rather than an empty
32
38
  // box: "nothing is configured" and "we couldn't read what is configured" both land here, and a
@@ -36,23 +42,33 @@ const nothingConfigured = computed(() => !github.canConnectGitHubApp && !github.
36
42
 
37
43
  <template>
38
44
  <div class="space-y-3">
39
- <template v-if="github.canConnectGitHubApp">
40
- <p v-if="props.appIntro" class="text-sm text-muted">{{ props.appIntro }}</p>
41
- <GitHubConnect />
42
- </template>
43
-
44
- <USeparator
45
- v-if="github.canConnectGitHubApp && github.canConnectGitLabPat"
46
- :label="t('vcs.connect.or')"
47
- />
48
-
49
- <GitLabConnect v-if="github.canConnectGitLabPat" />
50
-
51
45
  <p
52
- v-if="nothingConfigured"
46
+ v-if="adminRequired"
53
47
  class="rounded-md border border-dashed border-default px-3 py-3 text-sm text-muted"
48
+ data-testid="vcs-connect-admin-required"
54
49
  >
55
- {{ t('vcs.connect.noneConfigured') }}
50
+ {{ t('vcs.connect.adminRequired') }}
56
51
  </p>
52
+
53
+ <template v-else>
54
+ <template v-if="github.canConnectGitHubApp">
55
+ <p v-if="props.appIntro" class="text-sm text-muted">{{ props.appIntro }}</p>
56
+ <GitHubConnect />
57
+ </template>
58
+
59
+ <USeparator
60
+ v-if="github.canConnectGitHubApp && github.canConnectGitLabPat"
61
+ :label="t('vcs.connect.or')"
62
+ />
63
+
64
+ <GitLabConnect v-if="github.canConnectGitLabPat" />
65
+
66
+ <p
67
+ v-if="nothingConfigured"
68
+ class="rounded-md border border-dashed border-default px-3 py-3 text-sm text-muted"
69
+ >
70
+ {{ t('vcs.connect.noneConfigured') }}
71
+ </p>
72
+ </template>
57
73
  </div>
58
74
  </template>
@@ -0,0 +1,25 @@
1
+ import { computed } from 'vue'
2
+ import type { SetupActor } from '~/utils/setupAudience'
3
+
4
+ /**
5
+ * The caller as a setup prompt sees them: which app surface they chose, and the board and account
6
+ * grants that decide whether a "configure this" button would let them save. Every input is read
7
+ * from the source the nav gates read too (the role store, `useWorkspaceAccess`, and the accounts
8
+ * store's `isActiveAccountAdmin`), so a setup prompt and the sidebar cannot disagree about who the
9
+ * caller is.
10
+ */
11
+ export function useSetupActor() {
12
+ const access = useWorkspaceAccess()
13
+ const uiRole = useUiRoleStore()
14
+ const accounts = useAccountsStore()
15
+
16
+ const actor = computed<SetupActor>(() => ({
17
+ fullSurface: uiRole.fullSurface,
18
+ canManageIntegrations: access.canManageIntegrations.value,
19
+ canExecuteRuns: access.canExecuteRuns.value,
20
+ isAccountAdmin: accounts.isActiveAccountAdmin,
21
+ accountsEnabled: accounts.enabled,
22
+ }))
23
+
24
+ return { actor }
25
+ }
@@ -16,7 +16,7 @@ import type { WorkspacePermission, WorkspaceRole } from '~/types/domain'
16
16
  * SPA never hides an affordance the backend would have permitted.
17
17
  *
18
18
  * This is deliberately distinct from the ACCOUNT-scoped admin checks
19
- * (`accounts.activeAccount?.roles?.includes('admin')`), which stay account-scoped: this
19
+ * (`accounts.isActiveAccountAdmin`), which stay account-scoped: this
20
20
  * composable answers "what can you do inside THIS board", the account check answers "what
21
21
  * can you do to the tenant".
22
22
  */
@@ -75,7 +75,6 @@ export function createNavGates(): NavGates {
75
75
  providerConnections.isAvailable('runner-pool') ||
76
76
  providerConnections.isAvailable('environment'),
77
77
  )
78
- const isAccountAdmin = computed(() => accounts.activeAccount?.roles?.includes('admin') ?? false)
79
78
 
80
79
  return {
81
80
  get canWriteBoard() {
@@ -105,7 +104,7 @@ export function createNavGates(): NavGates {
105
104
  return accounts.enabled
106
105
  },
107
106
  get isAccountAdmin() {
108
- return isAccountAdmin.value
107
+ return accounts.isActiveAccountAdmin
109
108
  },
110
109
  get advancedMode() {
111
110
  return uiMode.isAdvanced
@@ -35,6 +35,13 @@ export const useAccountsStore = defineStore(
35
35
  )
36
36
  /** Whether accounts exist (auth on); gates the switcher UI. */
37
37
  const enabled = computed(() => accounts.value.length > 0)
38
+ /**
39
+ * Whether the caller is an admin of the ACTIVE account. The one reading of it, so the nav gates,
40
+ * the setup prompts and the account panels cannot disagree about who the account admin is.
41
+ */
42
+ const isActiveAccountAdmin = computed(
43
+ () => activeAccount.value?.roles?.includes('admin') ?? false,
44
+ )
38
45
 
39
46
  /** Load the user's accounts and resolve the active one (persisted or first). */
40
47
  async function load() {
@@ -209,6 +216,7 @@ export const useAccountsStore = defineStore(
209
216
  activeAccountId,
210
217
  activeAccount,
211
218
  enabled,
219
+ isActiveAccountAdmin,
212
220
  ready,
213
221
  members,
214
222
  invitations,
@@ -1,6 +1,6 @@
1
1
  import { ref } from 'vue'
2
2
  import { applyInfraSetupTransition, isInfraSetupHealthStatus } from '@cat-factory/contracts'
3
- import type { InfraSetup, InfraSetupArea, InfraSetupStatus } from '~/types/domain'
3
+ import type { InfraSetup, InfraSetupArea, InfraSetupOwners, InfraSetupStatus } from '~/types/domain'
4
4
 
5
5
  // The workspace store's infra-setup slice: the per-area setup/health projection the setup banner
6
6
  // renders, and the live `infraSetup` event patch the reachability watcher pushes into it. Its own
@@ -32,8 +32,16 @@ export function createInfraSetupState() {
32
32
  */
33
33
  const infraSetupDetails = ref<Partial<Record<InfraSetupArea, string>>>({})
34
34
 
35
- function hydrate(next: InfraSetup | null | undefined) {
35
+ /**
36
+ * Who can close each area's gap (workspace admin, account admin or the operator), from the
37
+ * snapshot. Static for the life of a deployment, so the live event never touches it. Null on an
38
+ * older backend, which the banner reads as "owner unknown".
39
+ */
40
+ const infraSetupOwners = ref<InfraSetupOwners | null>(null)
41
+
42
+ function hydrate(next: InfraSetup | null | undefined, owners?: InfraSetupOwners | null) {
36
43
  infraSetup.value = next ?? null
44
+ infraSetupOwners.value = owners ?? null
37
45
  // The reasons belong to the live pushes this session observed, so an authoritative snapshot
38
46
  // supersedes them: keeping one would caption a freshly-read status with a stale probe.
39
47
  infraSetupDetails.value = {}
@@ -73,5 +81,5 @@ export function createInfraSetupState() {
73
81
  if (!isInfraSetupHealthStatus(status)) useUiStore().clearInfraSetupSessionDismissal(area)
74
82
  }
75
83
 
76
- return { infraSetup, infraSetupDetails, hydrate, patchInfraSetup }
84
+ return { infraSetup, infraSetupDetails, infraSetupOwners, hydrate, patchInfraSetup }
77
85
  }
@@ -59,6 +59,7 @@ export const useWorkspaceStore = defineStore(
59
59
  const {
60
60
  infraSetup,
61
61
  infraSetupDetails,
62
+ infraSetupOwners,
62
63
  hydrate: hydrateInfraSetup,
63
64
  patchInfraSetup,
64
65
  } = createInfraSetupState()
@@ -98,7 +99,7 @@ export const useWorkspaceStore = defineStore(
98
99
  accountSpend.value = snapshot.accountSpend ?? null
99
100
  userSpend.value = snapshot.userSpend ?? null
100
101
  budgetCaps.value = snapshot.budgetCaps ?? null
101
- hydrateInfraSetup(snapshot.infraSetup)
102
+ hydrateInfraSetup(snapshot.infraSetup, snapshot.infraSetupOwners)
102
103
  access.value = snapshot.access ?? null
103
104
  // Keep the board list in step (e.g. a freshly created board, or a rename). The
104
105
  // snapshot's `workspace` carries no `viewerRole` (that's a `GET /workspaces` list
@@ -241,6 +242,7 @@ export const useWorkspaceStore = defineStore(
241
242
  budgetCaps,
242
243
  infraSetup,
243
244
  infraSetupDetails,
245
+ infraSetupOwners,
244
246
  patchInfraSetup,
245
247
  access,
246
248
  init,
@@ -108,6 +108,8 @@ export type {
108
108
  InfraSetup,
109
109
  InfraSetupStatus,
110
110
  InfraSetupArea,
111
+ InfraSetupOwner,
112
+ InfraSetupOwners,
111
113
  ServiceFragmentDefaults,
112
114
  KaizenGradingStatus,
113
115
  KaizenGrading,
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { InfraSetup, InfraSetupOwners } from '~/types/domain'
3
+ import { blockingSetupNotice, canActOnSetup, type SetupActor } from './setupAudience'
4
+
5
+ const admin: SetupActor = {
6
+ fullSurface: true,
7
+ canManageIntegrations: true,
8
+ canExecuteRuns: true,
9
+ isAccountAdmin: false,
10
+ accountsEnabled: true,
11
+ }
12
+ const member: SetupActor = { ...admin, canManageIntegrations: false }
13
+ const viewer: SetupActor = { ...member, canExecuteRuns: false }
14
+ const designerAdmin: SetupActor = { ...admin, fullSurface: false }
15
+ const designerMember: SetupActor = { ...member, fullSurface: false }
16
+
17
+ const owners: InfraSetupOwners = {
18
+ agentExecutor: 'workspace_admin',
19
+ ephemeralEnvironments: 'workspace_admin',
20
+ binaryStorage: 'account_admin',
21
+ }
22
+
23
+ function status(patch: Partial<InfraSetup>): InfraSetup {
24
+ return {
25
+ agentExecutor: 'configured',
26
+ ephemeralEnvironments: 'configured',
27
+ binaryStorage: 'configured',
28
+ ...patch,
29
+ }
30
+ }
31
+
32
+ const noRunner = status({ agentExecutor: 'not_defined' })
33
+
34
+ describe('canActOnSetup', () => {
35
+ it('lets a workspace admin act on the workspace areas, and not a member', () => {
36
+ expect(canActOnSetup('workspace_admin', admin)).toBe(true)
37
+ expect(canActOnSetup('workspace_admin', member)).toBe(false)
38
+ })
39
+
40
+ it('keeps every prompt off the designer surface, whatever the permissions', () => {
41
+ expect(canActOnSetup('workspace_admin', designerAdmin)).toBe(false)
42
+ expect(canActOnSetup('account_admin', { ...designerAdmin, isAccountAdmin: true })).toBe(false)
43
+ })
44
+
45
+ it('gives an account-owned gap to the account admin, not to a workspace admin', () => {
46
+ expect(canActOnSetup('account_admin', admin)).toBe(false)
47
+ expect(canActOnSetup('account_admin', { ...member, isAccountAdmin: true })).toBe(true)
48
+ })
49
+
50
+ it('lets the single user act on an account-owned gap when there are no accounts', () => {
51
+ expect(canActOnSetup('account_admin', { ...admin, accountsEnabled: false })).toBe(true)
52
+ })
53
+
54
+ it('lets nobody act on an operator-owned gap', () => {
55
+ expect(canActOnSetup('operator', { ...admin, isAccountAdmin: true })).toBe(false)
56
+ })
57
+
58
+ it('falls back to the workspace admin for an absent owner or one this build does not know', () => {
59
+ // An older backend sends nothing; a newer one may send a value past the compiled union. Both
60
+ // read as the workspace admin, and neither throws inside the banner's render.
61
+ for (const owner of [undefined, 'board_owner']) {
62
+ expect(canActOnSetup(owner, admin)).toBe(true)
63
+ expect(canActOnSetup(owner, member)).toBe(false)
64
+ }
65
+ })
66
+ })
67
+
68
+ describe('blockingSetupNotice', () => {
69
+ it('tells a member that agents cannot run, and who can fix it', () => {
70
+ expect(blockingSetupNotice(noRunner, owners, member)).toEqual({
71
+ area: 'agentExecutor',
72
+ kind: 'setup',
73
+ remedy: 'workspace_admin',
74
+ })
75
+ })
76
+
77
+ it('reports an executor outage as an outage', () => {
78
+ expect(
79
+ blockingSetupNotice(status({ agentExecutor: 'unreachable' }), owners, member)?.kind,
80
+ ).toBe('outage')
81
+ })
82
+
83
+ it('says nothing to the admin, who gets the full card instead', () => {
84
+ expect(blockingSetupNotice(noRunner, owners, admin)).toBeNull()
85
+ })
86
+
87
+ it('says nothing to a viewer, whose work the gap does not stop', () => {
88
+ expect(blockingSetupNotice(noRunner, owners, viewer)).toBeNull()
89
+ })
90
+
91
+ it('tells an admin on the designer surface to switch surface, not to find an admin', () => {
92
+ expect(blockingSetupNotice(noRunner, owners, designerAdmin)?.remedy).toBe('full_surface')
93
+ // A designer without the grant is still pointed at the owner.
94
+ expect(blockingSetupNotice(noRunner, owners, designerMember)?.remedy).toBe('workspace_admin')
95
+ })
96
+
97
+ it('says nothing about gaps that only degrade some runs', () => {
98
+ const optional = status({ ephemeralEnvironments: 'not_defined', binaryStorage: 'not_defined' })
99
+ expect(blockingSetupNotice(optional, owners, member)).toBeNull()
100
+ })
101
+
102
+ it('says nothing when the executor is configured or not used here', () => {
103
+ expect(blockingSetupNotice(status({}), owners, member)).toBeNull()
104
+ expect(
105
+ blockingSetupNotice(status({ agentExecutor: 'not_applicable' }), owners, member),
106
+ ).toBeNull()
107
+ expect(blockingSetupNotice(null, owners, member)).toBeNull()
108
+ })
109
+
110
+ it('names the workspace admin when the backend reports no owner, or one it does not know', () => {
111
+ expect(blockingSetupNotice(noRunner, null, member)?.remedy).toBe('workspace_admin')
112
+ const unknown = { ...owners, agentExecutor: 'board_owner' } as unknown as InfraSetupOwners
113
+ expect(blockingSetupNotice(noRunner, unknown, member)?.remedy).toBe('workspace_admin')
114
+ })
115
+ })
@@ -0,0 +1,102 @@
1
+ import { isInfraSetupHealthStatus, isInfraSetupOwner } from '@cat-factory/contracts'
2
+ import type { InfraSetup, InfraSetupArea, InfraSetupOwner, InfraSetupOwners } from '~/types/domain'
3
+
4
+ // Who a setup prompt is FOR. The infra-setup cards used to render for every caller, so a member,
5
+ // a viewer and a designer each got "Configure runner pool" buttons that opened screens their save
6
+ // would 403 on. The rule here keeps each card to the people who can act on it, and gives everyone
7
+ // else one line naming who can, but only when the gap stops their own work.
8
+
9
+ /** What the caller can do, as far as a setup prompt cares. */
10
+ export interface SetupActor {
11
+ /** The full app surface (engineer / PM). The designer (`intake`) surface never configures. */
12
+ fullSurface: boolean
13
+ /** `integrations.manage` on the active board (true in dev-open, like every permission). */
14
+ canManageIntegrations: boolean
15
+ /** `runs.execute` on the active board: whether a gap that stops runs stops THEIR work. */
16
+ canExecuteRuns: boolean
17
+ /** An admin of the board's account. */
18
+ isAccountAdmin: boolean
19
+ /** Whether this deployment has accounts at all (auth on). */
20
+ accountsEnabled: boolean
21
+ }
22
+
23
+ /**
24
+ * The areas whose gap stops EVERY run, so a caller who cannot fix one still needs to know it is
25
+ * there: without an executor no agent starts. A missing test environment or content storage only
26
+ * degrades some runs, and the run that needs them already says so when it starts.
27
+ */
28
+ export const BLOCKING_INFRA_AREAS = ['agentExecutor'] as const satisfies readonly InfraSetupArea[]
29
+ export type BlockingInfraArea = (typeof BLOCKING_INFRA_AREAS)[number]
30
+
31
+ /**
32
+ * The owner to act on: the reported one when this build knows it, else the workspace admin, which
33
+ * is who the cards assumed before the owner was reported. An older backend sends none; a newer one
34
+ * may send an owner this bundle has never seen, and neither may break the board's render.
35
+ */
36
+ export function effectiveOwner(owner: unknown): InfraSetupOwner {
37
+ return isInfraSetupOwner(owner) ? owner : 'workspace_admin'
38
+ }
39
+
40
+ /** Whether the caller can close a gap owned by `owner` (any value; see {@link effectiveOwner}). */
41
+ export function canActOnSetup(owner: unknown, actor: SetupActor): boolean {
42
+ if (!actor.fullSurface) return false
43
+ const known = effectiveOwner(owner)
44
+ switch (known) {
45
+ case 'workspace_admin':
46
+ return actor.canManageIntegrations
47
+ case 'account_admin':
48
+ // Without accounts there is no account admin to name, and the single user holds every
49
+ // permission, so the workspace grant stands in for it.
50
+ return actor.accountsEnabled ? actor.isAccountAdmin : actor.canManageIntegrations
51
+ case 'operator':
52
+ return false
53
+ default: {
54
+ // Compile-time exhaustiveness only: `effectiveOwner` already narrowed the runtime value.
55
+ const unreachable: never = known
56
+ return unreachable
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * How a caller who cannot act on a gap from where they are CAN get it fixed: the owner to ask, or
63
+ * `full_surface` when they hold the grant themselves and only the designer surface they chose hides
64
+ * the card. Telling a board admin that "a board admin can fix this" would send them looking for
65
+ * someone else.
66
+ */
67
+ export type SetupRemedy = InfraSetupOwner | 'full_surface'
68
+
69
+ /** The line a caller who cannot act sees for a blocking gap. */
70
+ export interface SetupNotice {
71
+ area: BlockingInfraArea
72
+ /** `outage` when the area is configured but unreachable, else `setup`. */
73
+ kind: 'setup' | 'outage'
74
+ remedy: SetupRemedy
75
+ }
76
+
77
+ /**
78
+ * The first blocking gap the caller cannot act on, or null. The caller who CAN act gets the full
79
+ * card instead, so the two never describe the same gap twice, and a caller who cannot start a run
80
+ * (a viewer) gets nothing: the gap does not stop anything they do.
81
+ */
82
+ export function blockingSetupNotice(
83
+ status: InfraSetup | null,
84
+ owners: InfraSetupOwners | null,
85
+ actor: SetupActor,
86
+ ): SetupNotice | null {
87
+ if (!status || !actor.canExecuteRuns) return null
88
+ for (const area of BLOCKING_INFRA_AREAS) {
89
+ const areaStatus = status[area]
90
+ const kind = isInfraSetupHealthStatus(areaStatus)
91
+ ? 'outage'
92
+ : areaStatus === 'not_defined'
93
+ ? 'setup'
94
+ : null
95
+ if (!kind) continue
96
+ const owner = effectiveOwner(owners?.[area])
97
+ if (canActOnSetup(owner, actor)) continue
98
+ const remedy = canActOnSetup(owner, { ...actor, fullSurface: true }) ? 'full_surface' : owner
99
+ return { area, kind, remedy }
100
+ }
101
+ return null
102
+ }
@@ -2687,6 +2687,18 @@
2687
2687
  "action": "Verbindung prüfen"
2688
2688
  }
2689
2689
  },
2690
+ "infraSetupNotice": {
2691
+ "agentExecutor": {
2692
+ "setup": "Auf diesem Board können noch keine Agenten laufen: Es ist kein Runner-Pool verbunden.",
2693
+ "outage": "Agenten können gerade nicht laufen: Der Runner-Pool dieses Boards ist nicht erreichbar."
2694
+ },
2695
+ "owner": {
2696
+ "workspace_admin": "Ein Board-Admin kann das beheben.",
2697
+ "account_admin": "Ein Konto-Admin kann das beheben.",
2698
+ "operator": "Nur der Betreiber des Deployments kann das beheben."
2699
+ },
2700
+ "fullSurface": "Sie können das selbst beheben: Wechseln Sie Ihre Rolle oben in der Seitenleiste."
2701
+ },
2690
2702
  "spendWarningBanner": {
2691
2703
  "title": "Ausgabenlimit erreicht, Agentenausführung pausiert",
2692
2704
  "body": "Die Token-Ausgaben dieses Monats haben das konfigurierte Budget erreicht, sodass laufende Pipelines pausiert wurden, um weitere Kosten zu vermeiden. Die Ausführung wird automatisch fortgesetzt, wenn das Budget im nächsten Monat zurückgesetzt wird, oder sobald das Limit angehoben wird.",
@@ -7985,6 +7997,7 @@
7985
7997
  "connect": {
7986
7998
  "or": "oder",
7987
7999
  "noneConfigured": "Für dieses Deployment ist keine Quellcodeverbindung konfiguriert. Bitten Sie einen Betreiber, eine GitHub App oder einen GitLab-Zugang einzurichten.",
8000
+ "adminRequired": "Nur ein Board-Admin kann einen Repository-Host verbinden. Bitten Sie einen Admin dieses Boards darum.",
7988
8001
  "gitlab": {
7989
8002
  "intro": "Fügen Sie ein persönliches GitLab-Zugriffstoken ein, um diesen Workspace zu verbinden. cat-factory nutzt es, um Ihre Projekte aufzulisten, Branches zu pushen und Merge Requests zu öffnen.",
7990
8003
  "tokenLabel": "Persönliches Zugriffstoken",
@@ -8002,7 +8015,8 @@
8002
8015
  "onboarding": {
8003
8016
  "title": "cat-factory mit {provider} verbinden",
8004
8017
  "titleAny": "cat-factory mit Ihren Repositorys verbinden",
8005
- "intro": "cat-factory funktioniert, indem es Pull Requests in Ihren Repositorys öffnet. Verbinden Sie Ihren Repository-Anbieter, um fortzufahren."
8018
+ "intro": "cat-factory funktioniert, indem es Pull Requests in Ihren Repositorys öffnet. Verbinden Sie Ihren Repository-Anbieter, um fortzufahren.",
8019
+ "switchBoard": "Nicht das gewünschte Board? Wechseln Sie zu einem anderen oder erstellen Sie ein eigenes."
8006
8020
  },
8007
8021
  "addService": {
8008
8022
  "intro": "Wählen Sie ein bestehendes {provider}-Repository, um es als Board-Service hinzuzufügen. Kein Bootstrapping: Das Repo wird unverändert mit einem neuen Service-Frame verknüpft, und Aufgaben, die Sie darauf ausführen, zielen auf dieses Repo.",
@@ -2715,6 +2715,18 @@
2715
2715
  "action": "Check connection"
2716
2716
  }
2717
2717
  },
2718
+ "infraSetupNotice": {
2719
+ "agentExecutor": {
2720
+ "setup": "Agents can't run on this board yet: no runner pool is connected.",
2721
+ "outage": "Agents can't run right now: this board's runner pool is unreachable."
2722
+ },
2723
+ "owner": {
2724
+ "workspace_admin": "A board admin can fix this.",
2725
+ "account_admin": "An account admin can fix this.",
2726
+ "operator": "Only the deployment operator can fix this."
2727
+ },
2728
+ "fullSurface": "You can fix this yourself: switch your role at the top of the sidebar."
2729
+ },
2718
2730
  "spendWarningBanner": {
2719
2731
  "title": "Spend limit reached, agent execution paused",
2720
2732
  "body": "This month's token spend has hit the configured budget, so running pipelines were paused to avoid further cost. Execution resumes automatically when the budget rolls over next month, or once the limit is raised.",
@@ -8289,6 +8301,7 @@
8289
8301
  "connect": {
8290
8302
  "or": "or",
8291
8303
  "noneConfigured": "No source-control connection is configured for this deployment. Ask an operator to set up a GitHub App or GitLab access.",
8304
+ "adminRequired": "Only a board admin can connect a repository host. Ask an admin of this board.",
8292
8305
  "gitlab": {
8293
8306
  "intro": "Paste a GitLab personal access token to connect this workspace. cat-factory uses it to list your projects, push branches and open merge requests.",
8294
8307
  "tokenLabel": "Personal access token",
@@ -8306,7 +8319,8 @@
8306
8319
  "onboarding": {
8307
8320
  "title": "Connect cat-factory to {provider}",
8308
8321
  "titleAny": "Connect cat-factory to your repositories",
8309
- "intro": "cat-factory works by opening pull requests on your repositories. Connect your repository host to continue."
8322
+ "intro": "cat-factory works by opening pull requests on your repositories. Connect your repository host to continue.",
8323
+ "switchBoard": "Not the board you meant? Switch to another one, or create your own."
8310
8324
  },
8311
8325
  "addService": {
8312
8326
  "intro": "Pick an existing {provider} repository to add as a board service. No bootstrapping: the repo is linked to a new service frame as-is, and tasks you run on it target that repo.",
@@ -2560,6 +2560,18 @@
2560
2560
  "action": "Comprobar conexión"
2561
2561
  }
2562
2562
  },
2563
+ "infraSetupNotice": {
2564
+ "agentExecutor": {
2565
+ "setup": "Los agentes aún no pueden ejecutarse en este tablero: no hay ningún pool de runners conectado.",
2566
+ "outage": "Los agentes no pueden ejecutarse ahora mismo: el pool de runners de este tablero no está accesible."
2567
+ },
2568
+ "owner": {
2569
+ "workspace_admin": "Un administrador del tablero puede solucionarlo.",
2570
+ "account_admin": "Un administrador de la cuenta puede solucionarlo.",
2571
+ "operator": "Solo el operador del despliegue puede solucionarlo."
2572
+ },
2573
+ "fullSurface": "Puedes solucionarlo tú: cambia tu rol desde la parte superior de la barra lateral."
2574
+ },
2563
2575
  "spendWarningBanner": {
2564
2576
  "title": "Límite de gasto alcanzado, ejecución de agentes en pausa",
2565
2577
  "body": "El gasto de tokens de este mes ha alcanzado el presupuesto configurado, por lo que las canalizaciones en curso se pausaron para evitar más costes. La ejecución se reanuda automáticamente cuando el presupuesto se renueva el mes que viene, o una vez que se aumente el límite.",
@@ -7985,6 +7997,7 @@
7985
7997
  "connect": {
7986
7998
  "or": "o",
7987
7999
  "noneConfigured": "Este despliegue no tiene configurada ninguna conexión de control de código. Pide a un operador que configure una GitHub App o el acceso a GitLab.",
8000
+ "adminRequired": "Solo un administrador del tablero puede conectar un host de repositorios. Pídeselo a un administrador de este tablero.",
7988
8001
  "gitlab": {
7989
8002
  "intro": "Pega un token de acceso personal de GitLab para conectar este espacio de trabajo. cat-factory lo usa para listar tus proyectos, subir ramas y abrir solicitudes de fusión.",
7990
8003
  "tokenLabel": "Token de acceso personal",
@@ -8002,7 +8015,8 @@
8002
8015
  "onboarding": {
8003
8016
  "title": "Conecta cat-factory con {provider}",
8004
8017
  "titleAny": "Conecta cat-factory con tus repositorios",
8005
- "intro": "cat-factory funciona abriendo solicitudes de incorporación de cambios en tus repositorios. Conecta tu proveedor de repositorios para continuar."
8018
+ "intro": "cat-factory funciona abriendo solicitudes de incorporación de cambios en tus repositorios. Conecta tu proveedor de repositorios para continuar.",
8019
+ "switchBoard": "¿No es el tablero que buscabas? Cambia a otro o crea uno propio."
8006
8020
  },
8007
8021
  "addService": {
8008
8022
  "intro": "Elige un repositorio de {provider} existente para añadirlo como servicio del tablero. Sin arranque inicial: el repositorio se vincula tal cual a un nuevo marco de servicio, y las tareas que ejecutes en él apuntan a ese repositorio.",
@@ -2560,6 +2560,18 @@
2560
2560
  "action": "Vérifier la connexion"
2561
2561
  }
2562
2562
  },
2563
+ "infraSetupNotice": {
2564
+ "agentExecutor": {
2565
+ "setup": "Les agents ne peuvent pas encore s'exécuter sur ce tableau : aucun pool de runners n'est connecté.",
2566
+ "outage": "Les agents ne peuvent pas s'exécuter pour le moment : le pool de runners de ce tableau est injoignable."
2567
+ },
2568
+ "owner": {
2569
+ "workspace_admin": "Un administrateur du tableau peut corriger cela.",
2570
+ "account_admin": "Un administrateur du compte peut corriger cela.",
2571
+ "operator": "Seul l'opérateur du déploiement peut corriger cela."
2572
+ },
2573
+ "fullSurface": "Vous pouvez corriger cela vous-même : changez de rôle en haut de la barre latérale."
2574
+ },
2563
2575
  "spendWarningBanner": {
2564
2576
  "title": "Limite de dépense atteinte, exécution des agents en pause",
2565
2577
  "body": "La dépense en jetons de ce mois a atteint le budget configuré, les pipelines en cours ont donc été mis en pause pour éviter des coûts supplémentaires. L'exécution reprend automatiquement au renouvellement du budget le mois prochain, ou dès que la limite est relevée.",
@@ -7985,6 +7997,7 @@
7985
7997
  "connect": {
7986
7998
  "or": "ou",
7987
7999
  "noneConfigured": "Aucune connexion de gestion de code n'est configurée pour ce déploiement. Demandez à un opérateur de configurer une GitHub App ou un accès GitLab.",
8000
+ "adminRequired": "Seul un administrateur du tableau peut connecter un hébergeur de dépôts. Demandez-le à un administrateur de ce tableau.",
7988
8001
  "gitlab": {
7989
8002
  "intro": "Collez un jeton d'accès personnel GitLab pour connecter cet espace de travail. cat-factory l'utilise pour lister vos projets, pousser des branches et ouvrir des merge requests.",
7990
8003
  "tokenLabel": "Jeton d'accès personnel",
@@ -8002,7 +8015,8 @@
8002
8015
  "onboarding": {
8003
8016
  "title": "Connecter cat-factory à {provider}",
8004
8017
  "titleAny": "Connecter cat-factory à vos dépôts",
8005
- "intro": "cat-factory fonctionne en ouvrant des pull requests sur vos dépôts. Connectez votre hébergeur de dépôts pour continuer."
8018
+ "intro": "cat-factory fonctionne en ouvrant des pull requests sur vos dépôts. Connectez votre hébergeur de dépôts pour continuer.",
8019
+ "switchBoard": "Ce n'est pas le bon tableau ? Passez à un autre, ou créez le vôtre."
8006
8020
  },
8007
8021
  "addService": {
8008
8022
  "intro": "Choisissez un dépôt {provider} existant à ajouter comme service du tableau. Sans amorçage : le dépôt est lié tel quel à un nouveau cadre de service, et les tâches que vous y exécutez ciblent ce dépôt.",
@@ -2560,6 +2560,18 @@
2560
2560
  "action": "בדיקת החיבור"
2561
2561
  }
2562
2562
  },
2563
+ "infraSetupNotice": {
2564
+ "agentExecutor": {
2565
+ "setup": "סוכנים עדיין לא יכולים לפעול בלוח הזה: לא מחובר מאגר ראנרים.",
2566
+ "outage": "סוכנים לא יכולים לפעול כרגע: מאגר הראנרים של הלוח הזה אינו נגיש."
2567
+ },
2568
+ "owner": {
2569
+ "workspace_admin": "מנהל של הלוח יכול לתקן זאת.",
2570
+ "account_admin": "מנהל של החשבון יכול לתקן זאת.",
2571
+ "operator": "רק המפעיל של הפריסה יכול לתקן זאת."
2572
+ },
2573
+ "fullSurface": "אפשר לתקן זאת בעצמכם: החליפו תפקיד בראש סרגל הצד."
2574
+ },
2563
2575
  "spendWarningBanner": {
2564
2576
  "title": "מגבלת ההוצאה הושגה, הרצת הסוכנים הושהתה",
2565
2577
  "body": "הוצאת האסימונים של החודש הגיעה לתקציב המוגדר, ולכן צינורות פעילים הושהו כדי להימנע מעלות נוספת. ההרצה מתחדשת אוטומטית כשהתקציב מתחדש בחודש הבא, או כאשר המגבלה מועלית.",
@@ -7985,6 +7997,7 @@
7985
7997
  "connect": {
7986
7998
  "or": "או",
7987
7999
  "noneConfigured": "לא הוגדר חיבור לניהול קוד מקור בפריסה הזו. בקש ממפעיל להגדיר אפליקציית GitHub או גישה ל-GitLab.",
8000
+ "adminRequired": "רק מנהל של הלוח יכול לחבר שירות אחסון מאגרים. בקשו זאת ממנהל של הלוח הזה.",
7988
8001
  "gitlab": {
7989
8002
  "intro": "הדבק אסימון גישה אישי של GitLab כדי לחבר את סביבת העבודה הזו. cat-factory משתמש בו כדי להציג את הפרויקטים שלך, לדחוף ענפים ולפתוח בקשות מיזוג.",
7990
8003
  "tokenLabel": "אסימון גישה אישי",
@@ -8002,7 +8015,8 @@
8002
8015
  "onboarding": {
8003
8016
  "title": "חבר את cat-factory ל-{provider}",
8004
8017
  "titleAny": "חבר את cat-factory למאגרים שלך",
8005
- "intro": "cat-factory פועל על ידי פתיחת בקשות משיכה במאגרים שלך. חבר את ספק המאגרים שלך כדי להמשיך."
8018
+ "intro": "cat-factory פועל על ידי פתיחת בקשות משיכה במאגרים שלך. חבר את ספק המאגרים שלך כדי להמשיך.",
8019
+ "switchBoard": "זה לא הלוח שהתכוונתם אליו? עברו ללוח אחר או צרו לוח משלכם."
8006
8020
  },
8007
8021
  "addService": {
8008
8022
  "intro": "בחר מאגר {provider} קיים כדי להוסיף כשירות בלוח. ללא אתחול: המאגר מקושר למסגרת שירות חדשה כפי שהוא, ומשימות שתריץ עליו ממוקדות באותו מאגר.",
@@ -2687,6 +2687,18 @@
2687
2687
  "action": "Verifica connessione"
2688
2688
  }
2689
2689
  },
2690
+ "infraSetupNotice": {
2691
+ "agentExecutor": {
2692
+ "setup": "Gli agenti non possono ancora funzionare su questa board: nessun runner pool è connesso.",
2693
+ "outage": "Gli agenti non possono funzionare in questo momento: il runner pool di questa board non è raggiungibile."
2694
+ },
2695
+ "owner": {
2696
+ "workspace_admin": "Un amministratore della board può risolvere il problema.",
2697
+ "account_admin": "Un amministratore dell'account può risolvere il problema.",
2698
+ "operator": "Solo l'operatore del deployment può risolvere il problema."
2699
+ },
2700
+ "fullSurface": "Puoi risolverlo tu: cambia ruolo dall'alto della barra laterale."
2701
+ },
2690
2702
  "spendWarningBanner": {
2691
2703
  "title": "Limite di spesa raggiunto, esecuzione degli agenti sospesa",
2692
2704
  "body": "La spesa in token di questo mese ha raggiunto il budget configurato, quindi le pipeline in esecuzione sono state sospese per evitare ulteriori costi. L'esecuzione riprende automaticamente quando il budget si rinnova il mese prossimo, oppure una volta che il limite viene aumentato.",
@@ -7985,6 +7997,7 @@
7985
7997
  "connect": {
7986
7998
  "or": "oppure",
7987
7999
  "noneConfigured": "Per questo deployment non è configurata alcuna connessione al controllo del codice. Chiedi a un operatore di configurare una GitHub App o l'accesso a GitLab.",
8000
+ "adminRequired": "Solo un amministratore della board può connettere un host di repository. Chiedilo a un amministratore di questa board.",
7988
8001
  "gitlab": {
7989
8002
  "intro": "Incolla un token di accesso personale GitLab per collegare questo workspace. cat-factory lo usa per elencare i tuoi progetti, inviare branch e aprire merge request.",
7990
8003
  "tokenLabel": "Token di accesso personale",
@@ -8002,7 +8015,8 @@
8002
8015
  "onboarding": {
8003
8016
  "title": "Collega cat-factory a {provider}",
8004
8017
  "titleAny": "Collega cat-factory ai tuoi repository",
8005
- "intro": "cat-factory funziona aprendo pull request sui tuoi repository. Collega il tuo host di repository per continuare."
8018
+ "intro": "cat-factory funziona aprendo pull request sui tuoi repository. Collega il tuo host di repository per continuare.",
8019
+ "switchBoard": "Non è la board che cercavi? Passa a un'altra, oppure creane una tua."
8006
8020
  },
8007
8021
  "addService": {
8008
8022
  "intro": "Scegli un repository {provider} esistente da aggiungere come servizio sulla board. Nessun bootstrapping: il repo viene collegato a un nuovo frame di servizio così com'è, e le attività che vi esegui puntano a quel repo.",
@@ -2560,6 +2560,18 @@
2560
2560
  "action": "接続を確認"
2561
2561
  }
2562
2562
  },
2563
+ "infraSetupNotice": {
2564
+ "agentExecutor": {
2565
+ "setup": "このボードではまだエージェントを実行できません。ランナープールが接続されていません。",
2566
+ "outage": "現在エージェントを実行できません。このボードのランナープールに接続できません。"
2567
+ },
2568
+ "owner": {
2569
+ "workspace_admin": "ボードの管理者が解決できます。",
2570
+ "account_admin": "アカウントの管理者が解決できます。",
2571
+ "operator": "解決できるのはデプロイの運用者だけです。"
2572
+ },
2573
+ "fullSurface": "ご自身で解決できます。サイドバー上部でロールを切り替えてください。"
2574
+ },
2563
2575
  "spendWarningBanner": {
2564
2576
  "title": "支出上限に達したため、エージェントの実行を一時停止しました",
2565
2577
  "body": "今月のトークン支出が設定された予算に達したため、これ以上のコストを避けるために実行中のパイプラインを一時停止しました。来月に予算がリセットされると、または上限を引き上げると、実行は自動的に再開されます。",
@@ -7985,6 +7997,7 @@
7985
7997
  "connect": {
7986
7998
  "or": "または",
7987
7999
  "noneConfigured": "このデプロイにはソース管理の接続が設定されていません。GitHub App または GitLab へのアクセスの設定を運用者に依頼してください。",
8000
+ "adminRequired": "リポジトリホストを接続できるのはボードの管理者だけです。このボードの管理者に依頼してください。",
7988
8001
  "gitlab": {
7989
8002
  "intro": "GitLab の個人用アクセストークンを貼り付けて、このワークスペースを接続します。cat-factory はこれを使ってプロジェクトの一覧取得、ブランチのプッシュ、マージリクエストの作成を行います。",
7990
8003
  "tokenLabel": "個人用アクセストークン",
@@ -8002,7 +8015,8 @@
8002
8015
  "onboarding": {
8003
8016
  "title": "cat-factory を {provider} に接続",
8004
8017
  "titleAny": "cat-factory をリポジトリに接続",
8005
- "intro": "cat-factory はリポジトリ上でプルリクエストを開くことで動作します。続行するにはリポジトリホストを接続してください。"
8018
+ "intro": "cat-factory はリポジトリ上でプルリクエストを開くことで動作します。続行するにはリポジトリホストを接続してください。",
8019
+ "switchBoard": "目的のボードではありませんか?別のボードに切り替えるか、自分のボードを作成してください。"
8006
8020
  },
8007
8021
  "addService": {
8008
8022
  "intro": "ボードサービスとして追加する既存の{provider}リポジトリを選択してください。ブートストラップはありません。リポジトリはそのまま新しいサービスフレームにリンクされ、その上で実行するタスクはそのリポジトリを対象とします。",
@@ -2560,6 +2560,18 @@
2560
2560
  "action": "Sprawdź połączenie"
2561
2561
  }
2562
2562
  },
2563
+ "infraSetupNotice": {
2564
+ "agentExecutor": {
2565
+ "setup": "Na tej tablicy agenci nie mogą jeszcze działać: nie podłączono żadnej puli runnerów.",
2566
+ "outage": "Agenci nie mogą teraz działać: pula runnerów tej tablicy jest nieosiągalna."
2567
+ },
2568
+ "owner": {
2569
+ "workspace_admin": "Może to naprawić administrator tablicy.",
2570
+ "account_admin": "Może to naprawić administrator konta.",
2571
+ "operator": "Może to naprawić tylko operator wdrożenia."
2572
+ },
2573
+ "fullSurface": "Możesz to naprawić sam: zmień rolę u góry panelu bocznego."
2574
+ },
2563
2575
  "spendWarningBanner": {
2564
2576
  "title": "Osiągnięto limit wydatków, wykonywanie agentów wstrzymane",
2565
2577
  "body": "Wydatki na tokeny w tym miesiącu osiągnęły skonfigurowany budżet, dlatego działające potoki zostały wstrzymane, aby uniknąć dalszych kosztów. Wykonywanie wznowi się automatycznie po odnowieniu budżetu w przyszłym miesiącu lub po podniesieniu limitu.",
@@ -7985,6 +7997,7 @@
7985
7997
  "connect": {
7986
7998
  "or": "lub",
7987
7999
  "noneConfigured": "W tym wdrożeniu nie skonfigurowano połączenia z systemem kontroli źródeł. Poproś operatora o skonfigurowanie aplikacji GitHub lub dostępu do GitLab.",
8000
+ "adminRequired": "Tylko administrator tablicy może podłączyć hosta repozytoriów. Poproś o to administratora tej tablicy.",
7988
8001
  "gitlab": {
7989
8002
  "intro": "Wklej osobisty token dostępu GitLab, aby połączyć ten obszar roboczy. cat-factory używa go do wyświetlania Twoich projektów, wypychania gałęzi i otwierania merge requestów.",
7990
8003
  "tokenLabel": "Osobisty token dostępu",
@@ -8002,7 +8015,8 @@
8002
8015
  "onboarding": {
8003
8016
  "title": "Połącz cat-factory z {provider}",
8004
8017
  "titleAny": "Połącz cat-factory ze swoimi repozytoriami",
8005
- "intro": "cat-factory działa, otwierając pull requesty w Twoich repozytoriach. Połącz swojego dostawcę repozytoriów, aby kontynuować."
8018
+ "intro": "cat-factory działa, otwierając pull requesty w Twoich repozytoriach. Połącz swojego dostawcę repozytoriów, aby kontynuować.",
8019
+ "switchBoard": "To nie ta tablica? Przełącz się na inną albo utwórz własną."
8006
8020
  },
8007
8021
  "addService": {
8008
8022
  "intro": "Wybierz istniejące repozytorium {provider}, aby dodać je jako usługę tablicy. Bez inicjowania: repozytorium jest wiązane bez zmian z nową ramką usługi, a uruchamiane na nim zadania celują w to repozytorium.",
@@ -2560,6 +2560,18 @@
2560
2560
  "action": "Bağlantıyı denetle"
2561
2561
  }
2562
2562
  },
2563
+ "infraSetupNotice": {
2564
+ "agentExecutor": {
2565
+ "setup": "Bu panoda aracılar henüz çalışamaz: bağlı bir runner havuzu yok.",
2566
+ "outage": "Aracılar şu anda çalışamıyor: bu panonun runner havuzuna erişilemiyor."
2567
+ },
2568
+ "owner": {
2569
+ "workspace_admin": "Bir pano yöneticisi bunu düzeltebilir.",
2570
+ "account_admin": "Bir hesap yöneticisi bunu düzeltebilir.",
2571
+ "operator": "Bunu yalnızca dağıtımın operatörü düzeltebilir."
2572
+ },
2573
+ "fullSurface": "Bunu kendiniz düzeltebilirsiniz: kenar çubuğunun üstünden rolünüzü değiştirin."
2574
+ },
2563
2575
  "spendWarningBanner": {
2564
2576
  "title": "Harcama limitine ulaşıldı, ajan yürütmesi duraklatıldı",
2565
2577
  "body": "Bu ayki belirteç harcaması yapılandırılan bütçeye ulaştı, bu yüzden daha fazla maliyeti önlemek için çalışan boru hatları duraklatıldı. Yürütme, bütçe gelecek ay yenilendiğinde veya limit yükseltildiğinde otomatik olarak devam eder.",
@@ -7985,6 +7997,7 @@
7985
7997
  "connect": {
7986
7998
  "or": "veya",
7987
7999
  "noneConfigured": "Bu dağıtım için yapılandırılmış bir kaynak denetimi bağlantısı yok. Bir operatörden GitHub App veya GitLab erişimi kurmasını isteyin.",
8000
+ "adminRequired": "Bir depo sağlayıcısını yalnızca pano yöneticisi bağlayabilir. Bu panonun bir yöneticisinden isteyin.",
7988
8001
  "gitlab": {
7989
8002
  "intro": "Bu çalışma alanını bağlamak için bir GitLab kişisel erişim belirteci yapıştırın. cat-factory bunu projelerinizi listelemek, dal göndermek ve merge request açmak için kullanır.",
7990
8003
  "tokenLabel": "Kişisel erişim belirteci",
@@ -8002,7 +8015,8 @@
8002
8015
  "onboarding": {
8003
8016
  "title": "cat-factory'yi {provider}'a bağla",
8004
8017
  "titleAny": "cat-factory'yi depolarınıza bağlayın",
8005
- "intro": "cat-factory, depolarınızda pull request'ler açarak çalışır. Devam etmek için depo sağlayıcınızı bağlayın."
8018
+ "intro": "cat-factory, depolarınızda pull request'ler açarak çalışır. Devam etmek için depo sağlayıcınızı bağlayın.",
8019
+ "switchBoard": "Aradığınız pano bu değil mi? Başka bir panoya geçin veya kendinizinkini oluşturun."
8006
8020
  },
8007
8021
  "addService": {
8008
8022
  "intro": "Pano servisi olarak eklenecek mevcut bir {provider} deposu seçin. Bootstrap yok: depo olduğu gibi yeni bir servis çerçevesine bağlanır ve üzerinde çalıştırdığınız görevler o depoyu hedefler.",
@@ -2560,6 +2560,18 @@
2560
2560
  "action": "Перевірити з’єднання"
2561
2561
  }
2562
2562
  },
2563
+ "infraSetupNotice": {
2564
+ "agentExecutor": {
2565
+ "setup": "На цій дошці агенти ще не можуть працювати: не підключено жодного пулу раннерів.",
2566
+ "outage": "Агенти зараз не можуть працювати: пул раннерів цієї дошки недоступний."
2567
+ },
2568
+ "owner": {
2569
+ "workspace_admin": "Це може виправити адміністратор дошки.",
2570
+ "account_admin": "Це може виправити адміністратор облікового запису.",
2571
+ "operator": "Це може виправити лише оператор розгортання."
2572
+ },
2573
+ "fullSurface": "Ви можете виправити це самі: змініть роль угорі бічної панелі."
2574
+ },
2563
2575
  "spendWarningBanner": {
2564
2576
  "title": "Досягнуто ліміту витрат, виконання агентів призупинено",
2565
2577
  "body": "Витрати на токени цього місяця досягли налаштованого бюджету, тому запущені конвеєри було призупинено, щоб уникнути подальших витрат. Виконання відновиться автоматично, коли бюджет оновиться наступного місяця, або щойно ліміт буде підвищено.",
@@ -7985,6 +7997,7 @@
7985
7997
  "connect": {
7986
7998
  "or": "або",
7987
7999
  "noneConfigured": "Для цього розгортання не налаштовано підключення до системи керування кодом. Попросіть оператора налаштувати застосунок GitHub або доступ до GitLab.",
8000
+ "adminRequired": "Підключити хост репозиторіїв може лише адміністратор дошки. Попросіть про це адміністратора цієї дошки.",
7988
8001
  "gitlab": {
7989
8002
  "intro": "Вставте персональний токен доступу GitLab, щоб під'єднати цей робочий простір. cat-factory використовує його, щоб отримувати перелік ваших проєктів, надсилати гілки та відкривати запити на злиття.",
7990
8003
  "tokenLabel": "Персональний токен доступу",
@@ -8002,7 +8015,8 @@
8002
8015
  "onboarding": {
8003
8016
  "title": "Підключіть cat-factory до {provider}",
8004
8017
  "titleAny": "Підключіть cat-factory до ваших репозиторіїв",
8005
- "intro": "cat-factory працює, відкриваючи pull requests у ваших репозиторіях. Під'єднайте свого постачальника репозиторіїв, щоб продовжити."
8018
+ "intro": "cat-factory працює, відкриваючи pull requests у ваших репозиторіях. Під'єднайте свого постачальника репозиторіїв, щоб продовжити.",
8019
+ "switchBoard": "Не та дошка? Перейдіть на іншу або створіть власну."
8006
8020
  },
8007
8021
  "addService": {
8008
8022
  "intro": "Виберіть наявний репозиторій {provider}, щоб додати його як сервіс дошки. Без початкового налаштування: репозиторій прив’язується як є до нового фрейму сервісу, а задачі, які ви на ньому запускаєте, націлені на цей репозиторій.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.310.3",
3
+ "version": "0.311.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.364.0",
21
+ "@cat-factory/contracts": "0.365.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",