002 Anrufinformation in Teams und aktive ConMeet-Instanz Entwurf

Definiert die Anrufinformation als Teams-App für den Proof of Concept – Anmeldung in Teams, Erkennen des Anrufers über seine Rufnummer in der aktiven ConMeet-Instanz und das Umschalten dieser Instanz durch die Person selbst. Stand 2026-10-30

Ausgangslage

Von Spezifikation 002 ist nichts umgesetzt. Sie baut auf der Vertikale ConMeetInstances aus 001 auf (Stand im Arbeitsverzeichnis, teils uncommittet): ConMeetClient, Entitäten, Service, Erweiterungspunkt der Replik und die Webapp-Bausteine zum Verknüpfen sind vorhanden und werden erweitert. Neu sind die Vertikale CallInformation, die zweite Anmeldung mit eigenem Keycloak-Client und eigener Sitzung im BFF und die Seite /teams/call mit Teams-App-Manifest. Den größten Aufwand machen die Teams-Anmeldung (S8, S9) und die Seite selbst (S11–S14). 002-issues.md hat keine offenen Punkte.

Pfade: Backend unter source/conmeet-server/, BFF unter source/conmeet-webapp/. Regeln aus docs/backend-development.md, docs/frontend-development.md und den Skills validdata-backend-patterns, validdata-vertical-generator, ef-core, hotchocolate-best-practices, validdata-frontend-patterns, svelte-code-writer, validdata-styleguide, validdata-aspire-patterns gelten in jedem Schritt.

Bestand je Anforderung

Anforderung Stand Fundstelle Schritt
R1 fehlt — S11
R2 fehlt — S15
R3 fehlt — S15
R4 fehlt — S10
R5 fehlt — S10
R6 fehlt — S11
R7 fehlt — S11
R8 fehlt — S11
R9 fehlt — S11
R10 fehlt — S13
R11 fehlt — S9
R12 fehlt — S9
R13 fehlt — S9
R14 fehlt — S9
R15 fehlt — S9
R17 fehlt — S9
R18 fehlt — S9
R19 teilweise source/conmeet-webapp/src/lib/server/suite-access.ts:18 — Zustandsseiten vorhanden S11
R20 fehlt — S3
R21 fehlt — S3
R22 fehlt — S5, S12
R23 fehlt — S12
R24 fehlt — S12
R25 fehlt — S12
R26 fehlt — S5, S12
R27 fehlt — S3
R28 teilweise source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/ConMeetInstancesService.cs:199 — Entzug löscht nur das InstanceGrant S3
R29 teilweise source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/ConMeetInstancesService.cs:180 — setzt nur GrantedToAll S3
R30 teilweise source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/ConMeetInstancesService.cs:138 — löscht Links und Grants S3
R31 teilweise source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/ConMeetInstancesService.cs:232 — Erweiterungspunkt löscht Links und Grants S3
R32 fehlt — S3
R33 fehlt — S11
R34 teilweise source/conmeet-webapp/src/lib/features/conmeet-instances/link-sheet.svelte — Verknüpfen vorhanden, liest locals.session S14
R35 teilweise source/conmeet-webapp/src/lib/features/conmeet-instances/personal-key-field.svelte:29 — Einfügen mit Fehleranzeige vorhanden S14
R36 fehlt — S13
R37 fehlt — S13
R38 fehlt — S6
R39 fehlt — S6
R40 fehlt — S6
R41 fehlt — S6
R42 fehlt — S6
R43 fehlt — S6
R44 fehlt — S6
R45 fehlt — S6
R46 fehlt — S6
R47 fehlt — S6
R48 fehlt — S6
R49 fehlt — S6
R50 fehlt — S6
R51 fehlt — S6
R52 teilweise source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/PublicApi/ConMeetClient.cs:20 — Prüfung ohne Begrenzung S2
R53 fehlt — S2
R54 fehlt — S2
R55 fehlt — S6
R56 fehlt — S13
R57 fehlt — S13
R58 fehlt — S13
R59 fehlt — S13
R60 fehlt — S6
R61 fehlt — S12
R62 fehlt — S5
R63 teilweise source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/Api/ConMeetInstancesErrorFilter.cs:42 — Codes vorhanden, Mutation fehlt S5
R64 fehlt — S6
R65 fehlt — S6
R66 teilweise source/conmeet-webapp/src/lib/server/graphql/client.ts:9 — urql ohne preferGetMethod: false; Queries können per GET laufen S13
R67 fehlt — S6
R68 fehlt — S6
R69 fehlt — S6
R70 fehlt — S13
R71 fehlt — S11
R72 fehlt — S9
R73 fehlt — S9
R74 teilweise source/conmeet-webapp/src/lib/server/auth.ts:402 — Remote Requests ohne Weiterleitung; Fehler ohne Sitzung fehlt S8
R75 fehlt — S13
R76 fehlt — S6
R77 fehlt — S13
R78 fehlt — S13
R79 fehlt — S13
R80 fehlt — S11
R81 fehlt — S4
R82 fehlt — S4
R83 teilweise source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/PublicApi/ConMeetClient.cs:23 — Bearer je Anfrage, aber ohne Handler S2
R84 teilweise source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/PublicApi/ConMeetClient.cs:22 — Adresse per Verkettung, kein BaseAddress S2
R85 fehlt — S4
R86 teilweise source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/PublicApi/ConMeetClient.cs:31 — Zuordnung steckt in CheckAsync S2
R87 fehlt — S12
R89 fehlt — S9
R90 fehlt — S9
R91 fehlt — S8
R92 fehlt — S3
R93 fehlt — S3
R94 fehlt — S9
R95 teilweise source/conmeet-webapp/src/lib/server/suite-access.ts:30 — loadSuiteAccess nutzt nur locals.session S8, S11
R96 teilweise source/conmeet-server/ConMeet.Service/Features/AuthenticationFeature.cs:74 prüft nur die Audience; Client fehlt in keycloak/vd-suite-realm.json:696 S7
R97 fehlt — S8
R98 teilweise source/conmeet-webapp/src/lib/server/db/schema.ts:47 — oauth_states ohne Hash des Teams-state S9
R99 fehlt — S9
R100 fehlt — S9
R101 fehlt — S9
R102 fehlt — S15
R103 fehlt — S6
R104 fehlt — S2
R105 fehlt — S8
R106 fehlt — S3
R107 teilweise source/conmeet-webapp/src/lib/server/auth.ts:303 — Abmeldung der Webseite vorhanden; Teams-Abmeldung fehlt S9
R108 fehlt — S5, S12
R16 ist entfallen, R88 nicht vergeben.

