From f6846443f3f5e65c62df9e7fc02f70cc73d845d4 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 1 Apr 2026 20:32:22 +0100 Subject: [PATCH] Tighten hosted portal shell spacing --- .../v6/internal/subsystems/api-contracts.md | 6 ++++ .../v6/internal/subsystems/cloud-paid.md | 5 +++ .../cloudcp/portal/dist/build_manifest.json | 2 +- internal/cloudcp/portal/dist/portal_app.css | 25 +++++++++------ internal/cloudcp/portal/dist/portal_app.js | 5 +-- .../portal/frontend/src/shell_view.test.ts | 3 ++ .../cloudcp/portal/frontend/src/shell_view.ts | 5 +-- .../cloudcp/portal/frontend/src/styles.css | 32 +++++++++++-------- 8 files changed, 51 insertions(+), 32 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 9c39035fe..b79bcd460 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -183,6 +183,12 @@ Own canonical runtime payload shapes between backend and frontend. 26. Keep storage chart identity canonical on that same shared API surface: the shared storage charts endpoint must key pool and physical-disk series by the resolved unified-resource `MetricsTarget.ResourceID`, not by canonical resource IDs or page-local aliases, so storage rows, focused summary cards, sticky summary shells, and detail charts all address the same history series in live and mock mode. 27. Keep synthetic summary-chart fallback identity canonical on that same shared API surface: when `internal/api/router.go` has to synthesize mock summary history for infrastructure, workloads, or storage cards, it must derive the fallback from canonical `resourceType`, `resourceID`, and `metricType` ownership instead of raw min/max seed-prefix helpers, so range changes and runtime mock updates stay on one governed timeline. 28. Keep workload-chart response identity canonical on that same shared API surface: `internal/api/router.go`, `internal/api/contract_test.go`, and workload summary consumers must emit provider-backed VM and system-container series under the same canonical workload IDs that workloads page rows use, while resolving history through the unified `MetricsTarget.ResourceID`, so hover and focus selection do not fall off for provider-backed rows. +29. Keep the hosted account portal bootstrap intelligible without duplicate + chrome. `internal/cloudcp/portal/page.go`, the maintained portal frontend + bundle, and the shared portal styles may refine layout density, but the + account/billing shell must remain understandable from the primary header, + section title, and factual body content alone instead of depending on a + second context-chip strip to restate the same scope. ## Forbidden Paths diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index d0aa33ed9..b9f55d2a4 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -110,6 +110,11 @@ agreement, and cloud-specific enforcement rules. 19. Add or change cloud plan presentation through `frontend-modern/src/pages/CloudPricing.tsx` 20. Add contract tests where runtime and pricing need to stay aligned 21. Add or change hosted browser org-context bootstrap through `frontend-modern/src/App.tsx`, `frontend-modern/src/AppLayout.tsx`, `frontend-modern/src/useAppRuntimeState.ts`, and `frontend-modern/src/utils/apiClient.ts` +22. Keep the hosted account portal shell task-first and compact. Section + headers, billing action rows, and the maintained portal bundle under + `internal/cloudcp/portal/` may surface the facts an operator needs, but the + shell should not spend vertical space on duplicate context-chip strips when + the page header and section body already communicate that scope. ## Forbidden Paths diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json index f93305f4d..3b7df84d5 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "dee5325d02ca13ee1eb3b4ff7d09478406bbd5805999ced971f4b7f62700d7f0", + "source_hash": "8cda796f9a573443304df3d61c11f26041c8cd7f050fc737b67e3c056cd09b6c", "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 3122a8054..74d82df2e 100644 --- a/internal/cloudcp/portal/dist/portal_app.css +++ b/internal/cloudcp/portal/dist/portal_app.css @@ -81,6 +81,7 @@ --warn: #9a6700; --warn-subtle: #fff8c5; --radius: 6px; + --header-height: 49px; } *, *::before, @@ -89,6 +90,7 @@ } html { scroll-behavior: smooth; + background: var(--bg); } body { margin: 0; @@ -118,15 +120,16 @@ header { align-items: center; justify-content: space-between; gap: 12px; - height: 48px; - padding: 0 16px; + height: var(--header-height); + padding: 0 24px; border-bottom: 1px solid var(--border); - background: var(--bg); + background: var(--bg-subtle); } header .brand { font-size: 14px; font-weight: 600; color: var(--ink); + white-space: nowrap; } .header-account-chip { display: inline-flex; @@ -204,9 +207,9 @@ header .logout-btn:hover, font-size: 12px; } .main { - max-width: 800px; + max-width: 960px; margin: 0 auto; - padding: 0 24px 64px; + padding: 0 32px 64px; } .portal-shell { display: flex; @@ -217,13 +220,13 @@ header .logout-btn:hover, display: flex; align-items: baseline; gap: 8px; - padding: 16px 0 12px; + padding: 16px 0 0; font-size: 13px; color: var(--ink-secondary); } .portal-identity-bar h2 { margin: 0; - font-size: 20px; + font-size: 18px; font-weight: 600; color: var(--ink); } @@ -234,7 +237,8 @@ header .logout-btn:hover, display: flex; align-items: stretch; gap: 0; - border-bottom: 1px solid var(--border); + border-bottom: 1px solid var(--border-muted); + margin-top: 8px; margin-bottom: 0; } .portal-tab { @@ -293,7 +297,7 @@ header .logout-btn:hover, display: none !important; } .portal-content-panel { - padding: 20px 0 0; + padding: 16px 0 0; } .portal-section-header { display: flex; @@ -1591,6 +1595,9 @@ header .logout-btn:hover, background: #82181a; } @media (max-width: 768px) { + header { + padding: 0 16px; + } .main { padding: 0 16px 48px; } diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js index 3694bf991..9cf73140f 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -2062,10 +2062,7 @@ return '

