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 06f7d840e..cc9e649cd 100644 --- a/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md +++ b/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md @@ -1,6 +1,6 @@ # Pulse Account Portal Spec -Last updated: 2026-03-28 +Last updated: 2026-03-29 Status: ACTIVE ## Purpose @@ -257,6 +257,10 @@ Core rules: workspaces remain. The shell may not imply active work is ready merely because a suspended workspace exists; suspended-only states must say that no active workspace is ready for routine use right now. +39. `Overview` must stay fact-first. It may not synthesize urgency or health + verdicts such as `Nothing urgent` or `Healthy now`; it must report + concrete counts, explicit workspace state, and the next action directly + from the owned runtime truth. ## 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 e1203279d..5fcde2376 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -366,6 +366,10 @@ ready until the first hosted workspace exists. That same typed overview contract must also keep `Needs attention` honest when only suspended workspaces remain: hosted workspace history alone may not make the shell imply that active work is ready. +That same typed overview contract must also stay fact-first: overview copy may +not synthesize urgency or health verdicts such as `Nothing urgent` or +`Healthy now`, and must instead render concrete counts, explicit workspace +state, and next-action routing from the owned runtime payload. 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 bc5e2217c..9ccad7555 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -934,6 +934,10 @@ is ready yet until the first hosted workspace exists. That same owned `Overview` surface must also keep `Needs attention` honest when only suspended workspaces remain: a suspended-only account may not imply that active work is ready simply because hosted workspace history exists. +That same owned `Overview` surface must also stay fact-first: it may not +invent urgency or health verdicts such as `Nothing urgent` or `Healthy now`, +and must instead report concrete counts, explicit workspace state, and the +next action directly from runtime-backed account truth. 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 9ee8abb67..8e24a7c7b 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "ec5ecce14e80d4018df77d407516b38b7c0c9ab4ca502f9da79f4b25e75219a4", + "source_hash": "90be82b1835d65ae5df2bf6c5793071dd69a2cb4f9ee855356e9ce0e375f7ac1", "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 77466156c..7d5c33d98 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -1955,6 +1955,21 @@ function workspaceCountLabel(count) { return count === 1 ? "1 workspace" : String(count) + " workspaces"; } + function reviewWorkspaceHeadline(count) { + return count === 1 ? "1 workspace needs review" : String(count) + " workspaces need review"; + } + function readyWorkspaceHeadline(count) { + return count === 1 ? "1 workspace is ready to use" : String(count) + " workspaces are ready to use"; + } + function reviewWorkspaceChipLabel(count) { + return count === 1 ? "1 workspace to review" : String(count) + " workspaces to review"; + } + function readyWorkspaceChipLabel(count) { + return count === 1 ? "1 ready workspace" : String(count) + " ready workspaces"; + } + function suspendedWorkspaceChipLabel(count) { + return count === 1 ? "1 suspended workspace" : String(count) + " suspended workspaces"; + } function hasHostedAccounts(accounts) { return accounts.length > 0; } @@ -2031,11 +2046,11 @@ function workspaceStatusCopy(workspace) { var status = workspaceHealthState(workspace); var state = String(workspace.state || ""); - if (state === "suspended") return "This workspace is suspended and will stay closed until you resume it."; - if (state === "failed") return "This workspace needs attention before it is trustworthy."; - if (status === "healthy") return "Live updates and health checks are currently good."; - if (status === "unhealthy") return "This workspace needs attention before it is trustworthy."; - return "This workspace is still waiting on a completed health check."; + if (state === "suspended") return "This workspace is suspended."; + if (state === "failed") return "This workspace is in a failed state."; + if (status === "healthy") return "Latest health check is healthy."; + if (status === "unhealthy") return "Latest health check is unhealthy."; + return "Latest health check is still pending."; } function workspaceRowNote(workspace) { var status = workspaceHealthState(workspace); @@ -2190,46 +2205,6 @@ if (!includeAccountName) return note; return entry.account.name + " \xB7 " + note; } - function overviewBillingSeparationCopy(accounts, showSelfHostedCommercial) { - var hostedBillingCount = 0; - var canManageHostedBilling = false; - for (var i = 0; i < accounts.length; i += 1) { - if (accounts[i].has_billing) { - hostedBillingCount += 1; - if (accounts[i].can_manage) { - canManageHostedBilling = true; - } - } - } - if (!accounts.length) { - return { - title: "Billing stays separate", - copy: "Self-hosted billing, licenses, refunds, and privacy stay in Billing." - }; - } - if (showSelfHostedCommercial) { - if (hostedBillingCount > 0) { - return { - title: "Billing stays separate", - copy: canManageHostedBilling ? "Hosted billing stays in Billing, and self-hosted tools appear there only when relevant." : "Hosted billing stays in Billing, an owner or admin opens it, and self-hosted tools appear there only when relevant." - }; - } - return { - title: "Billing stays separate", - copy: "Self-hosted tools appear in Billing only when they are relevant to this account." - }; - } - if (hostedBillingCount > 0) { - return { - title: "Hosted billing stays separate", - copy: canManageHostedBilling ? "Use Billing only for hosted invoices, payment methods, or subscription changes." : "Hosted billing stays in Billing, and an owner or admin must open it." - }; - } - return { - title: "Billing stays separate", - copy: "Use Billing only when the task is commercial, not operational." - }; - } function renderOverviewAttentionCard(accounts, entries, showSelfHostedCommercial) { var attention = attentionOverviewEntries(entries); var ready = readyOverviewEntries(entries); @@ -2237,17 +2212,16 @@ var suspendedCount = countWorkspacesByState(entries.map(function(entry) { return entry.workspace; }), "suspended"); - var billingSeparation = overviewBillingSeparationCopy(accounts, showSelfHostedCommercial); if (!attention.length) { - return '
Needs attention

