diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json index 9ed4d67ac1..3609eaccc2 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "087912d40011626d575748045a827f33b34abfd10a3a35ddf8f79ba776b2f549", + "source_hash": "02f5d00566a4d55ae7999d1121d77557d884ffcedd01d2a278a62f90d61be7a4", "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 6ae2e3efe3..731c612ab6 100644 --- a/internal/cloudcp/portal/dist/portal_app.css +++ b/internal/cloudcp/portal/dist/portal_app.css @@ -1,15 +1,15 @@ /* src/styles.css */ :root { color-scheme: light; - --bg: #f3f4f6; + --bg: #edf1f5; --panel: #ffffff; - --panel-strong: #fbfbfc; - --panel-muted: #f6f7f9; - --ink: #17202a; - --ink-soft: #5a6675; - --ink-muted: #7d8895; - --line: #dde3ea; - --line-strong: #c5d0da; + --panel-strong: #f8fafc; + --panel-muted: #f3f6f9; + --ink: #17212b; + --ink-soft: #4d5b6a; + --ink-muted: #6d7784; + --line: #d6dfe8; + --line-strong: #bcc8d3; --accent: #0b6a72; --accent-strong: #084d53; --accent-soft: #dceef0; @@ -19,11 +19,15 @@ --danger-soft: #fdeaea; --success: #19603c; --success-soft: #e3f3e8; + --nav-bg: #111b24; + --nav-line: #203240; + --nav-copy: #8fa0b0; + --nav-ink: #edf4f8; --shadow-sm: 0 1px 2px rgba(23, 32, 42, 0.06); --shadow-md: 0 10px 24px rgba(23, 32, 42, 0.08); --radius-sm: 6px; - --radius-md: 12px; - --radius-lg: 18px; + --radius-md: 10px; + --radius-lg: 16px; } * { box-sizing: border-box; @@ -35,14 +39,16 @@ body { margin: 0; min-height: 100vh; font-family: - Inter, + "SF Pro Text", + "SF Pro Display", "Segoe UI Variable Text", "Segoe UI", -apple-system, BlinkMacSystemFont, "Helvetica Neue", + Arial, sans-serif; - background: #f3f4f6; + background: var(--bg); color: var(--ink); } a { @@ -56,9 +62,9 @@ header { align-items: center; justify-content: space-between; gap: 16px; - padding: 18px 28px; - border-bottom: 1px solid rgba(197, 208, 218, 0.8); - background: rgba(248, 249, 251, 0.92); + padding: 14px 24px; + border-bottom: 1px solid rgba(197, 208, 218, 0.75); + background: rgba(247, 249, 252, 0.94); backdrop-filter: blur(14px); } header .brand { @@ -147,30 +153,30 @@ header .logout-btn:hover, font-size: 13px; } .main { - max-width: 1360px; + max-width: 1480px; margin: 0 auto; - padding: 28px 24px 56px; + padding: 22px 24px 56px; } .portal-shell { display: flex; flex-direction: column; - gap: 24px; + gap: 18px; } .portal-hero { display: grid; - grid-template-columns: minmax(0, 1.7fr) minmax(300px, 0.95fr); - gap: 20px; - padding: 30px; + grid-template-columns: minmax(0, 1.8fr) minmax(360px, 0.92fr); + gap: 16px; + padding: 22px 24px; border: 1px solid var(--line); border-radius: var(--radius-lg); - background: rgba(255, 255, 255, 0.98); - box-shadow: var(--shadow-md); + background: var(--panel); + box-shadow: var(--shadow-sm); } .portal-hero-copy { display: flex; flex-direction: column; - gap: 10px; - max-width: 820px; + gap: 8px; + max-width: 900px; } .portal-hero-kicker, .account-panel-kicker, @@ -192,32 +198,32 @@ header .logout-btn:hover, .portal-hero h1, .intro-card h1 { margin: 0; - font-size: clamp(1.85rem, 3.1vw, 2.9rem); + font-size: clamp(1.75rem, 2.6vw, 2.6rem); line-height: 0.98; letter-spacing: -0.045em; } .portal-hero p, .intro-card p { margin: 0; - font-size: 15px; - line-height: 1.6; + font-size: 14px; + line-height: 1.55; color: var(--ink-soft); } .portal-hero-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; + gap: 10px; align-self: stretch; } .portal-hero-stat { display: flex; flex-direction: column; justify-content: space-between; - min-height: 120px; - padding: 16px; + min-height: 92px; + padding: 14px; border: 1px solid var(--line); border-radius: var(--radius-md); - background: rgba(255, 255, 255, 0.92); + background: var(--panel-strong); } .portal-hero-stat-label { font-size: 12px; @@ -227,7 +233,7 @@ header .logout-btn:hover, text-transform: uppercase; } .portal-hero-stat-value { - font-size: 24px; + font-size: 22px; font-weight: 800; line-height: 1.05; letter-spacing: -0.04em; @@ -243,19 +249,45 @@ header .logout-btn:hover, } .portal-shell-layout { display: grid; - grid-template-columns: 248px minmax(0, 1fr); - gap: 24px; + grid-template-columns: 240px minmax(0, 1fr); + gap: 18px; align-items: start; } .portal-shell-nav { position: sticky; top: 88px; - border: 1px solid var(--line); + border: 1px solid var(--nav-line); border-radius: var(--radius-md); - background: rgba(255, 255, 255, 0.98); + background: var(--nav-bg); box-shadow: var(--shadow-sm); overflow: hidden; } +.portal-shell-nav-header { + display: flex; + flex-direction: column; + gap: 6px; + padding: 18px 18px 16px; + border-bottom: 1px solid var(--nav-line); +} +.portal-shell-nav-eyebrow { + font-size: 11px; + font-weight: 800; + letter-spacing: 0.14em; + text-transform: uppercase; + color: #78b6c0; +} +.portal-shell-nav-title { + font-size: 20px; + line-height: 1.02; + letter-spacing: -0.03em; + font-weight: 800; + color: var(--nav-ink); +} +.portal-shell-nav-support { + font-size: 12px; + line-height: 1.45; + color: var(--nav-copy); +} .portal-shell-nav-group { display: flex; flex-direction: column; @@ -265,9 +297,9 @@ header .logout-btn:hover, flex-direction: column; gap: 6px; width: 100%; - padding: 15px 16px; + padding: 14px 18px; border: none; - border-bottom: 1px solid var(--line); + border-bottom: 1px solid var(--nav-line); background: transparent; text-align: left; cursor: pointer; @@ -277,21 +309,23 @@ header .logout-btn:hover, border-bottom: none; } .portal-shell-nav-link:hover { - background: #f7fafc; + background: rgba(255, 255, 255, 0.04); } .portal-shell-nav-link.active { - background: #eef6f7; + background: rgba(120, 182, 192, 0.12); + box-shadow: inset 3px 0 0 #78b6c0; } .portal-shell-nav-label { display: block; font-size: 14px; font-weight: 700; + color: var(--nav-ink); } .portal-shell-nav-copy { display: block; font-size: 12px; line-height: 1.45; - color: var(--ink-soft); + color: var(--nav-copy); } .portal-shell-main { min-width: 0; @@ -339,7 +373,7 @@ header .logout-btn:hover, .portal-top-section-header h2, .service-header h2 { margin: 6px 0 0; - font-size: clamp(1.7rem, 2.2vw, 2.35rem); + font-size: clamp(1.55rem, 2vw, 2.15rem); line-height: 1.02; letter-spacing: -0.05em; } @@ -360,7 +394,7 @@ header .logout-btn:hover, } .account-surface { border-radius: var(--radius-lg); - padding: 26px; + padding: 22px; } .account-surface + .account-surface { margin-top: 18px; @@ -371,7 +405,7 @@ header .logout-btn:hover, justify-content: space-between; gap: 16px; padding-bottom: 18px; - border-bottom: 1px solid rgba(203, 183, 153, 0.6); + border-bottom: 1px solid var(--line); } .account-heading h2 { margin: 4px 0 0; @@ -427,7 +461,7 @@ header .logout-btn:hover, .service-panel, .team-roster, .team-invite-panel { - padding: 18px; + padding: 16px; } .account-overview-card h3, .account-stage-header h3, @@ -456,8 +490,8 @@ header .logout-btn:hover, } .account-command-deck { display: grid; - grid-template-columns: minmax(280px, 1.05fr) minmax(0, 1.35fr); - gap: 14px; + grid-template-columns: minmax(320px, 1.08fr) minmax(0, 1.3fr); + gap: 12px; align-items: stretch; } .account-metric-strip { @@ -503,7 +537,7 @@ header .logout-btn:hover, justify-content: space-between; gap: 18px; grid-column: 1 / -1; - padding: 16px 18px 0; + padding: 14px 16px 0; border-top: 1px solid var(--line); } .account-action-copy { @@ -526,7 +560,7 @@ header .logout-btn:hover, .portal-support-panel { border: 1px solid var(--line); border-radius: var(--radius-lg); - padding: 26px; + padding: 22px; background: rgba(255, 255, 255, 0.98); box-shadow: var(--shadow-sm); } @@ -572,19 +606,19 @@ header .logout-btn:hover, .workspace-list { display: flex; flex-direction: column; - gap: 12px; + gap: 10px; } .workspace-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; - gap: 16px; + gap: 14px; align-items: center; - padding: 20px; + padding: 16px; } .workspace-row-main { display: grid; - grid-template-columns: minmax(0, 1.4fr) minmax(220px, 0.85fr); - gap: 18px; + grid-template-columns: minmax(0, 1.55fr) minmax(220px, 0.8fr); + gap: 14px; align-items: center; } .workspace-row-title { @@ -601,7 +635,7 @@ header .logout-btn:hover, .workspace-management-content h4, .team-panel-heading h4 { margin: 0; - font-size: 18px; + font-size: 17px; line-height: 1.1; letter-spacing: -0.025em; } @@ -636,8 +670,8 @@ header .logout-btn:hover, display: flex; flex-direction: column; gap: 8px; - min-height: 74px; - padding: 14px; + min-height: 68px; + padding: 12px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--panel-muted); @@ -759,7 +793,7 @@ header .logout-btn:hover, } .team-management-grid { display: grid; - grid-template-columns: minmax(0, 1.6fr) minmax(320px, 0.95fr); + grid-template-columns: minmax(0, 1.65fr) minmax(320px, 0.92fr); gap: 14px; } .team-panel-heading { @@ -771,11 +805,11 @@ header .logout-btn:hover, gap: 10px; } .team-member-row { - display: flex; + display: grid; + grid-template-columns: minmax(0, 1fr) 120px auto; align-items: center; - justify-content: space-between; gap: 16px; - padding: 14px 16px; + padding: 12px 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel-muted); @@ -798,9 +832,17 @@ header .logout-btn:hover, .team-member-controls { display: flex; align-items: center; + justify-content: end; gap: 10px; flex-wrap: wrap; } +.team-member-role-summary { + font-size: 12px; + font-weight: 800; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--ink-muted); +} .team-role-badge { display: inline-flex; align-items: center; @@ -846,13 +888,13 @@ header .logout-btn:hover, .service-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 14px; + gap: 12px; } .service-card { display: flex; flex-direction: column; gap: 8px; - padding: 18px; + padding: 16px; } .service-card-button { width: 100%; @@ -862,7 +904,7 @@ header .logout-btn:hover, .service-card:hover, .service-card-button:hover { border-color: var(--accent); - box-shadow: var(--shadow-md); + box-shadow: var(--shadow-sm); } .service-panel { padding: 22px; @@ -943,6 +985,14 @@ header .logout-btn:hover, .support-copy { margin-top: 10px !important; } +.account-overview-callout { + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid var(--line); + font-size: 13px; + line-height: 1.65; + color: var(--ink-soft); +} .badge { display: inline-flex; align-items: center; @@ -1044,7 +1094,7 @@ header .logout-btn:hover, @media (max-width: 920px) { .portal-hero { grid-template-columns: 1fr; - padding: 28px; + padding: 20px; } .portal-hero-stats { grid-template-columns: 1fr; @@ -1060,12 +1110,12 @@ header .logout-btn:hover, } @media (max-width: 720px) { header { - padding: 16px 18px; + padding: 14px 16px; align-items: flex-start; flex-direction: column; } .main { - padding: 18px 16px 40px; + padding: 18px 14px 40px; } .portal-shell-layout, .portal-support-card-grid, @@ -1076,7 +1126,7 @@ header .logout-btn:hover, .portal-hero, .intro-card, .service-panel { - padding: 20px; + padding: 18px; } .account-surface-header, .workspace-management-header, @@ -1097,9 +1147,12 @@ header .logout-btn:hover, } .team-member-row, .team-member-controls { - flex-direction: column; + grid-template-columns: 1fr; align-items: stretch; } + .team-member-controls { + justify-content: start; + } .workspace-row-status-grid { grid-template-columns: 1fr; } diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js index c6f62a81d0..d4f4cc557e 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -1,5 +1,19 @@ (() => { // src/account_view.ts + function roleLabel(role) { + switch (role) { + case "owner": + return "Owner"; + case "admin": + return "Admin"; + case "tech": + return "Tech"; + case "read_only": + return "Read-only"; + default: + return role || "Member"; + } + } function getElement(id) { return document.getElementById(id); } @@ -104,7 +118,7 @@ if (member.role === "owner" && !isOwner) { var locked = document.createElement("span"); locked.className = "team-role-badge"; - locked.textContent = "Owner"; + locked.textContent = roleLabel(member.role); return locked; } var sel = document.createElement("select"); @@ -113,7 +127,7 @@ for (var j = 0; j < roles.length; j += 1) { var opt = document.createElement("option"); opt.value = roles[j]; - opt.textContent = roles[j].replace("_", " "); + opt.textContent = roleLabel(roles[j]); if (member.role === roles[j]) opt.selected = true; sel.appendChild(opt); } @@ -147,14 +161,18 @@ identity.appendChild(email); var caption = document.createElement("div"); caption.className = "team-member-caption"; - caption.textContent = member.role === "owner" ? "Full account owner access" : "Hosted account operator"; + caption.textContent = member.role === "owner" ? "Full account control" : "Hosted account access"; identity.appendChild(caption); + var roleSummary = document.createElement("div"); + roleSummary.className = "team-member-role-summary"; + roleSummary.textContent = roleLabel(member.role); var controls = document.createElement("div"); controls.className = "team-member-controls"; controls.appendChild(renderTeamRoleControl(accountID, member, isOwner)); var action = renderTeamMemberAction(accountID, member, isOwner); if (action) controls.appendChild(action); row.appendChild(identity); + row.appendChild(roleSummary); row.appendChild(controls); return row; } @@ -1638,7 +1656,7 @@ var hosted = hasHostedAccounts(accounts); var workspaceTotal = countWorkspaces(accounts); var title = hosted ? "Pulse Account" : "Self-hosted Pulse Account"; - var summary = hosted ? "Open hosted workspaces, manage account access, and handle commercial account services from one place." : "Use this account for self-hosted billing, license recovery, refunds, and privacy actions. Hosted workspace access will appear here when it is attached to this email."; + var summary = hosted ? "Run hosted operations, operator access, and commercial account services from one control surface." : "Use this account for billing, license recovery, refunds, and privacy actions. Hosted workspace access will appear here when it is attached to this email."; return '
' + (hosted ? "Hosted access is active on this account." : "No hosted workspace access is attached to this account yet.") + "

