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 = '
RosterLoading\u2026
Mode' + (canManage ? "Manage" : "View") + "
"; + stats.innerHTML = '
RosterLoading\u2026
Access' + (canManage ? "Manage access" : "View roster") + "
"; return; } if (entry.accessQuery.status === "error") { - stats.innerHTML = '
RosterNeeds attention
Mode' + (canManage ? "Manage" : "View") + "
"; + stats.innerHTML = '
RosterLoad failed
Access' + (canManage ? "Manage access" : "View roster") + "
"; return; } var members = entry.accessQuery.data; @@ -411,7 +411,7 @@ } 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) { @@ -1988,6 +1988,25 @@ function suspendedWorkspaceChipLabel(count) { return count === 1 ? "1 suspended workspace" : String(count) + " suspended workspaces"; } + function overviewNavBadgeLabel(hosted, attentionCount, readyCount) { + if (!hosted) return "Billing available"; + if (attentionCount > 0) return reviewWorkspaceChipLabel(attentionCount); + return readyWorkspaceChipLabel(readyCount); + } + function workspacesNavBadgeLabel(hosted, totalWorkspaces) { + if (!hosted) return "Unavailable"; + return workspaceCountLabel(totalWorkspaces); + } + function accessNavBadgeLabel(hosted, canManage) { + if (!hosted) return "Unavailable"; + return canManage ? "Manage access" : "View roster"; + } + function billingNavBadgeLabel(hostedBillingCount) { + return hostedBillingCount > 0 ? "Hosted billing" : "Self-hosted billing"; + } + function supportNavBadgeLabel() { + return "Escalation only"; + } function hasHostedAccounts(accounts) { return accounts.length > 0; } @@ -2072,7 +2091,7 @@ return results; } function accountContextRoleMeta(account) { - return portalRoleLabel(account.role) + (account.can_manage ? " access" : " role"); + return portalRoleLabel(account.role) + " role"; } function accountContextLeadCopy(account) { var accountPrefix = account.kind === "msp" ? "Hosted workspace account" : "Hosted account"; @@ -2082,11 +2101,11 @@ return accountPrefix + (account.has_billing ? " where you can open workspaces and review who already has access. An owner or admin handles access changes and billing." : " where you can open workspaces and review who already has access. An owner or admin handles account changes."); } function accountContextAccessSummary(account) { - return account.can_manage ? portalRoleLabel(account.role) : "View only"; + return account.can_manage ? "Manage access" : "View roster"; } function accountContextBillingSummary(account) { 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) { var workspaceLabel = workspaceCountLabel((account.workspaces || []).length); @@ -2153,7 +2172,7 @@ } } } - return '"; + return '"; } function renderWorkspaceCard(account, workspace, accountAPIBasePath) { var status = workspaceHealthState(workspace); @@ -2218,7 +2237,7 @@ ) + '

Ready' + escapeHTML( entries.length > 0 ? readyWorkspaceHeadline(ready.length) : accounts.length > 0 ? readyWorkspaceHeadline(0) : "0 hosted workspaces are ready to use" ) + '
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." ) + "
"; } return '
Needs attention

' + escapeHTML(reviewWorkspaceHeadline(attention.length)) + '

Each listed workspace is failed or still waiting on a completed health check.

' + attention.slice(0, 3).map(function(entry) { @@ -2239,7 +2258,7 @@ return '

' + escapeHTML( !accounts.length ? "Billing is available" : readyWorkspaceHeadline(0) ) + "

" + 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 '

' + escapeHTML(readyWorkspaceHeadline(ready.length)) + '

Each listed workspace is active and passed its latest health check.

