From e9224f2259c8c79c9b82a8cf4fdcdffabf1e3958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Weberru=C3=9F?= Date: Mon, 26 Oct 2026 18:50:07 +0000 Subject: [PATCH 1/2] fix(configurator): expose Assistant beside application template drafts --- .../app/apps/web/src/locales/en.ts | 3 + .../app/apps/web/src/main.tsx | 48 +++-- .../app/e2e/platform-owner-flow.spec.ts | 186 +++++++++++++++++- .../docs/project-template-drafts.de.md | 9 + .../docs/project-template-drafts.md | 8 + 5 files changed, 232 insertions(+), 22 deletions(-) diff --git a/landing-zone-configurator/app/apps/web/src/locales/en.ts b/landing-zone-configurator/app/apps/web/src/locales/en.ts index 951da1d..d34ee59 100644 --- a/landing-zone-configurator/app/apps/web/src/locales/en.ts +++ b/landing-zone-configurator/app/apps/web/src/locales/en.ts @@ -1,4 +1,7 @@ export const englishMessages: Record = { + "Assistant für Template-Entwürfe": "Assistant for template drafts", + "Der Assistant verwendet die geöffnete Plattformkonfiguration einschließlich ihrer Template-Entwürfe. Nenne das gewünschte Template und prüfe die vorgeschlagenen Änderungen vor der Übernahme. Speichere sie anschließend ausdrücklich; eine Veröffentlichung erfolgt separat.": + "The Assistant uses the open platform configuration, including its template drafts. Name the template you want to change and review the proposed changes before accepting them. Then save explicitly; publication is a separate step.", "Für diesen Plattformvertrag fehlt die Ausführungsbindung. Richte sie unter Plattformanbindung mit dem passenden State-Backend ein.": "This platform contract has no execution binding. Configure it under platform binding with the appropriate state backend.", "Konfiguration bearbeiten": "Edit configuration", diff --git a/landing-zone-configurator/app/apps/web/src/main.tsx b/landing-zone-configurator/app/apps/web/src/main.tsx index d78e73c..bb3e68c 100644 --- a/landing-zone-configurator/app/apps/web/src/main.tsx +++ b/landing-zone-configurator/app/apps/web/src/main.tsx @@ -371,6 +371,25 @@ function App() { "editor", ].includes(view); + const assistantPanel = + platformAccess && draft && session ? ( + { + setDraft(loaded); + setDraftEpoch((value) => value + 1); + setConfigurationBinding(binding); + setSavedDraft(JSON.stringify(persisted)); + setDeploymentSelection(null); + }} + /> + ) : null; + return ( <> @@ -761,6 +780,13 @@ function App() { isCommonDraft(draft) && draft.projectTemplates !== undefined ? ( <> +

{t("Assistant für Template-Entwürfe")}

+

+ {t( + "Der Assistant verwendet die geöffnete Plattformkonfiguration einschließlich ihrer Template-Entwürfe. Nenne das gewünschte Template und prüfe die vorgeschlagenen Änderungen vor der Übernahme. Speichere sie anschließend ausdrücklich; eine Veröffentlichung erfolgt separat.", + )} +

+ {assistantPanel} - {view === "editor" && draft && session && ( - { - setDraft(loaded); - setDraftEpoch((value) => value + 1); - setConfigurationBinding(binding); - setSavedDraft(JSON.stringify(persisted)); - setDeploymentSelection(null); - }} - /> - )} + {view === "editor" && assistantPanel} {view === "editor" && !draft && (

{t("Kein Entwurf in diesem Tab")}

diff --git a/landing-zone-configurator/app/e2e/platform-owner-flow.spec.ts b/landing-zone-configurator/app/e2e/platform-owner-flow.spec.ts index 3f1b58d..d5867e1 100644 --- a/landing-zone-configurator/app/e2e/platform-owner-flow.spec.ts +++ b/landing-zone-configurator/app/e2e/platform-owner-flow.spec.ts @@ -1,3 +1,4 @@ +import { assistantProposalFields } from "@lzc/contracts"; import { type CommonConfiguration, createEditorConfiguration, @@ -10,12 +11,13 @@ const userId = "11111111-1111-4111-8111-111111111111"; const tenantId = "22222222-2222-4222-8222-222222222222"; const organizationId = "33333333-3333-4333-8333-333333333333"; const configurationId = "44444444-4444-4444-8444-444444444444"; +const documentId = "66666666-6666-4666-8666-666666666666"; const contractRevision = "55555555-5555-4555-8555-555555555555"; function platformConfiguration(): CommonConfiguration { return editCommonInput( { - ...createEditorConfiguration("standalone", configurationId), + ...createEditorConfiguration("standalone", documentId), name: "Existing platform workflow", projectTemplates: [structuredClone(legacyAccessTemplate)], }, @@ -299,6 +301,9 @@ test("Application Owner receives the catalogue without platform editor, binding await expect( page.getByLabel("Name des Templates", { exact: true }), ).toHaveCount(0); + await expect( + page.getByRole("region", { name: "Konfigurations-Chat", exact: true }), + ).toHaveCount(0); expect(state.privateReads).toEqual([]); expect(state.writes).toEqual([]); expect(state.executionWrites).toEqual([]); @@ -472,3 +477,182 @@ test("configuration reopen protects edits made during its read and skips confirm expect(state.writes).toEqual([]); expect(state.executionWrites).toEqual([]); }); + +test("template-draft Assistant uses the saved configuration and accepts locally before explicit save", async ({ + page, +}) => { + const state = await mockWorkspace(page); + const original = platformConfiguration(); + expect(original.id).not.toBe(configurationId); + const template = original.projectTemplates?.[0]; + if (!template) throw new Error("Expected template"); + const candidate = structuredClone(original); + const proposedTemplate = candidate.projectTemplates?.[0]; + if (!proposedTemplate) throw new Error("Expected proposed template"); + proposedTemplate.name = "Assistant reviewed corporate template"; + const change = { + path: "projectTemplates", + operation: "merge" as const, + before: {}, + value: { [template.key]: { name: proposedTemplate.name } }, + }; + const conversation = { + id: contractRevision, + revision: 2, + baseRevision: 1, + expiresAt: new Date(Date.now() + 3600000).toISOString(), + stale: false, + messages: [], + proposal: { + explanation: + "Name des bestehenden Templates vorschlagen, sonstige Einstellungen erhalten.", + questions: [], + references: ["project-templates"], + changes: [change], + }, + }; + const reads: string[] = []; + let sent = 0; + let accepted = 0; + let proposalAvailable = false; + await page.route( + `**/api/v1/configurations/${configurationId}/assistant`, + (route) => { + const request = route.request(); + if (request.method() === "GET") { + reads.push(new URL(request.url()).pathname); + return route.fulfill({ + json: { conversation: proposalAvailable ? conversation : null }, + }); + } + expect(request.method()).toBe("POST"); + expect(request.headers()["x-lzc-csrf"]).toBe("csrf-workflow"); + expect(request.postDataJSON()).toEqual({ + revision: 1, + message: "Bitte den Namen dieses Templates verbessern.", + draft: original, + }); + sent++; + proposalAvailable = true; + return route.fulfill({ json: { conversation } }); + }, + ); + await page.route( + `**/api/v1/configurations/${configurationId}/assistant/accept`, + (route) => { + expect(route.request().method()).toBe("POST"); + expect(route.request().headers()["x-lzc-csrf"]).toBe("csrf-workflow"); + expect(route.request().postDataJSON()).toEqual({ + revision: 2, + baseRevision: 1, + selectedFields: assistantProposalFields([change]).map( + (field) => field.id, + ), + draft: original, + savedDraft: original, + }); + accepted++; + proposalAvailable = false; + return route.fulfill({ + json: { + configuration: { + id: configurationId, + revision: 1, + draft: candidate, + persisted: false, + }, + savedConfiguration: original, + conversation: { ...conversation, proposal: null }, + }, + }); + }, + ); + await page.goto("/configurations"); + await page + .getByRole("button", { + name: "Konfiguration öffnen: Existing platform workflow", + exact: true, + }) + .click(); + await page + .getByRole("navigation", { name: "Hauptnavigation", exact: true }) + .getByRole("button", { name: "Application Landing Zones", exact: true }) + .click(); + const drafts = page.getByRole("tabpanel", { + name: "Template-Entwürfe", + exact: true, + }); + const chat = drafts.getByRole("region", { + name: "Konfigurations-Chat", + exact: true, + }); + const templateName = drafts.getByLabel("Name des Templates", { exact: true }); + const next = drafts.getByRole("button", { + name: "Weiter: Plattformanbindung", + exact: true, + }); + await expect( + drafts.getByRole("heading", { + name: "Assistant für Template-Entwürfe", + exact: true, + }), + ).toBeVisible(); + await expect(chat).toBeVisible(); + await expect(templateName).toHaveValue(template.name); + await expect(next).toBeEnabled(); + await chat + .getByLabel("Deine Nachricht", { exact: true }) + .fill("Bitte den Namen dieses Templates verbessern."); + await chat + .getByRole("button", { name: "Nachricht senden", exact: true }) + .click(); + await expect(chat).toContainText(proposedTemplate.name); + expect(sent).toBe(1); + expect(accepted).toBe(0); + const readsBeforeNavigation = reads.length; + await page + .getByRole("navigation", { name: "Hauptnavigation", exact: true }) + .getByRole("button", { name: "Konfigurationen", exact: true }) + .click(); + const editorChat = page.getByRole("region", { + name: "Konfigurations-Chat", + exact: true, + }); + await expect(editorChat).toBeVisible(); + await expect(editorChat).toContainText(proposedTemplate.name); + await page + .getByRole("navigation", { name: "Hauptnavigation", exact: true }) + .getByRole("button", { name: "Application Landing Zones", exact: true }) + .click(); + await expect(chat).toBeVisible(); + await expect(chat).toContainText(proposedTemplate.name); + expect(reads.length).toBeGreaterThan(readsBeforeNavigation); + expect(sent).toBe(1); + expect(accepted).toBe(0); + await expect(templateName).toHaveValue(template.name); + await expect(next).toBeEnabled(); + expect(state.writes).toEqual([]); + await chat + .getByRole("button", { name: "Gesamten Vorschlag übernehmen", exact: true }) + .click(); + await expect(templateName).toHaveValue(proposedTemplate.name); + await expect(next).toBeDisabled(); + await expect(drafts).toContainText("Speichere zuerst die Konfiguration."); + expect(accepted).toBe(1); + expect(state.writes).toEqual([]); + expect(state.stored()).toEqual(original); + await drafts + .getByRole("button", { name: "Konfiguration speichern", exact: true }) + .click(); + await expect.poll(() => state.writes.length).toBe(1); + await expect(next).toBeEnabled(); + expect(state.stored()).toEqual(candidate); + expect(state.revision()).toBe(2); + expect(reads.length).toBeGreaterThan(0); + expect( + reads.every( + (path) => path === `/api/v1/configurations/${configurationId}/assistant`, + ), + ).toBe(true); + expect(state.executionWrites).toEqual([]); +}); diff --git a/landing-zone-configurator/docs/project-template-drafts.de.md b/landing-zone-configurator/docs/project-template-drafts.de.md index e752a97..6644b4b 100644 --- a/landing-zone-configurator/docs/project-template-drafts.de.md +++ b/landing-zone-configurator/docs/project-template-drafts.de.md @@ -98,6 +98,15 @@ noch fehlenden Application-Apply sind durch diesen Stand überholt. ## Fachlicher Ablauf +Der Reiter **Template-Entwürfe** unter **Application Landing Zones** zeigt den +bestehenden Konfigurations-Assistant direkt oberhalb des Template-Editors. Er nutzt +dieselbe gespeicherte Konfigurations-ID, dasselbe Gespräch und denselben Entwurf wie +der Konfigurationseditor. Nennen Sie das gewünschte Template und prüfen Sie die +vorgeschlagenen Felder vor der Übernahme. Die Übernahme ändert den lokalen Entwurf; +speichern Sie ausdrücklich vor Plattformanbindung oder Veröffentlichung. Sie +veröffentlicht keine Version und erteilt keine Ausführungsfreigabe. Application Owner +sehen diesen Konfigurations-Chat nicht und erhalten keinen Zugriff auf Plattformdaten. + Der Platform Engineer definiert zentrale Plattformdienste und die dafür nötigen Infrastrukturprojekte. Im Schritt **Projekt-Templates** erstellt er Vorlagen für spätere Anwendungsprojekte. Die Plattformkonfiguration erzeugt diese Projekte diff --git a/landing-zone-configurator/docs/project-template-drafts.md b/landing-zone-configurator/docs/project-template-drafts.md index d12ac42..78ef7c3 100644 --- a/landing-zone-configurator/docs/project-template-drafts.md +++ b/landing-zone-configurator/docs/project-template-drafts.md @@ -40,6 +40,14 @@ Apply never runs automatically. A potentially partially executed Apply is blocke
## Business workflow +The **Template drafts** tab under **Application Landing Zones** shows the existing +configuration Assistant directly above the template editor. It uses the same saved +configuration ID, conversation and root draft as the configuration editor. Name the +template you want to change and review the proposed fields before accepting them. +Acceptance changes the local draft; save explicitly before platform binding or +publication. It does not publish a version or authorize execution. Application Owners +do not see this configuration chat or gain access to platform configuration data. + The Platform Engineer defines central platform services and the infrastructure projects required for them. In the **Project templates** step, they create templates for future application projects. Platform configuration does not create those projects itself. The Application Owner later orders an instance of a published template version in separate Application self-service. New drafts from all eight Accelerator templates use this workflow. `landing_zones`, `sandboxes` and `landing_zone_namespace_services` from the examples become template drafts. Platform, connectivity, governance, central services and their projects remain part of platform configuration. The preview shows platform resources and template drafts separately. From ef11a96fe4bf50097dbf213c6a91b756158d7ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Weberru=C3=9F?= Date: Mon, 26 Oct 2026 19:07:06 +0000 Subject: [PATCH 2/2] fix(ci): retain full browser acceptance after slow OS provisioning --- .github/workflows/validate-configurator.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-configurator.yml b/.github/workflows/validate-configurator.yml index d92b0e7..a934786 100644 --- a/.github/workflows/validate-configurator.yml +++ b/.github/workflows/validate-configurator.yml @@ -64,7 +64,8 @@ jobs: application: runs-on: ubuntu-latest - timeout-minutes: 15 + # Browser OS dependencies can exhaust 15 minutes before acceptance starts. + timeout-minutes: 25 services: postgres: image: postgres@sha256:d74eeac9a635390a49bc21bd49fccd973de707e2a53a76ac49b552b8712ec46f