Nothing urgent

' + escapeHTML( - entries.length > 0 ? "No active workspace is currently asking for review." : "No hosted workspace is currently asking for review." - ) + '

Healthy now' + escapeHTML( - ready.length > 0 ? "Active workspaces look clear for routine use." : entries.length > 0 ? "No active workspace is ready for routine use right now." : "There is no hosted workspace waiting for review yet." - ) + '
' + escapeHTML(suspendedCount > 0 ? "Suspended stays parked" : billingSeparation.title) + "" + escapeHTML( - suspendedCount > 0 ? String(suspendedCount) + " suspended workspace" + (suspendedCount === 1 ? " stays" : "s stay") + " out of the way until you deliberately resume it." : billingSeparation.copy + return '

' + escapeHTML(accounts.length > 0 ? reviewWorkspaceHeadline(0) : "0 hosted workspaces need review") + "

" + escapeHTML( + entries.length > 0 ? "No active workspace is failed or waiting on a completed health check." : accounts.length > 0 ? "No hosted workspace is attached to this account yet." : "No hosted account is attached to this sign-in." + ) + '

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." ) + "
"; } - return '

Review these first

These workspaces still need a human check before you treat the account as settled.

' + attention.slice(0, 3).map(function(entry) { + return '

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

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

' + attention.slice(0, 3).map(function(entry) { return '
' + escapeHTML(entry.workspace.display_name) + "" + escapeHTML(overviewWorkspaceContext(entry, includeAccountName, workspaceStatusCopy(entry.workspace))) + "
"; }).join("") + "
"; } @@ -2258,14 +2232,17 @@ var canManageHosted = accounts.some(function(account) { return account.can_manage; }); + var suspendedCount = countWorkspacesByState(entries.map(function(entry) { + return entry.workspace; + }), "suspended"); if (!ready.length) { return '

' + escapeHTML( - !accounts.length ? "Billing tools are ready" : totalWorkspaces > 0 ? "No workspace is ready yet" : "Nothing is ready yet" + !accounts.length ? "Billing is available" : readyWorkspaceHeadline(0) ) + "

" + escapeHTML( - !accounts.length ? "Use Billing for self-hosted subscriptions, licenses, refunds, and privacy requests." : totalWorkspaces > 0 ? "Use Workspaces to review current state before you start routine work." : 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 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." ) + "

"; } - return '

Open and work

These workspaces are active and healthy right now.

' + ready.slice(0, 3).map(function(entry) { + return '

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

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

