From e2e835b9b391dc4a3501ca34653d4454f934abd8 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 27 Mar 2026 16:16:24 +0000 Subject: [PATCH] feat(portal): simplify hosted operations shell --- .../cloudcp/portal/dist/build_manifest.json | 2 +- internal/cloudcp/portal/dist/portal_app.css | 178 +++++++--------- internal/cloudcp/portal/dist/portal_app.js | 8 +- .../portal/frontend/src/shell_view.test.ts | 2 +- .../cloudcp/portal/frontend/src/shell_view.ts | 52 ++--- .../cloudcp/portal/frontend/src/styles.css | 194 ++++++++---------- 6 files changed, 190 insertions(+), 246 deletions(-) diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json index 9ef5825dce..0538365204 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "c60aecd13aa823a23de54658d2f067e93ce5d5d05b80cee353495dccbb3c07d2", + "source_hash": "2cbfa509f8e18522b02bce02cea0af8db454746381d6e7cae0e1b5a0eefcad5d", "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 bf5b04f20c..ee3a0421b9 100644 --- a/internal/cloudcp/portal/dist/portal_app.css +++ b/internal/cloudcp/portal/dist/portal_app.css @@ -232,18 +232,7 @@ header .logout-btn:hover, line-height: 1.05; letter-spacing: -0.04em; } -.portal-shell-bar { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 16px; - align-items: stretch; -} -.portal-section-nav { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 10px; -} -.portal-section-link, +.portal-command-strip, .intro-card, .service-panel, .account-surface, @@ -252,28 +241,31 @@ header .logout-btn:hover, background: rgba(255, 255, 255, 0.98); box-shadow: var(--shadow-sm); } -.portal-section-link { +.portal-command-strip { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0; + border-radius: var(--radius-md); +} +.portal-command-link { display: block; padding: 16px 18px; - border-radius: var(--radius-md); text-decoration: none; - transition: - transform 140ms ease, - border-color 140ms ease, - box-shadow 140ms ease; + transition: background 140ms ease; } -.portal-section-link:hover { - transform: translateY(-1px); - border-color: var(--accent); - box-shadow: var(--shadow-md); +.portal-command-link + .portal-command-link { + border-left: 1px solid var(--line); } -.portal-section-link-label { +.portal-command-link:hover { + background: #f7fafc; +} +.portal-command-label { display: block; font-size: 14px; font-weight: 700; margin-bottom: 5px; } -.portal-section-link-copy { +.portal-command-copy { display: block; font-size: 13px; line-height: 1.5; @@ -285,35 +277,8 @@ header .logout-btn:hover, color: var(--accent-strong); font-weight: 600; } -.portal-support-strip { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - min-width: 340px; - padding: 16px 18px; - border: 1px solid var(--line); - border-radius: var(--radius-md); - background: rgba(255, 255, 255, 0.98); - box-shadow: var(--shadow-sm); -} -.portal-support-copy { - display: flex; - flex-direction: column; - gap: 4px; -} -.portal-support-label { - font-size: 11px; - font-weight: 800; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--ink-muted); -} -.portal-support-text { - max-width: 280px; - font-size: 13px; - line-height: 1.5; - color: var(--ink-soft); +.portal-command-link-support { + text-align: right; } .portal-stage { display: flex; @@ -391,15 +356,9 @@ header .logout-btn:hover, gap: 18px; margin-top: 22px; } -.account-overview-rail { - display: grid; - grid-template-columns: minmax(280px, 0.95fr) minmax(0, 1.15fr); - gap: 14px; - align-items: start; -} +.account-command-deck, .account-overview-card, .account-stat-card, -.account-operations-panel, .workspace-row, .workspace-management-panel, .team-management-panel, @@ -424,7 +383,6 @@ header .logout-btn:hover, } .account-overview-card h3, .account-stage-header h3, -.account-panel-copy h3, .workspace-management-header h3, .team-management-header h3, .service-card h3, @@ -437,7 +395,6 @@ header .logout-btn:hover, } .account-overview-card p, .account-stage-header p, -.account-panel-copy p, .workspace-management-header p, .team-management-header p, .team-panel-heading p, @@ -449,21 +406,21 @@ header .logout-btn:hover, line-height: 1.65; color: var(--ink-soft); } -.account-status-grid, -.team-management-stats { +.account-command-deck { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: minmax(280px, 1.05fr) minmax(0, 1.35fr); + gap: 14px; + align-items: stretch; +} +.account-metric-strip { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; } -.account-overview-card { - grid-column: 1; -} -.account-status-grid { - grid-column: 2; - grid-row: 1 / span 2; -} -.account-stat-card { - min-height: 108px; +.team-management-stats { + display: flex; + gap: 10px; + flex-wrap: wrap; } .account-stat-label, .team-stat-label { @@ -492,12 +449,23 @@ header .logout-btn:hover, .team-stat-error { color: var(--danger); } -.account-operations-panel { +.account-action-strip { display: flex; - flex-direction: column; - gap: 16px; - background: var(--panel); - grid-column: 1; + align-items: start; + justify-content: space-between; + gap: 18px; + grid-column: 1 / -1; + padding: 16px 18px 0; + border-top: 1px solid var(--line); +} +.account-action-copy { + max-width: 720px; +} +.account-action-copy p { + margin: 8px 0 0; + font-size: 14px; + line-height: 1.6; + color: var(--ink-soft); } .add-workspace-form { grid-column: 1 / -1; @@ -508,7 +476,7 @@ header .logout-btn:hover, gap: 16px; } .account-stage-header { - padding: 6px 4px 0; + padding: 2px 4px 0; } .workspace-list { display: flex; @@ -520,7 +488,7 @@ header .logout-btn:hover, grid-template-columns: minmax(0, 1fr) auto; gap: 16px; align-items: center; - padding: 18px; + padding: 20px; } .workspace-row-main { display: grid; @@ -577,7 +545,7 @@ header .logout-btn:hover, display: flex; flex-direction: column; gap: 8px; - min-height: 84px; + min-height: 74px; padding: 14px; border: 1px solid var(--line); border-radius: var(--radius-md); @@ -685,13 +653,18 @@ header .logout-btn:hover, outline: 2px solid rgba(15, 109, 115, 0.18); border-color: var(--accent); } +.team-stat-card { + min-width: 140px; + padding: 12px 14px; + border-radius: var(--radius-sm); + background: var(--panel-muted); +} .team-management-stats { margin: 16px 0; } -.team-stat-card { - padding: 16px; - border-radius: var(--radius-sm); - background: var(--panel-muted); +.team-stat-value { + margin-top: 6px; + font-size: 22px; } .team-management-grid { display: grid; @@ -713,11 +686,11 @@ header .logout-btn:hover, letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-muted); - border-bottom: 1px solid rgba(203, 183, 153, 0.7); + border-bottom: 1px solid var(--line); } .team-table td { padding: 12px 8px; - border-bottom: 1px solid rgba(223, 210, 189, 0.55); + border-bottom: 1px solid var(--line); vertical-align: middle; } .team-message-cell { @@ -925,18 +898,12 @@ header .logout-btn:hover, .team-management-grid { grid-template-columns: 1fr; } - .portal-shell-bar { + .portal-command-strip, + .account-command-deck { grid-template-columns: 1fr; } - .portal-support-strip { - min-width: 0; - } - .account-overview-rail { - grid-template-columns: 1fr; - } - .account-status-grid { - grid-column: auto; - grid-row: auto; + .account-metric-strip { + grid-template-columns: repeat(2, minmax(0, 1fr)); } .workspace-management-content { grid-template-columns: 1fr; @@ -968,9 +935,7 @@ header .logout-btn:hover, .main { padding: 18px 16px 40px; } - .portal-section-nav, - .account-status-grid, - .team-management-stats { + .portal-command-strip { grid-template-columns: 1fr; } .account-surface, @@ -986,6 +951,17 @@ header .logout-btn:hover, flex-direction: column; align-items: flex-start; } + .portal-command-link + .portal-command-link { + border-left: none; + border-top: 1px solid var(--line); + } + .portal-command-link-support { + text-align: left; + } + .account-action-strip { + flex-direction: column; + align-items: stretch; + } .workspace-actions, .workspace-actions form, .workspace-actions .btn-primary, diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js index 8c9cfb7c9a..0b971b8d04 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -1600,7 +1600,7 @@ } function renderShellNavigation(accounts, supportEmail) { var hosted = hasHostedAccounts(accounts); - return '"; + return '"; } function renderWorkspaceCard(account, workspace, accountAPIBasePath) { var status = workspaceHealthState(workspace); @@ -1640,7 +1640,7 @@ var addWorkspaceForm = ""; var workspaceManagement = ""; if (account.can_manage) { - actions = '
'; + actions = '
'; teamSection = '

