mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 22:12:23 +00:00
Make portal overview fact-first
This commit is contained in:
@@ -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
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"source_hash": "ec5ecce14e80d4018df77d407516b38b7c0c9ab4ca502f9da79f4b25e75219a4",
|
||||
"source_hash": "90be82b1835d65ae5df2bf6c5793071dd69a2cb4f9ee855356e9ce0e375f7ac1",
|
||||
"build_inputs": [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
|
||||
+38
-61
@@ -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>' +
|
||||
|
||||
Reference in New Issue
Block a user