" + title + "

" + summary + '

Hosted access' + (hosted ? "Active" : "Not attached") + '
Accounts' + (accounts.length === 1 ? "1 account" : String(accounts.length) + " accounts") + '
Workspaces' + String(workspaceTotal) + "
"; } function shellSectionButton(section, activeSection, title, copy) { @@ -1646,7 +1664,7 @@ } function renderShellNavigation(accounts, supportEmail, activeSection) { var hosted = hasHostedAccounts(accounts); - return '"; + return '"; } function renderWorkspaceCard(account, workspace, accountAPIBasePath) { var status = workspaceHealthState(workspace); @@ -1689,7 +1707,9 @@ addWorkspaceForm = '
'; } } - return '
" + actions + addWorkspaceForm + "
"; + return '
" + actions + addWorkspaceForm + "
"; } function renderAccountWorkspaceSection(account, accountAPIBasePath) { var workspaces = Array.isArray(account.workspaces) ? account.workspaces : []; @@ -1706,7 +1726,7 @@ return '

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.

Loading\u2026

Invite someone new

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

'; } function renderSupportSection(context) { - return '
Support

Get help without leaving Pulse Account

Use support when hosted access looks wrong, billing does not behave as expected, or you need help with commercial licensing and privacy actions.

Account support

For access, tenant handoff, team, and billing issues, contact the hosted operations desk.