' + ready.slice(0, 3).map(function(entry) { @@ -2265,10 +2284,10 @@ }) || null; var hostedViewOnly = accounts.length > 0 && !accessAccount; if (attention.length) { - title = "Review workspace health"; - 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 = ''; - secondaryAction = accessAccount ? '' : ""; + title = "Open Workspaces"; + description = attention.length > 1 ? "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"; description = accounts.length > 1 ? "Open " + ready[0].workspace.display_name + " in " + ready[0].account.name + "." : "Open the ready workspace."; @@ -2278,12 +2297,12 @@ 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 ? '' : ""; + secondaryAction = accessAccount ? '' : ""; } else if (!accounts.length) { title = "Open billing"; description = "Use Billing for self-hosted subscriptions, licenses, refunds, or privacy requests."; @@ -2292,12 +2311,12 @@ 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 ? '' : ""; } } else if (accessAccount) { @@ -2328,7 +2347,7 @@ reviewWorkspaceChipLabel(attentionCount), suspendedWorkspaceChipLabel(suspendedCount) ] : ["No hosted account", "0 hosted workspaces", "Billing available", "Support only on escalation"]; - return '"; + return '"; } function renderNoHostedWorkspacesSection() { return ''; diff --git a/internal/cloudcp/portal/frontend/src/account_view.test.ts b/internal/cloudcp/portal/frontend/src/account_view.test.ts index 46e1250cc..466c4db80 100644 --- a/internal/cloudcp/portal/frontend/src/account_view.test.ts +++ b/internal/cloudcp/portal/frontend/src/account_view.test.ts @@ -104,7 +104,7 @@ describe('account view', function() { expect(document.getElementById('add-ws-form-acct_1')?.classList.contains('visible')).toBe(false); }); - it('renders team loading, error, and populated member states', function() { + it('renders access loading, error, and populated member states', function() { document.body.innerHTML = '
' + '
' + @@ -116,16 +116,17 @@ describe('account view', function() { accessQuery: { status: 'loading', error: '', data: [] }, })); expect(document.getElementById('access-list-acct_1')?.textContent).toContain('Loading roster'); - expect(document.getElementById('access-stats-acct_1')?.textContent).toContain('Mode'); - expect(document.getElementById('access-stats-acct_1')?.textContent).toContain('Manage'); + expect(document.getElementById('access-stats-acct_1')?.textContent).toContain('Access'); + expect(document.getElementById('access-stats-acct_1')?.textContent).toContain('Manage access'); renderAccessSection('acct_1', createEntry({ accessVisible: true, accessQuery: { status: 'error', error: 'Failed to load access roster.', data: [] }, })); - expect(document.getElementById('access-list-acct_1')?.textContent).toContain('Roster needs attention'); + expect(document.getElementById('access-list-acct_1')?.textContent).toContain('Failed to load roster'); expect(document.getElementById('access-list-acct_1')?.textContent).toContain('Failed to load access roster.'); - expect(document.getElementById('access-stats-acct_1')?.textContent).toContain('Manage'); + expect(document.getElementById('access-stats-acct_1')?.textContent).toContain('Load failed'); + expect(document.getElementById('access-stats-acct_1')?.textContent).toContain('Manage access'); renderAccessSection( 'acct_1', diff --git a/internal/cloudcp/portal/frontend/src/account_view.ts b/internal/cloudcp/portal/frontend/src/account_view.ts index d1c54df19..315f8c0ef 100644 --- a/internal/cloudcp/portal/frontend/src/account_view.ts +++ b/internal/cloudcp/portal/frontend/src/account_view.ts @@ -179,13 +179,13 @@ function renderAccessStats(accountID: string, entry: PortalAccountUIEntry, canMa if (entry.accessQuery.status === 'loading') { stats.innerHTML = '
RosterLoading…
' + - '
Mode' + (canManage ? 'Manage' : 'View') + '
'; + '
Access' + (canManage ? 'Manage access' : 'View roster') + '
'; return; } if (entry.accessQuery.status === 'error') { stats.innerHTML = - '
RosterNeeds attention
' + - '
Mode' + (canManage ? 'Manage' : 'View') + '
'; + '
RosterLoad failed
' + + '
Access' + (canManage ? 'Manage access' : 'View roster') + '
'; 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.' + ) + '
' + '
' + '
' + - shellSectionButton('overview', activeSection, '01', 'Overview', 'Review status, ready workspaces, and the next action.', attentionCount > 0 ? String(attentionCount) + ' review' : (hosted ? String(readyWorkspaces) + ' ready' : 'Summary')) + - shellSectionButton('workspaces', activeSection, '02', 'Workspaces', workspaceNavCopy(hosted, canManage), hosted ? String(readyWorkspaces) + ' ready' : 'Unavailable') + - shellSectionButton('access', activeSection, '03', 'Access', accessNavCopy(hosted, canManage), hosted ? (canManage ? 'Manage' : 'View') : 'Unavailable') + - shellSectionButton('billing', activeSection, '04', 'Billing', billingNavCopy(hostedBillingCount, canManageHostedBilling), hostedBillingCount > 0 ? (hostedBillingCount > 1 ? 'Hosted +' : 'Hosted') : 'Self-hosted') + - shellSectionButton('support', activeSection, '05', 'Support', supportNavCopy(hosted, canManage), supportEmail ? 'Email' : 'Help') + + shellSectionButton('overview', activeSection, '01', 'Overview', 'Review workspace counts, current state, and the current action.', overviewNavBadgeLabel(hosted, attentionCount, readyWorkspaces)) + + shellSectionButton('workspaces', activeSection, '02', 'Workspaces', workspaceNavCopy(hosted, canManage), workspacesNavBadgeLabel(hosted, totalWorkspaces)) + + shellSectionButton('access', activeSection, '03', 'Access', accessNavCopy(hosted, canManage), accessNavBadgeLabel(hosted, canManage)) + + shellSectionButton('billing', activeSection, '04', 'Billing', billingNavCopy(hostedBillingCount, canManageHostedBilling), billingNavBadgeLabel(hostedBillingCount)) + + shellSectionButton('support', activeSection, '05', 'Support', supportNavCopy(hosted, canManage), supportNavBadgeLabel()) + '
' + '' ); @@ -544,8 +571,8 @@ function renderOverviewAttentionCard( ) + '
' + '
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 { '
' + '' + '

Account state

' + - '

Current hosted workspace state, readiness, and next action.

' + + '

Hosted workspace counts, current state, and current action.

' + renderSectionContextChips(chips) + '
' + '
' +