You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(meetings): gate the meeting details page and scaffold v2 behind the flag - #2910
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:
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.
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>
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.
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.
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>
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>
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
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>
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>
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.
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.
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>
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>
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.
* 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>
…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>
* 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Puts
/meetings/:idbehindMEETING_V2_ENABLED_FLAGso 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
mainfor 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 aredisplay: contents, so V1 lays out exactly as when the route rendered it directly. V1's files moved frommeeting-join/tomeeting-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@errorbranch renders V1.V1 is untouched.
meeting-join.component.tsis byte-identical, its 23-test spec is unmodified and green, and the flag defaults tofalse, so behaviour onmainis unchanged for every user until LaunchDarkly targeting says otherwise.Mechanism: a shim component, not a
canMatchguardThis is the same pattern as
health-metrics-gate.component.ts, which is already spec'd and in production. The route keeps one stableloadComponent; the component holds ahydratedsignal latched inafterNextRenderand an@ifover the two trees.Why the guard was rejected.
feature-flag.provider.tsreturns early whentypeof window === 'undefined', and no flag value crosses TransferState — so SSR always evaluates this flag asfalse. AcanMatchguard following the established pattern returnstrueon 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:
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
hydratedlatch 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
meeting-joinspec unmodified and green.yarn e2e:check-collection: 131 spec files, 0 collection errors. The flag-gate E2E spec gains a/meetings/:iddescribe asserting one branch present and the other absent.yarn lint(0 errors),yarn build,./check-headers.shall clean.Closes #2873
Closes #2874
🤖 Generated with Claude Code
Scaffold commits (from #2923)
3dc333710: scaffold. V1's directory becomesmeeting-join-v1/(directory-only move). V2 becomesmeeting-details-page/(MeetingDetailsPageComponent), loaded via@defer. Conventions and the V1-deletion definition of done are inspecs/011-meeting-details-redesign/v2-scaffold.md.037962f30: renames V2 away fromlfx-meeting-details, which the create/edit wizard already uses, and adds the@errorfallback.90a8d4733:display: contentson 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.