From dac55b522187f31a617fdcf061978cbd5bae15dd Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 27 Mar 2026 18:25:50 +0000 Subject: [PATCH] style(portal): densify operator console surfaces --- .../cloudcp/portal/dist/build_manifest.json | 2 +- internal/cloudcp/portal/dist/portal_app.css | 367 ++++++++++------- internal/cloudcp/portal/dist/portal_app.js | 18 +- .../portal/frontend/src/shell_view.test.ts | 4 +- .../cloudcp/portal/frontend/src/shell_view.ts | 145 ++++--- .../cloudcp/portal/frontend/src/styles.css | 388 +++++++++++------- 6 files changed, 560 insertions(+), 364 deletions(-) diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json index ce91bcce57..00fc56a4bc 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "c95bc530e614dc996aaee2d380fabea9d0d0c2546c9062d0c75414c4229d8acf", + "source_hash": "db22f20e5c01de6c11aa19319736af2e3d32a613e07002fbcab79c4d07f08e5d", "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 40d393d3f6..15bbdec7f6 100644 --- a/internal/cloudcp/portal/dist/portal_app.css +++ b/internal/cloudcp/portal/dist/portal_app.css @@ -164,17 +164,17 @@ header .logout-btn:hover, } .portal-hero { display: grid; - grid-template-columns: minmax(0, 1.8fr) minmax(360px, 0.92fr); - gap: 16px; - padding: 18px 20px; + grid-template-columns: minmax(0, 1fr) auto; + gap: 18px; + padding: 12px 16px; border: 1px solid var(--line); border-radius: var(--radius-lg); - background: #fbfcfd; - box-shadow: var(--shadow-sm); + background: #fff; + box-shadow: 0 10px 24px rgba(10, 25, 41, 0.035); } .portal-hero-compact { align-items: center; - padding: 14px 18px; + padding: 12px 16px; } .portal-hero-copy { display: flex; @@ -233,20 +233,21 @@ header .logout-btn:hover, text-transform: uppercase; } .portal-hero-stats { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 10px; - align-self: stretch; + display: flex; + align-items: stretch; + gap: 8px; + align-self: center; } .portal-hero-stat { display: flex; flex-direction: column; - justify-content: space-between; - min-height: 72px; - padding: 11px 13px; + justify-content: center; + min-width: 132px; + min-height: 0; + padding: 10px 12px; border: 1px solid var(--line); - border-radius: var(--radius-md); - background: #fcfdff; + border-radius: 12px; + background: #fbfcfd; } .portal-hero-stat-label { font-size: 12px; @@ -256,9 +257,9 @@ header .logout-btn:hover, text-transform: uppercase; } .portal-hero-stat-value { - font-size: 18px; + font-size: 16px; font-weight: 800; - line-height: 1.2; + line-height: 1.25; letter-spacing: -0.04em; } .portal-command-strip, @@ -272,13 +273,13 @@ header .logout-btn:hover, } .portal-shell-layout { display: grid; - grid-template-columns: 240px minmax(0, 1fr); + grid-template-columns: 224px minmax(0, 1fr); gap: 0; align-items: start; border: 1px solid var(--line); border-radius: var(--radius-lg); - background: #f7fafc; - box-shadow: var(--shadow-sm); + background: #fff; + box-shadow: 0 16px 40px rgba(10, 25, 41, 0.05); overflow: hidden; } .portal-shell-nav { @@ -294,8 +295,8 @@ header .logout-btn:hover, .portal-shell-nav-header { display: flex; flex-direction: column; - gap: 6px; - padding: 20px 20px 18px; + gap: 7px; + padding: 18px 18px 16px; border-bottom: 1px solid var(--nav-line); } .portal-shell-nav-eyebrow { @@ -306,7 +307,7 @@ header .logout-btn:hover, color: #78b6c0; } .portal-shell-nav-title { - font-size: 20px; + font-size: 19px; line-height: 1.02; letter-spacing: -0.03em; font-weight: 800; @@ -320,14 +321,14 @@ header .logout-btn:hover, .portal-shell-nav-group { display: flex; flex-direction: column; - padding: 8px 0; + padding: 10px 0; } .portal-shell-nav-link { display: flex; flex-direction: column; gap: 6px; width: 100%; - padding: 15px 20px; + padding: 14px 18px; border: none; background: transparent; text-align: left; @@ -344,6 +345,30 @@ header .logout-btn:hover, background: rgba(120, 182, 192, 0.12); box-shadow: inset 3px 0 0 #78b6c0; } +.portal-shell-nav-footer { + margin-top: auto; + padding: 16px 18px 18px; + border-top: 1px solid var(--nav-line); + background: rgba(255, 255, 255, 0.03); +} +.portal-shell-nav-footer-label { + font-size: 11px; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #78b6c0; +} +.portal-shell-nav-footer-link { + display: inline-block; + margin-top: 8px; + font-size: 12px; + line-height: 1.5; + color: #d9f4fb; + text-decoration: none; +} +.portal-shell-nav-footer-link:hover { + text-decoration: underline; +} .portal-shell-nav-label { display: block; font-size: 14px; @@ -359,8 +384,8 @@ header .logout-btn:hover, .portal-shell-main { min-width: 0; min-height: 760px; - padding: 24px 26px 30px; - background: #f7fafc; + padding: 20px 22px 26px; + background: #fbfcfd; } .portal-shell-main > .portal-content-panel { display: none !important; @@ -467,6 +492,16 @@ header .logout-btn:hover, gap: 10px; flex-wrap: wrap; } +.workspace-actions { + justify-content: flex-end; +} +.workspace-actions form { + margin: 0; +} +.workspace-actions .btn-primary, +.workspace-actions .btn-secondary { + min-width: 118px; +} .account-surface-body { display: grid; grid-template-columns: 1fr; @@ -536,10 +571,21 @@ header .logout-btn:hover, } .account-command-deck { display: grid; - grid-template-columns: minmax(320px, 1.08fr) minmax(0, 1.3fr); + grid-template-columns: 1fr; gap: 14px; align-items: stretch; } +.account-overview-card { + display: grid; + grid-template-columns: minmax(0, 1.1fr) minmax(360px, 0.9fr); + gap: 14px; + align-items: stretch; +} +.account-overview-lead { + display: flex; + flex-direction: column; + min-width: 0; +} .account-metric-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); @@ -786,7 +832,7 @@ header .logout-btn:hover, } .workspace-list-head { display: grid; - grid-template-columns: minmax(0, 1.55fr) 120px 120px 180px; + grid-template-columns: minmax(0, 1.8fr) 132px 132px 176px; gap: 14px; padding: 2px 16px 6px; font-size: 11px; @@ -798,31 +844,34 @@ header .logout-btn:hover, .workspace-list { display: flex; flex-direction: column; - gap: 10px; + gap: 0; + border: 1px solid var(--line); + border-radius: 12px; + background: #fff; + overflow: hidden; } .workspace-row { display: grid; - grid-template-columns: minmax(0, 1fr) auto; + grid-template-columns: minmax(0, 1.8fr) 132px 132px 176px; gap: 14px; align-items: center; - padding: 16px; - background: var(--panel-muted); + padding: 14px 16px; + background: #fff; + border-top: 1px solid var(--line); } -.workspace-row-main { - display: grid; - grid-template-columns: minmax(0, 1.55fr) minmax(220px, 0.8fr); - gap: 14px; - align-items: center; +.workspace-row:first-child { + border-top: none; } -.workspace-row-title { +.workspace-row-primary { display: flex; flex-direction: column; - gap: 10px; + gap: 8px; + min-width: 0; } .workspace-row-heading { display: flex; flex-direction: column; - gap: 8px; + gap: 6px; } .workspace-name, .workspace-management-content h4, @@ -840,41 +889,28 @@ header .logout-btn:hover, .workspace-meta-item { display: inline-flex; align-items: center; - min-height: 28px; - padding: 0 10px; + min-height: 24px; + padding: 0 9px; border-radius: 999px; - background: var(--panel-muted); - font-size: 12px; + background: #f1f5f8; + font-size: 11px; color: var(--ink-soft); } -.workspace-summary, +.workspace-row-note, .workspace-management-summary { margin: 0; - font-size: 14px; - line-height: 1.65; + font-size: 12px; + line-height: 1.5; color: var(--ink-soft); } -.workspace-row-status-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; -} .workspace-row-status-cell { display: flex; - flex-direction: column; - gap: 8px; - min-height: 68px; - padding: 12px; - border: 1px solid var(--line); - border-radius: var(--radius-md); - background: #fff; + align-items: center; + min-width: 0; } -.workspace-row-status-label { - font-size: 11px; - font-weight: 800; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--ink-muted); +.workspace-row-status-cell-badge .badge { + width: 100%; + justify-content: center; } .workspace-state-label { font-size: 13px; @@ -990,9 +1026,15 @@ header .logout-btn:hover, .team-management-stats { margin: 16px 0; } +.team-stat-card { + padding: 12px 14px; + border: 1px solid var(--line); + border-radius: 10px; + background: #fbfcfd; +} .team-stat-value { - margin-top: 6px; - font-size: 22px; + margin-top: 8px; + font-size: 24px; } .team-management-grid { display: grid; @@ -1007,16 +1049,23 @@ header .logout-btn:hover, .team-panel-heading { margin-bottom: 12px; } +.team-panel-heading p { + max-width: 720px; +} .team-roster-list { display: flex; flex-direction: column; - gap: 10px; + gap: 0; + border: 1px solid var(--line); + border-radius: 10px; + background: #fff; + overflow: hidden; } .team-roster-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 16px; - padding: 0 8px 2px; + padding: 0 6px 8px; font-size: 11px; font-weight: 800; letter-spacing: 0.12em; @@ -1026,41 +1075,44 @@ header .logout-btn:hover, .team-member-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; - align-items: start; - gap: 16px; - padding: 14px 16px; - border: 1px solid var(--line); - border-radius: 10px; + align-items: center; + gap: 12px; + padding: 12px 14px; + border: none; + border-top: 1px solid var(--line); background: #fff; } +.team-member-row:first-of-type { + border-top: none; +} .team-member-identity { display: flex; flex-direction: column; - gap: 8px; + gap: 6px; min-width: 0; } .team-member-topline { display: flex; align-items: center; - justify-content: space-between; - gap: 12px; + justify-content: flex-start; + gap: 10px; min-width: 0; } .team-member-email { - font-size: 14px; + font-size: 13px; font-weight: 700; color: var(--ink); } .team-member-caption { - font-size: 13px; - line-height: 1.55; + font-size: 12px; + line-height: 1.45; color: var(--ink-soft); } .team-member-controls { display: flex; align-items: center; justify-content: end; - gap: 10px; + gap: 8px; flex-wrap: wrap; min-width: 0; } @@ -1078,19 +1130,19 @@ header .logout-btn:hover, .team-inline-role-badge { display: inline-flex; align-items: center; - min-height: 28px; - padding: 0 10px; + min-height: 24px; + padding: 0 8px; border-radius: 999px; background: var(--panel-muted); color: var(--ink-soft); - font-size: 11px; + font-size: 10px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap; } .team-role-select { - min-width: 140px; + min-width: 124px; } .team-list-message { display: flex; @@ -1123,8 +1175,8 @@ header .logout-btn:hover, .team-invite-guide { display: flex; flex-direction: column; - gap: 8px; - padding: 12px; + gap: 6px; + padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: #fff; @@ -1141,18 +1193,22 @@ header .logout-btn:hover, color: var(--ink-muted); } .team-invite-guide-row span { - font-size: 13px; - line-height: 1.5; + font-size: 12px; + line-height: 1.4; color: var(--ink-soft); } .service-action-list { display: grid; grid-template-columns: 1fr; - gap: 12px; + gap: 0; + border: 1px solid var(--line); + border-radius: 12px; + background: #fff; + overflow: hidden; } .service-shell { display: grid; - grid-template-columns: minmax(320px, 0.72fr) minmax(0, 1.28fr); + grid-template-columns: minmax(280px, 0.82fr) minmax(0, 1.18fr); gap: 16px; align-items: start; } @@ -1162,6 +1218,20 @@ header .logout-btn:hover, flex-direction: column; gap: 12px; } +.service-list-intro, +.service-inline-support { + padding: 14px 16px; + border: 1px solid var(--line); + border-radius: 12px; + background: #fff; +} +.service-list-intro p, +.service-inline-support p { + margin: 8px 0 0; + font-size: 13px; + line-height: 1.55; + color: var(--ink-soft); +} .service-detail-shell { min-height: 100%; border: 1px solid var(--line); @@ -1171,7 +1241,7 @@ header .logout-btn:hover, overflow: hidden; } .service-brief-card { - padding: 18px; + padding: 16px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--panel-muted); @@ -1181,7 +1251,7 @@ header .logout-btn:hover, } .service-brief-card h3 { margin: 8px 0 0; - font-size: 19px; + font-size: 18px; line-height: 1.08; letter-spacing: -0.03em; } @@ -1192,38 +1262,48 @@ header .logout-btn:hover, color: var(--ink-soft); } .service-action-row { - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; gap: 12px; - padding: 16px 18px; - border: 1px solid var(--line); - border-radius: var(--radius-md); + padding: 14px 16px; + border: none; + border-top: 1px solid var(--line); background: #fff; transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease; } +.service-action-row:first-child { + border-top: none; +} .service-action-row:hover, .service-action-row.active { border-color: rgba(15, 109, 115, 0.28); box-shadow: 0 10px 24px rgba(10, 25, 41, 0.08); } +.service-action-main { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; +} .service-action-tags { display: flex; align-items: center; - justify-content: space-between; - gap: 10px; + justify-content: flex-start; + gap: 8px; } .service-action-tag { display: inline-flex; align-items: center; - min-height: 26px; - padding: 0 10px; + min-height: 24px; + padding: 0 9px; border-radius: 999px; background: #fff; color: var(--ink-muted); - font-size: 11px; + font-size: 10px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; @@ -1231,61 +1311,46 @@ header .logout-btn:hover, .service-action-copy { display: flex; flex-direction: column; - gap: 6px; + gap: 4px; } .service-action-copy h3 { margin: 0; - font-size: 17px; + font-size: 16px; line-height: 1.16; letter-spacing: -0.03em; } .service-action-copy p { margin: 0; - font-size: 14px; - line-height: 1.6; + font-size: 13px; + line-height: 1.5; color: var(--ink-soft); max-width: 760px; } .service-action-highlights { display: flex; flex-wrap: wrap; - gap: 8px; + gap: 6px; } .service-action-highlight { display: inline-flex; align-items: center; - min-height: 28px; - padding: 0 10px; + min-height: 24px; + padding: 0 9px; border-radius: 999px; background: #fff; color: var(--ink-soft); - font-size: 12px; + font-size: 10px; font-weight: 600; } -.service-action-footer { - display: flex; - align-items: end; - justify-content: space-between; - gap: 16px; - margin-top: auto; - padding-top: 4px; - border-top: 1px solid var(--line); -} -.service-action-caption { - max-width: 320px; - font-size: 12px; - line-height: 1.5; - color: var(--ink-muted); -} .service-action-cta { display: flex; align-items: center; } .service-action-button { - min-width: 92px; + min-width: 86px; } .service-panel { - padding: 22px; + padding: 18px; border-radius: var(--radius-md); background: #fff; min-height: 100%; @@ -1302,14 +1367,22 @@ header .logout-btn:hover, .service-empty-points { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; + gap: 10px; margin-top: 20px; } +.service-empty-support { + margin-top: 16px; + padding-top: 14px; + border-top: 1px solid var(--line); + font-size: 13px; + line-height: 1.55; + color: var(--ink-soft); +} .service-empty-point { display: flex; flex-direction: column; gap: 6px; - padding: 14px; + padding: 12px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel-muted); @@ -1321,8 +1394,8 @@ header .logout-btn:hover, color: var(--ink-muted); } .service-empty-point span { - font-size: 13px; - line-height: 1.55; + font-size: 12px; + line-height: 1.45; color: var(--ink-soft); } .service-section-auth { @@ -1488,6 +1561,7 @@ header .logout-btn:hover, .account-management-grid, .team-management-grid, .account-overview-secondary, + .account-overview-card, .service-shell, .service-action-list, .service-empty-points, @@ -1520,14 +1594,14 @@ header .logout-btn:hover, padding: 20px; } .portal-hero-stats { - grid-template-columns: 1fr; + flex-wrap: wrap; } - .workspace-row, - .workspace-row-main { + .workspace-row { grid-template-columns: 1fr; } .workspace-list-toolbar { - grid-template-columns: 1fr; + flex-direction: column; + align-items: flex-start; } } @media (max-width: 720px) { @@ -1554,7 +1628,6 @@ header .logout-btn:hover, .workspace-management-header, .team-management-header, .service-header, - .service-action-footer, .service-action-tags { flex-direction: column; align-items: flex-start; @@ -1570,15 +1643,25 @@ header .logout-btn:hover, width: 100%; } .team-member-row, - .team-roster-head, - .team-member-controls { + .team-roster-head { grid-template-columns: 1fr; - align-items: stretch; } .team-member-controls { justify-content: start; + width: 100%; } - .workspace-row-status-grid { + .service-action-row, + .service-shell, + .workspace-operations-shell, + .account-overview-card, + .account-overview-secondary, + .overview-quick-actions, + .service-empty-points { grid-template-columns: 1fr; } + .service-action-cta, + .service-action-button { + width: 100%; + justify-content: start; + } } diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js index 5580b814e6..b4a0cc86bd 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -1696,7 +1696,7 @@ var highlightHTML = highlights.map(function(item) { return '' + escapeHTML(item) + ""; }).join(""); - return '
' + kicker + 'Self-hosted