Control who can operate this account

Owners manage billing and access. Admins and techs keep the hosted fleet running day to day.

People on this account

Keep the roster small and role assignment explicit. The people listed here are the ones who can operate the hosted fleet.

EmailRole
Loading\u2026

Invite someone new

Add another operator with the minimum role they need for this account.

'; workspaceManagement = '

Review one workspace at a time

Select a workspace from the fleet to review its lifecycle state and run explicit management actions.

Choose a workspace to manage from the fleet above.
'; if (account.kind === "msp") { @@ -1650,7 +1650,7 @@ var workspaceHTML = workspaces.length ? '
' + workspaces.map(function(workspace) { return renderWorkspaceCard(account, workspace, accountAPIBasePath); }).join("") + "
" : '

No hosted workspaces yet. Create one to get started.

'; - return '
"; + return '
"; } function renderHeaderHTML(context) { if (context.bootstrap.authenticated) { @@ -1672,7 +1672,7 @@ var hosted = hasHostedAccounts(accounts); 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."; - return '
' + renderOverviewBand(accounts) + '
' + renderShellNavigation(accounts, context.bootstrap.support_email || "") + '
SupportHosted operations and commercial account tools now share one account surface.
' + escapeHTML(context.bootstrap.support_email || "") + '

" + (hosted ? "Run hosted accounts from one place" : "No hosted workspaces are attached yet") + "

