From 508deff209a83aefd11df0c9286c27db18229a36 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 29 Mar 2026 17:11:21 +0100 Subject: [PATCH] Make portal support copy concise and literal --- .../v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md | 3 + .../v6/internal/subsystems/api-contracts.md | 4 +- .../v6/internal/subsystems/cloud-paid.md | 5 +- .../cloudcp/portal/dist/build_manifest.json | 2 +- internal/cloudcp/portal/dist/portal_app.js | 63 ++++++- .../portal/frontend/src/shell_view.test.ts | 42 ++--- .../cloudcp/portal/frontend/src/shell_view.ts | 177 +++++++++++------- 7 files changed, 205 insertions(+), 91 deletions(-) diff --git a/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md b/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md index c67462ccf..b7b14746d 100644 --- a/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md +++ b/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md @@ -269,6 +269,9 @@ Core rules: the exact state or action (for example `Manage access`, `Hosted billing attached`, `Email support`, or `Failed to load roster`) instead of shorthand such as `Manage`, `Hosted`, `Email`, or generic alert labels. + Support copy follows the same rule: escalation surfaces must name the + exact task path, account/email, and failed step in short literal wording, + not long procedural prose. ## Screen Model diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 593d918c0..da5ed8a7b 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -378,7 +378,9 @@ rendered. The same typed contract applies to shell badges, section labels, context chips, route labels, and error headings: they must render the exact action or state (`Manage access`, `Hosted billing attached`, `Email support`, `Failed to load roster`) instead of shorthand such as `Manage`, `Hosted`, or -generic alert labels. +generic alert labels. Support copy is part of the same typed contract: +escalation surfaces must render short literal path/account/action wording +instead of longer procedural prose. That same typed overview contract must also preserve a sharp, high-density enterprise visual aesthetic (e.g. Cloudflare/GCP density standards) across all portal scenarios, removing gradients and heavy box-shadows to ensure a calm, rigorous visual language with standard 256px sidebars, Inter-grade typography, clean text-transform rules, and cleanly unboxed typography without excessive pills or stacked metrics. plus a package-local `tsc --noEmit` gate, so future account-shell work should extend the typed source boundary instead of reviving opaque global runtime diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 066765d3e..bed0be695 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -945,7 +945,10 @@ the runtime already knows the concrete state, action, or failure being shown. That includes shell badges, section labels, context chips, route labels, and error headings: they must use the exact action or state (`Manage access`, `Hosted billing attached`, `Email support`, `Failed to load roster`) instead -of shorthand such as `Manage`, `Hosted`, or generic alert labels. +of shorthand such as `Manage`, `Hosted`, or generic alert labels. Support +copy follows the same contract: escalation guidance must stay short and typed +to the exact task path, account/email, and failed step instead of drifting +into longer procedural prose. That same canonical shell/runtime boundary now also owns the bootstrap truth for when self-hosted commercial history is relevant. Hosted-only accounts must not render self-hosted license, refund, privacy, or support-escalation copy diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json index 9107f04ad..ee11d29c8 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "efabad85565864017562509ae60da3b064b5d2dd20585cd8490be6d5f93452b7", + "source_hash": "8b83790627c0212a6ef4ba62079f11e11236c4f806f391908614e1b17eb7fcba", "build_inputs": [ "package.json", "tsconfig.json", diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js index 55da4e11a..5c1afd0a2 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -2032,6 +2032,63 @@ } return ["Escalation only", "Open Billing first", supportEmail ? "Email support" : "Support route"]; } + function supportLeadCopy(hasHostedAccounts2, hostedViewOnly, showSelfHostedCommercial) { + if (!hasHostedAccounts2) return "Use Support only after Billing fails."; + if (hostedViewOnly) { + return showSelfHostedCommercial ? "Use Support only after Workspaces review, Access review, owner/admin handoff, or Billing fails." : "Use Support only after Workspaces review, Access review, owner/admin handoff, or hosted Billing fails."; + } + return showSelfHostedCommercial ? "Use Support only after Workspaces, Access, or Billing fails." : "Use Support only after Workspaces, Access, or hosted Billing fails."; + } + function hostedSupportRouteTitle(hostedViewOnly) { + return hostedViewOnly ? "Review or owner/admin handoff failed" : "Workspace or access failed"; + } + function hostedSupportRouteDescription(hostedViewOnly) { + return hostedViewOnly ? "Recheck the same workspace or roster first. Escalate only if the required owner or admin change still cannot complete." : "Retry the same workspace or access task first. Escalate only if the same task still fails."; + } + function hostedSupportRouteTaskCopy(hostedViewOnly) { + return hostedViewOnly ? "Use Workspaces for workspace state or Access for roster state before you escalate." : "Use Workspaces for lifecycle work or Access for roster work before you escalate."; + } + function hostedSupportRouteContextLabel(hostedViewOnly) { + return hostedViewOnly ? "Owner/admin handoff" : "Failed step"; + } + function hostedSupportRouteContextCopy(hostedViewOnly) { + return hostedViewOnly ? "Include the account, workspace, and the change that still needs owner or admin action." : "Include the account, workspace, and the failed step."; + } + function billingSupportRouteTitle(hostedViewOnly, showSelfHostedCommercial) { + if (hostedViewOnly) { + return showSelfHostedCommercial ? "Billing or owner/admin handoff failed" : "Hosted billing or owner/admin handoff failed"; + } + return showSelfHostedCommercial ? "Billing failed" : "Hosted billing failed"; + } + function billingSupportRouteDescription(hostedViewOnly, showSelfHostedCommercial) { + if (hostedViewOnly) { + return showSelfHostedCommercial ? "Escalate only if hosted billing still needs owner/admin action or a self-hosted billing task still fails." : "Escalate only if hosted billing still needs owner/admin action or still fails."; + } + return showSelfHostedCommercial ? "Escalate only if hosted billing or a self-hosted billing task still fails." : "Escalate only if hosted billing still fails."; + } + function billingSupportRouteJobCopy(hostedViewOnly, showSelfHostedCommercial) { + if (hostedViewOnly) { + return showSelfHostedCommercial ? "Hosted billing, licenses, refunds, or privacy. Say if hosted billing still needed owner/admin action." : "Hosted billing. Say if it still needed owner/admin action."; + } + return showSelfHostedCommercial ? "Hosted billing, licenses, refunds, or privacy." : "Hosted billing."; + } + function billingSupportRouteAccountCopy(hostedViewOnly, showSelfHostedCommercial) { + if (showSelfHostedCommercial) { + return "Include the hosted account for hosted billing or the billing email for self-hosted work."; + } + return hostedViewOnly ? "Include the hosted account that still needed owner/admin billing action." : "Include the hosted account for the failed billing step."; + } + function supportRunbookPathCopy(hasHostedAccounts2, hostedViewOnly, showSelfHostedCommercial) { + if (!hasHostedAccounts2) return "Billing, licenses, refunds, or privacy."; + if (hostedViewOnly) { + return showSelfHostedCommercial ? "Workspaces review, Access review, owner/admin handoff, hosted billing, licenses, refunds, or privacy." : "Workspaces review, Access review, owner/admin handoff, or hosted billing."; + } + return showSelfHostedCommercial ? "Workspaces, Access, hosted billing, licenses, refunds, or privacy." : "Workspaces, Access, or hosted billing."; + } + function supportRunbookAccountCopy(hasHostedAccounts2, showSelfHostedCommercial) { + if (!hasHostedAccounts2) return "Commercial billing email used for the self-hosted purchase."; + return showSelfHostedCommercial ? "Hosted account and workspace when relevant, or billing email for self-hosted work." : "Hosted account and workspace, or hosted billing account."; + } function hasHostedAccounts(accounts) { return accounts.length > 0; } @@ -2447,10 +2504,10 @@ } } var hostedViewOnly = hasHostedAccounts2 && !canManageHostedTasks; - var supportLead = hasHostedAccounts2 ? hostedViewOnly ? showSelfHostedCommercial ? "Use support only when the same Workspaces review, Access review, owner/admin, or Billing path has already stopped you." : "Use support only when the same Workspaces review, Access review, owner/admin, or hosted Billing path has already stopped you." : showSelfHostedCommercial ? "Use support only when the Workspaces, Access, or Billing path has already stopped you." : "Use support only when the Workspaces, Access, or hosted Billing path has already stopped you." : "Use support only when the Billing path has already stopped you."; + var supportLead = supportLeadCopy(hasHostedAccounts2, hostedViewOnly, showSelfHostedCommercial); var supportChips = supportSectionChipLabels(hasHostedAccounts2, hostedViewOnly, supportEmail); - var routeCards = hasHostedAccounts2 ? '

