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 315eeb102..137abf82b 100644
--- a/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md
+++ b/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md
@@ -264,7 +264,11 @@ Core rules:
40. Portal task and status copy must stay literal. Customer-facing copy may
not rely on commentary such as `obvious`, `actual work`, `trustworthy`,
or `settled` when the runtime already knows the concrete state, action,
- or failure being shown.
+ or failure being shown. The same rule applies to shell badges, section
+ labels, and error headings: they must say the exact state or action (for
+ example `Manage access`, `Hosted billing attached`, or `Failed to load
+ roster`) instead of shorthand such as `Manage`, `Hosted`, `Email`, or
+ generic alert labels.
## 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 5d5221b6c..3b172d4a2 100644
--- a/docs/release-control/v6/internal/subsystems/api-contracts.md
+++ b/docs/release-control/v6/internal/subsystems/api-contracts.md
@@ -374,7 +374,10 @@ That same typed portal runtime contract must also keep task and status copy
literal across the account surface: customer-facing wording may not use
commentary such as `obvious`, `actual work`, `trustworthy`, or `settled` when
the runtime already knows the concrete state, action, or failure being
-rendered.
+rendered. The same typed contract applies to shell badges, section labels,
+and error headings: they must render the exact action or state (`Manage
+access`, `Hosted billing attached`, `Failed to load roster`) instead of
+shorthand such as `Manage`, `Hosted`, or generic alert labels.
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 8bf9c96f4..b6fa72893 100644
--- a/docs/release-control/v6/internal/subsystems/cloud-paid.md
+++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md
@@ -942,6 +942,10 @@ That same portal shell/runtime boundary must also keep task and status copy
literal across the account surface: customer-facing wording may not lean on
commentary such as `obvious`, `actual work`, `trustworthy`, or `settled` when
the runtime already knows the concrete state, action, or failure being shown.
+That includes shell badges, section labels, and error headings: they must use
+the exact action or state (`Manage access`, `Hosted billing attached`,
+`Failed to load roster`) instead of shorthand such as `Manage`, `Hosted`, or
+generic alert labels.
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 4c3e79795..59366ed63 100644
--- a/internal/cloudcp/portal/dist/build_manifest.json
+++ b/internal/cloudcp/portal/dist/build_manifest.json
@@ -1,5 +1,5 @@
{
- "source_hash": "21a6dc0fc99f359ec17be255e817006a957679e26097181db88399b969d7aadf",
+ "source_hash": "646baefa83486237ab1a2dabf6fb5fb019d16f45cbe8632aa8e881d986d9bb71",
"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 083ed181e..db04c96b4 100644
--- a/internal/cloudcp/portal/dist/portal_app.js
+++ b/internal/cloudcp/portal/dist/portal_app.js
@@ -240,11 +240,11 @@
return;
}
if (entry.accessQuery.status === "loading") {
- stats.innerHTML = '
Suspended' + escapeHTML(
- suspendedCount > 0 ? suspendedCount === 1 ? "1 workspace is suspended and excluded from routine use until you resume it." : String(suspendedCount) + " workspaces are suspended and excluded from routine use until you resume them." : "0 suspended workspaces."
+ suspendedCount > 0 ? suspendedCount === 1 ? "1 workspace is suspended. Resume it before opening it again." : String(suspendedCount) + " workspaces are suspended. Resume them before opening them again." : "0 suspended workspaces."
) + "
" + escapeHTML(
- !accounts.length ? "Use Billing for self-hosted subscriptions, licenses, refunds, and privacy requests." : totalWorkspaces > 0 ? suspendedCount === totalWorkspaces ? "Every hosted workspace is suspended right now." : "Open Workspaces for the current workspace state before routine use." : canManageHosted ? "The first hosted workspace still needs to be created before routine work can start." : "An owner or admin still needs to create the first hosted workspace before routine work can start."
+ !accounts.length ? "Use Billing for self-hosted subscriptions, licenses, refunds, and privacy requests." : totalWorkspaces > 0 ? suspendedCount === totalWorkspaces ? "Every hosted workspace is suspended right now." : "Open Workspaces to see the current state of each hosted workspace." : canManageHosted ? "No hosted workspace exists yet. Create the first one in Workspaces." : "No hosted workspace exists yet. An owner or admin must create the first one."
) + "
';
return;
}
@@ -384,7 +384,7 @@ export function renderAccessSection(accountID: string, entry: PortalAccountUIEnt
}
if (entry.accessQuery.status === 'error') {
if (rosterPanel) rosterPanel.classList.add('state-only');
- setContainerMessage(roster, 'Roster needs attention', entry.accessQuery.error, true);
+ setContainerMessage(roster, 'Failed to load roster', entry.accessQuery.error, true);
return;
}
if (!entry.accessQuery.data.length) {
diff --git a/internal/cloudcp/portal/frontend/src/shell_view.test.ts b/internal/cloudcp/portal/frontend/src/shell_view.test.ts
index 0a59c65ca..02eb5ac48 100644
--- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts
+++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts
@@ -136,17 +136,17 @@ describe('shell view', function() {
expect(html).toContain('Billing');
expect(html).toContain('Support');
expect(html).toContain('Account tasks');
- expect(html).toContain('Review status, ready workspaces, and the next action.');
+ expect(html).toContain('Review workspace counts, current state, and the current action.');
expect(html).toContain('1 account');
expect(html).toContain('3 workspaces');
expect(html).toContain('1 ready workspace');
expect(html).toContain('2 workspaces to review');
- expect(html).toContain('Manage');
+ expect(html).toContain('Manage access');
expect(html).toContain('id="billing-section"');
expect(html).toContain('portal-account-context');
- expect(html).toContain('Owner access');
+ expect(html).toContain('Owner role');
expect(html).toContain('portal-account-context-summary');
- expect(html).toContain('Billing enabled');
+ expect(html).toContain('Hosted billing attached');
expect(html).toContain('id="accounts-root"');
expect(html).toContain('MSP account');
expect(html).toContain('Acme MSP');
@@ -156,15 +156,15 @@ describe('shell view', function() {
expect(html).not.toContain('Manage billing');
expect(html).not.toContain('Manage team');
expect(html).toContain('Account state');
- expect(html).toContain('Current hosted workspace state, readiness, and next action.');
+ expect(html).toContain('Hosted workspace counts, current state, and current action.');
expect(html).toContain('section-context-strip');
expect(html).toContain('Needs attention');
expect(html).toContain('Ready');
expect(html).toContain('Next action');
expect(html).toContain('2 workspaces need review');
expect(html).toContain('1 workspace is ready to use');
- expect(html).toContain('Review workspaces');
- expect(html).toContain('Review access');
+ expect(html).toContain('Open Workspaces');
+ expect(html).toContain('Open Access');
expect(html).toContain('account-stage-header-actions');
expect(html).toContain('0 suspended workspaces');
expect(html).toContain('Alpha Workspace');
@@ -348,9 +348,10 @@ describe('shell view', function() {
expect((html.match(/portal-account-context\"/g) || []).length).toBe(1);
expect((html.match(/portal-account-context-stat/g) || []).length).toBe(3);
expect((html.match(/account-context-chip\"/g) || []).length).toBe(3);
- expect(html).toContain('Admin access');
+ expect(html).toContain('Admin role');
expect(html).toContain('Hosted account for workspace access, access control, and billing.');
- expect(html).toContain('Billing enabled');
+ expect(html).toContain('Manage access');
+ expect(html).toContain('Hosted billing attached');
});
it('renders a view-only access surface when the account cannot manage access', function() {
@@ -386,11 +387,11 @@ describe('shell view', function() {
expect(html).toContain('Open a workspace and review current state here. An owner or admin must create or change hosted workspaces.');
expect(html).toContain('Tech role');
expect(html).toContain('Hosted account where you can open workspaces and review who already has access. An owner or admin handles access changes and billing.');
- expect(html).toContain('View only');
+ expect(html).toContain('View roster');
expect(html).toContain('Owner/admin required');
expect(html).toContain('0 workspaces need review');
expect(html).toContain('1 workspace is ready to use');
- expect(html).toContain('Review access');
+ expect(html).toContain('View roster');
expect(html).toContain('Owner or admin required');
expect(html).toContain('Review who already has access to this hosted account. An owner or admin must make changes.');
expect(html).toContain('Review the hosted roster here. An owner or admin must make changes.');
@@ -481,7 +482,7 @@ describe('shell view', function() {
expect(html).toContain('Review who can act');
expect(html).toContain('Review Access to see who can create or manage the first workspace on this account.');
- expect(html).toContain('Review access');
+ expect(html).toContain('Open Access');
expect(html).not.toContain('Choose the right task path');
expect(html).not.toContain('If this is an access change, go to Access. If it is a billing or license issue, go to Billing. Support is only for escalation.');
});
@@ -508,8 +509,8 @@ describe('shell view', function() {
);
expect(html).toContain('0 workspaces are ready to use');
- expect(html).toContain('An owner or admin still needs to create the first hosted workspace before routine work can start.');
- expect(html).not.toContain('Open Workspaces for the current workspace state before routine use.');
+ expect(html).toContain('No hosted workspace exists yet. An owner or admin must create the first one.');
+ expect(html).not.toContain('Open Workspaces to see the current state of each hosted workspace.');
});
it('keeps ready state honest for managed hosted accounts with no workspace yet', function() {
@@ -534,8 +535,8 @@ describe('shell view', function() {
);
expect(html).toContain('0 workspaces are ready to use');
- expect(html).toContain('The first hosted workspace still needs to be created before routine work can start.');
- expect(html).not.toContain('Open Workspaces for the current workspace state before routine use.');
+ expect(html).toContain('No hosted workspace exists yet. Create the first one in Workspaces.');
+ expect(html).not.toContain('Open Workspaces to see the current state of each hosted workspace.');
});
it('keeps next action on review surfaces for suspended hosted view-only accounts', function() {
@@ -569,8 +570,8 @@ describe('shell view', function() {
expect(html).toContain('Review workspace state');
expect(html).toContain('Open Workspaces to review current state, then hand off changes to an owner or admin.');
- expect(html).toContain('Review workspaces');
- expect(html).toContain('Review access');
+ expect(html).toContain('Open Workspaces');
+ expect(html).toContain('Open Access');
expect(html).not.toContain('Choose the right task path');
});
@@ -604,7 +605,7 @@ describe('shell view', function() {
);
expect(html).toContain('0 workspaces are ready to use');
- expect(html).toContain('1 workspace is suspended and excluded from routine use until you resume it.');
+ expect(html).toContain('1 workspace is suspended. Resume it before opening it again.');
expect(html).not.toContain('Active workspaces look clear for routine use.');
});
@@ -715,7 +716,7 @@ describe('shell view', function() {
expect(html).toContain('0 workspaces are ready to use');
expect(html).toContain('Every hosted workspace is suspended right now.');
expect(html).toContain('Create workspace');
- expect(html).toContain('1 workspace is suspended and excluded from routine use until you resume it.');
+ expect(html).toContain('1 workspace is suspended. Resume it before opening it again.');
});
it('preserves the high-density grid, standard sidebar hooks, and strictly horizontal, pill-free action constraints in the rendered shell', function() {
diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts
index 715abd4ad..f177c35c1 100644
--- a/internal/cloudcp/portal/frontend/src/shell_view.ts
+++ b/internal/cloudcp/portal/frontend/src/shell_view.ts
@@ -77,6 +77,30 @@ function suspendedWorkspaceChipLabel(count: number): string {
return count === 1 ? '1 suspended workspace' : String(count) + ' suspended workspaces';
}
+function overviewNavBadgeLabel(hosted: boolean, attentionCount: number, readyCount: number): string {
+ if (!hosted) return 'Billing available';
+ if (attentionCount > 0) return reviewWorkspaceChipLabel(attentionCount);
+ return readyWorkspaceChipLabel(readyCount);
+}
+
+function workspacesNavBadgeLabel(hosted: boolean, totalWorkspaces: number): string {
+ if (!hosted) return 'Unavailable';
+ return workspaceCountLabel(totalWorkspaces);
+}
+
+function accessNavBadgeLabel(hosted: boolean, canManage: boolean): string {
+ if (!hosted) return 'Unavailable';
+ return canManage ? 'Manage access' : 'View roster';
+}
+
+function billingNavBadgeLabel(hostedBillingCount: number): string {
+ return hostedBillingCount > 0 ? 'Hosted billing' : 'Self-hosted billing';
+}
+
+function supportNavBadgeLabel(): string {
+ return 'Escalation only';
+}
+
function hasHostedAccounts(accounts: PortalAccountSummary[]): boolean {
return accounts.length > 0;
}
@@ -197,7 +221,7 @@ function attentionWorkspaces(workspaces: PortalWorkspaceSummary[]): PortalWorksp
}
function accountContextRoleMeta(account: PortalAccountSummary): string {
- return portalRoleLabel(account.role) + (account.can_manage ? ' access' : ' role');
+ return portalRoleLabel(account.role) + ' role';
}
function accountContextLeadCopy(account: PortalAccountSummary): string {
@@ -213,12 +237,12 @@ function accountContextLeadCopy(account: PortalAccountSummary): string {
}
function accountContextAccessSummary(account: PortalAccountSummary): string {
- return account.can_manage ? portalRoleLabel(account.role) : 'View only';
+ return account.can_manage ? 'Manage access' : 'View roster';
}
function accountContextBillingSummary(account: PortalAccountSummary): string {
if (!account.has_billing) return 'Not attached';
- return account.can_manage ? 'Billing enabled' : 'Owner/admin required';
+ return account.can_manage ? 'Hosted billing attached' : 'Owner/admin required';
}
function renderAccountContextStrip(account: PortalAccountSummary): string {
@@ -344,14 +368,17 @@ function renderShellNavigation(accounts: PortalAccountSummary[], supportEmail: s
'
' +
'
Pulse Account
' +
'
Account tasks
' +
- '
' + (hosted ? 'Workspaces, Access, Billing, then Support.' : 'Billing first. Support only after the billing path fails.') + '
' +
+ '
' + (hosted
+ ? 'Use Workspaces, then Access, then Billing. Use Support only if those paths fail.'
+ : 'Use Billing first. Use Support only if that path fails.'
+ ) + '
Suspended' + escapeHTML(suspendedCount > 0
? suspendedCount === 1
- ? '1 workspace is suspended and excluded from routine use until you resume it.'
- : String(suspendedCount) + ' workspaces are suspended and excluded from routine use until you resume them.'
+ ? '1 workspace is suspended. Resume it before opening it again.'
+ : String(suspendedCount) + ' workspaces are suspended. Resume them before opening them again.'
: '0 suspended workspaces.'
) + '
' +
'
' +
@@ -599,10 +626,10 @@ function renderOverviewReadyCard(
: totalWorkspaces > 0
? suspendedCount === totalWorkspaces
? 'Every hosted workspace is suspended right now.'
- : 'Open Workspaces for the current workspace state before routine use.'
+ : 'Open Workspaces to see the current state of each hosted workspace.'
: canManageHosted
- ? 'The first hosted workspace still needs to be created before routine work can start.'
- : 'An owner or admin still needs to create the first hosted workspace before routine work can start.'
+ ? 'No hosted workspace exists yet. Create the first one in Workspaces.'
+ : 'No hosted workspace exists yet. An owner or admin must create the first one.'
) + '' +
''
);
@@ -655,13 +682,13 @@ function renderOverviewNextActionCard(
var hostedViewOnly = accounts.length > 0 && !accessAccount;
if (attention.length) {
- title = 'Review workspace health';
+ title = 'Open Workspaces';
description = attention.length > 1
- ? 'Open Workspaces. Review each failed or pending workspace before taking another account action.'
- : 'Open Workspaces. Review ' + attention[0].workspace.display_name + ' before taking another account action.';
- primaryAction = '';
+ ? 'Open Workspaces to review each failed or pending workspace.'
+ : 'Open Workspaces to review ' + attention[0].workspace.display_name + '.';
+ primaryAction = '';
secondaryAction = accessAccount
- ? ''
+ ? ''
: '';
} else if (ready.length) {
title = 'Open workspace';
@@ -676,13 +703,13 @@ function renderOverviewNextActionCard(
title = 'Create workspace';
description = 'No workspace is ready. Create a workspace in ' + creatableAccount.name + '.';
primaryAction = '';
- secondaryAction = '';
+ secondaryAction = '';
} else if (billingAccount) {
title = 'Open billing';
description = 'Use Billing for invoices, payment methods, or subscription changes.';
primaryAction = '';
secondaryAction = accessAccount
- ? ''
+ ? ''
: '';
} else if (!accounts.length) {
title = 'Open billing';
@@ -692,14 +719,14 @@ function renderOverviewNextActionCard(
if (totalWorkspaces > 0) {
title = 'Review workspace state';
description = 'No workspace is ready. Open Workspaces to review current state, then hand off changes to an owner or admin.';
- primaryAction = '';
- secondaryAction = '';
+ primaryAction = '';
+ secondaryAction = '';
} else {
title = 'Review who can act';
description = showSelfHostedCommercial
? 'No hosted workspace is attached. Review Access to see who can manage this hosted account, or use Billing for self-hosted tasks.'
: 'No hosted workspace is attached yet. Review Access to see who can create or manage the first workspace on this account.';
- primaryAction = '';
+ primaryAction = '';
secondaryAction = showSelfHostedCommercial
? ''
: '';
@@ -753,7 +780,7 @@ function renderShellOverviewSection(context: ShellViewContext): string {
'
' +
'
Overview
' +
'
Account state
' +
- '
Current hosted workspace state, readiness, and next action.
' +
+ '
Hosted workspace counts, current state, and current action.