Add Contact Center module - #501
Open
MikeAlhayek wants to merge 339 commits into
Open
Conversation
MikeAlhayek
commented
Jun 29, 2026
MikeAlhayek
commented
Jun 29, 2026
MikeAlhayek
commented
Jun 29, 2026
MikeAlhayek
commented
Jun 29, 2026
|
This pull request has merge conflicts. Please resolve those before requesting a review. |
|
This pull request has merge conflicts. Please resolve those before requesting a review. |
|
This pull request has merge conflicts. Please resolve those before requesting a review. |
Use the shared CrestApps bootstrap-select implementation throughout Contact Center, preserve bulk queue and campaign selection, and guard the resource contract with a regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Call out that existing ACS-enabled tenants may auto-enable the Azure Email and SMS provider features and require post-upgrade configuration review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Pin the current session-blind routing behavior and missing server-swept after-call deadline so R3 can invert both invariants safely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: adb96b22-9043-4fb2-a5ac-79f3614af79d
Add a per-user, per-provider distributed lock around the Telephony OAuth token-refresh path so a provider that rotates its refresh token on first use can no longer lose the only valid replacement when several requests find the access token expiring at once. - Add TokenRefreshLockTimeout/TokenRefreshLockExpiration to TelephonyCoordinationOptions with startup validation ensuring the lease outlasts the wait window. - DefaultTelephonyAuthenticationService refreshes under the lock, reloads the current user (ITelephonyUserAccessor.ReloadCurrentUserAsync -> ISession.Detach) so a peer's committed refresh is observed instead of the stale scoped copy, and commits durably (SaveChangesAsync) before releasing the lock. - Commit failures degrade gracefully via TelephonyUserPersistenceException so the status probe never faults (consistent with OC-030). - Add a hardened concurrency test proving two racing callers trigger exactly one provider refresh. Independent gpt-5.6-sol review: APPROVE. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Asterisk and DialPad resilience pipelines retried on the standard resilience handler regardless of HTTP method, so a lost response to a call-origination POST could place a second outbound call and a retried OAuth authorization-code or refresh-token POST could fail with invalid_grant after the first request already succeeded. Both pipelines now call options.Retry.DisableForUnsafeHttpMethods() so POST/PATCH/PUT/ DELETE/CONNECT are never auto-replayed while idempotent safe methods still retry; neither provider exposes a deterministic idempotency key. A source-scanning architecture guard (ProviderHttpRetryArchitectureTests) dynamically discovers every source file under src that installs the standard resilience handler and requires each to disable unsafe-method retries (comment-stripped, per-invocation count). That guard surfaced the AbstractAPI, Veriphone, and Twilio Lookup phone-number-verification clients, which are GET-only today; the same fail-closed disable was applied so a future non-GET request cannot silently gain unsafe retries. Independent gpt-5.6-sol review: APPROVE (after hardening the guard from a hardcoded two-file list to a dynamic, comment-stripped, per-invocation count). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DialerPacingBackgroundTask and AgentAvailabilityRecoveryBackgroundTask each held a 60-second distributed lock while doing sequential work with no matching run deadline. A run that exceeded the lease let a second node begin an overlapping pacing cycle -- dropping the per-invocation cap that constrains aggregate call rate -- or race agent-state transitions. Both tasks now adopt the bounded-run pattern already proven in ReservationExpiryBackgroundTask: LockExpiration is raised to 120_000 (2x the one-minute schedule) and each run is bounded to a 90_000 ms wall-clock budget strictly below the lock expiration, enforced by a linked CancelAfter token that cancels in-flight work. DialerPacing additionally checks a between-profile IClock.UtcNow deadline and defers remaining profiles to the next tick. Shutdown cancellation is rethrown so the lease releases promptly; budget cancellation is logged at Debug and deferred. Tests: new DialerPacingBackgroundTaskTests (happy-path, quiescence, clock-advance budget-defer, shutdown propagation, metadata ordering guard) and updated AgentAvailabilityRecoveryBackgroundTaskTests (budgeted linked-token assertion + metadata ordering guard). Also fixes a CA1875 warning in ProviderHttpRetryArchitectureTests (Regex.Count) so the test project builds warning-clean under -warnaserror. Independent gpt-5.6-sol review: APPROVE. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hboard accessible The two real-time surfaces (agent workspace and supervisor dashboard) carried no ARIA semantics: offers, presence, queue depth, and the active-call/summary/ board regions were injected via innerHTML into containers with no live-region roles, the presence menu had no menu semantics or keyboard support, and failures surfaced through blocking window.alert() dialogs. A blind or low-vision agent received no announcement that a call was ringing. Agent workspace: - Incoming-offer container is role="alert" aria-live="assertive" aria-atomic and moves keyboard focus to Accept when a new offer renders. - Active-interaction, queue-chip regions are aria-live="polite" with accessible names; the per-second countdown and talk-time nodes are aria-hidden so state is announced once rather than every tick. - Presence control is a real ARIA menu: aria-haspopup/aria-expanded/aria-controls on the trigger, role="menu"/role="menuitem" children, first-item focus on open, Arrow/Home/End roving focus, Escape-to-close with focus return, click-away close. Supervisor dashboard: - Summary/tiles/board are aria-live="polite" with accessible names, and only re-render (and thus re-announce) when their content actually changes. Both surfaces: - Replace window.alert() with a non-blocking inline role="alert" error region. - Add a role="status" connection indicator driven by new lifecycle callbacks (onConnected/onReconnecting/onDisconnected) surfaced from the shared contact-center-realtime helper, which now hooks connection.onreconnecting and reports connect/close transitions. setConnectionStatus is idempotent so a restored connection announces "Connected" exactly once. - All new user-facing strings are localizable via the existing config dictionaries. Corrected the false "W6 agent-desktop accessibility completed" claim in .github/contact-center/PRODUCTION-READINESS.md to describe the actually-delivered agent-and-supervisor accessibility work (closes OC-038). Independent gpt-5.6-sol review: APPROVE. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The assignment loop and the voice/generic offer paths each materialized an entire queue's waiting backlog via QueueItemStore.ListWaitingAsync only to pick a single winner through QueueItemPrioritizer.SelectNext, producing roughly quadratic query traffic and allocations under a queue spike. Add a bounded IQueueItemStore.FindNextWaitingAsync (FirstOrDefaultAsync with the same Priority-desc, EnqueuedUtc-asc ordering as ListWaitingAsync) and IQueueItemManager.FindNextWaitingAsync(queue, utcNow). The manager routes to the bounded query whenever the queue does not apply SLA aging -- where an item's effective priority equals its base priority, so the store's first row is provably identical to SelectNext -- and falls back to the full in-memory scan only for queues that opt into SLA aging (wait-time reordering requires scoring every candidate). ListWaitingAsync is retained for the aging fallback and for OverflowDueAsync, which legitimately walks the whole backlog. Update the three call sites and the affected mocks; add QueueItemManagerTests proving the fast path uses only the bounded store query and the aging path scores the backlog. Update the ContactCenter.Core public API baseline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The supervisor dashboard refreshes every 10 seconds and previously issued several queries per queue and three queries per agent (active interaction, active count, and a user lookup for the display name), plus a per-queue and per-busy-agent supervisor authorization lookup and a per-agent monitoring-mode reload. A few hundred watched agents produced thousands of DB round-trips per minute per supervisor, competing with the routing hot paths. The poll now resolves the whole scoped set in fixed batches: - waiting depth via the existing batched CountWaitingByQueueIdsAsync - active interactions via a new index-backed ListActiveByAgentIdsAsync (chunked .IsIn query, most-recent-by-CreatedUtc per agent) - active counts via the existing CountActiveByAgentIdsAsync - display names by bulk-loading users with one chunked UserId.IsIn query and feeding each materialized user to IDisplayNameProvider.GetAsync (no further DB) Supervisor queue authorization is memoized per queue for the request, and a new GetAvailableModesAsync(Interaction) overload resolves engagement modes from the already-batched interaction instead of reloading it via FindByIdAsync. No new raw SQL was added, so the query-plan budget gate is unaffected. Remaining per-queue longest-wait/SLA reads are bounded residuals (queues are far fewer than agents). Adds ListActiveByAgentIdsAsync to IInteractionStore/IInteractionManager and their default implementations, and GetAvailableModesAsync(Interaction) to IContactCenterMonitoringService. Independently reviewed (gpt-5.6-sol: APPROVE). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The encrypted recording media store previously protected the full recording as one byte[] on write and, on read, copied the file into a MemoryStream, materialized it to an array, unprotected the whole array, and returned another MemoryStream -- several times the recording size in managed memory and LOH pressure. The Asterisk download side pulled the stored file into a byte array before handing it over. Recording media now streams end to end through a chunked authenticated- encryption container (RecordingMediaCryptoFormat + ChunkedAeadEncryptingReadStream / ChunkedAeadDecryptingReadStream). Envelope encryption: a per-recording random AES-256-GCM data key encrypts the media as independently authenticated 64 KiB frames, and that data key is wrapped by the data-protection provider, so key management (tenant isolation, rotation) stays with data protection while bulk media flows a fixed chunk at a time in both directions. Every frame binds its ordinal counter, length, and an end-of-stream marker into the AES-GCM associated data, so tampering, reordering, truncation, and trailing data are rejected on read (CryptographicException). RecordingMediaWriteRequest.Content changes from byte[] to Stream; the store reads and writes straight through IFileStore without a whole-recording copy. AsteriskAriClient.DownloadStoredRecordingAsync uses ResponseHeadersRead and returns an owning AsteriskAriStoredRecordingContent whose stream is await using-scoped across the store call; an empty chunked body (no Content-Length) is detected by a one-byte peek replayed through LeadingByteStream. The store's encrypting stream honors the operation cancellation token on every source read. Recording is off by default and the media format is new, so there is no stored media to migrate. Added multi-chunk, empty, tamper, truncation, and trailing- data tests; updated the public API baseline. Independently reviewed (gpt-5.6) and approved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reservation cleanup previously loaded every expired pending reservation in a single query and reporting materialized whole date ranges with no upper bound, so a spike or a wide report window could pull an unbounded result set into memory. Reservation cleanup now drains the expiry backlog in bounded, oldest-first pages using keyset (seek) paging over the stable (ExpiresUtc, DocumentId) order. ListExpiredAsync takes a keyset cursor and returns an ExpiredReservationPage; because the cursor is an absolute position rather than a numeric offset, concurrent expirations or insertions never shift the window, so a live reservation is never skipped and a locked oldest page never starves the drainable reservations behind it. Locked candidates are retried on the next scheduled sweep. Reporting now fails fast on an over-wide window instead of silently trimming rows (which would corrupt aggregate totals): a new ContactCenterReportingOptions.MaximumReportRange (bound from CrestApps_ContactCenter:Reporting, default 400 days, validated on start) is enforced by a shared guard at the top of every report query path, including the enterprise report providers. This changes the ListExpiredAsync signature on IActivityReservationStore and IActivityReservationManager and adds an IOptions<ContactCenterReportingOptions> constructor parameter to ContactCenterReportingService. Adds unit tests for multi-page draining and the anti-starvation case, a real-store integration test proving the keyset query pages in stable order against SQLite, and reporting range-guard tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nistration (OC-006) Both provider modules registered their site settings display driver in the un-attributed base Startup, even though the driver's SettingsGroupId (TelephonyConstants.SettingsGroupId) only has an admin menu entry contributed by the Telephony Administration feature. This gave headless deployments admin drivers they did not want, and — when the provider was enabled without Telephony Administration — a settings editor with no navigation entry reachable only by guessing the groupId URL. Move each AddSiteDisplayDriver call into a dedicated [RequireFeatures(TelephonyConstants.Feature.Admin)]-gated AsteriskAdminStartup / DialPadAdminStartup class so the provider settings tab and its telephony-group navigation entry always appear together. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Contact Center catalog and entitlement POST actions carry no explicit antiforgery attribute; they rely on Orchard Core's globally registered AutoValidateAntiforgeryToken filter. That reliance was assumed rather than asserted. Add AdminPostAntiforgeryArchitectureTests, a reflection-based architecture test that enumerates every concrete MVC controller in the ContactCenter, Telephony, Asterisk and DialPad module assemblies, treats every POST-capable action (an unsafe verb constraint, an [AcceptVerbs] provider, or no verb constraint at all) as in scope, and asserts none opts out of antiforgery via [IgnoreAntiforgeryToken] on the action or its controller. Because the only way a cross-site POST could bypass the global filter is an explicit opt-out, proving no opt-out exists proves the coverage holds. A companion test pins the known catalog controllers so the scan can never silently degrade to zero actions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…elephony async paths
Cancellation was handled as an ordinary fault across the ContactCenter/Telephony/
Asterisk/DialPad async paths, and the Asterisk realtime listener accepted unbounded
WebSocket frames and could stall on a hostile peer's close handshake.
- Add `catch (OperationCanceledException) when (token.IsCancellationRequested) { throw; }`
guards before broad generic catches in DialPadTelephonyProvider (6 sites),
ContactCenterOutbox, and TelephonyInteractionReconciliationBackgroundTask so
host/shutdown cancellation is never masqueraded as a provider failure.
- Introduce tri-state AsteriskAgentChannelReadyOutcome (Ready/NotReady/Canceled)
replacing the bool WaitAsync result; move the cancellation-precedence check to run
immediately after Task.WhenAny so a simultaneous supersede(false)+cancel is not
misreported as NotReady. Update Conference/Transfer/Monitoring callers to throw OCE
on Canceled instead of returning a false no-answer disposition.
- Restructure the AsteriskContactCenterVoiceProvider connect path with a dedicated OCE
catch that compensates using the `bridgeCreateAttempted || originateAttempted`
ambiguity flag (mirroring the generic catch's OutcomeUnknown), RETAINING the durable
binding record so the age-gated reconciler can reclaim a late-committing resource.
- Cap accumulated realtime message size (MaxRealtimeMessageBytes, default 1 MiB,
ceiling 64 MiB, validated at startup) and add CloseSocketSafelyAsync using
CloseOutputAsync under a bounded 5s token for both MessageTooBig and NormalClosure
paths, so a hostile peer cannot stall the close indefinitely.
- Split the two challenged Won't-Fix sub-parts into tracked items OC-050 (binding-store
bounded/cancellable locking) and OC-051 (MustComplete bounded shutdown seam).
- Add enum + Canceled readiness tests and a cancellation-during-bridge-create regression
test asserting OCE propagation and record retention.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nter modules The repository conventions require a README.md for every module, but none of ContactCenter, Telephony, Asterisk, or DialPad had one. Adds a top-level README.md to each module documenting its purpose, a feature table with feature IDs verified against the module manifests and *Constants classes, a recipe-based installation snippet, configuration, usage, dependencies, and a link to the documentation site: - Telephony: provider-agnostic call layer, hub, soft phone, and provider authoring guidance. - Asterisk: provider, ARI/real-time listener, Contact Center voice/media adapters, and the single-active-process ARI ownership deployment constraint. Configuration section distinguishes the admin settings screen fields from the CrestApps:Asterisk:Coordination shell-configuration options. - DialPad: provider and Contact Center voice boundary; configuration lists the actual editor fields (environment, API token or OAuth credentials, caller id, user id, webhook signing secret). - ContactCenter: grouped feature tables for the full capability set, the five actual *.Admin features, and an installation recipe that enables the admin features the example needs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ocalized resource The approved external-transfer destinations editor (ContactCenterExternalTransferSettings.Edit.cshtml) rendered a ~57-line inline <script> block whose dynamically added rows hardcoded the "Display name" placeholder and "Remove destination" button title in English, so added rows were never localized and those strings were invisible to localization extraction. - Extract the add/remove/renumber logic into a Gulp-built asset, Assets/js/contact-center-external-transfer-settings.js (minified output under wwwroot/scripts), and register it in Assets.json. - Register it as the named resource contact-center-external-transfer-settings via a new ContactCenterExternalTransferResourceConfiguration, wired into the Contact Center Administration feature startup that already registers the settings driver. - Require the script with at="Foot" and pass a localized `strings` config object (displayName, removeDestination) through a data-config attribute, mirroring the AgentWorkspace/SupervisorDashboard pattern, so dynamically added rows honor the active culture. The row template HTML-attribute-encodes the injected strings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…API (OC-013) Move five storage/schema/projection scalars (CollectionName, CurrentEventSchemaVersion, MetricsProjectionHandlerId, MetricsProjectionVersion, ProviderNameLength) out of the public ContactCenterConstants in the Abstractions package into a new internal ContactCenterStorage class in ContactCenter.Core, so bumping an internal projection version or schema revision no longer forces a public-package version bump for consumers that only depend on the webhook and event contracts. The five constants keep the same CrestApps.OrchardCore.ContactCenter namespace, so the ~430 mechanical reference updates across Core/Module/Tests/ DistributedTests need no new usings. Core exposes its internals to the module and distributed-test assemblies via InternalsVisibleTo. The manual-call aggregate-type discriminator stays public: it is emitted as InteractionEvent.AggregateType on the published ManualDialSuppressed event and is part of the event contract webhook/workflow consumers may inspect. It moved into a new public ContactCenterConstants.AggregateTypes group, matching how the other event-contract discriminators (Events.*, Components.*) are grouped. SystemActor and the diagnostic Components taxonomy also remain public. Regenerate the ContactCenter.Abstractions and .Core public-API baselines to reflect the reduced surface and the two new InternalsVisibleTo grants. Also fix a pre-existing ContactCenterFeatureDependencyArchitectureTests failure introduced by OC-006's AsteriskAdminStartup: that startup carries [RequireFeatures] but no [Feature], so the test's parser maps it to the base Asterisk feature id and the base-startup .Single() lookup became ambiguous. Disambiguate by selecting the ungated base startup (RequiredFeatureIds.Count == 0). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the tenant-wide logout middleware (matched on two hardcoded account URLs) with an IPostConfigureOptions<CookieAuthenticationOptions> that chains the application cookie scheme's OnSigningOut and OnValidatePrincipal events, so agent presence sign-out and soft-phone credential revocation run whenever the cookie session ends by any mechanism, including external front-channel sign-out and security-stamp rejection. OnValidatePrincipal captures the user id before the prior handler and synchronizes only when the principal is rejected, covering the security-stamp case where OnSigningOut fires before HttpContext.User is populated. The synchronization body is isolated from the sign-out flow: all exceptions are swallowed and a bounded, request-independent CancellationTokenSource replaces RequestAborted, so a slow or failing cleanup can never abort logout and leave a live auth cookie. Centralize the per-revoker revocation loop into a shared internal SoftPhoneCredentialRevocation helper in Core, and call it from AgentSessionService.ExpireStaleAsync so the cleanup backstop revokes browser SIP credentials for pure cookie-expiry sessions too. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…er Administration The ContactCenter module declared five per-capability administration features (Agents.Admin, Queues.Admin, Dialer.Admin, Recording.Admin, EntryPoints.Admin), each depending on both the root Admin feature and its capability feature. None could ever be enabled in isolation, so they added state-space complexity with no independent activation value; several had one- or two-line startup bodies. Fold all five into the single Contact Center Administration feature. Their admin StartupBase classes and the eight admin controllers now carry [Feature(Admin)] + [RequireFeatures(<capability>)], so every administration registration belongs to the single Admin feature and each capability's screens light up only when both Admin and that capability are enabled. This is verified equivalent to the old design: OrchardCore's CompositionStrategy skips any exported type (startup or controller) whose [RequireFeatures] set is not fully enabled, and RequireFeaturesAttribute returns 404 for a routed admin controller whose capability is disabled. - Manifest.cs: removed 5 .Admin Feature declarations (24 -> 19 features) - ContactCenterConstants.cs: removed 5 Feature.<Capability>Admin constants - Startup.cs + 8 controllers: [Feature(Admin)] + [RequireFeatures(<capability>)] - feature-dependency-violations.v1.json: removed 5 admin closure blocks - ContactCenterHeadlessClosureTests: trimmed _userExperienceFeatures; replaced the obsolete per-.Admin-feature test with two proofs (Admin+capability registers strictly more surface than Admin alone; capability without Admin registers no surface) - ContactCenterFeatureDependencyArchitectureTests: entry-point nav owner is now Admin requiring EntryPoints - Regenerated Abstractions public-API baseline; updated README, docs, plan, changelog Builds 0 warnings; ContactCenter unit (1372), FeatureActivation (58), PublicApi (33), architecture tests all pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-032) Provider revocation failures were previously swallowed while the local tokens were deleted as if revocation had succeeded, leaving an external grant active with no retained state or signal to the caller. - ITelephonyAuthenticationProvider.RevokeTokensAsync now returns a typed TelephonyResult: confirmed Success (2xx / nothing to revoke), definitive Failed (non-ambiguous 4xx), or indeterminate Unknown (timeout 408, throttling 429, 5xx, or transport error) because the unsafe deauthorize POST may still have committed. Reuses the existing IsAmbiguousStatusCode classifier. DialPad attempts revocation whenever a stored access token exists, regardless of the current authentication mode, so a switch to API-key auth cannot abandon a live OAuth grant. - ITelephonyAuthenticationService.DisconnectAsync now returns that result. It removes the local tokens first and durably commits the deletion via SaveChangesAsync before the remote revocation, so the credentials are cleared immediately, concurrent scopes cannot observe stale tokens, and a canceled/failing remote call cannot leave the local tokens behind. When a live token exists but no per-user authentication provider is available to revoke it, it returns Unknown instead of a false Success. Non-confirmed revocation is logged with the provider name and reason. - TelephonyOAuthController.Disconnect relays the outcome as remoteRevocationConfirmed: false with the reason when the grant could not be confirmed revoked. Adds tests covering Success/Failed/Unknown per status code, revocation after a switch to API-key mode, local-token clearing on failure/exception, and the Unknown-on-missing-provider branch. Regenerates the Telephony and Telephony.Abstractions public-API baselines. Updates the plan, changelog, and dialpad docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mination Bounds the channel-binding create-lock acquisition and makes the stranded-caller termination fence survive shell reloads so a late or reload-abandoned ARI hang up can never terminate a call a new shell generation has routed. - AsteriskChannelTenantBindingStore.CreateAsync now uses a bounded SemaphoreSlim.WaitAsync(timeout) (AsteriskCoordinationOptions .ChannelBindingCreateLockTimeout, default 10s) and throws AsteriskChannelBindingCreateTimeoutException on acquisition timeout instead of masquerading as a lost create race. Read-only lookups thread CancellationToken. - AsteriskPendingCallerTerminationRegistry holds BOTH the termination claim set and the pending-retry set as process-wide static, tenant-partitioned state (mirroring the static striped create locks), so the fence outlives a shell reload until termination completes and no claim leaks. - AsteriskPendingCallerTerminationReconciler re-claims each pending channel before hanging up: claim-lock timeout keeps it pending, a refused claim (binding recovered) resolves it out without a hang up, a granted claim hangs up outside all locks then releases + resolves. - AsteriskInboundCallOfferBridge enqueues the channel before inline termination so a reload mid-hang-up leaves it owned by the reconciler. Independently reviewed (gpt-5.6) across nine rounds; final APPROVE. Build 0 warnings; 715 Telephony tests pass (1 skipped browser E2E). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ration (OC-026) Enabled queues, skills, business-hours calendars, and entry points are small, slowly-changing collections read on the hot inbound-routing path, yet each read issued a fresh Session.Query. Introduce IContactCenterConfigurationCache, a per-tenant shell-scoped-state cache (ConcurrentDictionary snapshot guarded by an ISignal change token), and delegate the four managers' ListEnabledAsync to it. A generic ContactCenterConfigurationCacheInvalidationHandler<T> registered alongside the existing catalog handlers signals the change token on Created/Updated/Deleted via ShellScope.AddDeferredTask, so invalidation fires after the writing transaction commits. The token is captured before each load so a write landing mid-load forces the next read to reload rather than serve a stale snapshot. Uses shell-scoped state + ISignal instead of IMemoryCache to comply with the Contact Center architecture guard, and is honored across process instances sharing the tenant's signal backplane. Also fixes a latent xUnit1051 in the OC-050 AsteriskChannelTenantBindingIsolationTests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
escapeHtml was copy-pasted into four scripts, formatDuration/pad duplicated, and the ordered call-state name list — which must match the C# CallState enum position for position — was hard-coded in both soft phones, so the copies could drift from each other and from the enum unnoticed. Add a shared telephony-client resource in the Telephony base module exposing escapeHtml, formatDuration, normalizeCallState, and callStateNames on window.telephonyClient. telephony-soft-phone and contact-center-realtime now depend on it, so all four consumers reach one definition of each helper. A build-time guard test (CallStateNamesJsSyncTests) asserts the shared script's CALL_STATE_NAMES equals CallState in ordinal order and that the enum is a contiguous zero-based sequence, so JS/C# drift fails the build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t views The queues, skills, inbound entry points, dialer profiles, queue groups, agent state reason codes, and business-hours calendars index views were seven ~57-line files that differed only by title, create-label, list id, and empty-state message, with the action bar, search field, item-count row, list markup, no-results alert, and pager copied verbatim into each. That shared markup now lives once in Views/Shared/_CatalogList.cshtml (resolved for every controller in the module via OrchardCore's SharedViewLocationExpanderProvider), backed by new CatalogListViewModel/CatalogListEntry view models. Each index view now only builds a CatalogListViewModel and renders the partial, dropping from ~57 to ~22 lines. Per-type T["..."] literals stay in the individual views so localization extraction still sees each screen's strings, while the shared strings collapse from seven copies to one. The asp-for="Options.Search" binding is preserved via the Options property on the view model. Module and test project build with 0 warnings; all 1497 ContactCenter tests pass; docs site builds clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The module's Startup.cs declared 36 StartupBase types in 1,298 lines — the one place in the module set breaking the repository's one-public-type-per-file rule and making a single feature's registrations hard to locate. Each type now lives in its own file named for the class (VoiceStartup.cs, RoutingStartup.cs, ComplianceStartup.cs, AnalyticsStartup.cs, the *AdminStartup/*DeploymentStartup/*RecipesStartup files, etc.), carrying only the using directives it needs. Startup.cs keeps just the base Startup feature class. No type, namespace, attribute, XML doc, or service registration changed, so the split is behaviour-preserving. Two source-parsing architecture tests that assumed a single Startup.cs were updated to scan the whole module: ContactCenterFeatureDependencyArchitectureTests now uses ParseStartupClassesInDirectory at its twelve Contact Center call sites, and ContactCenterRetentionCoverageTests concatenates every *Startup.cs before asserting each retention policy is registered. Module and test project build 0 warnings; all 1497 ContactCenter tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Split the 793-line ContactCenterConstants.cs in the ContactCenter abstractions into seven domain-scoped partial-class files. The single file held fifteen nested constant groups (aggregate types, health checks, feature ids, component names, recording metadata and its governance/erasure reason codes, call-control metadata, event names, and settings), so unrelated concerns shared one screen. ContactCenterConstants is now a public static partial class whose members are grouped into ContactCenterConstants.cs, .Features.cs, .HealthChecks.cs, .Recording.cs, .CallControl.cs, .Events.cs, and .Settings.cs. Because partial classes merge to identical metadata, the public API is byte-for-byte unchanged: all fifteen groups and 120 constants are preserved, PublicApiApprovalTests pass with no baseline change, and no consumer required edits. Purely a navigability improvement with zero behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address six small correctness and consistency nits: - ContactCenter Manifest.cs: replace the two remaining string-literal feature dependencies with the OmnichannelConstants.Features.Managements and SignalRConstants.Feature.Area constants. - Move the outlier Telephony admin menu from Navigation/ to Services/ to match the repository-wide convention. - Remove three illustrative provider names from provider-agnostic XML docs on TelephonyConstants.AuthenticationSchemes and IProviderIdentityResolver. - TelephonyOAuthController: flow HttpContext.RequestAborted into the authentication-service calls it awaits. - AsteriskContactCenterVoiceMediaProvider: dispose the HttpRequestMessage built for each REST call via a using scope. - AsteriskContactCenterVoiceMediaSession: make DisposeAsync idempotent, stop disposing the two SemaphoreSlim locks (they hold no unmanaged handle, matching the sibling Asterisk primitives) to remove the teardown race, and release the feature work lease exactly once while preserving the retry-on-cleanup-failure contract and guaranteeing release on terminal disposal. Generalise the manifest-token architecture test to read every ContactCenterConstants*.cs partial file so it stays correct after the OC-047 partial-class split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Record the disposition for OC-009. The four telephony/contact-center modules set default shape placement in their display drivers rather than in a placement.json, which is correct: Orchard Core resolves placement globally by shape type, so a site or theme can already override or hide any of these shapes with no code, and the driver .Location() calls are sensible, fully overridable defaults. Neither form of the proposed change adds value: a module placement.json duplicating the 52 driver defaults would violate DRY with no behavioural benefit, and migrating the defaults into placement.json (deleting the .Location() calls) would be single-source but adds regression risk across all 52 placements for no functionality. No specific beneficial override was identified, so no change is made. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document the single-active-process constraint for Asterisk real-time voice at the point of use. Asterisk delivers each Stasis event to exactly one ARI application consumer, and the module arbitrates ownership of each (base URL, application name) pair through a process-wide (node-local) registry that cannot see a cross-node claim. Real-time voice is therefore supported only on a single active application process, and running exactly one active node is an operator responsibility. Add a "Single active process per ARI application" section to the Asterisk provider docs that is precise about enforcement: the only production-certified topology is single-node-distributed and the multi-node profile is not certified, but the Contact Center topology health check only validates a declared profile's infrastructure prerequisites (declared-profile-required-in-production, database provider, Redis distributed-lock/SignalR backplane) and does NOT count running nodes -- so two hosts can each declare the single-node profile and both report healthy. Operators must guarantee single-process operation through deployment. Add a matching <remarks> block to AsteriskAriApplicationOwnershipRegistry and a changelog bullet. No code behavior changed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
OC-015 flagged optional cross-feature services injected as IEnumerable<T> and reduced with FirstOrDefault(). Re-evaluated the premise: this is the sanctioned Orchard Core idiom for consuming a service that lives in a feature which may be disabled -- when the feature is off the tenant container holds no registration, so a direct ctor param would throw at activation. The pattern is applied uniformly across ~19 sites and degrades safely (already covered by an existing Compliance/Dialer test). Both recommended alternatives regress: splitting central orchestrators along a feature seam scatters one cohesive workflow across two classes for no correctness benefit; null-object defaults make an optional dependency look mandatory while silently no-opping, require ~10 no-op implementations kept in lockstep with their interfaces, and change nothing about correctness. Documented the Won't-Fix rationale in the readiness plan. No code changed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The provider webhook rate and concurrency limiters use in-process System.Threading.RateLimiting, so limits are enforced per application node. Document these semantics rather than build a distributed quota that only a future certified multi-node topology would need. Add a "Rate and concurrency limits are per node" subsection to the Provider webhook ingress section of production-support.md: on the only production- certified topology (a single application node) per-node equals the effective global limit and no distributed quota is required to make it fleet-wide; edge protection (WAF/reverse proxy/API gateway) remains the primary DoS control on every topology, single-node included; and any multi-node fleet-wide ceiling must be enforced at the edge since the in-process limiter cannot coordinate one. Add matching <remarks> blocks to ProviderWebhookIngressLimiter and ProviderWebhookIngressOptions. A Redis-backed global quota is recorded as a tracked option for a future certified multi-node topology and deliberately deferred. No behavior changed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add two provider-aligned, one-step feature-enablement recipes that stand up the certified GA-Core Contact Center feature set without manual toggling: - contact-center-asterisk-ga-core.recipe.json (ga-core-asterisk profile) - contact-center-dialpad-ga-core.recipe.json (ga-core-dialpad profile) Each recipe enables exactly the 11 features of its support-matrix tenant profile. This intentionally deviates from the plan's illustrative inbound/outbound split: the authoritative support ledger (.github/contact-center/support-matrix.v1.json) certifies only the two provider bundles, and its prohibitedCombinations forbid unlisted feature combinations, so an inbound/outbound-only recipe would guide operators into an uncertified (prohibited) state. Recipes are feature-only (no seed content) because queues, skills, dialer profiles, and entry points reference environment-specific provider endpoints and campaigns. Harvested via OrchardCore.Module.Targets Recipes/** and surfaced under Configuration -> Recipes. Add ContactCenterSetupRecipeTests binding each recipe's feature set to the ledger via set-equality and asserting every supported provider profile has a recipe. Update configuration-deployment docs, changelog, and the readiness plan (OC-010 -> Completed). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace 54 hand-written report service registrations in AnalyticsStartup (37 enterprise, 12 workforce, 5 individual — many over 400 characters) with a data-driven, extensible catalog. Reports framework: - Add IReportProvider to Reports.Abstractions: one registration contributes a family of reports via GetReports(). - ReportManager now merges reports from both individual IReport services and IReportProvider instances, still enforcing globally unique names. The original single-argument constructor is retained as a delegating overload, so this is not a source/binary break. Contact Center: - Move the 37 enterprise and 12 workforce report definitions into a configurable ContactCenterReportCatalogOptions, populated through the options pipeline by ConfigureContactCenterReportCatalog (keeping the S["..."] literals for localization extraction and deferring localization to request culture). - Project the catalog through one scoped ContactCenterReportProvider that constructs a fresh report instance per enumeration, preserving the per-request-scope concurrency guarantee. - AnalyticsStartup now registers the 5 individual reports, the configure options, and the provider; the 400-character registration lines and the imperative helpers are gone, and any feature can extend the catalog through options without editing service registration. Tests: add ReportManager provider-aggregation and collision cases; update the report concurrency test to assert IReport and IReportProvider descriptors are scoped. Document IReportProvider in the Reports framework docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.