Make portal overview fact-first

This commit is contained in:
rcourtman
2026-03-29 16:26:59 +01:00
parent 0de0245ea1
commit bb47bb09ea
7 changed files with 138 additions and 120 deletions
@@ -1,6 +1,6 @@
# Pulse Account Portal Spec
Last updated: 2026-03-28
Last updated: 2026-03-29
Status: ACTIVE
## Purpose
@@ -257,6 +257,10 @@ Core rules:
workspaces remain. The shell may not imply active work is ready merely
because a suspended workspace exists; suspended-only states must say that
no active workspace is ready for routine use right now.
39. `Overview` must stay fact-first. It may not synthesize urgency or health
verdicts such as `Nothing urgent` or `Healthy now`; it must report
concrete counts, explicit workspace state, and the next action directly
from the owned runtime truth.
## Screen Model
@@ -366,6 +366,10 @@ ready until the first hosted workspace exists.
That same typed overview contract must also keep `Needs attention` honest when
only suspended workspaces remain: hosted workspace history alone may not make
the shell imply that active work is ready.
That same typed overview contract must also stay fact-first: overview copy may
not synthesize urgency or health verdicts such as `Nothing urgent` or
`Healthy now`, and must instead render concrete counts, explicit workspace
state, and next-action routing from the owned runtime payload.
That same typed overview contract must also preserve a sharp, high-density enterprise visual aesthetic (e.g. Cloudflare/GCP density standards) across all portal scenarios, removing gradients and heavy box-shadows to ensure a calm, rigorous visual language with standard 256px sidebars, Inter-grade typography, clean text-transform rules, and cleanly unboxed typography without excessive pills or stacked metrics.
plus a package-local `tsc --noEmit` gate, so future account-shell work should
extend the typed source boundary instead of reviving opaque global runtime
@@ -934,6 +934,10 @@ is ready yet until the first hosted workspace exists.
That same owned `Overview` surface must also keep `Needs attention` honest
when only suspended workspaces remain: a suspended-only account may not imply
that active work is ready simply because hosted workspace history exists.
That same owned `Overview` surface must also stay fact-first: it may not
invent urgency or health verdicts such as `Nothing urgent` or `Healthy now`,
and must instead report concrete counts, explicit workspace state, and the
next action directly from runtime-backed account truth.
That same canonical shell/runtime boundary now also owns the bootstrap truth
for when self-hosted commercial history is relevant. Hosted-only accounts must
not render self-hosted license, refund, privacy, or support-escalation copy
+1 -1
View File
@@ -1,5 +1,5 @@
{
"source_hash": "ec5ecce14e80d4018df77d407516b38b7c0c9ab4ca502f9da79f4b25e75219a4",
"source_hash": "90be82b1835d65ae5df2bf6c5793071dd69a2cb4f9ee855356e9ce0e375f7ac1",
"build_inputs": [
"package.json",
"tsconfig.json",
+38 -61
View File
@@ -1955,6 +1955,21 @@
function workspaceCountLabel(count) {
return count === 1 ? "1 workspace" : String(count) + " workspaces";
}
function reviewWorkspaceHeadline(count) {
return count === 1 ? "1 workspace needs review" : String(count) + " workspaces need review";
}
function readyWorkspaceHeadline(count) {
return count === 1 ? "1 workspace is ready to use" : String(count) + " workspaces are ready to use";
}
function reviewWorkspaceChipLabel(count) {
return count === 1 ? "1 workspace to review" : String(count) + " workspaces to review";
}
function readyWorkspaceChipLabel(count) {
return count === 1 ? "1 ready workspace" : String(count) + " ready workspaces";
}
function suspendedWorkspaceChipLabel(count) {
return count === 1 ? "1 suspended workspace" : String(count) + " suspended workspaces";
}
function hasHostedAccounts(accounts) {
return accounts.length > 0;
}
@@ -2031,11 +2046,11 @@
function workspaceStatusCopy(workspace) {
var status = workspaceHealthState(workspace);
var state = String(workspace.state || "");
if (state === "suspended") return "This workspace is suspended and will stay closed until you resume it.";
if (state === "failed") return "This workspace needs attention before it is trustworthy.";
if (status === "healthy") return "Live updates and health checks are currently good.";
if (status === "unhealthy") return "This workspace needs attention before it is trustworthy.";
return "This workspace is still waiting on a completed health check.";
if (state === "suspended") return "This workspace is suspended.";
if (state === "failed") return "This workspace is in a failed state.";
if (status === "healthy") return "Latest health check is healthy.";
if (status === "unhealthy") return "Latest health check is unhealthy.";
return "Latest health check is still pending.";
}
function workspaceRowNote(workspace) {
var status = workspaceHealthState(workspace);
@@ -2190,46 +2205,6 @@
if (!includeAccountName) return note;
return entry.account.name + " \xB7 " + note;
}
function overviewBillingSeparationCopy(accounts, showSelfHostedCommercial) {
var hostedBillingCount = 0;
var canManageHostedBilling = false;
for (var i = 0; i < accounts.length; i += 1) {
if (accounts[i].has_billing) {
hostedBillingCount += 1;
if (accounts[i].can_manage) {
canManageHostedBilling = true;
}
}
}
if (!accounts.length) {
return {
title: "Billing stays separate",
copy: "Self-hosted billing, licenses, refunds, and privacy stay in Billing."
};
}
if (showSelfHostedCommercial) {
if (hostedBillingCount > 0) {
return {
title: "Billing stays separate",
copy: canManageHostedBilling ? "Hosted billing stays in Billing, and self-hosted tools appear there only when relevant." : "Hosted billing stays in Billing, an owner or admin opens it, and self-hosted tools appear there only when relevant."
};
}
return {
title: "Billing stays separate",
copy: "Self-hosted tools appear in Billing only when they are relevant to this account."
};
}
if (hostedBillingCount > 0) {
return {
title: "Hosted billing stays separate",
copy: canManageHostedBilling ? "Use Billing only for hosted invoices, payment methods, or subscription changes." : "Hosted billing stays in Billing, and an owner or admin must open it."
};
}
return {
title: "Billing stays separate",
copy: "Use Billing only when the task is commercial, not operational."
};
}
function renderOverviewAttentionCard(accounts, entries, showSelfHostedCommercial) {
var attention = attentionOverviewEntries(entries);
var ready = readyOverviewEntries(entries);
@@ -2237,17 +2212,16 @@
var suspendedCount = countWorkspacesByState(entries.map(function(entry) {
return entry.workspace;
}), "suspended");
var billingSeparation = overviewBillingSeparationCopy(accounts, showSelfHostedCommercial);
if (!attention.length) {
return '<article class="overview-task-card"><div class="account-panel-kicker">Needs attention</div><h4>Nothing urgent</h4><p>' + escapeHTML(
entries.length > 0 ? "No active workspace is currently asking for review." : "No hosted workspace is currently asking for review."
) + '</p><div class="overview-task-list"><div class="overview-task-item"><strong>Healthy now</strong><span>' + escapeHTML(
ready.length > 0 ? "Active workspaces look clear for routine use." : entries.length > 0 ? "No active workspace is ready for routine use right now." : "There is no hosted workspace waiting for review yet."
) + '</span></div><div class="overview-task-item"><strong>' + escapeHTML(suspendedCount > 0 ? "Suspended stays parked" : billingSeparation.title) + "</strong><span>" + escapeHTML(
suspendedCount > 0 ? String(suspendedCount) + " suspended workspace" + (suspendedCount === 1 ? " stays" : "s stay") + " out of the way until you deliberately resume it." : billingSeparation.copy
return '<article class="overview-task-card"><div class="account-panel-kicker">Needs attention</div><h4>' + escapeHTML(accounts.length > 0 ? reviewWorkspaceHeadline(0) : "0 hosted workspaces need review") + "</h4><p>" + escapeHTML(
entries.length > 0 ? "No active workspace is failed or waiting on a completed health check." : accounts.length > 0 ? "No hosted workspace is attached to this account yet." : "No hosted account is attached to this sign-in."
) + '</p><div class="overview-task-list"><div class="overview-task-item"><strong>Ready</strong><span>' + escapeHTML(
entries.length > 0 ? readyWorkspaceHeadline(ready.length) : accounts.length > 0 ? readyWorkspaceHeadline(0) : "0 hosted workspaces are ready to use"
) + '</span></div><div class="overview-task-item"><strong>Suspended</strong><span>' + escapeHTML(
suspendedCount > 0 ? suspendedCount === 1 ? "1 workspace is suspended and excluded from routine use until you resume it." : String(suspendedCount) + " workspaces are suspended and excluded from routine use until you resume them." : "0 suspended workspaces."
) + "</span></div></div></article>";
}
return '<article class="overview-task-card overview-task-card-attention"><div class="account-panel-kicker">Needs attention</div><h4>Review these first</h4><p>These workspaces still need a human check before you treat the account as settled.</p><div class="overview-task-list">' + attention.slice(0, 3).map(function(entry) {
return '<article class="overview-task-card overview-task-card-attention"><div class="account-panel-kicker">Needs attention</div><h4>' + escapeHTML(reviewWorkspaceHeadline(attention.length)) + '</h4><p>Each listed workspace is failed or still waiting on a completed health check.</p><div class="overview-task-list">' + attention.slice(0, 3).map(function(entry) {
return '<div class="overview-task-item"><strong>' + escapeHTML(entry.workspace.display_name) + "</strong><span>" + escapeHTML(overviewWorkspaceContext(entry, includeAccountName, workspaceStatusCopy(entry.workspace))) + "</span></div>";
}).join("") + "</div></article>";
}
@@ -2258,14 +2232,17 @@
var canManageHosted = accounts.some(function(account) {
return account.can_manage;
});
var suspendedCount = countWorkspacesByState(entries.map(function(entry) {
return entry.workspace;
}), "suspended");
if (!ready.length) {
return '<article class="overview-task-card"><div class="account-panel-kicker">Ready</div><h4>' + escapeHTML(
!accounts.length ? "Billing tools are ready" : totalWorkspaces > 0 ? "No workspace is ready yet" : "Nothing is ready yet"
!accounts.length ? "Billing is available" : readyWorkspaceHeadline(0)
) + "</h4><p>" + escapeHTML(
!accounts.length ? "Use Billing for self-hosted subscriptions, licenses, refunds, and privacy requests." : totalWorkspaces > 0 ? "Use Workspaces to review current state before you start routine work." : canManageHosted ? "The first hosted workspace still needs to be created before routine work can start." : "An owner or admin still needs to create the first hosted workspace before routine work can start."
!accounts.length ? "Use Billing for self-hosted subscriptions, licenses, refunds, and privacy requests." : totalWorkspaces > 0 ? suspendedCount === totalWorkspaces ? "Every hosted workspace is suspended right now." : "Open Workspaces for the current workspace state before routine use." : canManageHosted ? "The first hosted workspace still needs to be created before routine work can start." : "An owner or admin still needs to create the first hosted workspace before routine work can start."
) + "</p></article>";
}
return '<article class="overview-task-card"><div class="account-panel-kicker">Ready</div><h4>Open and work</h4><p>These workspaces are active and healthy right now.</p><div class="overview-task-list">' + ready.slice(0, 3).map(function(entry) {
return '<article class="overview-task-card"><div class="account-panel-kicker">Ready</div><h4>' + escapeHTML(readyWorkspaceHeadline(ready.length)) + '</h4><p>Each listed workspace is active and passed its latest health check.</p><div class="overview-task-list">' + ready.slice(0, 3).map(function(entry) {
return '<div class="overview-task-item overview-task-item-action"><div class="overview-task-copy"><strong>' + escapeHTML(entry.workspace.display_name) + "</strong><span>" + escapeHTML(overviewWorkspaceContext(entry, includeAccountName, workspaceRowNote(entry.workspace))) + "</span></div>" + renderWorkspaceHandoffForm(entry.account.id, entry.workspace.id, accountAPIBasePath, "Open workspace") + "</div>";
}).join("") + "</div></article>";
}
@@ -2347,11 +2324,11 @@
var chips = accounts.length ? [
accounts.length === 1 ? "1 account" : String(accounts.length) + " accounts",
workspaceCountLabel(totalCount),
String(readyCount) + " ready",
attentionCount > 0 ? String(attentionCount) + " attention" : "Nothing urgent",
suspendedCount > 0 ? String(suspendedCount) + " suspended" : "No suspended"
] : ["No hosted account", "Billing available", "Support only on escalation"];
return '<section class="account-content-panel account-content-panel-overview"><div class="account-stage-header account-stage-header-overview overview-stage-header"><div><div class="account-panel-kicker">Overview</div><h3>Account triage</h3><p>Only three questions matter here.</p>' + renderSectionContextChips(chips) + '</div></div><div class="overview-task-grid">' + renderOverviewAttentionCard(accounts, entries, showSelfHostedCommercial) + renderOverviewReadyCard(accounts, entries, context.accountAPIBasePath) + renderOverviewNextActionCard(accounts, entries, context.accountAPIBasePath, showSelfHostedCommercial) + "</div></section>";
readyWorkspaceChipLabel(readyCount),
reviewWorkspaceChipLabel(attentionCount),
suspendedWorkspaceChipLabel(suspendedCount)
] : ["No hosted account", "0 hosted workspaces", "Billing available", "Support only on escalation"];
return '<section class="account-content-panel account-content-panel-overview"><div class="account-stage-header account-stage-header-overview overview-stage-header"><div><div class="account-panel-kicker">Overview</div><h3>Account state</h3><p>Current hosted workspace state, readiness, and next action.</p>' + renderSectionContextChips(chips) + '</div></div><div class="overview-task-grid">' + renderOverviewAttentionCard(accounts, entries, showSelfHostedCommercial) + renderOverviewReadyCard(accounts, entries, context.accountAPIBasePath) + renderOverviewNextActionCard(accounts, entries, context.accountAPIBasePath, showSelfHostedCommercial) + "</div></section>";
}
function renderNoHostedWorkspacesSection() {
return '<section class="account-content-panel account-content-panel-workspaces"><div class="account-stage-header"><div><div class="account-panel-kicker">Workspaces</div><h3>Workspaces</h3><p>No hosted workspace is attached to this account.</p>' + renderSectionContextChips(["None attached", "Billing instead"]) + '</div></div><div class="empty-state empty-state-spaced"><p>There is nothing to open or manage here yet.</p><p class="support-copy">Use Billing for self-hosted subscriptions, licenses, refunds, or privacy requests.</p></div></section>';
@@ -76,11 +76,12 @@ describe('shell view', function() {
it('renders empty accounts state with support contact', function() {
var html = renderAccountsHTML(createContext());
expect(html).toContain('Account triage');
expect(html).toContain('Account state');
expect(html).toContain('No hosted account');
expect(html).toContain('0 hosted workspaces');
expect(html).toContain('Billing available');
expect(html).toContain('Nothing urgent');
expect(html).toContain('Billing tools are ready');
expect(html).toContain('0 hosted workspaces need review');
expect(html).toContain('Billing is available');
expect(html).toContain('Open billing');
});
@@ -138,8 +139,8 @@ describe('shell view', function() {
expect(html).toContain('What needs attention, what is ready, and the next obvious action.');
expect(html).toContain('1 account');
expect(html).toContain('3 workspaces');
expect(html).toContain('1 ready');
expect(html).toContain('2 attention');
expect(html).toContain('1 ready workspace');
expect(html).toContain('2 workspaces to review');
expect(html).toContain('Manage');
expect(html).toContain('id="billing-section"');
expect(html).toContain('portal-account-context');
@@ -154,18 +155,18 @@ describe('shell view', function() {
expect(html).toContain('Create workspace');
expect(html).not.toContain('Manage billing');
expect(html).not.toContain('Manage team');
expect(html).toContain('Account triage');
expect(html).toContain('Only three questions matter here.');
expect(html).toContain('Account state');
expect(html).toContain('Current hosted workspace state, readiness, and next action.');
expect(html).toContain('section-context-strip');
expect(html).toContain('Needs attention');
expect(html).toContain('Ready');
expect(html).toContain('Next action');
expect(html).toContain('Review these first');
expect(html).toContain('Open and work');
expect(html).toContain('2 workspaces need review');
expect(html).toContain('1 workspace is ready to use');
expect(html).toContain('Review workspaces');
expect(html).toContain('Review access');
expect(html).toContain('account-stage-header-actions');
expect(html).toContain('No suspended');
expect(html).toContain('0 suspended workspaces');
expect(html).toContain('Alpha Workspace');
expect(html).toContain('Beta Workspace');
expect(html).toContain('Gamma Workspace');
@@ -174,8 +175,8 @@ describe('shell view', function() {
expect(html).toContain('Needs attention</span>');
expect(html).toContain('Checking</span>');
expect(html).toContain('Ready to use');
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('This workspace is in a failed state.');
expect(html).toContain('Latest health check is still pending.');
expect(html).toContain('/api/accounts/acct_1/tenants/ws_active/handoff');
expect(html).toContain('Open workspace');
expect(html).toContain('data-action="select-workspace"');
@@ -387,8 +388,8 @@ describe('shell view', function() {
expect(html).toContain('Hosted account where you can open workspaces and review who already has access. An owner or admin handles access changes and billing.');
expect(html).toContain('View only');
expect(html).toContain('Owner/admin required');
expect(html).toContain('Hosted billing stays separate');
expect(html).toContain('Hosted billing stays in Billing, and an owner or admin must open it.');
expect(html).toContain('0 workspaces need review');
expect(html).toContain('1 workspace is ready to use');
expect(html).toContain('Review access');
expect(html).toContain('Owner or admin required');
expect(html).toContain('Review who already has access to this hosted account. An owner or admin must make changes.');
@@ -506,9 +507,9 @@ describe('shell view', function() {
})
);
expect(html).toContain('Nothing is ready yet');
expect(html).toContain('0 workspaces are ready to use');
expect(html).toContain('An owner or admin still needs to create the first hosted workspace before routine work can start.');
expect(html).not.toContain('Use Workspaces to review current state before you start routine work.');
expect(html).not.toContain('Open Workspaces for the current workspace state before routine use.');
});
it('keeps ready state honest for managed hosted accounts with no workspace yet', function() {
@@ -532,9 +533,9 @@ describe('shell view', function() {
})
);
expect(html).toContain('Nothing is ready yet');
expect(html).toContain('0 workspaces are ready to use');
expect(html).toContain('The first hosted workspace still needs to be created before routine work can start.');
expect(html).not.toContain('Use Workspaces to review current state before you start routine work.');
expect(html).not.toContain('Open Workspaces for the current workspace state before routine use.');
});
it('keeps next action on review surfaces for suspended hosted view-only accounts', function() {
@@ -602,8 +603,8 @@ describe('shell view', function() {
})
);
expect(html).toContain('No active workspace is ready for routine use right now.');
expect(html).toContain('1 suspended workspace stays out of the way until you deliberately resume it.');
expect(html).toContain('0 workspaces are ready to use');
expect(html).toContain('1 workspace is suspended and excluded from routine use until you resume it.');
expect(html).not.toContain('Active workspaces look clear for routine use.');
});
@@ -619,14 +620,15 @@ describe('shell view', function() {
expect(html).toContain('Pulse Account');
expect(html).toContain('Account tasks');
expect(html).toContain('Self-hosted');
expect(html).toContain('Account triage');
expect(html).toContain('Account state');
expect(html).toMatch(/Workspaces[\s\S]*Unavailable on this account\. Hosted workspaces are not attached here\./);
expect(html).toContain('Unavailable on this account. Hosted workspaces are not attached here.');
expect(html).toMatch(/Access[\s\S]*Unavailable on this account\. Hosted roster and role controls live only on hosted workspace accounts\./);
expect(html).toContain('Unavailable on this account. Hosted roster and role controls live only on hosted workspace accounts.');
expect(html).toMatch(/Support[\s\S]*Escalation only after the billing path is exhausted\./);
expect(html).toContain('No hosted account');
expect(html).toContain('Billing tools are ready');
expect(html).toContain('0 hosted workspaces need review');
expect(html).toContain('Billing is available');
expect(html).toContain('There is nothing to open or manage here yet.');
expect(html).toContain('There are no hosted roles or invites to manage for this account right now.');
expect(html).toContain('Use this billing surface only for self-hosted subscriptions, licenses, refunds, and privacy requests.');
@@ -709,10 +711,11 @@ describe('shell view', function() {
);
expect(html).toContain('Suspended until you resume it');
expect(html).toContain('Nothing urgent');
expect(html).toContain('No workspace is ready yet');
expect(html).toContain('0 workspaces need review');
expect(html).toContain('0 workspaces are ready to use');
expect(html).toContain('Every hosted workspace is suspended right now.');
expect(html).toContain('Create the next workspace');
expect(html).toContain('Suspended stays parked');
expect(html).toContain('1 workspace is suspended and excluded from routine use until you resume it.');
});
it('preserves the high-density grid, standard sidebar hooks, and strictly horizontal, pill-free action constraints in the rendered shell', function() {
@@ -69,6 +69,26 @@ function workspaceCountLabel(count: number): string {
return count === 1 ? '1 workspace' : String(count) + ' workspaces';
}
function reviewWorkspaceHeadline(count: number): string {
return count === 1 ? '1 workspace needs review' : String(count) + ' workspaces need review';
}
function readyWorkspaceHeadline(count: number): string {
return count === 1 ? '1 workspace is ready to use' : String(count) + ' workspaces are ready to use';
}
function reviewWorkspaceChipLabel(count: number): string {
return count === 1 ? '1 workspace to review' : String(count) + ' workspaces to review';
}
function readyWorkspaceChipLabel(count: number): string {
return count === 1 ? '1 ready workspace' : String(count) + ' ready workspaces';
}
function suspendedWorkspaceChipLabel(count: number): string {
return count === 1 ? '1 suspended workspace' : String(count) + ' suspended workspaces';
}
function hasHostedAccounts(accounts: PortalAccountSummary[]): boolean {
return accounts.length > 0;
}
@@ -188,11 +208,11 @@ function renderSectionContextChips(chips: string[]): string {
function workspaceStatusCopy(workspace: PortalWorkspaceSummary): string {
var status = workspaceHealthState(workspace);
var state = String(workspace.state || '');
if (state === 'suspended') return 'This workspace is suspended and will stay closed until you resume it.';
if (state === 'failed') return 'This workspace needs attention before it is trustworthy.';
if (status === 'healthy') return 'Live updates and health checks are currently good.';
if (status === 'unhealthy') return 'This workspace needs attention before it is trustworthy.';
return 'This workspace is still waiting on a completed health check.';
if (state === 'suspended') return 'This workspace is suspended.';
if (state === 'failed') return 'This workspace is in a failed state.';
if (status === 'healthy') return 'Latest health check is healthy.';
if (status === 'unhealthy') return 'Latest health check is unhealthy.';
return 'Latest health check is still pending.';
}
function workspaceRowNote(workspace: PortalWorkspaceSummary): string {
@@ -629,26 +649,29 @@ function renderOverviewAttentionCard(
var suspendedCount = countWorkspacesByState(entries.map(function(entry) {
return entry.workspace;
}), 'suspended');
var billingSeparation = overviewBillingSeparationCopy(accounts, showSelfHostedCommercial);
if (!attention.length) {
return (
'<article class="overview-task-card">' +
'<div class="account-panel-kicker">Needs attention</div>' +
'<h4>Nothing urgent</h4>' +
'<h4>' + escapeHTML(accounts.length > 0 ? reviewWorkspaceHeadline(0) : '0 hosted workspaces need review') + '</h4>' +
'<p>' + escapeHTML(entries.length > 0
? 'No active workspace is currently asking for review.'
: 'No hosted workspace is currently asking for review.'
? 'No active workspace is failed or waiting on a completed health check.'
: accounts.length > 0
? 'No hosted workspace is attached to this account yet.'
: 'No hosted account is attached to this sign-in.'
) + '</p>' +
'<div class="overview-task-list">' +
'<div class="overview-task-item"><strong>Healthy now</strong><span>' + escapeHTML(ready.length > 0
? 'Active workspaces look clear for routine use.'
: entries.length > 0
? 'No active workspace is ready for routine use right now.'
: 'There is no hosted workspace waiting for review yet.'
'<div class="overview-task-item"><strong>Ready</strong><span>' + escapeHTML(entries.length > 0
? readyWorkspaceHeadline(ready.length)
: accounts.length > 0
? readyWorkspaceHeadline(0)
: '0 hosted workspaces are ready to use'
) + '</span></div>' +
'<div class="overview-task-item"><strong>' + escapeHTML(suspendedCount > 0 ? 'Suspended stays parked' : billingSeparation.title) + '</strong><span>' + escapeHTML(suspendedCount > 0
? String(suspendedCount) + ' suspended workspace' + (suspendedCount === 1 ? ' stays' : 's stay') + ' out of the way until you deliberately resume it.'
: billingSeparation.copy
'<div class="overview-task-item"><strong>Suspended</strong><span>' + escapeHTML(suspendedCount > 0
? suspendedCount === 1
? '1 workspace is suspended and excluded from routine use until you resume it.'
: String(suspendedCount) + ' workspaces are suspended and excluded from routine use until you resume them.'
: '0 suspended workspaces.'
) + '</span></div>' +
'</div>' +
'</article>'
@@ -658,8 +681,8 @@ function renderOverviewAttentionCard(
return (
'<article class="overview-task-card overview-task-card-attention">' +
'<div class="account-panel-kicker">Needs attention</div>' +
'<h4>Review these first</h4>' +
'<p>These workspaces still need a human check before you treat the account as settled.</p>' +
'<h4>' + escapeHTML(reviewWorkspaceHeadline(attention.length)) + '</h4>' +
'<p>Each listed workspace is failed or still waiting on a completed health check.</p>' +
'<div class="overview-task-list">' +
attention.slice(0, 3).map(function(entry) {
return (
@@ -685,20 +708,23 @@ function renderOverviewReadyCard(
var canManageHosted = accounts.some(function(account) {
return account.can_manage;
});
var suspendedCount = countWorkspacesByState(entries.map(function(entry) {
return entry.workspace;
}), 'suspended');
if (!ready.length) {
return (
'<article class="overview-task-card">' +
'<div class="account-panel-kicker">Ready</div>' +
'<h4>' + escapeHTML(!accounts.length
? 'Billing tools are ready'
: totalWorkspaces > 0
? 'No workspace is ready yet'
: 'Nothing is ready yet'
? 'Billing is available'
: readyWorkspaceHeadline(0)
) + '</h4>' +
'<p>' + escapeHTML(!accounts.length
? 'Use Billing for self-hosted subscriptions, licenses, refunds, and privacy requests.'
: totalWorkspaces > 0
? 'Use Workspaces to review current state before you start routine work.'
? suspendedCount === totalWorkspaces
? 'Every hosted workspace is suspended right now.'
: 'Open Workspaces for the current workspace state before routine use.'
: canManageHosted
? 'The first hosted workspace still needs to be created before routine work can start.'
: 'An owner or admin still needs to create the first hosted workspace before routine work can start.'
@@ -710,8 +736,8 @@ function renderOverviewReadyCard(
return (
'<article class="overview-task-card">' +
'<div class="account-panel-kicker">Ready</div>' +
'<h4>Open and work</h4>' +
'<p>These workspaces are active and healthy right now.</p>' +
'<h4>' + escapeHTML(readyWorkspaceHeadline(ready.length)) + '</h4>' +
'<p>Each listed workspace is active and passed its latest health check.</p>' +
'<div class="overview-task-list">' +
ready.slice(0, 3).map(function(entry) {
return (
@@ -840,19 +866,19 @@ function renderShellOverviewSection(context: ShellViewContext): string {
? [
accounts.length === 1 ? '1 account' : String(accounts.length) + ' accounts',
workspaceCountLabel(totalCount),
String(readyCount) + ' ready',
attentionCount > 0 ? String(attentionCount) + ' attention' : 'Nothing urgent',
suspendedCount > 0 ? String(suspendedCount) + ' suspended' : 'No suspended',
readyWorkspaceChipLabel(readyCount),
reviewWorkspaceChipLabel(attentionCount),
suspendedWorkspaceChipLabel(suspendedCount),
]
: ['No hosted account', 'Billing available', 'Support only on escalation'];
: ['No hosted account', '0 hosted workspaces', 'Billing available', 'Support only on escalation'];
return (
'<section class="account-content-panel account-content-panel-overview">' +
'<div class="account-stage-header account-stage-header-overview overview-stage-header">' +
'<div>' +
'<div class="account-panel-kicker">Overview</div>' +
'<h3>Account triage</h3>' +
'<p>Only three questions matter here.</p>' +
'<h3>Account state</h3>' +
'<p>Current hosted workspace state, readiness, and next action.</p>' +
renderSectionContextChips(chips) +
'</div>' +
'</div>' +