From 8596d9de8a018fd783f685f98d04dcb5349e2dbb Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 27 Mar 2026 19:07:26 +0000 Subject: [PATCH] style(portal): simplify account console overview --- .../cloudcp/portal/dist/build_manifest.json | 2 +- internal/cloudcp/portal/dist/portal_app.css | 115 +++++++-------- internal/cloudcp/portal/dist/portal_app.js | 30 ++-- .../portal/frontend/src/shell_view.test.ts | 10 +- .../cloudcp/portal/frontend/src/shell_view.ts | 126 ++++++++--------- .../cloudcp/portal/frontend/src/styles.css | 132 +++++++++--------- 6 files changed, 215 insertions(+), 200 deletions(-) diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json index aab2dc64f..2fa9f4fd3 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "85ccadc015ace1a5ed37c100689292817f7c41de9b0d8547a6d26ca908c4d19f", + "source_hash": "b4d41db9eab964bde794244e2c7d9d7e0aa77de1523520c2c448d1f08cfe969f", "build_inputs": [ "package.json", "tsconfig.json", diff --git a/internal/cloudcp/portal/dist/portal_app.css b/internal/cloudcp/portal/dist/portal_app.css index a196e9fce..b99d432e4 100644 --- a/internal/cloudcp/portal/dist/portal_app.css +++ b/internal/cloudcp/portal/dist/portal_app.css @@ -399,6 +399,46 @@ header .logout-btn:hover, .portal-shell-main > .portal-content-panel { display: none !important; } +.portal-account-bar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 18px; + align-items: end; + padding: 16px 22px 18px; + border-bottom: 1px solid var(--line); + background: rgba(244, 247, 250, 0.92); +} +.portal-account-bar-copy { + display: flex; + flex-direction: column; + gap: 6px; +} +.portal-account-bar-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} +.portal-account-bar-row h2 { + margin: 0; + font-size: 28px; + line-height: 0.98; + letter-spacing: -0.05em; +} +.portal-account-bar-copy p { + margin: 0; + font-size: 13px; + line-height: 1.5; + color: var(--ink-soft); +} +.portal-account-bar-chips, +.portal-account-bar-actions { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} .portal-shell[data-shell-section=overview] .portal-content-panel-overview, .portal-shell[data-shell-section=services] .portal-content-panel-services, .portal-shell[data-shell-section=support] .portal-content-panel-support, @@ -470,29 +510,6 @@ header .logout-btn:hover, .account-surface + .account-surface { margin-top: 22px; } -.account-surface-header { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: end; - gap: 16px; - padding: 0 4px 12px; - border: none; - border-bottom: 1px solid var(--line); - border-radius: var(--radius-md); - background: transparent; - box-shadow: none; -} -.account-heading h2 { - margin: 2px 0 0; - font-size: 24px; - line-height: 1.02; - letter-spacing: -0.05em; -} -.account-summary { - margin-top: 5px; - font-size: 12px; - color: var(--ink-soft); -} .account-badges, .account-context-strip, .workspace-badges, @@ -536,7 +553,6 @@ header .logout-btn:hover, display: grid; grid-template-columns: 1fr; gap: 14px; - margin-top: 10px; } .account-content-panel { display: block; @@ -704,36 +720,11 @@ header .logout-btn:hover, .overview-side-card-primary { background: var(--panel-strong); } -.overview-quick-actions { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 10px; - margin-top: 16px; -} -.overview-quick-action { +.overview-next-actions { display: flex; - flex-direction: column; - gap: 6px; - padding: 14px; - border: 1px solid var(--line); - border-radius: 10px; - background: #fff; - text-align: left; - cursor: pointer; -} -.overview-quick-action:hover { - border-color: var(--accent); - box-shadow: var(--shadow-sm); -} -.overview-quick-action-title { - font-size: 14px; - font-weight: 700; - color: var(--ink); -} -.overview-quick-action-copy { - font-size: 12px; - line-height: 1.5; - color: var(--ink-soft); + gap: 10px; + flex-wrap: wrap; + margin-top: 16px; } .overview-alert-list { display: flex; @@ -1100,17 +1091,22 @@ header .logout-btn:hover, display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(320px, 0.92fr); gap: 14px; + align-items: start; } .team-side-column { display: flex; flex-direction: column; gap: 14px; + align-self: start; } .team-roster, .team-invite-panel { background: #fff; border-color: var(--line); } +.team-roster { + align-self: start; +} .team-policy-panel { padding: 16px; border: 1px solid var(--line); @@ -1695,9 +1691,6 @@ header .logout-btn:hover, .account-metric-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .overview-quick-actions { - grid-template-columns: 1fr; - } .portal-shell-nav { position: static; } @@ -1712,6 +1705,13 @@ header .logout-btn:hover, .portal-shell-head-row { grid-template-columns: 1fr; } + .portal-account-bar { + grid-template-columns: 1fr; + align-items: start; + } + .portal-account-bar-row { + align-items: start; + } .portal-shell-head-stats { flex-wrap: wrap; } @@ -1742,11 +1742,12 @@ header .logout-btn:hover, padding: 18px; } .portal-shell-head, + .portal-account-bar, .portal-content-panel { padding-left: 16px; padding-right: 16px; } - .account-surface-header, + .portal-account-bar-row, .workspace-management-header, .team-management-header, .service-header, diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js index f03cc3161..ceb8c1d25 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -1695,9 +1695,6 @@ }).join(""); return '
' + kicker + 'Self-hosted

' + title + "

" + description + '

' + highlightHTML + '
'; } - function renderOverviewQuickAction(section, title, copy) { - return '"; - } function workspaceStatusCopy(workspace) { var status = workspaceHealthState(workspace); if (status === "healthy") return "Live updates and health checks are currently good."; @@ -1737,6 +1734,20 @@ var summary = hosted ? "Hosted operations, operator access, and commercial account services." : "Billing, license recovery, refunds, and privacy actions until hosted access is attached."; return '
Pulse Account

' + (hosted ? "Operator console" : "Account console") + '

' + (hosted ? "Operator ready" : "Self-hosted only") + "

" + statusText + " " + summary + '

Hosted access' + (hosted ? "Active" : "Not attached") + '
Accounts' + (accounts.length === 1 ? "1 account" : String(accounts.length) + " accounts") + '
Workspace fleet' + (workspaceTotal ? workspaceCountLabel(workspaceTotal) : "0 workspaces") + "
"; } + function renderPrimaryAccountBar(account) { + var workspaceLabel = workspaceCountLabel((account.workspaces || []).length); + var actionHTML = ""; + if (account.kind === "msp" && account.can_manage) { + actionHTML += ''; + } + if (account.has_billing && account.can_manage) { + actionHTML += ''; + } + if (account.can_manage) { + actionHTML += ''; + } + return '
"; + } function shellSectionButton(section, activeSection, title, copy) { return '"; } @@ -1777,18 +1788,18 @@ var activeCount = countWorkspacesByState(workspaces, "active"); var postureTitle = unhealthyCount > 0 ? "Hosted posture needs review" : checkingCount > 0 ? "Hosted posture is still settling" : "Hosted posture is stable"; var postureCopy = unhealthyCount > 0 ? "One or more workspaces still need attention before the hosted fleet is trustworthy." : checkingCount > 0 ? "The hosted fleet is mostly healthy, but some workspaces are still waiting on a completed health check." : "The hosted fleet is healthy and ready for routine operator work."; - var operationsCopy = account.kind === "msp" ? "Manage the client fleet from this account surface. Workspace creation, billing, and team actions belong here." : "Use this account surface to open hosted workspaces, manage billing, and control access for this hosted account."; - var actions = ""; var addWorkspaceForm = ""; + var nextStepTitle = unhealthyCount > 0 ? "Start in Workspaces" : checkingCount > 0 ? "Review pending checks" : "Fleet is clear"; + var nextStepCopy = unhealthyCount > 0 ? "One or more workspaces need review before you treat the hosted fleet as trustworthy." : checkingCount > 0 ? "The fleet is mostly healthy, but there are still workspaces waiting on a completed health check." : "Hosted posture looks stable. Move into team or account services only if you need to change access or billing."; + var nextStepActions = '
"; if (account.can_manage) { - actions = '
'; if (account.kind === "msp") { addWorkspaceForm = '
'; } } return '
"; + ) + '
" + addWorkspaceForm + '

' + escapeHTML(nextStepTitle) + "

" + escapeHTML(nextStepCopy) + "

" + nextStepActions + "
" + renderAttentionPanel(workspaces) + "
"; } function renderAccountWorkspaceSection(account, accountAPIBasePath) { var workspaces = Array.isArray(account.workspaces) ? account.workspaces : []; @@ -1832,10 +1843,9 @@ var serviceHeading = hosted ? "Self-hosted licenses and billing" : "Account services"; var serviceNote = hosted ? "Hosted operations live above. Use these commercial tools for self-hosted licenses, billing, refunds, and privacy actions." : "Use these account tools for self-hosted licenses, billing, refunds, and privacy actions."; var hostedContent = accounts.map(function(account) { - var workspaceLabel = workspaceCountLabel((account.workspaces || []).length); - return '
"; + return '
"; }).join(""); - return '
' + renderShellNavigation(accounts, context.bootstrap.support_email || "", activeSection) + '
' + renderOverviewBand(accounts) + '
' + hostedContent + '

' + serviceHeading + '

' + serviceNote + '

Choose the next commercial action

Open a billing, license, refund, or privacy flow from the service navigator. The active request stays here so the account-services area behaves like one operating desk instead of a list of disconnected tools.

Start here
BillingOpen Stripe customer portal access after verification.
LicensesRecover the latest active self-hosted license and invoice link.
RefundsConfirm eligibility before revoking active commercial access.
PrivacyRequest export or deletion without leaving Pulse Account.
What to expect
Verification firstEach flow confirms the commercial email before opening sensitive account actions.
One task at a timeThe active commercial request stays in this panel until you finish or switch tools.
Support stays closeIf billing, licenses, refunds, or privacy behave unexpectedly, escalate from this surface.
Need help with billing, refund, privacy, or license actions? ' + escapeHTML(context.bootstrap.support_email || "") + '

Data and privacy

Request export or deletion of the commercial data tied to an email address. Payment data held directly by Stripe still requires support handling.

Payment-card data stays with Stripe. For Stripe deletion support, contact ' + escapeHTML(context.bootstrap.support_email || "") + '.

Use support if a billing, refund, privacy, or license flow does not behave as expected for this account.

' + escapeHTML(context.bootstrap.support_email || "") + '
' + renderSupportSection(context) + "
"; + return '
' + renderShellNavigation(accounts, context.bootstrap.support_email || "", activeSection) + '
' + renderOverviewBand(accounts) + (accounts.length === 1 ? renderPrimaryAccountBar(accounts[0]) : "") + '
' + hostedContent + '

' + serviceHeading + '

' + serviceNote + '

Choose the next commercial action

Open a billing, license, refund, or privacy flow from the service navigator. The active request stays here so the account-services area behaves like one operating desk instead of a list of disconnected tools.

Start here
BillingOpen Stripe customer portal access after verification.
LicensesRecover the latest active self-hosted license and invoice link.
RefundsConfirm eligibility before revoking active commercial access.
PrivacyRequest export or deletion without leaving Pulse Account.
What to expect
Verification firstEach flow confirms the commercial email before opening sensitive account actions.
One task at a timeThe active commercial request stays in this panel until you finish or switch tools.
Support stays closeIf billing, licenses, refunds, or privacy behave unexpectedly, escalate from this surface.
Need help with billing, refund, privacy, or license actions? ' + escapeHTML(context.bootstrap.support_email || "") + '

Data and privacy

Request export or deletion of the commercial data tied to an email address. Payment data held directly by Stripe still requires support handling.

Payment-card data stays with Stripe. For Stripe deletion support, contact ' + escapeHTML(context.bootstrap.support_email || "") + '.

Use support if a billing, refund, privacy, or license flow does not behave as expected for this account.

' + escapeHTML(context.bootstrap.support_email || "") + '
' + renderSupportSection(context) + "
"; } function renderSignedOutPortalHTML(context) { var statusHTML = ""; diff --git a/internal/cloudcp/portal/frontend/src/shell_view.test.ts b/internal/cloudcp/portal/frontend/src/shell_view.test.ts index 2ffa30d7c..68e68fe40 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts @@ -135,15 +135,17 @@ describe('shell view', function() { expect(html).toContain('Support'); expect(html).toContain('id="account-services-section"'); expect(html).toContain('Self-hosted licenses and billing'); + expect(html).toContain('portal-account-bar'); expect(html).toContain('id="accounts-root"'); expect(html).toContain('MSP account'); expect(html).toContain('Acme MSP'); expect(html).toContain('Operator workspace account'); expect(html).toContain('3 workspaces'); - expect(html).toContain('Account operations'); - expect(html).toContain('Manage the client fleet from this account surface.'); - expect(html).toContain('Operator overview'); - expect(html).toContain('Start from the next action, not the whole account'); + expect(html).toContain('Add workspace'); + expect(html).toContain('Manage billing'); + expect(html).toContain('Manage team'); + expect(html).toContain('Next move'); + expect(html).toContain('Start in Workspaces'); expect(html).toContain('Hosted posture needs review'); expect(html).toContain('Use this console to run client workspaces, account billing, and operator access from one place.'); expect(html).toContain('Open workspaces'); diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts index 81836a878..c1234441f 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -134,15 +134,6 @@ function renderServiceActionRow( ); } -function renderOverviewQuickAction(section: PortalShellSection, title: string, copy: string): string { - return ( - '' - ); -} - function workspaceStatusCopy(workspace: PortalWorkspaceSummary): string { var status = workspaceHealthState(workspace); if (status === 'healthy') return 'Live updates and health checks are currently good.'; @@ -242,6 +233,48 @@ function renderOverviewBand(accounts: PortalAccountSummary[]): string { ); } +function renderPrimaryAccountBar(account: PortalAccountSummary): string { + var workspaceLabel = workspaceCountLabel((account.workspaces || []).length); + var actionHTML = ''; + + if (account.kind === 'msp' && account.can_manage) { + actionHTML += + ''; + } + if (account.has_billing && account.can_manage) { + actionHTML += + ''; + } + if (account.can_manage) { + actionHTML += + ''; + } + + return ( + '
' + + '' + + '' + + '
' + ); +} + function shellSectionButton(section: PortalShellSection, activeSection: PortalShellSection, title: string, copy: string): string { return ( '' - : '') + - (account.has_billing - ? '' - : '') + - '' + - '' + - ''; + var nextStepTitle = unhealthyCount > 0 + ? 'Start in Workspaces' + : checkingCount > 0 + ? 'Review pending checks' + : 'Fleet is clear'; + var nextStepCopy = unhealthyCount > 0 + ? 'One or more workspaces need review before you treat the hosted fleet as trustworthy.' + : checkingCount > 0 + ? 'The fleet is mostly healthy, but there are still workspaces waiting on a completed health check.' + : 'Hosted posture looks stable. Move into team or account services only if you need to change access or billing.'; + var nextStepActions = + '
' + + '' + + '' + + '
'; + if (account.can_manage) { if (account.kind === 'msp') { addWorkspaceForm = '
' + '
' + - '' + - '

Start from the next action, not the whole account

' + - '

Use the overview for posture, then move into workspaces, team, or account services depending on what needs attention.

' + - '
' + - renderOverviewQuickAction('workspaces', 'Open workspaces', 'Review the hosted fleet and move into a workspace') + - renderOverviewQuickAction('team', 'Review team access', 'Check who can operate billing and hosted workspaces') + - renderOverviewQuickAction('services', 'Open account services', 'Handle billing, licenses, refunds, and privacy actions') + - '
' + + '' + + '

' + escapeHTML(nextStepTitle) + '

' + + '

' + escapeHTML(nextStepCopy) + '

' + + nextStepActions + '
' + renderAttentionPanel(workspaces) + '
' + @@ -716,21 +728,8 @@ export function renderAuthenticatedPortalHTML(context: ShellViewContext): string ? 'Hosted operations live above. Use these commercial tools for self-hosted licenses, billing, refunds, and privacy actions.' : 'Use these account tools for self-hosted licenses, billing, refunds, and privacy actions.'; var hostedContent = accounts.map(function(account) { - var workspaceLabel = workspaceCountLabel((account.workspaces || []).length); return ( '
' + - '' + '