' + (hostedViewOnly ? "Hosted review or owner/admin path failed" : "Workspace or access path failed") + "

" + (hostedViewOnly ? "Go back to the hosted task first. Review the same workspace or roster here, then have an owner or admin run the blocked change before you escalate." : "Go back to the hosted task first. Escalate only when the same workspace or access path still cannot finish the job.") + '

' + (hostedViewOnly ? "Review the same task" : "Start from the same task") + "" + (hostedViewOnly ? "Use Workspaces to confirm workspace state and Access to confirm the current roster before you escalate." : "Use Workspaces for lifecycle issues and Access for roster issues before you escalate.") + '
' + (hostedViewOnly ? "Name the blocked owner/admin action" : "Keep the hosted context intact") + "" + (hostedViewOnly ? "Include the account, workspace, and the lifecycle or access change that still needs an owner or admin." : "Include the account, workspace, and failed action so support inherits the same request.") + '
' + escapeHTML(supportEmail) + '

" + (hostedViewOnly ? showSelfHostedCommercial ? "Billing or owner/admin path failed" : "Hosted billing or owner/admin path failed" : showSelfHostedCommercial ? "Billing path failed" : "Hosted billing path failed") + "

" + (hostedViewOnly ? showSelfHostedCommercial ? "Use this route only after the relevant billing job has failed, or the affected hosted account still needs an owner or admin to finish hosted billing." : "Use this route only after the affected hosted account still needs an owner or admin to finish hosted billing and that path still cannot complete cleanly." : showSelfHostedCommercial ? "Use this route only after hosted billing or one self-hosted billing job has failed to complete cleanly." : "Use this route only after hosted billing has failed to complete cleanly.") + '

