@cat-factory/app 0.46.11 → 0.47.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.
Files changed (42) hide show
  1. package/app/components/auth/AuthGate.vue +3 -2
  2. package/app/components/auth/LoginScreen.vue +172 -32
  3. package/app/components/auth/ResetPasswordScreen.vue +16 -13
  4. package/app/components/auth/UserMenu.vue +3 -2
  5. package/app/components/layout/AccountDeploymentSettings.vue +307 -37
  6. package/app/components/layout/AccountFragmentSettings.vue +2 -3
  7. package/app/components/layout/AccountTeamSettings.vue +75 -45
  8. package/app/components/layout/AiProvidersBanner.vue +11 -11
  9. package/app/components/layout/BoardSwitcher.vue +63 -29
  10. package/app/components/layout/CommandBar.vue +68 -54
  11. package/app/components/layout/GitHubPatBanner.vue +13 -6
  12. package/app/components/layout/IntegrationBackTitle.vue +4 -1
  13. package/app/components/layout/IntegrationsHub.vue +67 -49
  14. package/app/components/layout/NotificationsInbox.vue +48 -19
  15. package/app/components/layout/PersonalSetupModal.vue +26 -16
  16. package/app/components/layout/ProviderConfigBanner.vue +14 -9
  17. package/app/components/layout/SideBar.vue +20 -18
  18. package/app/components/layout/SpendWarningBanner.vue +16 -16
  19. package/app/components/providers/ApiKeysSection.vue +27 -3
  20. package/app/components/providers/PersonalSubscriptionSection.vue +18 -4
  21. package/app/components/providers/SignInRequiredNotice.vue +19 -0
  22. package/app/components/settings/AccountSettingsPanel.vue +9 -6
  23. package/app/components/settings/IssueTrackerPanel.vue +114 -53
  24. package/app/components/settings/LocalModeSettingsPanel.vue +60 -28
  25. package/app/components/settings/LocalModelEndpointsPanel.vue +72 -27
  26. package/app/components/settings/MergeThresholdsPanel.vue +93 -45
  27. package/app/components/settings/ModelConfigurationPanel.vue +62 -36
  28. package/app/components/settings/ObservabilityConnectionPanel.vue +65 -35
  29. package/app/components/settings/OpenRouterCatalogPanel.vue +70 -40
  30. package/app/components/settings/ProviderConnectionPanel.vue +115 -61
  31. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +9 -12
  32. package/app/components/settings/UserSecretsSection.vue +37 -18
  33. package/app/components/settings/WorkspaceSettingsPanel.vue +114 -62
  34. package/app/composables/api/auth.ts +7 -0
  35. package/app/stores/auth.ts +25 -1
  36. package/app/types/accountSettings.ts +4 -0
  37. package/i18n/locales/en.json +908 -0
  38. package/i18n/locales/es.json +899 -0
  39. package/i18n/locales/fr.json +899 -0
  40. package/i18n/locales/pl.json +899 -0
  41. package/i18n/locales/uk.json +899 -0
  42. package/package.json +2 -2
@@ -7,6 +7,7 @@ import LoginScreen from '~/components/auth/LoginScreen.vue'
7
7
  // inside the default slot, so it only fires once the user is allowed in.
8
8
  const auth = useAuthStore()
9
9
  const route = useRoute()
10
+ const { t } = useI18n()
10
11
 
11
12
  // The password-reset page is public: a recipient of an emailed reset link is signed
12
13
  // out, so it must render even when auth is required and there's no user.
@@ -21,12 +22,12 @@ onMounted(() => auth.bootstrap())
21
22
  class="flex h-screen w-screen flex-col items-center justify-center gap-3 bg-slate-950 text-slate-400"
22
23
  >
23
24
  <UIcon name="i-lucide-loader" class="h-8 w-8 animate-spin" />
24
- <span class="text-sm">Loading…</span>
25
+ <span class="text-sm">{{ t('auth.gate.loading') }}</span>
25
26
  </div>
26
27
 
27
28
  <slot v-else-if="isPublicRoute" />
28
29
 