Schritte

S1 — Pakete aufnehmen und Lizenzen nachziehen

Erfüllt: Voraussetzung für R6, R40 Abhängig von: — Dateien: source/conmeet-server/Directory.Packages.props, source/conmeet-server/ConMeet.Core/ConMeet.Core.csproj, docs/nuget-licenses.md, source/conmeet-webapp/package.json Zu tun:

  1. libphonenumber-csharp (neueste stabile Version) zentral eintragen und in ConMeet.Core referenzieren; Lizenz nach Skill validdata-dotnet-package-licenses prüfen und Zeile in docs/nuget-licenses.md ergänzen.
  2. @microsoft/teams-js (neueste stabile Version) als Abhängigkeit der Webapp aufnehmen. Fertig, wenn: Restore und Build beider Projekte laufen; docs/nuget-licenses.md führt das neue Paket mit Lizenz und Prüfdatum.

S2 — Begrenzung, Handler-Kette und Zuordnung der Zustände in PublicApi

Erfüllt: R52, R53, R54, R83, R84 (Handler-Kette), R86 (Zuordnung), R104 Abhängig von: — Dateien: source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/PublicApi/ConMeetClient.cs, neue Handler daneben in PublicApi/, ConMeetInstancesRegistry.cs, source/conmeet-server/ConMeet.Core.Tests/Features/ConMeetInstances/ConMeetClientTests.cs Zu tun:

  1. Begrenzung als DelegatingHandler mit PartitionedRateLimiter aus System.Threading.RateLimiting: Token Bucket, Kapazität 1, Nachfüllen alle 1 / ConMeet:RequestsPerSecond s (Standard 10), Partition = normalisierte BaseUrl (Ergebnis von NormalizeBaseUrl, ConMeetInstancesService.cs:362); Warteschlange ohne Verwerfen, Warten mit dem Cancellation Token der Anfrage. Singleton über alle Anfragen.
  2. Zeitüberschreitung je Aufruf (10 s, heute ConMeetClient.Timeout) als eigener Handler innerhalb der Begrenzung, sodass Warten nicht zählt (R54).
  3. Schlüssel-Handler: setzt Authorization: Bearer je Anfrage nur, wenn die URI unter der BaseAddress liegt; sonst Abbruch ohne Senden (R104).
  4. Zuordnung „Antwort/Übertragungsfehler → ConnectionCheck“ aus CheckAsync (ConMeetClient.cs:31) in eine statische Funktion herauslösen; Fehler aus R104 ergibt Unexpected ohne Statuscode.
  5. CheckAsync auf dieselbe Kette umstellen (001-R95 läuft durch die Begrenzung); Adressbildung über BaseAddress mit abschließendem / und relativen Pfaden ohne führendes / (R84). Die Ausnahme vom Resilience-Handler (ConMeetClient.cs:62) bleibt.
  6. Tests: gleichmäßiger Abstand bei 10/s, Partition je BaseUrl (zwei Instanzen blockieren sich nicht), Warten zählt nicht zur 10-s-Grenze, Abbruch während des Wartens, Schlüssel nicht an fremden Host, BaseUrl mit Pfad bleibt erhalten (https://host/conmeet/api/public/v1/...), Zuordnung je Statuscode wie bisher. Fertig, wenn: Die Tests in ConMeetClientTests.cs sind grün, die bestehenden Prüfungen aus 001 verhalten sich unverändert (ConMeetInstancesApiTests.cs).

S3 — Entität ActiveInstanceChoice und Löschregeln

Erfüllt: R20, R21, R27, R28, R29, R30, R31, R32, R92 (Index, EF-Schreibweg), R93, R106 (Löschen) Abhängig von: — · unabhängig von S2, kann parallel laufen Dateien: source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/Entity/ (neue Entität und …ModelConfiguration nach Muster InstanceGrant.cs, InstanceGrantModelConfiguration.cs), ConMeetInstancesService.cs, vertical.json, neue Migration unter source/conmeet-server/ConMeet.Service/Persistence/Migrations/, Tests unter source/conmeet-server/ConMeet.Core.Tests/Features/ConMeetInstances/ Zu tun:

  1. ActiveInstanceChoice : TenantOwnedEntity mit UserId, ConMeetInstanceId; eindeutiger Index (TenantId, UserId) mit UniqueWithIsDeletedFilter(); Fremdschlüssel auf Instanz.
  2. Migration über Aspire mit --migrate erzeugen/anwenden (AGENTS.md).
  3. RevokeAsync (ConMeetInstancesService.cs:199): Wahl nur löschen, wenn die Person danach ohne GrantedToAll nicht mehr berechtigt ist (R28); per PurgeAsync mit Audit-Zeile.
  4. SetGrantedToAllAsync (:180): beim Entfernen die Wahl aller danach nicht mehr Berechtigten in derselben Transaktion löschen (R29).
  5. DisconnectAsync (:138) und RemoveUserDataAsync (:232) um die Wahl erweitern (R30, R31).
  6. Interne Methode „aktive Instanz lesen“ mit Berechtigungsprüfung im selben Lesevorgang (R93); Wahl ohne Berechtigung gilt als nicht vorhanden.
  7. Tests: je Löschregel (Einzelentzug mit und ohne GrantedToAll, GrantedToAll entfernen, Trennen, Benutzer entfernt, Mandantenwechsel), Audit-Zeile vorhanden, Persistenz/Index (ConMeetInstancesPersistenceTests.cs). Fertig, wenn: Alle Löschfälle aus R28–R31 sind getestet und grün; vertical.json nennt die Entität.

S4 — Contract DoOnConMeetInstance

Erfüllt: R81, R82, R83 (Schlüssel bleibt in der Vertikale), R84 (Lebensdauer des Clients), R85, R86 (Ablage in den Contracts) Abhängig von: S2, S3 Dateien: neues Contracts-Verzeichnis der Vertikale source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/ nach Konvention des Skills validdata-vertical-generator (Interface, ConMeetInstanceSelector, ConMeetInstanceContext, Zuordnungsfunktion aus S2), Implementierung im Service oder daneben, ConMeetInstancesRegistry.cs, vertical.json (contracts), Tests Zu tun:

  1. Interface mit genau DoOnConMeetInstance<T>(selector, action, cancellationToken); Selector nur Default.
  2. Implementierung: aktive Instanz der angemeldeten Person nach S3 lesen; ohne aktive Instanz NO_ACTIVE_INSTANCE, ohne Verknüpfung ACCOUNT_NOT_LINKED werfen, Callback nicht aufrufen.
  3. Je Aufruf einen HttpClient mit der Kette aus S2 und entschlüsseltem Schlüssel nur im Handler bauen; BaseAddress = BaseUrl + /; nach dem Callback verwerfen.
  4. Fehlertypen NO_ACTIVE_INSTANCE als neue Exception der Vertikale; ACCOUNT_NOT_LINKED wiederverwenden (Exceptions/AccountNotLinkedException.cs).
  5. Tests: Callback erhält Id, Name, Client mit BaseAddress; Schlüssel nicht über Kontext oder DefaultRequestHeaders erreichbar; Fehler vor dem Callback; Client nach dem Callback unbrauchbar; Architekturtest, dass Fremdvertikalen nur den Contract nutzen (ConMeetInstancesArchitectureTests.cs, Architecture/LayerDirectionTests.cs). Fertig, wenn: Tests grün; vertical.json führt den Contract.

S5 — Mutationen ensureMyActiveConMeetInstance und setMyActiveConMeetInstance

Erfüllt: R22, R26 (Speichern), R62, R63, R92 (Konfliktbehandlung), R106, R108 (Serverseite) Abhängig von: S3 Dateien: source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/Api/ConMeetInstanceMutations.cs, Api/ConMeetInstanceResultTypes.cs, ConMeetInstancesService.cs, Api/ConMeetInstancesErrorFilter.cs, Schema-Snapshot (ConMeet.Core.Tests/SchemaSnapshot.cs), Tests Zu tun:

  1. Ergebnistyp: berechtigte Instanzen (id, name), aktive Instanz oder null, activeLinked.
  2. ensureMyActiveConMeetInstance: verwaiste Wahl löschen (R106), bei genau einer berechtigten Instanz anlegen (R22); Eindeutigkeitsverletzung abfangen und neu lesen (R92).
  3. setMyActiveConMeetInstance(instanceId): Berechtigung prüfen (INSTANCE_NOT_GRANTED, INSTANCE_NOT_FOUND), ändern oder anlegen; bei DbUpdateConcurrencyException einmal neu laden und speichern, danach gespeicherten Stand ohne Fehler liefern.
  4. Beide unter Standard-Policy, ohne Benutzer-ID-Argument.
  5. Tests: einzige Instanz wird aktiv und bleibt bei zweiter Berechtigung; keine Auswahl bei mehreren; verwaiste Wahl; gleichzeitiges Anlegen; gleichzeitiges Umschalten; fremde/ungrantete Instanz; Schema-Snapshot aktualisiert. Fertig, wenn: Tests in ConMeetInstancesApiTests.cs/ConMeetInstancesServiceTests.cs grün, Snapshot aktualisiert.

S6 — Vertikale CallInformation mit findCallerContacts

Erfüllt: R38, R39, R40, R41, R42, R43, R44, R45, R46, R47, R48, R49, R50, R51, R55, R60 (Link), R64, R65, R67, R68, R69, R76 (Dienst), R103 Abhängig von: S1, S4 Dateien: neu source/conmeet-server/ConMeet.Core/Features/CallInformation/ (Struktur nach validdata-vertical-generator: vertical.json, Service, Api/ mit Query, Ergebnistypen und Error-Filter, Exceptions/, Registry), source/conmeet-server/ConMeet.Service/Features/VerticalFeatures.cs, Tests unter ConMeet.Core.Tests/Features/CallInformation/ Zu tun:

  1. Normalisierung: tel: entfernen, Parse mit Region DE, IsPossibleNumber, Ausgabe E.164; ungültig → INVALID_PHONE_NUMBER ohne ConMeet-Aufruf.
  2. Suche im Callback von DoOnConMeetInstance(Default): Kundenseiten nacheinander (limit=200, offset), Kontaktlisten mit höchstens 4 gleichzeitigen Aufrufen, 404 je Kunde überspringen, jede andere Nicht-2xx-Antwort und Übertragungsfehler über die Funktion aus S2 → CONMEET_REQUEST_FAILED mit Prüfergebnis (Muster ConMeetCheckFailedException und Error-Filter aus 001).
  3. Gesamtgrenze 10 s ab Eingang, verknüpft mit dem Abbruch der HTTP-Anfrage (RequestAborted); Ablauf → SEARCH_TIMEOUT.
  4. Treffer nach R47–R50 bilden, Zentrale nur ohne Kontakttreffer; Sortierung nach R58 serverseitig; conMeetUrl aus ctx.Http.BaseAddress + home/master-data/customers/{id}/dashboard.
  5. Query findCallerContacts(phoneNumber) unter Standard-Policy; Ergebnisfelder nach R64; Error-Codes nach R65.
  6. Datenschutz: keine Rufnummern, Namen oder E-Mails in Logs und Fehlermeldungen; prüfen, dass AddInstrumentation() in ConMeet.Service/Features/GraphQLFeature.cs:25 keine Variablen oder Dokumente mit Werten in Spans schreibt, sonst für diese Operation abschalten; Antworttext nur bei Fehlern (R68).
  7. Tests mit Fake-Handler nach Muster ConMeetClientTests.cs: Beispiel aus der Spezifikation (60 Kunden, Treffer Friedrich Meyer über FIX_PHONE, Zentrale-Treffer 5350), Schreibweisen 02871/2345670 und +49 2871 2345670, beide Felder → FIX_PHONE, 404 übersprungen, 401/429/5xx/Netzwerkfehler, Gesamtgrenze, Abbruch, ungültige Nummer ohne Aufruf, keine Speicherung; Architekturtest der neuen Vertikale; Schema-Snapshot. Fertig, wenn: Alle genannten Tests grün; LayerDirectionTests und Schema-Snapshot grün.

S7 — Keycloak-Client vd-suite-conmeet-teams und Konfiguration

Erfüllt: R96 Abhängig von: — · parallel zu S1–S6 Dateien: keycloak/vd-suite-realm.json, source/conmeet-server/ConMeet.AppHost/AppHost.cs, source/conmeet-webapp/src/env.ts Zu tun:

  1. Client nach Vorbild vd-suite-conmeet-web (keycloak/vd-suite-realm.json:696): vertraulich, PKCE S256, Redirect-URI https://conmeet.dev.localhost:15180/teams/auth/callback, Standard-Scope vd-suite-conmeet-api, offline_access optional, lokales Entwicklungs-Secret.
  2. AppHost: Umgebungsvariablen für Client-ID, Secret und Redirect-URI des Teams-Clients an die Webapp; lokal das Entwicklungs-Secret, entfernt neuer Parameter suite-oidc-teams-client-secret neben suite-oidc-client-secret (AppHost.cs:160).
  3. src/env.ts: Variablen für den Teams-Client deklarieren.
  4. Lokalen Zustand nach geänderter Realm-Datei zurücksetzen (AGENTS.md, „Lokalen Zustand zurücksetzen“). Fertig, wenn: Nach aspire start existiert der Client im Realm; ein Token des Teams-Clients wird vom Dienst angenommen (me antwortet).

S8 — Sitzungsmodell im BFF: Client je Sitzung, zwei locals

Erfüllt: R74 (BFF), R91, R95 (Sitzung für me), R97, R105 Abhängig von: S7 Dateien: source/conmeet-webapp/src/lib/server/db/schema.ts, neue Drizzle-Migration unter source/conmeet-webapp/drizzle/, src/lib/server/auth.ts, src/hooks.server.ts, src/app.d.ts, src/lib/server/graphql/client.ts, src/lib/server/suite-access.ts, Tests (*.spec.ts nach Muster src/lib/features/users/suite-session.spec.ts) Zu tun:

  1. sessions.client (web | teams, Standard web) und neue Tabelle teams_auth_codes (Hash des Codes, Hash von state, sub, verschlüsselte Tokens, Ablauf); oauth_states um den Hash des Teams-state und den Client erweitern.
  2. Hook: locals.session nur aus __Host-session mit Client web, locals.teamsSession nur aus __Host-teams-session mit Client teams; Verlängerung je Cookie mit gleicher Laufzeit; Cookie-Optionen für __Host-teams-session (SameSite=None, Partitioned).
  3. Funktion „Sitzung der Anrufinformation“ (teamsSession ?? session); GraphQL-Client und getAccessTokenForSession erhalten die Sitzung explizit statt aus locals.session (client.ts:47).
  4. loadSuiteAccess (suite-access.ts:30) mit übergebener Sitzung; Sitzungsende nach WP4 beendet nur diese Sitzung.
  5. handleAuthorization (auth.ts:400): Gruppe (teams) von der Weiterleitung ausnehmen; Remote Requests bleiben ohne Weiterleitung und antworten ohne Sitzung mit Fehler.
  6. Tests: Cookie des falschen Clients gilt nicht; Admin-Remote-Functions erkennen __Host-teams-session nicht; Sitzungsende einer Seite lässt die andere bestehen. Fertig, wenn: Tests grün; die bestehenden E2E-Tests (src/routes/access.e2e.ts) laufen unverändert.

S9 — Anmeldung und Abmeldung der Teams-App

Erfüllt: R11 (Anzeige ohne Weiterleitung, Serverseite), R12, R13, R14, R15, R17, R18, R72, R73, R89, R90, R94, R98, R99, R100, R101 (Erkennung), R107 Abhängig von: S8 Dateien: neu source/conmeet-webapp/src/routes/(teams)/teams/auth/start/+server.ts, …/callback/+page.server.ts und +page.svelte, …/exchange/+server.ts, Abmelde-Endpunkt bzw. Remote Function unter src/lib/features/call-information/; Erweiterungen in src/lib/server/auth.ts (Code Flow je Client nach Muster oidcLogin, handleOidcCallback) Zu tun:

  1. /teams/auth/start: state prüfen (base64url, 32 Bytes; sonst Hinweis aus R99), Hash mit PKCE-Verifier und eigenem OIDC-state in oauth_states, Weiterleitung zu Keycloak mit dem Teams-Client.
  2. /teams/auth/callback: Code Flow abschließen, Einmalcode erzeugen, Tokens verschlüsselt in teams_auth_codes, keine Sitzung; Seite übergibt den Code nur per authentication.notifySuccess(), ohne Teams-Kontext verwerfen und R99 zeigen; Fehler → notifyFailure().
  3. /teams/auth/exchange: nur POST mit eigenem Origin; Code und state prüfen, Teams-Sitzung anlegen, Tokens übernehmen, Code löschen, __Host-teams-session setzen; abgelaufene Codes bei jedem Erzeugen und Einlösen löschen.
  4. Keine Query-Strings von /teams/auth/* in Logs (eigene Logausgaben in auth.ts, z. B. :406, prüfen).
  5. Abmelden: Teams-Sitzung mit Tokens löschen, Refresh-Token am Revocation Endpoint widerrufen, Cookie löschen; außerhalb von Teams logOut (auth.ts:303).
  6. Tests: Code einmalig und 60 s gültig; falscher state; Origin-Prüfung; Abmelden lässt Webseiten-Sitzung bestehen. Fertig, wenn: Unit-Tests grün; im Browser außerhalb von Teams führt die Anmeldung über R94 zurück zu /teams/call; in Teams entsteht nach der Anmeldung __Host-teams-session.

S10 — Einbettung per frame-ancestors

Erfüllt: R4, R5 Abhängig von: — · parallel zu S8, S9 Dateien: source/conmeet-webapp/src/hooks.server.ts (eigener Handle in der sequence) Zu tun:

  1. Antwort-Header Content-Security-Policy: frame-ancestors mit den Teams-Hosts nur für /teams/call, frame-ancestors 'none' für alle übrigen Antworten. Fertig, wenn: E2E-Test prüft beide Header (/teams/call und /).

S11 — Grundgerüst /teams/call: Layout, Teams-SDK, Zustände 1–2a

Erfüllt: R1, R6, R7, R8, R9, R19, R33 (Reihenfolge), R71, R80, R95 (Layout), R11/R94/R101 (Anzeige) Abhängig von: S8, S9, S10 Dateien: neu source/conmeet-webapp/src/routes/(teams)/+layout.server.ts, +layout.svelte, teams/call/+page.svelte; neu src/lib/features/call-information/ (Teams-Kontext nach Vorbild des Prototyps Q4 src/lib/teams.svelte.ts, Komponenten) Zu tun:

  1. Layout ohne App-Shell; Layout-Load ruft loadSuiteAccess mit der Sitzung der Anrufinformation, ohne Weiterleitung; liefert me oder „keine Sitzung“.
  2. Teams-SDK im Browser laden (app.initialize, Kontext, Farbschema mit Wechsel, meeting.getMeetingDetailsVerbose()); außerhalb von Teams Browser-Modus.
  3. Zustandsanzeige nach R33 für Zustände 1, 2, 2a (Texte aus R11, R19, R101, R108) inkl. „Anmelden“ (in Teams authentication.authenticate() mit state, außerhalb /auth/start?returnTo=/teams/call) und „Abmelden“ ab Zustand 2.
  4. Externe Links über app.openLink() bzw. neuen Tab (R80).
  5. Layout einspaltig ab 320 px, validdata-Styleguide. Fertig, wenn: Außerhalb von Teams zeigt /teams/call ohne Sitzung „Anmelden“, nach Anmeldung den Zustand ab 3; E2E-Test für beide Fälle nach Muster access.e2e.ts.

S12 — Aktive Instanz in der Anrufinformation

Erfüllt: R22, R23, R24, R25, R26 (Anzeige), R61, R87, R108 (Anzeige) Abhängig von: S5, S11 Dateien: source/conmeet-webapp/src/lib/features/call-information/ (.graphql, .remote.ts mit command, Komponenten), generierter GraphQL-Code Zu tun:

  1. Remote Functions ensureActiveInstance und setActiveInstance als command mit der Sitzung der Anrufinformation.
  2. Zustände 3 und 4; Auswahl oben nur bei mindestens zwei berechtigten Instanzen, Name der aktiven sichtbar; bei genau einer nichts.
  3. Fehler nach R108 und Neuermittlung nach R87. Fertig, wenn: Mit den Personas des Simulators zeigt eine Person mit einer Instanz keine Auswahl, eine mit zwei die Auswahl; Umschalten speichert und löst die Suche aus (nach S13).

S13 — Suche und Treffer

Erfüllt: R10, R36, R37, R56, R57, R58 (Anzeige), R59, R66, R70, R75, R77, R78, R79 Abhängig von: S6, S12 Dateien: source/conmeet-webapp/src/lib/features/call-information/ (find-caller.graphql, .remote.ts, Komponenten Rufnummernfeld, Trefferliste, Fehleranzeige), src/lib/server/graphql/client.ts bzw. run.ts Zu tun:

  1. Remote Function findCaller als command; GraphQL-Anfrage als POST erzwingen (preferGetMethod: false für diese Operation), damit die Nummer in keiner URL steht.
  2. Rufnummernfeld als eigenständige Komponente ab Zustand 6, vorbelegt mit der Teams-Nummer; Fehler INVALID_PHONE_NUMBER am Feld, für Teams-Nummern Anzeige aus R41.
  3. Zustände 6–10; Anzeige je Kontakt bzw. Zentrale nach R56/R57; matchedBy nicht anzeigen; gesuchte Nummer über der Liste.
  4. Sperre von „Suchen“, „Erneut suchen“ und Instanzauswahl während einer Suche; veraltete Ergebnisse verwerfen.
  5. Fehleranzeigen nach R36 inkl. SEARCH_TIMEOUT, RATE_LIMITED-Text aus R55 und „Schlüssel ersetzen“ bei KEY_REJECTED.
  6. Tests: Komponententests für Feld und Trefferliste (Vitest), Zustandslogik als reine Funktion mit Spec nach Muster src/lib/features/users/access-state.spec.ts. Fertig, wenn: Im Browser außerhalb von Teams liefert die Eingabe 02871/2345670 gegen eine Test-Instanz den passenden Kontakt; während der Suche ist „Suchen“ gesperrt; Tests grün.

S14 — Verknüpfen und Schlüssel ersetzen in der Anrufinformation

Erfüllt: R34, R35 Abhängig von: S12 · parallel zu S13 Dateien: source/conmeet-webapp/src/lib/features/call-information/ (eigene Remote Functions für Verknüpfen und Ersetzen), Wiederverwendung von src/lib/features/conmeet-instances/link-sheet.svelte und personal-key-field.svelte Zu tun:

  1. Remote Functions mit derselben GraphQL-Operation wie src/lib/features/conmeet-instances/my-connections.remote.ts (LinkConMeetAccountDocument), aber mit der Sitzung der Anrufinformation.
  2. „Jetzt verknüpfen“ und „Schlüssel ersetzen“ öffnen das Verknüpfen in der Anrufinformation; danach sofort suchen.
  3. Schlüsselfeld: misslingt „Einfügen“, Hinweis „Bitte mit Strg+V einfügen.“ bzw. „⌘+V“ unter macOS; Einfügen per Tastatur bleibt (personal-key-field.svelte:29). Fertig, wenn: In der Anrufinformation lässt sich eine nicht verknüpfte aktive Instanz verknüpfen, danach erscheint das Suchergebnis.

S15 — Teams-App-Manifest und Paketskript

Erfüllt: R2, R3, R102 Abhängig von: — · parallel zu allen anderen Dateien: neu source/conmeet-webapp/teams/ (Manifest-Vorlage, Icons), Skript nach Vorbild Q4 scripts/package-teams.mjs in source/conmeet-webapp/scripts/, Eintrag in package.json Zu tun:

  1. Manifest nach Q4 teams/manifest.json: staticTab /teams/call, Scope personal, Kontext personalTab, meetingSidePanel; RSC-Berechtigungen aus R3; ohne die konfigurierbaren Meeting-Tabs von Q4.
  2. Skript setzt Host des BFF und Host von Keycloak je Umgebung in contentUrl, websiteUrl und validDomains und packt das ZIP. Fertig, wenn: Das erzeugte Paket lässt sich in Teams hochladen; im 1:1-Anruf erscheint die App in der Seitenleiste und liest die Rufnummer.

Reihenfolge

S1 ─────────────────────────┐
S2 ──┐                      ├─► S6 ──┐
S3 ──┼─► S4 ────────────────┘        │
     └─► S5 ──────────────┐          │
S7 ─► S8 ─► S9 ─┐         ├─► S12 ─┬─► S13
S10 ────────────┴─► S11 ──┘        └─► S14
S15 (unabhängig)

S1, S2, S3, S7, S10 und S15 sind voneinander unabhängig und können parallel beginnen. Backend (S2–S6) und BFF-Anmeldung (S7–S9) laufen parallel; S12 braucht beide Stränge. S13 und S14 sind nach S12 unabhängig.

Offene Entscheidungen

  • Aufrufe je Sekunde: Der Standardwert 10 (R53) ist angenommen; bis zur Antwort von ConMeet bleibt er konfigurierbar (S2). Für Tests eine Instanz mit höchstens rund 95 Kunden nutzen (RB-001).
  • Inaktive Kunden: Die Spezifikation sucht vorerst in allen Kunden (R42); kein is_active-Filter in S6.
  • Einwilligung in Teams: Ob für R3 eine eigene Entra-App-Registrierung nötig ist, ist offen; S15 übernimmt das Vorgehen des Prototyps (Teams-App-ID) und prüft es am Mandanten.
  • Client bei Core: Der Client vd-suite-conmeet-teams muss für jede entfernte Suite-Umgebung bei Core angefragt werden (R96); lokal reicht S7.
  • Code ohne Anforderung: source/conmeet-server/ConMeet.Core/Features/ConMeetInstances/ConMeetInstancesService.cs:242 lädt in MyInstancesAsync alle Instanzen in eine unbenutzte Variable (rr) — vermutlich ein Überbleibsel; Entfernen gehört zu 001, nicht in diesen Plan.
  • Stand von 001: Die Änderungen an ConMeetInstances und der Webapp aus 001 liegen teils uncommittet vor. Vor S2–S5 sollte dieser Stand festgeschrieben sein, sonst plant 002 gegen einen bewegten Stand.

Nicht im Plan

  • Verteilung und Anheften über Teams-Store und Einrichtungsrichtlinie (US-09) — Nicht im Scope.
  • Ein Cache oder eine Synchronisation der Kontakte — ausgeschlossen durch R69.
  • Abbruch einer laufenden Suche aus dem Browser — R75 sperrt stattdessen.
  • Umbau der bestehenden Webseiten-Anmeldung über das für R91/R105 Nötige hinaus.