From 48554917b9ffe7c5bb39b07c58c2585ce0ea480c Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 28 Mar 2026 19:42:59 +0000 Subject: [PATCH] Lead self-hosted billing with real jobs --- .../v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md | 4 +++- docs/release-control/v6/internal/status.json | 16 +--------------- .../v6/internal/subsystems/api-contracts.md | 4 +++- .../v6/internal/subsystems/cloud-paid.md | 4 +++- internal/cloudcp/portal/dist/build_manifest.json | 2 +- internal/cloudcp/portal/dist/portal_app.js | 3 ++- .../portal/frontend/src/shell_view.test.ts | 6 +++++- .../cloudcp/portal/frontend/src/shell_view.ts | 9 +++++++-- 8 files changed, 25 insertions(+), 23 deletions(-) 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 e1247d25ba..bb7f8c637a 100644 --- a/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md +++ b/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md @@ -188,7 +188,9 @@ Core rules: entry points, not an always-open billing dashboard. Self-hosted billing, license, refund, and privacy panels should appear only when that exact billing job is active, and opening one on phone widths must reveal the - active panel in-frame. + active panel in-frame. When no hosted account exists, `Billing` must lead + directly with the self-hosted job picker instead of front-loading an + empty hosted-billing block. 27. `Support` must stay honest to account shape. Self-hosted-only accounts must reduce `Support` to the billing escalation path only; hosted workspace or access failure routes and task buttons must not render diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index f6e034ebb7..ac3dbeb96c 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -3408,21 +3408,7 @@ ] } ], - "work_claims": [ - { - "id": "codex-portal-lane-l3", - "agent_id": "codex-portal", - "summary": "Continue Playwright-driven portal redesign from built review", - "target_id": "v6-rc-stabilization", - "claimed_at": "2026-03-28T19:12:30Z", - "heartbeat_at": "2026-03-28T19:12:30Z", - "expires_at": "2026-03-28T21:12:30Z", - "work_item": { - "kind": "lane", - "id": "L3" - } - } - ], + "work_claims": [], "open_decisions": [], "source_of_truth_file": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md", "resolved_decisions": [ diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 4e9fe7a47a..58b6bdbcf6 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -291,7 +291,9 @@ panel on phone-width layouts instead of leaving it offscreen. The same bootstrap/runtime contract must also carry explicit truth for whether 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. `Support` follows the same +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 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 15287cbac6..d03a640c10 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -846,7 +846,9 @@ controls appear only when that exact access job is active. `Billing` follows the same task-first rule: hosted billing leads when present, the self-hosted billing, license, refund, and privacy paths reduce to explicit job pickers, and the active self-hosted billing panel must stay hidden until that exact job -is opened and then be revealed in-frame on narrow layouts. `Support` follows +is opened and then be revealed in-frame on narrow layouts. When no hosted +account exists, the billing shell must skip any empty hosted-billing block and +lead directly with the self-hosted job picker. `Support` follows the same account-shape rule: self-hosted-only accounts reduce to the billing escalation path and billing-specific handoff packet only, and hosted workspace/access escalation routes must not render without hosted accounts. diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json index e5cc6629c7..df73e4eb28 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "30ae2fd08e074502921f7f7d0f942df540d1192e9e5234e6ea09387cc8593dbb", + "source_hash": "f417480ba4d79a766c8bb7511a50e02f825cb2199eb6961c618b5a9429d63975", "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 e66f2c2f9d..5ba82c77c0 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -2287,13 +2287,14 @@ return account.has_billing; }).length; var billingNote = hosted ? showSelfHostedCommercial ? "Use hosted billing first when the request belongs to a hosted workspace account. Self-hosted licenses, refunds, and privacy stay separate underneath it." : "Use this billing surface only for hosted billing on your hosted workspace accounts." : "Use this billing surface only for self-hosted subscriptions, licenses, refunds, and privacy requests."; + var selfHostedBillingEscalationCopy = hosted ? "Escalate with the same hosted billing action or self-hosted path and the exact failed step." : "Escalate with the same self-hosted billing path and the exact failed step."; var hostedContent = accounts.length ? accounts.map(function(account) { return '
"; }).join("") : renderNoHostedWorkspacesSection() + renderNoHostedAccessSection(); return '
' + renderShellNavigation(accounts, context.bootstrap.support_email || "", activeSection) + '
' + (accounts.length === 1 ? renderAccountContextStrip(accounts[0]) : "") + '
' + renderShellOverviewSection(context) + '
' + hostedContent + '

Billing

' + renderSectionContextChips([ hostedBillingCount > 0 ? "Hosted billing" : "No hosted billing", showSelfHostedCommercial ? "Self-hosted tools" : "Hosted only" - ]) + '
' + billingNote + '
' + renderHostedBillingCards(accounts, showSelfHostedCommercial) + "
" + (showSelfHostedCommercial ? '

Pick the self-hosted job

Use self-hosted billing only for self-hosted purchases. Open one path at a time when hosted billing does not apply.

' + renderBillingActionRow("open-manage-billing", "Self-hosted billing", "Manage subscriptions", "Billing", "Open Stripe for self-hosted plan, invoice, and payment changes.", "manage-billing-panel", "manage-inline-email", ["Plan changes", "Invoices"]) + renderBillingActionRow("open-retrieve-billing", "Licenses", "Retrieve licenses", "Licenses", "Recover the latest active self-hosted license and invoice link.", "retrieve-billing-panel", "retrieve-inline-email", ["Latest active license", "Invoice lookup"]) + renderBillingActionRow("open-refund-billing", "Refunds", "Refund requests", "Refunds", "Request a self-serve refund when the purchase is still eligible.", "refund-billing-panel", "refund-inline-email", ["Eligibility check", "Revocation"]) + renderBillingActionRow("open-data-billing", "Privacy", "Data and privacy", "Privacy", "Request export or deletion for commercial account data.", "data-billing-panel", "data-export-email", ["Export", "Deletion"]) + '

Use Support only after Billing fails

Escalate with the same hosted billing action or self-hosted path and the exact failed step.

' + billingNote + "
" + (hosted ? '
' + renderHostedBillingCards(accounts, showSelfHostedCommercial) + "
" : "") + (showSelfHostedCommercial ? '

Pick the self-hosted job

Use self-hosted billing only for self-hosted purchases. Open one path at a time when hosted billing does not apply.

' + renderBillingActionRow("open-manage-billing", "Self-hosted billing", "Manage subscriptions", "Billing", "Open Stripe for self-hosted plan, invoice, and payment changes.", "manage-billing-panel", "manage-inline-email", ["Plan changes", "Invoices"]) + renderBillingActionRow("open-retrieve-billing", "Licenses", "Retrieve licenses", "Licenses", "Recover the latest active self-hosted license and invoice link.", "retrieve-billing-panel", "retrieve-inline-email", ["Latest active license", "Invoice lookup"]) + renderBillingActionRow("open-refund-billing", "Refunds", "Refund requests", "Refunds", "Request a self-serve refund when the purchase is still eligible.", "refund-billing-panel", "refund-inline-email", ["Eligibility check", "Revocation"]) + renderBillingActionRow("open-data-billing", "Privacy", "Data and privacy", "Privacy", "Request export or deletion for commercial account data.", "data-billing-panel", "data-export-email", ["Export", "Deletion"]) + '

Use Support only after Billing fails

' + selfHostedBillingEscalationCopy + '

' + '
' + billingNote + '
' + '
' + - '
' + renderHostedBillingCards(accounts, showSelfHostedCommercial) + '
' + + (hosted + ? ('
' + renderHostedBillingCards(accounts, showSelfHostedCommercial) + '
') + : '') + (showSelfHostedCommercial ? ('
' + '
' + @@ -1276,7 +1281,7 @@ export function renderAuthenticatedPortalHTML(context: ShellViewContext): string '
' + '' + '

Use Support only after Billing fails

' + - '

Escalate with the same hosted billing action or self-hosted path and the exact failed step.

' + + '

' + selfHostedBillingEscalationCopy + '

' + '
' + '' + '