' + title + "

" + description + '

' + highlightHTML + '
'; + return '
' + kicker + 'Self-hosted

' + title + "

" + description + '

' + highlightHTML + '
'; } function renderOverviewQuickAction(section, title, copy) { return '"; @@ -1707,6 +1707,12 @@ if (status === "unhealthy") return "This workspace needs attention before it is trustworthy."; return "This workspace is still waiting on a completed health check."; } + function workspaceRowNote(workspace) { + var status = workspaceHealthState(workspace); + if (status === "healthy") return "Ready for operator work"; + if (status === "unhealthy") return "Review this workspace before treating it as stable"; + return "Awaiting a completed health check"; + } function attentionWorkspaces(workspaces) { var results = []; for (var i = 0; i < workspaces.length; i += 1) { @@ -1739,7 +1745,7 @@ } function renderShellNavigation(accounts, supportEmail, activeSection) { var hosted = hasHostedAccounts(accounts); - return '"; + return '"; } function renderWorkspaceCard(account, workspace, accountAPIBasePath) { var status = workspaceHealthState(workspace); @@ -1764,7 +1770,7 @@ if (account.can_manage && (state === "active" || state === "suspended" || state === "failed")) { manageAction = ''; } - return '

' + escapeHTML(workspace.display_name) + '