" + (hosted ? "Use this area for workspace access, fleet operations, hosted billing, and team management." : "This account does not currently have hosted workspace access. If that is unexpected, contact support while using the commercial tools below.") + '

' + renderAccountsHTML(context) + '

' + serviceHeading + '

' + serviceNote + '

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 || "") + ".
"; + return '
' + renderOverviewBand(accounts) + renderShellNavigation(accounts, context.bootstrap.support_email || "") + '

" + (hosted ? "Run hosted accounts from one place" : "No hosted workspaces are attached yet") + "

" + (hosted ? "Use this area for workspace access, fleet operations, hosted billing, and team management." : "This account does not currently have hosted workspace access. If that is unexpected, contact support while using the commercial tools below.") + '

' + renderAccountsHTML(context) + '

' + serviceHeading + '

' + serviceNote + '

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 || "") + ".
"; } 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 77d949f22a..b622eaa471 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts @@ -127,7 +127,7 @@ describe('shell view', function() { expect(html).toContain('

Pulse Account

'); expect(html).toContain('Hosted access is active on this account.'); - expect(html).toContain('portal-section-nav'); + expect(html).toContain('portal-command-strip'); expect(html).toContain('Hosted operations'); expect(html).toContain('Account services'); expect(html).toContain('Support'); diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts index 540c8d55e9..3eb7f5554e 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -143,18 +143,18 @@ function renderOverviewBand(accounts: PortalAccountSummary[]): string { function renderShellNavigation(accounts: PortalAccountSummary[], supportEmail: string): string { var hosted = hasHostedAccounts(accounts); return ( - '