Lead self-hosted billing with real jobs

This commit is contained in:
rcourtman 2026-03-28 19:42:59 +00:00
parent 35c056bc8f
commit 48554917b9
8 changed files with 25 additions and 23 deletions

View file

@ -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

View file

@ -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": [

View file

@ -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

View file

@ -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.

View file

@ -1,5 +1,5 @@
{
"source_hash": "30ae2fd08e074502921f7f7d0f942df540d1192e9e5234e6ea09387cc8593dbb",
"source_hash": "f417480ba4d79a766c8bb7511a50e02f825cb2199eb6961c618b5a9429d63975",
"build_inputs": [
"package.json",
"tsconfig.json",

View file

@ -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 '<section class="account-surface"><div class="account-surface-body">' + renderAccountWorkspaceSection(account, context.accountAPIBasePath) + renderAccountAccessSection(account) + "</div></section>";
}).join("") : renderNoHostedWorkspacesSection() + renderNoHostedAccessSection();
return '<div class="portal-shell" data-shell-section="' + activeSection + '"><div class="portal-shell-layout">' + renderShellNavigation(accounts, context.bootstrap.support_email || "", activeSection) + '<div class="portal-shell-main">' + (accounts.length === 1 ? renderAccountContextStrip(accounts[0]) : "") + '<section class="portal-content-panel portal-content-panel-overview">' + renderShellOverviewSection(context) + '<div id="accounts-root">' + hostedContent + '</div></section><section class="portal-content-panel portal-content-panel-billing billing-section" id="billing-section"><div class="billing-header"><div><div class="account-panel-kicker">Billing</div><h2>Billing</h2>' + renderSectionContextChips([
hostedBillingCount > 0 ? "Hosted billing" : "No hosted billing",
showSelfHostedCommercial ? "Self-hosted tools" : "Hosted only"
]) + '</div><div class="billing-note">' + billingNote + '</div></div><div class="billing-overview-grid">' + renderHostedBillingCards(accounts, showSelfHostedCommercial) + "</div>" + (showSelfHostedCommercial ? '<div class="billing-shell billing-shell-idle"><div class="billing-shell-main"><div class="billing-shell-main-head"><div class="account-panel-kicker">Self-hosted billing</div><h3>Pick the self-hosted job</h3><p>Use self-hosted billing only for self-hosted purchases. Open one path at a time when hosted billing does not apply.</p></div><div class="billing-action-list">' + 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"]) + '</div><div class="billing-inline-support"><div class="account-panel-kicker">Escalation only</div><h4>Use Support only after Billing fails</h4><p>Escalate with the same hosted billing action or self-hosted path and the exact failed step.</p><div class="billing-inline-support-actions"><button type="button" class="btn-secondary btn-compact" data-shell-action="activate-section" data-shell-section="support">Open support</button><a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '">' + escapeHTML(context.bootstrap.support_email || "") + '</a></div></div></div><div class="billing-shell-detail" id="billing-detail-shell" hidden>' + renderBillingTaskPanel(
]) + '</div><div class="billing-note">' + billingNote + "</div></div>" + (hosted ? '<div class="billing-overview-grid">' + renderHostedBillingCards(accounts, showSelfHostedCommercial) + "</div>" : "") + (showSelfHostedCommercial ? '<div class="billing-shell billing-shell-idle"><div class="billing-shell-main"><div class="billing-shell-main-head"><div class="account-panel-kicker">Self-hosted billing</div><h3>Pick the self-hosted job</h3><p>Use self-hosted billing only for self-hosted purchases. Open one path at a time when hosted billing does not apply.</p></div><div class="billing-action-list">' + 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"]) + '</div><div class="billing-inline-support"><div class="account-panel-kicker">Escalation only</div><h4>Use Support only after Billing fails</h4><p>' + selfHostedBillingEscalationCopy + '</p><div class="billing-inline-support-actions"><button type="button" class="btn-secondary btn-compact" data-shell-action="activate-section" data-shell-section="support">Open support</button><a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '">' + escapeHTML(context.bootstrap.support_email || "") + '</a></div></div></div><div class="billing-shell-detail" id="billing-detail-shell" hidden>' + renderBillingTaskPanel(
"Manage subscriptions",
"Open Stripe for self-hosted plan, invoice, and payment changes.",
"manage-billing-panel",

View file

@ -267,6 +267,7 @@ describe('shell view', function() {
expect(html).toContain('id="data-billing-panel"');
expect(html).toContain('Billing path failed');
expect(html).toContain('licenses, refunds, or privacy');
expect(html).toContain('Escalate with the same hosted billing action or self-hosted path and the exact failed step.');
});
it('keeps top-level task navigation in the canonical section order', function() {
@ -401,11 +402,14 @@ describe('shell view', function() {
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.');
expect(html).toContain('No hosted billing attached');
expect(html).toContain('Use self-hosted billing only for self-hosted purchases. Open one path at a time when hosted billing does not apply.');
expect(html).toContain('Escalate with the same self-hosted billing path and the exact failed step.');
expect(html).toContain('Billing');
expect(html).toContain('Use support only when the Billing path has already stopped you.');
expect(html).toContain('Self-hosted billing path failed');
expect(html).toContain('Purchase email');
expect(html).not.toContain('No hosted billing attached');
expect(html).not.toContain('Escalate with the same hosted billing action or self-hosted path and the exact failed step.');
expect(html).not.toContain('Workspace or access path failed');
expect(html).not.toContain('Open workspaces');
expect(html).not.toContain('Open access');

View file

@ -1223,6 +1223,9 @@ export function renderAuthenticatedPortalHTML(context: ShellViewContext): string
? '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 (
@ -1258,7 +1261,9 @@ export function renderAuthenticatedPortalHTML(context: ShellViewContext): string
'</div>' +
'<div class="billing-note">' + billingNote + '</div>' +
'</div>' +
'<div class="billing-overview-grid">' + renderHostedBillingCards(accounts, showSelfHostedCommercial) + '</div>' +
(hosted
? ('<div class="billing-overview-grid">' + renderHostedBillingCards(accounts, showSelfHostedCommercial) + '</div>')
: '') +
(showSelfHostedCommercial
? ('<div class="billing-shell billing-shell-idle">' +
'<div class="billing-shell-main">' +
@ -1276,7 +1281,7 @@ export function renderAuthenticatedPortalHTML(context: ShellViewContext): string
'<div class="billing-inline-support">' +
'<div class="account-panel-kicker">Escalation only</div>' +
'<h4>Use Support only after Billing fails</h4>' +
'<p>Escalate with the same hosted billing action or self-hosted path and the exact failed step.</p>' +
'<p>' + selfHostedBillingEscalationCopy + '</p>' +
'<div class="billing-inline-support-actions">' +
'<button type="button" class="btn-secondary btn-compact" data-shell-action="activate-section" data-shell-section="support">Open support</button>' +
'<a class="portal-support-link" href="mailto:' +