Name the billing job' + (hostedViewOnly ? showSelfHostedCommercial ? "Say whether the failed path was hosted billing, licenses, refunds, or privacy, and whether hosted billing still needed an owner or admin." : "Say whether the failed path was hosted billing and whether the account still needed an owner or admin to open it." : showSelfHostedCommercial ? "Say whether the failed path was hosted billing, licenses, refunds, or privacy." : "Say whether the failed path was hosted billing.") + '
Keep the request intact' + (hostedViewOnly ? showSelfHostedCommercial ? "Bring the same account or billing email and the failed owner/admin or billing step instead of reopening the story." : "Bring the same hosted account and the failed billing or owner/admin step instead of reopening the story." : showSelfHostedCommercial ? "Bring the same account or billing email and the failed action instead of reopening the story." : "Bring the same hosted account and the failed billing action instead of reopening the story.") + '
" : '

Self-hosted billing path failed

Use this route only after a self-hosted billing, license, refund, or privacy job has failed to complete cleanly.

Name the billing jobSay whether the failed path was billing, licenses, refunds, or privacy.
Keep the purchase context intactBring the same commercial email and the failed action instead of reopening the story.
"; - var runbookSteps = hasHostedAccounts2 ? '
1. Failed path' + (hostedViewOnly ? showSelfHostedCommercial ? "Say whether the blocked path was Workspaces review, Access review, owner/admin hosted change, hosted billing, licenses, refunds, or privacy." : "Say whether the blocked path was Workspaces review, Access review, owner/admin hosted change, or hosted billing." : showSelfHostedCommercial ? "Say whether the blocked path was Workspaces, Access, hosted billing, licenses, refunds, or privacy." : "Say whether the blocked path was Workspaces, Access, or hosted billing.") + '
2. Account or email' + (hostedViewOnly ? showSelfHostedCommercial ? "Include the hosted account and workspace for the blocked review or owner/admin path, or the commercial billing email for self-hosted work." : "Include the hosted account and workspace or hosted billing account that still needed owner/admin action." : showSelfHostedCommercial ? "Include the hosted account and workspace when relevant, or the commercial billing email for self-hosted work." : "Include the hosted account and workspace or hosted billing account that the failed path belongs to.") + '
3. Failed actionName the exact button, form, or billing step that failed and what happened next.
' : '
1. Billing jobSay whether the blocked path was billing, licenses, refunds, or privacy.
2. Purchase emailInclude the commercial billing email used for the self-hosted purchase.
3. Failed actionName the exact button, form, or billing step that failed and what happened next.
'; + var routeCards = hasHostedAccounts2 ? '

