diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json
index a3864c0737..1666413e3a 100644
--- a/internal/cloudcp/portal/dist/build_manifest.json
+++ b/internal/cloudcp/portal/dist/build_manifest.json
@@ -1,5 +1,5 @@
{
- "source_hash": "0401fbaf26ce16d3985c1cca3d6116f9fc2f3cb72e756d1bb8bca91d0d8633e2",
+ "source_hash": "46e32b3f10be76cc7ecaf28e84ea41b97ba549984641bbc401f9b9cd5484880a",
"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 7ce8fca438..6164526aae 100644
--- a/internal/cloudcp/portal/dist/portal_app.css
+++ b/internal/cloudcp/portal/dist/portal_app.css
@@ -912,10 +912,29 @@ header .logout-btn:hover,
padding: 14px 16px;
background: #fff;
border-top: 1px solid var(--line);
+ transition:
+ border-color 140ms ease,
+ background 140ms ease,
+ box-shadow 140ms ease;
}
.workspace-row:first-child {
border-top: none;
}
+.workspace-row:hover {
+ background: #fbfcfe;
+}
+.workspace-row.selected {
+ position: relative;
+ border-color: rgba(11, 106, 114, 0.34);
+ background: #eef8fa;
+ box-shadow: inset 4px 0 0 var(--accent), 0 0 0 1px rgba(11, 106, 114, 0.12);
+}
+.workspace-row.selected .workspace-name {
+ color: var(--accent-strong);
+}
+.workspace-row.selected .workspace-row-note {
+ color: var(--ink);
+}
.workspace-row-primary {
display: flex;
flex-direction: column;
@@ -1066,6 +1085,30 @@ header .logout-btn:hover,
font-size: 12px;
line-height: 1.5;
}
+.workspace-management-empty-checklist {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.workspace-management-empty-check {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 12px;
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ background: rgba(255, 255, 255, 0.78);
+}
+.workspace-management-empty-check strong {
+ font-size: 13px;
+ line-height: 1.3;
+ color: var(--ink);
+}
+.workspace-management-empty-check span {
+ font-size: 12px;
+ line-height: 1.5;
+ color: var(--ink-soft);
+}
.workspace-management-empty-actions {
display: flex;
gap: 10px;
diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js
index d2e4ae10dd..a9a3dc13f3 100644
--- a/internal/cloudcp/portal/dist/portal_app.js
+++ b/internal/cloudcp/portal/dist/portal_app.js
@@ -114,6 +114,10 @@
if (!empty || !content || !title || !meta || !summary || !health || !lifecycle || !created || !guidance || !actionButton || !closeButton) return;
var workspace = entry.selectedWorkspaceID ? findWorkspace(account, entry.selectedWorkspaceID) : null;
var hasSelection = !!workspace;
+ var rows = document.querySelectorAll("[data-workspace-row]");
+ for (var i = 0; i < rows.length; i += 1) {
+ rows[i].classList.toggle("selected", !!workspace && rows[i].getAttribute("data-workspace-row") === workspace.id);
+ }
panel.classList.add("visible");
empty.hidden = hasSelection;
content.hidden = !hasSelection;
@@ -1805,7 +1809,7 @@
if (account.can_manage && (state === "active" || state === "suspended" || state === "failed")) {
manageAction = '';
}
- return '' + escapeHTML(workspace.display_name) + '
' + metaParts.join("") + '
' + escapeHTML(workspaceRowNote(workspace)) + '
' + healthBadgeHTML(workspace) + '
' + escapeHTML(titleCase(state || "Unknown")) + '
' + openAction + manageAction + "
";
+ return '' + escapeHTML(workspace.display_name) + '
' + metaParts.join("") + '
' + escapeHTML(workspaceRowNote(workspace)) + '
' + healthBadgeHTML(workspace) + '
' + escapeHTML(titleCase(state || "Unknown")) + '
' + openAction + manageAction + "
";
}
function renderAccountOverviewSection(account) {
var workspaces = Array.isArray(account.workspaces) ? account.workspaces : [];
@@ -1844,7 +1848,7 @@
workspaceDeskActions += '';
}
workspaceDeskActions += '';
- workspaceManagement = 'Choose a workspace from the fleet to open its management desk.
Account desk
Keep account-wide actions close
Use the management desk for workspace-level decisions, and keep billing or team changes one click away.
' + workspaceDeskActions + '
What shows here
- Lifecycle and health summary for the selected workspace
- The next explicit lifecycle action for that workspace
- Account-level actions when you need billing or team changes instead
';
+ workspaceManagement = 'Choose a workspace from the fleet to open its management desk.
Account desk
Keep account-wide actions close
Use the desk for one workspace at a time while keeping billing, team, and create-workspace actions within reach.
' + workspaceDeskActions + '
Review flow
1. Pick the workspaceSelect a fleet row to bring its health, lifecycle, and next action into this desk.
2. Check posture firstTreat checking or needs-attention workspaces as review work, not routine operations.
3. Escalate account changes separatelyBilling and team changes stay account-wide even when you are focused on one workspace.
';
}
var workspaceHTML = workspaces.length ? '
WorkspaceHealthLifecycleActions
' + workspaces.map(function(workspace) {
return renderWorkspaceCard(account, workspace, accountAPIBasePath);
diff --git a/internal/cloudcp/portal/frontend/src/account_view.ts b/internal/cloudcp/portal/frontend/src/account_view.ts
index b96f024eb5..73825a08f2 100644
--- a/internal/cloudcp/portal/frontend/src/account_view.ts
+++ b/internal/cloudcp/portal/frontend/src/account_view.ts
@@ -136,6 +136,10 @@ export function renderWorkspaceManagement(account: PortalAccountSummary, entry:
var workspace = entry.selectedWorkspaceID ? findWorkspace(account, entry.selectedWorkspaceID) : null;
var hasSelection = !!workspace;
+ var rows = document.querySelectorAll
('[data-workspace-row]');
+ for (var i = 0; i < rows.length; i += 1) {
+ rows[i].classList.toggle('selected', !!workspace && rows[i].getAttribute('data-workspace-row') === workspace.id);
+ }
panel.classList.add('visible');
empty.hidden = hasSelection;
content.hidden = !hasSelection;
diff --git a/internal/cloudcp/portal/frontend/src/shell_view.test.ts b/internal/cloudcp/portal/frontend/src/shell_view.test.ts
index 25896acb2a..38d1486a9c 100644
--- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts
+++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts
@@ -166,8 +166,9 @@ describe('shell view', function() {
expect(html).toContain('data-action="select-workspace"');
expect(html).toContain('Workspace management');
expect(html).toContain('Choose a workspace from the fleet to open its management desk.');
- expect(html).toContain('What shows here');
- expect(html).toContain('Lifecycle and health summary for the selected workspace');
+ expect(html).toContain('Review flow');
+ expect(html).toContain('Pick the workspace');
+ expect(html).toContain('Escalate account changes separately');
expect(html).toContain('data-action="clear-workspace-selection"');
expect(html).toContain('Team management');
expect(html).toContain('Invite someone new');
diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts
index 2897cc2e90..cbffc5bc81 100644
--- a/internal/cloudcp/portal/frontend/src/shell_view.ts
+++ b/internal/cloudcp/portal/frontend/src/shell_view.ts
@@ -309,7 +309,7 @@ function renderWorkspaceCard(account: PortalAccountSummary, workspace: PortalWor
}
return (
- '' +
+ '' +
'' +
'
' +
'
' + escapeHTML(workspace.display_name) + '
' +
@@ -511,16 +511,16 @@ function renderAccountWorkspaceSection(account: PortalAccountSummary, accountAPI
'
' +
'
Account desk
' +
'
Keep account-wide actions close
' +
- '
Use the management desk for workspace-level decisions, and keep billing or team changes one click away.
' +
+ '
Use the desk for one workspace at a time while keeping billing, team, and create-workspace actions within reach.
' +
'
' + workspaceDeskActions + '
' +
'
' +
'
' +
- '
What shows here
' +
- '
' +
- '- Lifecycle and health summary for the selected workspace
' +
- '- The next explicit lifecycle action for that workspace
' +
- '- Account-level actions when you need billing or team changes instead
' +
- '
' +
+ '
Review flow
' +
+ '
' +
+ '
1. Pick the workspaceSelect a fleet row to bring its health, lifecycle, and next action into this desk.
' +
+ '
2. Check posture firstTreat checking or needs-attention workspaces as review work, not routine operations.
' +
+ '
3. Escalate account changes separatelyBilling and team changes stay account-wide even when you are focused on one workspace.
' +
+ '
' +
'
' +
'
' +
'
' +
diff --git a/internal/cloudcp/portal/frontend/src/styles.css b/internal/cloudcp/portal/frontend/src/styles.css
index e0649ddf1b..3437f513e4 100644
--- a/internal/cloudcp/portal/frontend/src/styles.css
+++ b/internal/cloudcp/portal/frontend/src/styles.css
@@ -1017,12 +1017,32 @@ header .logout-btn:hover,
padding: 14px 16px;
background: #fff;
border-top: 1px solid var(--line);
+ transition: border-color 140ms ease, background 140ms ease, box-shadow 140ms ease;
}
.workspace-row:first-child {
border-top: none;
}
+.workspace-row:hover {
+ background: #fbfcfe;
+}
+
+.workspace-row.selected {
+ position: relative;
+ border-color: rgba(11, 106, 114, 0.34);
+ background: #eef8fa;
+ box-shadow: inset 4px 0 0 var(--accent), 0 0 0 1px rgba(11, 106, 114, 0.12);
+}
+
+.workspace-row.selected .workspace-name {
+ color: var(--accent-strong);
+}
+
+.workspace-row.selected .workspace-row-note {
+ color: var(--ink);
+}
+
.workspace-row-primary {
display: flex;
flex-direction: column;
@@ -1198,6 +1218,34 @@ header .logout-btn:hover,
line-height: 1.5;
}
+.workspace-management-empty-checklist {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.workspace-management-empty-check {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 12px;
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ background: rgba(255, 255, 255, 0.78);
+}
+
+.workspace-management-empty-check strong {
+ font-size: 13px;
+ line-height: 1.3;
+ color: var(--ink);
+}
+
+.workspace-management-empty-check span {
+ font-size: 12px;
+ line-height: 1.5;
+ color: var(--ink-soft);
+}
+
.workspace-management-empty-actions {
display: flex;
gap: 10px;