' + metaParts.join("") + '

' + escapeHTML(workspaceStatusCopy(workspace)) + '

Health' + healthBadgeHTML(workspace) + '
Lifecycle' + escapeHTML(titleCase(state || "Unknown")) + '
' + openAction + manageAction + "
"; + return '

' + escapeHTML(workspace.display_name) + '

' + metaParts.join("") + '
' + escapeHTML(workspaceRowNote(workspace)) + '
' + healthBadgeHTML(workspace) + '
' + escapeHTML(titleCase(state || "Unknown")) + '
' + openAction + manageAction + "
"; } function renderAccountOverviewSection(account) { var workspaces = Array.isArray(account.workspaces) ? account.workspaces : []; @@ -1783,9 +1789,9 @@ addWorkspaceForm = '
'; } } - return '
" + actions + addWorkspaceForm + '
"; } function renderAccountWorkspaceSection(account, accountAPIBasePath) { var workspaces = Array.isArray(account.workspaces) ? account.workspaces : []; @@ -1822,7 +1828,7 @@ var hostedContent = accounts.map(function(account) { return '
"; }).join(""); - return '
' + renderOverviewBand(accounts) + '
' + renderShellNavigation(accounts, context.bootstrap.support_email || "", activeSection) + '
' + hostedContent + '