' + hostedSupportRouteTitle(hostedViewOnly) + "

" + hostedSupportRouteDescription(hostedViewOnly) + '

Task' + hostedSupportRouteTaskCopy(hostedViewOnly) + '
' + hostedSupportRouteContextLabel(hostedViewOnly) + "" + hostedSupportRouteContextCopy(hostedViewOnly) + '
' + escapeHTML(supportEmail) + '

" + billingSupportRouteTitle(hostedViewOnly, showSelfHostedCommercial) + "

" + billingSupportRouteDescription(hostedViewOnly, showSelfHostedCommercial) + '

Billing job' + billingSupportRouteJobCopy(hostedViewOnly, showSelfHostedCommercial) + '
Account or email' + billingSupportRouteAccountCopy(hostedViewOnly, showSelfHostedCommercial) + '
" : '

Self-hosted billing failed

Escalate only if a self-hosted billing task still fails.

Billing jobBilling, licenses, refunds, or privacy.
Account or emailInclude the commercial billing email and the failed step.
"; + var runbookSteps = hasHostedAccounts2 ? '
1. Failed path' + supportRunbookPathCopy(hasHostedAccounts2, hostedViewOnly, showSelfHostedCommercial) + '
2. Account or email' + supportRunbookAccountCopy(hasHostedAccounts2, showSelfHostedCommercial) + '
3. Failed actionName the exact button, form, or billing step that failed and what happened next.
' : '
1. Billing job' + supportRunbookPathCopy(hasHostedAccounts2, hostedViewOnly, showSelfHostedCommercial) + '
2. Account or email' + supportRunbookAccountCopy(hasHostedAccounts2, showSelfHostedCommercial) + '
3. Failed actionName the exact button, form, or billing step that failed and what happened next.
'; return '

Escalation only

' + escapeHTML(supportLead) + "

" + renderSectionContextChips(supportChips) + '
' + routeCards + '

Send these details

Send the failed path, account or email, and failed action.

