@cat-factory/app 0.102.0 → 0.104.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/app/components/auth/AuthGate.vue +3 -0
- package/app/components/auth/BackendMisconfiguredScreen.vue +55 -0
- package/app/components/layout/AccountDeploymentSettings.vue +5 -1
- package/app/components/layout/AccountModelPolicySettings.vue +247 -0
- package/app/components/layout/AccountTeamSettings.vue +11 -0
- package/app/composables/usePipelineErrorToast.ts +2 -0
- package/app/stores/auth.ts +25 -1
- package/app/types/accountSettings.ts +5 -0
- package/i18n/locales/de.json +69 -1
- package/i18n/locales/en.json +69 -1
- package/i18n/locales/es.json +69 -1
- package/i18n/locales/fr.json +69 -1
- package/i18n/locales/he.json +69 -1
- package/i18n/locales/it.json +69 -1
- package/i18n/locales/ja.json +69 -1
- package/i18n/locales/pl.json +69 -1
- package/i18n/locales/tr.json +69 -1
- package/i18n/locales/uk.json +69 -1
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
|
+
import BackendMisconfiguredScreen from '~/components/auth/BackendMisconfiguredScreen.vue'
|
|
3
4
|
import LoginScreen from '~/components/auth/LoginScreen.vue'
|
|
4
5
|
|
|
5
6
|
// Resolves auth state once on mount, then either renders the app (auth off, or
|
|
@@ -25,6 +26,8 @@ onMounted(() => auth.bootstrap())
|
|
|
25
26
|
<span class="text-sm">{{ t('auth.gate.loading') }}</span>
|
|
26
27
|
</div>
|
|
27
28
|
|
|
29
|
+
<BackendMisconfiguredScreen v-else-if="auth.isMisconfigured" />
|
|
30
|
+
|
|
28
31
|
<slot v-else-if="isPublicRoute" />
|
|
29
32
|
|
|
30
33
|
<LoginScreen v-else-if="auth.needsLogin" />
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ConfigProblem } from '@cat-factory/contracts'
|
|
3
|
+
|
|
4
|
+
// Shown when the backend booted into its misconfiguration fallback: it couldn't start normally
|
|
5
|
+
// because one or more mandatory environment variables / bindings are missing or invalid. We list
|
|
6
|
+
// each one with what it is for and how to fill it, so the developer can fix their env and reload
|
|
7
|
+
// rather than staring at a generic "can't reach the backend" panel. The `problems` never carry a
|
|
8
|
+
// secret value — only the variable name, its meaning, and the remedy.
|
|
9
|
+
const auth = useAuthStore()
|
|
10
|
+
const { t } = useI18n()
|
|
11
|
+
|
|
12
|
+
const problems = computed<ConfigProblem[]>(() => auth.misconfigured?.problems ?? [])
|
|
13
|
+
|
|
14
|
+
function reload() {
|
|
15
|
+
window.location.reload()
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<div
|
|
21
|
+
class="flex h-screen w-screen flex-col items-center justify-center bg-slate-950 p-6 text-slate-200"
|
|
22
|
+
>
|
|
23
|
+
<div class="w-full max-w-2xl">
|
|
24
|
+
<div class="mb-6 text-center">
|
|
25
|
+
<UIcon name="i-lucide-server-cog" class="mx-auto mb-3 h-10 w-10 text-amber-400" />
|
|
26
|
+
<h1 class="text-lg font-semibold">{{ t('app.misconfigured.title') }}</h1>
|
|
27
|
+
<p class="mx-auto mt-2 max-w-lg text-sm text-slate-400">
|
|
28
|
+
{{ t('app.misconfigured.intro') }}
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<ul class="space-y-3">
|
|
33
|
+
<li
|
|
34
|
+
v-for="problem in problems"
|
|
35
|
+
:key="problem.key"
|
|
36
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-4"
|
|
37
|
+
>
|
|
38
|
+
<code class="text-sm font-semibold text-amber-300">{{ problem.key }}</code>
|
|
39
|
+
<p class="mt-1 text-sm text-slate-300">{{ problem.summary }}</p>
|
|
40
|
+
<p class="mt-2 text-sm text-slate-400">
|
|
41
|
+
<span class="font-medium text-slate-300">{{ t('app.misconfigured.howToFix') }}</span>
|
|
42
|
+
{{ problem.remedy }}
|
|
43
|
+
</p>
|
|
44
|
+
</li>
|
|
45
|
+
</ul>
|
|
46
|
+
|
|
47
|
+
<div class="mt-6 flex items-center justify-center gap-3">
|
|
48
|
+
<UButton color="primary" icon="i-lucide-rotate-ccw" @click="reload">
|
|
49
|
+
{{ t('app.misconfigured.reload') }}
|
|
50
|
+
</UButton>
|
|
51
|
+
</div>
|
|
52
|
+
<p class="mt-4 text-center text-xs text-slate-500">{{ t('app.misconfigured.hint') }}</p>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
@@ -129,7 +129,11 @@ async function saveStorage() {
|
|
|
129
129
|
...(cs.forcePathStyle ? { forcePathStyle: true } : {}),
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
-
|
|
132
|
+
// `config` fully REPLACES the stored non-secret config, so carry the rest forward
|
|
133
|
+
// (e.g. the model-family policy) — only `contentStorage` is edited here.
|
|
134
|
+
const input: Parameters<typeof store.save>[1] = {
|
|
135
|
+
config: { ...store.view?.config, contentStorage: config },
|
|
136
|
+
}
|
|
133
137
|
if (backend === 's3') {
|
|
134
138
|
const id = cs.accessKeyId.trim()
|
|
135
139
|
const key = cs.secretAccessKey.trim()
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import { MODEL_FAMILY_POLICY_PRESETS } from '@cat-factory/contracts'
|
|
4
|
+
import type {
|
|
5
|
+
AccountRegion,
|
|
6
|
+
ModelFamily,
|
|
7
|
+
ModelFamilyPolicy,
|
|
8
|
+
ModelPolicyMode,
|
|
9
|
+
} from '~/types/accountSettings'
|
|
10
|
+
|
|
11
|
+
// Account-wide model-family allow/block policy (admin only). Constrains which LLM families
|
|
12
|
+
// the account's teams may run; a residency-guaranteed route (`trustedProviders`) can exempt
|
|
13
|
+
// an otherwise-blocked family. Region-grouped built-in presets are one-click templates the
|
|
14
|
+
// admin applies into the editable policy. Mounted only where the deployment supports it
|
|
15
|
+
// (hosted / mothership — never plain local mode), gated by the parent on the infra flag.
|
|
16
|
+
const props = defineProps<{ accountId: string }>()
|
|
17
|
+
|
|
18
|
+
const store = useAccountSettingsStore()
|
|
19
|
+
const toast = useToast()
|
|
20
|
+
const { t } = useI18n()
|
|
21
|
+
|
|
22
|
+
// The family / region / mode / trusted-provider domains, pinned to the contract unions so a
|
|
23
|
+
// new enum member fails the typecheck here (via the exhaustive label maps below).
|
|
24
|
+
const FAMILIES = [
|
|
25
|
+
'claude',
|
|
26
|
+
'openai',
|
|
27
|
+
'gemini',
|
|
28
|
+
'llama',
|
|
29
|
+
'qwen',
|
|
30
|
+
'kimi',
|
|
31
|
+
'deepseek',
|
|
32
|
+
'glm',
|
|
33
|
+
] as const satisfies readonly ModelFamily[]
|
|
34
|
+
const REGIONS = ['usa', 'europe', 'china', 'other'] as const satisfies readonly AccountRegion[]
|
|
35
|
+
const MODES = ['off', 'blocklist', 'allowlist'] as const satisfies readonly ModelPolicyMode[]
|
|
36
|
+
const TRUSTED_PROVIDERS = ['bedrock'] as const
|
|
37
|
+
|
|
38
|
+
// Exhaustive enum→key maps (drift guard): every member resolves to a static literal `t()`
|
|
39
|
+
// key, so adding a family/region/mode without a label fails the typecheck on the Record.
|
|
40
|
+
const familyLabels = computed<Record<ModelFamily, string>>(() => ({
|
|
41
|
+
claude: t('settings.modelPolicy.families.claude'),
|
|
42
|
+
openai: t('settings.modelPolicy.families.openai'),
|
|
43
|
+
gemini: t('settings.modelPolicy.families.gemini'),
|
|
44
|
+
llama: t('settings.modelPolicy.families.llama'),
|
|
45
|
+
qwen: t('settings.modelPolicy.families.qwen'),
|
|
46
|
+
kimi: t('settings.modelPolicy.families.kimi'),
|
|
47
|
+
deepseek: t('settings.modelPolicy.families.deepseek'),
|
|
48
|
+
glm: t('settings.modelPolicy.families.glm'),
|
|
49
|
+
}))
|
|
50
|
+
const regionLabels = computed<Record<AccountRegion, string>>(() => ({
|
|
51
|
+
usa: t('settings.modelPolicy.regions.usa'),
|
|
52
|
+
europe: t('settings.modelPolicy.regions.europe'),
|
|
53
|
+
china: t('settings.modelPolicy.regions.china'),
|
|
54
|
+
other: t('settings.modelPolicy.regions.other'),
|
|
55
|
+
}))
|
|
56
|
+
const modeLabels = computed<Record<ModelPolicyMode, string>>(() => ({
|
|
57
|
+
off: t('settings.modelPolicy.modes.off'),
|
|
58
|
+
blocklist: t('settings.modelPolicy.modes.blocklist'),
|
|
59
|
+
allowlist: t('settings.modelPolicy.modes.allowlist'),
|
|
60
|
+
}))
|
|
61
|
+
const providerLabels = computed<Record<(typeof TRUSTED_PROVIDERS)[number], string>>(() => ({
|
|
62
|
+
bedrock: t('settings.modelPolicy.providers.bedrock'),
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
const modeItems = computed(() => MODES.map((m) => ({ label: modeLabels.value[m], value: m })))
|
|
66
|
+
const regionItems = computed(() => REGIONS.map((r) => ({ label: regionLabels.value[r], value: r })))
|
|
67
|
+
|
|
68
|
+
// Editable local state (hydrated from the stored policy).
|
|
69
|
+
const mode = ref<ModelPolicyMode>('off')
|
|
70
|
+
const region = ref<AccountRegion>('other')
|
|
71
|
+
const families = ref<ModelFamily[]>([])
|
|
72
|
+
const trusted = ref<string[]>([])
|
|
73
|
+
const saving = ref(false)
|
|
74
|
+
|
|
75
|
+
function hydrate() {
|
|
76
|
+
const p = store.view?.config?.modelPolicy
|
|
77
|
+
mode.value = p?.mode ?? 'off'
|
|
78
|
+
region.value = p?.region ?? 'other'
|
|
79
|
+
families.value = [...(p?.families ?? [])]
|
|
80
|
+
trusted.value = [...(p?.trustedProviders ?? [])]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The presets offered for the currently-selected region.
|
|
84
|
+
const regionPresets = computed(() =>
|
|
85
|
+
MODEL_FAMILY_POLICY_PRESETS.filter((preset) => preset.region === region.value),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
function toggleFamily(fam: ModelFamily) {
|
|
89
|
+
families.value = families.value.includes(fam)
|
|
90
|
+
? families.value.filter((x) => x !== fam)
|
|
91
|
+
: [...families.value, fam]
|
|
92
|
+
}
|
|
93
|
+
function toggleTrusted(provider: string) {
|
|
94
|
+
trusted.value = trusted.value.includes(provider)
|
|
95
|
+
? trusted.value.filter((x) => x !== provider)
|
|
96
|
+
: [...trusted.value, provider]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function applyPreset(policy: ModelFamilyPolicy) {
|
|
100
|
+
mode.value = policy.mode
|
|
101
|
+
families.value = [...policy.families]
|
|
102
|
+
trusted.value = [...policy.trustedProviders]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onMounted(async () => {
|
|
106
|
+
// The sibling deployment-settings panel loads the store on mount too; guard against a
|
|
107
|
+
// double-load being unnecessary by only loading when the view isn't present yet.
|
|
108
|
+
if (!store.view && store.available !== false) {
|
|
109
|
+
try {
|
|
110
|
+
await store.load(props.accountId)
|
|
111
|
+
} catch {
|
|
112
|
+
// The deployment-settings panel surfaces the load error; stay quiet here.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
hydrate()
|
|
116
|
+
})
|
|
117
|
+
watch(() => store.view, hydrate)
|
|
118
|
+
|
|
119
|
+
async function save() {
|
|
120
|
+
const policy: ModelFamilyPolicy = {
|
|
121
|
+
mode: mode.value,
|
|
122
|
+
// Families are irrelevant when the policy is off; keep them so a later re-enable
|
|
123
|
+
// restores the admin's selection rather than silently emptying it.
|
|
124
|
+
families: families.value,
|
|
125
|
+
trustedProviders: trusted.value,
|
|
126
|
+
region: region.value,
|
|
127
|
+
}
|
|
128
|
+
saving.value = true
|
|
129
|
+
try {
|
|
130
|
+
// `config` fully replaces the stored non-secret config, so carry the rest forward.
|
|
131
|
+
await store.save(props.accountId, { config: { ...store.view?.config, modelPolicy: policy } })
|
|
132
|
+
toast.add({
|
|
133
|
+
title: t('settings.modelPolicy.saved'),
|
|
134
|
+
icon: 'i-lucide-check',
|
|
135
|
+
color: 'success',
|
|
136
|
+
})
|
|
137
|
+
} catch (e) {
|
|
138
|
+
toast.add({
|
|
139
|
+
title: t('settings.modelPolicy.saveFailed'),
|
|
140
|
+
description: e instanceof Error ? e.message : String(e),
|
|
141
|
+
color: 'error',
|
|
142
|
+
})
|
|
143
|
+
} finally {
|
|
144
|
+
saving.value = false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
</script>
|
|
148
|
+
|
|
149
|
+
<template>
|
|
150
|
+
<section
|
|
151
|
+
v-if="store.available !== false"
|
|
152
|
+
data-testid="account-model-policy"
|
|
153
|
+
class="space-y-3 border-t border-slate-800 pt-6"
|
|
154
|
+
>
|
|
155
|
+
<div>
|
|
156
|
+
<h4 class="text-sm font-semibold text-slate-200">{{ t('settings.modelPolicy.title') }}</h4>
|
|
157
|
+
<p class="text-[11px] text-slate-400">{{ t('settings.modelPolicy.description') }}</p>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<!-- Region + apply-preset templates -->
|
|
161
|
+
<div class="space-y-2">
|
|
162
|
+
<label class="text-[11px] font-medium text-slate-300">
|
|
163
|
+
{{ t('settings.modelPolicy.regionLabel') }}
|
|
164
|
+
</label>
|
|
165
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
166
|
+
<USelect v-model="region" :items="regionItems" value-key="value" size="sm" />
|
|
167
|
+
</div>
|
|
168
|
+
<div v-if="regionPresets.length" class="flex flex-wrap items-center gap-2">
|
|
169
|
+
<span class="text-[11px] text-slate-400">{{ t('settings.modelPolicy.applyPreset') }}</span>
|
|
170
|
+
<UButton
|
|
171
|
+
v-for="preset in regionPresets"
|
|
172
|
+
:key="preset.id"
|
|
173
|
+
color="neutral"
|
|
174
|
+
variant="subtle"
|
|
175
|
+
size="xs"
|
|
176
|
+
icon="i-lucide-wand-2"
|
|
177
|
+
:title="t(`settings.modelPolicy.presets.${preset.id}.description`)"
|
|
178
|
+
@click="applyPreset(preset.policy)"
|
|
179
|
+
>
|
|
180
|
+
{{ t(`settings.modelPolicy.presets.${preset.id}.label`) }}
|
|
181
|
+
</UButton>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<!-- Mode -->
|
|
186
|
+
<div class="space-y-2">
|
|
187
|
+
<label class="text-[11px] font-medium text-slate-300">
|
|
188
|
+
{{ t('settings.modelPolicy.modeLabel') }}
|
|
189
|
+
</label>
|
|
190
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
191
|
+
<USelect v-model="mode" :items="modeItems" value-key="value" size="sm" />
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<!-- Families -->
|
|
196
|
+
<div v-if="mode !== 'off'" class="space-y-2">
|
|
197
|
+
<label class="text-[11px] font-medium text-slate-300">
|
|
198
|
+
{{
|
|
199
|
+
mode === 'blocklist'
|
|
200
|
+
? t('settings.modelPolicy.familiesBlockLabel')
|
|
201
|
+
: t('settings.modelPolicy.familiesAllowLabel')
|
|
202
|
+
}}
|
|
203
|
+
</label>
|
|
204
|
+
<div class="grid grid-cols-2 gap-1 sm:grid-cols-4">
|
|
205
|
+
<UCheckbox
|
|
206
|
+
v-for="fam in FAMILIES"
|
|
207
|
+
:key="fam"
|
|
208
|
+
:model-value="families.includes(fam)"
|
|
209
|
+
:label="familyLabels[fam]"
|
|
210
|
+
size="sm"
|
|
211
|
+
@update:model-value="toggleFamily(fam)"
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<!-- Trusted (residency-guaranteed) routes -->
|
|
217
|
+
<div v-if="mode !== 'off'" class="space-y-2">
|
|
218
|
+
<label class="text-[11px] font-medium text-slate-300">
|
|
219
|
+
{{ t('settings.modelPolicy.trustedLabel') }}
|
|
220
|
+
</label>
|
|
221
|
+
<p class="text-[11px] text-slate-400">{{ t('settings.modelPolicy.trustedHint') }}</p>
|
|
222
|
+
<div class="grid grid-cols-2 gap-1 sm:grid-cols-4">
|
|
223
|
+
<UCheckbox
|
|
224
|
+
v-for="provider in TRUSTED_PROVIDERS"
|
|
225
|
+
:key="provider"
|
|
226
|
+
:model-value="trusted.includes(provider)"
|
|
227
|
+
:label="providerLabels[provider]"
|
|
228
|
+
size="sm"
|
|
229
|
+
@update:model-value="toggleTrusted(provider)"
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div class="flex gap-2">
|
|
235
|
+
<UButton
|
|
236
|
+
color="primary"
|
|
237
|
+
size="xs"
|
|
238
|
+
icon="i-lucide-save"
|
|
239
|
+
:loading="saving"
|
|
240
|
+
data-testid="account-model-policy-save"
|
|
241
|
+
@click="save"
|
|
242
|
+
>
|
|
243
|
+
{{ t('common.save') }}
|
|
244
|
+
</UButton>
|
|
245
|
+
</div>
|
|
246
|
+
</section>
|
|
247
|
+
</template>
|
|
@@ -4,6 +4,7 @@ import { apiErrorEnvelope } from '~/composables/api/errors'
|
|
|
4
4
|
import type { AccountRole } from '~/types/domain'
|
|
5
5
|
import type { InvitationStatus } from '@cat-factory/contracts'
|
|
6
6
|
import AccountDeploymentSettings from '~/components/layout/AccountDeploymentSettings.vue'
|
|
7
|
+
import AccountModelPolicySettings from '~/components/layout/AccountModelPolicySettings.vue'
|
|
7
8
|
import SecretInput from '~/components/common/SecretInput.vue'
|
|
8
9
|
|
|
9
10
|
// Team settings for an org account: the member roster (with combinable admin /
|
|
@@ -13,11 +14,16 @@ import SecretInput from '~/components/common/SecretInput.vue'
|
|
|
13
14
|
const props = defineProps<{ accountId: string }>()
|
|
14
15
|
|
|
15
16
|
const accounts = useAccountsStore()
|
|
17
|
+
const auth = useAuthStore()
|
|
16
18
|
const toast = useToast()
|
|
17
19
|
const { t, te } = useI18n()
|
|
18
20
|
const { confirmAction, toastDone } = useConfirmAction()
|
|
19
21
|
const busy = ref(false)
|
|
20
22
|
|
|
23
|
+
// The account-wide model-family policy is a hosted / mothership-only control (the backend
|
|
24
|
+
// reports `false` in plain local mode, where there is no account admin to govern).
|
|
25
|
+
const modelPolicySupported = computed(() => auth.infrastructure?.modelPolicy?.supported ?? false)
|
|
26
|
+
|
|
21
27
|
const ROLE_ITEMS = computed<{ label: string; value: AccountRole }[]>(() => [
|
|
22
28
|
{ label: t('layout.accountTeam.roles.admin'), value: 'admin' },
|
|
23
29
|
{ label: t('layout.accountTeam.roles.developer'), value: 'developer' },
|
|
@@ -326,5 +332,10 @@ async function disconnectEmail() {
|
|
|
326
332
|
<section v-if="isAdmin">
|
|
327
333
|
<AccountDeploymentSettings :account-id="accountId" />
|
|
328
334
|
</section>
|
|
335
|
+
|
|
336
|
+
<!-- account-wide model-family allow/block policy (admin-only; hosted/mothership only) -->
|
|
337
|
+
<section v-if="isAdmin && modelPolicySupported">
|
|
338
|
+
<AccountModelPolicySettings :account-id="accountId" />
|
|
339
|
+
</section>
|
|
329
340
|
</div>
|
|
330
341
|
</template>
|
|
@@ -48,6 +48,8 @@ const CONFLICT_TITLE_KEYS: Record<
|
|
|
48
48
|
provision_type_unhandled: 'errors.conflict.title.provision_type_unhandled',
|
|
49
49
|
preset_unsatisfiable: 'errors.conflict.title.preset_unsatisfiable',
|
|
50
50
|
visual_pipeline_no_frontend: 'errors.conflict.title.visual_pipeline_no_frontend',
|
|
51
|
+
model_policy_blocked: 'errors.conflict.title.model_policy_blocked',
|
|
52
|
+
model_policy_unsupported: 'errors.conflict.title.model_policy_unsupported',
|
|
51
53
|
deployer_required_before_tester: 'errors.conflict.title.deployer_required_before_tester',
|
|
52
54
|
}
|
|
53
55
|
|
package/app/stores/auth.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BackendMisconfigured,
|
|
3
|
+
InfrastructureCapabilities,
|
|
4
|
+
LocalModeConfig,
|
|
5
|
+
} from '@cat-factory/contracts'
|
|
2
6
|
import { defineStore } from 'pinia'
|
|
3
7
|
import { computed, ref } from 'vue'
|
|
4
8
|
import type { AuthUser } from '~/types/domain'
|
|
@@ -60,6 +64,14 @@ export const useAuthStore = defineStore(
|
|
|
60
64
|
const autoLoginProvider = ref<'github' | 'gitlab' | null>(null)
|
|
61
65
|
/** True once the initial auth handshake has settled. */
|
|
62
66
|
const ready = ref(false)
|
|
67
|
+
/**
|
|
68
|
+
* Set when the backend answered its boot handshake but reported that it is MISCONFIGURED — it
|
|
69
|
+
* failed to start normally because a mandatory env var / binding is missing, and is serving the
|
|
70
|
+
* fallback backend that lists the problems (each carries only a name + meaning + remedy, never a
|
|
71
|
+
* secret). Present ⇒ the SPA renders the dedicated misconfiguration screen instead of the
|
|
72
|
+
* login/board. Null on a normally-booted backend.
|
|
73
|
+
*/
|
|
74
|
+
const misconfigured = ref<BackendMisconfigured | null>(null)
|
|
63
75
|
/**
|
|
64
76
|
* Mothership mode: the last mothership sign-in failure (node unreachable / rejected session),
|
|
65
77
|
* or null. Set when the post-OAuth connect exchange fails, so the login screen can tell the
|
|
@@ -84,6 +96,9 @@ export const useAuthStore = defineStore(
|
|
|
84
96
|
/** May the app render? True when auth is off, or on with a known user. */
|
|
85
97
|
const isAuthenticated = computed(() => !required.value || user.value !== null)
|
|
86
98
|
|
|
99
|
+
/** Whether the backend reported itself misconfigured (drives the dedicated error screen). */
|
|
100
|
+
const isMisconfigured = computed(() => misconfigured.value !== null)
|
|
101
|
+
|
|
87
102
|
/**
|
|
88
103
|
* Whether the SPA must show the login screen before the board.
|
|
89
104
|
*
|
|
@@ -176,7 +191,14 @@ export const useAuthStore = defineStore(
|
|
|
176
191
|
testingNoAuth.value = config.testingNoAuth ?? false
|
|
177
192
|
localMode.value = config.localMode ?? null
|
|
178
193
|
infrastructure.value = config.infrastructure ?? null
|
|
194
|
+
misconfigured.value = config.misconfigured ?? null
|
|
179
195
|
configLoaded.value = true
|
|
196
|
+
// A misconfigured backend serves only the fallback app; there's no session/board to
|
|
197
|
+
// resolve, so settle here and let the SPA render the misconfiguration screen.
|
|
198
|
+
if (misconfigured.value) {
|
|
199
|
+
ready.value = true
|
|
200
|
+
return
|
|
201
|
+
}
|
|
180
202
|
} catch {
|
|
181
203
|
// Backend unreachable — let the board's own error UI handle it (configLoaded stays
|
|
182
204
|
// false, so we never mistake this for an unauthenticated session and gate it).
|
|
@@ -335,8 +357,10 @@ export const useAuthStore = defineStore(
|
|
|
335
357
|
ready,
|
|
336
358
|
mothershipError,
|
|
337
359
|
configLoaded,
|
|
360
|
+
misconfigured,
|
|
338
361
|
isLocalFacade,
|
|
339
362
|
isAuthenticated,
|
|
363
|
+
isMisconfigured,
|
|
340
364
|
needsLogin,
|
|
341
365
|
bootstrap,
|
|
342
366
|
login,
|
package/i18n/locales/de.json
CHANGED
|
@@ -745,6 +745,65 @@
|
|
|
745
745
|
"title": "Diesen Runner entfernen?",
|
|
746
746
|
"body": "\"{name}\" wird entfernt. Dies kann nicht rückgängig gemacht werden."
|
|
747
747
|
}
|
|
748
|
+
},
|
|
749
|
+
"modelPolicy": {
|
|
750
|
+
"title": "Modellzugriffsrichtlinie",
|
|
751
|
+
"description": "Beschränkt, welche Modellfamilien die Teams dieses Kontos ausführen dürfen. Das Datenresidenz-Risiko hängt von der Bereitstellungsroute ab, daher kann eine vertrauenswürdige (residenzgarantierte) Route eine andernfalls blockierte Familie freigeben.",
|
|
752
|
+
"regionLabel": "Betriebsregion",
|
|
753
|
+
"applyPreset": "Vorlage anwenden:",
|
|
754
|
+
"modeLabel": "Richtlinienmodus",
|
|
755
|
+
"modes": {
|
|
756
|
+
"off": "Keine Einschränkung",
|
|
757
|
+
"blocklist": "Aufgeführte Familien blockieren",
|
|
758
|
+
"allowlist": "Nur aufgeführte Familien zulassen"
|
|
759
|
+
},
|
|
760
|
+
"familiesBlockLabel": "Blockierte Familien",
|
|
761
|
+
"familiesAllowLabel": "Zugelassene Familien",
|
|
762
|
+
"trustedLabel": "Vertrauenswürdige (residenzgarantierte) Routen",
|
|
763
|
+
"trustedHint": "Eine blockierte Familie bleibt verfügbar, wenn sie über eine dieser Routen bereitgestellt wird.",
|
|
764
|
+
"families": {
|
|
765
|
+
"claude": "Claude (Anthropic)",
|
|
766
|
+
"openai": "OpenAI / ChatGPT",
|
|
767
|
+
"gemini": "Gemini (Google)",
|
|
768
|
+
"llama": "Llama (Meta)",
|
|
769
|
+
"qwen": "Qwen (Alibaba)",
|
|
770
|
+
"kimi": "Kimi (Moonshot)",
|
|
771
|
+
"deepseek": "DeepSeek",
|
|
772
|
+
"glm": "GLM (Z.ai)"
|
|
773
|
+
},
|
|
774
|
+
"regions": {
|
|
775
|
+
"usa": "Vereinigte Staaten",
|
|
776
|
+
"europe": "Europa",
|
|
777
|
+
"china": "China",
|
|
778
|
+
"other": "Andere"
|
|
779
|
+
},
|
|
780
|
+
"providers": {
|
|
781
|
+
"bedrock": "AWS Bedrock"
|
|
782
|
+
},
|
|
783
|
+
"presets": {
|
|
784
|
+
"us-block-cn": {
|
|
785
|
+
"label": "In China gehostete Familien blockieren",
|
|
786
|
+
"description": "Blockiert DeepSeek, Qwen, Kimi und GLM; erlaubt sie nur über eine residenzgarantierte Route (z. B. Bedrock)."
|
|
787
|
+
},
|
|
788
|
+
"eu-block-cn": {
|
|
789
|
+
"label": "EU: in China gehostete Familien blockieren",
|
|
790
|
+
"description": "Blockiert DeepSeek, Qwen, Kimi und GLM; eine residenzgarantierte EU-Route (z. B. Bedrock) bleibt zulässig."
|
|
791
|
+
},
|
|
792
|
+
"eu-guaranteed-only": {
|
|
793
|
+
"label": "EU: nur residenzgarantierte Routen",
|
|
794
|
+
"description": "Erlaubt ein Modell nur, wenn es über eine residenzgarantierte Route bereitgestellt wird."
|
|
795
|
+
},
|
|
796
|
+
"cn-prefer-domestic": {
|
|
797
|
+
"label": "In China gehostete Familien bevorzugen",
|
|
798
|
+
"description": "Erlaubt in China gehostete Familien (plus Llama) und jede residenzgarantierte Route."
|
|
799
|
+
},
|
|
800
|
+
"other-block-cn": {
|
|
801
|
+
"label": "In China gehostete Familien blockieren",
|
|
802
|
+
"description": "Blockiert DeepSeek, Qwen, Kimi und GLM; erlaubt sie nur über eine residenzgarantierte Route."
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
"saved": "Modellzugriffsrichtlinie gespeichert",
|
|
806
|
+
"saveFailed": "Modellzugriffsrichtlinie konnte nicht gespeichert werden"
|
|
748
807
|
}
|
|
749
808
|
},
|
|
750
809
|
"inspector": {
|
|
@@ -3493,7 +3552,14 @@
|
|
|
3493
3552
|
"loadingBoard": "Board wird geladen…",
|
|
3494
3553
|
"backendUnreachable": "Backend nicht erreichbar",
|
|
3495
3554
|
"reconnecting": "Verbindung wird wiederhergestellt…",
|
|
3496
|
-
"offline": "Keine Live-Updates"
|
|
3555
|
+
"offline": "Keine Live-Updates",
|
|
3556
|
+
"misconfigured": {
|
|
3557
|
+
"title": "Backend nicht konfiguriert",
|
|
3558
|
+
"intro": "Der Server wurde gestartet, kann aber erst laufen, wenn die folgenden Einstellungen vorhanden sind. Ergänze die fehlenden Werte in deiner Umgebung und lade neu.",
|
|
3559
|
+
"howToFix": "So behebst du es:",
|
|
3560
|
+
"reload": "Neu laden",
|
|
3561
|
+
"hint": "Hier werden nur Variablennamen und deren Einrichtung angezeigt, niemals geheime Werte."
|
|
3562
|
+
}
|
|
3497
3563
|
},
|
|
3498
3564
|
"language": {
|
|
3499
3565
|
"switcher": "Sprache",
|
|
@@ -3591,6 +3657,8 @@
|
|
|
3591
3657
|
"provision_type_unhandled": "Kein Handler für diesen Bereitstellungstyp",
|
|
3592
3658
|
"preset_unsatisfiable": "Modell-Preset kann diese Pipeline nicht ausführen",
|
|
3593
3659
|
"visual_pipeline_no_frontend": "Kein Frontend zum Testen",
|
|
3660
|
+
"model_policy_blocked": "Modell durch Kontorichtlinie blockiert",
|
|
3661
|
+
"model_policy_unsupported": "Modellrichtlinie hier nicht verfügbar",
|
|
3594
3662
|
"deployer_required_before_tester": "Füge einen Deployer vor dem Tester hinzu"
|
|
3595
3663
|
},
|
|
3596
3664
|
"fallbackMessage": "Diese Aktion steht im Konflikt mit dem aktuellen Zustand.",
|
package/i18n/locales/en.json
CHANGED
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
"loadingBoard": "Loading board…",
|
|
5
5
|
"backendUnreachable": "Can't reach the backend",
|
|
6
6
|
"reconnecting": "Reconnecting…",
|
|
7
|
-
"offline": "Not receiving live updates"
|
|
7
|
+
"offline": "Not receiving live updates",
|
|
8
|
+
"misconfigured": {
|
|
9
|
+
"title": "Backend not configured",
|
|
10
|
+
"intro": "The server started but can't run until the settings below are provided. Add the missing values to your environment, then reload.",
|
|
11
|
+
"howToFix": "How to fix:",
|
|
12
|
+
"reload": "Reload",
|
|
13
|
+
"hint": "Only variable names and how to set them are shown here, never any secret values."
|
|
14
|
+
}
|
|
8
15
|
},
|
|
9
16
|
"language": {
|
|
10
17
|
"switcher": "Language",
|
|
@@ -439,6 +446,8 @@
|
|
|
439
446
|
"provision_type_unhandled": "No handler for this provision type",
|
|
440
447
|
"preset_unsatisfiable": "Model preset can't run this pipeline",
|
|
441
448
|
"visual_pipeline_no_frontend": "No frontend to test",
|
|
449
|
+
"model_policy_blocked": "Model blocked by account policy",
|
|
450
|
+
"model_policy_unsupported": "Model policy not available here",
|
|
442
451
|
"deployer_required_before_tester": "Add a Deployer before the Tester"
|
|
443
452
|
},
|
|
444
453
|
"fallbackMessage": "This action conflicts with the current state.",
|
|
@@ -2442,6 +2451,65 @@
|
|
|
2442
2451
|
"title": "Remove this runner?",
|
|
2443
2452
|
"body": "\"{name}\" will be removed. This can't be undone."
|
|
2444
2453
|
}
|
|
2454
|
+
},
|
|
2455
|
+
"modelPolicy": {
|
|
2456
|
+
"title": "Model access policy",
|
|
2457
|
+
"description": "Restrict which model families this account’s teams can run. Data-residency risk depends on the serving route, so a trusted (residency-guaranteed) route can exempt an otherwise-blocked family.",
|
|
2458
|
+
"regionLabel": "Operating region",
|
|
2459
|
+
"applyPreset": "Apply preset:",
|
|
2460
|
+
"modeLabel": "Policy mode",
|
|
2461
|
+
"modes": {
|
|
2462
|
+
"off": "No restriction",
|
|
2463
|
+
"blocklist": "Block listed families",
|
|
2464
|
+
"allowlist": "Allow only listed families"
|
|
2465
|
+
},
|
|
2466
|
+
"familiesBlockLabel": "Blocked families",
|
|
2467
|
+
"familiesAllowLabel": "Allowed families",
|
|
2468
|
+
"trustedLabel": "Trusted (residency-guaranteed) routes",
|
|
2469
|
+
"trustedHint": "A blocked family stays available when it is served over one of these routes.",
|
|
2470
|
+
"families": {
|
|
2471
|
+
"claude": "Claude (Anthropic)",
|
|
2472
|
+
"openai": "OpenAI / ChatGPT",
|
|
2473
|
+
"gemini": "Gemini (Google)",
|
|
2474
|
+
"llama": "Llama (Meta)",
|
|
2475
|
+
"qwen": "Qwen (Alibaba)",
|
|
2476
|
+
"kimi": "Kimi (Moonshot)",
|
|
2477
|
+
"deepseek": "DeepSeek",
|
|
2478
|
+
"glm": "GLM (Z.ai)"
|
|
2479
|
+
},
|
|
2480
|
+
"regions": {
|
|
2481
|
+
"usa": "United States",
|
|
2482
|
+
"europe": "Europe",
|
|
2483
|
+
"china": "China",
|
|
2484
|
+
"other": "Other"
|
|
2485
|
+
},
|
|
2486
|
+
"providers": {
|
|
2487
|
+
"bedrock": "AWS Bedrock"
|
|
2488
|
+
},
|
|
2489
|
+
"presets": {
|
|
2490
|
+
"us-block-cn": {
|
|
2491
|
+
"label": "Block China-hosted families",
|
|
2492
|
+
"description": "Block DeepSeek, Qwen, Kimi and GLM; allow them only over a residency-guaranteed route (e.g. Bedrock)."
|
|
2493
|
+
},
|
|
2494
|
+
"eu-block-cn": {
|
|
2495
|
+
"label": "EU: block China-hosted families",
|
|
2496
|
+
"description": "Block DeepSeek, Qwen, Kimi and GLM; a residency-guaranteed EU route (e.g. Bedrock) stays allowed."
|
|
2497
|
+
},
|
|
2498
|
+
"eu-guaranteed-only": {
|
|
2499
|
+
"label": "EU: residency-guaranteed routes only",
|
|
2500
|
+
"description": "Allow a model only when it is served over a residency-guaranteed route."
|
|
2501
|
+
},
|
|
2502
|
+
"cn-prefer-domestic": {
|
|
2503
|
+
"label": "Prefer China-hosted families",
|
|
2504
|
+
"description": "Allow China-hosted families (plus Llama) and any residency-guaranteed route."
|
|
2505
|
+
},
|
|
2506
|
+
"other-block-cn": {
|
|
2507
|
+
"label": "Block China-hosted families",
|
|
2508
|
+
"description": "Block DeepSeek, Qwen, Kimi and GLM; allow them only over a residency-guaranteed route."
|
|
2509
|
+
}
|
|
2510
|
+
},
|
|
2511
|
+
"saved": "Model access policy saved",
|
|
2512
|
+
"saveFailed": "Could not save the model access policy"
|
|
2445
2513
|
}
|
|
2446
2514
|
},
|
|
2447
2515
|
"providers": {
|