29
- <LoginScreen v-else-if="auth.required && !auth.user" />
30
+ <LoginScreen v-else-if="auth.needsLogin" />
30
31
 
31
32
  <slot v-else />
32
33
  </template>
@@ -1,8 +1,73 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref } from 'vue'
2
+ import { computed, ref, watch } from 'vue'
3
3
  import { apiErrorEnvelope } from '~/composables/api/errors'
4
4
 
5
5
  const auth = useAuthStore()
6
+ const { t } = useI18n()
7
+
8
+ // Local-mode source-control PAT login. GitHub/GitLab are brand names (kept verbatim across
9
+ // locales), as are the token-settings URLs, so they're inline constants rather than catalog
10
+ // keys — same convention as the provider descriptors in ApiKeysSection. The actual link
11
+ // prefers the server's scopes-preselected deep link (`patLogin.setupUrls`); these are the
12
+ // fallback when it's absent.
13
+ type PatProvider = 'github' | 'gitlab'
14
+ const PROVIDER_LABELS: Record<PatProvider, string> = { github: 'GitHub', gitlab: 'GitLab' }
15
+ const PROVIDER_ICONS: Record<PatProvider, string> = {
16
+ github: 'i-lucide-github',
17
+ gitlab: 'i-lucide-gitlab',
18
+ }
19
+ // Fallback token-creation pages, used only if the server didn't advertise a deep link.
20
+ const PROVIDER_TOKEN_URLS: Record<PatProvider, string> = {
21
+ github: 'https://github.com/settings/tokens/new',
22
+ gitlab: 'https://gitlab.com/-/user_settings/personal_access_tokens',
23
+ }
24
+
25
+ const patLoginCfg = computed(() => auth.localMode?.patLogin)
26
+ const configuredProviders = computed<PatProvider[]>(
27
+ () => (patLoginCfg.value?.configured ?? []) as PatProvider[],
28
+ )
29
+ const availableProviders = computed<PatProvider[]>(
30
+ () => (patLoginCfg.value?.available ?? []) as PatProvider[],
31
+ )
32
+ const showLocalLogin = computed(() => availableProviders.value.length > 0)
33
+
34
+ const patProvider = ref<PatProvider>('github')
35
+ const patToken = ref('')
36
+ const patBusy = ref(false)
37
+ const patError = ref<string | null>(null)
38
+
39
+ // Keep the picker on an actually-available provider.
40
+ watch(
41
+ availableProviders,
42
+ (list) => {
43
+ if (list.length && !list.includes(patProvider.value)) patProvider.value = list[0]!
44
+ },
45
+ { immediate: true },
46
+ )
47
+
48
+ const patProviderItems = computed(() =>
49
+ availableProviders.value.map((p) => ({ label: PROVIDER_LABELS[p], value: p })),
50
+ )
51
+
52
+ // Prefer the server's scopes-preselected deep link (it owns the per-provider scopes);
53
+ // fall back to the plain token page if it wasn't advertised.
54
+ const tokenCreateUrl = computed(
55
+ () => patLoginCfg.value?.setupUrls?.[patProvider.value] ?? PROVIDER_TOKEN_URLS[patProvider.value],
56
+ )
57
+
58
+ /** One-click (configured PAT) or pasted-token sign-in; reloads so the app boots signed in. */
59
+ async function submitPat(provider: PatProvider, token?: string) {
60
+ patError.value = null
61
+ patBusy.value = true
62
+ try {
63
+ await auth.patLogin(token ? { provider, token } : { provider })
64
+ if (typeof window !== 'undefined') window.location.assign(window.location.pathname)
65
+ } catch (e) {
66
+ patError.value = apiErrorEnvelope(e)?.message ?? t('auth.localMode.failed')
67
+ } finally {
68
+ patBusy.value = false
69
+ }
70
+ }
6
71
 
7
72
  // An invite token may ride in on the URL (?invite=…) — it flows through the OAuth
8
73
  // redirect and the password signup so a brand-new user can join the org on first login.