' + ready.slice(0, 3).map(function(entry) { return '
' + escapeHTML(entry.workspace.display_name) + "" + escapeHTML(overviewWorkspaceContext(entry, includeAccountName, workspaceRowNote(entry.workspace))) + "
" + renderWorkspaceHandoffForm(entry.account.id, entry.workspace.id, accountAPIBasePath, "Open workspace") + "
"; }).join("") + "
"; } @@ -2347,11 +2324,11 @@ var chips = accounts.length ? [ accounts.length === 1 ? "1 account" : String(accounts.length) + " accounts", workspaceCountLabel(totalCount), - String(readyCount) + " ready", - attentionCount > 0 ? String(attentionCount) + " attention" : "Nothing urgent", - suspendedCount > 0 ? String(suspendedCount) + " suspended" : "No suspended" - ] : ["No hosted account", "Billing available", "Support only on escalation"]; - return '"; + readyWorkspaceChipLabel(readyCount), + reviewWorkspaceChipLabel(attentionCount), + suspendedWorkspaceChipLabel(suspendedCount) + ] : ["No hosted account", "0 hosted workspaces", "Billing available", "Support only on escalation"]; + return '"; } function renderNoHostedWorkspacesSection() { return ''; diff --git a/internal/cloudcp/portal/frontend/src/shell_view.test.ts b/internal/cloudcp/portal/frontend/src/shell_view.test.ts index 875f0fddf..192f7dbec 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts @@ -76,11 +76,12 @@ describe('shell view', function() { it('renders empty accounts state with support contact', function() { var html = renderAccountsHTML(createContext()); - expect(html).toContain('Account triage'); + expect(html).toContain('Account state'); expect(html).toContain('No hosted account'); + expect(html).toContain('0 hosted workspaces'); expect(html).toContain('Billing available'); - expect(html).toContain('Nothing urgent'); - expect(html).toContain('Billing tools are ready'); + expect(html).toContain('0 hosted workspaces need review'); + expect(html).toContain('Billing is available'); expect(html).toContain('Open billing'); }); @@ -138,8 +139,8 @@ describe('shell view', function() { expect(html).toContain('What needs attention, what is ready, and the next obvious action.'); expect(html).toContain('1 account'); expect(html).toContain('3 workspaces'); - expect(html).toContain('1 ready'); - expect(html).toContain('2 attention'); + expect(html).toContain('1 ready workspace'); + expect(html).toContain('2 workspaces to review'); expect(html).toContain('Manage'); expect(html).toContain('id="billing-section"'); expect(html).toContain('portal-account-context'); @@ -154,18 +155,18 @@ describe('shell view', function() { expect(html).toContain('Create workspace'); expect(html).not.toContain('Manage billing'); expect(html).not.toContain('Manage team'); - expect(html).toContain('Account triage'); - expect(html).toContain('Only three questions matter here.'); + expect(html).toContain('Account state'); + expect(html).toContain('Current hosted workspace state, readiness, and next action.'); expect(html).toContain('section-context-strip'); expect(html).toContain('Needs attention'); expect(html).toContain('Ready'); expect(html).toContain('Next action'); - expect(html).toContain('Review these first'); - expect(html).toContain('Open and work'); + 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('account-stage-header-actions'); - expect(html).toContain('No suspended'); + expect(html).toContain('0 suspended workspaces'); expect(html).toContain('Alpha Workspace'); expect(html).toContain('Beta Workspace'); expect(html).toContain('Gamma Workspace'); @@ -174,8 +175,8 @@ describe('shell view', function() { expect(html).toContain('Needs attention'); expect(html).toContain('Checking'); expect(html).toContain('Ready to use'); - expect(html).toContain('This workspace needs attention before it is trustworthy.'); - expect(html).toContain('This workspace is still waiting on a completed health check.'); + expect(html).toContain('This workspace is in a failed state.'); + expect(html).toContain('Latest health check is still pending.'); expect(html).toContain('/api/accounts/acct_1/tenants/ws_active/handoff'); expect(html).toContain('Open workspace'); expect(html).toContain('data-action="select-workspace"'); @@ -387,8 +388,8 @@ describe('shell view', function() { 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('Owner/admin required'); - expect(html).toContain('Hosted billing stays separate'); - expect(html).toContain('Hosted billing stays in Billing, and an owner or admin must open it.'); + expect(html).toContain('0 workspaces need review'); + expect(html).toContain('1 workspace is ready to use'); expect(html).toContain('Review access'); 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.'); @@ -506,9 +507,9 @@ describe('shell view', function() { }) ); - expect(html).toContain('Nothing is ready yet'); + 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('Use Workspaces to review current state before you start routine work.'); + expect(html).not.toContain('Open Workspaces for the current workspace state before routine use.'); }); it('keeps ready state honest for managed hosted accounts with no workspace yet', function() { @@ -532,9 +533,9 @@ describe('shell view', function() { }) ); - expect(html).toContain('Nothing is ready yet'); + 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('Use Workspaces to review current state before you start routine work.'); + expect(html).not.toContain('Open Workspaces for the current workspace state before routine use.'); }); it('keeps next action on review surfaces for suspended hosted view-only accounts', function() { @@ -602,8 +603,8 @@ describe('shell view', function() { }) ); - expect(html).toContain('No active workspace is ready for routine use right now.'); - expect(html).toContain('1 suspended workspace stays out of the way until you deliberately resume it.'); + 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).not.toContain('Active workspaces look clear for routine use.'); }); @@ -619,14 +620,15 @@ describe('shell view', function() { expect(html).toContain('Pulse Account'); expect(html).toContain('Account tasks'); expect(html).toContain('Self-hosted'); - expect(html).toContain('Account triage'); + expect(html).toContain('Account state'); expect(html).toMatch(/Workspaces[\s\S]*Unavailable on this account\. Hosted workspaces are not attached here\./); expect(html).toContain('Unavailable on this account. Hosted workspaces are not attached here.'); expect(html).toMatch(/Access[\s\S]*Unavailable on this account\. Hosted roster and role controls live only on hosted workspace accounts\./); expect(html).toContain('Unavailable on this account. Hosted roster and role controls live only on hosted workspace accounts.'); expect(html).toMatch(/Support[\s\S]*Escalation only after the billing path is exhausted\./); expect(html).toContain('No hosted account'); - expect(html).toContain('Billing tools are ready'); + expect(html).toContain('0 hosted workspaces need review'); + expect(html).toContain('Billing is available'); expect(html).toContain('There is nothing to open or manage here yet.'); expect(html).toContain('There are no hosted roles or invites to manage for this account right now.'); expect(html).toContain('Use this billing surface only for self-hosted subscriptions, licenses, refunds, and privacy requests.'); @@ -709,10 +711,11 @@ describe('shell view', function() { ); expect(html).toContain('Suspended until you resume it'); - expect(html).toContain('Nothing urgent'); - expect(html).toContain('No workspace is ready yet'); + expect(html).toContain('0 workspaces need review'); + expect(html).toContain('0 workspaces are ready to use'); + expect(html).toContain('Every hosted workspace is suspended right now.'); expect(html).toContain('Create the next workspace'); - expect(html).toContain('Suspended stays parked'); + expect(html).toContain('1 workspace is suspended and excluded from routine use until you resume it.'); }); 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 fdeb2057d..de5d1850e 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -69,6 +69,26 @@ function workspaceCountLabel(count: number): string { return count === 1 ? '1 workspace' : String(count) + ' workspaces'; } +function reviewWorkspaceHeadline(count: number): string { + return count === 1 ? '1 workspace needs review' : String(count) + ' workspaces need review'; +} + +function readyWorkspaceHeadline(count: number): string { + return count === 1 ? '1 workspace is ready to use' : String(count) + ' workspaces are ready to use'; +} + +function reviewWorkspaceChipLabel(count: number): string { + return count === 1 ? '1 workspace to review' : String(count) + ' workspaces to review'; +} + +function readyWorkspaceChipLabel(count: number): string { + return count === 1 ? '1 ready workspace' : String(count) + ' ready workspaces'; +} + +function suspendedWorkspaceChipLabel(count: number): string { + return count === 1 ? '1 suspended workspace' : String(count) + ' suspended workspaces'; +} + function hasHostedAccounts(accounts: PortalAccountSummary[]): boolean { return accounts.length > 0; } @@ -188,11 +208,11 @@ function renderSectionContextChips(chips: string[]): string { function workspaceStatusCopy(workspace: PortalWorkspaceSummary): string { var status = workspaceHealthState(workspace); var state = String(workspace.state || ''); - if (state === 'suspended') return 'This workspace is suspended and will stay closed until you resume it.'; - if (state === 'failed') return 'This workspace needs attention before it is trustworthy.'; - if (status === 'healthy') return 'Live updates and health checks are currently good.'; - if (status === 'unhealthy') return 'This workspace needs attention before it is trustworthy.'; - return 'This workspace is still waiting on a completed health check.'; + if (state === 'suspended') return 'This workspace is suspended.'; + if (state === 'failed') return 'This workspace is in a failed state.'; + if (status === 'healthy') return 'Latest health check is healthy.'; + if (status === 'unhealthy') return 'Latest health check is unhealthy.'; + return 'Latest health check is still pending.'; } function workspaceRowNote(workspace: PortalWorkspaceSummary): string { @@ -629,26 +649,29 @@ function renderOverviewAttentionCard( var suspendedCount = countWorkspacesByState(entries.map(function(entry) { return entry.workspace; }), 'suspended'); - var billingSeparation = overviewBillingSeparationCopy(accounts, showSelfHostedCommercial); if (!attention.length) { return ( '
' + '' + - '

Nothing urgent

' + + '

' + escapeHTML(accounts.length > 0 ? reviewWorkspaceHeadline(0) : '0 hosted workspaces need review') + '

' + '

' + escapeHTML(entries.length > 0 - ? 'No active workspace is currently asking for review.' - : 'No hosted workspace is currently asking for review.' + ? 'No active workspace is failed or waiting on a completed health check.' + : accounts.length > 0 + ? 'No hosted workspace is attached to this account yet.' + : 'No hosted account is attached to this sign-in.' ) + '

' + '
' + - '
Healthy now' + escapeHTML(ready.length > 0 - ? 'Active workspaces look clear for routine use.' - : entries.length > 0 - ? 'No active workspace is ready for routine use right now.' - : 'There is no hosted workspace waiting for review yet.' + '
Ready' + escapeHTML(entries.length > 0 + ? readyWorkspaceHeadline(ready.length) + : accounts.length > 0 + ? readyWorkspaceHeadline(0) + : '0 hosted workspaces are ready to use' ) + '
' + - '
' + escapeHTML(suspendedCount > 0 ? 'Suspended stays parked' : billingSeparation.title) + '' + escapeHTML(suspendedCount > 0 - ? String(suspendedCount) + ' suspended workspace' + (suspendedCount === 1 ? ' stays' : 's stay') + ' out of the way until you deliberately resume it.' - : billingSeparation.copy + '
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.' ) + '
' + '
' + '
' @@ -658,8 +681,8 @@ function renderOverviewAttentionCard( return ( '
' + '' + - '

Review these first

' + - '

These workspaces still need a human check before you treat the account as settled.

' + + '

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

' + + '

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

' + '
' + attention.slice(0, 3).map(function(entry) { return ( @@ -685,20 +708,23 @@ function renderOverviewReadyCard( var canManageHosted = accounts.some(function(account) { return account.can_manage; }); + var suspendedCount = countWorkspacesByState(entries.map(function(entry) { + return entry.workspace; + }), 'suspended'); if (!ready.length) { return ( '
' + '' + '

' + escapeHTML(!accounts.length - ? 'Billing tools are ready' - : totalWorkspaces > 0 - ? 'No workspace is ready yet' - : 'Nothing is ready yet' + ? 'Billing is available' + : readyWorkspaceHeadline(0) ) + '

' + '

' + escapeHTML(!accounts.length ? 'Use Billing for self-hosted subscriptions, licenses, refunds, and privacy requests.' : totalWorkspaces > 0 - ? 'Use Workspaces to review current state before you start routine work.' + ? 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.' @@ -710,8 +736,8 @@ function renderOverviewReadyCard( return ( '

' + '' + - '

Open and work

' + - '

These workspaces are active and healthy right now.

' + + '

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

' + + '

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

' + '
' + ready.slice(0, 3).map(function(entry) { return ( @@ -840,19 +866,19 @@ function renderShellOverviewSection(context: ShellViewContext): string { ? [ accounts.length === 1 ? '1 account' : String(accounts.length) + ' accounts', workspaceCountLabel(totalCount), - String(readyCount) + ' ready', - attentionCount > 0 ? String(attentionCount) + ' attention' : 'Nothing urgent', - suspendedCount > 0 ? String(suspendedCount) + ' suspended' : 'No suspended', + readyWorkspaceChipLabel(readyCount), + reviewWorkspaceChipLabel(attentionCount), + suspendedWorkspaceChipLabel(suspendedCount), ] - : ['No hosted account', 'Billing available', 'Support only on escalation']; + : ['No hosted account', '0 hosted workspaces', 'Billing available', 'Support only on escalation']; return ( '