@cat-factory/app 0.288.1 → 0.289.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.
@@ -13,6 +13,7 @@ import type {
13
13
  UpdateFoundationalServiceInput,
14
14
  } from '~/types/domain'
15
15
  import { useSingleFlightProbe } from '~/composables/useSingleFlightProbe'
16
+ import { createServiceCatalogState } from '~/stores/serviceCatalogConnection'
16
17
  import { useWorkspaceStore } from '~/stores/workspace'
17
18
 
18
19
  /**
@@ -88,6 +89,17 @@ function foundationalServicesSetup(
88
89
  return requireOwnerId()
89
90
  }
90
91
 
92
+ // The workspace's connected developer portal, in its own factory (`./serviceCatalogConnection`):
93
+ // one connection with one store behind it, where everything else here is the catalog's tiers,
94
+ // suppressions and repo sources. `reloadCatalog` is a THUNK so it can name `reload`, declared
95
+ // further down.
96
+ const portal = createServiceCatalogState({
97
+ api,
98
+ requireWorkspaceId,
99
+ isWorkspaceTier: hasResolved,
100
+ reloadCatalog: () => reload(),
101
+ })
102
+
91
103
  /** Probe the feature + load this owner's tier, sources and (ws) the merged catalog. */
92
104
  async function runProbe() {
93
105
  const id = resolveOwnerId()
@@ -115,6 +127,7 @@ function foundationalServicesSetup(
115
127
  sources.value = []
116
128
  sourceChanges.value = {}
117
129
  sourcesAvailable.value = false
130
+ portal.reset()
118
131
  return
119
132
  }
120
133
  // Repo sources need the GitHub integration; a 503 here hides only the linking UI — the
@@ -126,6 +139,9 @@ function foundationalServicesSetup(
126
139
  sources.value = []
127
140
  sourcesAvailable.value = false
128
141
  }
142
+ // The portal connection is its own gate again (the service-catalog encryption key), and it is
143
+ // workspace-only. See `createServiceCatalogState`, which owns that half.
144
+ await portal.load(id)
129
145
  }
130
146
  // Single-flight the probe keyed on the owner id, so a panel-open fan-out loads once per owner.
131
147
  const { probe, ensureProbed } = useSingleFlightProbe(runProbe, () => resolveOwnerId())
@@ -243,6 +259,8 @@ function foundationalServicesSetup(
243
259
  suppressions,
244
260
  sources,
245
261
  sourceChanges,
262
+ serviceCatalog: portal.serviceCatalog,
263
+ serviceCatalogAvailable: portal.serviceCatalogAvailable,
246
264
  contractBodies,
247
265
  inheritedCount,
248
266
  probe,
@@ -257,6 +275,10 @@ function foundationalServicesSetup(
257
275
  unlinkSource,
258
276
  syncSource,
259
277
  checkSource,
278
+ connectServiceCatalog: portal.connect,
279
+ disconnectServiceCatalog: portal.disconnect,
280
+ probeServiceCatalog: portal.probe,
281
+ importServiceCatalog: portal.importNow,
260
282
  }
261
283
  }
262
284
 
