diff --git a/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md b/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md
index 85b07804d3..7ac1328a5b 100644
--- a/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md
+++ b/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md
@@ -1,6 +1,6 @@
# Pulse Account Portal Spec
-Last updated: 2026-03-26
+Last updated: 2026-03-27
Status: ACTIVE
## Purpose
@@ -135,6 +135,11 @@ Core rules:
7. Self-hosted billing/license tools belong in the same account shell, but
they must present as a secondary account-services area when hosted access
is active.
+8. Hosted and MSP accounts must expose account-level operations separately from
+ individual workspace cards so operator actions are visible before a user
+ starts drilling into one workspace at a time.
+9. Workspace fleets must summarize health and attention state at the account
+ level, not force the user to scan badges one card at a time.
## Screen Model
@@ -158,9 +163,14 @@ The signed-in shell should be treated as four first-class states:
Each signed-in state should render:
1. a concise overview band that summarizes the account posture
-2. account cards with role, account kind, and workspace totals
-3. explicit action groups, not anonymous menu affordances
-4. a clearly separated `Self-hosted licenses and billing` section when those
+2. account cards with role, account kind, workspace totals, and account-level
+ fleet status
+3. an explicit `Account operations` area for billing, team, and workspace
+ management entry points
+4. a `Workspace fleet` area that makes health and lifecycle state legible
+ before the user opens a workspace
+5. explicit action groups, not anonymous menu affordances
+6. a clearly separated `Self-hosted licenses and billing` section when those
tools are present below hosted content
## Product-Specific Boundaries
diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json
index 9c37602b83..9998575396 100644
--- a/internal/cloudcp/portal/dist/build_manifest.json
+++ b/internal/cloudcp/portal/dist/build_manifest.json
@@ -1,5 +1,5 @@
{
- "source_hash": "ade0e3202886602f0f46ca656041930d213b8dc6ea3d681664a6596f18da0c12",
+ "source_hash": "f091b6dbbdf775a750bbba5c4047fbba2553fa0393420916e2e9e5ca315e1437",
"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 5b687d41c9..9e628e2ca8 100644
--- a/internal/cloudcp/portal/dist/portal_app.css
+++ b/internal/cloudcp/portal/dist/portal_app.css
@@ -110,6 +110,43 @@ header .logout-btn:hover {
color: #64748b;
max-width: 520px;
}
+.account-status-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 12px;
+ margin: 0 0 16px;
+}
+.account-stat-card {
+ background: #fff;
+ border: 1px solid #e2e8f0;
+ border-radius: 12px;
+ padding: 14px 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ box-shadow: 0 1px 3px rgba(15, 23, 42, .04);
+}
+.account-stat-label {
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: #64748b;
+}
+.account-stat-value {
+ font-size: 24px;
+ font-weight: 700;
+ color: #0f172a;
+}
+.account-stat-healthy {
+ color: #166534;
+}
+.account-stat-checking {
+ color: #1d4ed8;
+}
+.account-stat-unhealthy {
+ color: #b91c1c;
+}
.badge {
font-size: 11px;
font-weight: 600;
@@ -194,10 +231,16 @@ header .logout-btn:hover {
.ws-info {
flex: 1;
min-width: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+}
+.ws-title-row {
display: flex;
align-items: center;
gap: 10px;
- flex-wrap: wrap;
+ width: 100%;
}
.ws-name {
font-weight: 600;
@@ -209,6 +252,11 @@ header .logout-btn:hover {
gap: 6px;
flex-wrap: wrap;
}
+.ws-summary {
+ font-size: 13px;
+ color: #475569;
+ line-height: 1.45;
+}
.ws-created {
font-size: 11px;
color: #94a3b8;
@@ -305,10 +353,39 @@ header .logout-btn:hover {
.link-button {
text-decoration: none;
}
+.account-operations-panel {
+ margin: 0 0 18px;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 16px 18px;
+ background: #fff;
+ border: 1px solid #e2e8f0;
+ border-radius: 12px;
+ box-shadow: 0 1px 3px rgba(15, 23, 42, .04);
+}
+.account-operations-copy {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ max-width: 540px;
+}
+.account-operations-copy h3 {
+ margin: 0;
+ font-size: 15px;
+ font-weight: 700;
+}
+.account-operations-copy p {
+ margin: 0;
+ font-size: 13px;
+ color: #64748b;
+ line-height: 1.5;
+}
.account-actions {
- margin-top: 14px;
display: flex;
align-items: center;
+ justify-content: flex-end;
gap: 12px;
flex-wrap: wrap;
}
@@ -794,6 +871,15 @@ header .logout-btn:hover {
.account-badges {
justify-content: flex-start;
}
+ .account-status-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ .account-operations-panel {
+ flex-direction: column;
+ }
+ .account-actions {
+ justify-content: flex-start;
+ }
.account-subsection-header {
flex-direction: column;
align-items: flex-start;
@@ -803,6 +889,9 @@ header .logout-btn:hover {
}
}
@media (max-width: 560px) {
+ .account-status-grid {
+ grid-template-columns: 1fr;
+ }
.workspace-card {
flex-direction: column;
align-items: flex-start;
diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js
index a047a76689..7e4dd8d079 100644
--- a/internal/cloudcp/portal/dist/portal_app.js
+++ b/internal/cloudcp/portal/dist/portal_app.js
@@ -1464,6 +1464,20 @@
function hasHostedAccounts(accounts) {
return accounts.length > 0;
}
+ function countWorkspacesByHealth(workspaces, status) {
+ var count = 0;
+ for (var i = 0; i < workspaces.length; i += 1) {
+ if (String(workspaces[i].health_status || "") === status) count += 1;
+ }
+ return count;
+ }
+ function countWorkspacesByState(workspaces, state) {
+ var count = 0;
+ for (var i = 0; i < workspaces.length; i += 1) {
+ if (String(workspaces[i].state || "") === state) count += 1;
+ }
+ return count;
+ }
function renderOverviewBand(context, accounts) {
var hosted = hasHostedAccounts(accounts);
var workspaceTotal = countWorkspaces(accounts);
@@ -1478,6 +1492,7 @@
var state = String(workspace.state || "");
var safeState = escapeHTML(state);
var createdLabel = formatWorkspaceDate(workspace.created_at);
+ var healthSummary = workspace.health_status === "healthy" ? "Live updates and health checks are currently good." : workspace.health_status === "unhealthy" ? "This workspace needs attention before it is trustworthy." : "This workspace is still waiting on a completed health check.";
var openAction = "";
if (state === "active") {
openAction = '
';
@@ -1491,11 +1506,16 @@
manageAction = '";
}
var createdMeta = createdLabel ? 'Created ' + escapeHTML(createdLabel) + " " : "";
- return '' + escapeHTML(workspace.display_name) + ' ' + healthBadgeHTML(workspace) + '' + safeState + " " + createdMeta + '
' + openAction + manageAction + "
";
+ return '' + escapeHTML(workspace.display_name) + '
' + healthBadgeHTML(workspace) + '' + safeState + " " + createdMeta + '
' + escapeHTML(healthSummary) + '
' + openAction + manageAction + "
";
}
function renderAccountSection(account, accountAPIBasePath) {
var workspaces = Array.isArray(account.workspaces) ? account.workspaces : [];
var summaryText = accountKindLabel(account) + " \xB7 " + titleCase(account.role) + " \xB7 " + workspaceCountLabel(workspaces.length);
+ var healthyCount = countWorkspacesByHealth(workspaces, "healthy");
+ var checkingCount = countWorkspacesByHealth(workspaces, "checking");
+ var unhealthyCount = countWorkspacesByHealth(workspaces, "unhealthy");
+ var activeCount = countWorkspacesByState(workspaces, "active");
+ 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 workspaceHTML = "";
if (workspaces.length === 0) {
workspaceHTML = 'No hosted workspaces yet. Create one to get started.
';
@@ -1508,13 +1528,13 @@
var teamSection = "";
var addWorkspaceForm = "";
if (account.can_manage) {
- actions = '' + (account.kind === "msp" ? '+ Add workspace ' : "") + (account.has_billing ? 'Manage billing ' : "") + 'Manage team
';
+ actions = 'Account operations ' + escapeHTML(operationsCopy) + '
' + (account.kind === "msp" ? 'Add workspace ' : "") + (account.has_billing ? 'Manage billing ' : "") + 'Manage team
';
teamSection = '';
if (account.kind === "msp") {
addWorkspaceForm = '';
}
}
- return '' + workspaceHTML + actions + teamSection + addWorkspaceForm + " ";
+ return 'Active workspaces ' + String(activeCount) + '
Healthy ' + String(healthyCount) + '
Checking ' + String(checkingCount) + '
Needs attention ' + String(unhealthyCount) + "
" + actions + '' + workspaceHTML + teamSection + addWorkspaceForm + " ";
}
function renderHeaderHTML(context) {
if (context.bootstrap.authenticated) {
diff --git a/internal/cloudcp/portal/frontend/src/shell_view.test.ts b/internal/cloudcp/portal/frontend/src/shell_view.test.ts
index 7315a4a674..832e0a6cab 100644
--- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts
+++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts
@@ -131,12 +131,20 @@ describe('shell view', function() {
expect(html).toContain('MSP account');
expect(html).toContain('MSP account · Owner · 3 workspaces');
expect(html).toContain('Acme MSP');
+ expect(html).toContain('Account operations');
+ expect(html).toContain('Manage the client fleet from this account surface.');
+ expect(html).toContain('Workspace fleet');
expect(html).toContain('Alpha Workspace');
expect(html).toContain('Beta Workspace');
expect(html).toContain('Gamma Workspace');
+ expect(html).toContain('Active workspaces');
+ expect(html).toContain('Needs attention');
expect(html).toContain('Healthy');
expect(html).toContain('Unhealthy');
expect(html).toContain('Checking');
+ expect(html).toContain('Live updates and health checks are currently good.');
+ 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');
expect(html).toContain('Open workspace');
expect(html).toContain('data-action="toggle-workspace-menu"');
diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts
index e9df631429..29f3928f2c 100644
--- a/internal/cloudcp/portal/frontend/src/shell_view.ts
+++ b/internal/cloudcp/portal/frontend/src/shell_view.ts
@@ -77,6 +77,22 @@ function hasHostedAccounts(accounts: PortalAccountSummary[]): boolean {
return accounts.length > 0;
}
+function countWorkspacesByHealth(workspaces: PortalWorkspaceSummary[], status: string): number {
+ var count = 0;
+ for (var i = 0; i < workspaces.length; i += 1) {
+ if (String(workspaces[i].health_status || '') === status) count += 1;
+ }
+ return count;
+}
+
+function countWorkspacesByState(workspaces: PortalWorkspaceSummary[], state: string): number {
+ var count = 0;
+ for (var i = 0; i < workspaces.length; i += 1) {
+ if (String(workspaces[i].state || '') === state) count += 1;
+ }
+ return count;
+}
+
function renderOverviewBand(context: ShellViewContext, accounts: PortalAccountSummary[]): string {
var hosted = hasHostedAccounts(accounts);
var workspaceTotal = countWorkspaces(accounts);
@@ -117,6 +133,11 @@ function renderWorkspaceCard(account: PortalAccountSummary, workspace: PortalWor
var state = String(workspace.state || '');
var safeState = escapeHTML(state);
var createdLabel = formatWorkspaceDate(workspace.created_at);
+ var healthSummary = workspace.health_status === 'healthy'
+ ? 'Live updates and health checks are currently good.'
+ : workspace.health_status === 'unhealthy'
+ ? 'This workspace needs attention before it is trustworthy.'
+ : 'This workspace is still waiting on a completed health check.';
var openAction = '';
if (state === 'active') {
openAction =
@@ -172,12 +193,15 @@ function renderWorkspaceCard(account: PortalAccountSummary, workspace: PortalWor
return (
'' +
'
' +
- '
' + escapeHTML(workspace.display_name) + ' ' +
+ '
' +
+ '' + escapeHTML(workspace.display_name) + ' ' +
+ '
' +
'
' +
healthBadgeHTML(workspace) +
'' + safeState + ' ' +
createdMeta +
'
' +
+ '
' + escapeHTML(healthSummary) + '
' +
'
' +
'
' +
openAction +
@@ -190,6 +214,13 @@ function renderWorkspaceCard(account: PortalAccountSummary, workspace: PortalWor
function renderAccountSection(account: PortalAccountSummary, accountAPIBasePath: string): string {
var workspaces = Array.isArray(account.workspaces) ? account.workspaces : [];
var summaryText = accountKindLabel(account) + ' · ' + titleCase(account.role) + ' · ' + workspaceCountLabel(workspaces.length);
+ var healthyCount = countWorkspacesByHealth(workspaces, 'healthy');
+ var checkingCount = countWorkspacesByHealth(workspaces, 'checking');
+ var unhealthyCount = countWorkspacesByHealth(workspaces, 'unhealthy');
+ var activeCount = countWorkspacesByState(workspaces, 'active');
+ 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 workspaceHTML = '';
if (workspaces.length === 0) {
workspaceHTML = '
No hosted workspaces yet. Create one to get started.
';
@@ -207,13 +238,18 @@ function renderAccountSection(account: PortalAccountSummary, accountAPIBasePath:
var addWorkspaceForm = '';
if (account.can_manage) {
actions =
+ '
' +
+ '
' +
+ '
Account operations ' +
+ '
' + escapeHTML(operationsCopy) + '
' +
+ '
' +
'
' +
(account.kind === 'msp'
? '+ Add workspace '
+ '">Add workspace'
: '') +
(account.has_billing
? 'Manage team ' +
+ '
' +
'
';
teamSection =
@@ -298,12 +335,30 @@ function renderAccountSection(account: PortalAccountSummary, accountAPIBasePath:
roleBadgeHTML(account.role) +
'
' +
'
' +
+ '' +
+ '
' +
+ 'Active workspaces ' +
+ '' + String(activeCount) + ' ' +
+ '
' +
+ '
' +
+ 'Healthy ' +
+ '' + String(healthyCount) + ' ' +
+ '
' +
+ '
' +
+ 'Checking ' +
+ '' + String(checkingCount) + ' ' +
+ '
' +
+ '
' +
+ 'Needs attention ' +
+ '' + String(unhealthyCount) + ' ' +
+ '
' +
+ '
' +
+ actions +
'' +
workspaceHTML +
- actions +
teamSection +
addWorkspaceForm +
''
diff --git a/internal/cloudcp/portal/frontend/src/styles.css b/internal/cloudcp/portal/frontend/src/styles.css
index f094b0036f..7fdec4f16f 100644
--- a/internal/cloudcp/portal/frontend/src/styles.css
+++ b/internal/cloudcp/portal/frontend/src/styles.css
@@ -17,6 +17,13 @@
.account-subsection-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin: 0 0 14px; }
.account-subsection-header h3 { margin: 0; font-size: 15px; font-weight: 700; }
.account-subsection-header p { margin: 0; font-size: 13px; color: #64748b; max-width: 520px; }
+ .account-status-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 0 0 16px; }
+ .account-stat-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 14px 16px; display: flex; flex-direction: column; gap: 6px; box-shadow: 0 1px 3px rgba(15,23,42,.04); }
+ .account-stat-label { font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: #64748b; }
+ .account-stat-value { font-size: 24px; font-weight: 700; color: #0f172a; }
+ .account-stat-healthy { color: #166534; }
+ .account-stat-checking { color: #1d4ed8; }
+ .account-stat-unhealthy { color: #b91c1c; }
.badge { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.5px; }
.badge-msp { background: #dbeafe; color: #1e40af; }
.badge-cloud { background: #dcfce7; color: #166534; }
@@ -33,9 +40,11 @@
.workspace-list { display: flex; flex-direction: column; gap: 10px; }
.workspace-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 10px; padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; gap: 12px; box-shadow: 0 1px 3px rgba(15,23,42,.04); }
.workspace-card:hover { border-color: #cbd5e1; }
- .ws-info { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
+ .ws-info { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
+ .ws-title-row { display: flex; align-items: center; gap: 10px; width: 100%; }
.ws-name { font-weight: 600; font-size: 15px; }
.ws-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
+ .ws-summary { font-size: 13px; color: #475569; line-height: 1.45; }
.ws-created { font-size: 11px; color: #94a3b8; }
.ws-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.workspace-menu-wrap { position: relative; }
@@ -53,7 +62,11 @@
.btn-danger:hover { background: #fef2f2; }
.btn-compact { padding: 8px 14px; font-size: 13px; }
.link-button { text-decoration: none; }
- .account-actions { margin-top: 14px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
+ .account-operations-panel { margin: 0 0 18px; display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 16px 18px; background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; box-shadow: 0 1px 3px rgba(15,23,42,.04); }
+ .account-operations-copy { display: flex; flex-direction: column; gap: 4px; max-width: 540px; }
+ .account-operations-copy h3 { margin: 0; font-size: 15px; font-weight: 700; }
+ .account-operations-copy p { margin: 0; font-size: 13px; color: #64748b; line-height: 1.5; }
+ .account-actions { display: flex; align-items: center; justify-content: flex-end; gap: 12px; flex-wrap: wrap; }
.add-workspace-form { margin-top: 12px; display: none; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; padding: 16px; }
.add-workspace-form.visible { display: block; }
.add-workspace-form label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
@@ -141,10 +154,14 @@
@media (max-width: 720px) {
.account-header { flex-direction: column; }
.account-badges { justify-content: flex-start; }
+ .account-status-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .account-operations-panel { flex-direction: column; }
+ .account-actions { justify-content: flex-start; }
.account-subsection-header { flex-direction: column; align-items: flex-start; }
.overview-stats { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
+ .account-status-grid { grid-template-columns: 1fr; }
.workspace-card { flex-direction: column; align-items: flex-start; }
.ws-actions { align-self: stretch; }
.ws-actions form, .ws-actions .btn-primary { width: 100%; text-align: center; }