' + serviceHeading + '

' + serviceNote + '

Choose a commercial account 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 working surface instead of a list of disconnected tools.

BillingOpen Stripe customer portal access after verification.
LicensesRecover the latest active self-hosted license and invoice link.
PrivacyRequest export or deletion without leaving Pulse Account.

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

' + serviceHeading + '

' + serviceNote + '

Choose a commercial account 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 working surface instead of a list of disconnected tools.

BillingOpen Stripe customer portal access after verification.
LicensesRecover the latest active self-hosted license and invoice link.
PrivacyRequest export or deletion without leaving Pulse Account.
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 cb64b1fa47..5c2d895277 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts @@ -156,7 +156,7 @@ describe('shell view', function() { expect(html).toContain('Healthy'); expect(html).toContain('Needs attention'); expect(html).toContain('Checking'); - expect(html).toContain('Live updates and health checks are currently good.'); + expect(html).toContain('Ready for operator work'); expect(html).toContain('This workspace needs attention before it is trustworthy.'); expect(html).toContain('This workspace is still waiting on a completed health check.'); expect(html).toContain('/api/accounts/acct_1/tenants/ws_active/handoff'); @@ -188,7 +188,7 @@ describe('shell view', function() { expect(html).toContain('

Self-hosted Pulse Account

'); expect(html).toContain('No hosted workspace access is attached to this account yet.'); - expect(html).toContain('Use this area for self-hosted billing, license recovery, refunds, and privacy requests.'); + expect(html).toContain('Use these tools for self-hosted billing, license recovery, refunds, and privacy.'); expect(html).toContain('Account services'); expect(html).not.toContain('Self-hosted licenses and billing'); }); diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts index b73c0400fc..5775bd2db9 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -116,20 +116,19 @@ function renderServiceActionRow( }).join(''); return ( '
' + - '
' + - '' + kicker + '' + - 'Self-hosted' + - '
' + - '
' + - '

' + title + '

' + - '

' + description + '

' + - '
' + - '
' + highlightHTML + '
' + - '
' ); @@ -151,6 +150,13 @@ function workspaceStatusCopy(workspace: PortalWorkspaceSummary): string { return 'This workspace is still waiting on a completed health check.'; } +function workspaceRowNote(workspace: PortalWorkspaceSummary): string { + var status = workspaceHealthState(workspace); + if (status === 'healthy') return 'Ready for operator work'; + if (status === 'unhealthy') return 'Review this workspace before treating it as stable'; + return 'Awaiting a completed health check'; +} + function attentionWorkspaces(workspaces: PortalWorkspaceSummary[]): PortalWorkspaceSummary[] { var results: PortalWorkspaceSummary[] = []; for (var i = 0; i < workspaces.length; i += 1) { @@ -248,7 +254,7 @@ function renderShellNavigation(accounts: PortalAccountSummary[], supportEmail: s '
' + '
Pulse Account
' + '
' + (hosted ? 'Operator console' : 'Account console') + '
' + - '
' + escapeHTML(supportEmail || 'support@pulserelay.pro') + '
' + + '
' + (hosted ? 'Hosted operations and account services' : 'Commercial account services and support') + '
' + '
' + '
' + shellSectionButton('overview', activeSection, 'Overview', hosted ? 'Posture, accounts, and quick actions' : 'Account summary and access state') + @@ -257,6 +263,10 @@ function renderShellNavigation(accounts: PortalAccountSummary[], supportEmail: s shellSectionButton('services', activeSection, 'Account services', 'Licenses, billing, refunds, and privacy') + shellSectionButton('support', activeSection, 'Support', supportEmail || 'Support contact') + '
' + + '' + '' ); } @@ -299,24 +309,18 @@ function renderWorkspaceCard(account: PortalAccountSummary, workspace: PortalWor return ( '
' + - '
' + - '
' + - '
' + - '

' + escapeHTML(workspace.display_name) + '

' + - '
' + metaParts.join('') + '
' + - '
' + - '

' + escapeHTML(workspaceStatusCopy(workspace)) + '

' + - '
' + - '
' + - '
' + - 'Health' + - healthBadgeHTML(workspace) + - '
' + - '
' + - 'Lifecycle' + - '' + escapeHTML(titleCase(state || 'Unknown')) + '' + - '
' + + '
' + + '
' + + '

' + escapeHTML(workspace.display_name) + '

' + + '
' + metaParts.join('') + '
' + '
' + + '
' + escapeHTML(workspaceRowNote(workspace)) + '
' + + '
' + + '
' + + healthBadgeHTML(workspace) + + '
' + + '
' + + '' + escapeHTML(titleCase(state || 'Unknown')) + '' + '
' + '
' + openAction + @@ -406,30 +410,32 @@ function renderAccountOverviewSection(account: PortalAccountSummary): string { '
' + '
' + '' + diff --git a/internal/cloudcp/portal/frontend/src/styles.css b/internal/cloudcp/portal/frontend/src/styles.css index f24e885b42..2de322f17a 100644 --- a/internal/cloudcp/portal/frontend/src/styles.css +++ b/internal/cloudcp/portal/frontend/src/styles.css @@ -162,18 +162,18 @@ header .logout-btn:hover, .portal-hero { display: grid; - grid-template-columns: minmax(0, 1.8fr) minmax(360px, 0.92fr); - gap: 16px; - padding: 18px 20px; + grid-template-columns: minmax(0, 1fr) auto; + gap: 18px; + padding: 12px 16px; border: 1px solid var(--line); border-radius: var(--radius-lg); - background: #fbfcfd; - box-shadow: var(--shadow-sm); + background: #fff; + box-shadow: 0 10px 24px rgba(10, 25, 41, 0.035); } .portal-hero-compact { align-items: center; - padding: 14px 18px; + padding: 12px 16px; } .portal-hero-copy { @@ -241,21 +241,22 @@ header .logout-btn:hover, } .portal-hero-stats { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 10px; - align-self: stretch; + display: flex; + align-items: stretch; + gap: 8px; + align-self: center; } .portal-hero-stat { display: flex; flex-direction: column; - justify-content: space-between; - min-height: 72px; - padding: 11px 13px; + justify-content: center; + min-width: 132px; + min-height: 0; + padding: 10px 12px; border: 1px solid var(--line); - border-radius: var(--radius-md); - background: #fcfdff; + border-radius: 12px; + background: #fbfcfd; } .portal-hero-stat-label { @@ -267,9 +268,9 @@ header .logout-btn:hover, } .portal-hero-stat-value { - font-size: 18px; + font-size: 16px; font-weight: 800; - line-height: 1.2; + line-height: 1.25; letter-spacing: -0.04em; } @@ -285,13 +286,13 @@ header .logout-btn:hover, .portal-shell-layout { display: grid; - grid-template-columns: 240px minmax(0, 1fr); + grid-template-columns: 224px minmax(0, 1fr); gap: 0; align-items: start; border: 1px solid var(--line); border-radius: var(--radius-lg); - background: #f7fafc; - box-shadow: var(--shadow-sm); + background: #fff; + box-shadow: 0 16px 40px rgba(10, 25, 41, 0.05); overflow: hidden; } @@ -309,8 +310,8 @@ header .logout-btn:hover, .portal-shell-nav-header { display: flex; flex-direction: column; - gap: 6px; - padding: 20px 20px 18px; + gap: 7px; + padding: 18px 18px 16px; border-bottom: 1px solid var(--nav-line); } @@ -323,7 +324,7 @@ header .logout-btn:hover, } .portal-shell-nav-title { - font-size: 20px; + font-size: 19px; line-height: 1.02; letter-spacing: -0.03em; font-weight: 800; @@ -339,7 +340,7 @@ header .logout-btn:hover, .portal-shell-nav-group { display: flex; flex-direction: column; - padding: 8px 0; + padding: 10px 0; } .portal-shell-nav-link { @@ -347,7 +348,7 @@ header .logout-btn:hover, flex-direction: column; gap: 6px; width: 100%; - padding: 15px 20px; + padding: 14px 18px; border: none; background: transparent; text-align: left; @@ -368,6 +369,34 @@ header .logout-btn:hover, box-shadow: inset 3px 0 0 #78b6c0; } +.portal-shell-nav-footer { + margin-top: auto; + padding: 16px 18px 18px; + border-top: 1px solid var(--nav-line); + background: rgba(255, 255, 255, 0.03); +} + +.portal-shell-nav-footer-label { + font-size: 11px; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #78b6c0; +} + +.portal-shell-nav-footer-link { + display: inline-block; + margin-top: 8px; + font-size: 12px; + line-height: 1.5; + color: #d9f4fb; + text-decoration: none; +} + +.portal-shell-nav-footer-link:hover { + text-decoration: underline; +} + .portal-shell-nav-label { display: block; font-size: 14px; @@ -385,8 +414,8 @@ header .logout-btn:hover, .portal-shell-main { min-width: 0; min-height: 760px; - padding: 24px 26px 30px; - background: #f7fafc; + padding: 20px 22px 26px; + background: #fbfcfd; } .portal-shell-main > .portal-content-panel { @@ -510,6 +539,19 @@ header .logout-btn:hover, flex-wrap: wrap; } +.workspace-actions { + justify-content: flex-end; +} + +.workspace-actions form { + margin: 0; +} + +.workspace-actions .btn-primary, +.workspace-actions .btn-secondary { + min-width: 118px; +} + .account-surface-body { display: grid; grid-template-columns: 1fr; @@ -586,11 +628,24 @@ header .logout-btn:hover, .account-command-deck { display: grid; - grid-template-columns: minmax(320px, 1.08fr) minmax(0, 1.3fr); + grid-template-columns: 1fr; gap: 14px; align-items: stretch; } +.account-overview-card { + display: grid; + grid-template-columns: minmax(0, 1.1fr) minmax(360px, 0.9fr); + gap: 14px; + align-items: stretch; +} + +.account-overview-lead { + display: flex; + flex-direction: column; + min-width: 0; +} + .account-metric-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); @@ -872,7 +927,7 @@ header .logout-btn:hover, .workspace-list-head { display: grid; - grid-template-columns: minmax(0, 1.55fr) 120px 120px 180px; + grid-template-columns: minmax(0, 1.8fr) 132px 132px 176px; gap: 14px; padding: 2px 16px 6px; font-size: 11px; @@ -885,35 +940,38 @@ header .logout-btn:hover, .workspace-list { display: flex; flex-direction: column; - gap: 10px; + gap: 0; + border: 1px solid var(--line); + border-radius: 12px; + background: #fff; + overflow: hidden; } .workspace-row { display: grid; - grid-template-columns: minmax(0, 1fr) auto; + grid-template-columns: minmax(0, 1.8fr) 132px 132px 176px; gap: 14px; align-items: center; - padding: 16px; - background: var(--panel-muted); + padding: 14px 16px; + background: #fff; + border-top: 1px solid var(--line); } -.workspace-row-main { - display: grid; - grid-template-columns: minmax(0, 1.55fr) minmax(220px, 0.8fr); - gap: 14px; - align-items: center; +.workspace-row:first-child { + border-top: none; } -.workspace-row-title { +.workspace-row-primary { display: flex; flex-direction: column; - gap: 10px; + gap: 8px; + min-width: 0; } .workspace-row-heading { display: flex; flex-direction: column; - gap: 8px; + gap: 6px; } .workspace-name, @@ -934,45 +992,31 @@ header .logout-btn:hover, .workspace-meta-item { display: inline-flex; align-items: center; - min-height: 28px; - padding: 0 10px; + min-height: 24px; + padding: 0 9px; border-radius: 999px; - background: var(--panel-muted); - font-size: 12px; + background: #f1f5f8; + font-size: 11px; color: var(--ink-soft); } -.workspace-summary, +.workspace-row-note, .workspace-management-summary { margin: 0; - font-size: 14px; - line-height: 1.65; + font-size: 12px; + line-height: 1.5; color: var(--ink-soft); } -.workspace-row-status-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; -} - .workspace-row-status-cell { display: flex; - flex-direction: column; - gap: 8px; - min-height: 68px; - padding: 12px; - border: 1px solid var(--line); - border-radius: var(--radius-md); - background: #fff; + align-items: center; + min-width: 0; } -.workspace-row-status-label { - font-size: 11px; - font-weight: 800; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--ink-muted); +.workspace-row-status-cell-badge .badge { + width: 100%; + justify-content: center; } .workspace-state-label { @@ -1108,9 +1152,16 @@ header .logout-btn:hover, margin: 16px 0; } +.team-stat-card { + padding: 12px 14px; + border: 1px solid var(--line); + border-radius: 10px; + background: #fbfcfd; +} + .team-stat-value { - margin-top: 6px; - font-size: 22px; + margin-top: 8px; + font-size: 24px; } .team-management-grid { @@ -1129,17 +1180,25 @@ header .logout-btn:hover, margin-bottom: 12px; } +.team-panel-heading p { + max-width: 720px; +} + .team-roster-list { display: flex; flex-direction: column; - gap: 10px; + gap: 0; + border: 1px solid var(--line); + border-radius: 10px; + background: #fff; + overflow: hidden; } .team-roster-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 16px; - padding: 0 8px 2px; + padding: 0 6px 8px; font-size: 11px; font-weight: 800; letter-spacing: 0.12em; @@ -1150,38 +1209,42 @@ header .logout-btn:hover, .team-member-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; - align-items: start; - gap: 16px; - padding: 14px 16px; - border: 1px solid var(--line); - border-radius: 10px; + align-items: center; + gap: 12px; + padding: 12px 14px; + border: none; + border-top: 1px solid var(--line); background: #fff; } +.team-member-row:first-of-type { + border-top: none; +} + .team-member-identity { display: flex; flex-direction: column; - gap: 8px; + gap: 6px; min-width: 0; } .team-member-topline { display: flex; align-items: center; - justify-content: space-between; - gap: 12px; + justify-content: flex-start; + gap: 10px; min-width: 0; } .team-member-email { - font-size: 14px; + font-size: 13px; font-weight: 700; color: var(--ink); } .team-member-caption { - font-size: 13px; - line-height: 1.55; + font-size: 12px; + line-height: 1.45; color: var(--ink-soft); } @@ -1189,7 +1252,7 @@ header .logout-btn:hover, display: flex; align-items: center; justify-content: end; - gap: 10px; + gap: 8px; flex-wrap: wrap; min-width: 0; } @@ -1209,12 +1272,12 @@ header .logout-btn:hover, .team-inline-role-badge { display: inline-flex; align-items: center; - min-height: 28px; - padding: 0 10px; + min-height: 24px; + padding: 0 8px; border-radius: 999px; background: var(--panel-muted); color: var(--ink-soft); - font-size: 11px; + font-size: 10px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; @@ -1222,7 +1285,7 @@ header .logout-btn:hover, } .team-role-select { - min-width: 140px; + min-width: 124px; } .team-list-message { @@ -1261,8 +1324,8 @@ header .logout-btn:hover, .team-invite-guide { display: flex; flex-direction: column; - gap: 8px; - padding: 12px; + gap: 6px; + padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: #fff; @@ -1282,20 +1345,24 @@ header .logout-btn:hover, } .team-invite-guide-row span { - font-size: 13px; - line-height: 1.5; + font-size: 12px; + line-height: 1.4; color: var(--ink-soft); } .service-action-list { display: grid; grid-template-columns: 1fr; - gap: 12px; + gap: 0; + border: 1px solid var(--line); + border-radius: 12px; + background: #fff; + overflow: hidden; } .service-shell { display: grid; - grid-template-columns: minmax(320px, 0.72fr) minmax(0, 1.28fr); + grid-template-columns: minmax(280px, 0.82fr) minmax(0, 1.18fr); gap: 16px; align-items: start; } @@ -1307,6 +1374,22 @@ header .logout-btn:hover, gap: 12px; } +.service-list-intro, +.service-inline-support { + padding: 14px 16px; + border: 1px solid var(--line); + border-radius: 12px; + background: #fff; +} + +.service-list-intro p, +.service-inline-support p { + margin: 8px 0 0; + font-size: 13px; + line-height: 1.55; + color: var(--ink-soft); +} + .service-detail-shell { min-height: 100%; border: 1px solid var(--line); @@ -1317,7 +1400,7 @@ header .logout-btn:hover, } .service-brief-card { - padding: 18px; + padding: 16px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--panel-muted); @@ -1329,7 +1412,7 @@ header .logout-btn:hover, .service-brief-card h3 { margin: 8px 0 0; - font-size: 19px; + font-size: 18px; line-height: 1.08; letter-spacing: -0.03em; } @@ -1342,38 +1425,50 @@ header .logout-btn:hover, } .service-action-row { - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; gap: 12px; - padding: 16px 18px; - border: 1px solid var(--line); - border-radius: var(--radius-md); + padding: 14px 16px; + border: none; + border-top: 1px solid var(--line); background: #fff; transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease; } +.service-action-row:first-child { + border-top: none; +} + .service-action-row:hover, .service-action-row.active { border-color: rgba(15, 109, 115, 0.28); box-shadow: 0 10px 24px rgba(10, 25, 41, 0.08); } +.service-action-main { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; +} + .service-action-tags { display: flex; align-items: center; - justify-content: space-between; - gap: 10px; + justify-content: flex-start; + gap: 8px; } .service-action-tag { display: inline-flex; align-items: center; - min-height: 26px; - padding: 0 10px; + min-height: 24px; + padding: 0 9px; border-radius: 999px; background: #fff; color: var(--ink-muted); - font-size: 11px; + font-size: 10px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; @@ -1382,20 +1477,20 @@ header .logout-btn:hover, .service-action-copy { display: flex; flex-direction: column; - gap: 6px; + gap: 4px; } .service-action-copy h3 { margin: 0; - font-size: 17px; + font-size: 16px; line-height: 1.16; letter-spacing: -0.03em; } .service-action-copy p { margin: 0; - font-size: 14px; - line-height: 1.6; + font-size: 13px; + line-height: 1.5; color: var(--ink-soft); max-width: 760px; } @@ -1403,49 +1498,32 @@ header .logout-btn:hover, .service-action-highlights { display: flex; flex-wrap: wrap; - gap: 8px; + gap: 6px; } .service-action-highlight { display: inline-flex; align-items: center; - min-height: 28px; - padding: 0 10px; + min-height: 24px; + padding: 0 9px; border-radius: 999px; background: #fff; color: var(--ink-soft); - font-size: 12px; + font-size: 10px; font-weight: 600; } -.service-action-footer { - display: flex; - align-items: end; - justify-content: space-between; - gap: 16px; - margin-top: auto; - padding-top: 4px; - border-top: 1px solid var(--line); -} - -.service-action-caption { - max-width: 320px; - font-size: 12px; - line-height: 1.5; - color: var(--ink-muted); -} - .service-action-cta { display: flex; align-items: center; } .service-action-button { - min-width: 92px; + min-width: 86px; } .service-panel { - padding: 22px; + padding: 18px; border-radius: var(--radius-md); background: #fff; min-height: 100%; @@ -1466,15 +1544,24 @@ header .logout-btn:hover, .service-empty-points { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; + gap: 10px; margin-top: 20px; } +.service-empty-support { + margin-top: 16px; + padding-top: 14px; + border-top: 1px solid var(--line); + font-size: 13px; + line-height: 1.55; + color: var(--ink-soft); +} + .service-empty-point { display: flex; flex-direction: column; gap: 6px; - padding: 14px; + padding: 12px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel-muted); @@ -1488,8 +1575,8 @@ header .logout-btn:hover, } .service-empty-point span { - font-size: 13px; - line-height: 1.55; + font-size: 12px; + line-height: 1.45; color: var(--ink-soft); } @@ -1678,6 +1765,7 @@ header .logout-btn:hover, .account-management-grid, .team-management-grid, .account-overview-secondary, + .account-overview-card, .service-shell, .service-action-list, .service-empty-points, @@ -1718,16 +1806,16 @@ header .logout-btn:hover, } .portal-hero-stats { - grid-template-columns: 1fr; + flex-wrap: wrap; } - .workspace-row, - .workspace-row-main { + .workspace-row { grid-template-columns: 1fr; } .workspace-list-toolbar { - grid-template-columns: 1fr; + flex-direction: column; + align-items: flex-start; } } @@ -1759,7 +1847,6 @@ header .logout-btn:hover, .workspace-management-header, .team-management-header, .service-header, - .service-action-footer, .service-action-tags { flex-direction: column; align-items: flex-start; @@ -1778,17 +1865,28 @@ header .logout-btn:hover, } .team-member-row, - .team-roster-head, - .team-member-controls { + .team-roster-head { grid-template-columns: 1fr; - align-items: stretch; } .team-member-controls { justify-content: start; + width: 100%; } - .workspace-row-status-grid { + .service-action-row, + .service-shell, + .workspace-operations-shell, + .account-overview-card, + .account-overview-secondary, + .overview-quick-actions, + .service-empty-points { grid-template-columns: 1fr; } + + .service-action-cta, + .service-action-button { + width: 100%; + justify-content: start; + } }