' + title + "

" + description + '

' + meta + '
"; } function renderSectionContextChips(chips) { - if (!chips.length) return ""; - return '
' + chips.map(function(chip) { - return '' + escapeHTML(chip) + ""; - }).join("") + "
"; + return ""; } function attentionWorkspaces(workspaces) { var results = []; diff --git a/internal/cloudcp/portal/frontend/src/shell_view.test.ts b/internal/cloudcp/portal/frontend/src/shell_view.test.ts index 20ffb486e..3c5345e75 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts @@ -192,6 +192,9 @@ describe('shell view', function() { expect(html).not.toContain('>Licenses<'); expect(html).not.toContain('>Refunds<'); expect(html).not.toContain('>Privacy<'); + expect(html).not.toContain('section-context-strip'); + expect(html).not.toContain('View roster'); + expect(html).not.toContain('Owner or admin required'); }); it('defaults the authenticated hosted shell to workspaces', function() { diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts index 25f3a218c..9c44e3b4b 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -217,10 +217,7 @@ function renderBillingActionRow( } function renderSectionContextChips(chips: string[]): string { - if (!chips.length) return ''; - return '
' + chips.map(function(chip) { - return '' + escapeHTML(chip) + ''; - }).join('') + '
'; + return ''; } function renderFactLine(className: string, facts: string[]): string { diff --git a/internal/cloudcp/portal/frontend/src/styles.css b/internal/cloudcp/portal/frontend/src/styles.css index a8287ed1d..f8d710f01 100644 --- a/internal/cloudcp/portal/frontend/src/styles.css +++ b/internal/cloudcp/portal/frontend/src/styles.css @@ -38,11 +38,12 @@ --warn: #9a6700; --warn-subtle: #fff8c5; --radius: 6px; + --header-height: 49px; } /* ── Reset ────────────────────────────────────────────────────── */ *, *::before, *::after { box-sizing: border-box; } -html { scroll-behavior: smooth; } +html { scroll-behavior: smooth; background: var(--bg); } body { margin: 0; @@ -59,7 +60,7 @@ a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; } [hidden] { display: none !important; } -/* ── Header ───────────────────────────────────────────────────── */ +/* ── Header (top bar) ─────────────────────────────────────────── */ header { position: sticky; top: 0; @@ -68,16 +69,17 @@ header { align-items: center; justify-content: space-between; gap: 12px; - height: 48px; - padding: 0 16px; + height: var(--header-height); + padding: 0 24px; border-bottom: 1px solid var(--border); - background: var(--bg); + background: var(--bg-subtle); } header .brand { font-size: 14px; font-weight: 600; color: var(--ink); + white-space: nowrap; } .header-account-chip { @@ -158,9 +160,9 @@ header .logout-btn:hover, /* ── Main container ───────────────────────────────────────────── */ .main { - max-width: 800px; + max-width: 960px; margin: 0 auto; - padding: 0 24px 64px; + padding: 0 32px 64px; } /* ── Portal shell (signed-in) ─────────────────────────────────── */ @@ -170,19 +172,19 @@ header .logout-btn:hover, gap: 0; } -/* Identity bar — tighter, no padding top */ +/* ── Identity bar — compact page header ───────────────────────── */ .portal-identity-bar { display: flex; align-items: baseline; gap: 8px; - padding: 16px 0 12px; + padding: 16px 0 0; font-size: 13px; color: var(--ink-secondary); } .portal-identity-bar h2 { margin: 0; - font-size: 20px; + font-size: 18px; font-weight: 600; color: var(--ink); } @@ -191,12 +193,13 @@ header .logout-btn:hover, color: var(--border); } -/* Tab navigation */ +/* ── Tab navigation ───────────────────────────────────────────── */ .portal-tab-bar { display: flex; align-items: stretch; gap: 0; - border-bottom: 1px solid var(--border); + border-bottom: 1px solid var(--border-muted); + margin-top: 8px; margin-bottom: 0; } @@ -229,7 +232,7 @@ header .logout-btn:hover, font-weight: 400; } -/* Content panels — hide/show logic */ +/* ── Content panels — hide/show logic ─────────────────────────── */ .portal-shell-main { display: flex; flex-direction: column; @@ -263,7 +266,7 @@ header .logout-btn:hover, } .portal-content-panel { - padding: 20px 0 0; + padding: 16px 0 0; } /* ── Section headers ──────────────────────────────────────────── */ @@ -1306,6 +1309,7 @@ header .logout-btn:hover, /* ── Responsive ───────────────────────────────────────────────── */ @media (max-width: 768px) { + header { padding: 0 16px; } .main { padding: 0 16px 48px; } .portal-tab-bar { overflow-x: auto; scrollbar-width: none; gap: 0; }