' + runbookSteps + "
"; } function renderHeaderHTML(context) { diff --git a/internal/cloudcp/portal/frontend/src/shell_view.test.ts b/internal/cloudcp/portal/frontend/src/shell_view.test.ts index 0575d315d..1a006e347 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts @@ -213,11 +213,11 @@ describe('shell view', function() { expect(html).toContain('Escalation only'); expect(html).toContain('Open Workspaces, Access, or Billing first'); expect(html).toContain('Email support'); - expect(html).toContain('Hosted workspace or access'); - expect(html).toContain('Workspace or access path failed'); + expect(html).toContain('Workspaces or Access'); + expect(html).toContain('Workspace or access failed'); expect(html).toContain('Open Workspaces'); expect(html).toContain('Open Access'); - expect(html).toContain('Hosted billing path failed'); + expect(html).toContain('Hosted billing failed'); expect(html).toContain('Send these details'); expect(html).toContain('What to send'); expect(html).toContain('Open billing'); @@ -274,7 +274,7 @@ describe('shell view', function() { expect(html).toContain('id="billing-detail-shell" hidden'); expect(html).toContain('data-account-billing-action="clear-billing-panel"'); expect(html).toContain('id="data-billing-panel"'); - expect(html).toContain('Billing path failed'); + expect(html).toContain('Billing failed'); expect(html).toContain('licenses, refunds, or privacy'); expect(html).toContain('Escalate with the same hosted billing action or self-hosted path and the exact failed step.'); }); @@ -403,24 +403,24 @@ describe('shell view', function() { expect(html).toContain('Review the hosted roster here. An owner or admin must make changes.'); expect(html).toContain('Hosted billing is attached here, but an owner or admin must open it.'); expect(html).toContain('Escalation only after the review, owner/admin, or billing path is exhausted.'); - expect(html).toContain('Use support only when the same Workspaces review, Access review, owner/admin, or hosted Billing path has already stopped you.'); + expect(html).toContain('Use Support only after Workspaces review, Access review, owner/admin handoff, or hosted Billing fails.'); expect(html).toContain('Review Workspaces or Access, or contact owner/admin first'); expect(html).toContain('Email support'); - expect(html).toContain('Hosted workspace or access'); - expect(html).toContain('Hosted review or owner/admin path failed'); - expect(html).toContain('Review the same workspace or roster here, then have an owner or admin run the blocked change before you escalate.'); - expect(html).toContain('Review the same task'); - expect(html).toContain('Use Workspaces to confirm workspace state and Access to confirm the current roster before you escalate.'); - expect(html).toContain('Name the blocked owner/admin action'); - expect(html).toContain('Include the account, workspace, and the lifecycle or access change that still needs an owner or admin.'); + expect(html).toContain('Workspaces or Access'); + expect(html).toContain('Review or owner/admin handoff failed'); + expect(html).toContain('Recheck the same workspace or roster first. Escalate only if the required owner or admin change still cannot complete.'); + expect(html).toContain('Task'); + expect(html).toContain('Use Workspaces for workspace state or Access for roster state before you escalate.'); + expect(html).toContain('Owner/admin handoff'); + expect(html).toContain('Include the account, workspace, and the change that still needs owner or admin action.'); expect(html).toContain('Open Workspaces'); expect(html).toContain('Open Access'); - expect(html).toContain('Hosted billing or owner/admin path failed'); - expect(html).toContain('Use this route only after the affected hosted account still needs an owner or admin to finish hosted billing and that path still cannot complete cleanly.'); - expect(html).toContain('Say whether the failed path was hosted billing and whether the account still needed an owner or admin to open it.'); - expect(html).toContain('Bring the same hosted account and the failed billing or owner/admin step instead of reopening the story.'); - expect(html).toContain('Say whether the blocked path was Workspaces review, Access review, owner/admin hosted change, or hosted billing.'); - expect(html).toContain('Include the hosted account and workspace or hosted billing account that still needed owner/admin action.'); + expect(html).toContain('Hosted billing or owner/admin handoff failed'); + expect(html).toContain('Escalate only if hosted billing still needs owner/admin action or still fails.'); + expect(html).toContain('Hosted billing. Say if it still needed owner/admin action.'); + expect(html).toContain('Include the hosted account that still needed owner/admin billing action.'); + expect(html).toContain('Workspaces review, Access review, owner/admin handoff, or hosted billing.'); + expect(html).toContain('Hosted account and workspace, or hosted billing account.'); expect(html).toContain('data-can-manage="false"'); expect(html).not.toContain('Invite people, change roles, and remove account access.'); expect(html).not.toContain('Open a workspace, review lifecycle state, or create one.'); @@ -647,9 +647,9 @@ describe('shell view', function() { expect(html).toContain('Self-hosted billing available'); expect(html).toContain('Open Billing first'); expect(html).toContain('Email support'); - expect(html).toContain('Use support only when the Billing path has already stopped you.'); - expect(html).toContain('Self-hosted billing path failed'); - expect(html).toContain('Purchase email'); + expect(html).toContain('Use Support only after Billing fails.'); + expect(html).toContain('Self-hosted billing failed'); + expect(html).toContain('Account or email'); expect(html).toContain('No hosted billing attached'); expect(html).not.toContain('Escalate with the same hosted billing action or self-hosted path and the exact failed step.'); expect(html).not.toContain('Workspace or access path failed'); diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts index a9cd6462a..210969b9b 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -131,6 +131,101 @@ function supportSectionChipLabels(hasHostedAccounts: boolean, hostedViewOnly: bo return ['Escalation only', 'Open Billing first', supportEmail ? 'Email support' : 'Support route']; } +function supportLeadCopy(hasHostedAccounts: boolean, hostedViewOnly: boolean, showSelfHostedCommercial: boolean): string { + if (!hasHostedAccounts) return 'Use Support only after Billing fails.'; + if (hostedViewOnly) { + return showSelfHostedCommercial + ? 'Use Support only after Workspaces review, Access review, owner/admin handoff, or Billing fails.' + : 'Use Support only after Workspaces review, Access review, owner/admin handoff, or hosted Billing fails.'; + } + return showSelfHostedCommercial + ? 'Use Support only after Workspaces, Access, or Billing fails.' + : 'Use Support only after Workspaces, Access, or hosted Billing fails.'; +} + +function hostedSupportRouteTitle(hostedViewOnly: boolean): string { + return hostedViewOnly ? 'Review or owner/admin handoff failed' : 'Workspace or access failed'; +} + +function hostedSupportRouteDescription(hostedViewOnly: boolean): string { + return hostedViewOnly + ? 'Recheck the same workspace or roster first. Escalate only if the required owner or admin change still cannot complete.' + : 'Retry the same workspace or access task first. Escalate only if the same task still fails.'; +} + +function hostedSupportRouteTaskCopy(hostedViewOnly: boolean): string { + return hostedViewOnly + ? 'Use Workspaces for workspace state or Access for roster state before you escalate.' + : 'Use Workspaces for lifecycle work or Access for roster work before you escalate.'; +} + +function hostedSupportRouteContextLabel(hostedViewOnly: boolean): string { + return hostedViewOnly ? 'Owner/admin handoff' : 'Failed step'; +} + +function hostedSupportRouteContextCopy(hostedViewOnly: boolean): string { + return hostedViewOnly + ? 'Include the account, workspace, and the change that still needs owner or admin action.' + : 'Include the account, workspace, and the failed step.'; +} + +function billingSupportRouteTitle(hostedViewOnly: boolean, showSelfHostedCommercial: boolean): string { + if (hostedViewOnly) { + return showSelfHostedCommercial ? 'Billing or owner/admin handoff failed' : 'Hosted billing or owner/admin handoff failed'; + } + return showSelfHostedCommercial ? 'Billing failed' : 'Hosted billing failed'; +} + +function billingSupportRouteDescription(hostedViewOnly: boolean, showSelfHostedCommercial: boolean): string { + if (hostedViewOnly) { + return showSelfHostedCommercial + ? 'Escalate only if hosted billing still needs owner/admin action or a self-hosted billing task still fails.' + : 'Escalate only if hosted billing still needs owner/admin action or still fails.'; + } + return showSelfHostedCommercial + ? 'Escalate only if hosted billing or a self-hosted billing task still fails.' + : 'Escalate only if hosted billing still fails.'; +} + +function billingSupportRouteJobCopy(hostedViewOnly: boolean, showSelfHostedCommercial: boolean): string { + if (hostedViewOnly) { + return showSelfHostedCommercial + ? 'Hosted billing, licenses, refunds, or privacy. Say if hosted billing still needed owner/admin action.' + : 'Hosted billing. Say if it still needed owner/admin action.'; + } + return showSelfHostedCommercial + ? 'Hosted billing, licenses, refunds, or privacy.' + : 'Hosted billing.'; +} + +function billingSupportRouteAccountCopy(hostedViewOnly: boolean, showSelfHostedCommercial: boolean): string { + if (showSelfHostedCommercial) { + return 'Include the hosted account for hosted billing or the billing email for self-hosted work.'; + } + return hostedViewOnly + ? 'Include the hosted account that still needed owner/admin billing action.' + : 'Include the hosted account for the failed billing step.'; +} + +function supportRunbookPathCopy(hasHostedAccounts: boolean, hostedViewOnly: boolean, showSelfHostedCommercial: boolean): string { + if (!hasHostedAccounts) return 'Billing, licenses, refunds, or privacy.'; + if (hostedViewOnly) { + return showSelfHostedCommercial + ? 'Workspaces review, Access review, owner/admin handoff, hosted billing, licenses, refunds, or privacy.' + : 'Workspaces review, Access review, owner/admin handoff, or hosted billing.'; + } + return showSelfHostedCommercial + ? 'Workspaces, Access, hosted billing, licenses, refunds, or privacy.' + : 'Workspaces, Access, or hosted billing.'; +} + +function supportRunbookAccountCopy(hasHostedAccounts: boolean, showSelfHostedCommercial: boolean): string { + if (!hasHostedAccounts) return 'Commercial billing email used for the self-hosted purchase.'; + return showSelfHostedCommercial + ? 'Hosted account and workspace when relevant, or billing email for self-hosted work.' + : 'Hosted account and workspace, or hosted billing account.'; +} + function hasHostedAccounts(accounts: PortalAccountSummary[]): boolean { return accounts.length > 0; } @@ -1237,31 +1332,17 @@ function renderSupportSection(context: ShellViewContext): string { } } var hostedViewOnly = hasHostedAccounts && !canManageHostedTasks; - var supportLead = hasHostedAccounts - ? (hostedViewOnly - ? (showSelfHostedCommercial - ? 'Use support only when the same Workspaces review, Access review, owner/admin, or Billing path has already stopped you.' - : 'Use support only when the same Workspaces review, Access review, owner/admin, or hosted Billing path has already stopped you.') - : (showSelfHostedCommercial - ? 'Use support only when the Workspaces, Access, or Billing path has already stopped you.' - : 'Use support only when the Workspaces, Access, or hosted Billing path has already stopped you.')) - : 'Use support only when the Billing path has already stopped you.'; + var supportLead = supportLeadCopy(hasHostedAccounts, hostedViewOnly, showSelfHostedCommercial); var supportChips = supportSectionChipLabels(hasHostedAccounts, hostedViewOnly, supportEmail); var routeCards = hasHostedAccounts ? ( '
' + - '' + - '

