From a5162d96cb2e86bd2b990450b3c7e7e368f8efee Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 29 Mar 2026 20:30:40 +0100 Subject: [PATCH] Redesign portal as a simpler account tool --- .../v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md | 103 +-- .../v6/internal/subsystems/api-contracts.md | 30 +- .../v6/internal/subsystems/cloud-paid.md | 46 +- .../cloudcp/portal/dist/build_manifest.json | 2 +- internal/cloudcp/portal/dist/portal_app.css | 597 +++++++++++++-- internal/cloudcp/portal/dist/portal_app.js | 184 ++--- .../portal/frontend/src/shell_view.test.ts | 131 ++-- .../cloudcp/portal/frontend/src/shell_view.ts | 450 ++++++----- .../cloudcp/portal/frontend/src/styles.css | 702 ++++++++++++++++-- 9 files changed, 1638 insertions(+), 607 deletions(-) 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 '
' + facts.map(function(fact) { + return "" + escapeHTML(fact) + ""; + }).join('\u2022') + "
"; + } + function renderSummaryStrip(items) { + if (!items.length) return ""; + return '
' + items.map(function(item) { + return '
' + escapeHTML(item.label) + "" + escapeHTML(item.value) + "
"; + }).join("") + "
"; + } + 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 '
"; + return '
"; } function renderPortalContextStrip(accounts, showSelfHostedCommercial) { if (accounts.length === 1) { return renderAccountContextStrip(accounts[0]); } if (accounts.length > 1) { - return '
'; + return '
'; } return '
'; } - function visibleShellSections(bootstrap) { + function primaryShellSections(bootstrap) { var accounts = Array.isArray(bootstrap.accounts) ? bootstrap.accounts : []; var hosted = hasHostedAccounts(accounts); var showSelfHostedCommercial = hasSelfHostedCommercial(bootstrap); @@ -2200,19 +2171,21 @@ if (hostedBillingCount > 0 || showSelfHostedCommercial) { sections.push({ section: "billing", title: "Billing" }); } - sections.push({ section: "support", title: "Support" }); - if (hosted) { - sections.push({ section: "overview", title: "Overview" }); - } return sections; } - function shellSectionButton(section, activeSection, title) { - return '"; + function utilityShellSections(_bootstrap) { + return [{ section: "support", title: "Support" }]; + } + function visibleShellSections(bootstrap) { + return primaryShellSections(bootstrap).concat(utilityShellSections(bootstrap)); + } + function shellSectionButton(section, activeSection, title, variant) { + return '"; } function renderShellNavigation(bootstrap, activeSection) { var sections = visibleShellSections(bootstrap); - return '