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:
libphonenumber-csharp(neueste stabile Version) zentral eintragen und inConMeet.Corereferenzieren; Lizenz nach Skillvaliddata-dotnet-package-licensesprüfen und Zeile indocs/nuget-licenses.mdergänzen.@microsoft/teams-js(neueste stabile Version) als Abhängigkeit der Webapp aufnehmen. Fertig, wenn: Restore und Build beider Projekte laufen;docs/nuget-licenses.mdfü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:
- Begrenzung als
DelegatingHandlermitPartitionedRateLimiterausSystem.Threading.RateLimiting: Token Bucket, Kapazität 1, Nachfüllen alle1 / ConMeet:RequestsPerSeconds (Standard 10), Partition = normalisierteBaseUrl(Ergebnis vonNormalizeBaseUrl,ConMeetInstancesService.cs:362); Warteschlange ohne Verwerfen, Warten mit dem Cancellation Token der Anfrage. Singleton über alle Anfragen. - Zeitüberschreitung je Aufruf (10 s, heute
ConMeetClient.Timeout) als eigener Handler innerhalb der Begrenzung, sodass Warten nicht zählt (R54). - Schlüssel-Handler: setzt
Authorization: Bearerje Anfrage nur, wenn die URI unter derBaseAddressliegt; sonst Abbruch ohne Senden (R104). - Zuordnung „Antwort/Übertragungsfehler →
ConnectionCheck“ ausCheckAsync(ConMeetClient.cs:31) in eine statische Funktion herauslösen; Fehler aus R104 ergibtUnexpectedohne Statuscode. CheckAsyncauf dieselbe Kette umstellen (001-R95 läuft durch die Begrenzung); Adressbildung überBaseAddressmit abschließendem/und relativen Pfaden ohne führendes/(R84). Die Ausnahme vom Resilience-Handler (ConMeetClient.cs:62) bleibt.- 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,BaseUrlmit Pfad bleibt erhalten (https://host/conmeet/api/public/v1/...), Zuordnung je Statuscode wie bisher. Fertig, wenn: Die Tests inConMeetClientTests.cssind 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:
ActiveInstanceChoice : TenantOwnedEntitymitUserId,ConMeetInstanceId; eindeutiger Index(TenantId, UserId)mitUniqueWithIsDeletedFilter(); Fremdschlüssel auf Instanz.- Migration über Aspire mit
--migrateerzeugen/anwenden (AGENTS.md). RevokeAsync(ConMeetInstancesService.cs:199): Wahl nur löschen, wenn die Person danach ohneGrantedToAllnicht mehr berechtigt ist (R28); perPurgeAsyncmit Audit-Zeile.SetGrantedToAllAsync(:180): beim Entfernen die Wahl aller danach nicht mehr Berechtigten in derselben Transaktion löschen (R29).DisconnectAsync(:138) undRemoveUserDataAsync(:232) um die Wahl erweitern (R30, R31).- Interne Methode „aktive Instanz lesen“ mit Berechtigungsprüfung im selben Lesevorgang (R93); Wahl ohne Berechtigung gilt als nicht vorhanden.
- Tests: je Löschregel (Einzelentzug mit und ohne
GrantedToAll,GrantedToAllentfernen, 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.jsonnennt 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:
- Interface mit genau
DoOnConMeetInstance<T>(selector, action, cancellationToken); Selector nurDefault. - Implementierung: aktive Instanz der angemeldeten Person nach S3 lesen; ohne aktive Instanz
NO_ACTIVE_INSTANCE, ohne VerknüpfungACCOUNT_NOT_LINKEDwerfen, Callback nicht aufrufen. - Je Aufruf einen
HttpClientmit der Kette aus S2 und entschlüsseltem Schlüssel nur im Handler bauen;BaseAddress=BaseUrl+/; nach dem Callback verwerfen. - Fehlertypen
NO_ACTIVE_INSTANCEals neue Exception der Vertikale;ACCOUNT_NOT_LINKEDwiederverwenden (Exceptions/AccountNotLinkedException.cs). - Tests: Callback erhält Id, Name, Client mit
BaseAddress; Schlüssel nicht über Kontext oderDefaultRequestHeaderserreichbar; 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.jsonfü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:
- Ergebnistyp: berechtigte Instanzen (
id,name), aktive Instanz odernull,activeLinked. ensureMyActiveConMeetInstance: verwaiste Wahl löschen (R106), bei genau einer berechtigten Instanz anlegen (R22); Eindeutigkeitsverletzung abfangen und neu lesen (R92).setMyActiveConMeetInstance(instanceId): Berechtigung prüfen (INSTANCE_NOT_GRANTED,INSTANCE_NOT_FOUND), ändern oder anlegen; beiDbUpdateConcurrencyExceptioneinmal neu laden und speichern, danach gespeicherten Stand ohne Fehler liefern.- Beide unter Standard-Policy, ohne Benutzer-ID-Argument.
- 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.csgrü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:
- Normalisierung:
tel:entfernen,Parsemit RegionDE,IsPossibleNumber, Ausgabe E.164; ungültig →INVALID_PHONE_NUMBERohne ConMeet-Aufruf. - 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_FAILEDmit Prüfergebnis (MusterConMeetCheckFailedExceptionund Error-Filter aus 001). - Gesamtgrenze 10 s ab Eingang, verknüpft mit dem Abbruch der HTTP-Anfrage (
RequestAborted); Ablauf →SEARCH_TIMEOUT. - Treffer nach R47–R50 bilden, Zentrale nur ohne Kontakttreffer; Sortierung nach R58 serverseitig;
conMeetUrlausctx.Http.BaseAddress+home/master-data/customers/{id}/dashboard. - Query
findCallerContacts(phoneNumber)unter Standard-Policy; Ergebnisfelder nach R64; Error-Codes nach R65. - Datenschutz: keine Rufnummern, Namen oder E-Mails in Logs und Fehlermeldungen; prüfen, dass
AddInstrumentation()inConMeet.Service/Features/GraphQLFeature.cs:25keine Variablen oder Dokumente mit Werten in Spans schreibt, sonst für diese Operation abschalten; Antworttext nur bei Fehlern (R68). - Tests mit Fake-Handler nach Muster
ConMeetClientTests.cs: Beispiel aus der Spezifikation (60 Kunden, Treffer Friedrich Meyer überFIX_PHONE, Zentrale-Treffer 5350), Schreibweisen02871/2345670und+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;LayerDirectionTestsund 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:
- Client nach Vorbild
vd-suite-conmeet-web(keycloak/vd-suite-realm.json:696): vertraulich, PKCE S256, Redirect-URIhttps://conmeet.dev.localhost:15180/teams/auth/callback, Standard-Scopevd-suite-conmeet-api,offline_accessoptional, lokales Entwicklungs-Secret. - 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-secretnebensuite-oidc-client-secret(AppHost.cs:160). src/env.ts: Variablen für den Teams-Client deklarieren.- Lokalen Zustand nach geänderter Realm-Datei zurücksetzen (AGENTS.md, „Lokalen Zustand zurücksetzen“).
Fertig, wenn: Nach
aspire startexistiert der Client im Realm; ein Token des Teams-Clients wird vom Dienst angenommen (meantwortet).
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:
sessions.client(web|teams, Standardweb) und neue Tabelleteams_auth_codes(Hash des Codes, Hash vonstate,sub, verschlüsselte Tokens, Ablauf);oauth_statesum den Hash des Teams-stateund den Client erweitern.- Hook:
locals.sessionnur aus__Host-sessionmit Clientweb,locals.teamsSessionnur aus__Host-teams-sessionmit Clientteams; Verlängerung je Cookie mit gleicher Laufzeit; Cookie-Optionen für__Host-teams-session(SameSite=None,Partitioned). - Funktion „Sitzung der Anrufinformation“ (
teamsSession ?? session); GraphQL-Client undgetAccessTokenForSessionerhalten die Sitzung explizit statt auslocals.session(client.ts:47). loadSuiteAccess(suite-access.ts:30) mit übergebener Sitzung; Sitzungsende nach WP4 beendet nur diese Sitzung.handleAuthorization(auth.ts:400): Gruppe(teams)von der Weiterleitung ausnehmen; Remote Requests bleiben ohne Weiterleitung und antworten ohne Sitzung mit Fehler.- Tests: Cookie des falschen Clients gilt nicht; Admin-Remote-Functions erkennen
__Host-teams-sessionnicht; 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:
/teams/auth/start:stateprüfen (base64url, 32 Bytes; sonst Hinweis aus R99), Hash mit PKCE-Verifier und eigenem OIDC-stateinoauth_states, Weiterleitung zu Keycloak mit dem Teams-Client./teams/auth/callback: Code Flow abschließen, Einmalcode erzeugen, Tokens verschlüsselt inteams_auth_codes, keine Sitzung; Seite übergibt den Code nur perauthentication.notifySuccess(), ohne Teams-Kontext verwerfen und R99 zeigen; Fehler →notifyFailure()./teams/auth/exchange: nur POST mit eigenemOrigin; Code undstateprüfen, Teams-Sitzung anlegen, Tokens übernehmen, Code löschen,__Host-teams-sessionsetzen; abgelaufene Codes bei jedem Erzeugen und Einlösen löschen.- Keine Query-Strings von
/teams/auth/*in Logs (eigene Logausgaben inauth.ts, z. B.:406, prüfen). - Abmelden: Teams-Sitzung mit Tokens löschen, Refresh-Token am Revocation Endpoint widerrufen, Cookie löschen; außerhalb von Teams
logOut(auth.ts:303). - 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:
- Antwort-Header
Content-Security-Policy: frame-ancestorsmit 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/callund/).
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:
- Layout ohne App-Shell; Layout-Load ruft
loadSuiteAccessmit der Sitzung der Anrufinformation, ohne Weiterleitung; liefertmeoder „keine Sitzung“. - Teams-SDK im Browser laden (
app.initialize, Kontext, Farbschema mit Wechsel,meeting.getMeetingDetailsVerbose()); außerhalb von Teams Browser-Modus. - Zustandsanzeige nach R33 für Zustände 1, 2, 2a (Texte aus R11, R19, R101, R108) inkl. „Anmelden“ (in Teams
authentication.authenticate()mitstate, außerhalb/auth/start?returnTo=/teams/call) und „Abmelden“ ab Zustand 2. - Externe Links über
app.openLink()bzw. neuen Tab (R80). - Layout einspaltig ab 320 px, validdata-Styleguide.
Fertig, wenn: Außerhalb von Teams zeigt
/teams/callohne Sitzung „Anmelden“, nach Anmeldung den Zustand ab 3; E2E-Test für beide Fälle nach Musteraccess.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:
- Remote Functions
ensureActiveInstanceundsetActiveInstancealscommandmit der Sitzung der Anrufinformation. - Zustände 3 und 4; Auswahl oben nur bei mindestens zwei berechtigten Instanzen, Name der aktiven sichtbar; bei genau einer nichts.
- 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:
- Remote Function
findCalleralscommand; GraphQL-Anfrage als POST erzwingen (preferGetMethod: falsefür diese Operation), damit die Nummer in keiner URL steht. - Rufnummernfeld als eigenständige Komponente ab Zustand 6, vorbelegt mit der Teams-Nummer; Fehler
INVALID_PHONE_NUMBERam Feld, für Teams-Nummern Anzeige aus R41. - Zustände 6–10; Anzeige je Kontakt bzw. Zentrale nach R56/R57;
matchedBynicht anzeigen; gesuchte Nummer über der Liste. - Sperre von „Suchen“, „Erneut suchen“ und Instanzauswahl während einer Suche; veraltete Ergebnisse verwerfen.
- Fehleranzeigen nach R36 inkl.
SEARCH_TIMEOUT,RATE_LIMITED-Text aus R55 und „Schlüssel ersetzen“ beiKEY_REJECTED. - 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 Eingabe02871/2345670gegen 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:
- Remote Functions mit derselben GraphQL-Operation wie
src/lib/features/conmeet-instances/my-connections.remote.ts(LinkConMeetAccountDocument), aber mit der Sitzung der Anrufinformation. - „Jetzt verknüpfen“ und „Schlüssel ersetzen“ öffnen das Verknüpfen in der Anrufinformation; danach sofort suchen.
- 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:
- Manifest nach Q4
teams/manifest.json:staticTab/teams/call, Scopepersonal, KontextpersonalTab,meetingSidePanel; RSC-Berechtigungen aus R3; ohne die konfigurierbaren Meeting-Tabs von Q4. - Skript setzt Host des BFF und Host von Keycloak je Umgebung in
contentUrl,websiteUrlundvalidDomainsund 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-teamsmuss 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:242lädt inMyInstancesAsyncalle Instanzen in eine unbenutzte Variable (rr) — vermutlich ein Überbleibsel; Entfernen gehört zu 001, nicht in diesen Plan. - Stand von 001: Die Änderungen an
ConMeetInstancesund 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.