' + (hostedViewOnly ? 'Hosted review or owner/admin path failed' : 'Workspace or access path failed') + '

' + - '

' + (hostedViewOnly - ? 'Go back to the hosted task first. Review the same workspace or roster here, then have an owner or admin run the blocked change before you escalate.' - : 'Go back to the hosted task first. Escalate only when the same workspace or access path still cannot finish the job.') + '

' + + '' + + '

' + hostedSupportRouteTitle(hostedViewOnly) + '

' + + '

' + hostedSupportRouteDescription(hostedViewOnly) + '

' + '
' + - '
' + (hostedViewOnly ? 'Review the same task' : 'Start from the same task') + '' + (hostedViewOnly - ? 'Use Workspaces to confirm workspace state and Access to confirm the current roster before you escalate.' - : 'Use Workspaces for lifecycle issues and Access for roster issues before you escalate.') + '
' + - '
' + (hostedViewOnly ? 'Name the blocked owner/admin action' : 'Keep the hosted context intact') + '' + (hostedViewOnly - ? 'Include the account, workspace, and the lifecycle or access change that still needs an owner or admin.' - : 'Include the account, workspace, and failed action so support inherits the same request.') + '
' + + '
Task' + hostedSupportRouteTaskCopy(hostedViewOnly) + '
' + + '
' + hostedSupportRouteContextLabel(hostedViewOnly) + '' + hostedSupportRouteContextCopy(hostedViewOnly) + '
' + '
' + '
' + '' + @@ -1271,31 +1352,11 @@ function renderSupportSection(context: ShellViewContext): string { '
' + '
' + '' + - '

