Keep portal overview ready state honest

This commit is contained in:
rcourtman 2026-03-28 22:40:32 +00:00
parent 4c41103cf0
commit 59bf0c9cee
7 changed files with 89 additions and 7 deletions

View file

@ -244,6 +244,11 @@ Core rules:
not leak raw transport strings such as `Network error.` into `Access`,
`Workspaces`, or `Billing`; each failure must stay on the task-specific
action the user was trying to complete.
36. `Overview` must keep `Ready` honest when no hosted workspace exists yet.
Hosted accounts with zero workspaces may not tell the user to review
current workspace state; they must say that nothing is ready yet and that
the first hosted workspace still needs owner/admin creation before routine
work can start.
## Screen Model

View file

@ -337,6 +337,10 @@ That same shared request/runtime boundary must also preserve task-specific
failure copy on transport errors: portal job surfaces may not leak raw strings
such as `Network error.`, and must instead surface the owned fallback for the
exact action that failed.
That same typed overview contract must also keep `Ready` honest when no hosted
workspace exists yet: hosted accounts with zero workspaces may not route the
user into current workspace review, and must instead render that nothing is
ready until the first hosted workspace exists.
plus a package-local `tsc --noEmit` gate, so future account-shell work should
extend the typed source boundary instead of reviving opaque global runtime
objects, document-wide render events, or untyped embedded asset edits.

View file

@ -898,6 +898,10 @@ That same owned task surface must also keep failure copy on the user job
instead of leaking raw transport wording: `Access`, `Workspaces`, and
`Billing` failures must render the task-specific action that could not
complete, not generic copy such as `Network error.`.
That same owned `Overview` surface must also keep `Ready` honest when no
hosted workspace exists yet: hosted accounts with zero workspaces may not tell
the user to review current workspace state, and must instead say that nothing
is ready yet until the first hosted workspace exists.
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

View file

@ -1,5 +1,5 @@
{
"source_hash": "d515bbe22da7a6154237402934adaefe1e896687914a72d8bf36587689abf31a",
"source_hash": "2b04882678dc1fd7fd9160747d190ba62581f1c44ab3a6632f04a0c55756b78f",
"build_inputs": [
"package.json",
"tsconfig.json",

View file

@ -2232,9 +2232,15 @@
function renderOverviewReadyCard(accounts, entries, accountAPIBasePath) {
var ready = readyOverviewEntries(entries);
var includeAccountName = accounts.length > 1;
var totalWorkspaces = countWorkspaces(accounts);
var canManageHosted = accounts.some(function(account) {
return account.can_manage;
});
if (!ready.length) {
return '<article class="overview-task-card"><div class="account-panel-kicker">Ready</div><h4>' + escapeHTML(accounts.length > 0 ? "No workspace is ready yet" : "Billing tools are ready") + "</h4><p>" + escapeHTML(
accounts.length > 0 ? "Use Workspaces to review current state before you start routine work." : "Use Billing for self-hosted subscriptions, licenses, refunds, and privacy requests."
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"
) + "</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."
) + "</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) {

View file

@ -478,6 +478,56 @@ describe('shell view', function() {
expect(html).not.toContain('If this is an access change, go to Access. If it is a billing or license issue, go to Billing. Support is only for escalation.');
});
it('keeps ready state honest for hosted view-only accounts with no workspace yet', function() {
var html = renderAuthenticatedPortalHTML(
createContext({
bootstrap: createBootstrap({
accounts: [
{
id: 'acct_view_empty',
name: 'Empty Hosted Account',
kind: 'cloud',
kind_label: 'Cloud',
role: 'read_only',
can_manage: false,
has_billing: true,
workspaces: [],
},
],
}),
})
);
expect(html).toContain('Nothing is ready yet');
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.');
});
it('keeps ready state honest for managed hosted accounts with no workspace yet', function() {
var html = renderAuthenticatedPortalHTML(
createContext({
bootstrap: createBootstrap({
accounts: [
{
id: 'acct_manage_empty',
name: 'Managed Empty Account',
kind: 'msp',
kind_label: 'MSP',
role: 'owner',
can_manage: true,
has_billing: true,
workspaces: [],
},
],
}),
})
);
expect(html).toContain('Nothing is ready yet');
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.');
});
it('keeps next action on review surfaces for suspended hosted view-only accounts', function() {
var html = renderAuthenticatedPortalHTML(
createContext({

View file

@ -678,14 +678,27 @@ function renderOverviewReadyCard(
): string {
var ready = readyOverviewEntries(entries);
var includeAccountName = accounts.length > 1;
var totalWorkspaces = countWorkspaces(accounts);
var canManageHosted = accounts.some(function(account) {
return account.can_manage;
});
if (!ready.length) {
return (
'<article class="overview-task-card">' +
'<div class="account-panel-kicker">Ready</div>' +
'<h4>' + escapeHTML(accounts.length > 0 ? 'No workspace is ready yet' : 'Billing tools are ready') + '</h4>' +
'<p>' + escapeHTML(accounts.length > 0
? 'Use Workspaces to review current state before you start routine work.'
: 'Use Billing for self-hosted subscriptions, licenses, refunds, and privacy requests.'
'<h4>' + escapeHTML(!accounts.length
? 'Billing tools are ready'
: totalWorkspaces > 0
? 'No workspace is ready yet'
: 'Nothing is ready yet'
) + '</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.'
) + '</p>' +
'</article>'
);