Make portal support copy concise and literal

This commit is contained in:
rcourtman
2026-03-29 17:11:21 +01:00
parent 401808ee66
commit 508deff209
7 changed files with 205 additions and 91 deletions
@@ -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
@@ -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
@@ -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
+1 -1
View File
@@ -1,5 +1,5 @@
{
"source_hash": "efabad85565864017562509ae60da3b064b5d2dd20585cd8490be6d5f93452b7",
"source_hash": "8b83790627c0212a6ef4ba62079f11e11236c4f806f391908614e1b17eb7fcba",
"build_inputs": [
"package.json",
"tsconfig.json",
File diff suppressed because one or more lines are too long
@@ -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');
@@ -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
? (
'<div class="portal-support-route-card">' +
'<div class="account-panel-kicker">Hosted workspace or access</div>' +
'<h3>' + (hostedViewOnly ? 'Hosted review or owner/admin path failed' : 'Workspace or access path failed') + '</h3>' +
'<p>' + (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.') + '</p>' +
'<div class="account-panel-kicker">Workspaces or Access</div>' +
'<h3>' + hostedSupportRouteTitle(hostedViewOnly) + '</h3>' +
'<p>' + hostedSupportRouteDescription(hostedViewOnly) + '</p>' +
'<div class="portal-support-points">' +
'<div class="portal-support-point"><strong>' + (hostedViewOnly ? 'Review the same task' : 'Start from the same task') + '</strong><span>' + (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.') + '</span></div>' +
'<div class="portal-support-point"><strong>' + (hostedViewOnly ? 'Name the blocked owner/admin action' : 'Keep the hosted context intact') + '</strong><span>' + (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.') + '</span></div>' +
'<div class="portal-support-point"><strong>Task</strong><span>' + hostedSupportRouteTaskCopy(hostedViewOnly) + '</span></div>' +
'<div class="portal-support-point"><strong>' + hostedSupportRouteContextLabel(hostedViewOnly) + '</strong><span>' + hostedSupportRouteContextCopy(hostedViewOnly) + '</span></div>' +
'</div>' +
'<div class="portal-support-actions">' +
'<button type="button" class="btn-secondary btn-compact" data-shell-action="activate-section" data-shell-section="workspaces">Open Workspaces</button>' +
@@ -1271,31 +1352,11 @@ function renderSupportSection(context: ShellViewContext): string {
'</div>' +
'<div class="portal-support-route-card">' +
'<div class="account-panel-kicker">' + (showSelfHostedCommercial ? 'Billing' : 'Hosted billing') + '</div>' +
'<h3>' + (hostedViewOnly
? (showSelfHostedCommercial ? 'Billing or owner/admin path failed' : 'Hosted billing or owner/admin path failed')
: (showSelfHostedCommercial ? 'Billing path failed' : 'Hosted billing path failed')) + '</h3>' +
'<p>' + (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.')) + '</p>' +
'<h3>' + billingSupportRouteTitle(hostedViewOnly, showSelfHostedCommercial) + '</h3>' +
'<p>' + billingSupportRouteDescription(hostedViewOnly, showSelfHostedCommercial) + '</p>' +
'<div class="portal-support-points">' +
'<div class="portal-support-point"><strong>Name the billing job</strong><span>' + (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.')) + '</span></div>' +
'<div class="portal-support-point"><strong>Keep the request intact</strong><span>' + (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.')) + '</span></div>' +
'<div class="portal-support-point"><strong>Billing job</strong><span>' + billingSupportRouteJobCopy(hostedViewOnly, showSelfHostedCommercial) + '</span></div>' +
'<div class="portal-support-point"><strong>Account or email</strong><span>' + billingSupportRouteAccountCopy(hostedViewOnly, showSelfHostedCommercial) + '</span></div>' +
'</div>' +
'<div class="portal-support-actions">' +
'<button type="button" class="btn-secondary btn-compact" data-shell-action="activate-section" data-shell-section="billing">Open billing</button>' +
@@ -1306,11 +1367,11 @@ function renderSupportSection(context: ShellViewContext): string {
: (
'<div class="portal-support-route-card">' +
'<div class="account-panel-kicker">Self-hosted billing</div>' +
'<h3>Self-hosted billing path failed</h3>' +
'<p>Use this route only after a self-hosted billing, license, refund, or privacy job has failed to complete cleanly.</p>' +
'<h3>Self-hosted billing failed</h3>' +
'<p>Escalate only if a self-hosted billing task still fails.</p>' +
'<div class="portal-support-points">' +
'<div class="portal-support-point"><strong>Name the billing job</strong><span>Say whether the failed path was billing, licenses, refunds, or privacy.</span></div>' +
'<div class="portal-support-point"><strong>Keep the purchase context intact</strong><span>Bring the same commercial email and the failed action instead of reopening the story.</span></div>' +
'<div class="portal-support-point"><strong>Billing job</strong><span>Billing, licenses, refunds, or privacy.</span></div>' +
'<div class="portal-support-point"><strong>Account or email</strong><span>Include the commercial billing email and the failed step.</span></div>' +
'</div>' +
'<div class="portal-support-actions">' +
'<button type="button" class="btn-secondary btn-compact" data-shell-action="activate-section" data-shell-section="billing">Open billing</button>' +
@@ -1320,25 +1381,13 @@ function renderSupportSection(context: ShellViewContext): string {
);
var runbookSteps = hasHostedAccounts
? (
'<div class="portal-support-runbook-step"><strong>1. Failed path</strong><span>' + (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.')) + '</span></div>' +
'<div class="portal-support-runbook-step"><strong>2. Account or email</strong><span>' + (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.')) + '</span></div>' +
'<div class="portal-support-runbook-step"><strong>1. Failed path</strong><span>' + supportRunbookPathCopy(hasHostedAccounts, hostedViewOnly, showSelfHostedCommercial) + '</span></div>' +
'<div class="portal-support-runbook-step"><strong>2. Account or email</strong><span>' + supportRunbookAccountCopy(hasHostedAccounts, showSelfHostedCommercial) + '</span></div>' +
'<div class="portal-support-runbook-step"><strong>3. Failed action</strong><span>Name the exact button, form, or billing step that failed and what happened next.</span></div>'
)
: (
'<div class="portal-support-runbook-step"><strong>1. Billing job</strong><span>Say whether the blocked path was billing, licenses, refunds, or privacy.</span></div>' +
'<div class="portal-support-runbook-step"><strong>2. Purchase email</strong><span>Include the commercial billing email used for the self-hosted purchase.</span></div>' +
'<div class="portal-support-runbook-step"><strong>1. Billing job</strong><span>' + supportRunbookPathCopy(hasHostedAccounts, hostedViewOnly, showSelfHostedCommercial) + '</span></div>' +
'<div class="portal-support-runbook-step"><strong>2. Account or email</strong><span>' + supportRunbookAccountCopy(hasHostedAccounts, showSelfHostedCommercial) + '</span></div>' +
'<div class="portal-support-runbook-step"><strong>3. Failed action</strong><span>Name the exact button, form, or billing step that failed and what happened next.</span></div>'
);
return (