' + (hostedViewOnly - ? (showSelfHostedCommercial ? 'Billing or owner/admin path failed' : 'Hosted billing or owner/admin path failed') - : (showSelfHostedCommercial ? 'Billing path failed' : 'Hosted billing path failed')) + '

' + - '

' + (hostedViewOnly - ? (showSelfHostedCommercial - ? 'Use this route only after the relevant billing job has failed, or the affected hosted account still needs an owner or admin to finish hosted billing.' - : 'Use this route only after the affected hosted account still needs an owner or admin to finish hosted billing and that path still cannot complete cleanly.') - : (showSelfHostedCommercial - ? 'Use this route only after hosted billing or one self-hosted billing job has failed to complete cleanly.' - : 'Use this route only after hosted billing has failed to complete cleanly.')) + '

' + + '

' + billingSupportRouteTitle(hostedViewOnly, showSelfHostedCommercial) + '

' + + '

' + billingSupportRouteDescription(hostedViewOnly, showSelfHostedCommercial) + '

' + '
' + - '
Name the billing job' + (hostedViewOnly - ? (showSelfHostedCommercial - ? 'Say whether the failed path was hosted billing, licenses, refunds, or privacy, and whether hosted billing still needed an owner or admin.' - : 'Say whether the failed path was hosted billing and whether the account still needed an owner or admin to open it.') - : (showSelfHostedCommercial - ? 'Say whether the failed path was hosted billing, licenses, refunds, or privacy.' - : 'Say whether the failed path was hosted billing.')) + '
' + - '
Keep the request intact' + (hostedViewOnly - ? (showSelfHostedCommercial - ? 'Bring the same account or billing email and the failed owner/admin or billing step instead of reopening the story.' - : 'Bring the same hosted account and the failed billing or owner/admin step instead of reopening the story.') - : (showSelfHostedCommercial - ? 'Bring the same account or billing email and the failed action instead of reopening the story.' - : 'Bring the same hosted account and the failed billing action instead of reopening the story.')) + '
' + + '
Billing job' + billingSupportRouteJobCopy(hostedViewOnly, showSelfHostedCommercial) + '
' + + '
Account or email' + billingSupportRouteAccountCopy(hostedViewOnly, showSelfHostedCommercial) + '
' + '
' + '
' + '' + @@ -1306,11 +1367,11 @@ function renderSupportSection(context: ShellViewContext): string { : ( '
' + '' + - '