@@ -153,6 +153,12 @@ export const useObservabilityStore = defineStore('observability', () => {
153
153
  // HTTP call at a time and files it. Only a harness CLI's step-level remainder is spend-only,
154
154
  // and that arrives through the stored row, never here.
155
155
  spendOnly: false,
156
+ // The live event is the COMPACT wire shape, which carries no gateway report: the cost a
157
+ // gateway states and the upstream it routed to arrive with the stored row. Null rather than
158
+ // 0, because absent and free are different facts everywhere else on this type and a
159
+ // placeholder zero here would render this run's live rows as free until the panel reloads.
160
+ reportedCostUsd: null,
161
+ upstreamProvider: null,
156
162
  promptText: '',
157
163
  promptPrefixCount: 0,
158
164
  promptHash: '',
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type {
3
+ ConnectServiceCatalogInput,
4
+ ServiceCatalogConnection,
5
+ ServiceCatalogSyncResult,
6
+ } from '~/types/domain'
7
+ import { createServiceCatalogState } from '~/stores/serviceCatalogConnection'
8
+
9
+ // The half of the service-catalog store worth pinning is the SPLIT between connecting and the
10
+ // first import. They fail differently, they have different remedies, and the panel presents them
11
+ // under different titles.
12
+
13
+ const connection: ServiceCatalogConnection = {
14
+ provider: 'backstage',
15
+ baseUrl: 'https://backstage.example.com',
16
+ authMode: 'static-token',
17
+ entityFilter: ['kind=component'],
18
+ includeApis: true,
19
+ maxServices: 200,
20
+ lastSyncedAt: null,
21
+ lastSyncStatus: null,
22
+ lastSyncMessage: null,
23
+ connectedAt: 1,
24
+ }
25
+
26
+ const result: ServiceCatalogSyncResult = {
27
+ upserted: 1,
28
+ tombstoned: 0,
29
+ unchanged: 0,
30
+ contracts: 1,
31
+ coverage: 'complete',
32
+ skippedServices: 0,
33
+ skippedConflicts: 0,
34
+ skippedApis: 0,
35
+ status: 'ok',
36
+ }
37
+
38
+ const input: ConnectServiceCatalogInput = {
39
+ baseUrl: 'https://backstage.example.com',
40
+ auth: { mode: 'static-token', token: 't' },
41
+ }
42
+
43
+ function build(overrides: { syncServiceCatalog?: () => Promise<ServiceCatalogSyncResult> } = {}) {
44
+ const calls: string[] = []
45
+ const state = createServiceCatalogState({
46
+ api: {
47
+ getServiceCatalog: async () => {
48
+ calls.push('get')
49
+ return connection
50
+ },
51
+ connectServiceCatalog: async () => {
52
+ calls.push('connect')
53
+ return connection
54
+ },
55
+ disconnectServiceCatalog: async () => {
56
+ calls.push('disconnect')
57
+ return null
58
+ },
59
+ probeServiceCatalog: async () => ({ ok: true }),
60
+ syncServiceCatalog: async () => {
61
+ calls.push('sync')
62
+ return overrides.syncServiceCatalog ? await overrides.syncServiceCatalog() : result
63
+ },
64
+ },
65
+ requireWorkspaceId: () => 'ws-1',
66
+ isWorkspaceTier: true,
67
+ reloadCatalog: async () => {
68
+ calls.push('reload')
69
+ },
70
+ })
71
+ return { state, calls }
72
+ }
73
+
74
+ describe('createServiceCatalogState', () => {
75
+ it('settles connect() on the CONNECT alone, so an import failure is not a connect failure', async () => {
76
+ // The connection is stored by the time an import runs, so rejecting here would tell the
77
+ // operator the connection failed while the panel renders it connected, and would bury the
78
+ // remedy the real failure names under a title saying the opposite.
79
+ const { state, calls } = build()
80
+
81
+ await expect(state.connect(input)).resolves.toEqual(connection)
82
+
83
+ expect(calls).toEqual(['connect'])
84
+ expect(state.serviceCatalog.value).toEqual(connection)
85
+ })
86
+
87
+ it('re-reads the connection after an import, for the verdict the import stamped there', async () => {
88
+ const { state, calls } = build()
89
+
90
+ await expect(state.importNow()).resolves.toEqual(result)
91
+
92
+ // The stamped verdict is the only thing that tells a human the estate they are looking at is a
93
+ // PREFIX of the portal's.
94
+ expect(calls).toEqual(['sync', 'get', 'reload'])
95
+ })
96
+
97
+ it('propagates an import failure to its own caller', async () => {
98
+ const { state } = build({
99
+ syncServiceCatalog: async () => {
100
+ throw new Error('service catalog unauthorized')
101
+ },
102
+ })
103
+
104
+ await expect(state.importNow()).rejects.toThrow('service catalog unauthorized')
105
+ })
106
+
107
+ it('clears the connection on disconnect and reloads the catalog views it tombstoned', async () => {
108
+ const { state, calls } = build()
109
+ await state.connect(input)
110
+
111
+ await state.disconnect()
112
+
113
+ expect(state.serviceCatalog.value).toBeNull()
114
+ expect(calls).toEqual(['connect', 'disconnect', 'reload'])
115
+ })
116
+ })
@@ -0,0 +1,131 @@
1
+ import { ref } from 'vue'
2
+ import type {
3
+ ConnectServiceCatalogInput,
4
+ ServiceCatalogConnection,
5
+ ServiceCatalogSyncResult,
6
+ } from '~/types/domain'
7
+
8
+ /**
9
+ * The SERVICE CATALOG half of the foundational-services store: the workspace's connected developer
10
+ * portal, and the four actions over it.
11
+ *
12
+ * A separate factory rather than more of `foundationalServicesSetup`, which had reached its
13
+ * per-function line budget. The seam is the feature's own: this is one connection with one store
14
+ * behind it, where everything else in that setup is the catalog's tiers, its suppressions and its
15
+ * repo sources.
16
+ *
17
+ * It is WORKSPACE-only. The portal credential rides the workspace-keyed secret delegation, so the
18
+ * backend serves this connection at no other scope, and the account-tier store gets the same state
19
+ * declared and permanently unavailable rather than a shape that pretends otherwise.
20
+ */
21
+ export interface ServiceCatalogStateDependencies {
22
+ /** The workspace-scoped API calls, narrowed to the five this half makes. */
23
+ api: {
24
+ getServiceCatalog: (workspaceId: string) => Promise<ServiceCatalogConnection | null>
25
+ connectServiceCatalog: (
26
+ workspaceId: string,
27
+ body: ConnectServiceCatalogInput,
28
+ ) => Promise<ServiceCatalogConnection>
29
+ // The contract's 204 sends no body, which the generated client surfaces as `null` rather than
30
+ // `void`; declaring the narrower shape here would refuse the real client.
31
+ disconnectServiceCatalog: (workspaceId: string) => Promise<unknown>
32
+ probeServiceCatalog: (
33
+ workspaceId: string,
34
+ body: ConnectServiceCatalogInput,
35
+ ) => Promise<{ ok: boolean; message?: string }>
36
+ syncServiceCatalog: (workspaceId: string) => Promise<ServiceCatalogSyncResult>
37
+ }
38
+ /** Throws unless this store is the workspace tier; the caller's own guard. */
39
+ requireWorkspaceId: () => string
40
+ /** False on the account tier, where there is no connection to read. */
41
+ isWorkspaceTier: boolean
42
+ /** Reload the catalog views a connect / disconnect / import changed. */
43
+ reloadCatalog: () => Promise<void>
44
+ }
45
+
46
+ export function createServiceCatalogState(deps: ServiceCatalogStateDependencies) {
47
+ const { api, requireWorkspaceId, isWorkspaceTier, reloadCatalog } = deps
48
+
49
+ /** The connection, or null when the workspace has none. */
50
+ const serviceCatalog = ref<ServiceCatalogConnection | null>(null)
51
+ /**
52
+ * false when the deployment configured no service-catalog encryption key: the catalog itself
53
+ * works (contracts can be uploaded), and only the portal-import surface 503s. The finer gate,
54
+ * exactly as `sourcesAvailable` is for the GitHub half.
55
+ */
56
+ const serviceCatalogAvailable = ref(true)
57
+
58
+ /**
59
+ * Read the connection as part of the store's probe.
60
+ *
61
+ * Its own `try` rather than part of the probe's: a 503 here hides only the import panel, and the
62
+ * catalog read that ran before it already succeeded, so folding the two would report a configured
63
+ * catalog as absent because its optional portal half is not wired.
64
+ */
65
+ async function load(workspaceId: string): Promise<void> {
66
+ if (!isWorkspaceTier) {
67
+ serviceCatalogAvailable.value = false
68
+ return
69
+ }
70
+ try {
71
+ serviceCatalog.value = await api.getServiceCatalog(workspaceId)
72
+ serviceCatalogAvailable.value = true
73
+ } catch {
74
+ serviceCatalog.value = null
75
+ serviceCatalogAvailable.value = false
76
+ }
77
+ }
78
+
79
+ /** Clear both views, for a probe whose whole catalog read failed. */
80
+ function reset(): void {
81
+ serviceCatalog.value = null
82
+ serviceCatalogAvailable.value = false
83
+ }
84
+
85
+ /**
86
+ * Store the connection. The first import is the CALLER's next step, not part of this promise.
87
+ *
88
+ * A connection that shows nothing reads as a broken one, so an import does follow immediately
89
+ * (the same reason a linked repo source syncs on link). What it must not do is settle this
90
+ * promise: by the time the import runs the connection is already stored, so folding an import
91
+ * failure in here would reject a call that succeeded, and the panel would show a "could not
92
+ * connect" toast beside the connection it just made, hiding the remedy the real failure names.
93
+ */
94
+ async function connect(input: ConnectServiceCatalogInput) {
95
+ serviceCatalog.value = await api.connectServiceCatalog(requireWorkspaceId(), input)
96
+ return serviceCatalog.value
97
+ }
98
+
99
+ async function disconnect(): Promise<void> {
100
+ await api.disconnectServiceCatalog(requireWorkspaceId())
101
+ serviceCatalog.value = null
102
+ // Disconnecting TOMBSTONES what the portal produced, so both catalog views are now wrong.
103
+ await reloadCatalog()
104
+ }
105
+
106
+ function probe(input: ConnectServiceCatalogInput) {
107
+ return api.probeServiceCatalog(requireWorkspaceId(), input)
108
+ }
109
+
110
+ /** Import now, then refresh both catalog views. */
111
+ async function importNow(): Promise<ServiceCatalogSyncResult> {
112
+ const id = requireWorkspaceId()
113
+ const result = await api.syncServiceCatalog(id)
114
+ // The connection is re-read too: the import stamps its verdict there, and that verdict is the
115
+ // only thing that tells a human the estate they are looking at is a PREFIX of the portal's.
116
+ const [connection] = await Promise.all([api.getServiceCatalog(id), reloadCatalog()])
117
+ serviceCatalog.value = connection
118
+ return result
119
+ }
120
+
121
+ return {
122
+ serviceCatalog,
123
+ serviceCatalogAvailable,
124
+ load,
125
+ reset,
126
+ connect,
127
+ disconnect,
128
+ probe,
129
+ importNow,
130
+ }
131
+ }
@@ -31,3 +31,17 @@ export type {
31
31
  UpdateFoundationalServiceInput,
32
32
  UploadApiContract,
33
33
  } from '@cat-factory/contracts'
34
+
35
+ // The SERVICE CATALOG connection: the developer portal (Backstage) whose services are imported
36
+ // into the catalog above as `workspace`-tier rows. Beside the catalog types rather than in a file
37
+ // of its own, because everything it produces IS that catalog.
38
+ export type {
39
+ ConnectServiceCatalogInput,
40
+ ServiceCatalogAuth,
41
+ ServiceCatalogAuthMode,
42
+ ServiceCatalogConnection,
43
+ ServiceCatalogCoverage,
44
+ ServiceCatalogProvider,
45
+ ServiceCatalogSyncResult,
46
+ ServiceCatalogSyncStatus,
47
+ } from '@cat-factory/contracts'
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { missingI18nKeys } from '../../test/i18nKeys'
3
+ import {
4
+ SERVICE_CATALOG_AUTH_KEYS,
5
+ SERVICE_CATALOG_AUTH_ORDER,
6
+ SERVICE_CATALOG_STATUS_COLORS,
7
+ SERVICE_CATALOG_STATUS_KEYS,
8
+ serviceCatalogStatusKey,
9
+ } from './serviceCatalog'
10
+
11
+ describe('service-catalog i18n keys', () => {
12
+ // The gap `test/i18nKeys.ts` exists for: the exhaustive `Record` proves every union member has an
13
+ // entry, and nothing proves the entry still names a key that exists. Deleting one would otherwise
14
+ // read as a clean removal, with the badge rendering its own key path at runtime.
15
+ it('every auth-mode key resolves in the base catalog', () => {
16
+ expect(missingI18nKeys(Object.values(SERVICE_CATALOG_AUTH_KEYS))).toEqual([])
17
+ })
18
+
19
+ it('every status key resolves, the never-imported case included', () => {
20
+ expect(
21
+ missingI18nKeys([
22
+ ...Object.values(SERVICE_CATALOG_STATUS_KEYS),
23
+ serviceCatalogStatusKey(null),
24
+ ]),
25
+ ).toEqual([])
26
+ })
27
+ })
28
+
29
+ describe('serviceCatalogStatusKey', () => {
30
+ it('keeps "never imported" apart from a failure', () => {
31
+ // Collapsing them would tell an operator their portal is broken the moment they connect it.
32
+ expect(serviceCatalogStatusKey(null)).not.toBe(SERVICE_CATALOG_STATUS_KEYS.failed)
33
+ })
34
+ })
35
+
36
+ describe('SERVICE_CATALOG_AUTH_ORDER', () => {
37
+ it('offers exactly the modes the label map knows, once each', () => {
38
+ // A relation over the two, not a pinned count: a new auth mode adds a member to the union, and
39
+ // this fails until the form offers it and the label map names it.
40
+ expect([...SERVICE_CATALOG_AUTH_ORDER].sort()).toEqual(
41
+ Object.keys(SERVICE_CATALOG_AUTH_KEYS).sort(),
42
+ )
43
+ })
44
+ })
45
+
46
+ describe('SERVICE_CATALOG_STATUS_COLORS', () => {
47
+ it('renders a PARTIAL import as a warning rather than a success', () => {
48
+ expect(SERVICE_CATALOG_STATUS_COLORS.partial).toBe('warning')
49
+ expect(SERVICE_CATALOG_STATUS_COLORS.ok).toBe('success')
50
+ })
51
+ })
@@ -0,0 +1,68 @@
1
+ import type { ServiceCatalogAuthMode, ServiceCatalogSyncStatus } from '~/types/domain'
2
+
3
+ // The SERVICE CATALOG connection's two closed vocabularies → their i18n keys, in ONE place each.
4
+ //
5
+ // Here rather than inline in `ServiceCatalogConnection.vue` for the reason `utils/vcs.ts` holds the
6
+ // per-provider constants: an exhaustive `Record` over a wire union is the drift guard, and it only
7
+ // guards if there is exactly one of it. A second copy in a template would keep rendering while this
8
+ // one gained a member.
9
+ //
10
+ // Both are also the case the typed-message-key guard cannot see: the keys are reached through a
11
+ // lookup rather than written out at the call site, so nothing proves the entry still names a key
12
+ // that exists. `serviceCatalog.spec.ts` asserts every value against the base catalog, which is the
13
+ // convention `test/i18nKeys.ts` exists for.
14
+
15
+ /**
16
+ * The label for one authentication mode.
17
+ *
18
+ * The keys are camelCase where the wire values are kebab-case, because a vue-i18n path segment is
19
+ * read as a nested lookup and a hyphen in one reads fine but breaks the moment anyone writes the
20
+ * literal form.
21
+ */
22
+ export const SERVICE_CATALOG_AUTH_KEYS: Record<ServiceCatalogAuthMode, string> = {
23
+ none: 'serviceCatalog.auth.none',
24
+ 'static-token': 'serviceCatalog.auth.staticToken',
25
+ 'legacy-shared-secret': 'serviceCatalog.auth.legacySharedSecret',
26
+ 'oauth2-client-credentials': 'serviceCatalog.auth.oauth2',
27
+ basic: 'serviceCatalog.auth.basic',
28
+ headers: 'serviceCatalog.auth.headers',
29
+ }
30
+
31
+ /**
32
+ * The label for what the last import concluded.
33
+ *
34
+ * `never` is NOT a member of the wire union and has its own key beside these: "no import has run"
35
+ * is a null `lastSyncStatus` rather than a status value, and collapsing it into `failed` would tell
36
+ * an operator their portal is broken the moment they connect it.
37
+ */
38
+ export const SERVICE_CATALOG_STATUS_KEYS: Record<ServiceCatalogSyncStatus, string> = {
39
+ ok: 'serviceCatalog.status.ok',
40
+ partial: 'serviceCatalog.status.partial',
41
+ failed: 'serviceCatalog.status.failed',
42
+ }
43
+
44
+ /** The key for a connection's last verdict, including the "never imported" case. */
45
+ export function serviceCatalogStatusKey(status: ServiceCatalogSyncStatus | null): string {
46
+ return status ? SERVICE_CATALOG_STATUS_KEYS[status] : 'serviceCatalog.status.never'
47
+ }
48
+
49
+ /**
50
+ * The badge colour for one verdict. `partial` is a WARNING rather than a success, which is the
51
+ * whole point of the three-value status: a truncated import holds real services and not all of
52
+ * them, and a green badge over it is how a prefix of an estate comes to read as the estate.
53
+ */
54
+ export const SERVICE_CATALOG_STATUS_COLORS = {
55
+ ok: 'success',
56
+ partial: 'warning',
57
+ failed: 'error',
58
+ } as const satisfies Record<ServiceCatalogSyncStatus, string>
59
+
60
+ /** The modes offered in the connect form, in the order an operator is most likely to want them. */
61
+ export const SERVICE_CATALOG_AUTH_ORDER: readonly ServiceCatalogAuthMode[] = [
62
+ 'static-token',
63
+ 'legacy-shared-secret',
64
+ 'oauth2-client-credentials',
65
+ 'basic',
66
+ 'headers',
67
+ 'none',
68
+ ]
@@ -1021,6 +1021,7 @@
1021
1021
  "context": "{value} Ktx",
1022
1022
  "contextThousands": "{value}K Ktx",
1023
1023
  "price": "{input}/{output} pro Mtok",
1024
+ "retiresOn": "Auslauf am {date}",
1024
1025
  "toast": {
1025
1026
  "connected": "OpenRouter-Schlüssel verbunden",
1026
1027
  "connectFailed": "Schlüssel konnte nicht verbunden werden",
@@ -3843,6 +3844,8 @@
3843
3844
  "cacheRead": "{tokens} aus dem Cache gelesen",
3844
3845
  "cacheWrite": "{tokens} in den Cache geschrieben",
3845
3846
  "total": "gesamt {duration}",
3847
+ "viaUpstream": "über {upstream}",
3848
+ "reportedCost": "abgerechnet {cost}",
3846
3849
  "prompt": "Prompt",
3847
3850
  "promptPrefixOmitted": "(nur neue Nachrichten: {count} frühere ausgelassen)",
3848
3851
  "response": "Antwort",
@@ -5231,7 +5234,8 @@
5231
5234
  },
5232
5235
  "tab": {
5233
5236
  "catalog": "Katalog",
5234
- "sources": "Repo-Quellen"
5237
+ "sources": "Repo-Quellen",
5238
+ "portal": "Entwicklerportal"
5235
5239
  },
5236
5240
  "panel": {
5237
5241
  "title": "Basisdienste",
@@ -5245,7 +5249,10 @@
5245
5249
  "format": {
5246
5250
  "openapi": "OpenAPI",
5247
5251
  "toadContract": "toad-contracts",
5248
- "lokaliseApiContract": "lokalise/api-contract"
5252
+ "lokaliseApiContract": "lokalise/api-contract",
5253
+ "asyncapi": "AsyncAPI",
5254
+ "graphql": "GraphQL",
5255
+ "grpc": "gRPC"
5249
5256
  },
5250
5257
  "contracts": {
5251
5258
  "omitted": "+{count} weitere nicht aufgeführt",
@@ -5923,7 +5930,11 @@
5923
5930
  "binary_generators_unreachable": "Die generativen Integrationen dieser Installation konnten gerade nicht gelesen werden, deshalb wurde der Lauf nicht gestartet. Es ist nichts falsch konfiguriert und keine Änderung nötig: versuchen Sie es erneut, sobald die Verbindung wieder steht.",
5924
5931
  "foundational_builtins_unreachable": "Die integrierten Basisdienste dieser Installation konnten gerade nicht gelesen werden. Es ist nichts falsch konfiguriert und keine Änderung nötig: versuchen Sie es erneut, sobald die Verbindung wieder steht.",
5925
5932
  "connection_credentials_unreadable": "Die gespeicherten Zugangsdaten dieser Verbindung konnten nicht gelesen werden. Wenn diese Installation den Dienst mit dem zugehörigen Schlüssel erreicht, verbinden Sie die Quelle neu, um sie zu ersetzen; andernfalls versuchen Sie es erneut, sobald diese Verbindung wiederhergestellt ist.",
5926
- "vcs_capability_unsupported": "Der mit diesem Workspace verbundene Quellcode-Anbieter bietet diesen Vorgang nicht an. Es ist nichts falsch konfiguriert und eine Einrichtung hilft hier nicht: Für diesen Anbieter ist der Vorgang nicht verfügbar."
5933
+ "vcs_capability_unsupported": "Der mit diesem Workspace verbundene Quellcode-Anbieter bietet diesen Vorgang nicht an. Es ist nichts falsch konfiguriert und eine Einrichtung hilft hier nicht: Für diesen Anbieter ist der Vorgang nicht verfügbar.",
5934
+ "service_catalog_unreachable": "Das Entwicklerportal dieses Boards hat nicht geantwortet oder eine Antwort geliefert, die diese Plattform nicht lesen kann. Hier ist nichts falsch konfiguriert und keine Änderung hilft: versuche es erneut, sobald das Portal erreichbar ist.",
5935
+ "service_catalog_unauthorized": "Das Entwicklerportal hat die gespeicherte Zugangsdaten abgelehnt. Sie wurden wahrscheinlich rotiert oder widerrufen: öffne die Servicekatalog-Einstellungen und trage aktuelle ein.",
5936
+ "service_catalog_filter_missing": "Der Entitätsfilter dieser Verbindung konnte nicht gelesen werden, es gibt also nichts zu importieren. Speichere die Servicekatalog-Verbindung erneut, um ihn wiederherzustellen.",
5937
+ "service_catalog_response_too_large": "Das Entwicklerportal hat mit mehr Daten geantwortet, als diese Plattform in einer Antwort aufnimmt. Öffne die Servicekatalog-Einstellungen und senke das Service-Limit oder deaktiviere den Import von Schnittstellendefinitionen, und importiere dann erneut."
5927
5938
  }
5928
5939
  },
5929
5940
  "action": {
@@ -8098,5 +8109,79 @@
8098
8109
  "title": "Nichts zu vergleichen"
8099
8110
  },
8100
8111
  "loadFailed": "Die erzeugten Kandidaten konnten nicht geladen werden."
8112
+ },
8113
+ "serviceCatalog": {
8114
+ "unavailable": "Der Import eines Servicekatalogs ist für diese Installation nicht konfiguriert.",
8115
+ "intro": "Verweise die Plattform auf das Entwicklerportal, das deine Organisation bereits betreibt: Ihre Services werden in den Katalog oben importiert, mit Identität, Verantwortlichkeit und den API-Definitionen, die jeder Service veröffentlicht. Agenten, die Fehler einordnen oder untersuchen, lesen sie, um zu bestimmen, zu welchem Service eine Aufgabe gehört.",
8116
+ "summary": "Filter: {filter} · höchstens {max} Services",
8117
+ "form": {
8118
+ "connect": "Portal verbinden",
8119
+ "replace": "Verbindung ersetzen"
8120
+ },
8121
+ "field": {
8122
+ "baseUrl": "Portal-URL",
8123
+ "authMode": "Authentifizierung",
8124
+ "token": "Service-Token",
8125
+ "sharedSecret": "Gemeinsames Geheimnis",
8126
+ "tokenUrl": "Token-Endpunkt",
8127
+ "clientId": "Client-ID",
8128
+ "clientSecret": "Client-Secret",
8129
+ "scope": "Scope (optional)",
8130
+ "audience": "Audience (optional)",
8131
+ "username": "Benutzername",
8132
+ "password": "Passwort",
8133
+ "headerName": "Header-Name",
8134
+ "headerValue": "Header-Wert",
8135
+ "secondHeaderName": "Zweiter Header-Name (optional)",
8136
+ "secondHeaderValue": "Zweiter Header-Wert",
8137
+ "entityFilter": "Entitätsfilter",
8138
+ "maxServices": "Maximale Anzahl Services",
8139
+ "includeApis": "API-Definitionen der Services mitimportieren"
8140
+ },
8141
+ "help": {
8142
+ "baseUrl": "Die Basis-URL deiner Backstage-Instanz. Ein privater oder interner Host ist nur zulässig, wenn die Installation ihn erlaubt.",
8143
+ "token": "Ein Service-zu-Service-Token aus der External-Access-Konfiguration von Backstage.",
8144
+ "sharedSecret": "Ein base64-Geheimnis aus `backend.auth.keys`. Die Plattform signiert damit pro Anfrage ein kurzlebiges Token.",
8145
+ "headers": "Für ein Gateway, das über eigene Header authentifiziert, etwa ein Cloudflare-Access-Service-Token.",
8146
+ "entityFilter": "Ein `key=value`-Ausdruck pro Zeile; alle müssen zutreffen. Standard ist `kind=component`. Grenze ihn auf die Services ein, die als gemeinsame Fähigkeiten angeboten werden sollen.",
8147
+ "maxServices": "Ein Import, der dieses Limit erreicht, wird als Auszug des Portals gemeldet, nicht als dessen Gesamtheit."
8148
+ },
8149
+ "auth": {
8150
+ "none": "Keine",
8151
+ "staticToken": "Statisches Service-Token",
8152
+ "legacySharedSecret": "Gemeinsames Geheimnis (Legacy-Token)",
8153
+ "oauth2": "OAuth2 Client Credentials",
8154
+ "basic": "HTTP Basic",
8155
+ "headers": "Eigene Header"
8156
+ },
8157
+ "status": {
8158
+ "ok": "Importiert",
8159
+ "partial": "Teilweise importiert",
8160
+ "failed": "Import fehlgeschlagen",
8161
+ "never": "Nie importiert"
8162
+ },
8163
+ "action": {
8164
+ "connect": "Verbinden",
8165
+ "replace": "Ersetzen",
8166
+ "probe": "Verbindung testen",
8167
+ "import": "Jetzt importieren",
8168
+ "disconnect": "Trennen"
8169
+ },
8170
+ "disconnect": {
8171
+ "title": "Servicekatalog trennen?",
8172
+ "body": "Die von diesem Portal importierten Services werden aus dem Katalog zurückgezogen. Manuell registrierte oder hochgeladene Einträge bleiben unberührt.",
8173
+ "confirm": "Trennen"
8174
+ },
8175
+ "toast": {
8176
+ "connected": "Servicekatalog verbunden",
8177
+ "connectFailed": "Servicekatalog konnte nicht verbunden werden",
8178
+ "probeOk": "Das Portal hat geantwortet",
8179
+ "probeFailed": "Das Portal war nicht erreichbar",
8180
+ "imported": "Servicekatalog importiert",
8181
+ "importedDetail": "{upserted} aktualisiert, {unchanged} unverändert, {tombstoned} zurückgezogen",
8182
+ "importFailed": "Servicekatalog konnte nicht importiert werden",
8183
+ "disconnected": "Servicekatalog getrennt",
8184
+ "disconnectFailed": "Servicekatalog konnte nicht getrennt werden"
8185
+ }
8101
8186
  }
8102
8187
  }