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 bb7f8c637..2ecd97f33 100644 --- a/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md +++ b/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md @@ -159,7 +159,8 @@ Core rules: current account set, not a repeated per-account dashboard. 17. When `Workspaces` or `Access` are unavailable, the tab must show an explicit unavailable state that points to the right live task instead of - rendering blank space. + rendering blank space. The shell navigation must stay honest too: it must + not badge or describe unavailable hosted tasks as though they are live. 18. `Access` must stay action-first: roster, invite, role change, and remove access are the job; view-only users may review the roster but must never see live controls that imply they can mutate it. diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 58b6bdbcf..8ecc7f251 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -293,7 +293,9 @@ self-hosted commercial history is relevant to the signed-in account, so hosted-only accounts do not render self-hosted license, refund, privacy, or self-hosted escalation paths by default, and self-hosted-only accounts do not front-load an empty hosted-billing block before the real self-hosted jobs. -`Support` follows the same +The same account-shape runtime contract must also keep the shell navigation +honest: unavailable hosted `Workspaces` or `Access` tasks must read as +unavailable in the nav itself rather than implying live hosted work. `Support` follows the same account-shape runtime contract: self-hosted-only accounts expose only the billing escalation path and billing-specific handoff packet, and hosted workspace/access escalation controls must not render when no hosted account diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index d03a640c1..8a175ec22 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -827,7 +827,9 @@ standalone Relay portal. That shell now also owns two product rules explicitly: `Overview` is one shell-level triage surface across the current account set rather than a repeated per-account dashboard, and unavailable top-level `Workspaces` or `Access` tasks must render an explicit unavailable -state instead of blank space. The same shell also owns action-first task +state instead of blank space. The shell navigation must stay honest to that +same account shape rather than labelling unavailable hosted tasks as though +they are still live. The same shell also owns action-first task surfaces for `Access`, `Billing`, and `Support`: access mutations must be permission-honest and roster-led, billing must reduce to one obvious job at a time with hosted billing first when relevant, support must stay a failed-path diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json index df73e4eb2..631792236 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "f417480ba4d79a766c8bb7511a50e02f825cb2199eb6961c618b5a9429d63975", + "source_hash": "176432adedcb3aa8f1e151678f7e93e3db683ef6029b8d6b60a04264e2bff8fd", "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 5ba82c77c..25722f272 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -2038,6 +2038,7 @@ } function renderShellNavigation(accounts, supportEmail, activeSection) { var hosted = hasHostedAccounts(accounts); + var selfHostedOnly = !hosted; var workspaces = collectWorkspaces(accounts); var totalWorkspaces = workspaces.length; var readyWorkspaces = countReadyWorkspaces(workspaces); @@ -2052,7 +2053,7 @@ hostedBillingCount += 1; } } - return '"; + return '"; } function renderWorkspaceCard(account, workspace, accountAPIBasePath) { var status = workspaceHealthState(workspace); diff --git a/internal/cloudcp/portal/frontend/src/shell_view.test.ts b/internal/cloudcp/portal/frontend/src/shell_view.test.ts index a6ea467fe..df79e3683 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts @@ -397,6 +397,11 @@ describe('shell view', function() { expect(html).toContain('Account tasks'); expect(html).toContain('Self-hosted'); expect(html).toContain('Account triage'); + 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('There is nothing to open or manage here yet.'); diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts index bcf248af1..450917076 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -360,6 +360,7 @@ function shellSectionButton(section: PortalShellSection, activeSection: PortalSh function renderShellNavigation(accounts: PortalAccountSummary[], supportEmail: string, activeSection: PortalShellSection): string { var hosted = hasHostedAccounts(accounts); + var selfHostedOnly = !hosted; var workspaces = collectWorkspaces(accounts); var totalWorkspaces = workspaces.length; var readyWorkspaces = countReadyWorkspaces(workspaces); @@ -379,14 +380,14 @@ function renderShellNavigation(accounts: PortalAccountSummary[], supportEmail: s '