Self-hosted billing path failed

' + - '

Use this route only after a self-hosted billing, license, refund, or privacy job has failed to complete cleanly.

' + + '

Self-hosted billing failed

' + + '

Escalate only if a self-hosted billing task still fails.

' + '
' + - '
Name the billing jobSay whether the failed path was billing, licenses, refunds, or privacy.
' + - '
Keep the purchase context intactBring the same commercial email and the failed action instead of reopening the story.
' + + '
Billing jobBilling, licenses, refunds, or privacy.
' + + '
Account or emailInclude the commercial billing email and the failed step.
' + '
' + '
' + '' + @@ -1320,25 +1381,13 @@ function renderSupportSection(context: ShellViewContext): string { ); var runbookSteps = hasHostedAccounts ? ( - '
1. Failed path' + (hostedViewOnly - ? (showSelfHostedCommercial - ? 'Say whether the blocked path was Workspaces review, Access review, owner/admin hosted change, hosted billing, licenses, refunds, or privacy.' - : 'Say whether the blocked path was Workspaces review, Access review, owner/admin hosted change, or hosted billing.') - : (showSelfHostedCommercial - ? 'Say whether the blocked path was Workspaces, Access, hosted billing, licenses, refunds, or privacy.' - : 'Say whether the blocked path was Workspaces, Access, or hosted billing.')) + '
' + - '
2. Account or email' + (hostedViewOnly - ? (showSelfHostedCommercial - ? 'Include the hosted account and workspace for the blocked review or owner/admin path, or the commercial billing email for self-hosted work.' - : 'Include the hosted account and workspace or hosted billing account that still needed owner/admin action.') - : (showSelfHostedCommercial - ? 'Include the hosted account and workspace when relevant, or the commercial billing email for self-hosted work.' - : 'Include the hosted account and workspace or hosted billing account that the failed path belongs to.')) + '
' + + '
1. Failed path' + supportRunbookPathCopy(hasHostedAccounts, hostedViewOnly, showSelfHostedCommercial) + '
' + + '
2. Account or email' + supportRunbookAccountCopy(hasHostedAccounts, showSelfHostedCommercial) + '
' + '
3. Failed actionName the exact button, form, or billing step that failed and what happened next.
' ) : ( - '
1. Billing jobSay whether the blocked path was billing, licenses, refunds, or privacy.
' + - '
2. Purchase emailInclude the commercial billing email used for the self-hosted purchase.
' + + '
1. Billing job' + supportRunbookPathCopy(hasHostedAccounts, hostedViewOnly, showSelfHostedCommercial) + '
' + + '
2. Account or email' + supportRunbookAccountCopy(hasHostedAccounts, showSelfHostedCommercial) + '
' + '
3. Failed actionName the exact button, form, or billing step that failed and what happened next.
' ); return (