Skip to content

Portal v3 chrome parity — one builder-owned header and footer

Status: draft for review · Owner: Jeff Stolz · Scope: app/ui-customer v3 routes + Portal Studio contract/builder

Goal

A customer on a v3 portal sees exactly what the builder authored — nothing added, nothing hidden. Today the live portal wraps the authored body in legacy v1/v2 chrome the builder never renders, so the customer gets two stacked headers (legacy project header above the authored PortalNav) and a footer the author can't see or control. This spec inventories every function that legacy chrome carries, assigns each a disposition (absorb / already-covered / platform-overlay / drop), and sequences the retirement.

Principle: functional parity with v2 unless a past PR intentionally redesigned the function (those cases are cited inline). The only chrome that may render outside the published document is platform-integrity overlay (impersonation banner, AI dock) — and even that must appear in the Studio preview as a non-editable frame.

Current state

Two shells stack on every v3 project page:

LayerComponentRendered where
App shell navTopHeaderpages/v3/private/Private.vue:6Every authenticated page
App shell footer"Powered by OnRamp" tracked link — Private.vue:11-24Every authenticated page
Legacy project headerProjectSummaryHeaderpages/v3/private/pages/project/projectHome/ProjectHome.vue:5, imported from the v1 path (pages/private/...), unconditional, above the Studio shellProject pages
Authored chromePortalNav + optional PortalFooter inside PortalLayoutShellInside the body, below the legacy header
Platform overlaysPreviewHeader (impersonation banner), PortalAgentChatHost dockShell-level

ProjectSummaryHeader is shared by v1, v2, and v3 — retirement is dropping the v3 usage, never the file.

Two things this plan got wrong before the code was written, corrected here:

  • layout.footer was already validated — hardcoded in portal_config_validator.py rather than driven by the contract. The gap was single-source drift, not an unvalidated write, and the fix is a layout_fields declaration the validator loops over.
  • Retirement is scoped, not global. Only the account home and project home render the authored chassis, and only when a published config exists. The task page, welcome wizard, resource detail, and the legacy-widget fallback render no PortalNav at all, so deleting the shell header outright would strip those pages of the only route to Profile and Sign Out. A page claims the frame; the shell stands down only for it.

Dispositions

Absorb into builder-owned chrome

FunctionTodayTarget
Vendor logo → account homeTopHeader logo + PortalNav mark (duplicated)PortalNav mark only (already at parity)
User avatar → menu (Profile modal, Sign Out, conditional "Restart Welcome Experience")TopHeaderPortalNav gains a trailing account slot rendering the same menu. Not authorable-off: a customer must always be able to sign out. Authors control placement/brand via existing nav options only
Project nameProjectSummaryHeader titleNew {project_name} and {account_name} hero/text tokens; default project template's hero heading uses {project_name} so parity holds without author action
Customer account logo + name (CompanyLogos)ProjectSummaryHeaderHero prop show_account_identity (default on for project scope — parity), reading the same account branding source
Start date / goal dateProjectStatsHero meta chips prop (show_dates, default on), same fields and the same LLL dd, yyyy format. Not gated on portal_display_start_date — that flag governs task dates; the legacy header showed both unconditionally, so parity is unconditional
"Powered by OnRamp" tracked lead linkShell footer (handlePoweredByClick → portal leads)PortalFooter powered-by becomes the same tracked link component, always rendered (decided 2026-08-10): a slim standalone strip when layout.footer is off; the footer toggle governs only the brand/© block
Invite / member management (ProjectMemberButtonProjectTeamDialog + InviteMemberModal)ProjectSummaryHeaderFolds into the team widget behind author toggle allow_invite, default on for project scope (decided 2026-08-10). Depends on the team-widget plan; its known cross-project stale-team bug must be fixed in or before that phase
"Restart Welcome Experience" menu itemTopHeader menu, conditionalCarries into the PortalNav account menu unchanged; deleted in the same change that retires the FVE wizard (decided 2026-08-10)
Footer brand / © linePortalFooter (builder) — no v2 equivalentStays as-is, opt-in via layout.footer

Already covered by an intentional redesign (drop from chrome, cite the redesign)

FunctionCovered by
Tasks-remaining count (ProjectStats)task_checklist "N of M complete" rollup (task-checklist plan, shipped)
Comments count (ProjectStats, gated on allow_comments)Planned Conversations widget (portal-studio-conversations-widget plan). Until it ships, this count is simply absent from v3 — accepted gap, note in release comms
"Get started" affordancesnext_step widget (shipped)

Remain platform overlays — but represented in the Studio preview

FunctionWhy it stays outside the document
PreviewHeader impersonation/preview bannerSecurity/integrity signal; an author must not be able to hide it. Moves up to the v3 shell, so it shows on authored pages too rather than disappearing with the header that used to carry it
PortalAgentChatHost dockFlag + project gated. Already represented in the canvas as an inert launcher (#10724)

What the canvas gained instead of an impersonation frame: an inert account avatar in the nav. The banner is conditional chrome only an author's own impersonation triggers, so drawing it permanently would misrepresent the ordinary case; the account control is on every customer's screen and was the thing genuinely missing from the canvas.

Decisions log

Invite→team widget, powered-by always-visible, and Restart-FVE-until-wizard-dies were all decided 2026-08-10 (Jeff) and are folded into the absorb table above. No open questions remain.

Contract changes

Model what the renderer already reads, then the new knobs — all additive, gated by the existing contract-parity tests (test_portal_studio_contract_gate.py, plus the vitest registry parity suites; the frontend runner is vitest, not jest). The contract is also vendored byte-identical into onramp-agents/src/agents/portal_studio/contracts/, and test_agent_vocabulary_parity.py fails on any divergence — every contract edit has to be copied across.

  • layout.footer: boolean (exists in code today, missing from contract)
  • Hero widget props: show_account_identity, show_dates
  • Tokens: {project_name}, {account_name} added to the token vocabulary (single source: wherever {first_name}/{vendor_name} are defined)
  • team widget: allow_invite: boolean, default on for project scope

Sequencing

  1. Contract catch-up — add layout.footer + a parity test that every layout key the renderer reads exists in the contract. Mechanical; no behavior change.
  2. Footer parity — tracked powered-by link into PortalFooter; resolve Q2. Delete the shell footer from pages/v3/private/Private.vue only.
  3. Header absorption — tokens, hero identity/date props, PortalNav account slot. Each lands with the default-on parity defaults so existing published portals change appearance only by losing the duplicate, not by losing information.
  4. Retire legacy chrome from v3 — delete ProjectSummaryHeader usage from v3 ProjectHome.vue and TopHeader usage from v3 Private.vue (v1/v2 files untouched). This is the payoff commit; it must land after 2–3, never before.
  5. Preview truthfulness — static frames for the impersonation banner and agent dock in the Studio canvas. Deferred: the canvas draws neither frame, so a builder still previews a portal without the two platform overlays that sit over it.

Verification per phase: contract gate tests, Jest on renderer components, and a live pass as Corey (FastPass recipe, demo project 910001) comparing v2 and v3 side-by-side for each absorbed function.

Out of scope

Task detail page, welcome wizard replacement, auth screens, the 42 preview seams not named here, account-home SelfServeProjects/ProjectSummaryCard fallbacks (v2-parity path when no Studio config exists — unchanged by definition).

Internal documentation — gated behind Cloudflare Access.