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 6fdffcd9e..7cd841e40 100644
--- a/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md
+++ b/docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md
@@ -103,21 +103,26 @@ Pulse's internal product model.
Primary areas:
-1. `Overview`
- A short triage surface that answers only three questions: what needs
- attention, what is ready, and what the next obvious action is.
-2. `Workspaces`
+1. `Workspaces`
Hosted workspace list, health summary, open-workspace handoff, lifecycle
actions, and create-workspace entry points.
-3. `Access`
+2. `Access`
Account roster, invites, role changes, and access removal.
-4. `Billing`
+3. `Billing`
Hosted billing state and Stripe billing handoff when relevant. Self-hosted
license retrieval, refunds, privacy/data actions, and related commercial
utilities appear here only when the account has those needs.
-5. `Support`
+
+Secondary utilities:
+
+1. `Support`
Escalation path only. It must not compete with primary account tasks in the
main shell.
+2. Precise workspace counts
+ Signed-in workspace counts stay inline at the top of `Workspaces`. They
+ report account count, workspace count, ready count, review count, and
+ suspended count directly from runtime truth instead of as a separate
+ `Summary` or `Overview` tab.
Conditional content:
@@ -135,8 +140,9 @@ of utilities.
Core rules:
-1. The first screen must be triage, not a dashboard essay.
-2. The overview should answer only `Needs attention`, `Ready`, and `Next
+1. The first screen must be the first live task, not a dashboard essay or a
+ summary-first landing layer.
+2. The summary should answer only `Needs attention`, `Ready`, and `Next
action`.
3. Top-level navigation must be organized by user jobs, not by Pulse's
internal hosted, license, billing, or support implementation boundaries.
@@ -166,10 +172,12 @@ Core rules:
the create-workspace form opens below the fold, the shell must reveal it.
14. Access management must be a visible roster and invite surface, not a table
or control hidden behind unrelated account copy.
-15. Support must remain an escalation path, not a peer destination competing
- with the primary jobs a user came to do.
-16. The signed-in `Overview` must be one shell-level triage surface across the
- current account set, not a repeated per-account dashboard.
+15. Support must remain an escalation utility, not a peer destination
+ competing with the primary jobs a user came to do.
+16. The signed-in shell may not expose a separate `Summary` or `Overview` tab
+ ahead of the real task surfaces. Workspace counts and state belong inline
+ at the top of `Workspaces`, not as a repeated per-account dashboard or a
+ competing first-class destination.
17. Top-level navigation must stay honest to account shape. Hosted-only tasks
that are irrelevant to the current account should be removed from the
primary task row instead of rendered as fake live tabs, and any shared
@@ -233,35 +241,35 @@ Core rules:
and owner/admin handoff paths; `Support` must not imply they can perform
hosted lifecycle, access-mutation, or hosted-billing changes themselves
before escalation.
-33. `Overview` must keep billing cues honest to account shape and permission.
- Hosted-only accounts may not mention self-hosted billing utilities by
- default, and hosted view-only roles must say when owner/admin authority is
- still required to open hosted billing.
+33. Inline workspace counts and shell copy must keep billing cues honest to
+ account shape and permission. Hosted-only accounts may not mention self-
+ hosted billing utilities by default, and hosted view-only roles must say
+ when owner/admin authority is still required to open hosted billing.
34. User-facing role labels must stay on product vocabulary. The portal may
describe account role as `Owner`, `Admin`, `Tech`, or `Read-only`, but it
must not leak internal identifiers such as `read_only` or legacy aliases
such as `member`.
-35. `Overview` must also keep `Next action` permission-honest for hosted
- view-only accounts. When no workspace is ready, the primary route must
- stay on reviewable `Workspaces` or `Access` surfaces before any blocked
- hosted billing or owner/admin-only mutation path.
+35. The signed-in shell must keep the first available action permission-
+ honest for hosted view-only accounts. When no workspace is ready, the
+ primary route must stay on reviewable `Workspaces` or `Access` surfaces
+ before any blocked hosted billing or owner/admin-only mutation path.
36. Task surfaces must keep failure copy on owned user jobs. The portal may
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.
-37. `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.
-38. `Overview` must keep `Needs attention` honest when only suspended
- 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.
+37. Inline workspace counts and workspace-state copy 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.
+38. Inline workspace counts and workspace-state copy must keep suspended-only
+ states honest. 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. Inline workspace counts and shell status copy must stay fact-first. They
+ may not synthesize urgency or health verdicts such as `Nothing urgent` or
+ `Healthy now`; they must report concrete counts and explicit workspace
+ state directly from the owned runtime truth.
40. Portal task and status copy must stay literal. Customer-facing copy may
not rely on commentary such as `obvious`, `actual work`, `trustworthy`,
or `settled` when the runtime already knows the concrete state, action,
@@ -279,11 +287,11 @@ Core rules:
presentation, and hierarchy driven by spacing, typography, and dividers
instead of dashboard chrome, decorative dark rails, nested card stacks,
or dense pill collections competing with the active job.
-42. The signed-in shell must also open on the first live task, not the
- summary layer. Hosted accounts should land in `Workspaces`; self-hosted-
- only accounts should land in `Billing`. `Overview` remains available as a
- precise triage surface, but it must not be the default first impression
- for authenticated users.
+42. The signed-in shell must also open on the first live task, not a summary
+ layer. Hosted accounts should land in `Workspaces`; self-hosted-only
+ accounts should land in `Billing`. The shell must not expose a separate
+ `Overview` or `Summary` tab as the first impression for authenticated
+ users.
43. The signed-in shell must orient the user with one quiet account-context
header and one flat top task row. It must not add a second summary box,
sidebar shell, or badge-heavy frame that competes with the active task.
@@ -312,12 +320,12 @@ The signed-in shell should be treated as four first-class states:
Each signed-in state should render:
-1. a concise overview band that answers `Needs attention`, `Ready`, and `Next
- action`
- The overview band is shell-level triage, not one repeated card per account.
- The overview band remains available from navigation, but authenticated
- users should open on the first live task for the current account shape
- rather than landing here by default.
+1. a concise workspace counts strip at the top of `Workspaces`
+ It reports account count, workspace count, ready count, review count, and
+ suspended count directly from runtime truth. It is not a separate
+ `Summary` or `Overview` tab, and authenticated users should open on the
+ first live task for the current account shape rather than landing on a
+ separate triage page.
2. one quiet account-context header with account kind, role, current account
title, and one short orienting sentence
3. an explicit `Workspaces` area for open, create, and lifecycle actions
@@ -455,8 +463,9 @@ Accepted as sufficient for RC and GA:
The `customer-account-portal` lane should deliver:
-1. one named `Pulse Account` shell and task-first IA centered on `Overview`,
- `Workspaces`, `Access`, `Billing`, and `Support`
+1. one named `Pulse Account` shell and task-first IA centered on
+ `Workspaces`, `Access`, `Billing`, and `Support`, with precise workspace
+ counts inline at the top of `Workspaces`
2. shared identity and navigation across hosted account actions and
self-hosted commercial actions
3. canonical ownership boundaries for billing, licenses, hosted tenants,
diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md
index e97a0ec93..a61ddc5b0 100644
--- a/docs/release-control/v6/internal/subsystems/api-contracts.md
+++ b/docs/release-control/v6/internal/subsystems/api-contracts.md
@@ -326,13 +326,15 @@ self-hosted escalation paths by default, and self-hosted-only accounts do not
front-load an empty hosted-billing block before the real self-hosted jobs.
That same typed bootstrap/runtime contract must also derive the default signed-
in shell section from account shape: hosted accounts open on `Workspaces`,
-self-hosted-only accounts open on `Billing`, and `Overview` remains available
-as triage instead of acting as the default authenticated landing layer.
+self-hosted-only accounts open on `Billing`, and the signed-in shell keeps
+precise workspace counts inline on `Workspaces` instead of exposing a separate
+`Summary` tab as a primary or default destination.
The same account-shape runtime contract must also keep the shell navigation
-honest: self-hosted-only accounts must drop hosted-only `Workspaces`,
-`Access`, and `Overview` tabs from the primary task row rather than implying
-live hosted work, and any shared fallback surface that still resolves there
-must render an explicit unavailable state. `Support` follows the same
+honest: the task row is `Workspaces`, `Access`, `Billing`, and `Support`.
+Self-hosted-only accounts must drop hosted-only `Workspaces` and `Access`
+surfaces rather than implying live hosted work, and any shared fallback
+surface that still resolves there must render an explicit unavailable state. `Support`
+follows the same
account-shape runtime contract: self-hosted-only accounts expose only the
billing escalation path and billing-specific handoff packet, and hosted
workspace/access escalation controls must not render when no hosted account
@@ -350,10 +352,10 @@ The same permission contract must also drive hosted `Support`: when
`can_manage` is false, the support shell may route the user back to
`Workspaces`, `Access`, or `Billing` only as review and owner/admin handoff
paths, not as live hosted mutation paths the current role can execute.
-The same typed bootstrap/runtime contract must also keep `Overview` billing
-copy honest to account shape: hosted-only accounts may not mention self-hosted
-billing utilities by default, and hosted view-only roles must say when hosted
-billing still needs owner/admin authority.
+The same typed bootstrap/runtime contract must also keep inline workspace
+counts and shell copy honest to account shape: hosted-only accounts may not
+mention self-hosted billing utilities by default, and hosted view-only roles
+must say when hosted billing still needs owner/admin authority.
The same permission contract must also drive the compact account-context
summary: the strip may not describe full hosted access-control or billing
ownership when the current role can only review workspaces or roster state.
@@ -361,7 +363,7 @@ That same typed runtime contract must also normalize account-role labels before
render: customer-facing copy may say `Owner`, `Admin`, `Tech`, or `Read-only`,
but it must not surface raw runtime identifiers such as `read_only` or legacy
aliases such as `member`.
-That same runtime contract must also keep `Overview` next-action routing
+That same runtime contract must also keep the first available action
permission-honest for hosted view-only accounts: when no ready workspace
exists, the primary route must stay on reviewable `Workspaces` or `Access`
surfaces before any blocked hosted billing or owner/admin-only mutation path.
@@ -369,14 +371,14 @@ 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
+That same typed summary 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.
-That same typed overview contract must also keep `Needs attention` honest when
+That same typed summary 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
+That same typed summary contract must also stay fact-first: summary 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.
diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md
index 79c1049b5..43f36442e 100644
--- a/docs/release-control/v6/internal/subsystems/cloud-paid.md
+++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md
@@ -869,12 +869,12 @@ not yet form one coherent Pulse account product. The canonical future shape is
authenticated Pulse account shell that unifies Cloud tenants, self-hosted
licenses, billing, recovery, and MSP admin surfaces without creating a
standalone Relay portal. That shell now also owns two product rules
-explicitly: `Overview` is one shell-level triage surface across the current
-account set rather than a repeated per-account dashboard, and the top-level
-task row must stay honest to account shape by removing irrelevant hosted-only
-tasks instead of pretending they are live. Any shared fallback that still
-lands on a non-live task must render an explicit unavailable state instead of
-blank space. The same shell also owns action-first task
+explicitly: signed-in workspace counts stay inline at the top of
+`Workspaces` instead of as a separate `Overview` or `Summary` tab, and the
+top-level task row must stay honest to account shape by removing irrelevant
+hosted-only tasks instead of pretending they are live. Any shared fallback
+that still lands on a non-live task must render an explicit unavailable state
+instead of blank space. The same shell also owns action-first task
surfaces for `Access`, `Billing`, and `Support`: access mutations must be
permission-honest and roster-led, billing must reduce to one obvious job at a
time with hosted billing first when relevant, support must stay a failed-path
@@ -917,10 +917,10 @@ list-first task presentation instead of dark ornamental rails, sidebars, or
nested explanatory cards. The signed-in shell should orient the user with one
quiet account-context header and one flat top task row, not a second summary
deck competing with the task itself.
-That same owned shell must also open on the first live job instead of the
+That same owned shell must also open on the first live job instead of a
summary layer: hosted accounts should land in `Workspaces`, and self-hosted-
-only accounts should land in `Billing`. `Overview` remains part of the shell,
-but it is triage, not the default authenticated landing surface.
+only accounts should land in `Billing`. The signed-in shell must not expose a
+separate `Overview` or `Summary` tab ahead of the real task surfaces.
That same owned shell also owns the signed-out visual posture: the unauthenticated
`/portal` page must read like the same product boundary, not a leftover
marketing block plus a generic login card. The auth surface should present one
@@ -936,10 +936,10 @@ sent back to `Workspaces`, `Access`, or `Billing` only as review and
owner/admin handoff paths, not as though the user can perform hosted
lifecycle, access-mutation, or hosted-billing changes directly before
escalation.
-`Overview` follows the same account-shape and permission rule: hosted-only
-accounts must not mention self-hosted billing utilities by default, and hosted
-view-only roles must say when hosted billing still needs owner/admin
-authority.
+Inline workspace counts and shell copy follow the same account-shape and
+permission rule: hosted-only accounts must not mention self-hosted billing
+utilities by default, and hosted view-only roles must say when hosted billing
+still needs owner/admin authority.
The same rule applies to the compact account-context strip: it must describe
the current user's effective hosted tasks, not restate full access-control and
billing capability when those actions are blocked behind owner/admin roles.
@@ -947,7 +947,7 @@ That same owned shell must also keep role labels on product vocabulary:
customer-facing copy may say `Owner`, `Admin`, `Tech`, or `Read-only`, but it
must not leak internal identifiers such as `read_only` or legacy aliases such
as `member`.
-That same owned `Overview` surface must also keep `Next action`
+That same owned signed-in shell must also keep the first available action
permission-honest for hosted view-only users: when no workspace is ready, the
primary route must stay on reviewable `Workspaces` or `Access` surfaces before
any blocked hosted billing or owner/admin-only mutation path.
@@ -955,17 +955,13 @@ 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 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 owned inline workspace counts and workspace-state copy 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. The same counts and copy must keep suspended-only states honest, and
+must stay fact-first rather than inventing urgency or health verdicts such as
+`Nothing urgent` or `Healthy now`.
That same portal shell/runtime boundary must also keep task and status copy
literal across the account surface: customer-facing wording may not lean on
commentary such as `obvious`, `actual work`, `trustworthy`, or `settled` when
diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json
index f3ccd0314..56edfb55e 100644
--- a/internal/cloudcp/portal/dist/build_manifest.json
+++ b/internal/cloudcp/portal/dist/build_manifest.json
@@ -1,5 +1,5 @@
{
- "source_hash": "9f9d5468a58d29a9e38839b7c671e9b48a2cbedb21919ebe32e2a44256a030aa",
+ "source_hash": "105a2cf01f7b31e356d71c2f826b18be16165e4c9c61fba4504283bf965662ff",
"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 a315acca3..d928e67be 100644
--- a/internal/cloudcp/portal/dist/portal_app.css
+++ b/internal/cloudcp/portal/dist/portal_app.css
@@ -4101,13 +4101,16 @@ header .logout-btn,
}
}
.portal-shell-main {
+ width: min(1080px, calc(100vw - 48px));
+ margin: 0 auto;
display: flex;
flex-direction: column;
- gap: 20px;
- padding: 0 0 32px;
+ gap: 18px;
+ padding: 10px 0 40px;
}
.portal-shell-main > .portal-content-panel {
display: none !important;
+ width: 100%;
}
.portal-shell[data-shell-section=workspaces] .portal-content-panel-workspaces,
.portal-shell[data-shell-section=access] .portal-content-panel-access,
@@ -4118,7 +4121,7 @@ header .logout-btn,
}
.portal-account-context {
display: block;
- padding: 0 0 12px;
+ padding: 0 0 14px;
border: none;
border-bottom: 1px solid var(--line);
border-radius: 0;
@@ -4135,14 +4138,14 @@ header .logout-btn,
color: var(--line-strong);
}
.portal-account-context-row-title h2 {
- font-size: 30px;
- line-height: 1.04;
- letter-spacing: -0.04em;
+ font-size: 32px;
+ line-height: 1.02;
+ letter-spacing: -0.045em;
}
.portal-account-context-copy p {
- max-width: 820px;
- font-size: 15px;
- line-height: 1.55;
+ max-width: 760px;
+ font-size: 14px;
+ line-height: 1.5;
}
.portal-account-context-summary {
display: none !important;
@@ -4150,6 +4153,10 @@ header .logout-btn,
.portal-shell-nav {
position: static;
top: auto;
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 20px;
border: none;
border-bottom: 1px solid var(--line);
background: transparent;
@@ -4160,8 +4167,8 @@ header .logout-btn,
.portal-shell-nav-group {
display: flex;
flex-direction: row;
- gap: 12px;
- padding: 0 0 4px;
+ gap: 10px;
+ padding: 0 0 6px;
overflow-x: auto;
overscroll-behavior-x: contain;
scrollbar-width: none;
@@ -4169,6 +4176,14 @@ header .logout-btn,
.portal-shell-nav-group::-webkit-scrollbar {
display: none;
}
+.portal-shell-nav-group-primary {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+.portal-shell-nav-group-utility {
+ flex: 0 0 auto;
+ justify-content: flex-end;
+}
.portal-shell-nav-link {
min-width: 0;
padding: 0 8px 10px;
@@ -4188,6 +4203,12 @@ header .logout-btn,
background: transparent;
color: var(--ink);
}
+.portal-shell-nav-link-utility {
+ color: var(--ink-soft);
+}
+.portal-shell-nav-link-utility.active {
+ border-bottom-color: var(--line-strong);
+}
.portal-shell-nav-row {
justify-content: flex-start;
gap: 0;
@@ -4196,6 +4217,10 @@ header .logout-btn,
font-size: 14px;
font-weight: 600;
}
+.portal-shell-nav-link-utility .portal-shell-nav-label {
+ font-size: 13px;
+ font-weight: 500;
+}
.portal-shell-nav-badge {
display: none !important;
}
@@ -4211,29 +4236,29 @@ header .logout-btn,
box-shadow: none;
}
.account-surface + .account-surface {
- margin-top: 24px;
+ margin-top: 28px;
}
.account-stage-header,
.billing-header {
display: grid;
grid-template-columns: minmax(0, 1fr);
- gap: 12px;
- margin-bottom: 18px;
+ gap: 10px;
+ margin-bottom: 14px;
}
.account-stage-header h3,
.billing-header h2,
.portal-support-panel h2 {
- font-size: 24px;
- line-height: 1.12;
- letter-spacing: -0.03em;
+ font-size: 26px;
+ line-height: 1.08;
+ letter-spacing: -0.035em;
}
.account-stage-header p,
.billing-header p,
.billing-note,
-.portal-support-panel > p {
+.portal-support-panel p {
max-width: 760px;
font-size: 14px;
- line-height: 1.55;
+ line-height: 1.5;
}
.section-context-strip {
gap: 0;
@@ -4245,14 +4270,25 @@ header .logout-btn,
.section-context-chip + .section-context-chip::before {
margin-right: 8px;
}
-.overview-task-grid {
- grid-template-columns: 1fr;
- gap: 12px;
+.overview-task-grid-compact {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ overflow: hidden;
+ border: 1px solid var(--line);
+ border-radius: 14px;
+ background: #fff;
+}
+.overview-task-grid-compact .overview-task-card {
+ border: 0;
+ border-top: 1px solid var(--line);
+ border-radius: 0;
+ background: transparent;
+ padding: 16px 18px;
+}
+.overview-task-grid-compact .overview-task-card:first-child {
+ border-top: none;
}
-.overview-task-card,
-.billing-task-card,
-.portal-support-route-card,
-.portal-support-runbook,
.workspace-list-wrap,
.workspace-management-panel,
.access-roster,
@@ -4260,117 +4296,558 @@ header .logout-btn,
.access-invite-panel,
.access-job-note-panel,
.access-policy-panel,
+.billing-surface-block,
.billing-shell-main,
.billing-shell-detail,
-.billing-panel {
+.billing-panel,
+.portal-support-simple-card {
border: 1px solid var(--line);
- border-radius: 12px;
+ border-radius: 14px;
background: #fff;
}
-.overview-task-card,
-.billing-task-card,
-.portal-support-route-card,
-.portal-support-runbook,
-.billing-shell-main,
-.billing-shell-detail,
-.billing-panel {
- padding: 18px;
-}
.workspace-operations-shell,
.access-shell,
-.billing-shell,
-.portal-support-layout {
- grid-template-columns: 1fr;
+.billing-shell {
+ grid-template-columns: minmax(0, 1fr);
gap: 14px;
}
+.workspace-list-wrap,
+.access-roster,
+.billing-surface-block,
+.portal-support-simple-card {
+ overflow: hidden;
+}
.workspace-list-toolbar,
.access-panel-heading {
- padding: 16px 18px 10px;
+ padding: 14px 18px 8px;
}
.workspace-list-head {
- grid-template-columns: minmax(0, 1.6fr) 120px 120px 160px;
+ grid-template-columns: minmax(0, 1.7fr) 116px 116px 168px;
gap: 12px;
- padding: 8px 18px;
+ padding: 9px 18px;
}
.workspace-row {
- grid-template-columns: minmax(0, 1.6fr) 120px 120px 160px;
+ grid-template-columns: minmax(0, 1.7fr) 116px 116px 168px;
gap: 12px;
padding: 14px 18px;
}
.workspace-actions {
flex-wrap: wrap;
+ justify-content: flex-start !important;
+}
+.access-management-header {
+ margin-bottom: 12px;
}
.access-management-stats {
display: block;
- margin-bottom: 12px;
+ margin-bottom: 10px;
}
.access-stat-summary {
- padding: 0 0 8px;
- font-size: 12px;
+ padding: 0 0 10px;
+ font-size: 13px;
font-weight: 500;
color: var(--ink-muted);
}
.access-stat-summary-error {
color: var(--danger);
}
+.access-shell-main,
+.access-roster-column,
+.access-roster {
+ width: 100%;
+}
.access-task-strip {
- gap: 6px;
- margin-top: 10px;
+ gap: 8px;
+ margin-top: 12px;
}
.access-task-button {
border-radius: 10px;
}
-.access-task-button.active {
+.access-task-button.active,
+.access-task-button.is-active {
border-color: var(--accent);
+ background: #edf3fe;
+ color: var(--accent);
}
.access-roster-list {
- padding: 0 18px 8px;
+ padding: 0 18px 10px;
}
.access-roster-head {
- padding: 8px 18px;
+ padding: 9px 18px;
}
.access-member-row {
padding: 12px 0;
}
-.billing-overview-grid {
- grid-template-columns: 1fr;
- gap: 12px;
+.billing-header {
+ grid-template-columns: minmax(0, 1fr);
+ align-items: start;
}
.billing-note {
padding: 0;
border: none;
}
+.billing-surface-block {
+ padding: 0;
+}
+.billing-surface-block + .billing-surface-block {
+ margin-top: 14px;
+}
+.billing-surface-block-empty {
+ padding: 18px;
+}
+.billing-surface-header {
+ padding: 18px 18px 10px;
+}
+.billing-action-list-surface {
+ display: flex;
+ flex-direction: column;
+}
+.billing-action-row-surface {
+ padding: 16px 18px;
+}
.billing-action-row {
padding: 14px 0;
}
-.portal-support-route-grid {
- gap: 12px;
+.portal-support-simple {
+ max-width: 880px;
+}
+.portal-support-simple-card {
+ padding: 0;
+}
+.portal-support-simple-list {
+ display: flex;
+ flex-direction: column;
+}
+.portal-support-simple-row {
+ display: grid;
+ grid-template-columns: 160px minmax(0, 1fr);
+ gap: 16px;
+ padding: 14px 18px;
+ border-top: 1px solid var(--line);
+}
+.portal-support-simple-row:first-child {
+ border-top: none;
+}
+.portal-support-simple-row strong {
+ font-size: 13px;
+ color: var(--ink);
+}
+.portal-support-simple-row span {
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--ink-soft);
+}
+.portal-support-simple-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ padding: 16px 18px;
+ border-top: 1px solid var(--line);
+ background: #fafbfc;
}
@media (max-width: 960px) {
.portal-shell-main {
+ width: min(100%, calc(100vw - 32px));
gap: 16px;
}
+ .portal-shell-nav {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 4px;
+ }
.portal-shell-nav-group {
gap: 8px;
}
+ .portal-shell-nav-group-utility {
+ justify-content: flex-start;
+ }
.portal-account-context {
- padding-bottom: 10px;
+ padding-bottom: 12px;
}
.portal-account-context-row-title h2,
.account-stage-header h3,
.billing-header h2,
.portal-support-panel h2 {
- font-size: 22px;
+ font-size: 24px;
+ }
+ .portal-support-simple-row {
+ grid-template-columns: 1fr;
+ gap: 6px;
}
}
@media (max-width: 640px) {
+ .portal-shell-main {
+ width: min(100%, calc(100vw - 20px));
+ }
.portal-shell-nav-link {
padding: 0 6px 8px;
}
+ .portal-shell-nav-link-utility {
+ padding-bottom: 6px;
+ }
.portal-account-context-row-title h2 {
- font-size: 24px;
+ font-size: 26px;
}
.access-management-stats {
grid-template-columns: 1fr;
}
}
+.portal-shell-main {
+ max-width: 1120px;
+ margin: 0 auto;
+ width: min(100%, calc(100vw - 48px));
+ padding: 0 0 56px;
+ gap: 20px;
+}
+.portal-account-context {
+ padding: 28px 0 8px;
+ border-bottom: none;
+ background: transparent;
+}
+.portal-account-context-copy {
+ gap: 4px;
+}
+.portal-account-context-meta {
+ font-size: 12px;
+}
+.portal-account-context-row-title h2 {
+ font-size: 32px;
+ line-height: 1.04;
+ letter-spacing: -0.04em;
+}
+.portal-account-context-copy p {
+ margin: 0;
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--ink-muted);
+}
+.portal-shell-nav {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 20px;
+}
+.portal-shell-nav-group {
+ padding-bottom: 0;
+ gap: 20px;
+}
+.portal-shell-nav-group-primary {
+ flex: 0 0 auto;
+}
+.portal-shell-nav-group-utility {
+ flex: 0 0 auto;
+ margin-left: 0;
+}
+.portal-shell-nav-link {
+ padding: 0 0 12px;
+ width: auto;
+ min-width: max-content;
+ flex: 0 0 auto;
+ display: inline-flex;
+ white-space: nowrap;
+}
+.portal-shell-nav-link-utility {
+ color: var(--ink-muted);
+}
+.portal-page-header {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 16px;
+ margin-bottom: 18px;
+}
+.portal-page-header h2 {
+ font-size: 28px;
+ line-height: 1.06;
+ letter-spacing: -0.04em;
+}
+.portal-page-header p {
+ max-width: 640px;
+ margin: 0;
+ font-size: 14px;
+ line-height: 1.55;
+ color: var(--ink-muted);
+}
+.portal-page-facts,
+.portal-account-block-facts {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0;
+ margin-top: 8px;
+ font-size: 12px;
+ line-height: 1.5;
+ color: var(--ink-muted);
+}
+.portal-fact-separator {
+ margin: 0 10px;
+ color: var(--line-strong, var(--line));
+}
+.portal-summary-strip {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+ gap: 12px;
+ width: 100%;
+ min-width: 0;
+}
+.portal-summary-item {
+ padding: 12px 14px;
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ background: #fff;
+}
+.portal-summary-item span {
+ display: block;
+ margin-bottom: 4px;
+ font-size: 12px;
+ line-height: 1.4;
+ color: var(--ink-muted);
+}
+.portal-summary-item strong {
+ display: block;
+ font-size: 18px;
+ line-height: 1.2;
+ color: var(--ink);
+}
+.account-surface + .account-surface {
+ margin-top: 18px;
+}
+.portal-account-block-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+ margin-bottom: 12px;
+}
+.portal-account-block-copy h3 {
+ font-size: 20px;
+ line-height: 1.12;
+ letter-spacing: -0.03em;
+}
+.portal-account-block-copy p {
+ margin: 10px 0 0;
+ font-size: 14px;
+ line-height: 1.55;
+ color: var(--ink-muted);
+}
+.portal-account-block-actions {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+}
+.access-section-shell .portal-account-block-header,
+.billing-section .portal-page-header,
+.portal-support-panel .portal-page-header {
+ align-items: flex-start;
+}
+.access-section-shell .portal-account-block-header {
+ flex-direction: column;
+}
+.access-section-shell .portal-account-block-actions {
+ width: 100%;
+}
+.workspace-list-wrap,
+.workspace-management-panel,
+.access-roster,
+.access-task-panel,
+.billing-surface-block,
+.billing-shell-main,
+.billing-shell-detail,
+.billing-panel,
+.portal-support-panel {
+ border-radius: 14px;
+}
+.workspace-list-toolbar {
+ padding: 14px 18px 8px;
+}
+.workspace-list-summary {
+ font-size: 13px;
+ line-height: 1.5;
+ color: var(--ink-muted);
+}
+.workspace-list-head {
+ grid-template-columns: minmax(0, 1.8fr) 110px 120px 180px;
+ gap: 12px;
+ padding: 8px 18px;
+ font-size: 11px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: var(--ink-muted);
+}
+.workspace-row {
+ grid-template-columns: minmax(0, 1.8fr) 110px 120px 180px;
+ gap: 12px;
+ padding: 16px 18px;
+}
+.workspace-row-note {
+ font-size: 13px;
+}
+.workspace-actions {
+ justify-content: flex-end;
+ gap: 8px;
+}
+.workspace-operations-shell,
+.access-shell,
+.billing-shell {
+ gap: 12px;
+}
+.access-management-stats {
+ margin-bottom: 10px;
+}
+.access-task-strip {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 12px;
+}
+.access-task-button {
+ border-radius: 999px;
+ background: #fff;
+}
+.access-roster-list {
+ padding: 0 18px 4px;
+}
+.billing-surface-block {
+ padding: 18px;
+}
+.billing-surface-header {
+ margin-bottom: 8px;
+}
+.billing-action-list-surface {
+ gap: 0;
+}
+.billing-action-row-surface {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 16px;
+ align-items: center;
+ padding: 16px 0;
+ border-top: 1px solid var(--line);
+}
+.billing-action-row-surface:first-child {
+ padding-top: 0;
+ border-top: none;
+}
+.billing-action-row-surface:last-child {
+ padding-bottom: 0;
+}
+.billing-action-row-surface .billing-action-copy h3 {
+ font-size: 18px;
+}
+.billing-action-row-surface .billing-action-meta {
+ margin-top: 4px;
+ font-size: 13px;
+ line-height: 1.5;
+ color: var(--ink-muted);
+}
+.portal-support-panel {
+ border: 1px solid var(--line);
+ background: #fff;
+ padding: 18px;
+}
+.portal-support-simple-card {
+ padding: 0;
+ border: none;
+ background: transparent;
+}
+.portal-support-simple-row {
+ display: grid;
+ grid-template-columns: 160px minmax(0, 1fr);
+ gap: 16px;
+ padding: 14px 0;
+ border-top: 1px solid var(--line);
+}
+.portal-support-simple-row:first-child {
+ padding-top: 0;
+ border-top: none;
+}
+.portal-support-simple-actions {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 10px;
+ padding-top: 14px;
+ border-top: 1px solid var(--line);
+}
+@media (max-width: 960px) {
+ .portal-shell-main {
+ width: min(100%, calc(100vw - 40px));
+ padding-bottom: 40px;
+ }
+ .portal-shell-nav {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ }
+ .portal-shell-nav-group {
+ gap: 12px;
+ }
+ .portal-shell-nav-group-utility {
+ margin-left: 0;
+ }
+ .portal-account-block-header {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+ .portal-summary-strip {
+ width: 100%;
+ min-width: 0;
+ }
+ .billing-action-row-surface {
+ grid-template-columns: 1fr;
+ align-items: flex-start;
+ }
+}
+@media (max-width: 720px) {
+ .workspace-list-head {
+ display: none;
+ }
+ .workspace-row {
+ grid-template-columns: 1fr;
+ gap: 10px;
+ }
+ .workspace-row-status-cell {
+ justify-content: flex-start;
+ }
+ .workspace-actions {
+ justify-content: flex-start;
+ width: 100%;
+ }
+ .workspace-actions form,
+ .workspace-actions button {
+ max-width: none;
+ }
+ .portal-summary-strip {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ .access-task-strip {
+ overflow-x: auto;
+ padding-bottom: 2px;
+ scrollbar-width: none;
+ }
+ .access-task-strip::-webkit-scrollbar {
+ display: none;
+ }
+ .portal-support-simple-row {
+ grid-template-columns: 1fr;
+ gap: 6px;
+ }
+}
+@media (max-width: 640px) {
+ .portal-shell-main {
+ width: min(100%, calc(100vw - 24px));
+ padding-bottom: 32px;
+ }
+ .portal-account-context {
+ padding-top: 20px;
+ }
+ .portal-account-context-row-title h2 {
+ font-size: 28px;
+ }
+ .portal-page-header h2 {
+ font-size: 24px;
+ }
+ .portal-summary-strip {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js
index 3f36eb6f3..9ac4f7f53 100644
--- a/internal/cloudcp/portal/dist/portal_app.js
+++ b/internal/cloudcp/portal/dist/portal_app.js
@@ -2015,12 +2015,6 @@
function suspendedWorkspaceSectionChipLabel(count) {
return count === 1 ? "1 workspace suspended" : String(count) + " workspaces suspended";
}
- function billingHeaderChipLabels(hostedBillingCount, showSelfHostedCommercial) {
- return [
- hostedBillingCount > 0 ? "Hosted billing attached" : "No hosted billing attached",
- showSelfHostedCommercial ? "Self-hosted billing available" : "Hosted billing only"
- ];
- }
function supportSectionChipLabels(hasHostedAccounts2, hostedViewOnly, supportEmail) {
if (hasHostedAccounts2) {
return [
@@ -2038,45 +2032,6 @@
}
return showSelfHostedCommercial ? "Use Support only after Workspaces, Access, or Billing fails." : "Use Support only after Workspaces, Access, or hosted Billing fails.";
}
- function hostedSupportRouteTitle(hostedViewOnly) {
- return hostedViewOnly ? "Review or owner/admin handoff failed" : "Workspace or access failed";
- }
- function hostedSupportRouteDescription(hostedViewOnly) {
- return hostedViewOnly ? "Recheck the same workspace or roster first. Escalate only if the required owner or admin change still cannot complete." : "Retry the same workspace or access task first. Escalate only if the same task still fails.";
- }
- function hostedSupportRouteTaskCopy(hostedViewOnly) {
- return hostedViewOnly ? "Use Workspaces for workspace state or Access for roster state before you escalate." : "Use Workspaces for lifecycle work or Access for roster work before you escalate.";
- }
- function hostedSupportRouteContextLabel(hostedViewOnly) {
- return hostedViewOnly ? "Owner/admin handoff" : "Failed step";
- }
- function hostedSupportRouteContextCopy(hostedViewOnly) {
- return hostedViewOnly ? "Include the account, workspace, and the change that still needs owner or admin action." : "Include the account, workspace, and the failed step.";
- }
- function billingSupportRouteTitle(hostedViewOnly, showSelfHostedCommercial) {
- if (hostedViewOnly) {
- return showSelfHostedCommercial ? "Billing or owner/admin handoff failed" : "Hosted billing or owner/admin handoff failed";
- }
- return showSelfHostedCommercial ? "Billing failed" : "Hosted billing failed";
- }
- function billingSupportRouteDescription(hostedViewOnly, showSelfHostedCommercial) {
- if (hostedViewOnly) {
- return showSelfHostedCommercial ? "Escalate only if hosted billing still needs owner/admin action or a self-hosted billing task still fails." : "Escalate only if hosted billing still needs owner/admin action or still fails.";
- }
- return showSelfHostedCommercial ? "Escalate only if hosted billing or a self-hosted billing task still fails." : "Escalate only if hosted billing still fails.";
- }
- function billingSupportRouteJobCopy(hostedViewOnly, showSelfHostedCommercial) {
- if (hostedViewOnly) {
- return showSelfHostedCommercial ? "Hosted billing, licenses, refunds, or privacy. Say if hosted billing still needed owner/admin action." : "Hosted billing. Say if it still needed owner/admin action.";
- }
- return showSelfHostedCommercial ? "Hosted billing, licenses, refunds, or privacy." : "Hosted billing.";
- }
- function billingSupportRouteAccountCopy(hostedViewOnly, showSelfHostedCommercial) {
- if (showSelfHostedCommercial) {
- return "Include the hosted account for hosted billing or the billing email for self-hosted work.";
- }
- return hostedViewOnly ? "Include the hosted account that still needed owner/admin billing action." : "Include the hosted account for the failed billing step.";
- }
function supportRunbookPathCopy(hasHostedAccounts2, hostedViewOnly, showSelfHostedCommercial) {
if (!hasHostedAccounts2) return "Billing, licenses, refunds, or privacy.";
if (hostedViewOnly) {
@@ -2161,28 +2116,44 @@
return '' + escapeHTML(chip) + "";
}).join("") + "";
}
+ function renderFactLine(className, facts) {
+ if (!facts.length) return "";
+ return '
";
+ }
+ function attentionWorkspaces(workspaces) {
+ var results = [];
+ for (var i = 0; i < workspaces.length; i += 1) {
+ var status = workspaceHealthState(workspaces[i]);
+ if (status === "unhealthy" || status === "checking") {
+ results.push(workspaces[i]);
+ }
+ }
+ return results;
+ }
function accountContextRoleMeta(account) {
return portalRoleLabel(account.role) + " role";
}
- function accountContextLeadCopy(account) {
- if (account.can_manage) {
- return account.has_billing ? "Manage workspaces, access, and billing for this account." : "Manage workspaces and access for this account.";
- }
- return account.has_billing ? "Open workspaces and review access here. Billing and account changes require an owner or admin." : "Open workspaces and review access here. Account changes require an owner or admin.";
- }
function renderAccountContextStrip(account) {
- return '