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 d68b116da..b71ae88b4 100644 --- a/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md +++ b/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md @@ -244,6 +244,11 @@ Core rules: not leak raw transport strings such as `Network error.` into `Access`, `Workspaces`, or `Billing`; each failure must stay on the task-specific action the user was trying to complete. +36. `Overview` must keep `Ready` honest when no hosted workspace exists yet. + Hosted accounts with zero workspaces may not tell the user to review + current workspace state; they must say that nothing is ready yet and that + the first hosted workspace still needs owner/admin creation before routine + work can start. ## 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 ed75c5db3..4e6517e9d 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -337,6 +337,10 @@ That same shared request/runtime boundary must also preserve task-specific failure copy on transport errors: portal job surfaces may not leak raw strings such as `Network error.`, and must instead surface the owned fallback for the exact action that failed. +That same typed overview contract must also keep `Ready` honest when no hosted +workspace exists yet: hosted accounts with zero workspaces may not route the +user into current workspace review, and must instead render that nothing is +ready until the first hosted workspace exists. plus a package-local `tsc --noEmit` gate, so future account-shell work should extend the typed source boundary instead of reviving opaque global runtime objects, document-wide render events, or untyped embedded asset edits. diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 5ae050d63..690b5ac23 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -898,6 +898,10 @@ That same owned task surface must also keep failure copy on the user job instead of leaking raw transport wording: `Access`, `Workspaces`, and `Billing` failures must render the task-specific action that could not complete, not generic copy such as `Network error.`. +That same owned `Overview` surface must also keep `Ready` honest when no +hosted workspace exists yet: hosted accounts with zero workspaces may not tell +the user to review current workspace state, and must instead say that nothing +is ready yet until the first hosted workspace exists. 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 e476140ce..1c59c8e75 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "d515bbe22da7a6154237402934adaefe1e896687914a72d8bf36587689abf31a", + "source_hash": "2b04882678dc1fd7fd9160747d190ba62581f1c44ab3a6632f04a0c55756b78f", "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 3e4688be3..5ce0ee4d9 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -2232,9 +2232,15 @@ function renderOverviewReadyCard(accounts, entries, accountAPIBasePath) { var ready = readyOverviewEntries(entries); var includeAccountName = accounts.length > 1; + var totalWorkspaces = countWorkspaces(accounts); + var canManageHosted = accounts.some(function(account) { + return account.can_manage; + }); if (!ready.length) { - return '
Ready

' + escapeHTML(accounts.length > 0 ? "No workspace is ready yet" : "Billing tools are ready") + "

" + escapeHTML( - accounts.length > 0 ? "Use Workspaces to review current state before you start routine work." : "Use Billing for self-hosted subscriptions, licenses, refunds, and privacy requests." + return '

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

" + 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." ) + "

"; } return '

Open and work

These workspaces are active and healthy right now.

' + ready.slice(0, 3).map(function(entry) { diff --git a/internal/cloudcp/portal/frontend/src/shell_view.test.ts b/internal/cloudcp/portal/frontend/src/shell_view.test.ts index ae6e00d9d..660830046 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts @@ -478,6 +478,56 @@ describe('shell view', function() { 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.'); }); + it('keeps ready state honest for hosted view-only accounts with no workspace yet', function() { + var html = renderAuthenticatedPortalHTML( + createContext({ + bootstrap: createBootstrap({ + accounts: [ + { + id: 'acct_view_empty', + name: 'Empty Hosted Account', + kind: 'cloud', + kind_label: 'Cloud', + role: 'read_only', + can_manage: false, + has_billing: true, + workspaces: [], + }, + ], + }), + }) + ); + + expect(html).toContain('Nothing is ready yet'); + 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.'); + }); + + it('keeps ready state honest for managed hosted accounts with no workspace yet', function() { + var html = renderAuthenticatedPortalHTML( + createContext({ + bootstrap: createBootstrap({ + accounts: [ + { + id: 'acct_manage_empty', + name: 'Managed Empty Account', + kind: 'msp', + kind_label: 'MSP', + role: 'owner', + can_manage: true, + has_billing: true, + workspaces: [], + }, + ], + }), + }) + ); + + expect(html).toContain('Nothing is ready yet'); + 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.'); + }); + it('keeps next action on review surfaces for suspended hosted view-only accounts', function() { var html = renderAuthenticatedPortalHTML( createContext({ diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts index ed4b3043f..a3d0d88a4 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -678,14 +678,27 @@ function renderOverviewReadyCard( ): string { var ready = readyOverviewEntries(entries); var includeAccountName = accounts.length > 1; + var totalWorkspaces = countWorkspaces(accounts); + var canManageHosted = accounts.some(function(account) { + return account.can_manage; + }); if (!ready.length) { return ( '
' + '' + - '

' + escapeHTML(accounts.length > 0 ? 'No workspace is ready yet' : 'Billing tools are ready') + '

' + - '

' + escapeHTML(accounts.length > 0 - ? 'Use Workspaces to review current state before you start routine work.' - : 'Use Billing for self-hosted subscriptions, licenses, refunds, and privacy requests.' + '

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

' + + '

' + 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.' ) + '

' + '
' );