Skip to content

feat(meetings): gate the meeting details page and scaffold v2 behind the flag - #2910

Merged
nunoeufrasio merged 12 commits into
feat/meeting-details-v2from
feat/issue-2873
Oct 4, 2026
Merged

nunoeufrasio merged 12 commits into
feat/meeting-details-v2from
feat/issue-2873

Conversation

@nunoeufrasio

@nunoeufrasio nunoeufrasio commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Puts /meetings/:id behind MEETING_V2_ENABLED_FLAG so the V2 redesign can ship side by side with V1, and adds the V2-02 scaffold (#2874), folded in from #2923 (now closed). This is the only PR in the epic that touches the live route, so the gate and the scaffold land together as one tested unit. The V2 tree is a recognisable stub, not real UI.

What changes on main for users with the flag off (everyone): nothing visible. The route now loads the gate, which renders V1. The gate's host element and wrapper divs are display: contents, so V1 lays out exactly as when the route rendered it directly. V1's files moved from meeting-join/ to meeting-join-v1/ as pure renames, with byte-identical contents. V2 is a separate lazy chunk behind @defer, so V1 visitors never download it, and if that chunk fails to load the @error branch renders V1.

V1 is untouched. meeting-join.component.ts is byte-identical, its 23-test spec is unmodified and green, and the flag defaults to false, so behaviour on main is unchanged for every user until LaunchDarkly targeting says otherwise.

Mechanism: a shim component, not a canMatch guard

This is the same pattern as health-metrics-gate.component.ts, which is already spec'd and in production. The route keeps one stable loadComponent; the component holds a hydrated signal latched in afterNextRender and an @if over the two trees.

Why the guard was rejected. feature-flag.provider.ts returns early when typeof window === 'undefined', and no flag value crosses TransferState — so SSR always evaluates this flag as false. A canMatch guard following the established pattern returns true on the server, meaning the server would render V2's route target and the browser would then swap to V1 for ~100% of users: a whole-route component swap at hydration, on a public SSR-first page. The awaiting variant is worse — it holds the client render of an anonymous-reachable page behind a LaunchDarkly round trip (up to 10s) for every visitor, defeating the TransferState-seeded first paint from GH-2041.

Anonymous visitors are forced to V1 in code, not by targeting, because LaunchDarkly cannot target a logged-out visitor:

protected readonly v2Enabled = computed(() => this.hydrated() && this.userService.authenticated() && this.rawV2Enabled());

authenticated() reading false on an early client render only delays the swap, which fails in the safe direction.

Known limitation

For the targeted cohort, SSR renders V1 and the swap to V2 happens once, post-hydration — a visible content swap for those users. It is not a hydration mismatch (the hydrated latch means Angular never reconciles the two trees), and it is asserted by a test. Giving SSR a real flag source (an LD server SDK, or the BFF stamping the decision into a cookie) is the prerequisite before V2 ramps past a tester list through this gate. That is recorded in the gate's doc comment and belongs to #2875.

Testing

  • 5 new gate tests, each proven binding by mutation: flag unset → V1; provider never ready → V1 (fail closed); flag true but pre-hydration → V1; flag true but unauthenticated → V1; V2 only when hydrated and authenticated and flag true.
  • V1's existing meeting-join spec unmodified and green.
  • Full app suite: 5236 tests across 298 files, all passing.
  • yarn e2e:check-collection: 131 spec files, 0 collection errors. The flag-gate E2E spec gains a /meetings/:id describe asserting one branch present and the other absent.
  • yarn lint (0 errors), yarn build, ./check-headers.sh all clean.

Closes #2873
Closes #2874

🤖 Generated with Claude Code

Scaffold commits (from #2923)

  • 3dc333710: scaffold. V1's directory becomes meeting-join-v1/ (directory-only move). V2 becomes meeting-details-page/ (MeetingDetailsPageComponent), loaded via @defer. Conventions and the V1-deletion definition of done are in specs/011-meeting-details-redesign/v2-scaffold.md.
  • 037962f30: renames V2 away from lfx-meeting-details, which the create/edit wizard already uses, and adds the @error fallback.
  • 90a8d4733: display: contents on the gate's host element and wrappers, for V1 layout parity.
  • d98f72e9b: unit test that forces the V2 chunk to fail and asserts V1 mounts. Proven binding per R07.

`/meetings/:id` now loads `MeetingDetailsGateComponent`, which renders the
pre-v2 page by default and the v2 tree only for a hydrated, signed-in viewer
that `MEETING_V2_ENABLED_FLAG` resolves true for. The pre-v2 page is untouched
and stays the default for everyone else.

A shim component rather than a `canMatch` guard: LaunchDarkly is browser-only
and no flag value crosses TransferState, so SSR always evaluates this flag
false. A guard returning true on the server would SSR the v2 target and then
match v1 on the browser for ~100% of viewers, and a guard awaiting flag
readiness would hold the client render of a public, SSR-first page behind a
LaunchDarkly round trip for every visitor. Same call, and same shape, as
`HealthMetricsGateComponent`.

Anonymous viewers are held on v1 by the gate rather than by targeting, since a
tester list cannot express "not logged out". The v2 branch renders a
placeholder that #2874 replaces with the real scaffold.

Refs #2873

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Addresses the full-branch review of the #2873 gate.

- Correct the e2e comment on the flag-on case: the pre-v2 page does mount
  for the render before the hydration latch and is torn down with its
  in-flight lookup, rather than never mounting at all.
- Record in the gate's doc comment that SSR renders v1 for every viewer,
  so v1's lookup (and its not-found redirect) decides the URL for both
  branches until SSR has a flag source, and that #2874 should load the
  real v2 page through @defer rather than a static import.
- Give the v2 placeholder a templateUrl instead of an inline template.
- Point the standalone-routes map and the meetings module tree at
  MeetingDetailsGateComponent, and note in public-meeting-join.md that
  the join page is no longer the route target itself.

Refs #2873

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
@nunoeufrasio
nunoeufrasio requested a review from a team as a code owner September 23, 2026 23:06
Copilot AI balanced review requested due to automatic review settings September 23, 2026 23:06
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: ae5ee938-84f7-4a2d-922f-05d013b7ce43

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@cursor

cursor Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes the public SSR /meetings/:id entry point and post-hydration swap behavior for flag-targeted signed-in users, though the flag defaults off and the gate fails closed to unchanged v1.

Overview
/meetings/:id now loads a feature-flag gate instead of the pre-v2 join page directly. MeetingDetailsGateComponent keeps one route target and, after hydration, shows the v2 tree only when MEETING_V2_ENABLED_FLAG is on and the viewer is signed in; everyone else (including anonymous visitors) stays on v1. V1 is moved to meeting-join-v1/ with display: contents wrappers so layout matches the old direct route when the flag is off.

The v2 side is a deferred lazy chunk (meeting-details-page/ empty-state scaffold for now). If the chunk fails to load, the gate falls back to v1. Unit tests cover fail-closed defaults, hydration/auth gating, and chunk errors; Playwright E2E pins the /meetings/:id branch through the real flag service (SPA navigation + held lookup stub).

Docs and specs/011-meeting-details-redesign/v2-scaffold.md document v1/v2 naming, loading, and v1 retirement criteria.

Reviewed by Cursor Bugbot for commit 02f14a1. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Critical gate behavior and failing E2E assertion issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 High severity

Open (2)
What changed in this PR

Adds a feature-flag gate for /meetings/:id, preserving V1 by default and scaffolding a V2 placeholder.

Changes:

  • Adds gate and V2 placeholder components.
  • Updates routing and architecture documentation.
  • Adds unit and E2E coverage for branch selection.
File Review
docs/​architecture/​frontend/​public-meeting-join.md Documents the gated meeting route.
docs/​architecture/​frontend/​component-architecture.md Documents the V1/V2 component structure.
apps/​lfx-one/​src/​app/​modules/​meetings/​meeting-details-v2/​meeting-details-v2.component.ts Defines the V2 placeholder component.
apps/​lfx-one/​src/​app/​modules/​meetings/​meeting-details-v2/​meeting-details-v2.component.html Provides placeholder markup.
apps/​lfx-one/​src/​app/​modules/​meetings/​meeting-details-gate/​meeting-details-gate.component.ts Implements branch selection. Must address the wrong-tree flash and enforce provider readiness before enabling V2.
apps/​lfx-one/​src/​app/​modules/​meetings/​meeting-details-gate/​meeting-details-gate.component.spec.ts Tests hydration, authentication, and flag conditions; the provider-not-ready test must model readiness state.
apps/​lfx-one/​src/​app/​modules/​meetings/​meeting-details-gate/​meeting-details-gate.component.html Renders the selected meeting-details branch.
apps/​lfx-one/​src/​app/​app.routes.ts Routes meeting details through the gate.
apps/​lfx-one/​e2e/​meetings-v2-flag-gate.spec.ts Adds branch coverage, but the V2 visibility assertion will time out and direct SSR hydration remains untested.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/lfx-one/e2e/meetings-v2-flag-gate.spec.ts Outdated
The v2 placeholder renders an empty element, so the gate's v2 wrapper
has no box and toBeVisible() would time out even when the right branch
rendered. CI does not run the full e2e suite, so nothing caught it.
Asserting toBeAttached() proves the branch mounted, which is the claim
this test makes.

Also points the gate's doc comment at #2920, the follow-up that gives
SSR a flag source. #2873's "no flash of the wrong tree" criterion has
been amended to match the documented trade-off: a targeted viewer sees
a one-time v1 to v2 swap until #2920 lands, and targeting stays on a
named tester list until then.

Refs #2873

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI review requested due to automatic review settings September 24, 2026 12:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The reviewed changes have no unresolved approval-blocking issues.

Review effort: Balanced
Findings: None

Resolved since last review (2)

Establishes where every Phase 1 component of the meeting details
redesign lives, following the composer's -v1 precedent:

- meeting-join/ moves to meeting-join-v1/. Only the directory moves;
  all three files are byte-identical (100% renames), because the epic
  forbids editing the v1 component and renaming its class or selector
  would do exactly that.
- meeting-details-v2/ becomes meeting-details/ with a recognisable
  stub built on the existing lfx-empty-state wrapper. It reads no
  meeting data yet; the E1-01 shell adds the data flow on the same
  services and TransferState contract v1 uses.
- The gate now renders v2 inside @defer (on immediate), so v2 is its
  own lazy chunk and the v1 majority, every anonymous visitor
  included, never downloads it. The e2e gate spec asserts the scaffold
  renders, which proves the chunk loads.

specs/010-meeting-details-redesign/v2-scaffold.md records the naming
convention, where v2-only and shared components live, the rule that a
shared meetings component changes only additively, and the definition
of done for deleting v1 that V2-03 (#2875) builds on.

Stacked on #2910 (V2-01), which this depends on.

Closes #2874

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
The scaffold took MeetingDetailsComponent / lfx-meeting-details, which
the create/edit wizard's details step in components/meeting-details/
already owns. It compiled only because no file imports both; the first
V2 component to reuse the wizard step, which the scaffold doc invites,
would have hit a duplicate selector. V2 is now meeting-details-page/
with MeetingDetailsPageComponent / lfx-meeting-details-page, and the
doc says to check a selector is free before naming a V2 component.

Also from the pre-PR review:

- The deferred v2 block gains an @error branch that renders v1, so a
  chunk that fails to load (a deploy mid-session, a dropped
  connection) no longer leaves a targeted viewer on an empty page.
- v2-scaffold.md and the page's TSDoc cite spec.md, testid-contract.md
  and meeting-view-model.utils, none of which is on main yet. Each is
  now marked with the PR that lands it.

Refs #2874

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
On main, /meetings/:id rendered lfx-meeting-join directly. Behind the
gate it sits inside the gate's host element and a branch-marker div,
two extra boxes that could shift v1's layout for every visitor the
flag is off for, which is all of them.

The gate's host element and both wrapper divs are now
display: contents, so none of them creates a layout box and v1 lays
out exactly as it did. The divs keep their data-testids; Playwright
treats a display: contents element as visible when its children are.

This PR now also carries the V2-02 scaffold (folded in from #2923), so
the only change to the live route lands once, as one unit.

Refs #2873
Refs #2874

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
The @error branch of the gate's deferred v2 block renders v1 so a
targeted viewer is never left on an empty page when the lazy chunk
fails to load, but nothing exercised it: the existing tests run
deferred blocks through (DeferBlockBehavior.Playthrough, Angular's
default), which only ever reaches the success state.

The create helper now takes a defer behavior, and a new test uses
Manual to drive the block to DeferBlockState.Error and asserts v1
mounts inside the v2 wrapper. Proven binding per R07: replacing the
@error branch's lfx-meeting-join with an empty span failed exactly
this test.

Refs #2874

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 08:23
@nunoeufrasio nunoeufrasio changed the title feat(meetings): gate the meeting details page behind the v2 flag feat(meetings): gate the meeting details page and scaffold v2 behind the flag Oct 1, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The documented reachability and TransferState handoff do not match the gate’s actual client-side lifecycle.

Review effort: Balanced
Findings: 2 Low severity

Open (2)

Comment thread specs/011-meeting-details-redesign/v2-scaffold.md
Brings in 100 commits from main. #3044 changed V1's meeting-join component; git followed the directory rename, so those changes land in meeting-join-v1/ and its three files are byte-identical to main's meeting-join/. No textual conflicts. Gate + V1 specs (33), types, lint, format, build and e2e collection pass on the merged tree.

Refs #2873

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 08:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

TransferState ownership conflicts with the documented V2 contract, and the cancellation E2E assertion is not synchronized.

Review effort: Balanced
Findings: 2 Medium severity · 2 Low severity

Open (4)

Comment thread apps/lfx-one/e2e/meetings-v2-flag-gate.spec.ts
@nunoeufrasio

Copy link
Copy Markdown
Contributor Author

V1 layout parity check (flag off): passed.

I ran origin/main and this branch (3801b8b1a) one after the other on the same dev setup. Both had a 1024×768 viewport and an anonymous visitor, which always gets V1. I loaded V1 for the same synthetic public meeting on each, with the meeting API stubbed in-page, and measured every data-testid element inside lfx-meeting-join.

  • 19 of 19 elements match within 1px. Every difference is sub-pixel rounding. Examples: info card 55,152 · 900×824 vs 900×823; title 68,207 · 874×26 on both; materials 296×208 on both; page height 1017 vs 1016.
  • The gate adds no layout boxes. On this branch V1's ancestor chain is lfx-meeting-join → div#meeting-details-gate-v1 [display: contents] → lfx-meeting-details-gate [display: contents] → lfx-root.

One note on method: the browser pane was hidden, so requestIdleCallback never fired and V1's @defer (on idle) materials block stayed in its placeholder. The final comparison polyfilled requestIdleCallback so both builds reached the same settled state. It was not a difference between the builds.

Address review comments from the reviewers on PR #2910 (all per
copilot-pull-request-reviewer[bot]):

- meeting-details-gate.component.ts: the "v1's lookup governs
  reachability" note now holds for a full SSR navigation only. On an
  in-app navigation the hydration latch destroys v1 and aborts its
  lookup, so v2 must check reachability itself once it has a data
  flow.
- v2-scaffold.md, meeting-details-page.component.ts: v2 cannot read
  the MeetingJoinPageState seed from TransferState, because v1 mounts
  first and removes the meetingJoinState key on read. Both now record
  the E1-01 prerequisite: the gate snapshots the seed before either
  tree mounts and hands it to v2, without editing v1.
- meetings-v2-flag-gate.spec.ts: the flag-on test no longer passes on
  a URL that already matched. It asserts the browser aborted v1's held
  meeting lookup (requestfailed; Angular's FetchBackend aborts on
  unsubscribe, and v1's lookup feeds toSignal), then releases the 404,
  waits for it to settle, and asserts no navigation to not-found
  happened.

Resolves 4 review threads.

Refs #2873

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 09:21
@nunoeufrasio

Copy link
Copy Markdown
Contributor Author

Review feedback addressed

Commit: 1a69ce3

Changes made (all per copilot-pull-request-reviewer[bot])

  • meeting-details-gate.component.ts: "v1's lookup governs reachability" is scoped to full SSR navigations. On in-app navigation v1's lookup is aborted, so v2 must check reachability itself.
  • v2-scaffold.md, meeting-details-page.component.ts: v1 removes the meetingJoinState TransferState key on read, before v2 mounts. Recorded as an E1-01 prerequisite: the gate snapshots the seed and hands it to v2.
  • meetings-v2-flag-gate.spec.ts: the flag-on test asserts v1's held lookup was aborted (requestfailed), then releases the 404, waits for it to settle, and asserts no navigation to not-found happened.

Threads resolved

4 of 4.

Checks: format, lint, types and build pass; gate spec 7/7; e2e:check-collection OK.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

V2 drops meeting pageview analytics, and its E2E abort assertion conflicts with the gate’s actual request timing.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (4)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Preserve manual pageviews for targeted V2 meeting visits

apps/​lfx-one/​src/​app/​modules/​meetings/​meeting-details-page/​meeting-details-page.component.ts:25

Targeted V2 visits lose their Plausible pageview. PlausibleService suppresses automatic tracking for numeric /meetings/:id URLs, and the only replacement is MeetingJoinComponent.initializePublicMeetingPageviewTracking(). Switching to this empty V2 component destroys that subscription before analytics becomes ready, while this branch never calls trackPage(). Preserve the manual pageview (including project context) in the gate or V2 branch so rollout metrics do not omit the entire targeted cohort.

Comment thread apps/lfx-one/e2e/meetings-v2-flag-gate.spec.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1a69ce3. Configure here.

Comment thread apps/lfx-one/e2e/meetings-v2-flag-gate.spec.ts Outdated
Address review comments from the reviewers on PR #2910:

- meetings-v2-flag-gate.spec.ts: the flag-on test required a
  requestfailed on the pre-v2 page's meeting lookup, but that lookup
  sits behind debounceTime(0) (meeting-join.component.ts:780), so the
  hydration latch can destroy the page before the request is ever
  sent and the assertion would time out. The test now records every
  lookup the browser issues and passes when none was issued or every
  issued one was aborted, and only awaits the route handler's settled
  signal when a lookup actually reached it (per
  copilot-pull-request-reviewer[bot] and cursor[bot] Bugbot).

Resolves 2 review threads.

Refs #2873

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 09:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The gate fails safely to V1, preserves SSR hydration behavior, and has focused unit and E2E coverage.

Review effort: Balanced
Findings: None

Resolved since last review (1)

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 4, 2026 22:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The gate preserves the public SSR default, fails safely to V1, and has focused unit and E2E coverage.

Review effort: Balanced
Findings: None

main took specs/010 for specs/010-shared-ghcr-cleanup (#3202) after
this branch was cut, which made "spec 010" ambiguous. The meeting
details redesign spec moves to specs/011-meeting-details-redesign
across all of the epic's open PRs. This moves v2-scaffold.md and
updates the references to it in component-architecture.md and the
V2 page component's doc comment.

Refs #2874

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 4, 2026 22:42
@nunoeufrasio

Copy link
Copy Markdown
Contributor Author

Branch updated

Commits: e298fc4 (merge of main), 02f14a1 (spec move)

No open review threads. Checks: build, format, lint, types, 41 gate and V1 unit tests, and E2E collection all pass.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The gate preserves V1 behavior, fails closed, and has focused unit, E2E, fallback, SSR, and rollout documentation coverage.

Review effort: Balanced
Findings: None

@nunoeufrasio
nunoeufrasio changed the base branch from main to feat/meeting-details-v2 October 4, 2026 22:53
@nunoeufrasio

Copy link
Copy Markdown
Contributor Author

Base changed to feat/meeting-details-v2. The meeting details V2 epic (#1765) now lands in that integration branch instead of main. Merging this PR ships nothing to production: main only changes when the whole project is finished and one final PR, feat/meeting-details-v2 → main, merges. Please review and merge this PR into the integration branch as usual.

@nunoeufrasio
nunoeufrasio merged commit e718398 into feat/meeting-details-v2 Oct 4, 2026
18 checks passed
@nunoeufrasio
nunoeufrasio deleted the feat/issue-2873 branch October 4, 2026 23:06
nunoeufrasio added a commit that referenced this pull request Oct 5, 2026
* docs(meetings): add the v2 rollout and retirement plan

Adds specs/011-meeting-details-redesign/rollout.md for V2-03. It
separates the two controls: the integration branch decides whether the
code is in production at all (nothing reaches main until release PR
#3249), and MEETING_V2_ENABLED_FLAG decides who sees V2 once it is.

Defines eight stages (build, release, internal testers, extended
testers, signed-in percentage, anonymous, soak, retire), each with who
decides and exit criteria. States that the code default never changes
and targeting is the switch. Gives the anonymous plan: a code change
that needs #2920's server-side evaluation, then an anonymous
LaunchDarkly context ramped by percentage. Adds what is measured, a
rollback table, and flag retirement: this page drops its read of the
shared flag, and the flag itself retires only with the composer.

The V1-deletion checklist is filed as #3266. The README's contents
table now lists the files that actually exist.

Refs #2875

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): reconcile v2 spec cross-references after the merge

The foundation PRs (#2909, #2910, #2911, #2913, #2914, #2935) now sit
together on feat/meeting-details-v2, so the "pending sibling, not on
main yet" notes they carried are stale. They become plain
cross-references in spec.md, state-matrix.md, data-model.md,
requirements.md, testid-contract.md, v2-scaffold.md and the
meeting-view-model resolver comments.

Also records the newly filed Phase 1 issues (#3250-#3265) in spec.md's
work order and requirements.md's traceability table, and the V1
retirement issue #3266.

Refs #1766

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): fix rollout plan gating and rollback guidance

Make #2920 the stage 4 entry gate rather than a release blocker,
scope R05 and FR-001 to the stages before the anonymous stage, give
the V1 rollback a targeted-revert path, and stop reusing the D-2 label.

Refs #2875

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

* docs(meetings): address review feedback on the rollout plan

Make FR-001 platform-neutral with the signed-in and post-hydration
rules scoped to their stages, limit stage 2 to signed-in states, define
the baseline once a V1 cohort is gone, and re-wrap a spec paragraph.

Refs #2875

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>

---------

Signed-off-by: Nuno Eufrasio <nmeufrasio@gmail.com>
nunoeufrasio added a commit that referenced this pull request Oct 5, 2026
…the flag (#2910)

* feat(meetings): gate the meeting details page behind the v2 flag

`/meetings/:id` now loads `MeetingDetailsGateComponent`, which renders the
pre-v2 page by default and the v2 tree only for a hydrated, signed-in viewer
that `MEETING_V2_ENABLED_FLAG` resolves true for. The pre-v2 page is untouched
and stays the default for everyone else.

A shim component rather than a `canMatch` guard: LaunchDarkly is browser-only
and no flag value crosses TransferState, so SSR always evaluates this flag
false. A guard returning true on the server would SSR the v2 target and then
match v1 on the browser for ~100% of viewers, and a guard awaiting flag
readiness would hold the client render of a public, SSR-first page behind a
LaunchDarkly round trip for every visitor. Same call, and same shape, as
`HealthMetricsGateComponent`.

Anonymous viewers are held on v1 by the gate rather than by targeting, since a
tester list cannot express "not logged out". The v2 branch renders a
placeholder that #2874 replaces with the real scaffold.

Refs #2873

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): record the v2 gate in the architecture docs

Addresses the full-branch review of the #2873 gate.

- Correct the e2e comment on the flag-on case: the pre-v2 page does mount
  for the render before the hydration latch and is torn down with its
  in-flight lookup, rather than never mounting at all.
- Record in the gate's doc comment that SSR renders v1 for every viewer,
  so v1's lookup (and its not-found redirect) decides the URL for both
  branches until SSR has a flag source, and that #2874 should load the
  real v2 page through @defer rather than a static import.
- Give the v2 placeholder a templateUrl instead of an inline template.
- Point the standalone-routes map and the meetings module tree at
  MeetingDetailsGateComponent, and note in public-meeting-join.md that
  the join page is no longer the route target itself.

Refs #2873

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(meetings): assert the v2 gate branch is attached, not visible

The v2 placeholder renders an empty element, so the gate's v2 wrapper
has no box and toBeVisible() would time out even when the right branch
rendered. CI does not run the full e2e suite, so nothing caught it.
Asserting toBeAttached() proves the branch mounted, which is the claim
this test makes.

Also points the gate's doc comment at #2920, the follow-up that gives
SSR a flag source. #2873's "no flash of the wrong tree" criterion has
been amended to match the documented trade-off: a targeted viewer sees
a one-time v1 to v2 swap until #2920 lands, and targeting stays on a
named tester list until then.

Refs #2873

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* refactor(meetings): scaffold the v2 details page alongside v1

Establishes where every Phase 1 component of the meeting details
redesign lives, following the composer's -v1 precedent:

- meeting-join/ moves to meeting-join-v1/. Only the directory moves;
  all three files are byte-identical (100% renames), because the epic
  forbids editing the v1 component and renaming its class or selector
  would do exactly that.
- meeting-details-v2/ becomes meeting-details/ with a recognisable
  stub built on the existing lfx-empty-state wrapper. It reads no
  meeting data yet; the E1-01 shell adds the data flow on the same
  services and TransferState contract v1 uses.
- The gate now renders v2 inside @defer (on immediate), so v2 is its
  own lazy chunk and the v1 majority, every anonymous visitor
  included, never downloads it. The e2e gate spec asserts the scaffold
  renders, which proves the chunk loads.

specs/010-meeting-details-redesign/v2-scaffold.md records the naming
convention, where v2-only and shared components live, the rule that a
shared meetings component changes only additively, and the definition
of done for deleting v1 that V2-03 (#2875) builds on.

Stacked on #2910 (V2-01), which this depends on.

Closes #2874

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(meetings): rename the v2 page to avoid a selector collision

The scaffold took MeetingDetailsComponent / lfx-meeting-details, which
the create/edit wizard's details step in components/meeting-details/
already owns. It compiled only because no file imports both; the first
V2 component to reuse the wizard step, which the scaffold doc invites,
would have hit a duplicate selector. V2 is now meeting-details-page/
with MeetingDetailsPageComponent / lfx-meeting-details-page, and the
doc says to check a selector is free before naming a V2 component.

Also from the pre-PR review:

- The deferred v2 block gains an @error branch that renders v1, so a
  chunk that fails to load (a deploy mid-session, a dropped
  connection) no longer leaves a targeted viewer on an empty page.
- v2-scaffold.md and the page's TSDoc cite spec.md, testid-contract.md
  and meeting-view-model.utils, none of which is on main yet. Each is
  now marked with the PR that lands it.

Refs #2874

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(meetings): keep the v1 layout identical behind the details gate

On main, /meetings/:id rendered lfx-meeting-join directly. Behind the
gate it sits inside the gate's host element and a branch-marker div,
two extra boxes that could shift v1's layout for every visitor the
flag is off for, which is all of them.

The gate's host element and both wrapper divs are now
display: contents, so none of them creates a layout box and v1 lays
out exactly as it did. The divs keep their data-testids; Playwright
treats a display: contents element as visible when its children are.

This PR now also carries the V2-02 scaffold (folded in from #2923), so
the only change to the live route lands once, as one unit.

Refs #2873
Refs #2874

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* test(meetings): cover the gate's fallback when the v2 chunk fails

The @error branch of the gate's deferred v2 block renders v1 so a
targeted viewer is never left on an empty page when the lazy chunk
fails to load, but nothing exercised it: the existing tests run
deferred blocks through (DeferBlockBehavior.Playthrough, Angular's
default), which only ever reaches the success state.

The create helper now takes a defer behavior, and a new test uses
Manual to drive the block to DeferBlockState.Error and asserts v1
mounts inside the v2 wrapper. Proven binding per R07: replacing the
@error branch's lfx-meeting-join with an empty span failed exactly
this test.

Refs #2874

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(review): address PR #2910 review feedback

Address review comments from the reviewers on PR #2910 (all per
copilot-pull-request-reviewer[bot]):

- meeting-details-gate.component.ts: the "v1's lookup governs
  reachability" note now holds for a full SSR navigation only. On an
  in-app navigation the hydration latch destroys v1 and aborts its
  lookup, so v2 must check reachability itself once it has a data
  flow.
- v2-scaffold.md, meeting-details-page.component.ts: v2 cannot read
  the MeetingJoinPageState seed from TransferState, because v1 mounts
  first and removes the meetingJoinState key on read. Both now record
  the E1-01 prerequisite: the gate snapshots the seed before either
  tree mounts and hands it to v2, without editing v1.
- meetings-v2-flag-gate.spec.ts: the flag-on test no longer passes on
  a URL that already matched. It asserts the browser aborted v1's held
  meeting lookup (requestfailed; Angular's FetchBackend aborts on
  unsubscribe, and v1's lookup feeds toSignal), then releases the 404,
  waits for it to settle, and asserts no navigation to not-found
  happened.

Resolves 4 review threads.

Refs #2873

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* fix(review): address PR #2910 review feedback

Address review comments from the reviewers on PR #2910:

- meetings-v2-flag-gate.spec.ts: the flag-on test required a
  requestfailed on the pre-v2 page's meeting lookup, but that lookup
  sits behind debounceTime(0) (meeting-join.component.ts:780), so the
  hydration latch can destroy the page before the request is ever
  sent and the assertion would time out. The test now records every
  lookup the browser issues and passes when none was issued or every
  issued one was aborted, and only awaits the route handler's settled
  signal when a lookup actually reached it (per
  copilot-pull-request-reviewer[bot] and cursor[bot] Bugbot).

Resolves 2 review threads.

Refs #2873

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): move the v2 scaffold spec to specs/011

main took specs/010 for specs/010-shared-ghcr-cleanup (#3202) after
this branch was cut, which made "spec 010" ambiguous. The meeting
details redesign spec moves to specs/011-meeting-details-redesign
across all of the epic's open PRs. This moves v2-scaffold.md and
updates the references to it in component-architecture.md and the
V2 page component's doc comment.

Refs #2874

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

---------

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>
nunoeufrasio added a commit that referenced this pull request Oct 5, 2026
* docs(meetings): add the v2 rollout and retirement plan

Adds specs/011-meeting-details-redesign/rollout.md for V2-03. It
separates the two controls: the integration branch decides whether the
code is in production at all (nothing reaches main until release PR
#3249), and MEETING_V2_ENABLED_FLAG decides who sees V2 once it is.

Defines eight stages (build, release, internal testers, extended
testers, signed-in percentage, anonymous, soak, retire), each with who
decides and exit criteria. States that the code default never changes
and targeting is the switch. Gives the anonymous plan: a code change
that needs #2920's server-side evaluation, then an anonymous
LaunchDarkly context ramped by percentage. Adds what is measured, a
rollback table, and flag retirement: this page drops its read of the
shared flag, and the flag itself retires only with the composer.

The V1-deletion checklist is filed as #3266. The README's contents
table now lists the files that actually exist.

Refs #2875

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): reconcile v2 spec cross-references after the merge

The foundation PRs (#2909, #2910, #2911, #2913, #2914, #2935) now sit
together on feat/meeting-details-v2, so the "pending sibling, not on
main yet" notes they carried are stale. They become plain
cross-references in spec.md, state-matrix.md, data-model.md,
requirements.md, testid-contract.md, v2-scaffold.md and the
meeting-view-model resolver comments.

Also records the newly filed Phase 1 issues (#3250-#3265) in spec.md's
work order and requirements.md's traceability table, and the V1
retirement issue #3266.

Refs #1766

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): fix rollout plan gating and rollback guidance

Make #2920 the stage 4 entry gate rather than a release blocker,
scope R05 and FR-001 to the stages before the anonymous stage, give
the V1 rollback a targeted-revert path, and stop reusing the D-2 label.

Refs #2875

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

* docs(meetings): address review feedback on the rollout plan

Make FR-001 platform-neutral with the signed-in and post-hydration
rules scoped to their stages, limit stage 2 to signed-in states, define
the baseline once a V1 cohort is gone, and re-wrap a spec paragraph.

Refs #2875

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>

---------

Signed-off-by: Nuno Eufrásio <nmeufrasio@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(meetings): scaffold the v2 details component alongside v1 feat(meetings): gate a v2 meeting details route behind meeting-v2-enabled

2 participants