' + escapeHTML(context.bootstrap.support_email || "") + '

Commercial services

Self-hosted subscriptions, license recovery, refunds, and privacy requests all route through the same account surface.

'; + return '
Support

Support and escalation

Use support when hosted access looks wrong, billing does not behave as expected, or you need help with commercial licensing and privacy actions.

Account support

For access, tenant handoff, team, and billing issues, contact the hosted operations desk.

' + escapeHTML(context.bootstrap.support_email || "") + '

Commercial services

Self-hosted subscriptions, license recovery, refunds, and privacy requests all route through the same account surface.

'; } function renderHeaderHTML(context) { if (context.bootstrap.authenticated) { diff --git a/internal/cloudcp/portal/frontend/src/account_view.ts b/internal/cloudcp/portal/frontend/src/account_view.ts index 44c688899c..9ce7850503 100644 --- a/internal/cloudcp/portal/frontend/src/account_view.ts +++ b/internal/cloudcp/portal/frontend/src/account_view.ts @@ -8,6 +8,21 @@ import type { type FormValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; +function roleLabel(role: string): string { + switch (role) { + case 'owner': + return 'Owner'; + case 'admin': + return 'Admin'; + case 'tech': + return 'Tech'; + case 'read_only': + return 'Read-only'; + default: + return role || 'Member'; + } +} + export function getElement(id: string): T | null { return document.getElementById(id) as T | null; } @@ -131,7 +146,7 @@ function renderTeamRoleControl(accountID: string, member: PortalTeamMember, isOw if (member.role === 'owner' && !isOwner) { var locked = document.createElement('span'); locked.className = 'team-role-badge'; - locked.textContent = 'Owner'; + locked.textContent = roleLabel(member.role); return locked; } @@ -141,7 +156,7 @@ function renderTeamRoleControl(accountID: string, member: PortalTeamMember, isOw for (var j = 0; j < roles.length; j += 1) { var opt = document.createElement('option'); opt.value = roles[j]; - opt.textContent = roles[j].replace('_', ' '); + opt.textContent = roleLabel(roles[j]); if (member.role === roles[j]) opt.selected = true; sel.appendChild(opt); } @@ -181,9 +196,13 @@ function renderTeamMemberRow(accountID: string, member: PortalTeamMember, isOwne var caption = document.createElement('div'); caption.className = 'team-member-caption'; - caption.textContent = member.role === 'owner' ? 'Full account owner access' : 'Hosted account operator'; + caption.textContent = member.role === 'owner' ? 'Full account control' : 'Hosted account access'; identity.appendChild(caption); + var roleSummary = document.createElement('div'); + roleSummary.className = 'team-member-role-summary'; + roleSummary.textContent = roleLabel(member.role); + var controls = document.createElement('div'); controls.className = 'team-member-controls'; controls.appendChild(renderTeamRoleControl(accountID, member, isOwner)); @@ -191,6 +210,7 @@ function renderTeamMemberRow(accountID: string, member: PortalTeamMember, isOwne if (action) controls.appendChild(action); row.appendChild(identity); + row.appendChild(roleSummary); row.appendChild(controls); return row; } diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts index dc7a33dca7..5901db2fda 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -114,8 +114,8 @@ function renderOverviewBand(accounts: PortalAccountSummary[]): string { var workspaceTotal = countWorkspaces(accounts); var title = hosted ? 'Pulse Account' : 'Self-hosted Pulse Account'; var summary = hosted - ? 'Open hosted workspaces, manage account access, and handle commercial account services from one place.' - : 'Use this account for self-hosted billing, license recovery, refunds, and privacy actions. Hosted workspace access will appear here when it is attached to this email.'; + ? 'Run hosted operations, operator access, and commercial account services from one control surface.' + : 'Use this account for billing, license recovery, refunds, and privacy actions. Hosted workspace access will appear here when it is attached to this email.'; return ( '
' + @@ -155,6 +155,11 @@ function renderShellNavigation(accounts: PortalAccountSummary[], supportEmail: s var hosted = hasHostedAccounts(accounts); return ( '