@@ -41,8 +106,7 @@ async function submitPassword() {
41
106
  // Reload so the app boots with the new session.
42
107
  if (typeof window !== 'undefined') window.location.assign(window.location.pathname)
43
108
  } catch (e) {
44
- error.value =
45
- apiErrorEnvelope(e)?.message ?? 'Sign-in failed. Check your details and try again.'
109
+ error.value = apiErrorEnvelope(e)?.message ?? t('auth.login.signInFailed')
46
110
  } finally {
47
111
  busy.value = false
48
112
  }
@@ -56,7 +120,7 @@ async function submitForgot() {
56
120
  forgotSent.value = true
57
121
  } catch {
58
122
  // The request endpoint is generic by design; only an infra error lands here.
59
- error.value = 'Something went wrong. Please try again.'
123
+ error.value = t('auth.login.genericError')
60
124
  } finally {
61
125
  busy.value = false
62
126
  }
@@ -81,15 +145,81 @@ const showOAuthDivider = computed(
81
145
  >
82
146
  <div class="mb-6 text-center">
83
147
  <UIcon name="i-lucide-layout-dashboard" class="mx-auto mb-3 h-10 w-10 text-indigo-400" />
84
- <h1 class="mb-1 text-lg font-semibold text-white">Architecture Board</h1>
148
+ <h1 class="mb-1 text-lg font-semibold text-white">{{ t('auth.login.appTitle') }}</h1>
85
149
  <p class="text-sm text-slate-400">
86
- <template v-if="mode === 'forgot'">Reset your password.</template>
150
+ <template v-if="mode === 'forgot'">{{ t('auth.login.forgotSubtitle') }}</template>
87
151
  <template v-else>{{
88
- invite ? 'Accept your invitation to continue.' : 'Sign in to continue.'
152
+ invite ? t('auth.login.inviteSubtitle') : t('auth.login.subtitle')
89
153
  }}</template>
90
154
  </p>
91
155
  </div>
92
156
 
157
+ <!-- Local mode: sign in with a source-control PAT (no OAuth round-trip needed) -->
158
+ <div v-if="showLocalLogin && mode !== 'forgot'" class="space-y-3">
159
+ <!-- One-click: a PAT is already configured server-side -->
160
+ <UButton
161
+ v-for="p in configuredProviders"
162
+ :key="p"
163
+ block
164
+ size="lg"
165
+ color="primary"
166
+ :icon="PROVIDER_ICONS[p]"
167
+ :loading="patBusy"
168
+ @click="submitPat(p)"
169
+ >
170
+ {{ t('auth.localMode.continueWith', { provider: PROVIDER_LABELS[p] }) }}
171
+ </UButton>
172
+
173
+ <!-- Enter a PAT inline -->
174
+ <form class="space-y-2" @submit.prevent="submitPat(patProvider, patToken.trim())">
175
+ <p class="text-xs font-medium text-slate-400">{{ t('auth.localMode.enterPatTitle') }}</p>
176
+ <USelect
177
+ v-if="patProviderItems.length > 1"
178
+ v-model="patProvider"
179
+ :items="patProviderItems"
180
+ size="lg"
181
+ class="w-full"
182
+ />
183
+ <UTextarea
184
+ v-model="patToken"
185
+ :rows="2"
186
+ :placeholder="
187
+ t('auth.localMode.tokenPlaceholder', { provider: PROVIDER_LABELS[patProvider] })
188
+ "
189
+ class="w-full font-mono"
190
+ />
191
+ <div class="flex items-center justify-between gap-2">
192
+ <a
193
+ :href="tokenCreateUrl"
194
+ target="_blank"
195
+ rel="noopener noreferrer"
196
+ class="text-xs text-indigo-400 hover:underline"
197
+ >
198
+ {{ t('auth.localMode.createToken', { provider: PROVIDER_LABELS[patProvider] }) }}
199
+ </a>
200
+ <UButton
201
+ size="lg"
202
+ color="neutral"
203
+ variant="subtle"
204
+ type="submit"
205
+ :loading="patBusy"
206
+ :disabled="!patToken.trim()"
207
+ >
208
+ {{ t('auth.localMode.submit') }}
209
+ </UButton>
210
+ </div>
211
+ </form>
212
+ <p v-if="patError" class="text-sm text-rose-400">{{ patError }}</p>
213
+ </div>
214
+
215
+ <div
216
+ v-if="showLocalLogin && auth.providers.password && mode !== 'forgot'"
217
+ class="my-4 flex items-center gap-3 text-xs text-slate-500"
218
+ >
219
+ <span class="h-px flex-1 bg-slate-800" /> {{ t('auth.localMode.orDivider') }}
220
+ <span class="h-px flex-1 bg-slate-800" />
221
+ </div>
222
+
93
223
  <!-- OAuth providers -->
94
224
  <div v-if="mode !== 'forgot'" class="space-y-2">
95
225
  <UButton
@@ -100,7 +230,7 @@ const showOAuthDivider = computed(
100
230
  icon="i-lucide-github"
101
231
  @click="auth.login(invite)"
102
232
  >
103
- Continue with GitHub
233
+ {{ t('auth.login.continueWithGithub') }}
104
234
  </UButton>
105
235
  <UButton
106
236
  v-if="auth.providers.google"
@@ -111,7 +241,7 @@ const showOAuthDivider = computed(
111
241
  icon="i-lucide-mail"
112
242
  @click="auth.loginWithGoogle(invite)"
113
243
  >
114
- Continue with Google
244
+ {{ t('auth.login.continueWithGoogle') }}
115
245
  </UButton>
116
246
  </div>
117
247
 
@@ -119,7 +249,8 @@ const showOAuthDivider = computed(
119
249
  v-if="showOAuthDivider && mode !== 'forgot'"
120
250
  class="my-4 flex items-center gap-3 text-xs text-slate-500"
121
251
  >
122
- <span class="h-px flex-1 bg-slate-800" /> or <span class="h-px flex-1 bg-slate-800" />
252
+ <span class="h-px flex-1 bg-slate-800" /> {{ t('auth.login.or') }}
253
+ <span class="h-px flex-1 bg-slate-800" />
123
254
  </div>
124
255
 
125
256
  <!-- Email / password -->
@@ -131,7 +262,7 @@ const showOAuthDivider = computed(
131
262
  <UInput
132
263
  v-if="mode === 'signup'"
133
264
  v-model="name"
134
- placeholder="Name (optional)"
265
+ :placeholder="t('auth.login.namePlaceholder')"
135
266
  icon="i-lucide-user"
136
267
  size="lg"
137
268
  class="w-full"
@@ -140,7 +271,7 @@ const showOAuthDivider = computed(
140
271
  v-model="email"
141
272
  type="email"
142
273
  required
143
- placeholder="Email"
274
+ :placeholder="t('auth.login.emailPlaceholder')"
144
275
  icon="i-lucide-at-sign"
145
276
  size="lg"
146
277
  class="w-full"
@@ -149,36 +280,46 @@ const showOAuthDivider = computed(
149
280
  v-model="password"
150
281
  type="password"
151
282
  required
152
- placeholder="Password"
283
+ :placeholder="t('auth.login.passwordPlaceholder')"
153
284
  icon="i-lucide-lock"
154
285
  size="lg"
155
286
  class="w-full"
156
287
  />
157
288
  <p v-if="error" class="text-sm text-rose-400">{{ error }}</p>
158
289
  <UButton block size="lg" color="primary" type="submit" :loading="busy">
159
- {{ mode === 'signup' ? 'Create account' : 'Sign in' }}
290
+ {{ mode === 'signup' ? t('auth.login.createAccount') : t('auth.login.signIn') }}
160
291
  </UButton>
161
292
  <p class="text-center text-xs text-slate-400">
162
293
  <template v-if="mode === 'login'">
163
- Need an account?
164
- <button
165
- type="button"
166
- class="text-indigo-400 hover:underline"
167
- @click="setMode('signup')"
168
- >
169
- Sign up
170
- </button>
294
+ <i18n-t keypath="auth.login.needAccount" tag="span" scope="global">
295
+ <template #signUp>
296
+ <button
297
+ type="button"
298
+ class="text-indigo-400 hover:underline"
299
+ @click="setMode('signup')"
300
+ >
301
+ {{ t('auth.login.signUp') }}
302
+ </button>
303
+ </template>
304
+ </i18n-t>
171
305
  </template>
172
306
  <template v-else>
173
- Already have an account?
174
- <button type="button" class="text-indigo-400 hover:underline" @click="setMode('login')">
175
- Sign in
176
- </button>
307
+ <i18n-t keypath="auth.login.haveAccount" tag="span" scope="global">
308
+ <template #signIn>
309
+ <button
310
+ type="button"
311
+ class="text-indigo-400 hover:underline"
312
+ @click="setMode('login')"
313
+ >
314
+ {{ t('auth.login.signIn') }}
315
+ </button>
316
+ </template>
317
+ </i18n-t>
177
318
  </template>
178
319
  </p>
179
320
  <p v-if="mode === 'login'" class="text-center text-xs text-slate-400">
180
321
  <button type="button" class="text-indigo-400 hover:underline" @click="setMode('forgot')">
181
- Forgot password?
322
+ {{ t('auth.login.forgotPassword') }}
182
323
  </button>
183
324
  </p>
184
325
  </form>
@@ -191,8 +332,7 @@ const showOAuthDivider = computed(
191
332
  >
192
333
  <template v-if="forgotSent">
193
334
  <p class="text-sm text-slate-300">
194
- If an account exists for that email, a password reset link is on its way. Check your
195
- inbox.
335
+ {{ t('auth.login.forgotSent') }}
196
336
  </p>
197
337
  </template>
198
338
  <template v-else>
@@ -200,19 +340,19 @@ const showOAuthDivider = computed(
200
340
  v-model="email"
201
341
  type="email"
202
342
  required
203
- placeholder="Email"
343
+ :placeholder="t('auth.login.emailPlaceholder')"
204
344
  icon="i-lucide-at-sign"
205
345
  size="lg"
206
346
  class="w-full"
207
347
  />
208
348
  <p v-if="error" class="text-sm text-rose-400">{{ error }}</p>
209
349
  <UButton block size="lg" color="primary" type="submit" :loading="busy">
210
- Send reset link
350
+ {{ t('auth.login.sendResetLink') }}
211
351
  </UButton>
212
352
  </template>
213
353
  <p class="text-center text-xs text-slate-400">
214
354
  <button type="button" class="text-indigo-400 hover:underline" @click="setMode('login')">
215
- Back to sign in
355
+ {{ t('auth.login.backToSignIn') }}
216
356
  </button>
217
357
  </p>
218
358
  </form>
@@ -5,6 +5,7 @@ import { computed, ref } from 'vue'
5
5
  // (`/reset-password?token=…`). It is a public route (see AuthGate), so a recipient who
6
6
  // is signed out can still set a new password. On success we send them to the login.
7
7
  const auth = useAuthStore()
8
+ const { t } = useI18n()
8
9
 
9
10
  const token = computed(() => {
10
11
  if (typeof window === 'undefined') return ''
@@ -20,11 +21,11 @@ const done = ref(false)
20
21
  async function submit() {
21
22
  error.value = null
22
23
  if (password.value.length < 8) {
23
- error.value = 'Password must be at least 8 characters.'
24
+ error.value = t('auth.resetPassword.errorTooShort')
24
25
  return
25
26
  }
26
27
  if (password.value !== confirm.value) {
27
- error.value = 'Passwords do not match.'
28
+ error.value = t('auth.resetPassword.errorMismatch')
28
29
  return
29
30
  }
30
31
  busy.value = true
@@ -34,7 +35,7 @@ async function submit() {
34
35
  } catch (e) {
35
36
  error.value =
36
37
  (e as { data?: { error?: { message?: string } } })?.data?.error?.message ??
37
- 'This password reset link is invalid or has expired.'
38
+ t('auth.resetPassword.errorInvalidLink')
38
39
  } finally {
39
40
  busy.value = false
40
41
  }
@@ -52,23 +53,25 @@ function goToLogin() {
52
53
  >
53
54
  <div class="mb-6 text-center">
54
55
  <UIcon name="i-lucide-key-round" class="mx-auto mb-3 h-10 w-10 text-indigo-400" />
55
- <h1 class="mb-1 text-lg font-semibold text-white">Reset password</h1>
56
- <p class="text-sm text-slate-400">Choose a new password for your account.</p>
56
+ <h1 class="mb-1 text-lg font-semibold text-white">{{ t('auth.resetPassword.title') }}</h1>
57
+ <p class="text-sm text-slate-400">{{ t('auth.resetPassword.subtitle') }}</p>
57
58
  </div>
58
59
 
59
60
  <template v-if="done">
60
61
  <p class="mb-4 text-sm text-slate-300">
61
- Your password has been reset. You can now sign in with your new password.
62
+ {{ t('auth.resetPassword.doneBody') }}
62
63
  </p>
63
- <UButton block size="lg" color="primary" @click="goToLogin">Go to sign in</UButton>
64
+ <UButton block size="lg" color="primary" @click="goToLogin">{{
65
+ t('auth.resetPassword.goToSignIn')
66
+ }}</UButton>
64
67
  </template>
65
68
 
66
69
  <template v-else-if="!token">
67
70
  <p class="mb-4 text-sm text-rose-400">
68
- This reset link is missing its token. Request a new link from the sign-in screen.
71
+ {{ t('auth.resetPassword.missingToken') }}
69
72
  </p>
70
73
  <UButton block size="lg" color="neutral" variant="subtle" @click="goToLogin">
71
- Back to sign in
74
+ {{ t('auth.resetPassword.backToSignIn') }}
72
75
  </UButton>
73
76
  </template>
74
77
 
@@ -77,7 +80,7 @@ function goToLogin() {
77
80
  v-model="password"
78
81
  type="password"
79
82
  required
80
- placeholder="New password"
83
+ :placeholder="t('auth.resetPassword.newPasswordPlaceholder')"
81
84
  icon="i-lucide-lock"
82
85
  size="lg"
83
86
  class="w-full"
@@ -86,18 +89,18 @@ function goToLogin() {
86
89
  v-model="confirm"
87
90
  type="password"
88
91
  required
89
- placeholder="Confirm new password"
92
+ :placeholder="t('auth.resetPassword.confirmPasswordPlaceholder')"
90
93
  icon="i-lucide-lock"
91
94
  size="lg"
92
95
  class="w-full"
93
96
  />
94
97
  <p v-if="error" class="text-sm text-rose-400">{{ error }}</p>
95
98
  <UButton block size="lg" color="primary" type="submit" :loading="busy">
96
- Reset password
99
+ {{ t('auth.resetPassword.submit') }}
97
100
  </UButton>
98
101
  <p class="text-center text-xs text-slate-400">
99
102
  <button type="button" class="text-indigo-400 hover:underline" @click="goToLogin">
100
- Back to sign in
103
+ {{ t('auth.resetPassword.backToSignIn') }}
101
104
  </button>
102
105
  </p>
103
106
  </form>
@@ -4,20 +4,21 @@ import type { DropdownMenuItem } from '@nuxt/ui'
4
4
  // Signed-in identity + per-user actions, shown in the sidebar when auth is enabled.
5
5
  const auth = useAuthStore()
6
6
  const ui = useUiStore()
7
+ const { t } = useI18n()
7
8
 
8
9
  const items = computed<DropdownMenuItem[][]>(() => [
9
10
  [
10
11
  {
11
12
  // The user-scoped "My setup" hub (personal GitHub token, local runners, personal
12
13
  // subscriptions) — kept out of the workspace Integrations hub, reachable here.
13
- label: 'My setup',
14
+ label: t('auth.userMenu.mySetup'),
14
15
  icon: 'i-lucide-user-cog',
15
16
  onSelect: () => ui.openPersonalSetup(),
16
17
  },
17
18
  ],
18
19
  [
19
20
  {
20
- label: 'Sign out',
21
+ label: t('auth.userMenu.signOut'),
21
22
  icon: 'i-lucide-log-out',
22
23
  onSelect: () => auth.logout(),
23
24
  },