style(portal): simplify account console overview

This commit is contained in:
rcourtman 2026-03-27 19:07:26 +00:00
parent 1dd4c75eb5
commit 8596d9de8a
6 changed files with 215 additions and 200 deletions

View file

@ -1,5 +1,5 @@
{
"source_hash": "85ccadc015ace1a5ed37c100689292817f7c41de9b0d8547a6d26ca908c4d19f",
"source_hash": "b4d41db9eab964bde794244e2c7d9d7e0aa77de1523520c2c448d1f08cfe969f",
"build_inputs": [
"package.json",
"tsconfig.json",

View file

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

File diff suppressed because one or more lines are too long

View file

@ -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');

View file

@ -134,15 +134,6 @@ function renderServiceActionRow(
);
}
function renderOverviewQuickAction(section: PortalShellSection, title: string, copy: string): string {
return (
'<button class="overview-quick-action" type="button" data-shell-action="activate-section" data-shell-section="' + section + '">' +
'<span class="overview-quick-action-title">' + title + '</span>' +
'<span class="overview-quick-action-copy">' + copy + '</span>' +
'</button>'
);
}
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 +=
'<button type="button" class="btn-secondary btn-compact" data-action="toggle-add-workspace" data-account-id="' +
escapeAttr(account.id) +
'" data-shell-target="workspaces">Add workspace</button>';
}
if (account.has_billing && account.can_manage) {
actionHTML +=
'<button type="button" class="btn-secondary btn-compact" data-action="open-billing" data-account-id="' +
escapeAttr(account.id) +
'">Manage billing</button>';
}
if (account.can_manage) {
actionHTML +=
'<button type="button" class="btn-secondary btn-compact" data-action="toggle-team" data-account-id="' +
escapeAttr(account.id) +
'" data-shell-target="team">Manage team</button>';
}
return (
'<section class="portal-account-bar">' +
'<div class="portal-account-bar-copy">' +
'<div class="account-eyebrow">' + escapeHTML(accountKindLabel(account)) + '</div>' +
'<div class="portal-account-bar-row">' +
'<h2>' + escapeHTML(account.name) + '</h2>' +
'<div class="portal-account-bar-chips">' +
'<span class="account-context-chip">' + escapeHTML(account.kind_label) + '</span>' +
'<span class="account-context-chip">' + escapeHTML(titleCase(account.role)) + '</span>' +
'<span class="account-context-chip">' + escapeHTML(workspaceLabel) + '</span>' +
'</div>' +
'</div>' +
'<p>' + escapeHTML(account.kind === 'msp' ? 'Operator workspace account' : 'Hosted account operations') + '</p>' +
'</div>' +
'<div class="portal-account-bar-actions">' + actionHTML + '</div>' +
'</section>'
);
}
function shellSectionButton(section: PortalShellSection, activeSection: PortalShellSection, title: string, copy: string): string {
return (
'<button class="portal-shell-nav-link' + (activeSection === section ? ' active' : '') + '" type="button" data-shell-action="activate-section" data-shell-section="' + section + '">' +
@ -350,40 +383,24 @@ function renderAccountOverviewSection(account: PortalAccountSummary): string {
: 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 = '';
if (account.can_manage) {
actions =
'<div class="account-action-strip">' +
'<div class="account-action-copy">' +
'<div class="account-panel-kicker">Account operations</div>' +
'<p>' + escapeHTML(operationsCopy) + '</p>' +
'</div>' +
'<div class="account-actions">' +
(account.kind === 'msp'
? '<button type="button" class="btn-secondary" id="add-ws-btn-' +
escapeAttr(account.id) +
'" data-action="toggle-add-workspace" data-account-id="' +
escapeAttr(account.id) +
'" data-shell-target="workspaces">Add workspace</button>'
: '') +
(account.has_billing
? '<button type="button" class="btn-secondary" data-action="open-billing" data-account-id="' +
escapeAttr(account.id) +
'">Manage billing</button>'
: '') +
'<button type="button" class="btn-secondary" id="team-btn-' +
escapeAttr(account.id) +
'" data-action="toggle-team" data-account-id="' +
escapeAttr(account.id) +
'" data-shell-target="team">Manage team</button>' +
'</div>' +
'</div>';
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 =
'<div class="overview-next-actions">' +
'<button class="btn-primary btn-compact" type="button" data-shell-action="activate-section" data-shell-section="workspaces">Open workspaces</button>' +
'<button class="btn-secondary btn-compact" type="button" data-shell-action="activate-section" data-shell-section="' + (account.can_manage ? 'team' : 'services') + '">' + (account.can_manage ? 'Review team access' : 'Open account services') + '</button>' +
'</div>';
if (account.can_manage) {
if (account.kind === 'msp') {
addWorkspaceForm =
'<div class="add-workspace-form" id="add-ws-form-' +
@ -442,18 +459,13 @@ function renderAccountOverviewSection(account: PortalAccountSummary): string {
'</div>' +
'</div>' +
'</div>' +
actions +
addWorkspaceForm +
'<div class="account-overview-secondary">' +
'<div class="overview-side-card overview-side-card-primary">' +
'<div class="account-panel-kicker">Operator overview</div>' +
'<h4>Start from the next action, not the whole account</h4>' +
'<p>Use the overview for posture, then move into workspaces, team, or account services depending on what needs attention.</p>' +
'<div class="overview-quick-actions">' +
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') +
'</div>' +
'<div class="account-panel-kicker">Next move</div>' +
'<h4>' + escapeHTML(nextStepTitle) + '</h4>' +
'<p>' + escapeHTML(nextStepCopy) + '</p>' +
nextStepActions +
'</div>' +
renderAttentionPanel(workspaces) +
'</div>' +
@ -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 (
'<section class="account-surface">' +
'<div class="account-surface-header">' +
'<div class="account-heading">' +
'<div class="account-eyebrow">' + escapeHTML(accountKindLabel(account)) + '</div>' +
'<h2>' + escapeHTML(account.name) + '</h2>' +
'<div class="account-summary">' + escapeHTML(account.kind === 'msp' ? 'Operator workspace account' : 'Hosted account operations') + '</div>' +
'</div>' +
'<div class="account-context-strip">' +
'<span class="account-context-chip">' + escapeHTML(account.kind_label) + '</span>' +
'<span class="account-context-chip">' + escapeHTML(titleCase(account.role)) + '</span>' +
'<span class="account-context-chip">' + escapeHTML(workspaceLabel) + '</span>' +
'</div>' +
'</div>' +
'<div class="account-surface-body">' +
renderAccountOverviewSection(account) +
renderAccountWorkspaceSection(account, context.accountAPIBasePath) +
@ -746,6 +745,7 @@ export function renderAuthenticatedPortalHTML(context: ShellViewContext): string
renderShellNavigation(accounts, context.bootstrap.support_email || '', activeSection) +
'<div class="portal-shell-main">' +
renderOverviewBand(accounts) +
(accounts.length === 1 ? renderPrimaryAccountBar(accounts[0]) : '') +
'<section class="portal-content-panel portal-content-panel-overview">' +
'<div id="accounts-root">' + hostedContent + '</div>' +
'</section>' +

View file

@ -430,6 +430,52 @@ header .logout-btn:hover,
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,
@ -513,32 +559,6 @@ header .logout-btn:hover,
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,
@ -588,7 +608,6 @@ header .logout-btn:hover,
display: grid;
grid-template-columns: 1fr;
gap: 14px;
margin-top: 10px;
}
.account-content-panel {
@ -773,40 +792,11 @@ header .logout-btn:hover,
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 {
@ -1233,12 +1223,14 @@ 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,
@ -1247,6 +1239,10 @@ header .logout-btn:hover,
border-color: var(--line);
}
.team-roster {
align-self: start;
}
.team-policy-panel {
padding: 16px;
border: 1px solid var(--line);
@ -1917,10 +1913,6 @@ header .logout-btn:hover,
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.overview-quick-actions {
grid-template-columns: 1fr;
}
.portal-shell-nav {
position: static;
}
@ -1939,6 +1931,15 @@ header .logout-btn:hover,
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;
}
@ -1976,12 +1977,13 @@ header .logout-btn:hover,
}
.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,